Files
frontend/src/components/business/PostCard.tsx
lafay 937e8112f4
Some checks failed
Frontend CI / ota-android (push) Failing after 4m51s
Frontend CI / build-and-push-web (push) Has been cancelled
Frontend CI / build-android-apk (push) Has been cancelled
fix: 修复安卓端帖子卡片点击事件冲突问题
- 为嵌套TouchableOpacity添加stopPropagation防止事件冒泡
- 修复grid模式下图片和用户头像点击事件
- 修复列表模式下所有交互按钮点击事件
- 优化手势配置添加shouldCancelWhenOutside
2026-03-18 14:21:24 +08:00

978 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* PostCard 帖子卡片组件 - QQ频道风格响应式版本
* 简洁的帖子展示,无卡片边框
* 支持响应式布局,宽屏下优化显示
*/
import React, { useState, useCallback, useMemo } from 'react';
import {
View,
TouchableOpacity,
StyleSheet,
Alert,
useWindowDimensions,
} from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
import { Post } from '../../types';
import Text from '../common/Text';
import Avatar from '../common/Avatar';
import { ImageGrid, ImageGridItem, SmartImage } from '../common';
import VotePreview from './VotePreview';
import { useResponsive, useResponsiveValue } from '../../hooks/useResponsive';
import { getPreviewImageUrl, ImageDisplayMode } from '../../utils/imageHelper';
interface PostCardProps {
post: Post;
onPress: () => void;
onUserPress: () => void;
onLike: () => void;
onComment: () => void;
onBookmark: () => void;
onShare: () => void;
onImagePress?: (images: ImageGridItem[], index: number) => void;
onDelete?: () => void;
compact?: boolean;
index?: number;
isAuthor?: boolean;
isPostAuthor?: boolean; // 当前用户是否为帖子作者
variant?: 'default' | 'grid';
}
const PostCard: React.FC<PostCardProps> = ({
post,
onPress,
onUserPress,
onLike,
onComment,
onBookmark,
onShare,
onImagePress,
onDelete,
compact = false,
index,
isAuthor = false,
isPostAuthor = false,
variant = 'default',
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
// 使用响应式 hook
const {
width,
isMobile,
isTablet,
isDesktop,
isWideScreen,
isLandscape,
orientation
} = useResponsive();
const { width: windowWidth } = useWindowDimensions();
// 响应式字体大小
const responsiveFontSize = useResponsiveValue({
xs: fontSizes.md,
sm: fontSizes.md,
md: fontSizes.lg,
lg: isLandscape ? fontSizes.lg : fontSizes.xl,
xl: fontSizes.xl,
'2xl': fontSizes.xl,
'3xl': fontSizes['2xl'],
'4xl': fontSizes['2xl'],
});
// 响应式标题字体大小
const responsiveTitleFontSize = useResponsiveValue({
xs: fontSizes.lg,
sm: fontSizes.lg,
md: fontSizes.xl,
lg: isLandscape ? fontSizes.xl : fontSizes['2xl'],
xl: fontSizes['2xl'],
'2xl': fontSizes['2xl'],
'3xl': fontSizes['3xl'],
'4xl': fontSizes['3xl'],
});
// 响应式头像大小
const avatarSize = useResponsiveValue({
xs: 36,
sm: 38,
md: 40,
lg: 44,
xl: 48,
'2xl': 48,
'3xl': 52,
'4xl': 56,
});
// 响应式内边距 - 宽屏下增加更多内边距
const responsivePadding = useResponsiveValue({
xs: spacing.md,
sm: spacing.md,
md: spacing.lg,
lg: spacing.xl,
xl: spacing.xl * 1.2,
'2xl': spacing.xl * 1.5,
'3xl': spacing.xl * 2,
'4xl': spacing.xl * 2.5,
});
// 宽屏下额外的卡片内边距
const cardPadding = useMemo(() => {
if (isWideScreen) return spacing.xl;
if (isDesktop) return spacing.lg;
if (isTablet) return spacing.md;
return 0; // 移动端无额外内边距
}, [isWideScreen, isDesktop, isTablet]);
const formatDateTime = (dateString?: string | null): string => {
if (!dateString) return '';
const date = new Date(dateString);
if (Number.isNaN(date.getTime())) return '';
const pad = (num: number) => String(num).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
};
const isPostEdited = (createdAt?: string, updatedAt?: string): boolean => {
if (!createdAt || !updatedAt) return false;
const created = new Date(createdAt).getTime();
const updated = new Date(updatedAt).getTime();
if (Number.isNaN(created) || Number.isNaN(updated)) return false;
return updated - created > 1000;
};
const getTruncatedContent = (content: string | undefined | null, maxLength: number = 100): string => {
if (!content) return '';
if (content.length <= maxLength || isExpanded) return content;
return content.substring(0, maxLength) + '...';
};
// 宽屏下显示更多内容
const getResponsiveMaxLength = () => {
if (isWideScreen) return 300;
if (isDesktop) return 250;
if (isTablet) return 200;
return 100;
};
const formatNumber = (num: number): string => {
if (num >= 10000) {
return (num / 10000).toFixed(1) + 'w';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k';
}
return num.toString();
};
// 处理删除帖子
const handleDelete = () => {
if (!onDelete || isDeleting) return;
Alert.alert(
'删除帖子',
'确定要删除这篇帖子吗?删除后将无法恢复。',
[
{
text: '取消',
style: 'cancel',
},
{
text: '删除',
style: 'destructive',
onPress: async () => {
setIsDeleting(true);
try {
await onDelete();
} catch (error) {
console.error('删除帖子失败:', error);
Alert.alert('删除失败', '删除帖子时发生错误,请稍后重试');
} finally {
setIsDeleting(false);
}
},
},
],
);
};
// 渲染图片 - 使用新的 ImageGrid 组件
const renderImages = () => {
if (!post.images || !Array.isArray(post.images) || post.images.length === 0) return null;
// 转换图片数据格式
const gridImages: ImageGridItem[] = post.images.map(img => ({
id: img.id,
url: img.url,
thumbnail_url: img.thumbnail_url,
preview_url: img.preview_url,
preview_url_large: img.preview_url_large,
width: img.width,
height: img.height,
}));
// 宽屏下显示更大的图片
const imageGap = isDesktop ? 12 : isTablet ? 8 : 4;
const imageBorderRadius = isDesktop ? borderRadius.xl : borderRadius.md;
return (
<View style={styles.imagesContainer}>
<ImageGrid
images={gridImages}
maxDisplayCount={isWideScreen ? 12 : 9}
mode="auto"
gap={imageGap}
borderRadius={imageBorderRadius}
showMoreOverlay={true}
onImagePress={onImagePress}
displayMode={variant === 'grid' ? 'grid' : 'list'}
usePreview={true}
/>
</View>
);
};
const renderBadges = () => {
const badges = [];
if (isAuthor) {
badges.push(
<View key="author" style={[styles.badge, styles.authorBadge]}>
<Text variant="caption" style={styles.badgeText}></Text>
</View>
);
}
if (post.author?.id === '1') {
badges.push(
<View key="admin" style={[styles.badge, styles.adminBadge]}>
<Text variant="caption" style={styles.badgeText}></Text>
</View>
);
}
return badges;
};
const renderTopComment = () => {
if (!post.top_comment) return null;
const { top_comment } = post;
const topCommentContainerStyles = [
styles.topCommentContainer,
...(isDesktop ? [styles.topCommentContainerWide] : [])
];
const topCommentAuthorStyles = [
styles.topCommentAuthor,
...(isDesktop ? [styles.topCommentAuthorWide] : [])
];
const topCommentTextStyles = [
styles.topCommentText,
...(isDesktop ? [styles.topCommentTextWide] : [])
];
const moreCommentsStyles = [
styles.moreComments,
...(isDesktop ? [styles.moreCommentsWide] : [])
];
return (
<View style={topCommentContainerStyles}>
<View style={styles.topCommentContent}>
<Text
variant="caption"
color={colors.text.secondary}
style={topCommentAuthorStyles}
>
{top_comment.author?.nickname || '匿名用户'}:
</Text>
<Text
variant="caption"
color={colors.text.secondary}
style={topCommentTextStyles}
numberOfLines={isDesktop ? 2 : 1}
>
{top_comment.content}
</Text>
</View>
{post.comments_count > 1 && (
<Text
variant="caption"
color={colors.primary.main}
style={moreCommentsStyles}
>
{formatNumber(post.comments_count)}
</Text>
)}
</View>
);
};
// 渲染投票预览
const renderVotePreview = () => {
if (!post.is_vote) return null;
return (
<VotePreview
onPress={onPress}
/>
);
};
// 渲染小红书风格的两栏卡片
const renderGridVariant = () => {
// 获取封面图(第一张图)
const coverImage = post.images && Array.isArray(post.images) && post.images.length > 0 ? post.images[0] : null;
const hasImage = !!coverImage;
// 防御性检查:确保 author 存在
const author = post.author || { id: '', nickname: '匿名用户', avatar: null, username: '' };
// 根据图片实际宽高比或使用随机值创建错落感
// 使用帖子 ID 生成一个伪随机值,确保每次渲染一致但不同帖子有差异
const postId = post.id || '';
const hash = postId.split('').reduce((a, b) => a + b.charCodeAt(0), 0);
const aspectRatios = [0.7, 0.75, 0.8, 0.85, 0.9, 1, 1.1, 1.2];
const aspectRatio = aspectRatios[hash % aspectRatios.length];
// 获取封面图预览 URL
const coverUrl = hasImage
? getPreviewImageUrl(coverImage, 'grid')
: '';
// 获取正文预览(无图时显示)
const getContentPreview = (content: string | undefined | null): string => {
if (!content) return '';
// 移除 Markdown 标记和多余空白
return content
.replace(/[#*_~`\[\]()]/g, '')
.replace(/\n+/g, ' ')
.trim();
};
const contentPreview = getContentPreview(post.content);
// 响应式字体大小(网格模式)
const gridTitleFontSize = isDesktop ? 16 : isTablet ? 15 : 14;
const gridUsernameFontSize = isDesktop ? 14 : 12;
const gridLikeFontSize = isDesktop ? 14 : 12;
const handleContainerPress = () => {
onPress();
};
const handleImagePress = (e: any) => {
e.stopPropagation();
onImagePress?.(post.images || [], 0);
};
const handleUserPress = (e: any) => {
e.stopPropagation();
onUserPress();
};
return (
<TouchableOpacity
style={[
styles.gridContainer,
!hasImage && styles.gridContainerNoImage,
isDesktop && styles.gridContainerDesktop
]}
onPress={handleContainerPress}
activeOpacity={0.9}
>
{/* 封面图 - 只有有图片时才渲染,无图时不显示占位区域 */}
{hasImage && (
<TouchableOpacity
activeOpacity={0.8}
onPress={handleImagePress}
>
<SmartImage
source={{ uri: coverUrl }}
style={[styles.gridCoverImage, { aspectRatio }]}
resizeMode="cover"
usePreview={true}
/>
</TouchableOpacity>
)}
{/* 无图时的正文预览区域 */}
{!hasImage && contentPreview && (
<View style={[styles.gridContentPreview, isDesktop ? styles.gridContentPreviewDesktop : null]}>
<Text
style={[styles.gridContentText, ...(isDesktop ? [styles.gridContentTextDesktop] : [])]}
numberOfLines={isDesktop ? 8 : 6}
>
{contentPreview}
</Text>
</View>
)}
{/* 投票标识 */}
{post.is_vote && (
<View style={styles.gridVoteBadge}>
<MaterialCommunityIcons name="vote" size={isDesktop ? 14 : 12} color={colors.primary.contrast} />
<Text style={styles.gridVoteBadgeText}></Text>
</View>
)}
{/* 标题 - 无图时显示更多行 */}
{post.title && (
<Text
style={[
styles.gridTitle,
{ fontSize: gridTitleFontSize },
...(hasImage ? [] : [styles.gridTitleNoImage]),
...(isDesktop ? [styles.gridTitleDesktop] : [])
]}
numberOfLines={hasImage ? 2 : 3}
>
{post.title}
</Text>
)}
{/* 底部信息 */}
<View style={[styles.gridFooter, isDesktop && styles.gridFooterDesktop]}>
<TouchableOpacity style={styles.gridUserInfo} onPress={handleUserPress}>
<Avatar
source={author.avatar}
size={isDesktop ? 24 : 20}
name={author.nickname}
/>
<Text style={[styles.gridUsername, { fontSize: gridUsernameFontSize }]} numberOfLines={1}>
{author.nickname}
</Text>
</TouchableOpacity>
<View style={styles.gridLikeInfo}>
<MaterialCommunityIcons name="heart" size={isDesktop ? 16 : 14} color="#666" />
<Text style={[styles.gridLikeCount, { fontSize: gridLikeFontSize }]}>
{formatNumber(post.likes_count)}
</Text>
</View>
</View>
</TouchableOpacity>
);
};
// 根据 variant 渲染不同样式
if (variant === 'grid') {
return renderGridVariant();
}
// 防御性检查:确保 author 存在
const author = post.author || { id: '', nickname: '匿名用户', avatar: null, username: '' };
// 计算响应式内容最大行数
const contentNumberOfLines = useMemo(() => {
if (isWideScreen) return 8;
if (isDesktop) return 6;
if (isTablet) return 5;
return 3;
}, [isWideScreen, isDesktop, isTablet]);
const handleContainerPress = () => {
onPress();
};
const handleUserPress = (e: any) => {
e.stopPropagation();
onUserPress();
};
const handleLikePress = (e: any) => {
e.stopPropagation();
onLike();
};
const handleCommentPress = (e: any) => {
e.stopPropagation();
onComment();
};
const handleSharePress = (e: any) => {
e.stopPropagation();
onShare();
};
const handleBookmarkPress = (e: any) => {
e.stopPropagation();
onBookmark();
};
const handleDeletePress = (e: any) => {
e.stopPropagation();
handleDelete();
};
return (
<TouchableOpacity
style={[
styles.container,
{
paddingHorizontal: responsivePadding,
paddingVertical: responsivePadding * 0.75,
...(isDesktop || isWideScreen ? {
borderRadius: borderRadius.lg,
marginHorizontal: cardPadding,
marginVertical: spacing.sm,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 3,
} : {})
}
]}
onPress={handleContainerPress}
activeOpacity={0.9}
>
{/* 用户信息 */}
<View style={styles.userSection}>
<TouchableOpacity onPress={handleUserPress}>
<Avatar
source={author.avatar}
size={avatarSize}
name={author.nickname}
/>
</TouchableOpacity>
<View style={styles.userInfo}>
<View style={styles.userNameRow}>
<TouchableOpacity onPress={handleUserPress}>
<Text
variant="body"
style={[
styles.username,
{ fontSize: isDesktop ? fontSizes.lg : fontSizes.md }
]}
>
{author.nickname}
</Text>
</TouchableOpacity>
{renderBadges()}
</View>
<View style={styles.postMeta}>
<Text variant="caption" color={colors.text.hint} style={styles.timeText}>
{formatDateTime(post.created_at)}
</Text>
{isPostEdited(post.created_at, post.updated_at) && (
<Text variant="caption" color={colors.text.hint} style={styles.timeText}>
{' · 修改 '}{formatDateTime(post.updated_at)}
</Text>
)}
</View>
</View>
{post.is_pinned && (
<View style={styles.pinnedTag}>
<MaterialCommunityIcons name="pin" size={isDesktop ? 14 : 12} color={colors.warning.main} />
<Text variant="caption" color={colors.warning.main} style={styles.pinnedText}></Text>
</View>
)}
{/* 删除按钮 - 只对帖子作者显示 */}
{isPostAuthor && onDelete && (
<TouchableOpacity
style={styles.deleteButton}
onPress={handleDeletePress}
disabled={isDeleting}
>
<MaterialCommunityIcons
name={isDeleting ? 'loading' : 'delete-outline'}
size={isDesktop ? 20 : 18}
color={colors.text.hint}
/>
</TouchableOpacity>
)}
</View>
{/* 标题 */}
{post.title && (
<Text
variant="body"
numberOfLines={compact ? 2 : undefined}
style={[
styles.title,
{ fontSize: responsiveTitleFontSize, lineHeight: responsiveTitleFontSize * 1.4 }
]}
>
{post.title}
</Text>
)}
{/* 内容 */}
{!compact && (
<>
<Text
variant="body"
color={colors.text.secondary}
numberOfLines={isExpanded ? undefined : contentNumberOfLines}
style={[
styles.content,
{ fontSize: responsiveFontSize, lineHeight: responsiveFontSize * 1.5 }
]}
>
{getTruncatedContent(post.content, getResponsiveMaxLength())}
</Text>
{post.content && post.content.length > getResponsiveMaxLength() && (
<TouchableOpacity
onPress={() => setIsExpanded(!isExpanded)}
style={styles.expandButton}
>
<Text variant="caption" color={colors.primary.main}>
{isExpanded ? '收起' : '展开全文'}
</Text>
</TouchableOpacity>
)}
</>
)}
{/* 图片 */}
{renderImages()}
{/* 投票预览 */}
{renderVotePreview()}
{/* 热门评论预览 */}
{renderTopComment()}
{/* 交互按钮 */}
<View style={[styles.actionBar, isDesktop ? styles.actionBarWide : null]}>
<View style={styles.viewCount}>
<MaterialCommunityIcons name="eye-outline" size={isDesktop ? 18 : 16} color={colors.text.hint} />
<Text
variant="caption"
color={colors.text.hint}
style={[styles.actionText, ...(isDesktop ? [styles.actionTextWide] : [])]}
>
{formatNumber(post.views_count || 0)}
</Text>
</View>
<View style={styles.actionButtons}>
<TouchableOpacity style={[styles.actionButton, ...(isDesktop ? [styles.actionButtonWide] : [])]} onPress={handleLikePress}>
<MaterialCommunityIcons
name={post.is_liked ? 'heart' : 'heart-outline'}
size={isDesktop ? 22 : 19}
color={post.is_liked ? colors.error.main : colors.text.secondary}
/>
<Text
variant="caption"
color={post.is_liked ? colors.error.main : colors.text.secondary}
style={[styles.actionText, ...(isDesktop ? [styles.actionTextWide] : [])]}
>
{post.likes_count > 0 ? formatNumber(post.likes_count) : '赞'}
</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, ...(isDesktop ? [styles.actionButtonWide] : [])]} onPress={handleCommentPress}>
<MaterialCommunityIcons
name="comment-outline"
size={isDesktop ? 22 : 19}
color={colors.text.secondary}
/>
<Text variant="caption" color={colors.text.secondary} style={[styles.actionText, ...(isDesktop ? [styles.actionTextWide] : [])]}>
{post.comments_count > 0 ? formatNumber(post.comments_count) : '评论'}
</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, ...(isDesktop ? [styles.actionButtonWide] : [])]} onPress={handleSharePress}>
<MaterialCommunityIcons
name="share-outline"
size={isDesktop ? 22 : 19}
color={colors.text.secondary}
/>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, isDesktop && styles.actionButtonWide]} onPress={handleBookmarkPress}>
<MaterialCommunityIcons
name={post.is_favorited ? 'bookmark' : 'bookmark-outline'}
size={isDesktop ? 22 : 19}
color={post.is_favorited ? colors.warning.main : colors.text.secondary}
/>
</TouchableOpacity>
</View>
</View>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: colors.background.paper,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider,
},
// 宽屏卡片样式
wideCard: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 3,
},
userSection: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.sm,
},
userInfo: {
flex: 1,
marginLeft: spacing.sm,
},
userNameRow: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
},
username: {
fontWeight: '600',
color: colors.text.primary,
},
badge: {
paddingHorizontal: 4,
paddingVertical: 1,
borderRadius: 2,
marginLeft: spacing.xs,
},
authorBadge: {
backgroundColor: colors.primary.main,
},
adminBadge: {
backgroundColor: colors.error.main,
},
badgeText: {
color: colors.text.inverse,
fontSize: fontSizes.xs,
fontWeight: '600',
},
postMeta: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 2,
},
timeText: {
fontSize: fontSizes.sm,
color: colors.text.hint,
},
pinnedTag: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.warning.light + '20',
paddingHorizontal: spacing.xs,
paddingVertical: 2,
borderRadius: borderRadius.sm,
},
pinnedText: {
marginLeft: 2,
fontSize: fontSizes.xs,
fontWeight: '500',
},
deleteButton: {
padding: spacing.xs,
marginLeft: spacing.sm,
},
title: {
marginBottom: spacing.xs,
fontWeight: '600',
color: colors.text.primary,
},
content: {
marginBottom: spacing.xs,
color: colors.text.secondary,
},
expandButton: {
marginBottom: spacing.sm,
},
imagesContainer: {
marginBottom: spacing.md,
},
topCommentContainer: {
backgroundColor: colors.background.default,
borderRadius: borderRadius.sm,
padding: spacing.sm,
marginBottom: spacing.sm,
},
topCommentContainerWide: {
padding: spacing.md,
borderRadius: borderRadius.md,
},
topCommentContent: {
flexDirection: 'row',
alignItems: 'center',
},
topCommentAuthor: {
fontWeight: '600',
marginRight: spacing.xs,
},
topCommentAuthorWide: {
fontSize: fontSizes.sm,
},
topCommentText: {
flex: 1,
},
topCommentTextWide: {
fontSize: fontSizes.sm,
},
moreComments: {
marginTop: spacing.xs,
fontWeight: '500',
},
moreCommentsWide: {
fontSize: fontSizes.sm,
marginTop: spacing.sm,
},
actionBar: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
actionBarWide: {
marginTop: spacing.sm,
paddingTop: spacing.sm,
},
viewCount: {
flexDirection: 'row',
alignItems: 'center',
},
actionButtons: {
flexDirection: 'row',
alignItems: 'center',
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: spacing.md,
paddingVertical: spacing.xs,
},
actionButtonWide: {
marginLeft: spacing.lg,
paddingVertical: spacing.sm,
},
actionText: {
marginLeft: 6,
},
actionTextWide: {
fontSize: fontSizes.md,
},
// ========== 小红书风格两栏样式 ==========
gridContainer: {
backgroundColor: '#FFF',
overflow: 'hidden',
borderRadius: 8,
},
gridContainerDesktop: {
borderRadius: 12,
},
gridContainerNoImage: {
minHeight: 200,
justifyContent: 'space-between',
},
gridCoverImage: {
width: '100%',
aspectRatio: 0.7,
backgroundColor: '#F5F5F5',
},
gridCoverPlaceholder: {
width: '100%',
aspectRatio: 0.7,
backgroundColor: '#F5F5F5',
alignItems: 'center',
justifyContent: 'center',
},
// 无图时的正文预览区域
gridContentPreview: {
backgroundColor: '#F8F8F8',
margin: 4,
padding: 8,
borderRadius: 8,
minHeight: 100,
},
gridContentPreviewDesktop: {
margin: 8,
padding: 12,
borderRadius: 12,
minHeight: 150,
},
gridContentText: {
fontSize: 13,
color: '#666',
lineHeight: 20,
},
gridContentTextDesktop: {
fontSize: 15,
lineHeight: 24,
},
gridTitle: {
color: '#333',
lineHeight: 20,
paddingHorizontal: 4,
paddingTop: 8,
minHeight: 44,
},
gridTitleNoImage: {
minHeight: 0,
paddingTop: 4,
},
gridTitleDesktop: {
paddingHorizontal: 8,
paddingTop: 12,
lineHeight: 24,
minHeight: 56,
},
gridFooter: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 4,
paddingVertical: 8,
},
gridFooterDesktop: {
paddingHorizontal: 8,
paddingVertical: 12,
},
gridUserInfo: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
gridUsername: {
color: '#666',
marginLeft: 6,
flex: 1,
},
gridLikeInfo: {
flexDirection: 'row',
alignItems: 'center',
},
gridLikeCount: {
color: '#666',
marginLeft: 4,
},
gridVoteBadge: {
position: 'absolute',
top: 8,
right: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primary.main + 'CC',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: borderRadius.full,
gap: 4,
},
gridVoteBadgeText: {
fontSize: 10,
color: colors.primary.contrast,
fontWeight: '600',
},
});
export default PostCard;