import { Button } from '@internxt/ui'; import { WarningCircleIcon } from '@phosphor-icons/react'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; import { Translate } from 'app/i18n/types'; import { CSSProperties, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { GroupedVirtuoso, GroupedVirtuosoHandle, ListRange } from 'react-virtuoso'; import dateService from 'services/date.service'; import { useElementSize } from '../hooks/useElementSize'; import { DayKey, MonthKey, Photo } from '../types'; import PhotoGridItem from './PhotoGridItem'; const MIN_TILE_SIZE = 160; const TILE_GAP = 4; const SIDE_PADDING = 20; const DAY_GAP = 24; interface PhotosTimelineProps { photos: Photo[]; failedMonths: MonthKey[]; isLoadingMore: boolean; onEndReached: () => void; onRetryMonth: (month: MonthKey) => void; } interface TimelineFooterContext { isLoadingMore: boolean; columns: number; } type TimelineRow = { kind: 'photos'; photos: Photo[] } | { kind: 'monthError'; month: MonthKey }; interface PhotosOfDay { day: DayKey; photos: Photo[]; } interface Timeline { days: DayKey[]; rowsPerDay: number[]; rows: TimelineRow[]; isLastRowOfDay: boolean[]; } const groupByDayNewestFirst = (photos: Photo[]): PhotosOfDay[] => { const photosNewestFirst = [...photos].sort( (photo, otherPhoto) => otherPhoto.day.localeCompare(photo.day) || otherPhoto.takenAt.localeCompare(photo.takenAt), ); const days: PhotosOfDay[] = []; for (const photo of photosNewestFirst) { const lastDay = days.at(-1); if (lastDay?.day === photo.day) lastDay.photos.push(photo); else days.push({ day: photo.day, photos: [photo] }); } return days; }; const getDayLabel = (day: DayKey, translate: Translate): string => { if (dateService.isToday(day)) return translate('views.photos.timeline.today'); if (dateService.isYesterday(day)) return translate('views.photos.timeline.yesterday'); return dateService.format(day, 'ddd, D MMM YYYY'); }; const getColumns = (width: number): number => Math.max(1, Math.floor((width - 2 * SIDE_PADDING + TILE_GAP) / (MIN_TILE_SIZE + TILE_GAP))); const splitIntoRows = (photos: Photo[], columns: number): Photo[][] => { const rows: Photo[][] = []; for (let start = 0; start < photos.length; start += columns) rows.push(photos.slice(start, start + columns)); return rows; }; const getTimeline = (days: PhotosOfDay[], columns: number, failedMonths: MonthKey[]): Timeline => { const rowsOfEachDay = days.map(({ photos }) => splitIntoRows(photos, columns).map((rowPhotos): TimelineRow => ({ kind: 'photos', photos: rowPhotos })), ); for (const month of failedMonths) { const daysUntilMonthEnd = days.filter(({ day }) => day.slice(0, 7) >= month).length; rowsOfEachDay[Math.max(0, daysUntilMonthEnd - 1)]?.push({ kind: 'monthError', month }); } return { days: days.map(({ day }) => day), rowsPerDay: rowsOfEachDay.map((rows) => rows.length), rows: rowsOfEachDay.flat(), isLastRowOfDay: rowsOfEachDay.flatMap((rows) => rows.map((_, index) => index === rows.length - 1)), }; }; const findRowOfPhoto = (timeline: Timeline, photoUuid: string): number => timeline.rows.findIndex((row) => row.kind === 'photos' && row.photos.some(({ uuid }) => uuid === photoUuid)); const getRowStyle = (columns: number, paddingBottom: number): CSSProperties => ({ display: 'grid', gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`, gap: TILE_GAP, paddingInline: SIDE_PADDING, paddingBottom, }); const TimelineFooter = ({ context }: { context: TimelineFooterContext }): JSX.Element | null => { if (!context.isLoadingMore) return null; return (