219 lines
9.1 KiB
TypeScript
219 lines
9.1 KiB
TypeScript
import { View, Text, TextInput, ScrollView, RefreshControl, Pressable } from "react-native";
|
||
import { useState, useCallback } from "react";
|
||
import { useRouter } from "expo-router";
|
||
import { Ionicons } from "@expo/vector-icons";
|
||
import { useTheme } from "../../../components/theme";
|
||
import { useQuery, usePaginatedQuery } from "convex/react";
|
||
import { api } from "../../../convex/_generated/api";
|
||
import { useAuth } from "../../_layout";
|
||
import { CategoryGrid } from "../../../components/category-grid";
|
||
import { AdCard } from "../../../components/ad-card";
|
||
import { Loading } from "../../../components/ui/loading";
|
||
import { CATEGORIES } from "../../../lib/constants";
|
||
|
||
export default function HomeScreen() {
|
||
const theme = useTheme();
|
||
const router = useRouter();
|
||
const { isAuthenticated } = useAuth();
|
||
const [search, setSearch] = useState("");
|
||
const [refreshing, setRefreshing] = useState(false);
|
||
|
||
const categories = useQuery(api.categories.list);
|
||
const adsResult = usePaginatedQuery(api.ads.listPaginated, {}, { initialNumItems: 20 });
|
||
const ads = adsResult.results;
|
||
const loadMoreAds = adsResult.loadMore;
|
||
const adsStatus = adsResult.status;
|
||
|
||
const searchResults = useQuery(
|
||
api.ads.search,
|
||
search.trim() ? { query: search.trim() } : "skip"
|
||
);
|
||
|
||
const onRefresh = useCallback(() => {
|
||
setRefreshing(true);
|
||
setTimeout(() => setRefreshing(false), 800);
|
||
}, []);
|
||
|
||
const displayCategories = categories ?? CATEGORIES;
|
||
const displayAds = search.trim() ? (searchResults ?? []) : ads;
|
||
const isLoading = !search.trim() && adsStatus === "LoadingFirstPage";
|
||
|
||
return (
|
||
<View style={{ flex: 1, backgroundColor: theme.colors.background }}>
|
||
<ScrollView
|
||
contentInsetAdjustmentBehavior="automatic"
|
||
refreshControl={
|
||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.colors.primary} />
|
||
}
|
||
contentContainerStyle={{ paddingBottom: 100 }}
|
||
>
|
||
<View style={{ paddingHorizontal: theme.spacing.md, paddingTop: 60, paddingBottom: theme.spacing.md }}>
|
||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: theme.spacing.xs }}>
|
||
<View>
|
||
<Text style={{ ...theme.typography.h2, color: theme.colors.text }}>
|
||
МојМајстор
|
||
</Text>
|
||
<Text style={{ ...theme.typography.caption, color: theme.colors.textSecondary }}>
|
||
Најдете мајстор за секоја работа
|
||
</Text>
|
||
</View>
|
||
<View style={{ width: 48, height: 48, borderRadius: 24, backgroundColor: theme.colors.primaryLight, alignItems: "center", justifyContent: "center", ...theme.shadows.xs }}>
|
||
<Ionicons name="hammer" size={24} color={theme.colors.primary} />
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
<View style={{ paddingHorizontal: theme.spacing.md, marginBottom: theme.spacing.md }}>
|
||
<View
|
||
style={{
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
backgroundColor: theme.colors.surface,
|
||
borderRadius: theme.radius.xl,
|
||
paddingHorizontal: theme.spacing.md,
|
||
gap: theme.spacing.sm,
|
||
height: 50,
|
||
...theme.shadows.sm,
|
||
borderWidth: 1,
|
||
borderColor: theme.colors.borderLight,
|
||
}}
|
||
>
|
||
<Ionicons name="search" size={20} color={theme.colors.textTertiary} />
|
||
<TextInput
|
||
accessible
|
||
accessibilityLabel="Пребарувај мајстори"
|
||
accessibilityRole="search"
|
||
placeholder="Пребарувај мајстори..."
|
||
placeholderTextColor={theme.colors.textTertiary}
|
||
value={search}
|
||
onChangeText={setSearch}
|
||
style={{
|
||
flex: 1,
|
||
fontSize: 15,
|
||
color: theme.colors.text,
|
||
}}
|
||
/>
|
||
{search.length > 0 && (
|
||
<Pressable onPress={() => setSearch("")} style={{ padding: 4 }}>
|
||
<Ionicons name="close-circle" size={20} color={theme.colors.textTertiary} />
|
||
</Pressable>
|
||
)}
|
||
</View>
|
||
</View>
|
||
|
||
{!search.trim() && (
|
||
<>
|
||
<View style={{ paddingHorizontal: theme.spacing.md, marginBottom: theme.spacing.sm }}>
|
||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||
<Text style={{ ...theme.typography.h4, color: theme.colors.text }}>
|
||
Категории
|
||
</Text>
|
||
<Pressable onPress={() => router.push("/(tabs)/(explore)")} style={{ flexDirection: "row", alignItems: "center", gap: 2 }}>
|
||
<Text style={{ ...theme.typography.caption, color: theme.colors.primary, fontWeight: "500" }}>Види ги сите</Text>
|
||
<Ionicons name="chevron-forward" size={14} color={theme.colors.primary} />
|
||
</Pressable>
|
||
</View>
|
||
</View>
|
||
<View style={{ paddingHorizontal: theme.spacing.md, marginBottom: theme.spacing.lg }}>
|
||
<CategoryGrid categories={displayCategories} />
|
||
</View>
|
||
</>
|
||
)}
|
||
|
||
<View style={{ paddingHorizontal: theme.spacing.md }}>
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.sm, marginBottom: theme.spacing.md }}>
|
||
<Text style={{ ...theme.typography.h4, color: theme.colors.text }}>
|
||
{search.trim() ? "Резултати" : "Неодамна додадени"}
|
||
</Text>
|
||
{!search.trim() && (
|
||
<View style={{ backgroundColor: theme.colors.primaryLight, paddingHorizontal: 10, paddingVertical: 3, borderRadius: theme.radius.full }}>
|
||
<Text style={{ ...theme.typography.label, color: theme.colors.primary }}>
|
||
{ads.length}+
|
||
</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
|
||
<View style={{ paddingHorizontal: theme.spacing.md, gap: theme.spacing.sm }}>
|
||
{(search.trim() ? searchResults === undefined : isLoading) ? (
|
||
<Loading skeleton />
|
||
) : displayAds.length === 0 ? (
|
||
<View style={{ alignItems: "center", paddingVertical: theme.spacing.xxl }}>
|
||
<View
|
||
style={{
|
||
width: 72,
|
||
height: 72,
|
||
borderRadius: 36,
|
||
backgroundColor: theme.colors.primaryLight,
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
marginBottom: theme.spacing.md,
|
||
...theme.shadows.xs,
|
||
}}
|
||
>
|
||
<Ionicons name="search" size={30} color={theme.colors.primary} />
|
||
</View>
|
||
<Text style={{ ...theme.typography.bodyBold, color: theme.colors.text, marginBottom: theme.spacing.xs }}>
|
||
{search.trim() ? "Нема резултати" : "Нема огласи"}
|
||
</Text>
|
||
<Text style={{ ...theme.typography.caption, color: theme.colors.textSecondary, textAlign: "center" }}>
|
||
{search.trim() ? "Обидете се со друга пребарувачка фраза" : "Сè уште нема објавено огласи"}
|
||
</Text>
|
||
</View>
|
||
) : (
|
||
displayAds.map((ad) => (
|
||
<AdCard key={ad._id} ad={ad} />
|
||
))
|
||
)}
|
||
|
||
{!search.trim() && adsStatus === "CanLoadMore" && (
|
||
<Pressable
|
||
accessible
|
||
accessibilityLabel="Вчитај повеќе огласи"
|
||
accessibilityRole="button"
|
||
onPress={() => loadMoreAds(20)}
|
||
style={{
|
||
backgroundColor: theme.colors.surface,
|
||
borderRadius: theme.radius.lg,
|
||
paddingVertical: 14,
|
||
alignItems: "center",
|
||
borderWidth: 1,
|
||
borderColor: theme.colors.border,
|
||
marginTop: theme.spacing.sm,
|
||
...theme.shadows.xs,
|
||
}}
|
||
>
|
||
<Text style={{ color: theme.colors.primary, fontWeight: "600", fontSize: 14 }}>
|
||
Вчитај повеќе
|
||
</Text>
|
||
</Pressable>
|
||
)}
|
||
</View>
|
||
</ScrollView>
|
||
|
||
{isAuthenticated && !search.trim() && (
|
||
<View style={{ position: "absolute", bottom: 90, right: 18 }}>
|
||
<Pressable
|
||
accessible
|
||
accessibilityLabel="Креирај нов оглас"
|
||
accessibilityRole="button"
|
||
onPress={() => router.push("/ad/create" as any)}
|
||
style={{
|
||
width: 58,
|
||
height: 58,
|
||
borderRadius: 29,
|
||
backgroundColor: theme.colors.primary,
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
...theme.shadows.lg,
|
||
}}
|
||
>
|
||
<Ionicons name="add" size={30} color={theme.colors.textInverse} />
|
||
</Pressable>
|
||
</View>
|
||
)}
|
||
</View>
|
||
);
|
||
}
|