import { Feather } from "@expo/vector-icons"; import { useQuery } from "@tanstack/react-query"; import * as Clipboard from "expo-clipboard"; import * as Haptics from "expo-haptics"; import { router } from "expo-router"; import React, { useMemo, useState } from "react"; import { ActivityIndicator, Alert, FlatList, Platform, StyleSheet, Text, TouchableOpacity, View, } from "react-native"; import { Calendar, DateData } from "react-native-calendars"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { PostizPost, usePostiz } from "@/context/PostizContext"; import { useColors } from "@/hooks/useColors"; import { StatusBadge } from "@/components/StatusBadge"; import { extractError } from "@/lib/extractError"; function formatDate(date: Date): string { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } function toDateKey(dateStr: string): string { try { return formatDate(new Date(dateStr)); } catch { return dateStr.slice(0, 10); } } function formatPostTime(dateStr: string): string { try { const d = new Date(dateStr); return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }); } catch { return ""; } } export default function CalendarScreen() { const colors = useColors(); const insets = useSafeAreaInsets(); const { client, isConfigured } = usePostiz(); const showContextMenu = (post: PostizPost) => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); const preview = post.content.slice(0, 60) + (post.content.length > 60 ? "…" : ""); const integrations = post.integrations ?? (post.integration ? [post.integration] : []); Alert.alert( post.state === "PUBLISHED" ? "Published post" : post.state === "QUEUE" ? "Scheduled post" : post.state === "ERROR" ? "Failed post" : "Draft", preview, [ { text: "Copy text", onPress: () => { Clipboard.setStringAsync(post.content); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); }, }, { text: post.state === "PUBLISHED" ? "Repost" : "Edit", onPress: () => router.push({ pathname: "/(tabs)/compose", params: { prefillContent: post.content, prefillIntegrationIds: integrations.map((i) => i.id).join(","), }, }), }, { text: "Cancel", style: "cancel" }, ] ); }; const now = new Date(); const [currentMonth, setCurrentMonth] = useState({ year: now.getFullYear(), month: now.getMonth() + 1, }); const [selectedDay, setSelectedDay] = useState(formatDate(now)); const startDate = useMemo(() => { const d = new Date(currentMonth.year, currentMonth.month - 1, 1); return d.toISOString(); }, [currentMonth]); const endDate = useMemo(() => { const d = new Date(currentMonth.year, currentMonth.month, 0, 23, 59, 59); return d.toISOString(); }, [currentMonth]); const { data: posts, isLoading, error, refetch } = useQuery({ queryKey: ["posts", startDate, endDate, !!client], queryFn: async () => { if (!client) return []; const res = await client.get("posts", { params: { startDate, endDate }, }); return Array.isArray(res.data) ? res.data : res.data?.posts ?? []; }, enabled: !!client, retry: 1, staleTime: 0, }); const markedDates = useMemo(() => { const marks: Record; selected?: boolean; selectedColor?: string; }> = {}; (posts ?? []).forEach((post) => { const key = toDateKey(post.publishDate); if (!marks[key]) marks[key] = { dots: [] }; const dotColor = post.state === "PUBLISHED" ? colors.success : post.state === "ERROR" ? colors.error : colors.primary; marks[key].dots = [...(marks[key].dots ?? []), { color: dotColor }]; }); if (selectedDay) { marks[selectedDay] = { ...(marks[selectedDay] ?? {}), selected: true, selectedColor: colors.primary, }; } return marks; }, [posts, selectedDay, colors]); const dayPosts = useMemo(() => { if (!selectedDay || !posts) return []; return posts.filter((p) => toDateKey(p.publishDate) === selectedDay); }, [posts, selectedDay]); if (!isConfigured) { return ( Not Configured Add your API key in Settings to get started router.push("/(tabs)/settings")} activeOpacity={0.8} > Open Settings ); } return ( setSelectedDay(day.dateString)} onMonthChange={(month: DateData) => { setCurrentMonth({ year: month.year, month: month.month }); }} /> {isLoading ? ( ) : error ? ( Failed to load posts {extractError(error)} refetch()} style={styles.retryBtn}> Retry ) : ( item.id} contentInsetAdjustmentBehavior="automatic" ListHeaderComponent={ selectedDay ? ( {new Date(selectedDay).toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", })} {dayPosts.length} post{dayPosts.length !== 1 ? "s" : ""} ) : null } ListEmptyComponent={ No posts scheduled router.push("/(tabs)/compose")} style={styles.composeHint} > Create a post } renderItem={({ item }) => ( showContextMenu(item)} > {formatPostTime(item.publishDate)} {item.content} )} scrollEnabled={dayPosts.length > 0} showsVerticalScrollIndicator={false} /> )} ); } const styles = StyleSheet.create({ container: { flex: 1 }, centered: { flex: 1, alignItems: "center", justifyContent: "center", gap: 10, paddingHorizontal: 32, }, emptyTitle: { fontSize: 18, fontFamily: "Inter_600SemiBold" }, emptyText: { fontSize: 14, fontFamily: "Inter_400Regular", textAlign: "center" }, btn: { marginTop: 8, paddingHorizontal: 20, paddingVertical: 10, borderRadius: 10 }, btnText: { fontSize: 14, fontFamily: "Inter_600SemiBold" }, divider: { height: StyleSheet.hairlineWidth }, dayHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingHorizontal: 20, paddingVertical: 12, }, dayHeaderText: { fontSize: 13, fontFamily: "Inter_500Medium" }, countText: { fontSize: 12, fontFamily: "Inter_400Regular" }, dayPost: { flexDirection: "row", alignItems: "flex-start", paddingHorizontal: 20, paddingVertical: 12, borderBottomWidth: StyleSheet.hairlineWidth, gap: 12, }, dayPostLeft: { flex: 1, gap: 4 }, timeText: { fontSize: 12, fontFamily: "Inter_600SemiBold" }, postContent: { fontSize: 13, fontFamily: "Inter_400Regular", lineHeight: 18 }, emptyDay: { alignItems: "center", paddingTop: 32, gap: 10 }, emptyDayText: { fontSize: 14, fontFamily: "Inter_400Regular" }, composeHint: { flexDirection: "row", alignItems: "center", gap: 6 }, composeHintText: { fontSize: 14, fontFamily: "Inter_500Medium" }, retryBtn: { marginTop: 4 }, retryText: { fontSize: 14, fontFamily: "Inter_500Medium" }, });