Files
petfeed/mobile/src/components/RepostCard.tsx
T
adminandClaude Opus 5.5 9f34f0e5c3 refactor(mobile): Audit O1 – doppelte Helfer zusammengelegt (M1–M3, M5–M7)
- M1: typography.caption statt 44× label ohne Großschreibung; sectionTitleStyle.ts entfällt
- M2: uploadPickedImage() für verkleinern → presign → PUT (Post, Meilenstein, Story, Avatar)
- M3: uploadImageToSupabase in uploadFileToUrl aufgegangen
- M5: usePawToggle für PawButton + StoryPawButton (Beitrags-Pfote zählt jetzt sofort mit)
- M6: healthSectionStyles für Impfung, Tierarzt, Gewicht
- M7: unbenutzten Typ PostDetail entfernt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:18:44 +02:00

97 lines
3.6 KiB
TypeScript

import { useMemo } from "react";
import { View, Text, Pressable, StyleSheet, useWindowDimensions } from "react-native";
import { useRouter } from "expo-router";
import { useTranslation } from "react-i18next";
import { Image } from "expo-image";
import { PawPrint, Video, Trophy } from "lucide-react-native";
import { PetAvatar } from "./PetAvatar";
import { getMediaUrl } from "../lib/media-url";
import { useTheme } from "../context/ThemeContext";
import { getTypography, radii, type ColorTokens, type Typography } from "../theme/tokens";
import type { PostCardPost } from "./PostCard";
type Props = {
post: PostCardPost;
};
/**
* Port of web's src/components/feed/RepostCard.tsx — the "Paw-Back" (repost)
* attribution wrapper around a read-only rendering of the original post. No
* action row (matches web: reposts aren't reactable/commentable as a
* separate entity, only the original post is). Unlike web's bordered-card
* look, this stays visually flush with mobile's other feed items (hairline
* separators between list rows already provide the boundary — see
* DESIGN.md's "hairline rings not shadows").
*/
export function RepostCard({ post }: Props) {
const { t } = useTranslation();
const router = useRouter();
const { width } = useWindowDimensions();
const { colors } = useTheme();
const styles = useMemo(() => makeStyles(colors), [colors]);
const cardWidth = width - 32;
const originalPost = post.repost?.originalPost;
if (!originalPost) return null;
const origPet = originalPost.pet;
const origImage = originalPost.images[0];
return (
<View style={styles.card}>
<Pressable style={styles.attribution} onPress={() => router.push(`/pets/${post.pet.id}`)}>
<PawPrint size={14} color={colors.marigoldOrange} />
<Text style={styles.attributionText}>{t("social.repostAttribution", { name: post.pet.name })}</Text>
</Pressable>
<Pressable style={styles.header} onPress={() => router.push(`/pets/${origPet.id}`)}>
<PetAvatar name={origPet.name} avatarKey={origPet.avatarKey} size="sm" owner={origPet.owner} />
<Text style={styles.name}>{origPet.name}</Text>
</Pressable>
{originalPost.milestone ? (
<View style={[styles.placeholder, { width: cardWidth }]}>
<Trophy size={28} color={colors.marigoldOrange} />
</View>
) : originalPost.type === "VIDEO" ? (
<View style={[styles.placeholder, { width: cardWidth }]}>
<Video size={28} color={colors.pureWhite} />
</View>
) : origImage ? (
<Image
source={{ uri: getMediaUrl(origImage.storageKey) }}
style={{ width: cardWidth, aspectRatio: 1.2, borderRadius: radii.md }}
contentFit="cover"
placeholder={origImage.blurhash ? { blurhash: origImage.blurhash } : undefined}
transition={150}
/>
) : null}
{originalPost.caption ? (
<Text style={styles.caption} numberOfLines={3}>
{originalPost.caption}
</Text>
) : null}
</View>
);
}
function makeStyles(colors: ColorTokens) {
const typography: Typography = getTypography(colors);
return StyleSheet.create({
card: { gap: 8 },
attribution: { flexDirection: "row", alignItems: "center", gap: 6 },
attributionText: { ...typography.caption, color: colors.quietGray },
header: { flexDirection: "row", alignItems: "center", gap: 8 },
name: { ...typography.body, fontFamily: typography.title.fontFamily },
caption: { ...typography.body },
placeholder: {
aspectRatio: 1.2,
borderRadius: radii.md,
backgroundColor: colors.ink,
alignItems: "center",
justifyContent: "center",
},
});
}