From 9267e2763905dca9a8a5ea7e3c3ba6606e6f80f3 Mon Sep 17 00:00:00 2001 From: danactive Date: Mon, 18 Nov 2024 17:56:34 -0800 Subject: [PATCH 1/8] Move types to pages --- package.json | 2 +- pages/[gallery].tsx | 43 +++++----------------------- pages/[gallery]/[album]/nearby.tsx | 15 ---------- src/components/GalleryPage/index.tsx | 31 ++++++++++++++++++++ src/types/pages.d.ts | 19 ++++++++++++ 5 files changed, 58 insertions(+), 52 deletions(-) delete mode 100644 pages/[gallery]/[album]/nearby.tsx create mode 100644 src/components/GalleryPage/index.tsx create mode 100644 src/types/pages.d.ts diff --git a/package.json b/package.json index 6cafea77..2f30b1a4 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "test": "jest --watch", "test:ci": "jest --ci", "test:e2e": "npx playwright test", - "typecheck": "tsc", + "typecheck": "echo 'Typecheck check!' && tsc", "release": "standard-version" }, "dependencies": { diff --git a/pages/[gallery].tsx b/pages/[gallery].tsx index f8068684..6674b197 100644 --- a/pages/[gallery].tsx +++ b/pages/[gallery].tsx @@ -1,27 +1,13 @@ import type { GetStaticPaths, GetStaticProps } from 'next' -import Head from 'next/head' -import { type ParsedUrlQuery } from 'node:querystring' +import GalleryPageComponent from '../src/components/GalleryPage' import getAlbums from '../src/lib/albums' import getGalleries from '../src/lib/galleries' import indexKeywords from '../src/lib/search' +import type { ServerSideAlbumItem } from '../src/types/common' +import { Gallery } from '../src/types/pages' -import Galleries from '../src/components/Albums' -import Link from '../src/components/Link' -import useSearch from '../src/hooks/useSearch' -import type { AlbumMeta, IndexedKeywords, ServerSideAlbumItem } from '../src/types/common' - -type ComponentProps = { - gallery: NonNullable; - albums: ServerSideAlbumItem[]; - indexedKeywords: IndexedKeywords[]; -} - -interface Params extends ParsedUrlQuery { - gallery: NonNullable -} - -export const getStaticProps: GetStaticProps = async (context) => { +export const getStaticProps: GetStaticProps = async (context) => { const params = context.params! const { albums } = await getAlbums(params.gallery) const preparedAlbums = albums.map((album): ServerSideAlbumItem => ({ @@ -43,25 +29,10 @@ export const getStaticPaths: GetStaticPaths = async () => { } } -function AlbumsPage({ gallery, albums, indexedKeywords }: ComponentProps) { - const { - filtered, - searchBox, - } = useSearch({ items: albums, indexedKeywords }) - +function GalleryPage({ gallery, albums, indexedKeywords }: Gallery.ComponentProps) { return ( -
- - History App - List Albums - - -
{searchBox}
-

Links

-
  • All
-
  • Today
- -
+ ) } -export default AlbumsPage +export default GalleryPage diff --git a/pages/[gallery]/[album]/nearby.tsx b/pages/[gallery]/[album]/nearby.tsx deleted file mode 100644 index ce0fd65e..00000000 --- a/pages/[gallery]/[album]/nearby.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import Head from 'next/head' - -function Nearby() { - return ( - <> - - History App - Nearby - - -

Hello World

- - ) -} - -export default Nearby diff --git a/src/components/GalleryPage/index.tsx b/src/components/GalleryPage/index.tsx new file mode 100644 index 00000000..aebc7059 --- /dev/null +++ b/src/components/GalleryPage/index.tsx @@ -0,0 +1,31 @@ +import Head from 'next/head' + +import useSearch from '../../hooks/useSearch' +import { Gallery } from '../../types/pages' +import Galleries from '../Albums' +import Link from '../Link' + +function GalleryPage({ gallery, albums, indexedKeywords }: Gallery.ComponentProps) { + const { + filtered, + searchBox, + } = useSearch({ items: albums, indexedKeywords }) + + return ( + <> + + History App - List Albums + + +
{searchBox}
+
    +
  • View All
  • +
  • Today
  • +
  • by Age
  • +
+ + + ) +} + +export default GalleryPage diff --git a/src/types/pages.d.ts b/src/types/pages.d.ts new file mode 100644 index 00000000..b81fe8d0 --- /dev/null +++ b/src/types/pages.d.ts @@ -0,0 +1,19 @@ +import { type ParsedUrlQuery } from 'node:querystring' + +import type { + AlbumMeta, + IndexedKeywords, + ServerSideAlbumItem, + ServerSidePhotoItem, +} from './common' + +export namespace Gallery { + export type ComponentProps = { + gallery: NonNullable; + albums: ServerSideAlbumItem[]; + indexedKeywords: IndexedKeywords[]; + } + export interface Params extends ParsedUrlQuery { + gallery: NonNullable + } +} From 01000d6bc352f5b78be1dc597233686c46ebd833 Mon Sep 17 00:00:00 2001 From: danactive Date: Thu, 21 Nov 2024 20:24:48 -0800 Subject: [PATCH 2/8] Move types to pages --- package.json | 2 +- pages/[gallery]/[album].tsx | 21 +++------------------ src/components/AlbumPage/index.tsx | 11 ++--------- src/types/common.d.ts | 4 ++++ src/types/pages.d.ts | 13 +++++++++++++ 5 files changed, 23 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 2f30b1a4..ea9d8632 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "test": "jest --watch", "test:ci": "jest --ci", "test:e2e": "npx playwright test", - "typecheck": "echo 'Typecheck check!' && tsc", + "typecheck": "echo 'Typecheck...' && tsc", "release": "standard-version" }, "dependencies": { diff --git a/pages/[gallery]/[album].tsx b/pages/[gallery]/[album].tsx index 753eedde..0832f1f5 100644 --- a/pages/[gallery]/[album].tsx +++ b/pages/[gallery]/[album].tsx @@ -6,7 +6,7 @@ import getAlbum from '../../src/lib/album' import getAlbums from '../../src/lib/albums' import getGalleries from '../../src/lib/galleries' import indexKeywords, { addGeographyToSearch } from '../../src/lib/search' -import type { AlbumMeta, IndexedKeywords, Item } from '../../src/types/common' +import type { Album } from '../../src/types/pages' async function buildStaticPaths() { const { galleries } = await getGalleries() @@ -17,22 +17,7 @@ async function buildStaticPaths() { return groups.flat() } -interface ServerSideAlbumItem extends Item { - corpus: string; -} - -type ComponentProps = { - items?: ServerSideAlbumItem[]; - meta: AlbumMeta; - indexedKeywords: IndexedKeywords[]; -} - -interface Params extends ParsedUrlQuery { - gallery: NonNullable - album: NonNullable -} - -export const getStaticProps: GetStaticProps = async (context) => { +export const getStaticProps: GetStaticProps = async (context) => { const params = context.params! const { album: { items, meta } } = await getAlbum(params.gallery, params.album) const preparedItems = items.map((item) => ({ @@ -54,7 +39,7 @@ export const getStaticPaths: GetStaticPaths = async () => ( } ) -function AlbumPage({ items = [], meta, indexedKeywords }: ComponentProps) { +function AlbumPage({ items = [], meta, indexedKeywords }: Album.ComponentProps) { return } diff --git a/src/components/AlbumPage/index.tsx b/src/components/AlbumPage/index.tsx index ff3f44d0..612134df 100644 --- a/src/components/AlbumPage/index.tsx +++ b/src/components/AlbumPage/index.tsx @@ -9,16 +9,9 @@ import SplitViewer from '../SplitViewer' import ThumbImg from '../ThumbImg' import styles from './styles.module.css' -import type { IndexedKeywords, Item } from '../../types/common' +import type { Album } from '../../types/pages' -interface ServerSidePhotoItem extends Item { - corpus: string; -} - -function AlbumPage( - { items = [], meta, indexedKeywords }: - { items: ServerSidePhotoItem[], meta?: object, indexedKeywords: IndexedKeywords[] }, -) { +function AlbumPage({ items = [], meta, indexedKeywords }: Album.ComponentProps) { const refImageGallery = useRef(null) const [memoryIndex, setMemoryIndex] = useState(0) const { diff --git a/src/types/common.d.ts b/src/types/common.d.ts index 7ff02342..d1f05099 100644 --- a/src/types/common.d.ts +++ b/src/types/common.d.ts @@ -126,6 +126,10 @@ interface ServerSideAlbumItem extends GalleryAlbum { corpus: string; } +interface ServerSidePhotoItem extends Item { + corpus: string; +} + interface ServerSideAllItem extends Item { album?: NonNullable; gallery?: NonNullable; diff --git a/src/types/pages.d.ts b/src/types/pages.d.ts index b81fe8d0..b94ffd41 100644 --- a/src/types/pages.d.ts +++ b/src/types/pages.d.ts @@ -17,3 +17,16 @@ export namespace Gallery { gallery: NonNullable } } + +export namespace Album { + export type ComponentProps = { + items: ServerSidePhotoItem[]; + meta?: object; + indexedKeywords: IndexedKeywords[]; + } + + export interface Params extends ParsedUrlQuery { + gallery: NonNullable + album: NonNullable + } +} From 5be4000bba975fe2177c7b1104a430caba1ddf77 Mon Sep 17 00:00:00 2001 From: danactive Date: Thu, 28 Nov 2024 20:57:26 -0800 Subject: [PATCH 3/8] Person: GenAi lists all years when selected shows persons of that age --- pages/[gallery]/age.tsx | 141 +++++++++++++++++++++++++++ src/components/GalleryPage/index.tsx | 2 +- 2 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 pages/[gallery]/age.tsx diff --git a/pages/[gallery]/age.tsx b/pages/[gallery]/age.tsx new file mode 100644 index 00000000..1745ee2e --- /dev/null +++ b/pages/[gallery]/age.tsx @@ -0,0 +1,141 @@ +import type { GetStaticPaths, GetStaticProps } from 'next' +import Head from 'next/head' +import { type ParsedUrlQuery } from 'node:querystring' +import { useEffect, useMemo, useRef, useState } from 'react' +import type ReactImageGallery from 'react-image-gallery' + +import config from '../../config.json' +import getAlbum from '../../src/lib/album' +import getAlbums from '../../src/lib/albums' +import getGalleries from '../../src/lib/galleries' +import All from '../../src/components/All' +import AlbumContext from '../../src/components/Context' +import SplitViewer from '../../src/components/SplitViewer' +import useMemory from '../../src/hooks/useMemory' +import { AlbumMeta, Item, ServerSideAllItem } from '../../src/types/common' + +type ComponentProps = { + items: ServerSideAllItem[]; +} + +export const getStaticProps: GetStaticProps = async (context) => { + const params = context.params! + const { albums } = await getAlbums(params.gallery as string) + const allItems: ServerSideAllItem[] = [] + + await albums.reduce(async (previousPromise, album) => { + await previousPromise + const { album: { items, meta } } = await getAlbum(params.gallery as string, album.name) + const albumCoordinateAccuracy = meta?.geo?.zoom ?? config.defaultZoom + + items.forEach((item: Item) => { + if (item.persons) { + allItems.push({ + ...item, + gallery: params.gallery as string, + album: album.name, + corpus: [item.description, item.caption, item.location, item.city, item.search].join(' '), + coordinateAccuracy: item.coordinateAccuracy ?? albumCoordinateAccuracy, + }) + } + }) + }, Promise.resolve()) + + return { props: { items: allItems } } +} + +export const getStaticPaths: GetStaticPaths = async () => { + const { galleries } = await getGalleries() + const paths = galleries.map((gallery) => ({ params: { gallery } })) + return { paths, fallback: false } +} + +function calculateAge(dob: string, photoDate: string): number { + const birth = new Date(dob.substring(0, 10)) + const photo = new Date(photoDate.substring(0, 10)) + + let age = photo.getFullYear() - birth.getFullYear() + const m = photo.getMonth() - birth.getMonth() + if (m < 0 || (m === 0 && photo.getDate() < birth.getDate())) { + age-- + } + return age +} + +function AgePage({ items = [] }: ComponentProps) { + const refImageGallery = useRef(null) + const [selectedAge, setSelectedAge] = useState(null) + const [memoryIndex, setMemoryIndex] = useState(0) + const [uniqueAges, setUniqueAges] = useState([]) + const [mounted, setMounted] = useState(false) + + useEffect(() => { + const ages = new Set( + items.flatMap(item => + item.persons?.map(person => { + if (!person.dob || !item.filename) return null + const photoDate = Array.isArray(item.filename) + ? item.filename[0].substring(0, 10) + : item.filename.substring(0, 10) + return calculateAge(person.dob, photoDate) + }) + ).filter((age): age is number => age !== null) + ) + setUniqueAges(Array.from(ages).sort((a, b) => a - b)) + setMounted(true) + }, [items]) + + const filteredItems = useMemo(() => { + if (selectedAge === null) return [] + return items.filter(item => { + if (!item.persons || !item.filename) return false + const photoDate = Array.isArray(item.filename) + ? item.filename[0].substring(0, 10) + : item.filename.substring(0, 10) + return item.persons.some(person => { + if (!person.dob) return false + return calculateAge(person.dob, photoDate) === selectedAge + }) + }) + }, [items, selectedAge]) + + const { setViewed, memoryHtml } = useMemory(filteredItems, refImageGallery) + const zooms = useMemo(() => ({ geo: { zoom: config.defaultZoom } }), []) + + return ( +
+ + History App - Ages + + +
+ {uniqueAges.map((age) => ( + + ))} +
+ + {memoryHtml} + + + +
+ ) +} + +export default AgePage diff --git a/src/components/GalleryPage/index.tsx b/src/components/GalleryPage/index.tsx index aebc7059..1c69adf5 100644 --- a/src/components/GalleryPage/index.tsx +++ b/src/components/GalleryPage/index.tsx @@ -21,7 +21,7 @@ function GalleryPage({ gallery, albums, indexedKeywords }: Gallery.ComponentProp
  • View All
  • Today
  • -
  • by Age
  • +
  • by Age
From 2e403f3c7922c299d1b20e764a7869e6cce006d4 Mon Sep 17 00:00:00 2001 From: danactive Date: Thu, 28 Nov 2024 21:11:35 -0800 Subject: [PATCH 4/8] Person: GenAi lists all years and allows to filter by person of that age --- pages/[gallery]/age.tsx | 110 +++++++++++++++++++++++++++++++++------- 1 file changed, 92 insertions(+), 18 deletions(-) diff --git a/pages/[gallery]/age.tsx b/pages/[gallery]/age.tsx index 1745ee2e..90cf20d8 100644 --- a/pages/[gallery]/age.tsx +++ b/pages/[gallery]/age.tsx @@ -3,6 +3,8 @@ import Head from 'next/head' import { type ParsedUrlQuery } from 'node:querystring' import { useEffect, useMemo, useRef, useState } from 'react' import type ReactImageGallery from 'react-image-gallery' +import Link from 'next/link' +import { useRouter } from 'next/router' import config from '../../config.json' import getAlbum from '../../src/lib/album' @@ -62,13 +64,26 @@ function calculateAge(dob: string, photoDate: string): number { return age } +type PersonMatch = { + name: string; + age: number; + photoDate: string; +} + function AgePage({ items = [] }: ComponentProps) { + const router = useRouter() + const { gallery } = router.query const refImageGallery = useRef(null) const [selectedAge, setSelectedAge] = useState(null) + const [selectedPerson, setSelectedPerson] = useState(null) const [memoryIndex, setMemoryIndex] = useState(0) const [uniqueAges, setUniqueAges] = useState([]) const [mounted, setMounted] = useState(false) + const zooms = useMemo(() => ({ + geo: { zoom: config.defaultZoom } + }), []) + useEffect(() => { const ages = new Set( items.flatMap(item => @@ -85,6 +100,40 @@ function AgePage({ items = [] }: ComponentProps) { setMounted(true) }, [items]) + const peopleAtSelectedAge = useMemo(() => { + if (selectedAge === null) return [] + + const matches: PersonMatch[] = [] + items.forEach(item => { + if (!item.persons || !item.filename) return + const photoDate = Array.isArray(item.filename) + ? item.filename[0].substring(0, 10) + : item.filename.substring(0, 10) + + item.persons.forEach(person => { + if (!person.dob) return + const age = calculateAge(person.dob, photoDate) + if (age === selectedAge) { + matches.push({ + name: person.full, + age, + photoDate + }) + } + }) + }) + + // Get unique names with their earliest photo date + return Array.from( + matches.reduce((acc, match) => { + if (!acc.has(match.name) || acc.get(match.name)!.photoDate > match.photoDate) { + acc.set(match.name, match) + } + return acc + }, new Map()) + ).map(([_, match]) => match.name).sort() + }, [items, selectedAge]) + const filteredItems = useMemo(() => { if (selectedAge === null) return [] return items.filter(item => { @@ -94,13 +143,16 @@ function AgePage({ items = [] }: ComponentProps) { : item.filename.substring(0, 10) return item.persons.some(person => { if (!person.dob) return false - return calculateAge(person.dob, photoDate) === selectedAge + const matchesAge = calculateAge(person.dob, photoDate) === selectedAge + const matchesPerson = selectedPerson ? person.full === selectedPerson : true + return matchesAge && matchesPerson }) }) - }, [items, selectedAge]) + }, [items, selectedAge, selectedPerson]) const { setViewed, memoryHtml } = useMemory(filteredItems, refImageGallery) - const zooms = useMemo(() => ({ geo: { zoom: config.defaultZoom } }), []) + + if (!mounted) return null return (
@@ -108,21 +160,7 @@ function AgePage({ items = [] }: ComponentProps) { History App - Ages -
- {uniqueAges.map((age) => ( - - ))} -
+ {memoryHtml} + +
+ {/* Ages list */} +
+ {uniqueAges.map((age, index) => ( + + + {index < uniqueAges.length - 1 && · } + + ))} +
+ + {/* People list */} + {selectedAge !== null && peopleAtSelectedAge.length > 0 && ( +
+ {peopleAtSelectedAge.map((name, index) => ( + + + {index < peopleAtSelectedAge.length - 1 && · } + + ))} +
+ )} +
+
From 6839723007cc2c4cb30a375b761275b960745d1a Mon Sep 17 00:00:00 2001 From: danactive Date: Thu, 28 Nov 2024 22:18:24 -0800 Subject: [PATCH 5/8] Person: GenAi lists all years and allows to filter by person of that age as dropdowns --- pages/[gallery]/age.tsx | 72 ++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/pages/[gallery]/age.tsx b/pages/[gallery]/age.tsx index 90cf20d8..0478496c 100644 --- a/pages/[gallery]/age.tsx +++ b/pages/[gallery]/age.tsx @@ -135,7 +135,8 @@ function AgePage({ items = [] }: ComponentProps) { }, [items, selectedAge]) const filteredItems = useMemo(() => { - if (selectedAge === null) return [] + if (selectedAge === null) return items + return items.filter(item => { if (!item.persons || !item.filename) return false const photoDate = Array.isArray(item.filename) @@ -161,6 +162,40 @@ function AgePage({ items = [] }: ComponentProps) { +
+ + + {selectedAge !== null && peopleAtSelectedAge.length > 0 && ( + + )} +
+ {memoryHtml} -
- {/* Ages list */} -
- {uniqueAges.map((age, index) => ( - - - {index < uniqueAges.length - 1 && · } - - ))} -
- - {/* People list */} - {selectedAge !== null && peopleAtSelectedAge.length > 0 && ( -
- {peopleAtSelectedAge.map((name, index) => ( - - - {index < peopleAtSelectedAge.length - 1 && · } - - ))} -
- )} -
-
From ae0fbc818089538ac7238df3c35e16d5627557a6 Mon Sep 17 00:00:00 2001 From: danactive Date: Fri, 29 Nov 2024 17:07:29 -0800 Subject: [PATCH 6/8] feat(View All > Person): Filter by keyword then age then person --- pages/[gallery]/age.tsx | 215 ------------------------ pages/[gallery]/all.tsx | 233 ++++++++++++++++++++++++++- src/components/GalleryPage/index.tsx | 1 - src/hooks/useSearch.tsx | 11 +- 4 files changed, 235 insertions(+), 225 deletions(-) delete mode 100644 pages/[gallery]/age.tsx diff --git a/pages/[gallery]/age.tsx b/pages/[gallery]/age.tsx deleted file mode 100644 index 0478496c..00000000 --- a/pages/[gallery]/age.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import type { GetStaticPaths, GetStaticProps } from 'next' -import Head from 'next/head' -import { type ParsedUrlQuery } from 'node:querystring' -import { useEffect, useMemo, useRef, useState } from 'react' -import type ReactImageGallery from 'react-image-gallery' -import Link from 'next/link' -import { useRouter } from 'next/router' - -import config from '../../config.json' -import getAlbum from '../../src/lib/album' -import getAlbums from '../../src/lib/albums' -import getGalleries from '../../src/lib/galleries' -import All from '../../src/components/All' -import AlbumContext from '../../src/components/Context' -import SplitViewer from '../../src/components/SplitViewer' -import useMemory from '../../src/hooks/useMemory' -import { AlbumMeta, Item, ServerSideAllItem } from '../../src/types/common' - -type ComponentProps = { - items: ServerSideAllItem[]; -} - -export const getStaticProps: GetStaticProps = async (context) => { - const params = context.params! - const { albums } = await getAlbums(params.gallery as string) - const allItems: ServerSideAllItem[] = [] - - await albums.reduce(async (previousPromise, album) => { - await previousPromise - const { album: { items, meta } } = await getAlbum(params.gallery as string, album.name) - const albumCoordinateAccuracy = meta?.geo?.zoom ?? config.defaultZoom - - items.forEach((item: Item) => { - if (item.persons) { - allItems.push({ - ...item, - gallery: params.gallery as string, - album: album.name, - corpus: [item.description, item.caption, item.location, item.city, item.search].join(' '), - coordinateAccuracy: item.coordinateAccuracy ?? albumCoordinateAccuracy, - }) - } - }) - }, Promise.resolve()) - - return { props: { items: allItems } } -} - -export const getStaticPaths: GetStaticPaths = async () => { - const { galleries } = await getGalleries() - const paths = galleries.map((gallery) => ({ params: { gallery } })) - return { paths, fallback: false } -} - -function calculateAge(dob: string, photoDate: string): number { - const birth = new Date(dob.substring(0, 10)) - const photo = new Date(photoDate.substring(0, 10)) - - let age = photo.getFullYear() - birth.getFullYear() - const m = photo.getMonth() - birth.getMonth() - if (m < 0 || (m === 0 && photo.getDate() < birth.getDate())) { - age-- - } - return age -} - -type PersonMatch = { - name: string; - age: number; - photoDate: string; -} - -function AgePage({ items = [] }: ComponentProps) { - const router = useRouter() - const { gallery } = router.query - const refImageGallery = useRef(null) - const [selectedAge, setSelectedAge] = useState(null) - const [selectedPerson, setSelectedPerson] = useState(null) - const [memoryIndex, setMemoryIndex] = useState(0) - const [uniqueAges, setUniqueAges] = useState([]) - const [mounted, setMounted] = useState(false) - - const zooms = useMemo(() => ({ - geo: { zoom: config.defaultZoom } - }), []) - - useEffect(() => { - const ages = new Set( - items.flatMap(item => - item.persons?.map(person => { - if (!person.dob || !item.filename) return null - const photoDate = Array.isArray(item.filename) - ? item.filename[0].substring(0, 10) - : item.filename.substring(0, 10) - return calculateAge(person.dob, photoDate) - }) - ).filter((age): age is number => age !== null) - ) - setUniqueAges(Array.from(ages).sort((a, b) => a - b)) - setMounted(true) - }, [items]) - - const peopleAtSelectedAge = useMemo(() => { - if (selectedAge === null) return [] - - const matches: PersonMatch[] = [] - items.forEach(item => { - if (!item.persons || !item.filename) return - const photoDate = Array.isArray(item.filename) - ? item.filename[0].substring(0, 10) - : item.filename.substring(0, 10) - - item.persons.forEach(person => { - if (!person.dob) return - const age = calculateAge(person.dob, photoDate) - if (age === selectedAge) { - matches.push({ - name: person.full, - age, - photoDate - }) - } - }) - }) - - // Get unique names with their earliest photo date - return Array.from( - matches.reduce((acc, match) => { - if (!acc.has(match.name) || acc.get(match.name)!.photoDate > match.photoDate) { - acc.set(match.name, match) - } - return acc - }, new Map()) - ).map(([_, match]) => match.name).sort() - }, [items, selectedAge]) - - const filteredItems = useMemo(() => { - if (selectedAge === null) return items - - return items.filter(item => { - if (!item.persons || !item.filename) return false - const photoDate = Array.isArray(item.filename) - ? item.filename[0].substring(0, 10) - : item.filename.substring(0, 10) - return item.persons.some(person => { - if (!person.dob) return false - const matchesAge = calculateAge(person.dob, photoDate) === selectedAge - const matchesPerson = selectedPerson ? person.full === selectedPerson : true - return matchesAge && matchesPerson - }) - }) - }, [items, selectedAge, selectedPerson]) - - const { setViewed, memoryHtml } = useMemory(filteredItems, refImageGallery) - - if (!mounted) return null - - return ( -
- - History App - Ages - - - -
- - - {selectedAge !== null && peopleAtSelectedAge.length > 0 && ( - - )} -
- - - {memoryHtml} - - - - -
- ) -} - -export default AgePage diff --git a/pages/[gallery]/all.tsx b/pages/[gallery]/all.tsx index 3ab4b0f6..d69833f3 100644 --- a/pages/[gallery]/all.tsx +++ b/pages/[gallery]/all.tsx @@ -1,7 +1,7 @@ import type { GetStaticPaths, GetStaticProps } from 'next' import Head from 'next/head' import { type ParsedUrlQuery } from 'node:querystring' -import { useMemo, useRef, useState } from 'react' +import { useMemo, useRef, useState, useEffect } from 'react' import type ReactImageGallery from 'react-image-gallery' import config from '../../config.json' @@ -77,18 +77,198 @@ export const getStaticPaths: GetStaticPaths = async () => { } } +function calculateAge(dob: string, photoDate: string): number | null { + try { + const birth = new Date(dob.substring(0, 10)) + const photo = new Date(photoDate.substring(0, 10)) + + // Validate dates + if (isNaN(birth.getTime()) || isNaN(photo.getTime())) { + return null + } + + let age = photo.getFullYear() - birth.getFullYear() + const m = photo.getMonth() - birth.getMonth() + if (m < 0 || (m === 0 && photo.getDate() < birth.getDate())) { + age-- + } + return age + } catch (e) { + return null + } +} + +type PersonMatch = { + name: string; + age: number; + photoDate: string; +} + function AllPage({ items = [], indexedKeywords }: ComponentProps) { const refImageGallery = useRef(null) const [memoryIndex, setMemoryIndex] = useState(0) + const [selectedAge, setSelectedAge] = useState(null) + const [selectedPerson, setSelectedPerson] = useState(null) + const [uniqueAges, setUniqueAges] = useState([]) const { - filtered, + filtered: keywordFiltered, keyword, searchBox, + setFiltered } = useSearch({ items, setMemoryIndex, indexedKeywords }) - const { setViewed, memoryHtml } = useMemory(filtered, refImageGallery) + const [ageFiltered, setAgeFiltered] = useState(keywordFiltered) + + // Update age filtered results whenever keyword search or age/person selection changes + useEffect(() => { + if (selectedAge === null) { + setAgeFiltered(keywordFiltered) + return + } + + const filtered = keywordFiltered.filter(item => { + if (!item.persons || !item.filename) return false + const photoDate = Array.isArray(item.filename) + ? item.filename[0].substring(0, 10) + : item.filename.substring(0, 10) + return item.persons.some(person => { + if (!person.dob) return false + const matchesAge = calculateAge(person.dob, photoDate) === selectedAge + const matchesPerson = selectedPerson ? person.full === selectedPerson : true + return matchesAge && matchesPerson + }) + }) + + setAgeFiltered(filtered) + }, [keywordFiltered, selectedAge, selectedPerson]) + + // Update the search results with our age-filtered items + useEffect(() => { + setFiltered(ageFiltered) + }, [ageFiltered, setFiltered]) + + // Update uniqueAges whenever keyword search changes + useEffect(() => { + const ages = new Set( + keywordFiltered.flatMap(item => + item.persons?.map(person => { + if (!person.dob || !item.filename) return null + const photoDate = Array.isArray(item.filename) + ? item.filename[0].substring(0, 10) + : item.filename.substring(0, 10) + const age = calculateAge(person.dob, photoDate) + return age + }) + ).filter((age): age is number => age !== null && !isNaN(age)) + ) + setUniqueAges(Array.from(ages).sort((a, b) => a - b)) + + // Reset age and person filters if the selected age is no longer available + if (selectedAge !== null && !ages.has(selectedAge)) { + setSelectedAge(null) + setSelectedPerson(null) + } + }, [keywordFiltered, selectedAge, keyword]) + + const peopleAtSelectedAge = useMemo(() => { + if (selectedAge === null) return [] + + const matches: PersonMatch[] = [] + ageFiltered.forEach(item => { + if (!item.persons || !item.filename) return + const photoDate = Array.isArray(item.filename) + ? item.filename[0].substring(0, 10) + : item.filename.substring(0, 10) + + item.persons.forEach(person => { + if (!person.dob) return + const age = calculateAge(person.dob, photoDate) + if (age === selectedAge) { + matches.push({ + name: person.full, + age, + photoDate + }) + } + }) + }) + + return Array.from( + matches.reduce((acc, match) => { + if (!acc.has(match.name) || acc.get(match.name)!.photoDate > match.photoDate) { + acc.set(match.name, match) + } + return acc + }, new Map()) + ).map(([_, match]) => match.name).sort() + }, [ageFiltered, selectedAge]) + + const { setViewed, memoryHtml } = useMemory(ageFiltered, refImageGallery) const zooms = useMemo(() => ({ geo: { zoom: config.defaultZoom } }), [config.defaultZoom]) + const agesWithCounts = useMemo(() => { + const counts = new Map() + + keywordFiltered.forEach(item => { + if (!item.persons || !item.filename) return + const photoDate = Array.isArray(item.filename) + ? item.filename[0].substring(0, 10) + : item.filename.substring(0, 10) + + // Track if we've counted this photo for this age already + const agesCounted = new Set() + + item.persons.forEach(person => { + if (!person.dob) return + const age = calculateAge(person.dob, photoDate) + if (age !== null && !agesCounted.has(age)) { + counts.set(age, (counts.get(age) || 0) + 1) + agesCounted.add(age) + } + }) + }) + + return uniqueAges + .map(age => ({ + age, + count: counts.get(age) || 0 + })) + .filter(({ count, age }) => count > 0 && !isNaN(age)) + .sort((a, b) => a.age - b.age) + }, [keywordFiltered, uniqueAges, keyword]) + + const totalPhotoCount = useMemo(() => { + // Count unique photos, not persons + return keywordFiltered.filter(item => + item.persons?.some(person => person.dob) + ).length + }, [keywordFiltered]) + + const peopleWithCounts = useMemo(() => { + if (selectedAge === null) return [] + + const counts = new Map() + ageFiltered.forEach(item => { + if (!item.persons || !item.filename) return + const photoDate = Array.isArray(item.filename) + ? item.filename[0].substring(0, 10) + : item.filename.substring(0, 10) + + item.persons.forEach(person => { + if (!person.dob) return + const age = calculateAge(person.dob, photoDate) + if (age === selectedAge) { + counts.set(person.full, (counts.get(person.full) || 0) + 1) + } + }) + }) + + return peopleAtSelectedAge.map(name => ({ + name, + count: counts.get(name) || 0 + })) + }, [ageFiltered, selectedAge, peopleAtSelectedAge]) + return (
@@ -96,16 +276,57 @@ function AllPage({ items = [], indexedKeywords }: ComponentProps) { - {searchBox} +
+ {searchBox} +
+ + + {selectedAge !== null && peopleAtSelectedAge.length > 0 && ( + + )} +
+
+ {memoryHtml} - +
) diff --git a/src/components/GalleryPage/index.tsx b/src/components/GalleryPage/index.tsx index 1c69adf5..4934a0e0 100644 --- a/src/components/GalleryPage/index.tsx +++ b/src/components/GalleryPage/index.tsx @@ -21,7 +21,6 @@ function GalleryPage({ gallery, albums, indexedKeywords }: Gallery.ComponentProp
  • View All
  • Today
  • -
  • by Age
diff --git a/src/hooks/useSearch.tsx b/src/hooks/useSearch.tsx index b6149c27..7266e76c 100644 --- a/src/hooks/useSearch.tsx +++ b/src/hooks/useSearch.tsx @@ -12,10 +12,11 @@ interface ServerSideItem { function useSearch( { items, setMemoryIndex, indexedKeywords }: { items: ItemType[]; setMemoryIndex?: Function; indexedKeywords: IndexedKeywords[] }, -): { filtered: ItemType[]; keyword: string; setKeyword: Function; searchBox: JSX.Element; } { +): { filtered: ItemType[]; keyword: string; setKeyword: Function; searchBox: JSX.Element; setFiltered: Function; } { const router = useRouter() const [keyword, setKeyword] = useState(router.query.keyword?.toString() || '') const [selectedOption, setSelectedOption] = useState(null) + const [filteredItems, setFilteredItems] = useState(items) const getShareUrlStem = () => { if (router.asPath.includes('keyword=')) { @@ -55,6 +56,7 @@ function useSearch( keyword: '', setKeyword, searchBox: getSearchBox(items), + setFilteredItems, } useEffect(() => { @@ -68,9 +70,11 @@ function useSearch( setSelectedOption(newValue) } }, [router.isReady]) - if (!router.isReady) { - return defaultReturn + return { + ...defaultReturn, + setFiltered: () => {}, // Add missing setFiltered property + } } const AND_OPERATOR = '&&' @@ -93,6 +97,7 @@ function useSearch( return { filtered, + setFiltered: setFilteredItems, keyword, setKeyword, searchBox: getSearchBox(filtered), From a5b039bfbc7897195c9d5cbd658206005664e6ad Mon Sep 17 00:00:00 2001 From: danactive Date: Fri, 29 Nov 2024 17:53:41 -0800 Subject: [PATCH 7/8] lint --- pages/[gallery]/all.tsx | 192 +++++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 101 deletions(-) diff --git a/pages/[gallery]/all.tsx b/pages/[gallery]/all.tsx index d69833f3..fbf43ed2 100644 --- a/pages/[gallery]/all.tsx +++ b/pages/[gallery]/all.tsx @@ -1,7 +1,7 @@ import type { GetStaticPaths, GetStaticProps } from 'next' import Head from 'next/head' import { type ParsedUrlQuery } from 'node:querystring' -import { useMemo, useRef, useState, useEffect } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import type ReactImageGallery from 'react-image-gallery' import config from '../../config.json' @@ -9,81 +9,34 @@ import getAlbum from '../../src/lib/album' import getAlbums from '../../src/lib/albums' import getGalleries from '../../src/lib/galleries' import indexKeywords, { addGeographyToSearch } from '../../src/lib/search' - import All from '../../src/components/All' import AlbumContext from '../../src/components/Context' import SplitViewer from '../../src/components/SplitViewer' import useMemory from '../../src/hooks/useMemory' import useSearch from '../../src/hooks/useSearch' - import { - AlbumMeta, IndexedKeywords, Item, ServerSideAllItem, + AlbumMeta, + IndexedKeywords, + Item, + ServerSideAllItem, } from '../../src/types/common' type ComponentProps = { - items: ServerSideAllItem[]; - indexedKeywords: IndexedKeywords[]; + items: ServerSideAllItem[] + indexedKeywords: IndexedKeywords[] } interface Params extends ParsedUrlQuery { gallery: NonNullable } -export const getStaticProps: GetStaticProps = async (context) => { - const params = context.params! - const { albums } = await getAlbums(params.gallery) - - const prepareItems = ( - { albumName, albumCoordinateAccuracy, items }: - { - albumName: AlbumMeta['albumName'], - albumCoordinateAccuracy: NonNullable['zoom'], - items: Item[], - }, - ) => items.map((item) => ({ - ...item, - gallery: params.gallery, - album: albumName, - corpus: [item.description, item.caption, item.location, item.city, item.search].join(' '), - coordinateAccuracy: item.coordinateAccuracy ?? albumCoordinateAccuracy, - search: addGeographyToSearch(item), - })) - - // reverse order for albums in ascending order (oldest on top) - const allItems = (await albums.reduce(async (previousPromise, album) => { - const prev = await previousPromise - const { album: { items, meta } } = await getAlbum(params.gallery, album.name) - const albumCoordinateAccuracy = meta?.geo?.zoom ?? config.defaultZoom - const preparedItems = prepareItems({ - albumName: album.name, - albumCoordinateAccuracy, - items, - }) - return prev.concat(preparedItems) - }, Promise.resolve([] as ServerSideAllItem[]))).reverse() - - return { - props: { items: allItems, ...indexKeywords(allItems) }, - } -} - -export const getStaticPaths: GetStaticPaths = async () => { - const { galleries } = await getGalleries() - // Define these galleries as allowed, otherwise 404 - const paths = galleries.map((gallery) => ({ params: { gallery } })) - return { - paths, - fallback: false, - } -} - function calculateAge(dob: string, photoDate: string): number | null { try { const birth = new Date(dob.substring(0, 10)) const photo = new Date(photoDate.substring(0, 10)) // Validate dates - if (isNaN(birth.getTime()) || isNaN(photo.getTime())) { + if (Number.isNaN(birth.getTime()) || Number.isNaN(photo.getTime())) { return null } @@ -99,9 +52,9 @@ function calculateAge(dob: string, photoDate: string): number | null { } type PersonMatch = { - name: string; - age: number; - photoDate: string; + name: string + age: number + photoDate: string } function AllPage({ items = [], indexedKeywords }: ComponentProps) { @@ -110,6 +63,7 @@ function AllPage({ items = [], indexedKeywords }: ComponentProps) { const [selectedAge, setSelectedAge] = useState(null) const [selectedPerson, setSelectedPerson] = useState(null) const [uniqueAges, setUniqueAges] = useState([]) + const { filtered: keywordFiltered, keyword, @@ -170,42 +124,6 @@ function AllPage({ items = [], indexedKeywords }: ComponentProps) { } }, [keywordFiltered, selectedAge, keyword]) - const peopleAtSelectedAge = useMemo(() => { - if (selectedAge === null) return [] - - const matches: PersonMatch[] = [] - ageFiltered.forEach(item => { - if (!item.persons || !item.filename) return - const photoDate = Array.isArray(item.filename) - ? item.filename[0].substring(0, 10) - : item.filename.substring(0, 10) - - item.persons.forEach(person => { - if (!person.dob) return - const age = calculateAge(person.dob, photoDate) - if (age === selectedAge) { - matches.push({ - name: person.full, - age, - photoDate - }) - } - }) - }) - - return Array.from( - matches.reduce((acc, match) => { - if (!acc.has(match.name) || acc.get(match.name)!.photoDate > match.photoDate) { - acc.set(match.name, match) - } - return acc - }, new Map()) - ).map(([_, match]) => match.name).sort() - }, [ageFiltered, selectedAge]) - - const { setViewed, memoryHtml } = useMemory(ageFiltered, refImageGallery) - const zooms = useMemo(() => ({ geo: { zoom: config.defaultZoom } }), [config.defaultZoom]) - const agesWithCounts = useMemo(() => { const counts = new Map() @@ -244,10 +162,14 @@ function AllPage({ items = [], indexedKeywords }: ComponentProps) { ).length }, [keywordFiltered]) - const peopleWithCounts = useMemo(() => { - if (selectedAge === null) return [] + const { peopleAtSelectedAge, peopleWithCounts } = useMemo(() => { + if (selectedAge === null) { + return { peopleAtSelectedAge: [], peopleWithCounts: [] } + } + const matches: PersonMatch[] = [] const counts = new Map() + ageFiltered.forEach(item => { if (!item.persons || !item.filename) return const photoDate = Array.isArray(item.filename) @@ -258,16 +180,36 @@ function AllPage({ items = [], indexedKeywords }: ComponentProps) { if (!person.dob) return const age = calculateAge(person.dob, photoDate) if (age === selectedAge) { + matches.push({ + name: person.full, + age, + photoDate + }) counts.set(person.full, (counts.get(person.full) || 0) + 1) } }) }) - return peopleAtSelectedAge.map(name => ({ - name, - count: counts.get(name) || 0 - })) - }, [ageFiltered, selectedAge, peopleAtSelectedAge]) + const uniquePeople = Array.from( + matches.reduce((acc, match) => { + if (!acc.has(match.name) || acc.get(match.name)!.photoDate > match.photoDate) { + acc.set(match.name, match) + } + return acc + }, new Map()) + ).map(([_, match]) => match.name).sort() + + return { + peopleAtSelectedAge: uniquePeople, + peopleWithCounts: uniquePeople.map(name => ({ + name, + count: counts.get(name) || 0 + })) + } + }, [ageFiltered, selectedAge]) + + const { setViewed, memoryHtml } = useMemory(ageFiltered, refImageGallery) + const zooms = useMemo(() => ({ geo: { zoom: config.defaultZoom } }), []) return (
@@ -332,4 +274,52 @@ function AllPage({ items = [], indexedKeywords }: ComponentProps) { ) } +export const getStaticProps: GetStaticProps = async (context) => { + const params = context.params! + const { albums } = await getAlbums(params.gallery) + + const prepareItems = ( + { albumName, albumCoordinateAccuracy, items }: + { + albumName: AlbumMeta['albumName'], + albumCoordinateAccuracy: NonNullable['zoom'], + items: Item[], + }, + ) => items.map((item) => ({ + ...item, + gallery: params.gallery, + album: albumName, + corpus: [item.description, item.caption, item.location, item.city, item.search].join(' '), + coordinateAccuracy: item.coordinateAccuracy ?? albumCoordinateAccuracy, + search: addGeographyToSearch(item), + })) + + // reverse order for albums in ascending order (oldest on top) + const allItems = (await albums.reduce(async (previousPromise, album) => { + const prev = await previousPromise + const { album: { items, meta } } = await getAlbum(params.gallery, album.name) + const albumCoordinateAccuracy = meta?.geo?.zoom ?? config.defaultZoom + const preparedItems = prepareItems({ + albumName: album.name, + albumCoordinateAccuracy, + items, + }) + return prev.concat(preparedItems) + }, Promise.resolve([] as ServerSideAllItem[]))).reverse() + + return { + props: { items: allItems, ...indexKeywords(allItems) }, + } +} + +export const getStaticPaths: GetStaticPaths = async () => { + const { galleries } = await getGalleries() + // Define these galleries as allowed, otherwise 404 + const paths = galleries.map((gallery) => ({ params: { gallery } })) + return { + paths, + fallback: false, + } +} + export default AllPage From a3987b428a89c5d7ac295cdf93d823d0a754f4d3 Mon Sep 17 00:00:00 2001 From: danactive Date: Fri, 29 Nov 2024 18:07:16 -0800 Subject: [PATCH 8/8] lint --- pages/[gallery]/all.tsx | 97 +++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 52 deletions(-) diff --git a/pages/[gallery]/all.tsx b/pages/[gallery]/all.tsx index fbf43ed2..dea64ad9 100644 --- a/pages/[gallery]/all.tsx +++ b/pages/[gallery]/all.tsx @@ -1,7 +1,9 @@ import type { GetStaticPaths, GetStaticProps } from 'next' import Head from 'next/head' import { type ParsedUrlQuery } from 'node:querystring' -import { useEffect, useMemo, useRef, useState } from 'react' +import { + useEffect, useMemo, useRef, useState, +} from 'react' import type ReactImageGallery from 'react-image-gallery' import config from '../../config.json' @@ -34,16 +36,16 @@ function calculateAge(dob: string, photoDate: string): number | null { try { const birth = new Date(dob.substring(0, 10)) const photo = new Date(photoDate.substring(0, 10)) - + // Validate dates if (Number.isNaN(birth.getTime()) || Number.isNaN(photo.getTime())) { return null } - + let age = photo.getFullYear() - birth.getFullYear() const m = photo.getMonth() - birth.getMonth() if (m < 0 || (m === 0 && photo.getDate() < birth.getDate())) { - age-- + age -= 1 } return age } catch (e) { @@ -68,7 +70,7 @@ function AllPage({ items = [], indexedKeywords }: ComponentProps) { filtered: keywordFiltered, keyword, searchBox, - setFiltered + setFiltered, } = useSearch({ items, setMemoryIndex, indexedKeywords }) const [ageFiltered, setAgeFiltered] = useState(keywordFiltered) @@ -79,13 +81,13 @@ function AllPage({ items = [], indexedKeywords }: ComponentProps) { setAgeFiltered(keywordFiltered) return } - - const filtered = keywordFiltered.filter(item => { + + const filtered = keywordFiltered.filter((item) => { if (!item.persons || !item.filename) return false - const photoDate = Array.isArray(item.filename) + const photoDate = Array.isArray(item.filename) ? item.filename[0].substring(0, 10) : item.filename.substring(0, 10) - return item.persons.some(person => { + return item.persons.some((person) => { if (!person.dob) return false const matchesAge = calculateAge(person.dob, photoDate) === selectedAge const matchesPerson = selectedPerson ? person.full === selectedPerson : true @@ -104,20 +106,17 @@ function AllPage({ items = [], indexedKeywords }: ComponentProps) { // Update uniqueAges whenever keyword search changes useEffect(() => { const ages = new Set( - keywordFiltered.flatMap(item => - item.persons?.map(person => { - if (!person.dob || !item.filename) return null - const photoDate = Array.isArray(item.filename) - ? item.filename[0].substring(0, 10) - : item.filename.substring(0, 10) - const age = calculateAge(person.dob, photoDate) - return age - }) - ).filter((age): age is number => age !== null && !isNaN(age)) + keywordFiltered.flatMap((item) => item.persons?.map((person) => { + if (!person.dob || !item.filename) return null + const photoDate = Array.isArray(item.filename) + ? item.filename[0].substring(0, 10) + : item.filename.substring(0, 10) + const age = calculateAge(person.dob, photoDate) + return age + })).filter((age): age is number => age !== null && !Number.isNaN(age)), ) setUniqueAges(Array.from(ages).sort((a, b) => a - b)) - - // Reset age and person filters if the selected age is no longer available + if (selectedAge !== null && !ages.has(selectedAge)) { setSelectedAge(null) setSelectedPerson(null) @@ -126,17 +125,16 @@ function AllPage({ items = [], indexedKeywords }: ComponentProps) { const agesWithCounts = useMemo(() => { const counts = new Map() - - keywordFiltered.forEach(item => { + + keywordFiltered.forEach((item) => { if (!item.persons || !item.filename) return - const photoDate = Array.isArray(item.filename) + const photoDate = Array.isArray(item.filename) ? item.filename[0].substring(0, 10) : item.filename.substring(0, 10) - - // Track if we've counted this photo for this age already + const agesCounted = new Set() - - item.persons.forEach(person => { + + item.persons.forEach((person) => { if (!person.dob) return const age = calculateAge(person.dob, photoDate) if (age !== null && !agesCounted.has(age)) { @@ -147,43 +145,38 @@ function AllPage({ items = [], indexedKeywords }: ComponentProps) { }) return uniqueAges - .map(age => ({ - age, - count: counts.get(age) || 0 - })) - .filter(({ count, age }) => count > 0 && !isNaN(age)) + .map((age) => ({ age, count: counts.get(age) || 0 })) + .filter(({ count, age }) => count > 0 && !Number.isNaN(age)) .sort((a, b) => a.age - b.age) }, [keywordFiltered, uniqueAges, keyword]) - const totalPhotoCount = useMemo(() => { - // Count unique photos, not persons - return keywordFiltered.filter(item => - item.persons?.some(person => person.dob) - ).length - }, [keywordFiltered]) + const totalPhotoCount = useMemo( + () => keywordFiltered.filter((item) => item.persons?.some((person) => person.dob)).length, + [keywordFiltered], + ) const { peopleAtSelectedAge, peopleWithCounts } = useMemo(() => { if (selectedAge === null) { return { peopleAtSelectedAge: [], peopleWithCounts: [] } } - + const matches: PersonMatch[] = [] const counts = new Map() - - ageFiltered.forEach(item => { + + ageFiltered.forEach((item) => { if (!item.persons || !item.filename) return - const photoDate = Array.isArray(item.filename) + const photoDate = Array.isArray(item.filename) ? item.filename[0].substring(0, 10) : item.filename.substring(0, 10) - - item.persons.forEach(person => { + + item.persons.forEach((person) => { if (!person.dob) return const age = calculateAge(person.dob, photoDate) if (age === selectedAge) { matches.push({ name: person.full, age, - photoDate + photoDate, }) counts.set(person.full, (counts.get(person.full) || 0) + 1) } @@ -196,15 +189,15 @@ function AllPage({ items = [], indexedKeywords }: ComponentProps) { acc.set(match.name, match) } return acc - }, new Map()) + }, new Map()), ).map(([_, match]) => match.name).sort() return { peopleAtSelectedAge: uniquePeople, - peopleWithCounts: uniquePeople.map(name => ({ + peopleWithCounts: uniquePeople.map((name) => ({ name, - count: counts.get(name) || 0 - })) + count: counts.get(name) || 0, + })), } }, [ageFiltered, selectedAge]) @@ -224,8 +217,8 @@ function AllPage({ items = [], indexedKeywords }: ComponentProps) {