- 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.
148 lines
5.2 KiB
TypeScript
148 lines
5.2 KiB
TypeScript
import { View, Text, ScrollView, RefreshControl, Pressable } from "react-native";
|
||
import { useRouter } from "expo-router";
|
||
import { useState, useCallback } from "react";
|
||
import { usePaginatedQuery, useQuery } from "convex/react";
|
||
import { api } from "../../../convex/_generated/api";
|
||
import { useAuth } from "../../_layout";
|
||
import { useTheme } from "../../../components/theme";
|
||
import { PostCard } from "../../../components/post-card";
|
||
import { Loading } from "../../../components/ui/loading";
|
||
|
||
const TABS = [
|
||
{ key: "open", label: "Отворени" },
|
||
{ key: "all", label: "Сите" },
|
||
] as const;
|
||
|
||
type TabKey = (typeof TABS)[number]["key"];
|
||
|
||
export default function PostsScreen() {
|
||
const router = useRouter();
|
||
const theme = useTheme();
|
||
const { token, isAuthenticated } = useAuth();
|
||
const currentUser = useQuery(
|
||
api.users.getCurrentUser,
|
||
isAuthenticated && token ? { token } : "skip"
|
||
);
|
||
const [activeTab, setActiveTab] = useState<TabKey>("open");
|
||
const [refreshing, setRefreshing] = useState(false);
|
||
|
||
const postsResult = usePaginatedQuery(
|
||
api.posts.listPaginated,
|
||
activeTab === "open" ? { status: "open" as const } : {},
|
||
{ initialNumItems: 20 }
|
||
);
|
||
|
||
const onRefresh = useCallback(() => {
|
||
setRefreshing(true);
|
||
setTimeout(() => setRefreshing(false), 800);
|
||
}, []);
|
||
|
||
const isCustomer = currentUser?.role !== "handyman";
|
||
|
||
return (
|
||
<ScrollView
|
||
contentInsetAdjustmentBehavior="automatic"
|
||
refreshControl={
|
||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.colors.primary} />
|
||
}
|
||
contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.md, paddingBottom: 80 }}
|
||
>
|
||
{isAuthenticated && isCustomer && (
|
||
<Pressable
|
||
accessible
|
||
accessibilityLabel="Креирај ново побарување"
|
||
accessibilityRole="button"
|
||
onPress={() => router.push("/post/create" as any)}
|
||
>
|
||
<View
|
||
style={{
|
||
backgroundColor: theme.colors.primary,
|
||
borderRadius: theme.radius.md,
|
||
paddingVertical: 12,
|
||
paddingHorizontal: theme.spacing.lg,
|
||
alignItems: "center",
|
||
}}
|
||
>
|
||
<Text style={{ color: "#FFFFFF", fontWeight: "600", fontSize: 16 }}>
|
||
+ Ново побарување
|
||
</Text>
|
||
</View>
|
||
</Pressable>
|
||
)}
|
||
|
||
<View style={{ flexDirection: "row", backgroundColor: theme.colors.surface, borderRadius: theme.radius.md, padding: 4 }}>
|
||
{TABS.map((tab) => (
|
||
<Pressable
|
||
key={tab.key}
|
||
accessible
|
||
accessibilityLabel={`Прикажи ${tab.label.toLowerCase()} побарувања`}
|
||
accessibilityRole="button"
|
||
accessibilityState={{ selected: activeTab === tab.key }}
|
||
onPress={() => setActiveTab(tab.key)}
|
||
style={{
|
||
flex: 1,
|
||
paddingVertical: 10,
|
||
borderRadius: theme.radius.sm,
|
||
alignItems: "center",
|
||
backgroundColor: activeTab === tab.key ? theme.colors.card : "transparent",
|
||
...(activeTab === tab.key
|
||
? { boxShadow: "0 1px 3px rgba(0,0,0,0.08)" as any }
|
||
: {}),
|
||
}}
|
||
>
|
||
<Text
|
||
style={{
|
||
fontWeight: activeTab === tab.key ? "600" : "400",
|
||
fontSize: 14,
|
||
color: activeTab === tab.key ? theme.colors.text : theme.colors.textSecondary,
|
||
}}
|
||
>
|
||
{tab.label}
|
||
</Text>
|
||
</Pressable>
|
||
))}
|
||
</View>
|
||
|
||
{postsResult.status === "LoadingFirstPage" ? (
|
||
<Loading />
|
||
) : postsResult.results.length === 0 ? (
|
||
<View style={{ alignItems: "center", paddingVertical: 60 }}>
|
||
<Text style={{ fontSize: 48, marginBottom: 16 }}>📋</Text>
|
||
<Text style={{ fontSize: 18, fontWeight: "600", color: theme.colors.text, marginBottom: 8 }}>
|
||
Нема побарувања
|
||
</Text>
|
||
<Text style={{ color: theme.colors.textSecondary, textAlign: "center" }}>
|
||
{activeTab === "open"
|
||
? "Нема отворени побарувања во моментот."
|
||
: "Сеуште нема побарувања."}
|
||
</Text>
|
||
</View>
|
||
) : (
|
||
<View style={{ gap: theme.spacing.md }}>
|
||
{postsResult.results.map((post) => (
|
||
<PostCard key={post._id} post={post} />
|
||
))}
|
||
</View>
|
||
)}
|
||
|
||
{postsResult.status === "CanLoadMore" && (
|
||
<Pressable
|
||
accessible
|
||
accessibilityLabel="Вчитај повеќе побарувања"
|
||
accessibilityRole="button"
|
||
onPress={() => postsResult.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>
|
||
);
|
||
} |