template_0205
This commit is contained in:
320
RN_TEMPLATE/app/screens/ShowroomScreen/ShowroomScreen.tsx
Normal file
320
RN_TEMPLATE/app/screens/ShowroomScreen/ShowroomScreen.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
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<string, unknown>)[propName] === "string"
|
||||
)
|
||||
}
|
||||
|
||||
const WebListItem: FC<DemoListItem> = ({ item, sectionIndex }) => {
|
||||
const sectionSlug = item.name.toLowerCase()
|
||||
const { themed } = useAppTheme()
|
||||
return (
|
||||
<View>
|
||||
<Link screen="Showroom" params={{ queryIndex: sectionSlug }} style={themed($menuContainer)}>
|
||||
<Text preset="bold">{item.name}</Text>
|
||||
</Link>
|
||||
{item.useCases.map((u) => {
|
||||
const itemSlug = slugify(u)
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={`section${sectionIndex}-${u}`}
|
||||
screen="Showroom"
|
||||
params={{ queryIndex: sectionSlug, itemIndex: itemSlug }}
|
||||
>
|
||||
<Text>{u}</Text>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const NativeListItem: FC<DemoListItem> = ({ item, sectionIndex, handleScroll }) => {
|
||||
const { themed } = useAppTheme()
|
||||
return (
|
||||
<View>
|
||||
<Text
|
||||
onPress={() => handleScroll?.(sectionIndex)}
|
||||
preset="bold"
|
||||
style={themed($menuContainer)}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.useCases.map((u, index) => (
|
||||
<ListItem
|
||||
key={`section${sectionIndex}-${u}`}
|
||||
onPress={() => handleScroll?.(sectionIndex, index)}
|
||||
text={u}
|
||||
rightIcon={isRTL ? "caretLeft" : "caretRight"}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const ShowroomListItem = Platform.select({ web: WebListItem, default: NativeListItem })
|
||||
const isAndroid = Platform.OS === "android"
|
||||
|
||||
export const ShowroomScreen: FC<MainTabScreenProps<"Showroom">> = function ShowroomScreen(_props) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const timeout = useRef<ReturnType<typeof setTimeout>>(null)
|
||||
const listRef = useRef<SectionList>(null)
|
||||
const menuRef = useRef<FlatList<DemoListItem["item"]>>(null)
|
||||
const route = useRoute<RouteProp<MainTabParamList, "Showroom">>()
|
||||
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 (
|
||||
<Drawer
|
||||
open={open}
|
||||
onOpen={() => setOpen(true)}
|
||||
onClose={() => setOpen(false)}
|
||||
drawerType="back"
|
||||
drawerPosition={isRTL ? "right" : "left"}
|
||||
renderDrawerContent={() => (
|
||||
<View style={themed([$drawer, $drawerInsets])}>
|
||||
<View style={themed($logoContainer)}>
|
||||
<Image source={logo} style={$logoImage} />
|
||||
</View>
|
||||
<FlatList<DemoListItem["item"]>
|
||||
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 }) => (
|
||||
<ShowroomListItem {...{ item, sectionIndex, handleScroll }} />
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
>
|
||||
<Screen
|
||||
preset="fixed"
|
||||
contentContainerStyle={$styles.flex1}
|
||||
{...(isAndroid ? { KeyboardAvoidingViewProps: { behavior: undefined } } : {})}
|
||||
>
|
||||
<DrawerIconButton onPress={toggleDrawer} />
|
||||
|
||||
<SectionListWithKeyboardAwareScrollView
|
||||
ref={listRef}
|
||||
contentContainerStyle={themed($sectionListContentContainer)}
|
||||
stickySectionHeadersEnabled={false}
|
||||
sections={Object.values(Demos).map((d) => ({
|
||||
name: d.name,
|
||||
description: d.description,
|
||||
data: [d.data({ theme, themed })],
|
||||
}))}
|
||||
renderItem={({ item, index: sectionIndex }) => (
|
||||
<View>
|
||||
{item.map((demo: ReactElement, demoIndex: number) => (
|
||||
<View key={`${sectionIndex}-${demoIndex}`}>{demo}</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
renderSectionFooter={() => <View style={themed($demoUseCasesSpacer)} />}
|
||||
ListHeaderComponent={
|
||||
<View style={themed($heading)}>
|
||||
<Text preset="heading" tx="showroomScreen:jumpStart" />
|
||||
</View>
|
||||
}
|
||||
onScrollToIndexFailed={scrollToIndexFailed}
|
||||
renderSectionHeader={({ section }) => {
|
||||
return (
|
||||
<View>
|
||||
<Text preset="heading" style={themed($demoItemName)}>
|
||||
{section.name}
|
||||
</Text>
|
||||
<Text style={themed($demoItemDescription)}>{translate(section.description)}</Text>
|
||||
</View>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Screen>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
const $drawer: ThemedStyle<ViewStyle> = ({ colors }) => ({
|
||||
backgroundColor: colors.background,
|
||||
flex: 1,
|
||||
})
|
||||
|
||||
const $listContentContainer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
|
||||
paddingHorizontal: spacing.lg,
|
||||
})
|
||||
|
||||
const $sectionListContentContainer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
|
||||
paddingHorizontal: spacing.lg,
|
||||
})
|
||||
|
||||
const $heading: ThemedStyle<ViewStyle> = ({ spacing }) => ({
|
||||
marginBottom: spacing.xxxl,
|
||||
})
|
||||
|
||||
const $logoImage: ImageStyle = {
|
||||
height: s(42),
|
||||
width: s(77),
|
||||
}
|
||||
|
||||
const $logoContainer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
|
||||
alignSelf: "flex-start",
|
||||
justifyContent: "center",
|
||||
height: s(56),
|
||||
paddingHorizontal: spacing.lg,
|
||||
})
|
||||
|
||||
const $menuContainer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
|
||||
paddingBottom: spacing.xs,
|
||||
paddingTop: spacing.lg,
|
||||
})
|
||||
|
||||
const $demoItemName: ThemedStyle<TextStyle> = ({ spacing }) => ({
|
||||
fontSize: fs(24),
|
||||
marginBottom: spacing.md,
|
||||
})
|
||||
|
||||
const $demoItemDescription: ThemedStyle<TextStyle> = ({ spacing }) => ({
|
||||
marginBottom: spacing.xxl,
|
||||
})
|
||||
|
||||
const $demoUseCasesSpacer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
|
||||
paddingBottom: spacing.xxl,
|
||||
})
|
||||
Reference in New Issue
Block a user