Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions app/app/main/explore/_shared/page-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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 } from "expo-router";
import { useAtomValue } from "jotai/react";
import { filter, map, split, splitEvery } from "ramda";
import { useEffect } from "react";
import { Card, View } from "react-native-ui-lib";

/**
* Shared page content component that displays section cards and registers bookmark markers.
*/
export function PageContent({
pageQuery,
id,
basePath,
}: {
pageQuery: UseQueryResult<RootNode, Error>;
id: string;
basePath: 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: `${basePath}/${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, basePath],
);

return map(
([item1, item2]) => (
<View
flex
row
gap-8
marginB-8
key={item1.properties.title + item2?.properties.title}
>
<Infocard
title={item1.properties.title}
pageId={id!}
basePath={basePath}
/>
{item2 && (
<Infocard
title={item2.properties.title}
pageId={id!}
basePath={basePath}
/>
)}
</View>
),
splitEvery(
2,
filter((c) => c.type === NodeType.Section, pageQuery.data!.children),
),
);
}

/**
* Renders a clickable infocard that links to a specific page section.
*/
function Infocard({
title,
pageId,
basePath,
}: {
title: string;
pageId: string;
basePath: string;
}) {
return (
<Link
asChild
href={{
pathname: `${basePath}/[pageId]/section/[title]`,
params: { pageId, title },
}}
>
<Card flex padding-12 height={48}>
<Card.Section content={[{ text: title, text60: true, grey10: true }]} />
</Card>
</Link>
);
}
26 changes: 26 additions & 0 deletions app/app/main/explore/_shared/scroll-container.tsx
Original file line number Diff line number Diff line change
@@ -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<any>;
}) {
const isFullscreen = useIsFullscreen();

if (isFullscreen) {
return <ScrollView ref={scrollRef}>{children}</ScrollView>;
}

return (
<BottomSheetScrollView ref={scrollRef}>{children}</BottomSheetScrollView>
);
}
65 changes: 65 additions & 0 deletions app/app/main/explore/_shared/use-section-bookmarks.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
17 changes: 17 additions & 0 deletions app/app/main/explore/page-fullscreen/[pageId]/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { FullScreenProvider } from "@/hooks/use-is-fullscreen";
import { ScrollRefProvider } from "@/hooks/use-scroll-ref";
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 (
<FullScreenProvider fullscreen={true}>
<ScrollRefProvider>
<Stack />
</ScrollRefProvider>
</FullScreenProvider>
);
}
54 changes: 54 additions & 0 deletions app/app/main/explore/page-fullscreen/[pageId]/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import useWikiQuery from "@/hooks/use-wiki-query";
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.
*/
export default function FullscreenPage() {
let { pageId } = useLocalSearchParams();
pageId = typeof pageId === "string" ? pageId : pageId[0];
const pageQuery = useWikiQuery(pageId);

return <FullscreenPageRootView pageQuery={pageQuery} id={pageId} />;
}

function FullscreenPageRootView({
pageQuery,
id,
}: {
pageQuery: UseQueryResult<RootNode, Error>;
id: string | null;
}) {
return (
<View padding-8 flex>
<Stack.Screen
options={{ title: pageQuery.data?.properties.title ?? "Loading" }}
/>
<SkeletonView
template={SkeletonView.templates.LIST_ITEM}
showContent={pageQuery.isSuccess}
renderContent={() =>
pageQuery.error ? (
<Text color="red" text60>
A network error occured and the place information could not be
loaded.
</Text>
) : pageQuery.data && id ? (
<ScrollContainer>
<PageContent
id={id}
pageQuery={pageQuery}
basePath="/main/explore/page-fullscreen"
/>
</ScrollContainer>
) : null
}
/>
</View>
);
}
50 changes: 50 additions & 0 deletions app/app/main/explore/page-fullscreen/[pageId]/section/[title].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import WikiContent from "@/components/render-node";
import { useScrollRef } from "@/hooks/use-scroll-ref";
import useWikiQuery from "@/hooks/use-wiki-query";
import { NodeType, SectionNode } from "@bcye/structured-wikivoyage-types";
import { Stack, useLocalSearchParams } from "expo-router";
import { ScrollContainer } from "../../../_shared/scroll-container";
import { useSectionBookmarks } from "../../../_shared/use-section-bookmarks";
import { SkeletonView, View } from "react-native-ui-lib";

/**
* Renders a specific section of a Wikipedia page in fullscreen mode.
*/
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 pageTitle = wikiQuery.data?.properties.title;

const { isBookmarked, toggleBookmarked } = useSectionBookmarks(
pageId as string,
title,
section,
pageTitle,
);

if (!section) return null;

return (
<View padding-8 flex>
<Stack.Screen options={{ title: section.properties.title }} />
<SkeletonView
template={SkeletonView.templates.TEXT_CONTENT}
showContent={wikiQuery.isSuccess}
renderContent={() => (
<ScrollContainer scrollRef={ref}>
<WikiContent
node={section}
root={true}
isBookmarked={isBookmarked}
toggleBookmarked={toggleBookmarked}
/>
</ScrollContainer>
)}
/>
</View>
);
}
Loading