- 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.
72 lines
2.4 KiB
TypeScript
72 lines
2.4 KiB
TypeScript
import { View, Text } from "react-native";
|
|
import { useTheme } from "./theme";
|
|
import { Avatar } from "./ui/avatar";
|
|
import { Rating } from "./ui/rating";
|
|
|
|
function formatTimeAgo(timestamp: number): string {
|
|
const now = Date.now();
|
|
const diff = now - timestamp;
|
|
|
|
const seconds = Math.floor(diff / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const hours = Math.floor(minutes / 60);
|
|
const days = Math.floor(hours / 24);
|
|
const months = Math.floor(days / 30);
|
|
const years = Math.floor(days / 365);
|
|
|
|
if (years > 0) return `пред ${years} ${years === 1 ? "година" : "години"}`;
|
|
if (months > 0) return `пред ${months} ${months === 1 ? "месец" : "месеци"}`;
|
|
if (days > 0) return `пред ${days} ${days === 1 ? "ден" : "денови"}`;
|
|
if (hours > 0) return `пред ${hours} ${hours === 1 ? "час" : "часа"}`;
|
|
if (minutes > 0) return `пред ${minutes} ${minutes === 1 ? "минута" : "минути"}`;
|
|
return "пред малку";
|
|
}
|
|
|
|
interface ReviewCardProps {
|
|
review: {
|
|
_id: string;
|
|
rating: number;
|
|
comment?: string;
|
|
createdAt: number;
|
|
};
|
|
customerName: string;
|
|
customerAvatarId?: string;
|
|
}
|
|
|
|
export function ReviewCard({ review, customerName, customerAvatarId }: ReviewCardProps) {
|
|
const theme = useTheme();
|
|
|
|
return (
|
|
<View
|
|
accessible
|
|
accessibilityLabel={`Оценка ${review.rating} од 5 од ${customerName}${review.comment ? `, ${review.comment}` : ""}`}
|
|
style={{
|
|
backgroundColor: theme.colors.card,
|
|
borderRadius: theme.radius.lg,
|
|
padding: theme.spacing.md,
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
gap: theme.spacing.sm,
|
|
}}
|
|
>
|
|
<View style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.sm }}>
|
|
<Avatar uri={customerAvatarId ?? null} name={customerName} size={36} />
|
|
<View style={{ flex: 1 }}>
|
|
<Text selectable style={{ fontSize: 14, fontWeight: "600", color: theme.colors.text }}>
|
|
{customerName}
|
|
</Text>
|
|
<Text style={{ fontSize: 12, color: theme.colors.textTertiary }}>
|
|
{formatTimeAgo(review.createdAt)}
|
|
</Text>
|
|
</View>
|
|
<Rating value={review.rating} size={16} />
|
|
</View>
|
|
|
|
{review.comment ? (
|
|
<Text selectable style={{ fontSize: 14, color: theme.colors.textSecondary, lineHeight: 20 }}>
|
|
{review.comment}
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
);
|
|
} |