204 lines
4.9 KiB
TypeScript
204 lines
4.9 KiB
TypeScript
import { useState } from "react";
|
|
import {
|
|
Modal,
|
|
Pressable,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
} from "react-native";
|
|
|
|
interface BirthdayItemProps {
|
|
id: string;
|
|
name: string;
|
|
date: string;
|
|
onDelete: (id: string) => void;
|
|
}
|
|
|
|
export function BirthdayItem({ id, name, date, onDelete }: BirthdayItemProps) {
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
|
|
|
|
const daysUntilNext = (birthdayDate: string) => {
|
|
const today = new Date();
|
|
const parsedDate = new Date(birthdayDate);
|
|
const month = parsedDate.getMonth();
|
|
const day = parsedDate.getDate();
|
|
const currentYear = today.getFullYear();
|
|
|
|
let target = new Date(currentYear, month, day);
|
|
if (target < today.setHours(0, 0, 0, 0)) {
|
|
target = new Date(currentYear + 1, month, day);
|
|
}
|
|
const msPerDay = 1000 * 60 * 60 * 24;
|
|
const diff = target.getTime() - new Date(today.setHours(0, 0, 0, 0)).getTime();
|
|
return Math.ceil(diff / msPerDay);
|
|
};
|
|
|
|
const computeNewAge = (birthdayDate: string) => {
|
|
const parsedDate = new Date(birthdayDate);
|
|
const today = new Date();
|
|
let age = today.getFullYear() - parsedDate.getFullYear();
|
|
|
|
const monthDiff = today.getMonth() - parsedDate.getMonth();
|
|
const dayDiff = today.getDate() - parsedDate.getDate();
|
|
|
|
if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
|
|
age--;
|
|
}
|
|
return age;
|
|
};
|
|
|
|
const days = daysUntilNext(date);
|
|
|
|
const handleOpenMenu = (event: {
|
|
nativeEvent: { pageX: number; pageY: number };
|
|
}) => {
|
|
setMenuPosition({
|
|
top: event.nativeEvent.pageY + 8,
|
|
left: event.nativeEvent.pageX - 136,
|
|
});
|
|
setMenuOpen(true);
|
|
};
|
|
|
|
const handleDelete = () => {
|
|
setMenuOpen(false);
|
|
onDelete(id);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<View style={styles.itemContainer}>
|
|
<View style={styles.contentRow}>
|
|
<View style={styles.textContainer}>
|
|
<Text style={styles.name}>{name}</Text>
|
|
{days === 0 ? (
|
|
<Text style={styles.age}>
|
|
{name} makes {computeNewAge(date)} years today!
|
|
</Text>
|
|
) : (
|
|
date && <Text style={styles.age}>Days remaining: {days}</Text>
|
|
)}
|
|
</View>
|
|
<Pressable
|
|
accessibilityLabel={`Open actions for ${name}`}
|
|
hitSlop={8}
|
|
onPress={handleOpenMenu}
|
|
style={styles.menuTrigger}
|
|
>
|
|
<Text style={styles.menuTriggerText}>⋮</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
<Modal
|
|
animationType="fade"
|
|
onRequestClose={() => setMenuOpen(false)}
|
|
transparent
|
|
visible={menuOpen}
|
|
>
|
|
<View style={styles.menuBackdrop}>
|
|
<Pressable
|
|
onPress={() => setMenuOpen(false)}
|
|
style={styles.menuBackdropPressable}
|
|
/>
|
|
<View
|
|
style={[
|
|
styles.menu,
|
|
{
|
|
top: Math.max(menuPosition.top, 12),
|
|
left: Math.max(menuPosition.left, 12),
|
|
},
|
|
]}
|
|
>
|
|
<Pressable disabled style={[styles.menuItem, styles.menuItemDisabled]}>
|
|
<Text style={[styles.menuItemText, styles.menuItemTextDisabled]}>
|
|
Edit
|
|
</Text>
|
|
</Pressable>
|
|
<Pressable onPress={handleDelete} style={styles.menuItem}>
|
|
<Text style={[styles.menuItemText, styles.deleteText]}>Delete</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
itemContainer: {
|
|
padding: 15,
|
|
borderRadius: 8,
|
|
backgroundColor: "#f0f0f0",
|
|
elevation: 2,
|
|
marginBottom: 10,
|
|
},
|
|
contentRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
gap: 12,
|
|
},
|
|
textContainer: {
|
|
flex: 1,
|
|
},
|
|
menuTrigger: {
|
|
alignItems: "center",
|
|
borderRadius: 999,
|
|
height: 36,
|
|
justifyContent: "center",
|
|
width: 36,
|
|
},
|
|
menuTriggerText: {
|
|
color: "#444",
|
|
fontSize: 22,
|
|
lineHeight: 22,
|
|
},
|
|
name: {
|
|
fontSize: 16,
|
|
fontWeight: "bold",
|
|
},
|
|
date: {
|
|
fontSize: 14,
|
|
},
|
|
age: {
|
|
fontSize: 12,
|
|
marginTop: 5,
|
|
},
|
|
menuBackdrop: {
|
|
...StyleSheet.absoluteFillObject,
|
|
},
|
|
menuBackdropPressable: {
|
|
...StyleSheet.absoluteFillObject,
|
|
},
|
|
menu: {
|
|
backgroundColor: "#fff",
|
|
borderRadius: 8,
|
|
elevation: 8,
|
|
minWidth: 140,
|
|
overflow: "hidden",
|
|
position: "absolute",
|
|
shadowColor: "#000",
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.15,
|
|
shadowRadius: 12,
|
|
},
|
|
menuItem: {
|
|
paddingHorizontal: 14,
|
|
paddingVertical: 12,
|
|
},
|
|
menuItemDisabled: {
|
|
opacity: 0.45,
|
|
},
|
|
menuItemText: {
|
|
color: "#222",
|
|
fontSize: 14,
|
|
fontWeight: "500",
|
|
},
|
|
menuItemTextDisabled: {
|
|
color: "#666",
|
|
},
|
|
deleteText: {
|
|
color: "#c53a3a",
|
|
},
|
|
});
|