Add Russian i18n with DB translations, locale API, and curator review UI.
UI chrome via react-i18next, catalog text in entity_translations with ru.wikipedia seeding, locale-aware search, and Translations page for publish workflow. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
f247b418d8
commit
ca58c43648
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Timeline from '../components/Timeline';
|
||||
import TimelineEventGuides from '../components/TimelineEventGuides';
|
||||
import MovementBands from '../components/MovementBands';
|
||||
@@ -6,13 +7,17 @@ import VirtualGallery from '../components/VirtualGallery';
|
||||
import PaintingDetailView from '../components/PaintingDetail';
|
||||
import ArtistBio from '../components/ArtistBio';
|
||||
import CheckupPage from '../pages/CheckupPage';
|
||||
import TranslationsPage from '../pages/TranslationsPage';
|
||||
import CuratorLoginModal from '../components/CuratorLoginModal';
|
||||
import CatalogSearchBar from '../components/CatalogSearchBar';
|
||||
import LocaleSwitcher from '../components/LocaleSwitcher';
|
||||
import GalleryLoadingMarker from '../components/GalleryLoadingMarker';
|
||||
import '../components/CatalogSearchBar.css';
|
||||
import '../components/CuratorLoginModal.css';
|
||||
import '../components/LocaleSwitcher.css';
|
||||
import '../pages/TranslationsPage.css';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { api, type FixPaintingImageResult, type FixArtistPortraitResult } from '../api/client';
|
||||
import { api, setApiLocale, type FixPaintingImageResult, type FixArtistPortraitResult } from '../api/client';
|
||||
import type { TimelineData, Artist, ArtistDetail, Painting, PaintingDetail, MovementGalleryDetail } from '../types';
|
||||
import { createViewChangeScheduler } from '../utils/timelineView';
|
||||
import { sortArtistPaintingsChronological } from '../utils/paintingUtils';
|
||||
@@ -22,6 +27,7 @@ import './HomePage.css';
|
||||
type View =
|
||||
| { type: 'timeline' }
|
||||
| { type: 'checkup' }
|
||||
| { type: 'translations' }
|
||||
| { type: 'gallery'; artistId: number; data: ArtistDetail }
|
||||
| { type: 'movement-gallery'; movementId: number; data: MovementGalleryDetail }
|
||||
| { type: 'painting'; paintingId: number; data: PaintingDetail; returnTo: View }
|
||||
@@ -102,6 +108,7 @@ function catalogNavigateTarget(
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const { t } = useTranslation('home');
|
||||
const { isCurator, username, login, logout } = useAuth();
|
||||
const [view, setView] = useState<View>({ type: 'timeline' });
|
||||
const [gallerySession, setGallerySession] = useState<GallerySession | null>(null);
|
||||
@@ -117,10 +124,11 @@ export default function HomePage() {
|
||||
const [detailArtistPaintings, setDetailArtistPaintings] = useState<Painting[]>([]);
|
||||
const [imageRevisions, setImageRevisions] = useState<Record<number, number>>({});
|
||||
const [portraitRevisions, setPortraitRevisions] = useState<Record<number, number>>({});
|
||||
const [localeVersion, setLocaleVersion] = useState(0);
|
||||
const [debugMode, setDebugMode] = useState(readDebugMode);
|
||||
const [debugShowMore, setDebugShowMore] = useState(readDebugShowMore);
|
||||
const [loginOpen, setLoginOpen] = useState(false);
|
||||
const [loginRedirect, setLoginRedirect] = useState<'checkup' | null>(null);
|
||||
const [loginRedirect, setLoginRedirect] = useState<'checkup' | 'translations' | null>(null);
|
||||
const effectiveDebugMode = debugMode && isCurator;
|
||||
const [galleryRevision, setGalleryRevision] = useState(0);
|
||||
const viewRef = useRef(view);
|
||||
@@ -170,6 +178,11 @@ export default function HomePage() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [localeVersion]);
|
||||
|
||||
const handleLocaleChange = useCallback((locale: 'en' | 'ru') => {
|
||||
setApiLocale(locale);
|
||||
setLocaleVersion((v) => v + 1);
|
||||
}, []);
|
||||
|
||||
const viewChangeScheduler = useRef(
|
||||
@@ -209,7 +222,7 @@ export default function HomePage() {
|
||||
writeDebugShowMore(enabled);
|
||||
};
|
||||
|
||||
const openCuratorLogin = (redirect: 'checkup' | null = null) => {
|
||||
const openCuratorLogin = (redirect: 'checkup' | 'translations' | null = null) => {
|
||||
setLoginRedirect(redirect);
|
||||
setLoginOpen(true);
|
||||
};
|
||||
@@ -219,6 +232,8 @@ export default function HomePage() {
|
||||
setLoginOpen(false);
|
||||
if (loginRedirect === 'checkup') {
|
||||
setView({ type: 'checkup' });
|
||||
} else if (loginRedirect === 'translations') {
|
||||
setView({ type: 'translations' });
|
||||
}
|
||||
setLoginRedirect(null);
|
||||
};
|
||||
@@ -227,7 +242,7 @@ export default function HomePage() {
|
||||
await logout();
|
||||
writeDebugMode(false);
|
||||
setDebugMode(false);
|
||||
if (view.type === 'checkup') {
|
||||
if (view.type === 'checkup' || view.type === 'translations') {
|
||||
goToTimelineHome();
|
||||
}
|
||||
};
|
||||
@@ -240,6 +255,14 @@ export default function HomePage() {
|
||||
setView({ type: 'checkup' });
|
||||
};
|
||||
|
||||
const openTranslations = () => {
|
||||
if (!isCurator) {
|
||||
openCuratorLogin('translations');
|
||||
return;
|
||||
}
|
||||
setView({ type: 'translations' });
|
||||
};
|
||||
|
||||
const handlePaintingImageFixed = useCallback(async (paintingId: number, fixResult: FixPaintingImageResult) => {
|
||||
const data = await api.getPainting(paintingId);
|
||||
const patch: Partial<Painting> = {
|
||||
@@ -663,7 +686,7 @@ export default function HomePage() {
|
||||
key={view.paintingId}
|
||||
data={view.data}
|
||||
artistPaintings={sortedDetailArtistPaintings}
|
||||
backLabel={view.returnTo.type === 'timeline' ? '← Back to Timeline' : '← Back to Gallery'}
|
||||
backLabel={view.returnTo.type === 'timeline' ? t('backToTimeline') : t('backToGallery')}
|
||||
onBack={() => {
|
||||
if (view.returnTo.type === 'timeline') {
|
||||
goToTimelineHome();
|
||||
@@ -723,6 +746,25 @@ export default function HomePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{view.type === 'translations' && (
|
||||
isCurator ? (
|
||||
<TranslationsPage onBack={goToTimelineHome} />
|
||||
) : (
|
||||
<div className="curator-login-gate">
|
||||
<h2>{t('curatorRequiredTitle')}</h2>
|
||||
<p>{t('curatorRequiredBody')}</p>
|
||||
<div className="curator-login-gate-actions">
|
||||
<button type="button" className="checkup-link-btn" onClick={() => openCuratorLogin('translations')}>
|
||||
{t('curatorLogin')}
|
||||
</button>
|
||||
<button type="button" className="debug-mode-toggle" onClick={goToTimelineHome}>
|
||||
{t('backToGalleryBtn')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{view.type === 'checkup' && (
|
||||
isCurator ? (
|
||||
<CheckupPage
|
||||
@@ -782,13 +824,21 @@ export default function HomePage() {
|
||||
/>
|
||||
Show more
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="checkup-link-btn"
|
||||
onClick={openTranslations}
|
||||
title="Review and publish Russian translations"
|
||||
>
|
||||
{t('translations')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="checkup-link-btn"
|
||||
onClick={openCheckup}
|
||||
title="Open painting image checkup table"
|
||||
>
|
||||
Checkup
|
||||
{t('checkup')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -796,7 +846,7 @@ export default function HomePage() {
|
||||
onClick={handleCuratorLogout}
|
||||
title="Sign out curator session"
|
||||
>
|
||||
Logout
|
||||
{t('curatorLogout')}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
@@ -806,12 +856,13 @@ export default function HomePage() {
|
||||
onClick={() => openCuratorLogin()}
|
||||
title="Sign in as curator to use debug tools"
|
||||
>
|
||||
Curator login
|
||||
{t('curatorLogin')}
|
||||
</button>
|
||||
)}
|
||||
<LocaleSwitcher onLocaleChange={handleLocaleChange} />
|
||||
</div>
|
||||
<h1>Virtual Art Gallery</h1>
|
||||
<p className="site-subtitle">Watch art movements branch forward through time — each flowing from what came before</p>
|
||||
<h1>{t('title')}</h1>
|
||||
<p className="site-subtitle">{t('subtitle')}</p>
|
||||
<CatalogSearchBar
|
||||
onSelectArtist={handleArtistClick}
|
||||
onSelectMovement={handleMovementClick}
|
||||
|
||||
Reference in New Issue
Block a user