- 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>
97 lines
3.6 KiB
TypeScript
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",
|
|
},
|
|
});
|
|
}
|