From 37f3d5c78825b9ad54fead152f7a1dd651ae0203 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Oct 2025 07:37:18 +0000 Subject: [PATCH 1/7] Initial plan From b3dd292d33fa189a277b0d820f01e52866076ed6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Oct 2025 07:43:12 +0000 Subject: [PATCH 2/7] Implement fullscreen mode for pages without geo data Co-authored-by: bcye <29666239+bcye@users.noreply.github.com> --- .../main/explore/page/[pageId]/_layout.tsx | 2 +- app/app/main/explore/page/[pageId]/index.tsx | 31 +++++++++++++------ app/hooks/use-is-fullscreen.tsx | 18 +++++++++-- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/app/app/main/explore/page/[pageId]/_layout.tsx b/app/app/main/explore/page/[pageId]/_layout.tsx index eb5258e..4fb0c08 100644 --- a/app/app/main/explore/page/[pageId]/_layout.tsx +++ b/app/app/main/explore/page/[pageId]/_layout.tsx @@ -52,7 +52,7 @@ export default function RootLayout() { ); return ( - + {!fullscreen ? {stack} : stack} diff --git a/app/app/main/explore/page/[pageId]/index.tsx b/app/app/main/explore/page/[pageId]/index.tsx index 8bd6a58..d6d8f3f 100644 --- a/app/app/main/explore/page/[pageId]/index.tsx +++ b/app/app/main/explore/page/[pageId]/index.tsx @@ -1,5 +1,6 @@ import useMoveTo from "@/hooks/use-move-to"; import useWikiQuery from "@/hooks/use-wiki-query"; +import { useSetFullscreen } from "@/hooks/use-is-fullscreen"; import { useLocalSearchParams } from "expo-router"; import { useEffect } from "react"; import PageRootView from "./_page-root-view"; @@ -9,19 +10,31 @@ export default function Page() { pageId = typeof pageId === "string" ? pageId : pageId[0]; const pageQuery = useWikiQuery(pageId); const moveTo = useMoveTo(); + const setFullscreen = useSetFullscreen(); useEffect(() => { if (pageQuery.data) { - moveTo( - // @ts-ignore NEEDS FIXING WHEN GEO REVISED - parseFloat(pageQuery.data.properties.geo["2"]), - // @ts-ignore NEEDS FIXING WHEN GEO REVISED - parseFloat(pageQuery.data.properties.geo["1"]), - // @ts-ignore NEEDS FIXING WHEN GEO REVISED - parseFloat(pageQuery.data.properties.geo?.zoom ?? "13"), - ); + const hasGeo = + pageQuery.data.properties.geo && + pageQuery.data.properties.geo["1"] && + pageQuery.data.properties.geo["2"]; + + if (hasGeo) { + // Page has geo data, zoom to coordinates + moveTo( + // @ts-ignore NEEDS FIXING WHEN GEO REVISED + parseFloat(pageQuery.data.properties.geo["2"]), + // @ts-ignore NEEDS FIXING WHEN GEO REVISED + parseFloat(pageQuery.data.properties.geo["1"]), + // @ts-ignore NEEDS FIXING WHEN GEO REVISED + parseFloat(pageQuery.data.properties.geo?.zoom ?? "13"), + ); + } else { + // Page has no geo data, open in fullscreen mode + setFullscreen(true); + } } - }, [pageQuery.data, moveTo]); + }, [pageQuery.data, moveTo, setFullscreen]); return ; } diff --git a/app/hooks/use-is-fullscreen.tsx b/app/hooks/use-is-fullscreen.tsx index 27e90d8..e093e07 100644 --- a/app/hooks/use-is-fullscreen.tsx +++ b/app/hooks/use-is-fullscreen.tsx @@ -1,19 +1,31 @@ import { createContext, ReactNode, useContext } from "react"; -const FullScreenContext = createContext(false); +type FullScreenContextValue = { + fullscreen: boolean; + setFullscreen: (value: boolean) => void; +}; + +const FullScreenContext = createContext({ + fullscreen: false, + setFullscreen: () => {}, +}); export function FullScreenProvider({ fullscreen, + setFullscreen, children, }: { fullscreen: boolean; + setFullscreen: (value: boolean) => void; children: ReactNode; }) { return ( - + {children} ); } -export const useIsFullscreen = () => useContext(FullScreenContext); +export const useIsFullscreen = () => useContext(FullScreenContext).fullscreen; +export const useSetFullscreen = () => + useContext(FullScreenContext).setFullscreen; From 70ad280ed78fdb423f2b9cbd2fa6b1df1b4d7e8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Oct 2025 07:46:47 +0000 Subject: [PATCH 3/7] Reset fullscreen when navigating to page with geo data Co-authored-by: bcye <29666239+bcye@users.noreply.github.com> --- app/app/main/explore/page/[pageId]/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/app/main/explore/page/[pageId]/index.tsx b/app/app/main/explore/page/[pageId]/index.tsx index d6d8f3f..f6c052c 100644 --- a/app/app/main/explore/page/[pageId]/index.tsx +++ b/app/app/main/explore/page/[pageId]/index.tsx @@ -20,7 +20,7 @@ export default function Page() { pageQuery.data.properties.geo["2"]; if (hasGeo) { - // Page has geo data, zoom to coordinates + // Page has geo data, zoom to coordinates and exit fullscreen moveTo( // @ts-ignore NEEDS FIXING WHEN GEO REVISED parseFloat(pageQuery.data.properties.geo["2"]), @@ -29,6 +29,7 @@ export default function Page() { // @ts-ignore NEEDS FIXING WHEN GEO REVISED parseFloat(pageQuery.data.properties.geo?.zoom ?? "13"), ); + setFullscreen(false); } else { // Page has no geo data, open in fullscreen mode setFullscreen(true); From 4c4556adfba9ca2a90deaefff683854d5a92e060 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Oct 2025 07:48:18 +0000 Subject: [PATCH 4/7] Optimize fullscreen state updates to avoid unnecessary re-renders Co-authored-by: bcye <29666239+bcye@users.noreply.github.com> --- app/app/main/explore/page/[pageId]/index.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/app/main/explore/page/[pageId]/index.tsx b/app/app/main/explore/page/[pageId]/index.tsx index f6c052c..e966ea0 100644 --- a/app/app/main/explore/page/[pageId]/index.tsx +++ b/app/app/main/explore/page/[pageId]/index.tsx @@ -1,6 +1,6 @@ import useMoveTo from "@/hooks/use-move-to"; import useWikiQuery from "@/hooks/use-wiki-query"; -import { useSetFullscreen } from "@/hooks/use-is-fullscreen"; +import { useIsFullscreen, useSetFullscreen } from "@/hooks/use-is-fullscreen"; import { useLocalSearchParams } from "expo-router"; import { useEffect } from "react"; import PageRootView from "./_page-root-view"; @@ -10,6 +10,7 @@ export default function Page() { pageId = typeof pageId === "string" ? pageId : pageId[0]; const pageQuery = useWikiQuery(pageId); const moveTo = useMoveTo(); + const isFullscreen = useIsFullscreen(); const setFullscreen = useSetFullscreen(); useEffect(() => { @@ -20,7 +21,7 @@ export default function Page() { pageQuery.data.properties.geo["2"]; if (hasGeo) { - // Page has geo data, zoom to coordinates and exit fullscreen + // Page has geo data, zoom to coordinates and exit fullscreen if needed moveTo( // @ts-ignore NEEDS FIXING WHEN GEO REVISED parseFloat(pageQuery.data.properties.geo["2"]), @@ -29,13 +30,17 @@ export default function Page() { // @ts-ignore NEEDS FIXING WHEN GEO REVISED parseFloat(pageQuery.data.properties.geo?.zoom ?? "13"), ); - setFullscreen(false); + if (isFullscreen) { + setFullscreen(false); + } } else { - // Page has no geo data, open in fullscreen mode - setFullscreen(true); + // Page has no geo data, open in fullscreen mode if needed + if (!isFullscreen) { + setFullscreen(true); + } } } - }, [pageQuery.data, moveTo, setFullscreen]); + }, [pageQuery.data, moveTo, isFullscreen, setFullscreen]); return ; } From 11e80ad36b4aa95caed3dd83033264ca2e60a933 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Oct 2025 07:49:52 +0000 Subject: [PATCH 5/7] Refactor fullscreen update logic into helper function Co-authored-by: bcye <29666239+bcye@users.noreply.github.com> --- app/app/main/explore/page/[pageId]/index.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/app/main/explore/page/[pageId]/index.tsx b/app/app/main/explore/page/[pageId]/index.tsx index e966ea0..792dfa6 100644 --- a/app/app/main/explore/page/[pageId]/index.tsx +++ b/app/app/main/explore/page/[pageId]/index.tsx @@ -20,8 +20,14 @@ export default function Page() { pageQuery.data.properties.geo["1"] && pageQuery.data.properties.geo["2"]; + const updateFullscreenIfNeeded = (shouldBeFullscreen: boolean) => { + if (isFullscreen !== shouldBeFullscreen) { + setFullscreen(shouldBeFullscreen); + } + }; + if (hasGeo) { - // Page has geo data, zoom to coordinates and exit fullscreen if needed + // Page has geo data, zoom to coordinates and exit fullscreen mode moveTo( // @ts-ignore NEEDS FIXING WHEN GEO REVISED parseFloat(pageQuery.data.properties.geo["2"]), @@ -30,14 +36,10 @@ export default function Page() { // @ts-ignore NEEDS FIXING WHEN GEO REVISED parseFloat(pageQuery.data.properties.geo?.zoom ?? "13"), ); - if (isFullscreen) { - setFullscreen(false); - } + updateFullscreenIfNeeded(false); } else { - // Page has no geo data, open in fullscreen mode if needed - if (!isFullscreen) { - setFullscreen(true); - } + // Page has no geo data, open in fullscreen mode + updateFullscreenIfNeeded(true); } } }, [pageQuery.data, moveTo, isFullscreen, setFullscreen]); From 0673701398db2e1f62cb416a6c33d8e6a1c1d34b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Oct 2025 08:57:06 +0000 Subject: [PATCH 6/7] Refactor to use separate fullscreen route instead of state-based fullscreen Co-authored-by: bcye <29666239+bcye@users.noreply.github.com> --- .../page-fullscreen/[pageId]/_layout.tsx | 9 ++ .../[pageId]/_page-root-view.tsx | 118 ++++++++++++++++++ .../page-fullscreen/[pageId]/index.tsx | 15 +++ .../[pageId]/section/[title].tsx | 96 ++++++++++++++ .../main/explore/page/[pageId]/_layout.tsx | 2 +- app/app/main/explore/page/[pageId]/index.tsx | 30 +++-- app/hooks/use-is-fullscreen.tsx | 18 +-- 7 files changed, 256 insertions(+), 32 deletions(-) create mode 100644 app/app/main/explore/page-fullscreen/[pageId]/_layout.tsx create mode 100644 app/app/main/explore/page-fullscreen/[pageId]/_page-root-view.tsx create mode 100644 app/app/main/explore/page-fullscreen/[pageId]/index.tsx create mode 100644 app/app/main/explore/page-fullscreen/[pageId]/section/[title].tsx diff --git a/app/app/main/explore/page-fullscreen/[pageId]/_layout.tsx b/app/app/main/explore/page-fullscreen/[pageId]/_layout.tsx new file mode 100644 index 0000000..93e53c7 --- /dev/null +++ b/app/app/main/explore/page-fullscreen/[pageId]/_layout.tsx @@ -0,0 +1,9 @@ +import { Stack } from "expo-router"; + +/** + * Layout for fullscreen page view (no map). + * Used for pages that don't have geographic coordinates. + */ +export default function FullscreenPageLayout() { + return ; +} diff --git a/app/app/main/explore/page-fullscreen/[pageId]/_page-root-view.tsx b/app/app/main/explore/page-fullscreen/[pageId]/_page-root-view.tsx new file mode 100644 index 0000000..aa11712 --- /dev/null +++ b/app/app/main/explore/page-fullscreen/[pageId]/_page-root-view.tsx @@ -0,0 +1,118 @@ +import { getCityAtom } from "@/utils/bookmarks"; +import { MapMarker, MarkerType, useMapStore } from "@/utils/store"; +import { NodeType, RootNode } from "@bcye/structured-wikivoyage-types"; +import { UseQueryResult } from "@tanstack/react-query"; +import { Link, Route, Stack } from "expo-router"; +import { useAtomValue } from "jotai/react"; +import { filter, map, split, splitEvery } from "ramda"; +import { useEffect } from "react"; +import { ScrollView } from "react-native"; +import { Card, SkeletonView, Text, View } from "react-native-ui-lib"; + +function PageContent({ + pageQuery, + id, +}: { + pageQuery: UseQueryResult; + id: string; +}) { + const bookmarks = useAtomValue(getCityAtom(id)); + const registerMarker = useMapStore((s) => s.registerMarker); + const deregisterMarker = useMapStore((s) => s.deregisterMarker); + + useEffect( + function registerBookmarks() { + const markers: MapMarker[] = []; + for (const [bId, bookmark] of Object.entries(bookmarks)) { + const [lat, long] = map(parseFloat, split(",", bId)); + const marker: MapMarker = { + id: bId, + link: `/main/explore/page-fullscreen/${id}/section/${bookmark.section}` as Route, + lat, + long, + type: MarkerType.Bookmark, + }; + markers.push(marker); + registerMarker(marker); + } + + return () => { + for (const marker of markers) { + deregisterMarker(marker); + } + }; + }, + [bookmarks, id, registerMarker, deregisterMarker], + ); + + return map( + ([item1, item2]) => ( + + + {item2 && } + + ), + splitEvery( + 2, + filter((c) => c.type === NodeType.Section, pageQuery.data!.children), + ), + ); +} + +export default function PageRootView({ + pageQuery, + id, +}: { + pageQuery: UseQueryResult; + id: string | null; +}) { + return ( + + + + pageQuery.error ? ( + + A network error occured and the place information could not be + loaded. + + ) : pageQuery.data && id ? ( + + + + ) : null + } + /> + + ); +} + +/** + * Renders a clickable infocard that links to a specific page section. + * Links to the fullscreen section route. + */ +function Infocard({ title, pageId }: { title: string; pageId: string }) { + return ( + + + + + + ); +} diff --git a/app/app/main/explore/page-fullscreen/[pageId]/index.tsx b/app/app/main/explore/page-fullscreen/[pageId]/index.tsx new file mode 100644 index 0000000..9c96a68 --- /dev/null +++ b/app/app/main/explore/page-fullscreen/[pageId]/index.tsx @@ -0,0 +1,15 @@ +import useWikiQuery from "@/hooks/use-wiki-query"; +import { useLocalSearchParams } from "expo-router"; +import PageRootView from "./_page-root-view"; + +/** + * Fullscreen page view for pages without geographic data. + * This route displays page content without the map layout. + */ +export default function FullscreenPage() { + let { pageId } = useLocalSearchParams(); + pageId = typeof pageId === "string" ? pageId : pageId[0]; + const pageQuery = useWikiQuery(pageId); + + return ; +} diff --git a/app/app/main/explore/page-fullscreen/[pageId]/section/[title].tsx b/app/app/main/explore/page-fullscreen/[pageId]/section/[title].tsx new file mode 100644 index 0000000..a62a16c --- /dev/null +++ b/app/app/main/explore/page-fullscreen/[pageId]/section/[title].tsx @@ -0,0 +1,96 @@ +import WikiContent from "@/components/render-node"; +import { useScrollRef } from "@/hooks/use-scroll-ref"; +import useWikiQuery from "@/hooks/use-wiki-query"; +import { citiesAtom, getCityAtom } from "@/utils/bookmarks"; +import { NodeType, SectionNode } from "@bcye/structured-wikivoyage-types"; +import { Stack, useLocalSearchParams } from "expo-router"; +import { useAtom } from "jotai/react"; +import { append, assoc, dissoc } from "ramda"; +import { useCallback } from "react"; +import { ScrollView } from "react-native"; +import { SkeletonView, View } from "react-native-ui-lib"; +import { toast } from "sonner-native"; + +/** + * Renders a specific section of a Wikipedia page in fullscreen mode. + * This is used for pages without geographic data. + */ +export default function FullscreenSection() { + const { title, pageId } = useLocalSearchParams(); + const wikiQuery = useWikiQuery(pageId as string); + const section = wikiQuery.data?.children.find( + (c) => c.type === NodeType.Section && c.properties.title === title, + ) as SectionNode | undefined; + const ref = useScrollRef(); + const [bookmarks, setBookmarks] = useAtom(getCityAtom(pageId as string)); + const [cities, setCities] = useAtom(citiesAtom); + const pageTitle = wikiQuery.data?.properties.title; + + const isBookmarked = useCallback( + function isBookmarked(id: string) { + return !!bookmarks[id]; + }, + [bookmarks], + ); + + const toggleBookmarked = useCallback( + function toggleBookmarked(id: string) { + if (id == ",") { + toast.error( + "This listing doesn't have a location and can't be bookmarked. Add one on en.wikivoyage.org", + ); + return; + } + + if (isBookmarked(id)) { + setBookmarks(dissoc(id, bookmarks)); + } else { + if (!cities.find((c) => c.qid === pageId)) { + setCities( + append({ qid: pageId as string, name: pageTitle! }, cities), + ); + } + setBookmarks( + assoc( + id, + { section: title, properties: section!.properties }, + bookmarks, + ), + ); + } + }, + [ + bookmarks, + setBookmarks, + section, + isBookmarked, + pageTitle, + cities, + setCities, + pageId, + title, + ], + ); + + if (!section) return null; + + return ( + + + ( + + + + )} + /> + + ); +} diff --git a/app/app/main/explore/page/[pageId]/_layout.tsx b/app/app/main/explore/page/[pageId]/_layout.tsx index 4fb0c08..eb5258e 100644 --- a/app/app/main/explore/page/[pageId]/_layout.tsx +++ b/app/app/main/explore/page/[pageId]/_layout.tsx @@ -52,7 +52,7 @@ export default function RootLayout() { ); return ( - + {!fullscreen ? {stack} : stack} diff --git a/app/app/main/explore/page/[pageId]/index.tsx b/app/app/main/explore/page/[pageId]/index.tsx index 792dfa6..bb76eff 100644 --- a/app/app/main/explore/page/[pageId]/index.tsx +++ b/app/app/main/explore/page/[pageId]/index.tsx @@ -1,7 +1,6 @@ import useMoveTo from "@/hooks/use-move-to"; import useWikiQuery from "@/hooks/use-wiki-query"; -import { useIsFullscreen, useSetFullscreen } from "@/hooks/use-is-fullscreen"; -import { useLocalSearchParams } from "expo-router"; +import { Redirect, useLocalSearchParams } from "expo-router"; import { useEffect } from "react"; import PageRootView from "./_page-root-view"; @@ -10,8 +9,6 @@ export default function Page() { pageId = typeof pageId === "string" ? pageId : pageId[0]; const pageQuery = useWikiQuery(pageId); const moveTo = useMoveTo(); - const isFullscreen = useIsFullscreen(); - const setFullscreen = useSetFullscreen(); useEffect(() => { if (pageQuery.data) { @@ -20,14 +17,7 @@ export default function Page() { pageQuery.data.properties.geo["1"] && pageQuery.data.properties.geo["2"]; - const updateFullscreenIfNeeded = (shouldBeFullscreen: boolean) => { - if (isFullscreen !== shouldBeFullscreen) { - setFullscreen(shouldBeFullscreen); - } - }; - if (hasGeo) { - // Page has geo data, zoom to coordinates and exit fullscreen mode moveTo( // @ts-ignore NEEDS FIXING WHEN GEO REVISED parseFloat(pageQuery.data.properties.geo["2"]), @@ -36,13 +26,21 @@ export default function Page() { // @ts-ignore NEEDS FIXING WHEN GEO REVISED parseFloat(pageQuery.data.properties.geo?.zoom ?? "13"), ); - updateFullscreenIfNeeded(false); - } else { - // Page has no geo data, open in fullscreen mode - updateFullscreenIfNeeded(true); } } - }, [pageQuery.data, moveTo, isFullscreen, setFullscreen]); + }, [pageQuery.data, moveTo]); + + // Redirect to fullscreen route if page has no geo data + if (pageQuery.data) { + const hasGeo = + pageQuery.data.properties.geo && + pageQuery.data.properties.geo["1"] && + pageQuery.data.properties.geo["2"]; + + if (!hasGeo) { + return ; + } + } return ; } diff --git a/app/hooks/use-is-fullscreen.tsx b/app/hooks/use-is-fullscreen.tsx index e093e07..27e90d8 100644 --- a/app/hooks/use-is-fullscreen.tsx +++ b/app/hooks/use-is-fullscreen.tsx @@ -1,31 +1,19 @@ import { createContext, ReactNode, useContext } from "react"; -type FullScreenContextValue = { - fullscreen: boolean; - setFullscreen: (value: boolean) => void; -}; - -const FullScreenContext = createContext({ - fullscreen: false, - setFullscreen: () => {}, -}); +const FullScreenContext = createContext(false); export function FullScreenProvider({ fullscreen, - setFullscreen, children, }: { fullscreen: boolean; - setFullscreen: (value: boolean) => void; children: ReactNode; }) { return ( - + {children} ); } -export const useIsFullscreen = () => useContext(FullScreenContext).fullscreen; -export const useSetFullscreen = () => - useContext(FullScreenContext).setFullscreen; +export const useIsFullscreen = () => useContext(FullScreenContext); From d137bcd858d787e1575986d8f9eb0b040fc1d660 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:08:51 +0000 Subject: [PATCH 7/7] Refactor to eliminate code duplication using shared components Co-authored-by: bcye <29666239+bcye@users.noreply.github.com> --- .../page-content.tsx} | 75 +++++------ .../main/explore/_shared/scroll-container.tsx | 26 ++++ .../explore/_shared/use-section-bookmarks.ts | 65 ++++++++++ .../page-fullscreen/[pageId]/_layout.tsx | 10 +- .../page-fullscreen/[pageId]/index.tsx | 47 ++++++- .../[pageId]/section/[title].tsx | 64 ++-------- .../explore/page/[pageId]/_page-root-view.tsx | 116 ++---------------- .../explore/page/[pageId]/section/[title].tsx | 100 +++------------ 8 files changed, 214 insertions(+), 289 deletions(-) rename app/app/main/explore/{page-fullscreen/[pageId]/_page-root-view.tsx => _shared/page-content.tsx} (55%) create mode 100644 app/app/main/explore/_shared/scroll-container.tsx create mode 100644 app/app/main/explore/_shared/use-section-bookmarks.ts diff --git a/app/app/main/explore/page-fullscreen/[pageId]/_page-root-view.tsx b/app/app/main/explore/_shared/page-content.tsx similarity index 55% rename from app/app/main/explore/page-fullscreen/[pageId]/_page-root-view.tsx rename to app/app/main/explore/_shared/page-content.tsx index aa11712..f61ed57 100644 --- a/app/app/main/explore/page-fullscreen/[pageId]/_page-root-view.tsx +++ b/app/app/main/explore/_shared/page-content.tsx @@ -2,19 +2,23 @@ import { getCityAtom } from "@/utils/bookmarks"; import { MapMarker, MarkerType, useMapStore } from "@/utils/store"; import { NodeType, RootNode } from "@bcye/structured-wikivoyage-types"; import { UseQueryResult } from "@tanstack/react-query"; -import { Link, Route, Stack } from "expo-router"; +import { Link, Route } from "expo-router"; import { useAtomValue } from "jotai/react"; import { filter, map, split, splitEvery } from "ramda"; import { useEffect } from "react"; -import { ScrollView } from "react-native"; -import { Card, SkeletonView, Text, View } from "react-native-ui-lib"; +import { Card, View } from "react-native-ui-lib"; -function PageContent({ +/** + * Shared page content component that displays section cards and registers bookmark markers. + */ +export function PageContent({ pageQuery, id, + basePath, }: { pageQuery: UseQueryResult; id: string; + basePath: string; }) { const bookmarks = useAtomValue(getCityAtom(id)); const registerMarker = useMapStore((s) => s.registerMarker); @@ -27,7 +31,7 @@ function PageContent({ const [lat, long] = map(parseFloat, split(",", bId)); const marker: MapMarker = { id: bId, - link: `/main/explore/page-fullscreen/${id}/section/${bookmark.section}` as Route, + link: `${basePath}/${id}/section/${bookmark.section}` as Route, lat, long, type: MarkerType.Bookmark, @@ -42,7 +46,7 @@ function PageContent({ } }; }, - [bookmarks, id, registerMarker, deregisterMarker], + [bookmarks, id, registerMarker, deregisterMarker, basePath], ); return map( @@ -54,8 +58,18 @@ function PageContent({ marginB-8 key={item1.properties.title + item2?.properties.title} > - - {item2 && } + + {item2 && ( + + )} ), splitEvery( @@ -65,48 +79,23 @@ function PageContent({ ); } -export default function PageRootView({ - pageQuery, - id, -}: { - pageQuery: UseQueryResult; - id: string | null; -}) { - return ( - - - - pageQuery.error ? ( - - A network error occured and the place information could not be - loaded. - - ) : pageQuery.data && id ? ( - - - - ) : null - } - /> - - ); -} - /** * Renders a clickable infocard that links to a specific page section. - * Links to the fullscreen section route. */ -function Infocard({ title, pageId }: { title: string; pageId: string }) { +function Infocard({ + title, + pageId, + basePath, +}: { + title: string; + pageId: string; + basePath: string; +}) { return ( diff --git a/app/app/main/explore/_shared/scroll-container.tsx b/app/app/main/explore/_shared/scroll-container.tsx new file mode 100644 index 0000000..0c9ced0 --- /dev/null +++ b/app/app/main/explore/_shared/scroll-container.tsx @@ -0,0 +1,26 @@ +import { useIsFullscreen } from "@/hooks/use-is-fullscreen"; +import { BottomSheetScrollView } from "@gorhom/bottom-sheet"; +import { ReactNode } from "react"; +import { ScrollView } from "react-native"; + +/** + * Shared scroll container that uses BottomSheetScrollView when not fullscreen, + * and regular ScrollView when fullscreen. + */ +export function ScrollContainer({ + children, + scrollRef, +}: { + children: ReactNode; + scrollRef?: React.Ref; +}) { + const isFullscreen = useIsFullscreen(); + + if (isFullscreen) { + return {children}; + } + + return ( + {children} + ); +} diff --git a/app/app/main/explore/_shared/use-section-bookmarks.ts b/app/app/main/explore/_shared/use-section-bookmarks.ts new file mode 100644 index 0000000..180cdc7 --- /dev/null +++ b/app/app/main/explore/_shared/use-section-bookmarks.ts @@ -0,0 +1,65 @@ +import { citiesAtom, getCityAtom } from "@/utils/bookmarks"; +import { SectionNode } from "@bcye/structured-wikivoyage-types"; +import { useAtom } from "jotai/react"; +import { append, assoc, dissoc } from "ramda"; +import { useCallback } from "react"; +import { toast } from "sonner-native"; + +/** + * Shared hook for managing section bookmarks + */ +export function useSectionBookmarks( + pageId: string, + title: string | string[], + section: SectionNode | undefined, + pageTitle: string | undefined, +) { + const [bookmarks, setBookmarks] = useAtom(getCityAtom(pageId)); + const [cities, setCities] = useAtom(citiesAtom); + + const isBookmarked = useCallback( + function isBookmarked(id: string) { + return !!bookmarks[id]; + }, + [bookmarks], + ); + + const toggleBookmarked = useCallback( + function toggleBookmarked(id: string) { + if (id == ",") { + toast.error( + "This listing doesn't have a location and can't be bookmarked. Add one on en.wikivoyage.org", + ); + return; + } + + if (isBookmarked(id)) { + setBookmarks(dissoc(id, bookmarks)); + } else { + if (!cities.find((c) => c.qid === pageId)) { + setCities(append({ qid: pageId, name: pageTitle! }, cities)); + } + setBookmarks( + assoc( + id, + { section: title, properties: section!.properties }, + bookmarks, + ), + ); + } + }, + [ + bookmarks, + setBookmarks, + section, + isBookmarked, + pageTitle, + cities, + setCities, + pageId, + title, + ], + ); + + return { isBookmarked, toggleBookmarked }; +} diff --git a/app/app/main/explore/page-fullscreen/[pageId]/_layout.tsx b/app/app/main/explore/page-fullscreen/[pageId]/_layout.tsx index 93e53c7..a9dbc3a 100644 --- a/app/app/main/explore/page-fullscreen/[pageId]/_layout.tsx +++ b/app/app/main/explore/page-fullscreen/[pageId]/_layout.tsx @@ -1,3 +1,5 @@ +import { FullScreenProvider } from "@/hooks/use-is-fullscreen"; +import { ScrollRefProvider } from "@/hooks/use-scroll-ref"; import { Stack } from "expo-router"; /** @@ -5,5 +7,11 @@ import { Stack } from "expo-router"; * Used for pages that don't have geographic coordinates. */ export default function FullscreenPageLayout() { - return ; + return ( + + + + + + ); } diff --git a/app/app/main/explore/page-fullscreen/[pageId]/index.tsx b/app/app/main/explore/page-fullscreen/[pageId]/index.tsx index 9c96a68..54449b8 100644 --- a/app/app/main/explore/page-fullscreen/[pageId]/index.tsx +++ b/app/app/main/explore/page-fullscreen/[pageId]/index.tsx @@ -1,15 +1,54 @@ import useWikiQuery from "@/hooks/use-wiki-query"; -import { useLocalSearchParams } from "expo-router"; -import PageRootView from "./_page-root-view"; +import { RootNode } from "@bcye/structured-wikivoyage-types"; +import { UseQueryResult } from "@tanstack/react-query"; +import { Stack, useLocalSearchParams } from "expo-router"; +import { PageContent } from "../../_shared/page-content"; +import { ScrollContainer } from "../../_shared/scroll-container"; +import { SkeletonView, Text, View } from "react-native-ui-lib"; /** * Fullscreen page view for pages without geographic data. - * This route displays page content without the map layout. */ export default function FullscreenPage() { let { pageId } = useLocalSearchParams(); pageId = typeof pageId === "string" ? pageId : pageId[0]; const pageQuery = useWikiQuery(pageId); - return ; + return ; +} + +function FullscreenPageRootView({ + pageQuery, + id, +}: { + pageQuery: UseQueryResult; + id: string | null; +}) { + return ( + + + + pageQuery.error ? ( + + A network error occured and the place information could not be + loaded. + + ) : pageQuery.data && id ? ( + + + + ) : null + } + /> + + ); } diff --git a/app/app/main/explore/page-fullscreen/[pageId]/section/[title].tsx b/app/app/main/explore/page-fullscreen/[pageId]/section/[title].tsx index a62a16c..789494a 100644 --- a/app/app/main/explore/page-fullscreen/[pageId]/section/[title].tsx +++ b/app/app/main/explore/page-fullscreen/[pageId]/section/[title].tsx @@ -1,19 +1,14 @@ import WikiContent from "@/components/render-node"; import { useScrollRef } from "@/hooks/use-scroll-ref"; import useWikiQuery from "@/hooks/use-wiki-query"; -import { citiesAtom, getCityAtom } from "@/utils/bookmarks"; import { NodeType, SectionNode } from "@bcye/structured-wikivoyage-types"; import { Stack, useLocalSearchParams } from "expo-router"; -import { useAtom } from "jotai/react"; -import { append, assoc, dissoc } from "ramda"; -import { useCallback } from "react"; -import { ScrollView } from "react-native"; +import { ScrollContainer } from "../../../_shared/scroll-container"; +import { useSectionBookmarks } from "../../../_shared/use-section-bookmarks"; import { SkeletonView, View } from "react-native-ui-lib"; -import { toast } from "sonner-native"; /** * Renders a specific section of a Wikipedia page in fullscreen mode. - * This is used for pages without geographic data. */ export default function FullscreenSection() { const { title, pageId } = useLocalSearchParams(); @@ -22,54 +17,13 @@ export default function FullscreenSection() { (c) => c.type === NodeType.Section && c.properties.title === title, ) as SectionNode | undefined; const ref = useScrollRef(); - const [bookmarks, setBookmarks] = useAtom(getCityAtom(pageId as string)); - const [cities, setCities] = useAtom(citiesAtom); const pageTitle = wikiQuery.data?.properties.title; - const isBookmarked = useCallback( - function isBookmarked(id: string) { - return !!bookmarks[id]; - }, - [bookmarks], - ); - - const toggleBookmarked = useCallback( - function toggleBookmarked(id: string) { - if (id == ",") { - toast.error( - "This listing doesn't have a location and can't be bookmarked. Add one on en.wikivoyage.org", - ); - return; - } - - if (isBookmarked(id)) { - setBookmarks(dissoc(id, bookmarks)); - } else { - if (!cities.find((c) => c.qid === pageId)) { - setCities( - append({ qid: pageId as string, name: pageTitle! }, cities), - ); - } - setBookmarks( - assoc( - id, - { section: title, properties: section!.properties }, - bookmarks, - ), - ); - } - }, - [ - bookmarks, - setBookmarks, - section, - isBookmarked, - pageTitle, - cities, - setCities, - pageId, - title, - ], + const { isBookmarked, toggleBookmarked } = useSectionBookmarks( + pageId as string, + title, + section, + pageTitle, ); if (!section) return null; @@ -81,14 +35,14 @@ export default function FullscreenSection() { template={SkeletonView.templates.TEXT_CONTENT} showContent={wikiQuery.isSuccess} renderContent={() => ( - + - + )} /> diff --git a/app/app/main/explore/page/[pageId]/_page-root-view.tsx b/app/app/main/explore/page/[pageId]/_page-root-view.tsx index ae5b057..b2f7580 100644 --- a/app/app/main/explore/page/[pageId]/_page-root-view.tsx +++ b/app/app/main/explore/page/[pageId]/_page-root-view.tsx @@ -1,72 +1,9 @@ -import { useIsFullscreen } from "@/hooks/use-is-fullscreen"; -import { getCityAtom } from "@/utils/bookmarks"; -import { MapMarker, MarkerType, useMapStore } from "@/utils/store"; -import { NodeType, RootNode } from "@bcye/structured-wikivoyage-types"; -import { BottomSheetScrollView } from "@gorhom/bottom-sheet"; +import { RootNode } from "@bcye/structured-wikivoyage-types"; import { UseQueryResult } from "@tanstack/react-query"; -import { Link, Route, Stack } from "expo-router"; -import { useAtomValue } from "jotai/react"; -import { filter, map, split, splitEvery } from "ramda"; -import { useEffect } from "react"; -import { ScrollView } from "react-native"; -import { Card, SkeletonView, Text, View } from "react-native-ui-lib"; - -function PageContent({ - pageQuery, - id, -}: { - pageQuery: UseQueryResult; - id: string; -}) { - const bookmarks = useAtomValue(getCityAtom(id)); - const registerMarker = useMapStore((s) => s.registerMarker); - const deregisterMarker = useMapStore((s) => s.deregisterMarker); - - useEffect( - function registerBookmarks() { - const markers: MapMarker[] = []; - for (const [bId, bookmark] of Object.entries(bookmarks)) { - const [lat, long] = map(parseFloat, split(",", bId)); - const marker: MapMarker = { - id: bId, - // somehow broken else - link: `/main/explore/page/${id}/section/${bookmark.section}` as Route, - lat, - long, - type: MarkerType.Bookmark, - }; - markers.push(marker); - registerMarker(marker); - } - - return () => { - for (const marker of markers) { - deregisterMarker(marker); - } - }; - }, - [bookmarks, id, registerMarker, deregisterMarker], - ); - - return map( - ([item1, item2]) => ( - - - {item2 && } - - ), - splitEvery( - 2, - filter((c) => c.type === NodeType.Section, pageQuery.data!.children), - ), - ); -} +import { Stack } from "expo-router"; +import { PageContent } from "../../_shared/page-content"; +import { ScrollContainer } from "../../_shared/scroll-container"; +import { SkeletonView, Text, View } from "react-native-ui-lib"; export default function PageRootView({ pageQuery, @@ -75,8 +12,6 @@ export default function PageRootView({ pageQuery: UseQueryResult; id: string | null; }) { - const isFullscreen = useIsFullscreen(); - return ( ) : pageQuery.data && id ? ( - !isFullscreen ? ( - - - - ) : ( - - - - ) + + + ) : null } /> ); } - -/** - * Renders a clickable infocard that links to a specific page section. - * - * This component creates a card-based link that navigates to a dynamic route structured as "/page/[pageId]/section/[title]". - * The card displays the provided title, offering a concise navigational element within the app. - * - * @param title - The title displayed on the card and used as part of the destination route. - * @param pageId - The identifier for the page, used to construct the dynamic navigation route. - */ -function Infocard({ title, pageId }: { title: string; pageId: string }) { - return ( - - - - - - ); -} diff --git a/app/app/main/explore/page/[pageId]/section/[title].tsx b/app/app/main/explore/page/[pageId]/section/[title].tsx index 7605b90..ccf8dd8 100644 --- a/app/app/main/explore/page/[pageId]/section/[title].tsx +++ b/app/app/main/explore/page/[pageId]/section/[title].tsx @@ -1,24 +1,14 @@ import WikiContent from "@/components/render-node"; -import { useIsFullscreen } from "@/hooks/use-is-fullscreen"; import { useScrollRef } from "@/hooks/use-scroll-ref"; import useWikiQuery from "@/hooks/use-wiki-query"; -import { citiesAtom, getCityAtom } from "@/utils/bookmarks"; import { NodeType, SectionNode } from "@bcye/structured-wikivoyage-types"; -import { BottomSheetScrollView } from "@gorhom/bottom-sheet"; import { Stack, useLocalSearchParams } from "expo-router"; -import { useAtom } from "jotai/react"; -import { append, assoc, dissoc } from "ramda"; -import { useCallback } from "react"; -import { ScrollView } from "react-native"; +import { ScrollContainer } from "../../../_shared/scroll-container"; +import { useSectionBookmarks } from "../../../_shared/use-section-bookmarks"; import { SkeletonView, View } from "react-native-ui-lib"; -import { toast } from "sonner-native"; /** - * Renders a specific section of a Wikipedia page in Markdown format. - * - * The component retrieves the page title and ID from local search parameters, fetches the page's wikitext - * via a TRPC query, and then extracts and converts the designated section—identified by the title—to Markdown. - * It slices off the section header from the converted Markdown and displays a skeleton view while the content loads. + * Renders a specific section of a Wikipedia page. */ export default function Section() { const { title, pageId } = useLocalSearchParams(); @@ -27,56 +17,13 @@ export default function Section() { (c) => c.type === NodeType.Section && c.properties.title === title, ) as SectionNode | undefined; const ref = useScrollRef(); - const isFullscreen = useIsFullscreen(); - const [bookmarks, setBookmarks] = useAtom(getCityAtom(pageId as string)); - const [cities, setCities] = useAtom(citiesAtom); const pageTitle = wikiQuery.data?.properties.title; - const isBookmarked = useCallback( - function isBookmarked(id: string) { - return !!bookmarks[id]; - }, - [bookmarks], - ); - - const toggleBookmarked = useCallback( - function toggleBookmarked(id: string) { - if (id == ",") { - toast.error( - "This listing doesn't have a location and can't be bookmarked. Add one on en.wikivoyage.org", - ); - return; - } - - if (isBookmarked(id)) { - setBookmarks(dissoc(id, bookmarks)); - } else { - if (!cities.find((c) => c.qid === pageId)) { - setCities( - append({ qid: pageId as string, name: pageTitle! }, cities), - ); - } - // no valid lat-lng - setBookmarks( - assoc( - id, - { section: title, properties: section!.properties }, - bookmarks, - ), - ); - } - }, - [ - bookmarks, - setBookmarks, - section, - isBookmarked, - pageTitle, - cities, - setCities, - pageId, - title, - ], + const { isBookmarked, toggleBookmarked } = useSectionBookmarks( + pageId as string, + title, + section, + pageTitle, ); if (!section) return null; @@ -87,27 +34,16 @@ export default function Section() { - !isFullscreen ? ( - - - - ) : ( - - - - ) - } + renderContent={() => ( + + + + )} /> );