mojmajstor/app/ad/[id].tsx
2026-06-06 03:54:22 +02:00

307 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
</>
);
}