Add core functionality for mobile post scheduling app
Adds necessary dependencies including axios and react-native-calendars to pnpm-lock.yaml. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7b0991ce-c7b8-4c82-9acc-fd3f9e762a01 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: dc1266fa-8375-43e1-aca0-9df31350f647 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/86064bd6-c937-4ca5-a5bf-bbef5749fb60/7b0991ce-c7b8-4c82-9acc-fd3f9e762a01/kWnlAIM Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
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";
|
||||
|
||||
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"],
|
||||
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,
|
||||
});
|
||||
|
||||
const filteredPosts =
|
||||
filter === "all"
|
||||
? posts ?? []
|
||||
: (posts ?? []).filter((p) => p.status === 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"], (old) =>
|
||||
(old ?? []).filter((p) => p.id !== id)
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ["posts"] });
|
||||
} catch (e) {
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
<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",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user