179 lines
8.0 KiB
TypeScript
179 lines
8.0 KiB
TypeScript
import { useCallback, useState } from "react";
|
||
import { View, Text, ScrollView, Alert, RefreshControl } from "react-native";
|
||
import { useLocalSearchParams, Stack, useRouter } from "expo-router";
|
||
import { Ionicons } from "@expo/vector-icons";
|
||
import { useQuery, useMutation } from "convex/react";
|
||
import { api } from "../../convex/_generated/api";
|
||
import { useAuth } from "../_layout";
|
||
import { useTheme } from "../../components/theme";
|
||
import { Badge } from "../../components/ui/badge";
|
||
import { Button } from "../../components/ui/button";
|
||
import { Card } from "../../components/ui/card";
|
||
import { Loading } from "../../components/ui/loading";
|
||
import { getCategoryName } from "../../lib/constants";
|
||
|
||
export default function PostDetailScreen() {
|
||
const { id } = useLocalSearchParams<{ id: string }>();
|
||
const router = useRouter();
|
||
const theme = useTheme();
|
||
const { token, userId, isAuthenticated } = useAuth();
|
||
|
||
const post = useQuery(api.posts.getById, { id: id as any });
|
||
const currentUser = useQuery(
|
||
api.users.getCurrentUser,
|
||
isAuthenticated && token ? { token } : "skip"
|
||
);
|
||
|
||
const closePost = useMutation(api.posts.close);
|
||
const getOrCreateChat = useMutation(api.chats.getOrCreate);
|
||
const [refreshing, setRefreshing] = useState(false);
|
||
|
||
const onRefresh = useCallback(() => {
|
||
setRefreshing(true);
|
||
setTimeout(() => setRefreshing(false), 800);
|
||
}, []);
|
||
|
||
if (!post) {
|
||
return (
|
||
<>
|
||
<Stack.Screen options={{ title: "Побарување", headerShown: true, headerBackButtonDisplayMode: "minimal" }} />
|
||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: theme.colors.background }}>
|
||
<Loading />
|
||
</View>
|
||
</>
|
||
);
|
||
}
|
||
|
||
const customerId = post.customerId as string;
|
||
const isOwner = isAuthenticated && userId && customerId === userId;
|
||
const isHandyman = currentUser?.role === "handyman";
|
||
const isOpen = post.status === "open";
|
||
|
||
async function handleClose() {
|
||
Alert.alert(
|
||
"Затвори побарување",
|
||
"Дали сте сигурни дека сакате да го затворите оваа побарување?",
|
||
[
|
||
{ text: "Откажи", style: "cancel" },
|
||
{
|
||
text: "Затвори",
|
||
style: "destructive",
|
||
onPress: async () => {
|
||
try {
|
||
await closePost({ token: token!, postId: id as any });
|
||
} catch (e: any) {
|
||
Alert.alert("Грешка", e.message || "Неуспешно затворање");
|
||
}
|
||
},
|
||
},
|
||
]
|
||
);
|
||
}
|
||
|
||
async function handleRespond() {
|
||
try {
|
||
const chatId = await getOrCreateChat({ token: token!, partnerId: post!.customerId });
|
||
router.push(`/chat/${chatId}` as any);
|
||
} catch (e: any) {
|
||
Alert.alert("Грешка", e.message || "Неуспешно поврзување");
|
||
}
|
||
}
|
||
|
||
const categoryName = post.category ? getCategoryName(post.category) : null;
|
||
const timeAgo = getTimeAgo(post.createdAt);
|
||
|
||
return (
|
||
<>
|
||
<Stack.Screen options={{ title: "Побарување", headerShown: true, headerBackButtonDisplayMode: "minimal" }} />
|
||
<ScrollView
|
||
contentInsetAdjustmentBehavior="automatic"
|
||
refreshControl={
|
||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.colors.primary} />
|
||
}
|
||
contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.md, paddingBottom: 100 }}
|
||
>
|
||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "flex-start", gap: theme.spacing.sm }}>
|
||
<View style={{ flex: 1 }}>
|
||
<Text style={{ ...theme.typography.h2, color: theme.colors.text, letterSpacing: -0.3, fontSize: 24 }}>
|
||
{post.title}
|
||
</Text>
|
||
</View>
|
||
<Badge label={isOpen ? "Отворено" : "Затворено"} variant={isOpen ? "success" : "error"} />
|
||
</View>
|
||
|
||
<Card variant="elevated">
|
||
<View style={{ gap: theme.spacing.md }}>
|
||
{categoryName && (
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.sm }}>
|
||
<Ionicons name="folder-outline" size={18} color={theme.colors.textSecondary} />
|
||
<Text style={{ ...theme.typography.body, color: theme.colors.textSecondary }}>Категорија:</Text>
|
||
<Badge label={categoryName} variant="primary" />
|
||
</View>
|
||
)}
|
||
{post.location && (
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.sm }}>
|
||
<Ionicons name="location-outline" size={18} color={theme.colors.textSecondary} />
|
||
<Text style={{ ...theme.typography.body, color: theme.colors.textSecondary }}>Локација:</Text>
|
||
<Text selectable style={{ ...theme.typography.bodyBold, color: theme.colors.text }}>{post.location}</Text>
|
||
</View>
|
||
)}
|
||
{post.budget && (
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.sm }}>
|
||
<Ionicons name="wallet-outline" size={18} color={theme.colors.primary} />
|
||
<Text style={{ ...theme.typography.body, color: theme.colors.textSecondary }}>Буџет:</Text>
|
||
<Text selectable style={{ ...theme.typography.bodyBold, color: theme.colors.primary }}>{post.budget}</Text>
|
||
</View>
|
||
)}
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.sm }}>
|
||
<Ionicons name="time-outline" size={18} color={theme.colors.textSecondary} />
|
||
<Text style={{ ...theme.typography.body, color: theme.colors.textSecondary }}>Објавено:</Text>
|
||
<Text style={{ ...theme.typography.body, color: theme.colors.text }}>{timeAgo}</Text>
|
||
</View>
|
||
</View>
|
||
</Card>
|
||
|
||
<Card variant="elevated">
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.sm, marginBottom: theme.spacing.sm }}>
|
||
<Ionicons name="document-text-outline" size={20} color={theme.colors.primary} />
|
||
<Text style={{ ...theme.typography.captionBold, color: theme.colors.text, fontSize: 13 }}>Опис</Text>
|
||
</View>
|
||
<Text style={{ ...theme.typography.body, color: theme.colors.textSecondary, lineHeight: 24 }}>
|
||
{post.description}
|
||
</Text>
|
||
</Card>
|
||
</ScrollView>
|
||
|
||
{isOwner && isOpen && (
|
||
<View style={{ position: "absolute", bottom: 0, left: 0, right: 0, padding: theme.spacing.lg, backgroundColor: theme.colors.background, borderTopWidth: 1, borderTopColor: theme.colors.borderLight, ...theme.shadows.xs }}>
|
||
<Button variant="destructive" onPress={handleClose} fullWidth>
|
||
<Ionicons name="close-circle" size={18} color={theme.colors.textInverse} style={{ marginRight: 4 }} />
|
||
Затвори оглас
|
||
</Button>
|
||
</View>
|
||
)}
|
||
|
||
{!isOwner && isHandyman && isOpen && isAuthenticated && (
|
||
<View style={{ position: "absolute", bottom: 0, left: 0, right: 0, padding: theme.spacing.lg, backgroundColor: theme.colors.background, borderTopWidth: 1, borderTopColor: theme.colors.borderLight, ...theme.shadows.xs }}>
|
||
<Button onPress={handleRespond} fullWidth>
|
||
<Ionicons name="chatbubble" size={18} color={theme.colors.textInverse} style={{ marginRight: 4 }} />
|
||
Одговори
|
||
</Button>
|
||
</View>
|
||
)}
|
||
</>
|
||
);
|
||
}
|
||
|
||
function getTimeAgo(timestamp: number): string {
|
||
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
||
if (seconds < 60) return "Пред неколку секунди";
|
||
const minutes = Math.floor(seconds / 60);
|
||
if (minutes < 60) return `Пред ${minutes} мин`;
|
||
const hours = Math.floor(minutes / 60);
|
||
if (hours < 24) return `Пред ${hours} ч`;
|
||
const days = Math.floor(hours / 24);
|
||
if (days < 30) return `Пред ${days} ден`;
|
||
const months = Math.floor(days / 30);
|
||
return `Пред ${months} месеци`;
|
||
}
|