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,195 @@
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import React, { useRef } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Animated,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Swipeable } from "react-native-gesture-handler";
|
||||
import { useColors } from "@/hooks/useColors";
|
||||
import { PostizPost } from "@/context/PostizContext";
|
||||
import { StatusBadge } from "./StatusBadge";
|
||||
|
||||
interface PostCardProps {
|
||||
post: PostizPost;
|
||||
onDelete: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
try {
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
function getNetworkIcon(type?: string): React.ComponentProps<typeof Feather>["name"] {
|
||||
const t = (type ?? "").toLowerCase();
|
||||
if (t.includes("twitter") || t.includes("x")) return "twitter";
|
||||
if (t.includes("linkedin")) return "linkedin";
|
||||
if (t.includes("instagram")) return "instagram";
|
||||
if (t.includes("facebook")) return "facebook";
|
||||
if (t.includes("youtube")) return "youtube";
|
||||
return "globe";
|
||||
}
|
||||
|
||||
export function PostCard({ post, onDelete }: PostCardProps) {
|
||||
const colors = useColors();
|
||||
const swipeRef = useRef<Swipeable>(null);
|
||||
|
||||
const handleDelete = () => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
Alert.alert("Delete Post", "Are you sure you want to delete this post?", [
|
||||
{
|
||||
text: "Cancel",
|
||||
style: "cancel",
|
||||
onPress: () => swipeRef.current?.close(),
|
||||
},
|
||||
{
|
||||
text: "Delete",
|
||||
style: "destructive",
|
||||
onPress: async () => {
|
||||
await onDelete(post.id);
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const renderRightActions = (
|
||||
_progress: Animated.AnimatedInterpolation<number>,
|
||||
dragX: Animated.AnimatedInterpolation<number>
|
||||
) => {
|
||||
const scale = dragX.interpolate({
|
||||
inputRange: [-80, 0],
|
||||
outputRange: [1, 0.8],
|
||||
extrapolate: "clamp",
|
||||
});
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.deleteAction, { backgroundColor: colors.destructive }]}
|
||||
onPress={handleDelete}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Animated.View style={{ transform: [{ scale }] }}>
|
||||
<Feather name="trash-2" size={20} color="#fff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const integrations = post.integrations ?? (post.integration ? [post.integration] : []);
|
||||
const truncatedContent =
|
||||
post.content.length > 140
|
||||
? post.content.slice(0, 140) + "…"
|
||||
: post.content;
|
||||
|
||||
return (
|
||||
<Swipeable
|
||||
ref={swipeRef}
|
||||
renderRightActions={renderRightActions}
|
||||
rightThreshold={40}
|
||||
friction={2}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.card,
|
||||
{ backgroundColor: colors.card, borderBottomColor: colors.border },
|
||||
]}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.integrations}>
|
||||
{integrations.slice(0, 3).map((intg) => (
|
||||
<View
|
||||
key={intg.id}
|
||||
style={[
|
||||
styles.networkIcon,
|
||||
{ backgroundColor: colors.secondary },
|
||||
]}
|
||||
>
|
||||
<Feather
|
||||
name={getNetworkIcon(intg.type ?? intg.internalType)}
|
||||
size={12}
|
||||
color={colors.mutedForeground}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
{integrations.length > 3 && (
|
||||
<Text style={[styles.moreText, { color: colors.mutedForeground }]}>
|
||||
+{integrations.length - 3}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<StatusBadge status={post.status} />
|
||||
</View>
|
||||
<Text style={[styles.content, { color: colors.foreground }]}>
|
||||
{truncatedContent}
|
||||
</Text>
|
||||
<View style={styles.footer}>
|
||||
<Feather name="clock" size={12} color={colors.mutedForeground} />
|
||||
<Text style={[styles.date, { color: colors.mutedForeground }]}>
|
||||
{formatDate(post.publishDate)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Swipeable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 14,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
gap: 8,
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
integrations: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
},
|
||||
networkIcon: {
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
moreText: {
|
||||
fontSize: 11,
|
||||
fontFamily: "Inter_500Medium",
|
||||
},
|
||||
content: {
|
||||
fontSize: 14,
|
||||
fontFamily: "Inter_400Regular",
|
||||
lineHeight: 20,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
},
|
||||
date: {
|
||||
fontSize: 12,
|
||||
fontFamily: "Inter_400Regular",
|
||||
},
|
||||
deleteAction: {
|
||||
width: 72,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user