import { memo, useEffect, useState } from 'react'; import { Pressable, StyleSheet, View } from 'react-native'; import Animated, { useSharedValue, useAnimatedStyle, withRepeat, withSequence, withTiming, cancelAnimation, } from 'react-native-reanimated'; import { Text } from '@/aesthetics/Text'; import { radius, spacing, useColors } from '@/aesthetics/styles'; import { Plus, X } from 'lucide-react-native'; import type { MemoLogger } from './types'; const GAP = spacing.lg; const MIN_TILE = 60; const MAX_TILE = 76; const ICON_SIZE = 30; const LABEL_SIZE = 11; const WIGGLE_DEG = 2; const WIGGLE_MS = 160; interface LoggerTileGridProps { memoLoggers: MemoLogger[]; isLoading: boolean; isEditMode: boolean; onEnterEditMode: () => void; onExitEditMode: () => void; onSelect: (id: string) => void; onEdit?: (id: string) => void; onDelete: (id: string) => void; onAddNew: () => void; getLabel: (logger: MemoLogger) => string; } export function LoggerTileGrid({ memoLoggers, isEditMode, onEnterEditMode, onExitEditMode, onSelect, onEdit, onDelete, onAddNew, getLabel, }: LoggerTileGridProps) { const [tileSize, setTileSize] = useState(0); const handleLayout = (e: { nativeEvent: { layout: { width: number } } }) => { const width = e.nativeEvent.layout.width; const cols = Math.max(1, Math.floor((width + GAP) / (MIN_TILE + GAP))); const size = (width - GAP * (cols - 1)) / cols; setTileSize(Math.min(size, MAX_TILE)); }; const handleTilePress = (id: string) => { if (isEditMode && onEdit) { onEdit(id); } else { onSelect(id); } }; return ( {tileSize > 0 && ( <> {memoLoggers.map((logger, i) => ( handleTilePress(logger.id)} onLongPress={onEnterEditMode} onDelete={() => onDelete(logger.id)} /> ))} )} ); } interface LoggerTileProps { logger: MemoLogger; label: string; size: number; isEditMode: boolean; index: number; onPress: () => void; onLongPress: () => void; onDelete: () => void; } const LoggerTile = memo(function LoggerTile({ logger, label, size, isEditMode, index, onPress, onLongPress, onDelete, }: LoggerTileProps) { const c = useColors(); const rotation = useSharedValue(0); useEffect(() => { if (isEditMode) { const offset = index % 2 === 0 ? WIGGLE_DEG : -WIGGLE_DEG; rotation.value = withRepeat( withSequence( withTiming(offset, { duration: WIGGLE_MS }), withTiming(-offset, { duration: WIGGLE_MS }) ), -1, true ); } else { cancelAnimation(rotation); rotation.value = withTiming(0, { duration: WIGGLE_MS }); } }, [isEditMode, index, rotation]); const animatedStyle = useAnimatedStyle(() => ({ transform: [{ rotate: `${rotation.value}deg` }], })); return ( {isEditMode && } {label} ); }); function TileIcon({ icon, label }: { icon?: string; label: string }) { const c = useColors(); if (icon) { return {icon}; } return {label.charAt(0)}; } function DeleteButton({ onPress }: { onPress: () => void }) { const c = useColors(); return ( ); } interface EditGridButtonProps { size: number; isEditMode: boolean; onAdd: () => void; onDone: () => void; } function EditGridButton({ size, isEditMode, onAdd, onDone }: EditGridButtonProps) { const c = useColors(); return ( {isEditMode ? ( Done ) : ( )} ); } const styles = StyleSheet.create({ grid: { flexDirection: 'row', flexWrap: 'wrap', gap: GAP, }, tileWrapper: { alignItems: 'center', gap: spacing.xs, }, tile: { aspectRatio: 1, width: '100%', borderRadius: radius.md, alignItems: 'center', justifyContent: 'center', }, editButton: { borderWidth: 1.5, borderStyle: 'dashed', }, emoji: { fontSize: ICON_SIZE, }, iconFallback: { fontSize: ICON_SIZE, fontWeight: '600', }, label: { fontSize: LABEL_SIZE, textAlign: 'center', }, doneLabel: { fontSize: 13, fontWeight: '600', }, deleteBtn: { position: 'absolute', top: -6, left: -6, width: 18, height: 18, borderRadius: 9, alignItems: 'center', justifyContent: 'center', }, });