248 lines
7.7 KiB
TypeScript
248 lines
7.7 KiB
TypeScript
import { Feather } from "@expo/vector-icons";
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import axios from "axios";
|
|
import React, { useState } from "react";
|
|
import {
|
|
ActivityIndicator,
|
|
Alert,
|
|
FlatList,
|
|
Platform,
|
|
RefreshControl,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableOpacity,
|
|
View,
|
|
} from "react-native";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
import { PostCard } from "@/components/PostCard";
|
|
import { PostizPost, usePostiz } from "@/context/PostizContext";
|
|
import { useColors } from "@/hooks/useColors";
|
|
|
|
function extractError(err: unknown): string {
|
|
if (axios.isAxiosError(err)) {
|
|
const status = err.response?.status;
|
|
const data = err.response?.data;
|
|
if (data) {
|
|
const body =
|
|
typeof data === "string"
|
|
? data.slice(0, 200)
|
|
: (data?.message ?? data?.error ?? JSON.stringify(data)).toString().slice(0, 200);
|
|
return status ? `HTTP ${status}: ${body}` : body;
|
|
}
|
|
if (status) return `HTTP ${status} — ${err.message}`;
|
|
if (err.message) return err.message;
|
|
}
|
|
if (err instanceof Error) return err.message;
|
|
return "Unknown error";
|
|
}
|
|
|
|
type FilterType = "all" | "QUEUE" | "PUBLISHED" | "ERROR" | "DRAFT";
|
|
|
|
const FILTERS: { key: FilterType; label: string }[] = [
|
|
{ key: "all", label: "All" },
|
|
{ key: "QUEUE", label: "Queue" },
|
|
{ key: "PUBLISHED", label: "Published" },
|
|
{ key: "DRAFT", label: "Draft" },
|
|
{ key: "ERROR", label: "Error" },
|
|
];
|
|
|
|
export default function PostsScreen() {
|
|
const colors = useColors();
|
|
const insets = useSafeAreaInsets();
|
|
const { client, isConfigured } = usePostiz();
|
|
const queryClient = useQueryClient();
|
|
const [filter, setFilter] = useState<FilterType>("all");
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
const start = new Date();
|
|
start.setMonth(start.getMonth() - 3);
|
|
const end = new Date();
|
|
end.setMonth(end.getMonth() + 6);
|
|
|
|
const { data: posts, isLoading, error, refetch } = useQuery<PostizPost[]>({
|
|
queryKey: ["posts-list", !!client],
|
|
queryFn: async () => {
|
|
if (!client) return [];
|
|
const res = await client.get("posts", {
|
|
params: {
|
|
startDate: start.toISOString(),
|
|
endDate: end.toISOString(),
|
|
},
|
|
});
|
|
return Array.isArray(res.data) ? res.data : res.data?.posts ?? [];
|
|
},
|
|
enabled: !!client,
|
|
retry: 1,
|
|
staleTime: 0,
|
|
});
|
|
|
|
const filteredPosts =
|
|
filter === "all"
|
|
? posts ?? []
|
|
: (posts ?? []).filter((p) => p.state === filter);
|
|
|
|
const handleRefresh = async () => {
|
|
setRefreshing(true);
|
|
await refetch();
|
|
setRefreshing(false);
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
if (!client) return;
|
|
try {
|
|
await client.delete(`posts/${id}`);
|
|
queryClient.setQueryData<PostizPost[]>(["posts-list", true], (old) =>
|
|
(old ?? []).filter((p) => p.id !== id)
|
|
);
|
|
queryClient.invalidateQueries({ queryKey: ["posts"] });
|
|
} catch (e: unknown) {
|
|
const msg = extractError(e);
|
|
Alert.alert("Delete failed", msg);
|
|
}
|
|
};
|
|
|
|
if (!isConfigured) {
|
|
return (
|
|
<View
|
|
style={[
|
|
styles.centered,
|
|
{ backgroundColor: colors.background, paddingTop: Platform.OS === "web" ? 67 : 0 },
|
|
]}
|
|
>
|
|
<Feather name="lock" size={32} color={colors.mutedForeground} />
|
|
<Text style={[styles.emptyTitle, { color: colors.foreground }]}>
|
|
Not Configured
|
|
</Text>
|
|
<Text style={[styles.emptyText, { color: colors.mutedForeground }]}>
|
|
Add your API key in Settings
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View
|
|
style={[
|
|
styles.container,
|
|
{
|
|
backgroundColor: colors.background,
|
|
paddingTop: Platform.OS === "web" ? 67 : 0,
|
|
},
|
|
]}
|
|
>
|
|
<FlatList
|
|
horizontal
|
|
data={FILTERS}
|
|
keyExtractor={(item) => item.key}
|
|
showsHorizontalScrollIndicator={false}
|
|
contentContainerStyle={styles.filterList}
|
|
renderItem={({ item }) => (
|
|
<TouchableOpacity
|
|
onPress={() => setFilter(item.key)}
|
|
activeOpacity={0.7}
|
|
style={[
|
|
styles.filterChip,
|
|
{
|
|
backgroundColor:
|
|
filter === item.key ? colors.primary : colors.secondary,
|
|
borderColor:
|
|
filter === item.key ? colors.primary : colors.border,
|
|
},
|
|
]}
|
|
>
|
|
<Text
|
|
style={[
|
|
styles.filterText,
|
|
{
|
|
color:
|
|
filter === item.key
|
|
? colors.primaryForeground
|
|
: colors.mutedForeground,
|
|
},
|
|
]}
|
|
>
|
|
{item.label}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
style={[styles.filterBar, { borderBottomColor: colors.border }]}
|
|
/>
|
|
|
|
{isLoading ? (
|
|
<View style={styles.centered}>
|
|
<ActivityIndicator color={colors.primary} size="large" />
|
|
</View>
|
|
) : error ? (
|
|
<View style={styles.centered}>
|
|
<Feather name="alert-circle" size={28} color={colors.error} />
|
|
<Text style={[styles.emptyTitle, { color: colors.foreground }]}>
|
|
Failed to load
|
|
</Text>
|
|
<Text style={[styles.emptyText, { color: colors.mutedForeground }]} selectable>
|
|
{extractError(error)}
|
|
</Text>
|
|
<TouchableOpacity
|
|
onPress={() => refetch()}
|
|
style={[styles.retryBtn, { backgroundColor: colors.primary }]}
|
|
>
|
|
<Text style={[styles.retryText, { color: colors.primaryForeground }]}>
|
|
Try Again
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
) : (
|
|
<FlatList
|
|
data={filteredPosts}
|
|
keyExtractor={(item) => item.id}
|
|
renderItem={({ item }) => (
|
|
<PostCard post={item} onDelete={handleDelete} />
|
|
)}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={refreshing}
|
|
onRefresh={handleRefresh}
|
|
tintColor={colors.primary}
|
|
/>
|
|
}
|
|
contentInsetAdjustmentBehavior="automatic"
|
|
showsVerticalScrollIndicator={false}
|
|
ListEmptyComponent={
|
|
<View style={styles.emptyState}>
|
|
<Feather name="inbox" size={36} color={colors.mutedForeground} />
|
|
<Text style={[styles.emptyTitle, { color: colors.foreground }]}>
|
|
No posts
|
|
</Text>
|
|
<Text style={[styles.emptyText, { color: colors.mutedForeground }]}>
|
|
{filter === "all"
|
|
? "No posts found in the last 3 months"
|
|
: `No ${filter.toLowerCase()} posts`}
|
|
</Text>
|
|
</View>
|
|
}
|
|
scrollEnabled={filteredPosts.length > 0}
|
|
/>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1 },
|
|
centered: {
|
|
flex: 1,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: 10,
|
|
paddingHorizontal: 32,
|
|
},
|
|
filterBar: { borderBottomWidth: StyleSheet.hairlineWidth, flexGrow: 0 },
|
|
filterList: { paddingHorizontal: 16, paddingVertical: 10, gap: 8 },
|
|
filterChip: { paddingHorizontal: 14, paddingVertical: 6, borderRadius: 20, borderWidth: 1 },
|
|
filterText: { fontSize: 13, fontFamily: "Inter_500Medium" },
|
|
emptyState: { alignItems: "center", paddingTop: 64, gap: 10 },
|
|
emptyTitle: { fontSize: 18, fontFamily: "Inter_600SemiBold" },
|
|
emptyText: { fontSize: 14, fontFamily: "Inter_400Regular", textAlign: "center" },
|
|
retryBtn: { marginTop: 4, paddingHorizontal: 20, paddingVertical: 10, borderRadius: 10 },
|
|
retryText: { fontSize: 14, fontFamily: "Inter_600SemiBold" },
|
|
});
|