- 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.
144 lines
5.4 KiB
TypeScript
144 lines
5.4 KiB
TypeScript
import { View, Text, Pressable, ScrollView, RefreshControl } from "react-native";
|
||
import { useLocalSearchParams, Link } from "expo-router";
|
||
import { useState, useCallback } from "react";
|
||
import { useTheme } from "../../../components/theme";
|
||
import { usePaginatedQuery, useQuery } from "convex/react";
|
||
import { api } from "../../../convex/_generated/api";
|
||
import { CategoryGrid } from "../../../components/category-grid";
|
||
import { AdCard } from "../../../components/ad-card";
|
||
import { Loading } from "../../../components/ui/loading";
|
||
import { CATEGORIES, CATEGORY_EMOJI, getCategoryName } from "../../../lib/constants";
|
||
|
||
export default function ExploreScreen() {
|
||
const theme = useTheme();
|
||
const { category } = useLocalSearchParams<{ category?: string }>();
|
||
const [refreshing, setRefreshing] = useState(false);
|
||
|
||
const categories = useQuery(api.categories.list);
|
||
const categoryAdsResult = usePaginatedQuery(
|
||
api.ads.getByCategoryPaginated,
|
||
category ? { category } : "skip",
|
||
{ initialNumItems: 20 }
|
||
);
|
||
const allAdsResult = usePaginatedQuery(api.ads.listPaginated, {}, { initialNumItems: 20 });
|
||
|
||
const onRefresh = useCallback(() => {
|
||
setRefreshing(true);
|
||
setTimeout(() => setRefreshing(false), 800);
|
||
}, []);
|
||
|
||
const displayCategories = categories ?? CATEGORIES;
|
||
|
||
if (category) {
|
||
const catName = getCategoryName(category);
|
||
const catEmoji = CATEGORY_EMOJI[category] ?? "📋";
|
||
|
||
return (
|
||
<ScrollView
|
||
contentInsetAdjustmentBehavior="automatic"
|
||
refreshControl={
|
||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.colors.primary} />
|
||
}
|
||
contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.md }}
|
||
>
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.sm }}>
|
||
<Link href="/(tabs)/(explore)" asChild>
|
||
<Pressable accessible accessibilityLabel="Назад кон сите категории" accessibilityRole="link" style={{ marginRight: theme.spacing.xs }}>
|
||
<Text style={{ fontSize: 20, color: theme.colors.primary }}>← Сите</Text>
|
||
</Pressable>
|
||
</Link>
|
||
<Text style={{ fontSize: 28 }}>{catEmoji}</Text>
|
||
<Text selectable style={{ fontSize: 22, fontWeight: "700", color: theme.colors.text }}>
|
||
{catName}
|
||
</Text>
|
||
</View>
|
||
|
||
{categoryAdsResult.status === "LoadingFirstPage" ? (
|
||
<Loading />
|
||
) : categoryAdsResult.results.length === 0 ? (
|
||
<Text style={{ color: theme.colors.textTertiary, textAlign: "center", padding: 24 }}>
|
||
Нема огласи во оваа категорија
|
||
</Text>
|
||
) : (
|
||
<View style={{ gap: theme.spacing.sm }}>
|
||
{categoryAdsResult.results.map((ad) => (
|
||
<AdCard key={ad._id} ad={ad} />
|
||
))}
|
||
</View>
|
||
)}
|
||
|
||
{categoryAdsResult.status === "CanLoadMore" && (
|
||
<Pressable
|
||
accessible
|
||
accessibilityLabel="Вчитај повеќе огласи"
|
||
accessibilityRole="button"
|
||
onPress={() => categoryAdsResult.loadMore(20)}
|
||
style={{
|
||
backgroundColor: theme.colors.surface,
|
||
borderRadius: theme.radius.md,
|
||
paddingVertical: theme.spacing.md,
|
||
alignItems: "center",
|
||
borderWidth: 1,
|
||
borderColor: theme.colors.border,
|
||
}}
|
||
>
|
||
<Text style={{ color: theme.colors.primary, fontWeight: "600", fontSize: 14 }}>Вчитај повеќе</Text>
|
||
</Pressable>
|
||
)}
|
||
</ScrollView>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<ScrollView
|
||
contentInsetAdjustmentBehavior="automatic"
|
||
refreshControl={
|
||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.colors.primary} />
|
||
}
|
||
contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.md }}
|
||
>
|
||
<Text style={{ fontSize: 20, fontWeight: "700", color: theme.colors.text }}>
|
||
Пребарувај по категорија
|
||
</Text>
|
||
|
||
<CategoryGrid categories={displayCategories} />
|
||
|
||
<Text style={{ fontSize: 18, fontWeight: "600", color: theme.colors.text, marginTop: theme.spacing.sm }}>
|
||
Сите огласи
|
||
</Text>
|
||
|
||
{allAdsResult.status === "LoadingFirstPage" ? (
|
||
<Loading />
|
||
) : allAdsResult.results.length === 0 ? (
|
||
<Text style={{ color: theme.colors.textTertiary, textAlign: "center", padding: 24 }}>
|
||
Нема огласи
|
||
</Text>
|
||
) : (
|
||
<View style={{ gap: theme.spacing.sm }}>
|
||
{allAdsResult.results.map((ad) => (
|
||
<AdCard key={ad._id} ad={ad} />
|
||
))}
|
||
</View>
|
||
)}
|
||
|
||
{allAdsResult.status === "CanLoadMore" && (
|
||
<Pressable
|
||
accessible
|
||
accessibilityLabel="Вчитај повеќе огласи"
|
||
accessibilityRole="button"
|
||
onPress={() => allAdsResult.loadMore(20)}
|
||
style={{
|
||
backgroundColor: theme.colors.surface,
|
||
borderRadius: theme.radius.md,
|
||
paddingVertical: theme.spacing.md,
|
||
alignItems: "center",
|
||
borderWidth: 1,
|
||
borderColor: theme.colors.border,
|
||
}}
|
||
>
|
||
<Text style={{ color: theme.colors.primary, fontWeight: "600", fontSize: 14 }}>Вчитај повеќе</Text>
|
||
</Pressable>
|
||
)}
|
||
</ScrollView>
|
||
);
|
||
} |