import { useState, useEffect, useRef } from 'react'; import { View, Text, Modal, TouchableOpacity, TextInput, FlatList, StyleSheet, Animated, Dimensions, KeyboardAvoidingView, Platform, ActivityIndicator, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useQuery, useMutation } from '@tanstack/react-query'; import { queryClient } from '@/lib/queryClient'; import { API_BASE_URL } from '@/lib/config'; interface Comment { id: string; articleId: string; nickname: string; content: string; createdAt: string; } interface CommentsDrawerProps { articleId: string; isOpen: boolean; onClose: () => void; } const SCREEN_HEIGHT = Dimensions.get('window').height; const DRAWER_HEIGHT = SCREEN_HEIGHT * 0.6; export default function CommentsDrawer({ articleId, isOpen, onClose, }: CommentsDrawerProps) { const [nickname, setNickname] = useState(''); const [commentContent, setCommentContent] = useState(''); const slideAnim = useRef(new Animated.Value(DRAWER_HEIGHT)).current; const opacityAnim = useRef(new Animated.Value(0)).current; // Load saved nickname from AsyncStorage useEffect(() => { loadNickname(); }, []); const loadNickname = async () => { try { const saved = await AsyncStorage.getItem('commentNickname'); if (saved) { setNickname(saved); } } catch (error) { console.error('Failed to load nickname:', error); } }; // Animate drawer on open/close useEffect(() => { if (isOpen) { Animated.parallel([ Animated.timing(slideAnim, { toValue: 0, duration: 300, useNativeDriver: true, }), Animated.timing(opacityAnim, { toValue: 1, duration: 200, useNativeDriver: true, }), ]).start(); } else { Animated.parallel([ Animated.timing(slideAnim, { toValue: DRAWER_HEIGHT, duration: 300, useNativeDriver: true, }), Animated.timing(opacityAnim, { toValue: 0, duration: 200, useNativeDriver: true, }), ]).start(); } }, [isOpen]); // Fetch comments const { data: commentsData, isLoading } = useQuery<{ comments: Comment[]; total: number; }>({ queryKey: ['/api/v1/comments', articleId], queryFn: async () => { const response = await fetch(`${API_BASE_URL}/comments?articleId=${articleId}`); return response.json(); }, enabled: isOpen && !!articleId, }); // Create comment mutation const createComment = useMutation({ mutationFn: async (data: { content: string; nickname: string }) => { const response = await fetch(`${API_BASE_URL}/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ articleId, content: data.content, nickname: data.nickname, }), }); return response.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/v1/comments', articleId] }); queryClient.invalidateQueries({ queryKey: ['/api/v1/articles', articleId, 'comment-count'], }); setCommentContent(''); }, }); const handleSubmit = async () => { if (!commentContent.trim() || !nickname.trim()) return; try { await AsyncStorage.setItem('commentNickname', nickname); createComment.mutate({ content: commentContent, nickname: nickname, }); } catch (error) { console.error('Failed to save nickname:', error); } }; const formatTimeAgo = (dateString: string) => { const date = new Date(dateString); const now = new Date(); const seconds = Math.floor((now.getTime() - date.getTime()) / 1000); if (seconds < 60) return 'just now'; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 7) return `${days}d ago`; const weeks = Math.floor(days / 7); if (weeks < 4) return `${weeks}w ago`; const months = Math.floor(days / 30); if (months < 12) return `${months}mo ago`; const years = Math.floor(days / 365); return `${years}y ago`; }; const comments = commentsData?.comments || []; const total = commentsData?.total || 0; if (!isOpen) return null; return ( {/* Backdrop */} {/* Drawer */} {/* Header */} Comments ({total}) {/* Comments List */} {isLoading ? ( ) : comments.length === 0 ? ( No comments yet. Be the first to comment! ) : ( item.id} renderItem={({ item }) => ( {item.nickname.charAt(0).toUpperCase()} {item.nickname} {formatTimeAgo(item.createdAt)} {item.content} )} contentContainerStyle={styles.listContent} showsVerticalScrollIndicator={false} /> )} {/* Input Form */} {!nickname && ( )} {createComment.isPending ? ( ) : ( )} ); } const styles = StyleSheet.create({ container: { flex: 1, }, backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0, 0, 0, 0.3)', }, backdropTouchable: { flex: 1, }, drawer: { position: 'absolute', left: 0, right: 0, bottom: 0, height: DRAWER_HEIGHT, backgroundColor: '#fff', borderTopLeftRadius: 16, borderTopRightRadius: 16, shadowColor: '#000', shadowOffset: { width: 0, height: -2 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 5, }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 16, borderBottomWidth: 1, borderBottomColor: '#e0e0e0', }, title: { fontSize: 18, fontWeight: '600', color: '#000', }, closeButton: { padding: 4, }, commentsContainer: { flex: 1, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 32, }, emptyText: { fontSize: 14, color: '#666', textAlign: 'center', }, listContent: { padding: 16, }, commentCard: { backgroundColor: '#f5f5f5', borderRadius: 12, padding: 12, marginBottom: 12, }, commentHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 8, }, avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#007AFF20', justifyContent: 'center', alignItems: 'center', marginRight: 8, }, avatarText: { fontSize: 14, fontWeight: '600', color: '#007AFF', }, commentMeta: { flex: 1, }, nickname: { fontSize: 14, fontWeight: '600', color: '#000', marginBottom: 2, }, timeAgo: { fontSize: 12, color: '#666', }, commentContent: { fontSize: 14, color: '#333', lineHeight: 20, }, inputContainer: { borderTopWidth: 1, borderTopColor: '#e0e0e0', padding: 16, backgroundColor: '#fff', }, nicknameInput: { borderWidth: 1, borderColor: '#e0e0e0', borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14, marginBottom: 12, color: '#000', }, commentInputRow: { flexDirection: 'row', gap: 8, }, commentInput: { flex: 1, borderWidth: 1, borderColor: '#e0e0e0', borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14, maxHeight: 80, color: '#000', }, sendButton: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#007AFF', justifyContent: 'center', alignItems: 'center', }, sendButtonDisabled: { backgroundColor: '#ccc', }, });