- Add create/update/remove ad mutations with auth & ownership checks - Ad detail screen: gallery placeholder, title, description, category badge, location, price range, availability, rating, handyman info card, start-chat and write-review buttons for non-owners, edit/delete for ad owner, auth redirect for anonymous - Create ad screen: 4-step form (category → title/description → location → price/availability/submit), handyman-only guard, edit mode via editId param - Profile screen: user avatar/name/role/email/phone, my-ads section for handymen with edit/delete, new-ad button, placeholder my-posts section for customers, proper logout mutation All UI text in Macedonian. TypeScript clean, Convex functions deployed.
191 lines
6.9 KiB
TypeScript
191 lines
6.9 KiB
TypeScript
import { View, Text, ScrollView, Pressable, Alert } from "react-native";
|
||
import { useLocalSearchParams, Stack, useRouter } from "expo-router";
|
||
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 { 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 deleteAd = useMutation(api.ads.remove);
|
||
|
||
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] || "📋";
|
||
|
||
function handleDelete() {
|
||
Alert.alert(
|
||
"Избриши оглас",
|
||
"Дали сте сигурни дека сакате да го избришете овој оглас?",
|
||
[
|
||
{ text: "Откажи", style: "cancel" },
|
||
{
|
||
text: "Избриши",
|
||
style: "destructive",
|
||
onPress: async () => {
|
||
try {
|
||
if (!ad) return;
|
||
await deleteAd({ token: token!, adId: ad._id 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"
|
||
contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.md, paddingBottom: 100 }}
|
||
>
|
||
{/* Gallery placeholder */}
|
||
<View
|
||
style={{
|
||
backgroundColor: theme.colors.surface,
|
||
borderRadius: theme.radius.lg,
|
||
aspectRatio: 16 / 9,
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
borderWidth: 1,
|
||
borderColor: theme.colors.border,
|
||
borderStyle: "dashed",
|
||
}}
|
||
>
|
||
<Text style={{ fontSize: 48 }}>{categoryEmoji}</Text>
|
||
<Text style={{ color: theme.colors.textTertiary, marginTop: theme.spacing.sm, fontSize: 14 }}>
|
||
Нема слики
|
||
</Text>
|
||
</View>
|
||
|
||
{/* Title + Price */}
|
||
<View>
|
||
<Text style={{ fontSize: 22, fontWeight: "700", color: theme.colors.text }}>{ad.title}</Text>
|
||
{ad.priceRange && (
|
||
<Text style={{ fontSize: 18, fontWeight: "600", color: theme.colors.primary, marginTop: theme.spacing.xs }}>
|
||
{ad.priceRange}
|
||
</Text>
|
||
)}
|
||
</View>
|
||
|
||
{/* Category + Location */}
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.sm, flexWrap: "wrap" }}>
|
||
<Badge label={categoryName} variant="primary" />
|
||
<Text style={{ fontSize: 14, color: theme.colors.textSecondary }}>📍 {ad.location}</Text>
|
||
</View>
|
||
|
||
{/* Rating */}
|
||
{ad.ratingAvg != null && ad.ratingAvg > 0 && (
|
||
<Rating value={ad.ratingAvg} count={ad.reviewCount} size={20} />
|
||
)}
|
||
|
||
{/* Availability */}
|
||
{ad.availability && (
|
||
<Card>
|
||
<Text style={{ fontSize: 14, fontWeight: "600", color: theme.colors.text, marginBottom: theme.spacing.xs }}>
|
||
Расположивост
|
||
</Text>
|
||
<Text style={{ fontSize: 14, color: theme.colors.textSecondary }}>{ad.availability}</Text>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Description */}
|
||
<Card>
|
||
<Text style={{ fontSize: 14, fontWeight: "600", color: theme.colors.text, marginBottom: theme.spacing.xs }}>
|
||
Опис
|
||
</Text>
|
||
<Text style={{ fontSize: 15, color: theme.colors.textSecondary, lineHeight: 22 }}>{ad.description}</Text>
|
||
</Card>
|
||
|
||
{/* Handyman info */}
|
||
<Card>
|
||
<Pressable
|
||
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={48} />
|
||
<View style={{ flex: 1 }}>
|
||
<Text style={{ fontSize: 16, fontWeight: "600", color: theme.colors.text }}>
|
||
{handyman?.name || "Мајстор"}
|
||
</Text>
|
||
<Text style={{ fontSize: 13, color: theme.colors.textSecondary }}>Мајстор</Text>
|
||
</View>
|
||
</Pressable>
|
||
</Card>
|
||
|
||
{/* Action buttons */}
|
||
{!isOwner && isAuthenticated && (
|
||
<View style={{ gap: theme.spacing.sm }}>
|
||
<Button onPress={() => {}}>Започни разговор</Button>
|
||
<Button variant="outline" onPress={() => {}}>Напиши оценка</Button>
|
||
</View>
|
||
)}
|
||
|
||
{/* Owner actions */}
|
||
{isOwner && (
|
||
<View style={{ gap: theme.spacing.sm }}>
|
||
<Button
|
||
variant="outline"
|
||
onPress={() => router.push(`/ad/create?editId=${ad._id}` as any)}
|
||
>
|
||
Уреди оглас
|
||
</Button>
|
||
<Button variant="destructive" onPress={handleDelete}>
|
||
Избриши оглас
|
||
</Button>
|
||
</View>
|
||
)}
|
||
|
||
{!isOwner && !isAuthenticated && (
|
||
<Card>
|
||
<Text style={{ fontSize: 14, color: theme.colors.textSecondary, textAlign: "center" }}>
|
||
Најавете се за да започнете разговор или да напишете оценка.
|
||
</Text>
|
||
<View style={{ marginTop: theme.spacing.md }}>
|
||
<Button onPress={() => router.push("/(auth)/login")}>Најави се</Button>
|
||
</View>
|
||
</Card>
|
||
)}
|
||
</ScrollView>
|
||
</>
|
||
);
|
||
} |