{
return (
<>
- {status === 'loading' ? (
+ {status === 'pending' ? (
diff --git a/app/containers/Main/TroparionFavsHome.tsx b/app/containers/Main/TroparionFavsHome.tsx
index 85d1fd0262..2f1ec48a45 100644
--- a/app/containers/Main/TroparionFavsHome.tsx
+++ b/app/containers/Main/TroparionFavsHome.tsx
@@ -4,10 +4,10 @@ import React from 'react';
import { useRecoilState } from 'recoil';
import troparionFavsState from 'state/troparionFavsState';
import ErrorMessage500 from 'components/ErrorMessage500/ErrorMessage500';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import { Link } from 'react-router-dom';
import { HymnButton } from 'containers/Hymns/HymnButton';
-import { useTheme } from 'emotion-theming';
+import { useTheme } from '@emotion/react';
import ButtonBox from 'components/ButtonBox/ButtonBox';
import PlusIcon from 'components/svgs/PlusIcon';
@@ -21,7 +21,7 @@ export const TroparionFavsHome = () => {
return (
<>
- {status === 'loading' ? (
+ {status === 'pending' ? (
diff --git a/app/containers/Profile/Profile.tsx b/app/containers/Profile/Profile.tsx
index 5d8551fc8c..1c08bfb0b5 100644
--- a/app/containers/Profile/Profile.tsx
+++ b/app/containers/Profile/Profile.tsx
@@ -1,15 +1,17 @@
import React, { useEffect, useState } from 'react';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import { useQuery } from 'convex/react';
-import { useTheme } from 'emotion-theming';
-import { useHistory } from 'react-router-dom';
+import { useTheme } from '@emotion/react';
+import { useNavigate } from 'react-router-dom';
+
+import { api } from '../../../convex/_generated/api';
+
import { useSession } from 'containers/AuthProvider';
import HeaderMain from 'containers/Main/HeaderMain';
import Button from 'components/Button/Button';
import Loader from 'components/Loader/Loader';
import BottomNav from 'components/BottomNav/BottomNav';
import { useDocumentTitle } from 'utils/useDocumentTitle';
-import type { AppTheme } from 'styles/AppTheme';
import { formatDateKey } from 'utils/formatDateKey';
import PrayerSetup from 'containers/HabitTracker/PrayerSetup';
import ContributionGraph from 'containers/HabitTracker/ContributionGraph';
@@ -17,8 +19,6 @@ import DrawerWithHeader from 'components/Drawer/DrawerWithHeader';
import PrayerCheck from 'components/svgs/PrayerCheck';
import QuestionMarkCircle from 'components/svgs/QuestionMarkCircle';
-import { api } from '../../../convex/_generated/api';
-
const GRAPH_SKELETON_COLUMNS = 53;
const GRAPH_SKELETON_ROWS = 7;
const GRAPH_SKELETON_CELL = 15;
@@ -526,10 +526,14 @@ const ProfileLoadingState = ({
{
- const theme = useTheme
();
+ const theme = useTheme();
const session = useSession();
- const history = useHistory();
+ const navigate = useNavigate();
const [signingOut, setSigningOut] = useState(false);
const [showSetup, setShowSetup] = useState(false);
const today = new Date();
@@ -628,26 +632,28 @@ const Inner = () => {
const skeletonHighlight = theme.colours?.bgGrayLight || '#f3f3f7';
const graphRollingStart = addDays(today, -364);
const graphGridStart = addDays(graphRollingStart, -getMondayBasedDay(graphRollingStart));
- const graphMonthAnchors = filterGraphMonthAnchors([
- {
- label: GRAPH_SKELETON_MONTHS[graphRollingStart.getMonth()],
- left: 0,
- },
- ...Array.from({ length: 12 }, (_, offset) => {
- const monthStartDate = new Date(
- graphRollingStart.getFullYear(),
- graphRollingStart.getMonth() + offset + 1,
- 1
- );
- return {
- label: GRAPH_SKELETON_MONTHS[monthStartDate.getMonth()],
- left:
- Math.floor((monthStartDate.getTime() - graphGridStart.getTime()) / GRAPH_SKELETON_DAY_MS / 7) *
- (GRAPH_SKELETON_CELL + GRAPH_SKELETON_GAP),
- date: monthStartDate,
- };
- }).filter((anchor) => anchor.date <= today),
- ].filter((anchor, index, anchors) => index === 0 || anchor.left !== anchors[index - 1].left));
+ const graphMonthAnchors = filterGraphMonthAnchors(
+ [
+ {
+ label: GRAPH_SKELETON_MONTHS[graphRollingStart.getMonth()],
+ left: 0,
+ },
+ ...Array.from({ length: 12 }, (_, offset) => {
+ const monthStartDate = new Date(
+ graphRollingStart.getFullYear(),
+ graphRollingStart.getMonth() + offset + 1,
+ 1
+ );
+ return {
+ label: GRAPH_SKELETON_MONTHS[monthStartDate.getMonth()],
+ left:
+ Math.floor((monthStartDate.getTime() - graphGridStart.getTime()) / GRAPH_SKELETON_DAY_MS / 7) *
+ (GRAPH_SKELETON_CELL + GRAPH_SKELETON_GAP),
+ date: monthStartDate,
+ };
+ }).filter((anchor) => anchor.date <= today),
+ ].filter((anchor, index, anchors) => index === 0 || anchor.left !== anchors[index - 1].left)
+ );
useEffect(() => {
if (!signingOut && !session.isLoading && !profile) {
@@ -737,7 +743,8 @@ const Inner = () => {
line-height: 1.2;
`}
>
- Вы можете установить ритм ежедневной домашней утренней и вечерней молитвы и отслеживать его
+ Вы можете установить ритм ежедневной домашней утренней и вечерней молитвы и
+ отслеживать его
) : null}
diff --git a/app/containers/Readings/Readings.tsx b/app/containers/Readings/Readings.tsx
index 8e6a062b43..2471665406 100644
--- a/app/containers/Readings/Readings.tsx
+++ b/app/containers/Readings/Readings.tsx
@@ -1,12 +1,12 @@
import { getFeastInfo } from 'domain/getDayInfo';
import React, { useEffect } from 'react';
-import { useParams, Link, useHistory, useLocation } from 'react-router-dom';
-import { css } from 'emotion';
+import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
+import { css } from '@emotion/css';
import Loader from 'components/Loader/Loader';
import useDay from 'hooks/useDay';
import Prayer from 'components/svgs/Prayer';
-import { useTheme } from 'emotion-theming';
+import { useTheme } from '@emotion/react';
import LayoutInner from 'components/LayoutInner/LayoutInner';
import CalendarToggle from 'components/CalendarToggle/CalendarToggle';
import { useDocumentTitle } from 'utils/useDocumentTitle';
@@ -15,12 +15,14 @@ import ServiceSelector from './ServiceSelector';
import ReadingsForService from './ReadingsForService';
const Readings = ({ brother = false }) => {
- const { service, date } = useParams();
- const history = useHistory();
+ const { service = '', date = '' } = useParams<'date' | 'service'>();
+ const location = useLocation();
+ const navigate = useNavigate();
const { data: day } = useDay(date);
const readings = brother ? day?.bReadings : day?.readings;
const readingsForService = readings?.[service];
const services = Object.keys(readings || {});
+ const firstService = services[0];
const theme = useTheme();
useDocumentTitle(`${date} - Чтения - Православное богослужение на русском языке`);
@@ -28,48 +30,35 @@ const Readings = ({ brother = false }) => {
useEffect(() => {
// Redirect to first available service, if current one doesn't exist
if (readings && !readingsForService) {
- history.replace({ pathname: `/date/${date}/readings/${services[0]}`, state: history.location.state });
+ void navigate(`/date/${date}/readings/${firstService}`, {
+ replace: true,
+ state: location.state,
+ });
}
- });
+ }, [date, firstService, location.state, navigate, readings, readingsForService]);
const setNewDate = (dateString) => {
- history.push({
- pathname: `/date/${dateString}/readings/${service}`,
+ void navigate(`/date/${dateString}/readings/${service}`, {
state: {
- backLink: history.location.state?.backLink,
+ backLink: location.state?.backLink,
},
});
};
const { lpod } = getFeastInfo(new Date(date));
- let to = null;
+ let servicePath: string | null = null;
if (brother && service === 'Утром') {
- to = {
- pathname: `/date/${date}/service/matins`,
- state: { backLink: history.location.pathname },
- };
+ servicePath = `/date/${date}/service/matins`;
} else if (brother && service === 'Вечером') {
- to = {
- pathname: `/date/${date}/service/vespers`,
- state: { backLink: history.location.pathname },
- };
+ servicePath = `/date/${date}/service/vespers`;
} else if (service === 'Литургия') {
- to = {
- pathname: `/date/${date}/service/Литургия`,
- state: { backLink: history.location.pathname },
- };
+ servicePath = `/date/${date}/service/Литургия`;
} else if (service === 'Вечерня' && lpod) {
- to = {
- pathname: `/date/${date}/service/Вечерня`,
- state: { backLink: history.location.pathname },
- };
+ servicePath = `/date/${date}/service/Вечерня`;
} else if (service === '6-й час') {
- to = {
- pathname: `/date/${date}/service/sixthHour`,
- state: { backLink: history.location.pathname },
- };
+ servicePath = `/date/${date}/service/sixthHour`;
}
const left = (
@@ -85,18 +74,19 @@ const Readings = ({ brother = false }) => {
{...{
service,
services,
- onChange: (value) =>
- history.push({
- pathname: `/date/${date}/${brother ? 'bReadings' : 'readings'}/${value}`,
+ onChange: (value) => {
+ void navigate(`/date/${date}/${brother ? 'bReadings' : 'readings'}/${value}`, {
state: {
- backLink: history.location.state?.backLink,
+ backLink: location.state?.backLink,
},
- }),
+ });
+ },
}}
/>
- {to && (
+ {servicePath && (
{
return (
- {status === 'loading' ? (
+ {status === 'pending' ? (
) : status === 'error' ? (
diff --git a/app/containers/Sermon/Sermon.tsx b/app/containers/Sermon/Sermon.tsx
index c3ea179221..f043697baf 100644
--- a/app/containers/Sermon/Sermon.tsx
+++ b/app/containers/Sermon/Sermon.tsx
@@ -1,13 +1,13 @@
import React from 'react';
import { useParams } from 'react-router-dom';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import Loader from 'components/Loader/Loader';
import useExternalDay from 'hooks/useExternalDay';
import Zoom from 'components/Zoom/Zoom';
import ErrorMessage404 from 'components/ErrorMessage404/ErrorMessage404';
import ErrorMessage500 from 'components/ErrorMessage500/ErrorMessage500';
import RteText from 'components/RteText/RteText';
-import { useTheme } from 'emotion-theming';
+import { useTheme } from '@emotion/react';
import LayoutInner from 'components/LayoutInner/LayoutInner';
import { useDocumentTitle } from 'utils/useDocumentTitle';
@@ -23,7 +23,7 @@ const Sermon = () => {
return (
- {externalDayStatus === 'loading' ? (
+ {externalDayStatus === 'pending' ? (
) : externalDayStatus === 'error' ? (
diff --git a/app/containers/SermonDetail/SermonDetail.tsx b/app/containers/SermonDetail/SermonDetail.tsx
index e667b36d7d..0a4ad04764 100644
--- a/app/containers/SermonDetail/SermonDetail.tsx
+++ b/app/containers/SermonDetail/SermonDetail.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { useParams } from 'react-router-dom';
-import { css } from 'emotion';
-import { useTheme } from 'emotion-theming';
+import { css } from '@emotion/css';
+import { useTheme } from '@emotion/react';
import LayoutInner from 'components/LayoutInner/LayoutInner';
import Loader from 'components/Loader/Loader';
import ErrorMessage404 from 'components/ErrorMessage404/ErrorMessage404';
@@ -22,7 +22,7 @@ const SermonDetail = () => {
return (
- {status === 'loading' ? (
+ {status === 'pending' ? (
) : status === 'error' ? (
diff --git a/app/containers/SermonList/SermonList.tsx b/app/containers/SermonList/SermonList.tsx
index c82ec4521f..6f3a45bebb 100644
--- a/app/containers/SermonList/SermonList.tsx
+++ b/app/containers/SermonList/SermonList.tsx
@@ -1,8 +1,8 @@
import React, { useState } from 'react';
-import { css } from 'emotion';
-import { useTheme } from 'emotion-theming';
+import { css } from '@emotion/css';
+import { useTheme } from '@emotion/react';
import LayoutInner from 'components/LayoutInner/LayoutInner';
-import { useHistory, useParams } from 'react-router-dom';
+import { useNavigate, useParams } from 'react-router-dom';
import { useDocumentTitle } from 'utils/useDocumentTitle';
import useSermonFacets from 'hooks/useSermonFacets';
import SelectBox from 'components/SelectBox/SelectBox';
@@ -12,15 +12,15 @@ import BottomNav from 'components/BottomNav/BottomNav';
const SermonListContainer = () => {
const theme = useTheme();
- const history = useHistory();
+ const navigate = useNavigate();
const { authorId } = useParams();
const [themeId, setThemeId] = useState();
const setAuthorId = (authorId: string) => {
if (authorId) {
- history.push(`/sermons/${authorId}`);
+ void navigate(`/sermons/${authorId}`);
} else {
- history.push('/sermons');
+ void navigate('/sermons');
}
};
diff --git a/app/containers/Service/LanguageSwitcher.tsx b/app/containers/Service/LanguageSwitcher.tsx
index fc36372d34..866ccb6fbf 100644
--- a/app/containers/Service/LanguageSwitcher.tsx
+++ b/app/containers/Service/LanguageSwitcher.tsx
@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import { useRecoilState } from 'recoil';
import langState from 'state/langState';
diff --git a/app/containers/Service/ParallelLanguageBar.tsx b/app/containers/Service/ParallelLanguageBar.tsx
index 1439e83a0a..b3d6701124 100644
--- a/app/containers/Service/ParallelLanguageBar.tsx
+++ b/app/containers/Service/ParallelLanguageBar.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import Button from 'components/Button/Button';
import Cross from 'components/svgs/Cross';
import { useRecoilState } from 'recoil';
diff --git a/app/containers/Service/Service.tsx b/app/containers/Service/Service.tsx
index fd5fd9f31a..a4af414e38 100644
--- a/app/containers/Service/Service.tsx
+++ b/app/containers/Service/Service.tsx
@@ -2,8 +2,8 @@ import { getFeastInfo } from 'domain/getDayInfo';
import * as Sentry from '@sentry/react';
import React, { Suspense, useState, useEffect, useContext } from 'react';
-import { useParams, useHistory, Redirect } from 'react-router-dom';
-import { css } from 'emotion';
+import { Navigate, useLocation, useNavigate, useParams } from 'react-router-dom';
+import { css } from '@emotion/css';
import useDay from 'hooks/useDay';
import Zoom from 'components/Zoom/Zoom';
import Loader from 'components/Loader/Loader';
@@ -40,10 +40,11 @@ const reloadOnFailedImport = (e) => {
const toUpperCase = (name) => name.charAt(0).toUpperCase() + name.slice(1);
const Service = () => {
- const { serviceId: originalServiceId, date, prayerId } = useParams();
+ const { serviceId: originalServiceId = '', date = '', prayerId } = useParams<'date' | 'prayerId' | 'serviceId'>();
const { data: day } = useDay(date);
- const history = useHistory();
+ const location = useLocation();
+ const navigate = useNavigate();
const langState = useContext(LangContext);
@@ -112,19 +113,18 @@ const Service = () => {
if (!serviceId) {
if (day?.readings) {
if (day?.readings?.['Вечерня'] && lpod) {
- return ;
+ return ;
}
if (day?.readings?.['Литургия']) {
- return ;
+ return ;
}
- return ;
+ return ;
}
}
const setNewDate = (dateString) => {
- history.push({
- pathname: `/date/${dateString}/service/${originalServiceId}`,
- state: { backLink: history.location.state?.backLink },
+ void navigate(`/date/${dateString}/service/${originalServiceId}`, {
+ state: { backLink: location.state?.backLink },
});
};
diff --git a/app/containers/Service/TOCSwitcher.tsx b/app/containers/Service/TOCSwitcher.tsx
index 192e45566d..de19f179b6 100644
--- a/app/containers/Service/TOCSwitcher.tsx
+++ b/app/containers/Service/TOCSwitcher.tsx
@@ -1,62 +1,72 @@
import React, { useEffect, useState } from 'react';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import { useRecoilValue } from 'recoil';
-import TOCState from 'state/TOCState';
import SelectBox from '../../components/SelectBox/SelectBox';
-const TOCSwitcher = ({ lang }) => {
+import TOCState from 'state/TOCState';
+
+interface TOCSwitcherProps {
+ lang: string;
+ service?: unknown;
+}
+
+const TOCSwitcher = ({ lang }: TOCSwitcherProps): JSX.Element => {
const TOC = useRecoilValue(TOCState);
const [activeItem, setActiveItem] = useState('');
- if ('IntersectionObserver' in window) {
- useEffect(() => {
- let observer = null;
- if (TOC.length) {
- observer = new IntersectionObserver(
- (entries) => {
- entries.forEach((entry) => {
- if (entry.isIntersecting && entry.target.id) {
- if (observer) {
- setActiveItem(entry.target.id);
- }
- }
- });
- },
- {
- rootMargin: '-50px 0px -250px 0px',
- threshold: 0.3,
- }
- );
- // @TODO: any better way to do it without timeout?
- // We keep on polling for dom nodes, till all nodes are found
- const observeOrCue = (nodeId) => {
- if (!observer) {
- return;
- }
- const node = document.getElementById(nodeId);
- if (node) {
- observer.observe(node);
- } else {
- setTimeout(() => {
- observeOrCue(nodeId);
- }, 500);
- }
- };
+ useEffect(() => {
+ if (typeof window.IntersectionObserver !== 'function' || !TOC.length) {
+ return undefined;
+ }
- TOC.map((nodeId) => {
- observeOrCue(nodeId.value);
+ let disposed = false;
+ const pendingTimeouts = new Set();
+ const observer = new IntersectionObserver(
+ (entries) => {
+ if (disposed) {
+ return;
+ }
+ entries.forEach((entry) => {
+ if (entry.isIntersecting && entry.target.id) {
+ setActiveItem(entry.target.id);
+ }
});
- setTimeout(() => {}, 0);
+ },
+ {
+ rootMargin: '-50px 0px -250px 0px',
+ threshold: 0.3,
}
- return () => {
- if (observer) {
- observer.disconnect();
- observer = null;
- }
- };
- }, [lang, TOC]);
- }
+ );
+
+ // The MDX content can mount after the TOC data, so retry until each heading exists.
+ const observeOrCue = (nodeId: string) => {
+ if (disposed) {
+ return;
+ }
+ const node = document.getElementById(nodeId);
+ if (node) {
+ observer.observe(node);
+ return;
+ }
+
+ const timeoutId = window.setTimeout(() => {
+ pendingTimeouts.delete(timeoutId);
+ observeOrCue(nodeId);
+ }, 500);
+ pendingTimeouts.add(timeoutId);
+ };
+
+ TOC.forEach(({ value }) => observeOrCue(value));
+
+ return () => {
+ disposed = true;
+ pendingTimeouts.forEach((timeoutId) => window.clearTimeout(timeoutId));
+ pendingTimeouts.clear();
+ observer.disconnect();
+ };
+ }, [lang, TOC]);
+
return (
-
diff --git a/app/containers/Service/Texts/Liturgies/Katekhumen/Katekhumen.tsx b/app/containers/Service/Texts/Liturgies/Katekhumen/Katekhumen.tsx
index c5afdb9c8c..50a05d0155 100644
--- a/app/containers/Service/Texts/Liturgies/Katekhumen/Katekhumen.tsx
+++ b/app/containers/Service/Texts/Liturgies/Katekhumen/Katekhumen.tsx
@@ -2,7 +2,7 @@ import isGospel from 'domain/isGospel';
import { makeIsEasterOffsetRange, getFeastInfo, makeIsDate } from 'domain/getDayInfo';
import React from 'react';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import SolidSection from 'components/SolidSection/SolidSection';
import forEach from 'lodash.foreach';
import useExternalDay from 'hooks/useExternalDay';
diff --git a/app/containers/Service/Texts/Liturgies/Vasiliy.mdx b/app/containers/Service/Texts/Liturgies/Vasiliy.mdx
index fcda6d2f8d..3b1e2fc82e 100644
--- a/app/containers/Service/Texts/Liturgies/Vasiliy.mdx
+++ b/app/containers/Service/Texts/Liturgies/Vasiliy.mdx
@@ -1,5 +1,5 @@
import { Link } from 'react-router-dom';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import MdxLoader from 'containers/Service/Texts/MdxLoader';
import Tooltip from 'components/Tooltip/Tooltip';
diff --git a/app/containers/Service/Texts/Liturgies/Vasiliy.tsx b/app/containers/Service/Texts/Liturgies/Vasiliy.tsx
index 4032e47f4a..2258bddd18 100644
--- a/app/containers/Service/Texts/Liturgies/Vasiliy.tsx
+++ b/app/containers/Service/Texts/Liturgies/Vasiliy.tsx
@@ -5,7 +5,7 @@ import useDay from 'hooks/useDay';
import Loader from 'components/Loader/Loader';
import ReadingsForService from 'containers/Readings/ReadingsForService';
import dateFormat from 'dateformat';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import VariableSection from '../VariableSection';
diff --git a/app/containers/Service/Texts/Liturgies/VespersWithVasilyChristmas.mdx b/app/containers/Service/Texts/Liturgies/VespersWithVasilyChristmas.mdx
index 6bf351dcfb..38f99558d9 100644
--- a/app/containers/Service/Texts/Liturgies/VespersWithVasilyChristmas.mdx
+++ b/app/containers/Service/Texts/Liturgies/VespersWithVasilyChristmas.mdx
@@ -2,7 +2,7 @@ import MdxLoader from 'containers/Service/Texts/MdxLoader';
import Tooltip from 'components/Tooltip/Tooltip';
import Parts from 'components/Parts/Parts';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import ReadingsForService from 'containers/Readings/ReadingsForService';
import Aliluja from 'containers/Service/Texts/Liturgies/Katekhumen/Aliluja/Aliluja';
import VariableSection from 'containers/Service/Texts/VariableSection';
diff --git a/app/containers/Service/Texts/Liturgies/VespersWithVasilyEpiphany.mdx b/app/containers/Service/Texts/Liturgies/VespersWithVasilyEpiphany.mdx
index 864c806f09..7ac05968aa 100644
--- a/app/containers/Service/Texts/Liturgies/VespersWithVasilyEpiphany.mdx
+++ b/app/containers/Service/Texts/Liturgies/VespersWithVasilyEpiphany.mdx
@@ -2,7 +2,7 @@ import MdxLoader from 'containers/Service/Texts/MdxLoader';
import Tooltip from 'components/Tooltip/Tooltip';
import Parts from 'components/Parts/Parts';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import ReadingsForService from 'containers/Readings/ReadingsForService';
import Aliluja from 'containers/Service/Texts/Liturgies/Katekhumen/Aliluja/Aliluja';
import VariableSection from 'containers/Service/Texts/VariableSection';
diff --git a/app/containers/Service/Texts/Liturgies/VespersWithVasilyPassionSaturday.mdx b/app/containers/Service/Texts/Liturgies/VespersWithVasilyPassionSaturday.mdx
index 0a1a1ad930..f58c9d9901 100644
--- a/app/containers/Service/Texts/Liturgies/VespersWithVasilyPassionSaturday.mdx
+++ b/app/containers/Service/Texts/Liturgies/VespersWithVasilyPassionSaturday.mdx
@@ -2,7 +2,7 @@ import MdxLoader from 'containers/Service/Texts/MdxLoader';
import Tooltip from 'components/Tooltip/Tooltip';
import Parts from 'components/Parts/Parts';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import ReadingsForService from 'containers/Readings/ReadingsForService';
import Html from 'components/Html/Html';
diff --git a/app/containers/Service/Texts/Liturgies/VespersWithVasilyPassionThursday.mdx b/app/containers/Service/Texts/Liturgies/VespersWithVasilyPassionThursday.mdx
index 3a4b49daa7..0093d8c879 100644
--- a/app/containers/Service/Texts/Liturgies/VespersWithVasilyPassionThursday.mdx
+++ b/app/containers/Service/Texts/Liturgies/VespersWithVasilyPassionThursday.mdx
@@ -2,7 +2,7 @@ import MdxLoader from 'containers/Service/Texts/MdxLoader';
import Tooltip from 'components/Tooltip/Tooltip';
import Parts from 'components/Parts/Parts';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import ReadingsForService from 'containers/Readings/ReadingsForService';
import Html from 'components/Html/Html';
diff --git a/app/containers/Service/Texts/Liturgies/VespersWithZlatoust.mdx b/app/containers/Service/Texts/Liturgies/VespersWithZlatoust.mdx
index e3a195bb46..eb04a69037 100644
--- a/app/containers/Service/Texts/Liturgies/VespersWithZlatoust.mdx
+++ b/app/containers/Service/Texts/Liturgies/VespersWithZlatoust.mdx
@@ -2,7 +2,7 @@ import MdxLoader from 'containers/Service/Texts/MdxLoader';
import Tooltip from 'components/Tooltip/Tooltip';
import Parts from 'components/Parts/Parts';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import ReadingsForService from 'containers/Readings/ReadingsForService';
import Html from 'components/Html/Html';
diff --git a/app/containers/Service/Texts/Liturgies/Zlatoust.mdx b/app/containers/Service/Texts/Liturgies/Zlatoust.mdx
index 5e0fcd129e..0c3142bd90 100644
--- a/app/containers/Service/Texts/Liturgies/Zlatoust.mdx
+++ b/app/containers/Service/Texts/Liturgies/Zlatoust.mdx
@@ -1,5 +1,5 @@
import { Link } from 'react-router-dom';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import MdxLoader from 'containers/Service/Texts/MdxLoader';
import Tooltip from 'components/Tooltip/Tooltip';
diff --git a/app/containers/Service/Texts/Liturgies/Zlatoust.tsx b/app/containers/Service/Texts/Liturgies/Zlatoust.tsx
index aa8ed4e477..0863ff69b0 100644
--- a/app/containers/Service/Texts/Liturgies/Zlatoust.tsx
+++ b/app/containers/Service/Texts/Liturgies/Zlatoust.tsx
@@ -5,7 +5,7 @@ import useDay from 'hooks/useDay';
import dateFormat from 'dateformat';
import forEach from 'lodash.foreach';
import ReadingItem from 'containers/Readings/ReadingItem';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import ZlatoustMdx from './Zlatoust.mdx';
import VespersWithZlatoustMdx from './VespersWithZlatoust.mdx';
diff --git a/app/containers/Service/Texts/Lpod/index.dyn.tsx b/app/containers/Service/Texts/Lpod/index.dyn.tsx
index 5a94f93524..ef372aa94d 100644
--- a/app/containers/Service/Texts/Lpod/index.dyn.tsx
+++ b/app/containers/Service/Texts/Lpod/index.dyn.tsx
@@ -4,7 +4,7 @@ import dateFormat from 'dateformat';
import forEach from 'lodash.foreach';
import useDay from 'hooks/useDay';
import ReadingItem from 'containers/Readings/ReadingItem';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import SolidSection from 'components/SolidSection/SolidSection';
import SectionHeading from 'containers/Main/SectionHeading';
import Saints from 'containers/Main/Saints';
diff --git a/app/containers/Service/Texts/Matins/index.dyn.tsx b/app/containers/Service/Texts/Matins/index.dyn.tsx
index fbdf9e4bd0..241fc1eb7f 100644
--- a/app/containers/Service/Texts/Matins/index.dyn.tsx
+++ b/app/containers/Service/Texts/Matins/index.dyn.tsx
@@ -4,7 +4,7 @@ import React from 'react';
import useDay from 'hooks/useDay';
import ReadingsForService from 'containers/Readings/ReadingsForService';
import Loader from 'components/Loader/Loader';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import SolidSection from 'components/SolidSection/SolidSection';
import SectionHeading from 'containers/Main/SectionHeading';
import Saints from 'containers/Main/Saints';
diff --git a/app/containers/Service/Texts/MdxLoader.tsx b/app/containers/Service/Texts/MdxLoader.tsx
index a5b286859c..2b4bc1ab9d 100644
--- a/app/containers/Service/Texts/MdxLoader.tsx
+++ b/app/containers/Service/Texts/MdxLoader.tsx
@@ -1,9 +1,9 @@
import React, { createContext, useContext } from 'react';
import * as Sentry from '@sentry/react';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import Button from 'components/Button/Button';
import SolidSection from 'components/SolidSection/SolidSection';
-import { useTheme } from 'emotion-theming';
+import { useTheme } from '@emotion/react';
import { useRecoilState, useRecoilValue } from 'recoil';
import scriptEditorIsActiveState from 'state/scriptEditorIsActiveState';
import disabledPrayersState from 'state/disabledPrayersState';
diff --git a/app/containers/Service/Texts/MolitvaPamyati/MolitvaPamyati.mdx b/app/containers/Service/Texts/MolitvaPamyati/MolitvaPamyati.mdx
index 8b79d0e5a6..c7f8de9ccc 100644
--- a/app/containers/Service/Texts/MolitvaPamyati/MolitvaPamyati.mdx
+++ b/app/containers/Service/Texts/MolitvaPamyati/MolitvaPamyati.mdx
@@ -1,7 +1,7 @@
import MdxLoader from 'containers/Service/Texts/MdxLoader';
import Oct30 from 'containers/Main/Oct30';
import { Note } from 'components/Note/Note';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import TagManager from 'react-gtm-module';
import Tooltip from 'components/Tooltip/Tooltip';
diff --git a/app/containers/Service/Texts/SixthHour/index.dyn.tsx b/app/containers/Service/Texts/SixthHour/index.dyn.tsx
index 4337a1b7b9..d7342a88b6 100644
--- a/app/containers/Service/Texts/SixthHour/index.dyn.tsx
+++ b/app/containers/Service/Texts/SixthHour/index.dyn.tsx
@@ -1,7 +1,7 @@
import { makeIsEasterOffsetRange } from 'domain/getDayInfo';
import React from 'react';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import useDay from 'hooks/useDay';
import Loader from 'components/Loader/Loader';
import ReadingsForService from 'containers/Readings/ReadingsForService';
diff --git a/app/containers/Service/Texts/VariableSection.tsx b/app/containers/Service/Texts/VariableSection.tsx
index 979bb75da5..65c3980ea7 100644
--- a/app/containers/Service/Texts/VariableSection.tsx
+++ b/app/containers/Service/Texts/VariableSection.tsx
@@ -1,7 +1,7 @@
import React from 'react';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import Tooltip from 'components/Tooltip/Tooltip';
-import { useTheme } from 'emotion-theming';
+import { useTheme } from '@emotion/react';
const VariableSection = ({ children, date }) => {
const theme = useTheme();
diff --git a/app/containers/Service/Texts/Vespers/index.dyn.tsx b/app/containers/Service/Texts/Vespers/index.dyn.tsx
index 7d3b73fca8..a324e42d92 100644
--- a/app/containers/Service/Texts/Vespers/index.dyn.tsx
+++ b/app/containers/Service/Texts/Vespers/index.dyn.tsx
@@ -5,7 +5,7 @@ import React from 'react';
import useDay from 'hooks/useDay';
import ReadingsForService from 'containers/Readings/ReadingsForService';
import Loader from 'components/Loader/Loader';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import SolidSection from 'components/SolidSection/SolidSection';
import SectionHeading from 'containers/Main/SectionHeading';
import Saints from 'containers/Main/Saints';
diff --git a/app/containers/Service/Texts/mdxContracts.test.mjs b/app/containers/Service/Texts/mdxContracts.test.mjs
new file mode 100644
index 0000000000..8e4aa65f52
--- /dev/null
+++ b/app/containers/Service/Texts/mdxContracts.test.mjs
@@ -0,0 +1,226 @@
+import assert from 'node:assert/strict';
+import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
+import path from 'node:path';
+import test from 'node:test';
+import { fileURLToPath } from 'node:url';
+
+const textsRoot = path.dirname(fileURLToPath(import.meta.url));
+
+const walkFiles = (directory) =>
+ readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
+ const entryPath = path.join(directory, entry.name);
+ return entry.isDirectory() ? walkFiles(entryPath) : [entryPath];
+ });
+
+const sourceFiles = walkFiles(textsRoot).filter((file) => /\.(?:mdx|tsx?)$/.test(file));
+
+const sourceTextByFile = new Map(sourceFiles.map((file) => [file, readFileSync(file, 'utf8')]));
+
+const requireMdxSource = (source, languages, context) => {
+ assert.equal(path.isAbsolute(source), false, `${context}: MDX source must be relative`);
+ assert.equal(source.includes('..'), false, `${context}: MDX source must stay inside Texts`);
+
+ const sourceDirectory = path.join(textsRoot, source);
+ assert.equal(
+ existsSync(sourceDirectory) && statSync(sourceDirectory).isDirectory(),
+ true,
+ `${context}: missing MDX source directory ${source}`
+ );
+
+ for (const language of languages) {
+ assert.equal(
+ existsSync(path.join(sourceDirectory, `${language}.mdx`)),
+ true,
+ `${context}: missing ${source}/${language}.mdx`
+ );
+ }
+};
+
+test('every literal MdxLoader source resolves to its default Russian MDX module', () => {
+ let referenceCount = 0;
+
+ for (const [file, sourceText] of sourceTextByFile) {
+ const relativeFile = path.relative(textsRoot, file);
+ const literalPatterns = [
+ /= 1500, true, `expected the full MDX graph, found only ${referenceCount} references`);
+});
+
+test('the dynamically loadable language corpus preserves complete Russian files and paired Church Slavonic files', () => {
+ const languageFiles = sourceFiles.filter((file) => /(?:^|\/)(?:ru|csj)\.mdx$/.test(file));
+ const russianFiles = languageFiles.filter((file) => path.basename(file) === 'ru.mdx');
+ const churchSlavonicFiles = languageFiles.filter((file) => path.basename(file) === 'csj.mdx');
+
+ assert.equal(russianFiles.length, 915);
+ assert.equal(churchSlavonicFiles.length, 836);
+
+ for (const churchSlavonicFile of churchSlavonicFiles) {
+ assert.equal(
+ existsSync(path.join(path.dirname(churchSlavonicFile), 'ru.mdx')),
+ true,
+ `${path.relative(textsRoot, churchSlavonicFile)} has no Russian fallback`
+ );
+ }
+});
+
+test('all finite dynamic MdxLoader templates resolve for every supported value and language', () => {
+ const ranges = {
+ days: Array.from({ length: 7 }, (_, index) => index),
+ weekdays: Array.from({ length: 6 }, (_, index) => index + 1),
+ tones: Array.from({ length: 8 }, (_, index) => index + 1),
+ matinsGospels: Array.from({ length: 11 }, (_, index) => index + 1),
+ };
+ const bratPrayerIds = [
+ 'PravdaMir',
+ 'BratEdinstvo',
+ 'ChurchCountry',
+ 'MolitvaRodNarod',
+ 'Neplyuev',
+ 'Opotsky1',
+ 'Opotsky2',
+ 'Opotsky3',
+ ];
+ const dynamicSources = new Map([
+ ['BratMolitvoslov/${prayerId}', bratPrayerIds.map((id) => [`BratMolitvoslov/${id}`, ['ru']])],
+ [
+ 'Liturgies/Katekhumen/Aliluja/Sunday/Glas${glas}',
+ ranges.tones.map((tone) => [`Liturgies/Katekhumen/Aliluja/Sunday/Glas${tone}`, ['ru', 'csj']]),
+ ],
+ [
+ 'Liturgies/Katekhumen/Aliluja/WeekDays/${dayOfWeek}',
+ ranges.weekdays.map((day) => [`Liturgies/Katekhumen/Aliluja/WeekDays/${day}`, ['ru', 'csj']]),
+ ],
+ [
+ 'Liturgies/Katekhumen/Prokimens/Sunday/Glas${glas}',
+ ranges.tones.map((tone) => [`Liturgies/Katekhumen/Prokimens/Sunday/Glas${tone}`, ['ru', 'csj']]),
+ ],
+ [
+ 'Liturgies/Katekhumen/Prokimens/WeekDays/${dayOfWeek}',
+ ranges.weekdays.map((day) => [`Liturgies/Katekhumen/Prokimens/WeekDays/${day}`, ['ru', 'csj']]),
+ ],
+ [
+ 'Liturgies/Vernie/Prichasten/${dayOfWeek}',
+ ranges.days.map((day) => [`Liturgies/Vernie/Prichasten/${day}`, ['ru', 'csj']]),
+ ],
+ ['Matins/Exapostilari/${matinsKey}', ranges.matinsGospels.map((key) => [`Matins/Exapostilari/${key}`, ['ru']])],
+ [
+ 'Matins/Prokimens/Glas${glas}',
+ ranges.tones.map((tone) => [`Matins/Prokimens/Glas${tone}`, ['ru', 'csj']]),
+ ],
+ [
+ 'Matins/Stepenny/Glas${glas}',
+ ranges.tones.map((tone) => [`Matins/Stepenny/Glas${tone}`, ['ru', 'csj']]),
+ ],
+ [
+ 'Matins/StihiriNaHvalite/Glas${glas}',
+ ranges.tones.map((tone) => [`Matins/StihiriNaHvalite/Glas${tone}`, ['ru', 'csj']]),
+ ],
+ [
+ 'Matins/StihiriNaHvalite/Gospel/${day?.matinsGospelKey}',
+ ranges.matinsGospels.map((key) => [`Matins/StihiriNaHvalite/Gospel/${key}`, ['ru', 'csj']]),
+ ],
+ [
+ 'Shared/IpakoyiVoskr/Glas${glas}',
+ ranges.tones.map((tone) => [`Shared/IpakoyiVoskr/Glas${tone}`, ['ru', 'csj']]),
+ ],
+ [
+ 'Shared/Kondacs/Day${dayOfWeek}',
+ ranges.weekdays.map((day) => [`Shared/Kondacs/Day${day}`, ['ru', 'csj']]),
+ ],
+ [
+ 'Shared/Kondacs/Glas${glas}',
+ ranges.tones.map((tone) => [`Shared/Kondacs/Glas${tone}`, ['ru', 'csj']]),
+ ],
+ [
+ 'Shared/Troparions/Day${dayOfWeek}',
+ ranges.weekdays.map((day) => [`Shared/Troparions/Day${day}`, ['ru', 'csj']]),
+ ],
+ [
+ 'Shared/Troparions/Glas${glas}',
+ ranges.tones.map((tone) => [`Shared/Troparions/Glas${tone}`, ['ru', 'csj']]),
+ ],
+ [
+ 'Vespers/Prokimens/Prokimen${dayOfWeek}',
+ ranges.days.map((day) => [`Vespers/Prokimens/Prokimen${day}`, ['ru', 'csj']]),
+ ],
+ [
+ 'Vespers/StihiriNaGV/Glas${glas}',
+ ranges.tones.map((tone) => [`Vespers/StihiriNaGV/Glas${tone}`, ['ru', 'csj']]),
+ ],
+ [
+ 'Vespers/StihiriNaStihah/Glas${glas}',
+ ranges.tones.map((tone) => [`Vespers/StihiriNaStihah/Glas${tone}`, ['ru', 'csj']]),
+ ],
+ ]);
+
+ const templatesInUse = new Set();
+ for (const sourceText of sourceTextByFile.values()) {
+ for (const match of sourceText.matchAll(/ {
+ let importCount = 0;
+
+ for (const [file, sourceText] of sourceTextByFile) {
+ for (const match of sourceText.matchAll(/\bfrom\s+['"]([^'"]+\.mdx)['"]/g)) {
+ importCount += 1;
+ const specifier = match[1];
+ const resolved = specifier.startsWith('containers/Service/Texts/')
+ ? path.join(textsRoot, specifier.slice('containers/Service/Texts/'.length))
+ : path.resolve(path.dirname(file), specifier);
+
+ assert.equal(
+ resolved.startsWith(`${textsRoot}${path.sep}`),
+ true,
+ `${path.relative(textsRoot, file)}: MDX import escapes Texts: ${specifier}`
+ );
+ assert.equal(
+ existsSync(resolved) && statSync(resolved).isFile(),
+ true,
+ `${path.relative(textsRoot, file)}: unresolved MDX import ${specifier}`
+ );
+ }
+ }
+
+ assert.equal(importCount >= 65, true, `expected the full direct-import graph, found only ${importCount} imports`);
+});
+
+test('every statically declared service has the top-level module required by Service', () => {
+ const servicesSource = readFileSync(path.join(textsRoot, 'Texts.tsx'), 'utf8');
+ const staticServiceIds = [...servicesSource.matchAll(/\bid:\s*'([^']+)'/g)].map((match) => match[1]);
+ const dynamicServiceIds = ['bratMolitvoslov', 'customPrayer'];
+ const serviceIds = [...staticServiceIds, ...dynamicServiceIds];
+
+ assert.equal(new Set(serviceIds).size, serviceIds.length, 'service IDs must be unique');
+
+ for (const serviceId of serviceIds) {
+ const directory = `${serviceId.charAt(0).toUpperCase()}${serviceId.slice(1)}`;
+ assert.equal(
+ existsSync(path.join(textsRoot, directory, 'index.dyn.tsx')),
+ true,
+ `${serviceId} is missing ${directory}/index.dyn.tsx`
+ );
+ }
+});
diff --git a/app/containers/ThisDay/ThisDay.tsx b/app/containers/ThisDay/ThisDay.tsx
index 2155aa41bb..ab93b3bdeb 100644
--- a/app/containers/ThisDay/ThisDay.tsx
+++ b/app/containers/ThisDay/ThisDay.tsx
@@ -1,18 +1,18 @@
import React from 'react';
import { useParams } from 'react-router-dom';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import Loader from 'components/Loader/Loader';
import Zoom from 'components/Zoom/Zoom';
import useExternalDay from 'hooks/useExternalDay';
import ErrorMessage500 from 'components/ErrorMessage500/ErrorMessage500';
import ErrorMessage404 from 'components/ErrorMessage404/ErrorMessage404';
import RteText from 'components/RteText/RteText';
-import { useTheme } from 'emotion-theming';
+import { useTheme } from '@emotion/react';
import LayoutInner from 'components/LayoutInner/LayoutInner';
import { useDocumentTitle } from 'utils/useDocumentTitle';
const InnerContent = ({ theme, thisDay, externalDayStatus }) => {
- if (externalDayStatus === 'loading') {
+ if (externalDayStatus === 'pending') {
return ;
}
diff --git a/app/containers/Updates/Updates.tsx b/app/containers/Updates/Updates.tsx
index b527e4a3f8..7932f412e6 100644
--- a/app/containers/Updates/Updates.tsx
+++ b/app/containers/Updates/Updates.tsx
@@ -1,8 +1,11 @@
import React, { useEffect, useRef } from 'react';
-import { css } from 'emotion';
+import { css } from '@emotion/css';
import { useMutation, usePaginatedQuery, useQuery } from 'convex/react';
-import { useTheme } from 'emotion-theming';
-import { useHistory } from 'react-router-dom';
+import { useTheme } from '@emotion/react';
+import { useNavigate } from 'react-router-dom';
+
+import { api } from '../../../convex/_generated/api';
+
import Header from 'components/Header/Header';
import Button from 'components/Button/Button';
import LeftIcon from 'components/svgs/LeftIcon';
@@ -10,9 +13,6 @@ import CrossIcon from 'components/svgs/CrossIcon';
import Loader from 'components/Loader/Loader';
import { useSession } from 'containers/AuthProvider';
import { useDocumentTitle } from 'utils/useDocumentTitle';
-import type { AppTheme } from 'styles/AppTheme';
-
-import { api } from '../../../convex/_generated/api';
const PAGE_SIZE = 12;
@@ -37,8 +37,8 @@ function formatUpdateDate(timestamp: number): string {
}
const UpdatesHeader = () => {
- const history = useHistory();
- const theme = useTheme();
+ const navigate = useNavigate();
+ const theme = useTheme();
return (
@@ -54,7 +54,7 @@ const UpdatesHeader = () => {