mojmajstor/app/ad/[id].tsx
echo ff464b0f9b feat: Phase 3 - ad creation, ad detail, profile my-ads
- 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.
2026-05-29 18:28:01 +02:00

191 lines
6.9 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 { 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>
</>
);
}