153 lines
5.0 KiB
TypeScript
153 lines
5.0 KiB
TypeScript
import React, { useState, useRef } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
TouchableOpacity,
|
|
Animated,
|
|
LayoutAnimation,
|
|
Platform,
|
|
UIManager,
|
|
useColorScheme,
|
|
} from "react-native";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
|
|
// Enable LayoutAnimation on Android idk honestly probably ok?
|
|
if (Platform.OS === "android") {
|
|
UIManager.setLayoutAnimationEnabledExperimental?.(true);
|
|
}
|
|
|
|
export interface FeedItem {
|
|
id: string;
|
|
title: string;
|
|
author: string;
|
|
timestamp: string;
|
|
tag: string;
|
|
paragraphs: string[];
|
|
}
|
|
|
|
interface ExpandableItemProps {
|
|
item: FeedItem;
|
|
}
|
|
|
|
export default function ExpandableItem({ item }: ExpandableItemProps) {
|
|
const [expanded, setExpanded] = useState(false);
|
|
const rotateAnim = useRef(new Animated.Value(0)).current;
|
|
const scheme = useColorScheme();
|
|
const dark = scheme === "dark";
|
|
|
|
const toggle = () => {
|
|
LayoutAnimation.configureNext({
|
|
duration: 280,
|
|
create: { type: "easeInEaseOut", property: "opacity" },
|
|
update: { type: "spring", springDamping: 0.8 },
|
|
});
|
|
Animated.timing(rotateAnim, {
|
|
toValue: expanded ? 0 : 1,
|
|
duration: 250,
|
|
useNativeDriver: true,
|
|
}).start();
|
|
setExpanded((v) => !v);
|
|
};
|
|
|
|
const chevronRotation = rotateAnim.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: ["0deg", "180deg"],
|
|
});
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
activeOpacity={0.85}
|
|
onPress={toggle}
|
|
className={`
|
|
mx-4 mb-3 rounded-2xl overflow-hidden
|
|
${dark
|
|
? "bg-obsidian-100 border border-border-dark"
|
|
: "bg-surface-light border border-border-light"
|
|
}
|
|
`}
|
|
style={{
|
|
shadowColor: dark ? "#000" : "#1A1714",
|
|
shadowOpacity: dark ? 0.35 : 0.06,
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowRadius: 8,
|
|
elevation: 2,
|
|
}}
|
|
>
|
|
{/* Header */}
|
|
<View className="flex-row items-center px-4 py-4">
|
|
<View className="flex-1 pr-3">
|
|
{/* Tag + timestamp row */}
|
|
<View className="flex-row items-center mb-1 gap-2">
|
|
<View
|
|
className={`px-2 py-0.5 rounded-full ${
|
|
dark ? "bg-accent-dark/20" : "bg-accent/10"
|
|
}`}
|
|
>
|
|
<Text
|
|
className={`text-xs font-sans font-semibold tracking-wide ${
|
|
dark ? "text-accent-dark" : "text-accent"
|
|
}`}
|
|
>
|
|
{item.tag}
|
|
</Text>
|
|
</View>
|
|
<Text
|
|
className={`text-xs ${
|
|
dark ? "text-muted-dark" : "text-muted-light"
|
|
}`}
|
|
>
|
|
{item.timestamp}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Title */}
|
|
<Text
|
|
className={`text-base font-sans font-semibold leading-snug ${
|
|
dark ? "text-ink-dark" : "text-ink-light"
|
|
}`}
|
|
numberOfLines={expanded ? undefined : 2}
|
|
>
|
|
{item.title}
|
|
</Text>
|
|
|
|
{/* Author */}
|
|
<Text
|
|
className={`text-xs mt-1 ${
|
|
dark ? "text-muted-dark" : "text-muted-light"
|
|
}`}
|
|
>
|
|
by {item.author}
|
|
</Text>
|
|
</View>
|
|
|
|
<Animated.View style={{ transform: [{ rotate: chevronRotation }] }}>
|
|
<Ionicons
|
|
name="chevron-down"
|
|
size={18}
|
|
color={dark ? "#706D67" : "#8A8278"}
|
|
/>
|
|
</Animated.View>
|
|
</View>
|
|
|
|
{/* Expanded body */}
|
|
{expanded && (
|
|
<View
|
|
className={`px-4 pb-5 pt-1 border-t ${
|
|
dark ? "border-border-dark" : "border-border-light"
|
|
}`}
|
|
>
|
|
{item.paragraphs.map((para, i) => (
|
|
<Text
|
|
key={i}
|
|
className={`text-sm font-sans leading-relaxed mb-3 last:mb-0 ${
|
|
dark ? "text-ink-dark/80" : "text-ink-light/80"
|
|
}`}
|
|
>
|
|
{para}
|
|
</Text>
|
|
))}
|
|
</View>
|
|
)}
|
|
</TouchableOpacity>
|
|
);
|
|
} |