198 lines
8.3 KiB
TypeScript
198 lines
8.3 KiB
TypeScript
import React, { useRef, useState } from "react";
|
|
import {
|
|
Animated, LayoutAnimation, Linking,
|
|
Platform, Text, TouchableOpacity,
|
|
UIManager, View, useColorScheme,
|
|
} from "react-native";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { Entry } from "../services/api";
|
|
|
|
if (Platform.OS === "android") {
|
|
UIManager.setLayoutAnimationEnabledExperimental?.(true);
|
|
}
|
|
|
|
function formatDate(iso: string): string {
|
|
try {
|
|
return new Date(iso).toLocaleDateString("en-GB", {
|
|
day: "numeric", month: "short", year: "numeric",
|
|
});
|
|
} catch {
|
|
return iso;
|
|
}
|
|
}
|
|
|
|
function fileName(path: string): string {
|
|
return path.split(/[\\/]/).pop() ?? path;
|
|
}
|
|
|
|
export default function ExpandableItem({ item }: { item: Entry }) {
|
|
const [expanded, setExpanded] = useState(false);
|
|
const rotateAnim = useRef(new Animated.Value(0)).current;
|
|
const dark = useColorScheme() === "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,
|
|
}}
|
|
>
|
|
{/* ── Collapsed header ── */}
|
|
<View className="flex-row items-center px-4 py-4">
|
|
<View className="flex-1 pr-3">
|
|
|
|
{/* Subject tag + date */}
|
|
<View className="flex-row items-center mb-1.5 gap-2 flex-wrap">
|
|
<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.subject?.name ?? item.groupName}
|
|
</Text>
|
|
</View>
|
|
|
|
<View className={`px-2 py-0.5 rounded-full ${dark ? "bg-obsidian-50" : "bg-parchment-200"}`}>
|
|
<Text className={`text-xs font-sans ${dark ? "text-muted-dark" : "text-muted-light"}`}>
|
|
{item.groupName}
|
|
</Text>
|
|
</View>
|
|
|
|
<Text className={`text-xs ${dark ? "text-muted-dark" : "text-muted-light"}`}>
|
|
{formatDate(item.timePublished)}
|
|
</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>
|
|
|
|
{/* info_entry — always visible as subtitle */}
|
|
{item.infoEntry ? (
|
|
<Text
|
|
className={`text-xs mt-1 leading-relaxed ${dark ? "text-muted-dark" : "text-muted-light"}`}
|
|
numberOfLines={expanded ? undefined : 2}
|
|
>
|
|
{item.infoEntry}
|
|
</Text>
|
|
) : null}
|
|
</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={`border-t ${dark ? "border-border-dark" : "border-border-light"}`}>
|
|
|
|
{/* paragraph */}
|
|
{item.paragraph ? (
|
|
<View className="px-4 pt-3 pb-2">
|
|
<Text className={`text-xs font-sans font-bold tracking-widest uppercase mb-1.5 ${dark ? "text-muted-dark" : "text-muted-light"}`}>
|
|
Content
|
|
</Text>
|
|
<Text className={`text-sm font-sans leading-relaxed ${dark ? "text-ink-dark/85" : "text-ink-light/85"}`}>
|
|
{item.paragraph}
|
|
</Text>
|
|
</View>
|
|
) : null}
|
|
|
|
{/* Meta row: subject + group + date */}
|
|
<View className={`mx-4 my-3 p-3 rounded-xl ${dark ? "bg-obsidian-50" : "bg-parchment-100"}`}>
|
|
<Row label="Subject" value={item.subject?.name} dark={dark} />
|
|
<Row label="Group" value={item.groupName} dark={dark} />
|
|
<Row label="Published" value={formatDate(item.timePublished)} dark={dark} last />
|
|
</View>
|
|
|
|
{/* filepath — clickable attachment */}
|
|
{item.filepath ? (
|
|
<TouchableOpacity
|
|
onPress={() => Linking.openURL(item.filepath!)}
|
|
activeOpacity={0.75}
|
|
className={`mx-4 mb-4 flex-row items-center gap-3 px-4 py-3 rounded-xl border ${
|
|
dark
|
|
? "bg-obsidian-50 border-border-dark"
|
|
: "bg-parchment-100 border-border-light"
|
|
}`}
|
|
// stop the expand/collapse toggle firing
|
|
onStartShouldSetResponder={() => true}
|
|
>
|
|
<View className={`w-8 h-8 rounded-lg items-center justify-center ${dark ? "bg-accent-dark/20" : "bg-accent/10"}`}>
|
|
<Ionicons
|
|
name="document-attach-outline"
|
|
size={17}
|
|
color={dark ? "#E07B45" : "#C4622D"}
|
|
/>
|
|
</View>
|
|
<View className="flex-1">
|
|
<Text className={`text-xs font-sans font-semibold ${dark ? "text-ink-dark" : "text-ink-light"}`}>
|
|
{fileName(item.filepath)}
|
|
</Text>
|
|
<Text className={`text-xs font-sans mt-0.5 ${dark ? "text-muted-dark" : "text-muted-light"}`}>
|
|
Tap to open attachment
|
|
</Text>
|
|
</View>
|
|
<Ionicons
|
|
name="open-outline"
|
|
size={15}
|
|
color={dark ? "#706D67" : "#8A8278"}
|
|
/>
|
|
</TouchableOpacity>
|
|
) : null}
|
|
</View>
|
|
)}
|
|
</TouchableOpacity>
|
|
);
|
|
}
|
|
|
|
|
|
function Row({ label, value, dark, last = false }: {
|
|
label: string;
|
|
value?: string;
|
|
dark: boolean;
|
|
last?: boolean;
|
|
}) {
|
|
if (!value) return null;
|
|
return (
|
|
<View className={`flex-row justify-between py-1.5 ${!last ? `border-b ${dark ? "border-border-dark" : "border-border-light"}` : ""}`}>
|
|
<Text className={`text-xs font-sans font-semibold ${dark ? "text-muted-dark" : "text-muted-light"}`}>
|
|
{label}
|
|
</Text>
|
|
<Text className={`text-xs font-sans ${dark ? "text-ink-dark" : "text-ink-light"}`}>
|
|
{value}
|
|
</Text>
|
|
</View>
|
|
);
|
|
} |