diff --git a/app/(tabs)/(explore)/index.tsx b/app/(tabs)/(explore)/index.tsx
index f52bc3c..4945044 100644
--- a/app/(tabs)/(explore)/index.tsx
+++ b/app/(tabs)/(explore)/index.tsx
@@ -1,32 +1,92 @@
-import { View, Text, ScrollView } from "react-native";
+import { View, Text, Pressable, ScrollView } from "react-native";
+import { useLocalSearchParams, Link } from "expo-router";
import { useTheme } from "../../../components/theme";
-import { CATEGORIES } from "../../../lib/constants";
+import { 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 categories = useQuery(api.categories.list);
+ const categoryAds = useQuery(
+ api.ads.getByCategory,
+ category ? { category } : "skip"
+ );
+ const allAds = useQuery(api.ads.list);
+
+ const displayCategories = categories ?? CATEGORIES;
+
+ if (category) {
+ const catName = getCategoryName(category);
+ const catEmoji = CATEGORY_EMOJI[category] ?? "📋";
+
+ return (
+
+
+
+
+ ← Сите
+
+
+ {catEmoji}
+
+ {catName}
+
+
+
+ {categoryAds === undefined ? (
+
+ ) : categoryAds.length === 0 ? (
+
+ Нема огласи во оваа категорија
+
+ ) : (
+
+ {categoryAds.map((ad) => (
+
+ ))}
+
+ )}
+
+ );
+ }
return (
-
- {CATEGORIES.map((cat) => (
-
- {cat.icon === "construct-outline" ? "🔧" : "📋"}
- {cat.name}
-
- ))}
-
+
+ Пребарувај по категорија
+
+
+
+
+
+ Сите огласи
+
+
+ {allAds === undefined ? (
+
+ ) : allAds.length === 0 ? (
+
+ Нема огласи
+
+ ) : (
+
+ {allAds.map((ad) => (
+
+ ))}
+
+ )}
);
}
\ No newline at end of file
diff --git a/app/(tabs)/(home)/index.tsx b/app/(tabs)/(home)/index.tsx
index 06295d0..e46811f 100644
--- a/app/(tabs)/(home)/index.tsx
+++ b/app/(tabs)/(home)/index.tsx
@@ -1,10 +1,26 @@
-import { View, Text, Pressable, ScrollView } from "react-native";
-import { Link } from "expo-router";
+import { View, Text, TextInput, ScrollView } from "react-native";
+import { useState } from "react";
import { useTheme } from "../../../components/theme";
+import { 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 } from "../../../lib/constants";
export default function HomeScreen() {
const theme = useTheme();
+ const [search, setSearch] = useState("");
+
+ const categories = useQuery(api.categories.list);
+ const allAds = useQuery(api.ads.list);
+ const searchResults = useQuery(
+ api.ads.search,
+ search.trim() ? { query: search.trim() } : "skip"
+ );
+
+ const ads = search.trim() ? searchResults : allAds;
+ const displayCategories = categories ?? CATEGORIES;
return (
-
-
- 🔍 Пребарувај мајстори...
-
-
+ }}
+ />
+
+ {!search.trim() && (
+ <>
+
+ Категории
+
+
+ >
+ )}
- Категории
+ {search.trim() ? "Резултати од пребарување" : "Неодамна додадени"}
-
- {CATEGORIES.map((cat) => (
-
-
- {cat.name}
-
-
- ))}
-
+ {ads === undefined ? (
+
+ ) : ads.length === 0 ? (
+
+ {search.trim() ? "Не се пронајдени резултати" : "Нема огласи"}
+
+ ) : (
+
+ {ads.map((ad) => (
+
+ ))}
+
+ )}
);
}
\ No newline at end of file
diff --git a/components/ad-card.tsx b/components/ad-card.tsx
new file mode 100644
index 0000000..2f04b30
--- /dev/null
+++ b/components/ad-card.tsx
@@ -0,0 +1,58 @@
+import { View, Text, Pressable } from "react-native";
+import { Link } from "expo-router";
+import { useTheme } from "./theme";
+import { Badge } from "./ui/badge";
+import { Rating } from "./ui/rating";
+import { getCategoryName } from "../lib/constants";
+
+interface AdCardProps {
+ ad: {
+ _id: string;
+ title: string;
+ category: string;
+ location: string;
+ priceRange?: string;
+ ratingAvg?: number;
+ reviewCount: number;
+ };
+}
+
+export function AdCard({ ad }: AdCardProps) {
+ const theme = useTheme();
+ const categoryName = getCategoryName(ad.category);
+
+ return (
+
+
+
+
+ {ad.title}
+
+ {ad.priceRange && (
+
+ {ad.priceRange}
+
+ )}
+
+
+
+
+ 📍 {ad.location}
+
+
+ {ad.ratingAvg != null && ad.ratingAvg > 0 && (
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/category-grid.tsx b/components/category-grid.tsx
new file mode 100644
index 0000000..04f8e13
--- /dev/null
+++ b/components/category-grid.tsx
@@ -0,0 +1,57 @@
+import { View, Text, Pressable } from "react-native";
+import { Link } from "expo-router";
+import { useTheme } from "./theme";
+import { CATEGORY_EMOJI } from "../lib/constants";
+
+interface CategoryItem {
+ slug: string;
+ name: string;
+ icon?: string;
+ sortOrder: number;
+}
+
+interface CategoryGridProps {
+ categories: readonly CategoryItem[];
+ columns?: number;
+}
+
+export function CategoryGrid({ categories, columns = 3 }: CategoryGridProps) {
+ const theme = useTheme();
+
+ return (
+
+ {categories.map((cat) => {
+ const emoji = CATEGORY_EMOJI[cat.slug] ?? cat.icon ?? "📋";
+ return (
+
+
+ {emoji}
+
+ {cat.name}
+
+
+
+ );
+ })}
+
+ );
+}
\ No newline at end of file
diff --git a/convex/ads.ts b/convex/ads.ts
index 3fc2571..87a79dd 100644
--- a/convex/ads.ts
+++ b/convex/ads.ts
@@ -1,26 +1,20 @@
import { query } from "./_generated/server";
import { v } from "convex/values";
-export const search = query({
- args: { query: v.string(), category: v.optional(v.string()) },
- handler: async (ctx, args) => {
- if (args.category) {
- return await ctx.db
- .query("ads")
- .withIndex("by_category", (q) => q.eq("category", args.category!))
- .order("desc")
- .collect();
- }
- return await ctx.db.query("ads").order("desc").take(50);
+export const list = query({
+ args: {},
+ handler: async (ctx) => {
+ return await ctx.db.query("ads").order("desc").collect();
},
});
-export const getByHandyman = query({
- args: { handymanId: v.id("users") },
+export const getByCategory = query({
+ args: { category: v.string() },
handler: async (ctx, args) => {
return await ctx.db
.query("ads")
- .withIndex("by_handyman", (q) => q.eq("handymanId", args.handymanId))
+ .withIndex("by_category", (q) => q.eq("category", args.category))
+ .order("desc")
.collect();
},
});
@@ -30,4 +24,39 @@ export const getById = query({
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
},
+});
+
+export const search = query({
+ args: { query: v.string(), category: v.optional(v.string()) },
+ handler: async (ctx, args) => {
+ const searchLower = args.query.toLowerCase();
+
+ let ads;
+ if (args.category) {
+ ads = await ctx.db
+ .query("ads")
+ .withIndex("by_category", (q) => q.eq("category", args.category!))
+ .collect();
+ } else {
+ ads = await ctx.db.query("ads").order("desc").collect();
+ }
+
+ return ads.filter(
+ (ad) =>
+ ad.title.toLowerCase().includes(searchLower) ||
+ ad.description.toLowerCase().includes(searchLower) ||
+ ad.location.toLowerCase().includes(searchLower)
+ );
+ },
+});
+
+export const getByHandyman = query({
+ args: { handymanId: v.id("users") },
+ handler: async (ctx, args) => {
+ return await ctx.db
+ .query("ads")
+ .withIndex("by_handyman", (q) => q.eq("handymanId", args.handymanId))
+ .order("desc")
+ .collect();
+ },
});
\ No newline at end of file
diff --git a/convex/categories.ts b/convex/categories.ts
index 2dba4a2..289b7e7 100644
--- a/convex/categories.ts
+++ b/convex/categories.ts
@@ -1,8 +1,49 @@
-import { query } from "./_generated/server";
+import { query, mutation } from "./_generated/server";
+import { v } from "convex/values";
+
+const SEED_CATEGORIES = [
+ { slug: "majstor-za-se", name: "Мајстор за сѐ", icon: "🔧", sortOrder: 0 },
+ { slug: "vodoinstalater", name: "Водоинсталатер", icon: "🚿", sortOrder: 1 },
+ { slug: "elektrichar", name: "Електричар", icon: "⚡", sortOrder: 2 },
+ { slug: "teskar", name: "Тескар", icon: "🔨", sortOrder: 3 },
+ { slug: "farbar", name: "Фарбар", icon: "🖌️", sortOrder: 4 },
+ { slug: "keramichar", name: "Керамичар", icon: "🧱", sortOrder: 5 },
+ { slug: "zidar", name: "Зидар", icon: "🏗️", sortOrder: 6 },
+ { slug: "gradezhnik", name: "Градежник", icon: "🏢", sortOrder: 7 },
+ { slug: "stolar", name: "Столар", icon: "🪵", sortOrder: 8 },
+ { slug: "moler", name: "Молер", icon: "🎨", sortOrder: 9 },
+ { slug: "klima-montazher", name: "Клима монтажер", icon: "❄️", sortOrder: 10 },
+ { slug: "drugo", name: "Друго", icon: "📋", sortOrder: 11 },
+];
export const list = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query("categories").withIndex("by_slug").order("asc").collect();
},
+});
+
+export const getBySlug = query({
+ args: { slug: v.string() },
+ handler: async (ctx, args) => {
+ return await ctx.db
+ .query("categories")
+ .withIndex("by_slug", (q) => q.eq("slug", args.slug))
+ .first();
+ },
+});
+
+export const seedCategories = mutation({
+ args: {},
+ handler: async (ctx) => {
+ for (const cat of SEED_CATEGORIES) {
+ const existing = await ctx.db
+ .query("categories")
+ .withIndex("by_slug", (q) => q.eq("slug", cat.slug))
+ .first();
+ if (!existing) {
+ await ctx.db.insert("categories", cat);
+ }
+ }
+ },
});
\ No newline at end of file
diff --git a/lib/constants.ts b/lib/constants.ts
index 81bfe8c..8e2f041 100644
--- a/lib/constants.ts
+++ b/lib/constants.ts
@@ -13,4 +13,23 @@ export const CATEGORIES = [
{ slug: "drugo", name: "Друго", icon: "ellipsis-horizontal-outline", sortOrder: 11 },
] as const;
-export type CategorySlug = (typeof CATEGORIES)[number]["slug"];
\ No newline at end of file
+export type CategorySlug = (typeof CATEGORIES)[number]["slug"];
+
+export const CATEGORY_EMOJI: Record = {
+ "majstor-za-se": "🔧",
+ "vodoinstalater": "🚿",
+ "elektrichar": "⚡",
+ "teskar": "🔨",
+ "farbar": "🖌️",
+ "keramichar": "🧱",
+ "zidar": "🏗️",
+ "gradezhnik": "🏢",
+ "stolar": "🪵",
+ "moler": "🎨",
+ "klima-montazher": "❄️",
+ "drugo": "📋",
+};
+
+export function getCategoryName(slug: string): string {
+ return CATEGORIES.find((c) => c.slug === slug)?.name ?? slug;
+}
\ No newline at end of file