mojmajstor/app/review/create.tsx
echo c700d139f7 feat: Phase 6 - reviews and ratings with computed fields
- 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.
2026-05-29 18:41:07 +02:00

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