mojmajstor/app/(tabs)/(home)/index.tsx
2026-06-06 03:54:22 +02:00

219 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}