- Review create mutation with auth, customer-only role check, self-review prevention, and ad ratingAvg/reviewCount recomputation - ReviewCard component: reviewer avatar, name, relative time in Macedonian, star rating, comment - Create review screen: interactive 5-star selector (Многу лошо–Отлично), comment field, auth gate, submit and navigate back - Ad detail: review section listing all reviews with reviewer names, overall rating summary with count, 'Напиши оценка' button - AdCard: rating stars shown next to title when ad has reviews All UI text in Macedonian. TypeScript clean, Convex functions deployed.
145 lines
5.2 KiB
TypeScript
145 lines
5.2 KiB
TypeScript
import { useState } from "react";
|
||
import { View, Text, Pressable, Alert, ScrollView } 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 { Button } from "../../components/ui/button";
|
||
import { Input } from "../../components/ui/input";
|
||
import { Loading } from "../../components/ui/loading";
|
||
|
||
export default function CreateReviewScreen() {
|
||
const { adId } = useLocalSearchParams<{ adId: string }>();
|
||
const theme = useTheme();
|
||
const router = useRouter();
|
||
const { token, isAuthenticated, userId } = useAuth();
|
||
|
||
const ad = useQuery(api.ads.getById, adId ? { id: adId as any } : "skip");
|
||
const handyman = useQuery(
|
||
api.users.getById,
|
||
ad ? { id: ad.handymanId } : "skip"
|
||
);
|
||
|
||
const [rating, setRating] = useState(0);
|
||
const [comment, setComment] = useState("");
|
||
const [submitting, setSubmitting] = useState(false);
|
||
|
||
const createReview = useMutation(api.reviews.create);
|
||
|
||
if (!isAuthenticated) {
|
||
return (
|
||
<>
|
||
<Stack.Screen options={{ title: "Оценка", headerShown: true, headerBackButtonDisplayMode: "minimal" }} />
|
||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: theme.colors.background, padding: theme.spacing.lg }}>
|
||
<Text style={{ fontSize: 16, color: theme.colors.textSecondary, textAlign: "center" }}>
|
||
Најавете се за да напишете оценка.
|
||
</Text>
|
||
<View style={{ marginTop: theme.spacing.md }}>
|
||
<Button onPress={() => router.push("/(auth)/login")}>Најави се</Button>
|
||
</View>
|
||
</View>
|
||
</>
|
||
);
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
if (rating === 0) {
|
||
Alert.alert("Грешка", "Одберете оценка (1-5 ѕвезди)");
|
||
return;
|
||
}
|
||
if (!token || !adId) return;
|
||
|
||
setSubmitting(true);
|
||
try {
|
||
await createReview({ token, adId: adId as any, rating, comment: comment.trim() || undefined });
|
||
router.back();
|
||
} catch (e: any) {
|
||
Alert.alert("Грешка", e.message || "Неуспешно креирање оценка");
|
||
} finally {
|
||
setSubmitting(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>
|
||
</>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<Stack.Screen options={{ title: "Оценка", headerShown: true, headerBackButtonDisplayMode: "minimal" }} />
|
||
<ScrollView
|
||
contentInsetAdjustmentBehavior="automatic"
|
||
contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.lg }}
|
||
>
|
||
<View style={{ backgroundColor: theme.colors.card, borderRadius: theme.radius.lg, padding: theme.spacing.md, borderWidth: 1, borderColor: theme.colors.border }}>
|
||
<Text style={{ fontSize: 18, fontWeight: "700", color: theme.colors.text }}>
|
||
{ad.title}
|
||
</Text>
|
||
{handyman && (
|
||
<Text style={{ fontSize: 14, color: theme.colors.textSecondary, marginTop: theme.spacing.xs }}>
|
||
Мајстор: {handyman.name}
|
||
</Text>
|
||
)}
|
||
</View>
|
||
|
||
<View style={{ gap: theme.spacing.sm }}>
|
||
<Text style={{ fontSize: 16, fontWeight: "600", color: theme.colors.text }}>
|
||
Оценка
|
||
</Text>
|
||
<View style={{ flexDirection: "row", gap: theme.spacing.sm }}>
|
||
{[1, 2, 3, 4, 5].map((star) => (
|
||
<Pressable
|
||
key={star}
|
||
onPress={() => setRating(star)}
|
||
style={{ padding: 4 }}
|
||
>
|
||
<Text style={{ fontSize: 40, color: star <= rating ? theme.colors.star : theme.colors.border }}>
|
||
★
|
||
</Text>
|
||
</Pressable>
|
||
))}
|
||
</View>
|
||
{rating > 0 && (
|
||
<Text style={{ fontSize: 13, color: theme.colors.textTertiary }}>
|
||
{rating === 1 && "Многу лошо"}
|
||
{rating === 2 && "Лошо"}
|
||
{rating === 3 && "Просечно"}
|
||
{rating === 4 && "Добро"}
|
||
{rating === 5 && "Одлично"}
|
||
</Text>
|
||
)}
|
||
</View>
|
||
|
||
<View style={{ gap: theme.spacing.sm }}>
|
||
<Text style={{ fontSize: 16, fontWeight: "600", color: theme.colors.text }}>
|
||
Коментар (опционално)
|
||
</Text>
|
||
<Input
|
||
value={comment}
|
||
onChangeText={setComment}
|
||
placeholder="Споделете го вашето искуство…"
|
||
multiline
|
||
numberOfLines={4}
|
||
style={{ minHeight: 100, textAlignVertical: "top" }}
|
||
/>
|
||
</View>
|
||
|
||
<Button
|
||
loading={submitting}
|
||
disabled={rating === 0}
|
||
onPress={handleSubmit}
|
||
>
|
||
Испрати оценка
|
||
</Button>
|
||
</ScrollView>
|
||
</>
|
||
);
|
||
} |