307 lines
12 KiB
TypeScript
307 lines
12 KiB
TypeScript
import { useState, useCallback } from "react";
|
||
import { View, Text, ScrollView, Pressable, 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 { Rating } from "../../components/ui/rating";
|
||
import { Button } from "../../components/ui/button";
|
||
import { Avatar } from "../../components/ui/avatar";
|
||
import { Loading } from "../../components/ui/loading";
|
||
import { Card } from "../../components/ui/card";
|
||
import { ReviewCard } from "../../components/review-card";
|
||
import { getCategoryName, CATEGORY_EMOJI } from "../../lib/constants";
|
||
|
||
export default function AdDetailScreen() {
|
||
const { id } = useLocalSearchParams<{ id: string }>();
|
||
const theme = useTheme();
|
||
const router = useRouter();
|
||
const { token, isAuthenticated, userId } = useAuth();
|
||
|
||
const ad = useQuery(api.ads.getById, id ? { id: id as any } : "skip");
|
||
const handyman = useQuery(
|
||
api.users.getById,
|
||
ad ? { id: ad.handymanId } : "skip"
|
||
);
|
||
const reviews = useQuery(
|
||
api.reviews.getByAd,
|
||
id ? { adId: id as any } : "skip"
|
||
);
|
||
|
||
const reviewCustomerIds = reviews
|
||
? [...new Set(reviews.map((r) => r.customerId))]
|
||
: [];
|
||
const customersById: Record<string, any> = {};
|
||
for (let i = 0; i < Math.min(reviewCustomerIds.length, 5); i++) {
|
||
const customer = useQuery(
|
||
api.users.getById,
|
||
reviewCustomerIds[i] ? { id: reviewCustomerIds[i] as any } : "skip"
|
||
);
|
||
if (customer) customersById[reviewCustomerIds[i] as string] = customer;
|
||
}
|
||
|
||
const deleteAd = useMutation(api.ads.remove);
|
||
const startChat = useMutation(api.chats.getOrCreate);
|
||
const [startingChat, setStartingChat] = useState(false);
|
||
const [refreshing, setRefreshing] = useState(false);
|
||
|
||
const onRefresh = useCallback(() => {
|
||
setRefreshing(true);
|
||
setTimeout(() => setRefreshing(false), 800);
|
||
}, []);
|
||
|
||
async function handleStartChat() {
|
||
if (!token || !ad) return;
|
||
setStartingChat(true);
|
||
try {
|
||
const chatId = await startChat({ token, partnerId: ad.handymanId });
|
||
router.push(`/chat/${chatId}` as any);
|
||
} catch (e: any) {
|
||
Alert.alert("Грешка", e.message || "Неуспешно започнување разговор");
|
||
} finally {
|
||
setStartingChat(false);
|
||
}
|
||
}
|
||
|
||
if (!ad) {
|
||
return (
|
||
<>
|
||
<Stack.Screen options={{ title: "Оглас", headerShown: true, headerBackButtonDisplayMode: "minimal" }} />
|
||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: theme.colors.background }}>
|
||
<Loading />
|
||
</View>
|
||
</>
|
||
);
|
||
}
|
||
|
||
const isOwner = userId === ad.handymanId;
|
||
const categoryName = getCategoryName(ad.category);
|
||
const categoryEmoji = CATEGORY_EMOJI[ad.category] || "📋";
|
||
const existingUserReview = reviews?.find((r) => r.customerId === userId);
|
||
|
||
const adId = ad._id;
|
||
|
||
function handleDelete() {
|
||
Alert.alert(
|
||
"Избриши оглас",
|
||
"Дали сте сигурни дека сакате да го избришете овој оглас?",
|
||
[
|
||
{ text: "Откажи", style: "cancel" },
|
||
{
|
||
text: "Избриши",
|
||
style: "destructive",
|
||
onPress: async () => {
|
||
try {
|
||
await deleteAd({ token: token!, adId: adId as any });
|
||
router.back();
|
||
} catch (e: any) {
|
||
Alert.alert("Грешка", e.message || "Неуспешно бришење");
|
||
}
|
||
},
|
||
},
|
||
]
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<Stack.Screen
|
||
options={{
|
||
title: ad.title.length > 20 ? ad.title.slice(0, 20) + "…" : ad.title,
|
||
headerShown: true,
|
||
headerBackButtonDisplayMode: "minimal",
|
||
}}
|
||
/>
|
||
<ScrollView
|
||
contentInsetAdjustmentBehavior="automatic"
|
||
refreshControl={
|
||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.colors.primary} />
|
||
}
|
||
contentContainerStyle={{ paddingBottom: 100 }}
|
||
>
|
||
<View style={{ backgroundColor: theme.colors.surface, ...theme.shadows.xs }}>
|
||
<View style={{ alignItems: "center", paddingVertical: theme.spacing.xl }}>
|
||
<View
|
||
style={{
|
||
width: 88,
|
||
height: 88,
|
||
borderRadius: 44,
|
||
backgroundColor: theme.colors.primaryLight,
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
...theme.shadows.sm,
|
||
}}
|
||
>
|
||
<Text style={{ fontSize: 44 }}>{categoryEmoji}</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
<View style={{ padding: theme.spacing.md, gap: theme.spacing.md, marginTop: -theme.spacing.md }}>
|
||
<Card variant="flat">
|
||
<View style={{ gap: theme.spacing.sm }}>
|
||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "flex-start" }}>
|
||
<Text style={{ ...theme.typography.h3, color: theme.colors.text, flex: 1, fontSize: 19 }}>
|
||
{ad.title}
|
||
</Text>
|
||
{ad.priceRange && (
|
||
<View style={{ backgroundColor: theme.colors.primaryLight, paddingHorizontal: 14, paddingVertical: 6, borderRadius: theme.radius.full }}>
|
||
<Text style={{ ...theme.typography.bodyBold, color: theme.colors.primary, fontSize: 14 }}>
|
||
{ad.priceRange}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.sm, flexWrap: "wrap" }}>
|
||
<Badge label={categoryName} variant="primary" size="sm" />
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
|
||
<Ionicons name="location-outline" size={14} color={theme.colors.textTertiary} />
|
||
<Text style={{ ...theme.typography.caption, color: theme.colors.textSecondary }}>
|
||
{ad.location}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{ad.ratingAvg != null && ad.ratingAvg > 0 && (
|
||
<Rating value={ad.ratingAvg} count={ad.reviewCount} size={14} />
|
||
)}
|
||
</View>
|
||
</Card>
|
||
|
||
{ad.description && (
|
||
<Card variant="flat">
|
||
<Text style={{ ...theme.typography.captionBold, color: theme.colors.text, marginBottom: theme.spacing.xs, fontSize: 13 }}>
|
||
Опис
|
||
</Text>
|
||
<Text style={{ ...theme.typography.body, color: theme.colors.textSecondary, lineHeight: 22 }}>
|
||
{ad.description}
|
||
</Text>
|
||
</Card>
|
||
)}
|
||
|
||
{ad.availability && (
|
||
<Card variant="flat">
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.sm }}>
|
||
<Ionicons name="time-outline" size={18} color={theme.colors.primary} />
|
||
<Text style={{ ...theme.typography.body, color: theme.colors.textSecondary }}>
|
||
{ad.availability}
|
||
</Text>
|
||
</View>
|
||
</Card>
|
||
)}
|
||
|
||
<Card variant="flat">
|
||
<Pressable
|
||
accessible
|
||
accessibilityLabel={`Профил на ${handyman?.name || "Мајстор"}`}
|
||
accessibilityRole="link"
|
||
onPress={() => {
|
||
if (handyman) router.push("/(tabs)/(profile)");
|
||
}}
|
||
style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.md }}
|
||
>
|
||
<Avatar uri={handyman?.avatarId ?? null} name={handyman?.name || "Мајстор"} size={52} />
|
||
<View style={{ flex: 1 }}>
|
||
<Text style={{ ...theme.typography.bodyBold, color: theme.colors.text, fontSize: 15 }}>
|
||
{handyman?.name || "Мајстор"}
|
||
</Text>
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
|
||
<Ionicons name="construct-outline" size={14} color={theme.colors.primary} />
|
||
<Text style={{ ...theme.typography.caption, color: theme.colors.textSecondary }}>
|
||
Мајстор
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
<Ionicons name="chevron-forward" size={18} color={theme.colors.textTertiary} />
|
||
</Pressable>
|
||
</Card>
|
||
|
||
{!isOwner && isAuthenticated && (
|
||
<View style={{ gap: theme.spacing.sm }}>
|
||
<Button loading={startingChat} onPress={handleStartChat} fullWidth>
|
||
<Ionicons name="chatbubble" size={18} color={theme.colors.textInverse} style={{ marginRight: 6 }} />
|
||
Започни разговор
|
||
</Button>
|
||
{!existingUserReview && (
|
||
<Button
|
||
variant="outline"
|
||
onPress={() => router.push(`/review/create?adId=${ad._id}` as any)}
|
||
fullWidth
|
||
>
|
||
<Ionicons name="star" size={18} color={theme.colors.primary} style={{ marginRight: 6 }} />
|
||
Напиши оценка
|
||
</Button>
|
||
)}
|
||
{existingUserReview && (
|
||
<Text style={{ ...theme.typography.caption, color: theme.colors.textTertiary, textAlign: "center" }}>
|
||
Веќе напишавте оценка за овој оглас
|
||
</Text>
|
||
)}
|
||
</View>
|
||
)}
|
||
|
||
{!isOwner && !isAuthenticated && (
|
||
<Card variant="flat">
|
||
<Text style={{ ...theme.typography.body, color: theme.colors.textSecondary, textAlign: "center", marginBottom: theme.spacing.md }}>
|
||
Најавете се за да започнете разговор
|
||
</Text>
|
||
<Button onPress={() => router.push("/(auth)/login")} fullWidth>
|
||
Најави се
|
||
</Button>
|
||
</Card>
|
||
)}
|
||
|
||
{isOwner && (
|
||
<View style={{ gap: theme.spacing.sm }}>
|
||
<Button
|
||
variant="outline"
|
||
onPress={() => router.push(`/ad/create?editId=${ad._id}` as any)}
|
||
fullWidth
|
||
>
|
||
<Ionicons name="pencil" size={18} color={theme.colors.primary} style={{ marginRight: 6 }} />
|
||
Уреди оглас
|
||
</Button>
|
||
<Button variant="destructive" onPress={handleDelete} fullWidth>
|
||
<Ionicons name="trash" size={18} color={theme.colors.textInverse} style={{ marginRight: 6 }} />
|
||
Избриши оглас
|
||
</Button>
|
||
</View>
|
||
)}
|
||
|
||
{reviews && reviews.length > 0 && (
|
||
<View style={{ gap: theme.spacing.md }}>
|
||
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
|
||
<Text style={{ ...theme.typography.h4, color: theme.colors.text }}>
|
||
Оцени ({ad.reviewCount})
|
||
</Text>
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
|
||
<Ionicons name="star" size={16} color={theme.colors.star} />
|
||
<Text style={{ ...theme.typography.bodyBold, color: theme.colors.star }}>
|
||
{ad.ratingAvg?.toFixed(1)}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{reviews.map((review) => {
|
||
const customer = customersById[review.customerId as string];
|
||
return (
|
||
<ReviewCard
|
||
key={review._id}
|
||
review={review}
|
||
customerName={customer?.name || "Клиент"}
|
||
customerAvatarId={customer?.avatarId}
|
||
/>
|
||
);
|
||
})}
|
||
</View>
|
||
)}
|
||
</View>
|
||
</ScrollView>
|
||
</>
|
||
);
|
||
}
|