import { FC, ReactElement, useCallback, useEffect, useRef, useState } from "react" import { FlatList, Image, ImageStyle, Platform, SectionList, TextStyle, View, ViewStyle, } from "react-native" import { Link, RouteProp, useRoute } from "@react-navigation/native" import { Drawer } from "react-native-drawer-layout" import { ListItem } from "@/components/ListItem" import { Screen } from "@/components/Screen" import { Text } from "@/components/Text" import { TxKeyPath, isRTL } from "@/i18n" import { translate } from "@/i18n/translate" import { MainTabParamList, MainTabScreenProps } from "@/navigators/navigationTypes" import { useAppTheme } from "@/theme/context" import { $styles } from "@/theme/styles" import type { ThemedStyle } from "@/theme/types" import { s, fs } from "@/utils/responsive" import { useSafeAreaInsetsStyle } from "@/utils/useSafeAreaInsetsStyle" import * as Demos from "./demos" import { DrawerIconButton } from "./DrawerIconButton" import SectionListWithKeyboardAwareScrollView from "./SectionListWithKeyboardAwareScrollView" const logo = require("@assets/images/logo.png") interface DemoListItem { item: { name: string; useCases: string[] } sectionIndex: number handleScroll?: (sectionIndex: number, itemIndex?: number) => void } const slugify = (str: string) => str .toLowerCase() .trim() .replace(/[^\w\s-]/g, "") .replace(/[\s_-]+/g, "-") .replace(/^-+|-+$/g, "") /** * Type-safe utility to check if an unknown object has a valid string property. * This is particularly useful in React 19 where props are typed as unknown by default. * The function safely narrows down the type by checking both property existence and type. * @param props - The unknown props to check. * @param propName - The name of the property to check. * @returns Whether the property is a valid string. */ function hasValidStringProp(props: unknown, propName: string): boolean { return ( props !== null && typeof props === "object" && propName in props && typeof (props as Record)[propName] === "string" ) } const WebListItem: FC = ({ item, sectionIndex }) => { const sectionSlug = item.name.toLowerCase() const { themed } = useAppTheme() return ( {item.name} {item.useCases.map((u) => { const itemSlug = slugify(u) return ( {u} ) })} ) } const NativeListItem: FC = ({ item, sectionIndex, handleScroll }) => { const { themed } = useAppTheme() return ( handleScroll?.(sectionIndex)} preset="bold" style={themed($menuContainer)} > {item.name} {item.useCases.map((u, index) => ( handleScroll?.(sectionIndex, index)} text={u} rightIcon={isRTL ? "caretLeft" : "caretRight"} /> ))} ) } const ShowroomListItem = Platform.select({ web: WebListItem, default: NativeListItem }) const isAndroid = Platform.OS === "android" export const ShowroomScreen: FC> = function ShowroomScreen(_props) { const [open, setOpen] = useState(false) const timeout = useRef>(null) const listRef = useRef(null) const menuRef = useRef>(null) const route = useRoute>() const params = route.params const { themed, theme } = useAppTheme() const toggleDrawer = useCallback(() => { if (!open) { setOpen(true) } else { setOpen(false) } }, [open]) const handleScroll = useCallback((sectionIndex: number, itemIndex = 0) => { try { listRef.current?.scrollToLocation({ animated: true, itemIndex, sectionIndex, viewPosition: 0.25, }) } catch (e) { console.error(e) } }, []) // handle Web links useEffect(() => { if (params !== undefined && Object.keys(params).length > 0) { const demoValues = Object.values(Demos) const findSectionIndex = demoValues.findIndex( (x) => x.name.toLowerCase() === params.queryIndex, ) let findItemIndex = 0 if (params.itemIndex) { try { findItemIndex = demoValues[findSectionIndex].data({ themed, theme }).findIndex((u) => { if (hasValidStringProp(u.props, "name")) { return slugify(translate((u.props as { name: TxKeyPath }).name)) === params.itemIndex } return false }) } catch (err) { console.error(err) } } handleScroll(findSectionIndex, findItemIndex) } }, [handleScroll, params, theme, themed]) const scrollToIndexFailed = (info: { index: number highestMeasuredFrameIndex: number averageItemLength: number }) => { listRef.current?.getScrollResponder()?.scrollToEnd() timeout.current = setTimeout( () => listRef.current?.scrollToLocation({ animated: true, itemIndex: info.index, sectionIndex: 0, }), 50, ) } useEffect(() => { return () => { if (timeout.current) { clearTimeout(timeout.current) } } }, []) const $drawerInsets = useSafeAreaInsetsStyle(["top"]) return ( setOpen(true)} onClose={() => setOpen(false)} drawerType="back" drawerPosition={isRTL ? "right" : "left"} renderDrawerContent={() => ( ref={menuRef} contentContainerStyle={themed($listContentContainer)} data={Object.values(Demos).map((d) => ({ name: d.name, useCases: d.data({ theme, themed }).map((u) => { if (hasValidStringProp(u.props, "name")) { return translate((u.props as { name: TxKeyPath }).name) } return "" }), }))} keyExtractor={(item) => item.name} renderItem={({ item, index: sectionIndex }) => ( )} /> )} > ({ name: d.name, description: d.description, data: [d.data({ theme, themed })], }))} renderItem={({ item, index: sectionIndex }) => ( {item.map((demo: ReactElement, demoIndex: number) => ( {demo} ))} )} renderSectionFooter={() => } ListHeaderComponent={ } onScrollToIndexFailed={scrollToIndexFailed} renderSectionHeader={({ section }) => { return ( {section.name} {translate(section.description)} ) }} /> ) } const $drawer: ThemedStyle = ({ colors }) => ({ backgroundColor: colors.background, flex: 1, }) const $listContentContainer: ThemedStyle = ({ spacing }) => ({ paddingHorizontal: spacing.lg, }) const $sectionListContentContainer: ThemedStyle = ({ spacing }) => ({ paddingHorizontal: spacing.lg, }) const $heading: ThemedStyle = ({ spacing }) => ({ marginBottom: spacing.xxxl, }) const $logoImage: ImageStyle = { height: s(42), width: s(77), } const $logoContainer: ThemedStyle = ({ spacing }) => ({ alignSelf: "flex-start", justifyContent: "center", height: s(56), paddingHorizontal: spacing.lg, }) const $menuContainer: ThemedStyle = ({ spacing }) => ({ paddingBottom: spacing.xs, paddingTop: spacing.lg, }) const $demoItemName: ThemedStyle = ({ spacing }) => ({ fontSize: fs(24), marginBottom: spacing.md, }) const $demoItemDescription: ThemedStyle = ({ spacing }) => ({ marginBottom: spacing.xxl, }) const $demoUseCasesSpacer: ThemedStyle = ({ spacing }) => ({ paddingBottom: spacing.xxl, })