- Error boundary with Macedonian fallback screen and retry button wrapping all navigation - Pull-to-refresh on all list/detail screens (home, explore, posts, chat, profile, ad detail, post detail) - Accessibility: labels on all interactive elements, roles on Pressable/Button, selectable text for user data - Deep linking: mojmajstor:// scheme configured in app.json, ad/[id] and chat/[id] routes work via Expo Router auto-routing - Pagination: cursor-based paginated queries for ads, posts, messages, chats, reviews with Load More buttons on frontend - Profile: shows customer posts with PostCard, loading/error states, create-post navigation - Auth error messages in Macedonian (Невалидни акредитиви, итн.) All UI text in Macedonian. TypeScript clean, Convex functions deployed.
150 lines
5.5 KiB
TypeScript
150 lines
5.5 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}
|
||
accessible
|
||
accessibilityLabel={`${star} ${star === 1 ? "ѕвезда" : "ѕвезди"}`}
|
||
accessibilityRole="button"
|
||
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" }}
|
||
accessibilityLabel="Коментар"
|
||
/>
|
||
</View>
|
||
|
||
<Button
|
||
loading={submitting}
|
||
disabled={rating === 0}
|
||
onPress={handleSubmit}
|
||
accessibilityLabel="Испрати оценка"
|
||
>
|
||
Испрати оценка
|
||
</Button>
|
||
</ScrollView>
|
||
</>
|
||
);
|
||
} |