feat: add TemplateGrid and TitleBar components with tests
- Implemented TemplateGrid component for displaying templates in a grid layout. - Added calculateCardWidth helper function for dynamic card sizing. - Created TitleBar component for displaying the title and points with interaction. - Added unit tests for TemplateGrid and TitleBar components to ensure proper functionality. - Introduced useStickyTabs and useTabNavigation hooks with tests for managing sticky tab behavior and navigation logic. - Implemented useTemplateFilter hook for filtering templates based on video content. - Added comprehensive tests for all new hooks and components to validate behavior and edge cases.
This commit is contained in:
124
components/blocks/home/TemplateCard.tsx
Normal file
124
components/blocks/home/TemplateCard.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, { memo } from 'react'
|
||||
import { Pressable, StyleSheet, Text, View, ViewStyle } from 'react-native'
|
||||
import { Image } from 'expo-image'
|
||||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
|
||||
export interface TemplateCardProps {
|
||||
id: string
|
||||
title: string
|
||||
previewUrl?: string
|
||||
webpPreviewUrl?: string
|
||||
coverImageUrl?: string
|
||||
aspectRatio?: string
|
||||
cardWidth: number
|
||||
onPress: (id: string) => void
|
||||
testID?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 aspectRatio 字符串为数字
|
||||
* 例如: "128:128" -> 1, "16:9" -> 1.777..., "1.5" -> 1.5
|
||||
*/
|
||||
export function parseAspectRatio(aspectRatioString?: string): number | undefined {
|
||||
if (!aspectRatioString) return undefined
|
||||
|
||||
if (aspectRatioString.includes(':')) {
|
||||
const [w, h] = aspectRatioString.split(':').map(Number)
|
||||
return w / h
|
||||
}
|
||||
|
||||
return Number(aspectRatioString)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片源 URI,按优先级: webpPreviewUrl > previewUrl > coverImageUrl
|
||||
*/
|
||||
export function getImageUri(
|
||||
webpPreviewUrl?: string,
|
||||
previewUrl?: string,
|
||||
coverImageUrl?: string
|
||||
): string | undefined {
|
||||
return webpPreviewUrl || previewUrl || coverImageUrl
|
||||
}
|
||||
|
||||
const TemplateCardComponent: React.FC<TemplateCardProps> = ({
|
||||
id,
|
||||
title,
|
||||
previewUrl,
|
||||
webpPreviewUrl,
|
||||
coverImageUrl,
|
||||
aspectRatio: aspectRatioString,
|
||||
cardWidth,
|
||||
onPress,
|
||||
testID,
|
||||
}) => {
|
||||
const aspectRatio = parseAspectRatio(aspectRatioString)
|
||||
const imageUri = getImageUri(webpPreviewUrl, previewUrl, coverImageUrl)
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[styles.card, { width: cardWidth }]}
|
||||
onPress={() => onPress(id)}
|
||||
testID={testID}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.cardImageContainer,
|
||||
aspectRatio ? { aspectRatio } : undefined,
|
||||
].filter(Boolean) as ViewStyle[]}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: imageUri }}
|
||||
style={[
|
||||
styles.cardImage,
|
||||
aspectRatio ? { aspectRatio } : undefined,
|
||||
].filter(Boolean) as ViewStyle[]}
|
||||
contentFit="cover"
|
||||
/>
|
||||
<LinearGradient
|
||||
colors={['rgba(17, 17, 17, 0)', 'rgba(17, 17, 17, 0.9)']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
style={styles.cardImageGradient}
|
||||
/>
|
||||
<Text style={styles.cardTitle} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
export const TemplateCard = memo(TemplateCardComponent)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
marginBottom: 5,
|
||||
},
|
||||
cardImageContainer: {
|
||||
width: '100%',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
},
|
||||
cardImage: {
|
||||
width: '100%',
|
||||
borderRadius: 8,
|
||||
},
|
||||
cardImageGradient: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '50%',
|
||||
},
|
||||
cardTitle: {
|
||||
position: 'absolute',
|
||||
bottom: 8,
|
||||
left: 8,
|
||||
right: 8,
|
||||
color: '#F5F5F5',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user