diff --git a/AGENTS.md b/AGENTS.md
index 22f3064..2187522 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -7,7 +7,7 @@
## Tech Stack
- **Frontend:** Expo SDK 56, React Native 0.85, TypeScript, Expo Router (file-based routing), inline styles (no NativeWind/Tailwind)
-- **Backend:** Convex (self-deployed at `https://backend-onhwnh06p0g3976vl34yx18v.testbed.mk:3210`)
+- **Backend:** Convex (cloud at `https://decisive-owl-34.eu-west-1.convex.cloud`)
- **Auth:** Custom email/password with SHA-256 (Convex actions), stored in `accounts`/`sessions` tables — no @convex-dev/auth (incompatible with self-hosted)
- **Deployment:** EAS Build + Update
@@ -18,7 +18,17 @@
- Self-deployed Convex does **not** support `@convex-dev/auth` — custom auth via `convex/auth.ts`
- Auth env vars already set: `AUTH_SECRET`, `AUTH_SECRET_1`-`AUTH_SECRET_10`, `AUTH_URL`
-## Key Conventions
+## Design System
+
+- **Colors:** Warm terracotta (`#D4764A`) + deep navy (`#1A2A3A`), off-white bg (`#FDF8F3`)
+- **Typography:** System font, hierarchy via `theme.typography` (h1/h2/h3/body/caption/label)
+- **Shadows:** Defined in `theme.shadows` as `{ shadowColor, shadowOffset, shadowOpacity, shadowRadius, elevation }`
+- **Theme context** via `ThemeProvider` + `useTheme()` from `components/theme.tsx`
+- **Spacing:** 4/8/16/24/32/48 via `theme.spacing`, radius via `theme.radius`
+- **Icons:** `@expo/vector-icons` (Ionicons) for tab bar; emoji for categories
+- **Styling:** Inline styles only, no NativeWind, no StyleSheet.create
+- **Card variants:** `default` (border), `elevated` (shadow), `outlined` (transparent bg + border)
+- **Button variants:** `primary` (terracotta + shadow), `secondary` (surface + border), `outline`, `ghost`, `destructive`
- **Language split:** Macedonian for all user-facing text, English for code
- **Role system:** `handyman` | `customer` — set at registration. Handymen create ads; customers create posts; both can chat
@@ -56,8 +66,9 @@ app/
```bash
npx expo start # Dev server
+npx expo start --dev-client # Dev server with dev client
npm run convex:dev # Convex backend dev
-CONVEX_DEPLOYMENT="" npx convex dev --url $URL --once # Deploy once
+npx convex deploy # Deploy to cloud
npx tsc --noEmit # Type check
```
diff --git a/CLAUDE.md b/CLAUDE.md
index 43c994c..04c7e56 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1 +1,15 @@
@AGENTS.md
+
+
+
+This project uses [Convex](https://convex.dev) as its backend.
+
+When working on Convex code, **always read
+`convex/_generated/ai/guidelines.md` first** for important guidelines on
+how to correctly use Convex APIs and patterns. The file contains rules that
+override what you may have learned about Convex from training data.
+
+Convex agent skills for common tasks can be installed by running
+`npx convex ai-files install`.
+
+
diff --git a/app/(auth)/login.tsx b/app/(auth)/login.tsx
index 2e75789..eea3f23 100644
--- a/app/(auth)/login.tsx
+++ b/app/(auth)/login.tsx
@@ -51,55 +51,85 @@ export default function LoginScreen() {
style={{ flex: 1, backgroundColor: theme.colors.background }}
>
-
-
- МојМајстор
-
-
- Најдете мајстор во вашата близина
-
-
+
+
+
+ 🔧
+
+
+ МојМајстор
+
+
+ Најдете мајстор во вашата близина
+
+
-
+
-
+
-
-
-
- Немате профил?
- router.push("/(auth)/register")}
- >
- Регистрирајте се
-
+
+ Немате профил?
+ router.push("/(auth)/register")}
+ >
+ Регистрирајте се
+
+
);
-}
\ No newline at end of file
+}
diff --git a/app/(auth)/register.tsx b/app/(auth)/register.tsx
index 5fdf155..986998b 100644
--- a/app/(auth)/register.tsx
+++ b/app/(auth)/register.tsx
@@ -67,104 +67,163 @@ export default function RegisterScreen() {
style={{ flex: 1, backgroundColor: theme.colors.background }}
>
-
-
- Креирајте профил
-
-
- Изберете ја вашата улога
-
-
-
-
- setRole("customer")}
- style={{
- flex: 1,
- paddingVertical: theme.spacing.md,
- paddingHorizontal: theme.spacing.sm,
- borderRadius: theme.radius.md,
- borderWidth: 2,
- borderColor: role === "customer" ? theme.colors.primary : theme.colors.border,
- backgroundColor: role === "customer" ? theme.colors.primaryLight : theme.colors.surface,
- alignItems: "center",
- gap: theme.spacing.sm,
- }}
- >
- 🏠
- Клиент
-
- Барајте мајстори
+
+
+
+ 👤
+
+
+ Креирајте профил
-
-
- setRole("handyman")}
- style={{
- flex: 1,
- paddingVertical: theme.spacing.md,
- paddingHorizontal: theme.spacing.sm,
- borderRadius: theme.radius.md,
- borderWidth: 2,
- borderColor: role === "handyman" ? theme.colors.primary : theme.colors.border,
- backgroundColor: role === "handyman" ? theme.colors.primaryLight : theme.colors.surface,
- alignItems: "center",
- gap: theme.spacing.sm,
- }}
- >
- 🔧
- Мајстор
-
- Нудете услуги
+
+ Изберете ја вашата улога
-
-
+
-
-
-
-
+
+ setRole("customer")}
+ style={{
+ flex: 1,
+ paddingVertical: theme.spacing.lg,
+ paddingHorizontal: theme.spacing.sm,
+ borderRadius: theme.radius.md,
+ borderWidth: 2,
+ borderColor: role === "customer" ? theme.colors.primary : theme.colors.borderLight,
+ backgroundColor: role === "customer" ? theme.colors.primaryLight : theme.colors.surface,
+ alignItems: "center",
+ gap: theme.spacing.sm,
+ }}
+ >
+
+ 🏠
+
+ Клиент
+
+ Барајте мајстори
+
+
-
-
+ setRole("handyman")}
+ style={{
+ flex: 1,
+ paddingVertical: theme.spacing.lg,
+ paddingHorizontal: theme.spacing.sm,
+ borderRadius: theme.radius.md,
+ borderWidth: 2,
+ borderColor: role === "handyman" ? theme.colors.primary : theme.colors.borderLight,
+ backgroundColor: role === "handyman" ? theme.colors.primaryLight : theme.colors.surface,
+ alignItems: "center",
+ gap: theme.spacing.sm,
+ }}
+ >
+
+ 🔧
+
+ Мајстор
+
+ Нудете услуги
+
+
+
+
+
+
+
+
+
+
Регистрирај се
-
-
- Веќе имате профил?
- router.back()}>
- Најавете се
-
+
+ Веќе имате профил?
+ router.back()}
+ >
+ Најавете се
+
+
);
-}
\ No newline at end of file
+}
diff --git a/app/(tabs)/(explore)/index.tsx b/app/(tabs)/(explore)/index.tsx
index 6598f51..b9ffe70 100644
--- a/app/(tabs)/(explore)/index.tsx
+++ b/app/(tabs)/(explore)/index.tsx
@@ -39,16 +39,27 @@ export default function ExploreScreen() {
refreshControl={
}
- contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.md }}
+ contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.md, paddingBottom: 40 }}
>
- ← Сите
+ ← Сите
- {catEmoji}
-
+
+ {catEmoji}
+
+
{catName}
@@ -56,9 +67,12 @@ export default function ExploreScreen() {
{categoryAdsResult.status === "LoadingFirstPage" ? (
) : categoryAdsResult.results.length === 0 ? (
-
- Нема огласи во оваа категорија
-
+
+ 🔎
+
+ Нема огласи во оваа категорија
+
+
) : (
{categoryAdsResult.results.map((ad) => (
@@ -74,15 +88,15 @@ export default function ExploreScreen() {
accessibilityRole="button"
onPress={() => categoryAdsResult.loadMore(20)}
style={{
- backgroundColor: theme.colors.surface,
+ backgroundColor: theme.colors.surfaceAlt,
borderRadius: theme.radius.md,
paddingVertical: theme.spacing.md,
alignItems: "center",
borderWidth: 1,
- borderColor: theme.colors.border,
+ borderColor: theme.colors.borderLight,
}}
>
- Вчитај повеќе
+ Вчитај повеќе
)}
@@ -95,24 +109,27 @@ export default function ExploreScreen() {
refreshControl={
}
- contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.md }}
+ contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.md, paddingBottom: 40 }}
>
-
+
Пребарувај по категорија
-
+
Сите огласи
{allAdsResult.status === "LoadingFirstPage" ? (
) : allAdsResult.results.length === 0 ? (
-
- Нема огласи
-
+
+ 📦
+
+ Нема огласи
+
+
) : (
{allAdsResult.results.map((ad) => (
@@ -128,17 +145,17 @@ export default function ExploreScreen() {
accessibilityRole="button"
onPress={() => allAdsResult.loadMore(20)}
style={{
- backgroundColor: theme.colors.surface,
+ backgroundColor: theme.colors.surfaceAlt,
borderRadius: theme.radius.md,
paddingVertical: theme.spacing.md,
alignItems: "center",
borderWidth: 1,
- borderColor: theme.colors.border,
+ borderColor: theme.colors.borderLight,
}}
>
- Вчитај повеќе
+ Вчитај повеќе
)}
);
-}
\ No newline at end of file
+}
diff --git a/app/(tabs)/(home)/index.tsx b/app/(tabs)/(home)/index.tsx
index b9d9e6a..8803cc6 100644
--- a/app/(tabs)/(home)/index.tsx
+++ b/app/(tabs)/(home)/index.tsx
@@ -1,15 +1,20 @@
import { View, Text, TextInput, ScrollView, RefreshControl, Pressable } from "react-native";
import { useState, useCallback } from "react";
+import { useRouter } from "expo-router";
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 { Badge } from "../../../components/ui/badge";
import { CATEGORIES } from "../../../lib/constants";
export default function HomeScreen() {
const theme = useTheme();
+ const router = useRouter();
+ const { userId } = useAuth();
const [search, setSearch] = useState("");
const [refreshing, setRefreshing] = useState(false);
@@ -40,81 +45,121 @@ export default function HomeScreen() {
refreshControl={
}
- contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.md, paddingBottom: 40 }}
+ contentContainerStyle={{ paddingBottom: 40 }}
>
-
-
- Добредојдовте 🏠
-
-
- Најдете мајстор во вашата близина
-
-
-
-
-
- {!search.trim() && (
- <>
-
- Категории
+
+
+
+ Добредојдовте
+
+
+ Најдете мајстор во вашата близина
-
- >
- )}
-
-
- {search.trim() ? "Резултати од пребарување" : "Неодамна додадени"}
-
-
- {(search.trim() ? searchResults === undefined : isLoading) ? (
-
- ) : displayAds.length === 0 ? (
-
- {search.trim() ? "Не се пронајдени резултати" : "Нема огласи"}
-
- ) : (
-
- {displayAds.map((ad) => (
-
- ))}
- )}
- {!search.trim() && adsStatus === "CanLoadMore" && (
- loadMoreAds(20)}
+
- Вчитај повеќе
-
- )}
+ 🔍
+
+ {search.length > 0 && (
+ setSearch("")}
+ style={{ padding: 4 }}
+ >
+ ✕
+
+ )}
+
+
+ {!search.trim() && (
+ <>
+
+
+ Категории
+
+ router.push("/(tabs)/(explore)")}
+ >
+
+ Сите
+
+
+
+
+ >
+ )}
+
+
+ {search.trim() ? "Резултати од пребарување" : "Неодамна додадени"}
+
+
+ {(search.trim() ? searchResults === undefined : isLoading) ? (
+
+ ) : displayAds.length === 0 ? (
+
+ 🔎
+
+ {search.trim() ? "Не се пронајдени резултати" : "Нема огласи"}
+
+
+ ) : (
+
+ {displayAds.map((ad) => (
+
+ ))}
+
+ )}
+
+ {!search.trim() && adsStatus === "CanLoadMore" && (
+ loadMoreAds(20)}
+ style={{
+ backgroundColor: theme.colors.surfaceAlt,
+ borderRadius: theme.radius.md,
+ paddingVertical: theme.spacing.md,
+ alignItems: "center",
+ borderWidth: 1,
+ borderColor: theme.colors.borderLight,
+ }}
+ >
+
+ Вчитај повеќе
+
+
+ )}
+
);
-}
\ No newline at end of file
+}
diff --git a/app/(tabs)/(posts)/index.tsx b/app/(tabs)/(posts)/index.tsx
index 8c4912c..0b22659 100644
--- a/app/(tabs)/(posts)/index.tsx
+++ b/app/(tabs)/(posts)/index.tsx
@@ -7,6 +7,7 @@ import { useAuth } from "../../_layout";
import { useTheme } from "../../../components/theme";
import { PostCard } from "../../../components/post-card";
import { Loading } from "../../../components/ui/loading";
+import { Button } from "../../../components/ui/button";
const TABS = [
{ key: "open", label: "Отворени" },
@@ -48,29 +49,21 @@ export default function PostsScreen() {
contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.md, paddingBottom: 80 }}
>
{isAuthenticated && isCustomer && (
- router.push("/post/create" as any)}
+ size="lg"
+ accessibilityLabel="Креирај ново побарување"
>
-
-
- + Ново побарување
-
-
-
+ + Ново побарување
+
)}
-
+
{TABS.map((tab) => (
) : postsResult.results.length === 0 ? (
- 📋
-
+ 📋
+
Нема побарувања
-
+
{activeTab === "open"
? "Нема отворени побарувања во моментот."
- : "Сеуште нема побарувања."}
+ : "Сè уште нема побарувања."}
) : (
@@ -132,17 +123,17 @@ export default function PostsScreen() {
accessibilityRole="button"
onPress={() => postsResult.loadMore(20)}
style={{
- backgroundColor: theme.colors.surface,
+ backgroundColor: theme.colors.surfaceAlt,
borderRadius: theme.radius.md,
paddingVertical: theme.spacing.md,
alignItems: "center",
borderWidth: 1,
- borderColor: theme.colors.border,
+ borderColor: theme.colors.borderLight,
}}
>
- Вчитај повеќе
+ Вчитај повеќе
)}
);
-}
\ No newline at end of file
+}
diff --git a/app/(tabs)/(profile)/index.tsx b/app/(tabs)/(profile)/index.tsx
index a37c597..e7120a8 100644
--- a/app/(tabs)/(profile)/index.tsx
+++ b/app/(tabs)/(profile)/index.tsx
@@ -7,13 +7,11 @@ import { useAuth } from "../../_layout";
import { useTheme } from "../../../components/theme";
import { Button } from "../../../components/ui/button";
import { Badge } from "../../../components/ui/badge";
-import { Rating } from "../../../components/ui/rating";
import { Loading } from "../../../components/ui/loading";
import { Card } from "../../../components/ui/card";
import { Avatar } from "../../../components/ui/avatar";
import { AdCard } from "../../../components/ad-card";
import { PostCard } from "../../../components/post-card";
-import { getCategoryName } from "../../../lib/constants";
export default function ProfileScreen() {
const router = useRouter();
@@ -73,15 +71,23 @@ export default function ProfileScreen() {
if (!isAuthenticated) {
return (
-
- 👤
-
+
+ 👤
+
Најавете се
-
+
За да ги видите вашиот профил и податоци.
- router.push("/(auth)/login")}>Најави се
+ router.push("/(auth)/login")} size="lg">
+ Најави се
+
);
}
@@ -106,52 +112,55 @@ export default function ProfileScreen() {
}
contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.md, paddingBottom: 80 }}
>
-
+
-
+
{user.name || "Корисник"}
{user.email && (
-
+
{user.email}
)}
{user.phone && (
-
+
{user.phone}
)}
-
+
{isHandyman && (
<>
-
+
Мои огласи
- router.push("/ad/create" as any)}
+ accessibilityLabel="Креирај нов оглас"
>
-
- + Нов оглас
-
-
+ + Нов оглас
+
{myAdsResult.status === "LoadingFirstPage" ? (
) : myAds.length === 0 ? (
-
-
- 📋
-
- Сеуште немате огласи. Креирајте нов оглас за да се прикажете на клиентите.
-
-
+
+ 📋
+
+ Сè уште немате огласи.
+
+ router.push("/ad/create" as any)}
+ style={{ marginTop: theme.spacing.md }}
+ >
+ Креирај оглас
+
) : (
@@ -171,7 +180,7 @@ export default function ProfileScreen() {
paddingVertical: 10,
alignItems: "center",
borderWidth: 1,
- borderColor: theme.colors.border,
+ borderColor: theme.colors.borderLight,
}}
>
Уреди
@@ -204,15 +213,15 @@ export default function ProfileScreen() {
accessibilityRole="button"
onPress={() => myAdsResult.loadMore(20)}
style={{
- backgroundColor: theme.colors.surface,
+ backgroundColor: theme.colors.surfaceAlt,
borderRadius: theme.radius.md,
paddingVertical: theme.spacing.md,
alignItems: "center",
borderWidth: 1,
- borderColor: theme.colors.border,
+ borderColor: theme.colors.borderLight,
}}
>
- Вчитај повеќе
+ Вчитај повеќе
)}
>
@@ -221,31 +230,34 @@ export default function ProfileScreen() {
{!isHandyman && (
<>
-
+
Мои побарувања
- router.push("/post/create" as any)}
+ accessibilityLabel="Креирај ново побарување"
>
-
- + Ново побарување
-
-
+ + Ново
+
{myPostsResult.status === "LoadingFirstPage" ? (
) : myPosts.length === 0 ? (
-
-
- 📝
-
- Сеуште немате побарувања. Креирајте ново побарување за да најдете мајстор.
-
-
+
+ 📝
+
+ Сè уште немате побарувања.
+
+ router.push("/post/create" as any)}
+ style={{ marginTop: theme.spacing.md }}
+ >
+ Креирај побарување
+
) : (
@@ -262,32 +274,43 @@ export default function ProfileScreen() {
accessibilityRole="button"
onPress={() => myPostsResult.loadMore(20)}
style={{
- backgroundColor: theme.colors.surface,
+ backgroundColor: theme.colors.surfaceAlt,
borderRadius: theme.radius.md,
paddingVertical: theme.spacing.md,
alignItems: "center",
borderWidth: 1,
- borderColor: theme.colors.border,
+ borderColor: theme.colors.borderLight,
}}
>
- Вчитај повеќе
+ Вчитај повеќе
)}
>
)}
-
-
- Оцени
+
+
+ Поставки
-
- Поставки
+
+
+ Оцени ја апликацијата
-
+
-
+
Одјави се
);
-}
\ No newline at end of file
+}
diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
index 5ce21ea..091d160 100644
--- a/app/(tabs)/_layout.tsx
+++ b/app/(tabs)/_layout.tsx
@@ -1,26 +1,35 @@
import { Tabs } from "expo-router";
+import { Ionicons } from "@expo/vector-icons";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useTheme } from "../../components/theme";
export default function TabLayout() {
const theme = useTheme();
+ const insets = useSafeAreaInsets();
return (
,
tabBarAccessibilityLabel: "Почетна",
}}
/>
@@ -28,6 +37,7 @@ export default function TabLayout() {
name="(explore)"
options={{
title: "Пребарување",
+ tabBarIcon: ({ color, size }) => ,
tabBarAccessibilityLabel: "Пребарување",
}}
/>
@@ -35,6 +45,7 @@ export default function TabLayout() {
name="(posts)"
options={{
title: "Побарувања",
+ tabBarIcon: ({ color, size }) => ,
tabBarAccessibilityLabel: "Побарувања",
}}
/>
@@ -42,6 +53,7 @@ export default function TabLayout() {
name="(chat)"
options={{
title: "Чат",
+ tabBarIcon: ({ color, size }) => ,
tabBarAccessibilityLabel: "Чат",
}}
/>
@@ -49,9 +61,10 @@ export default function TabLayout() {
name="(profile)"
options={{
title: "Профил",
+ tabBarIcon: ({ color, size }) => ,
tabBarAccessibilityLabel: "Профил",
}}
/>
);
-}
\ No newline at end of file
+}
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 0783958..9ab3d59 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -2,6 +2,7 @@ import { Stack } from "expo-router/stack";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { useState, createContext, useContext, useEffect, useCallback, Component, type ReactNode, type ErrorInfo } from "react";
import { View, Text, Pressable, ScrollView } from "react-native";
+import AsyncStorage from "@react-native-async-storage/async-storage";
import { ThemeProvider, useTheme } from "../components/theme";
const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!);
@@ -86,8 +87,8 @@ export default function RootLayout() {
useEffect(() => {
Promise.all([
- localStorage.getItem(TOKEN_KEY),
- localStorage.getItem(USER_ID_KEY),
+ AsyncStorage.getItem(TOKEN_KEY),
+ AsyncStorage.getItem(USER_ID_KEY),
]).then(([t, u]) => {
if (t) setToken(t);
if (u) setUserId(u);
@@ -96,15 +97,15 @@ export default function RootLayout() {
}, []);
const login = useCallback(async (newToken: string, newUserId: string) => {
- localStorage.setItem(TOKEN_KEY, newToken);
- localStorage.setItem(USER_ID_KEY, newUserId);
+ await AsyncStorage.setItem(TOKEN_KEY, newToken);
+ await AsyncStorage.setItem(USER_ID_KEY, newUserId);
setToken(newToken);
setUserId(newUserId);
}, []);
const logout = useCallback(async () => {
- localStorage.removeItem(TOKEN_KEY);
- localStorage.removeItem(USER_ID_KEY);
+ await AsyncStorage.removeItem(TOKEN_KEY);
+ await AsyncStorage.removeItem(USER_ID_KEY);
setToken(null);
setUserId(null);
}, []);
diff --git a/app/ad/[id].tsx b/app/ad/[id].tsx
index 30f1a3f..06f5359 100644
--- a/app/ad/[id].tsx
+++ b/app/ad/[id].tsx
@@ -143,54 +143,85 @@ export default function AdDetailScreen() {
accessible
accessibilityLabel={`Слика за ${ad.title}`}
style={{
- backgroundColor: theme.colors.surface,
+ backgroundColor: theme.colors.surfaceAlt,
borderRadius: theme.radius.lg,
aspectRatio: 16 / 9,
alignItems: "center",
justifyContent: "center",
borderWidth: 1,
- borderColor: theme.colors.border,
+ borderColor: theme.colors.borderLight,
borderStyle: "dashed",
}}
>
- {categoryEmoji}
-
+
+ {categoryEmoji}
+
+
Нема слики
-
- {ad.title}
- {ad.priceRange && (
-
- {ad.priceRange}
+
+
+
+ {ad.title}
+
+ {ad.priceRange && (
+
+
+ {ad.priceRange}
+
+
+ )}
+
+
+
+
+
+ 📍 {ad.location}
+
+
+ {ad.ratingAvg != null && ad.ratingAvg > 0 && (
+
)}
-
-
- 📍 {ad.location}
-
-
- {ad.ratingAvg != null && ad.ratingAvg > 0 && (
-
- )}
-
{ad.availability && (
-
+
Расположивост
- {ad.availability}
+
+ {ad.availability}
+
)}
-
+
Опис
- {ad.description}
+
+ {ad.description}
+
@@ -205,24 +236,33 @@ export default function AdDetailScreen() {
>
-
+
{handyman?.name || "Мајстор"}
- Мајстор
+
+ Мајстор
+
+ ›
{!isOwner && isAuthenticated && (
- Започни разговор
+
+ Започни разговор
+
{!existingUserReview && (
- router.push(`/review/create?adId=${ad._id}` as any)}>
+ router.push(`/review/create?adId=${ad._id}` as any)}
+ fullWidth
+ >
Напиши оценка
)}
{existingUserReview && (
-
+
Веќе напишавте оценка за овој оглас
)}
@@ -234,10 +274,11 @@ export default function AdDetailScreen() {
router.push(`/ad/create?editId=${ad._id}` as any)}
+ fullWidth
>
Уреди оглас
-
+
Избриши оглас
@@ -245,27 +286,28 @@ export default function AdDetailScreen() {
{!isOwner && !isAuthenticated && (
-
+
Најавете се за да започнете разговор или да напишете оценка.
- router.push("/(auth)/login")}>Најави се
+ router.push("/(auth)/login")} fullWidth>
+ Најави се
+
)}
- {/* Reviews section */}
{reviews && reviews.length > 0 && (
-
+
Оцени
-
+
{ad.ratingAvg?.toFixed(1)}
-
+
({ad.reviewCount} {ad.reviewCount === 1 ? "оценка" : "оцени"})
@@ -287,4 +329,4 @@ export default function AdDetailScreen() {
>
);
-}
\ No newline at end of file
+}
diff --git a/components/ad-card.tsx b/components/ad-card.tsx
index 2708eb7..4472808 100644
--- a/components/ad-card.tsx
+++ b/components/ad-card.tsx
@@ -3,7 +3,7 @@ import { Link } from "expo-router";
import { useTheme } from "./theme";
import { Badge } from "./ui/badge";
import { Rating } from "./ui/rating";
-import { getCategoryName } from "../lib/constants";
+import { getCategoryName, CATEGORY_EMOJI } from "../lib/constants";
interface AdCardProps {
ad: {
@@ -20,6 +20,7 @@ interface AdCardProps {
export function AdCard({ ad }: AdCardProps) {
const theme = useTheme();
const categoryName = getCategoryName(ad.category);
+ const emoji = CATEGORY_EMOJI[ad.category] ?? "🔧";
return (
@@ -30,33 +31,58 @@ export function AdCard({ ad }: AdCardProps) {
style={{
backgroundColor: theme.colors.card,
borderRadius: theme.radius.lg,
- padding: theme.spacing.md,
borderWidth: 1,
- borderColor: theme.colors.border,
- gap: theme.spacing.sm,
+ borderColor: theme.colors.borderLight,
+ ...theme.shadows.sm,
+ overflow: "hidden",
}}
>
-
-
-
- {ad.title}
-
- {ad.ratingAvg != null && ad.ratingAvg > 0 && (
-
+
+
+
+ {emoji}
+
+
+
+ {ad.title}
+
+ {ad.ratingAvg != null && ad.ratingAvg > 0 && (
+
+ )}
+
+ {ad.priceRange && (
+
+
+ {ad.priceRange}
+
+
)}
- {ad.priceRange && (
-
- {ad.priceRange}
-
- )}
-
-
-
- 📍 {ad.location}
+
+
+
+ 📍 {ad.location}
+
+
);
-}
\ No newline at end of file
+}
diff --git a/components/category-grid.tsx b/components/category-grid.tsx
index 40a6b96..6ffb94c 100644
--- a/components/category-grid.tsx
+++ b/components/category-grid.tsx
@@ -15,6 +15,13 @@ interface CategoryGridProps {
columns?: number;
}
+function getCategoryColor(slug: string): string {
+ const colors = ["#D4764A", "#2B5797", "#4A8C6F", "#C47A4A", "#8B5CF6", "#E07B5A",
+ "#3B82F6", "#7C3AED", "#06B6D4", "#D97706", "#059669", "#6B7280"];
+ const idx = slug.split("").reduce((a, c) => a + c.charCodeAt(0), 0);
+ return colors[idx % colors.length];
+}
+
export function CategoryGrid({ categories, columns = 3 }: CategoryGridProps) {
const theme = useTheme();
@@ -22,6 +29,7 @@ export function CategoryGrid({ categories, columns = 3 }: CategoryGridProps) {
{categories.map((cat) => {
const emoji = CATEGORY_EMOJI[cat.slug] ?? cat.icon ?? "📋";
+ const color = getCategoryColor(cat.slug);
return (
- {emoji}
+
+ {emoji}
+
{cat.name}
@@ -57,4 +78,4 @@ export function CategoryGrid({ categories, columns = 3 }: CategoryGridProps) {
})}
);
-}
\ No newline at end of file
+}
diff --git a/components/chat-list-item.tsx b/components/chat-list-item.tsx
index 9dcb6be..c0834e0 100644
--- a/components/chat-list-item.tsx
+++ b/components/chat-list-item.tsx
@@ -58,42 +58,28 @@ export function ChatListItem({ chat, currentUserId }: ChatListItemProps) {
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
gap: theme.spacing.md,
- borderBottomWidth: 1,
- borderBottomColor: theme.colors.border,
backgroundColor: theme.colors.card,
+ borderBottomWidth: 1,
+ borderBottomColor: theme.colors.borderLight,
}}
>
-
-
-
+
+
+
{chat.otherUser?.name ?? "Непознат"}
{chat.lastMessage && (
-
+
{formatRelativeTime(chat.lastMessage.createdAt)}
)}
-
+
{previewText}
@@ -102,4 +88,4 @@ export function ChatListItem({ chat, currentUserId }: ChatListItemProps) {
);
}
-export { formatRelativeTime };
\ No newline at end of file
+export { formatRelativeTime };
diff --git a/components/post-card.tsx b/components/post-card.tsx
index 991ec1a..9196707 100644
--- a/components/post-card.tsx
+++ b/components/post-card.tsx
@@ -2,7 +2,7 @@ import { View, Text, Pressable } from "react-native";
import { Link } from "expo-router";
import { useTheme } from "./theme";
import { Badge } from "./ui/badge";
-import { getCategoryName } from "../lib/constants";
+import { getCategoryName, CATEGORY_EMOJI } from "../lib/constants";
interface PostCardProps {
post: {
@@ -20,6 +20,7 @@ interface PostCardProps {
export function PostCard({ post }: PostCardProps) {
const theme = useTheme();
const categoryName = post.category ? getCategoryName(post.category) : null;
+ const emoji = post.category ? (CATEGORY_EMOJI[post.category] ?? "📋") : "📋";
const isOpen = post.status === "open";
return (
@@ -31,35 +32,56 @@ export function PostCard({ post }: PostCardProps) {
style={{
backgroundColor: theme.colors.card,
borderRadius: theme.radius.lg,
- padding: theme.spacing.md,
borderWidth: 1,
- borderColor: theme.colors.border,
- gap: theme.spacing.sm,
+ borderColor: theme.colors.borderLight,
+ ...theme.shadows.sm,
+ overflow: "hidden",
}}
>
-
-
- {post.title}
+
+
+
+
+ {emoji}
+
+
+ {post.title}
+
+
+
+
+
+
+ {post.description}
-
-
-
- {post.description}
-
-
-
- {categoryName && }
- {post.location && (
- 📍 {post.location}
- )}
- {post.budget && (
-
- 💰 {post.budget}
-
- )}
+
+ {categoryName && }
+ {post.location && (
+
+ 📍 {post.location}
+
+ )}
+ {post.budget && (
+
+ 💰 {post.budget}
+
+ )}
+
);
-}
\ No newline at end of file
+}
diff --git a/components/review-card.tsx b/components/review-card.tsx
index 51497d1..46f9493 100644
--- a/components/review-card.tsx
+++ b/components/review-card.tsx
@@ -45,28 +45,28 @@ export function ReviewCard({ review, customerName, customerAvatarId }: ReviewCar
borderRadius: theme.radius.lg,
padding: theme.spacing.md,
borderWidth: 1,
- borderColor: theme.colors.border,
+ borderColor: theme.colors.borderLight,
gap: theme.spacing.sm,
}}
>
-
+
{customerName}
-
+
{formatTimeAgo(review.createdAt)}
-
+
{review.comment ? (
-
+
{review.comment}
) : null}
);
-}
\ No newline at end of file
+}
diff --git a/components/theme.tsx b/components/theme.tsx
index cdaad88..a86078b 100644
--- a/components/theme.tsx
+++ b/components/theme.tsx
@@ -4,17 +4,53 @@ import { useColorScheme } from "react-native";
type ThemeColors = {
primary: string;
primaryLight: string;
+ primaryDark: string;
secondary: string;
+ secondaryLight: string;
background: string;
surface: string;
+ surfaceAlt: string;
card: string;
border: string;
+ borderLight: string;
text: string;
textSecondary: string;
textTertiary: string;
+ textInverse: string;
error: string;
errorLight: string;
+ success: string;
+ successLight: string;
star: string;
+ starLight: string;
+ tabActive: string;
+ tabInactive: string;
+};
+
+type Shadow = {
+ shadowColor: string;
+ shadowOffset: { width: number; height: number };
+ shadowOpacity: number;
+ shadowRadius: number;
+ elevation: number;
+};
+
+type ThemeShadows = {
+ sm: Shadow;
+ md: Shadow;
+ lg: Shadow;
+ button: Shadow;
+};
+
+type Typography = {
+ h1: { fontSize: number; fontWeight: "700"; lineHeight: number };
+ h2: { fontSize: number; fontWeight: "600"; lineHeight: number };
+ h3: { fontSize: number; fontWeight: "600"; lineHeight: number };
+ body: { fontSize: number; fontWeight: "400"; lineHeight: number };
+ bodyBold: { fontSize: number; fontWeight: "600"; lineHeight: number };
+ caption: { fontSize: number; fontWeight: "400"; lineHeight: number };
+ captionBold: { fontSize: number; fontWeight: "600"; lineHeight: number };
+ label: { fontSize: number; fontWeight: "500"; lineHeight: number };
};
type Theme = {
@@ -25,56 +61,117 @@ type Theme = {
md: number;
lg: number;
xl: number;
+ xxl: number;
};
radius: {
sm: number;
md: number;
lg: number;
xl: number;
+ full: number;
};
+ shadows: ThemeShadows;
+ typography: Typography;
+};
+
+const makeShadow = (elevation: number, color: string): Shadow => {
+ const shadows: Record = {
+ 2: { shadowColor: color, shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 3, elevation: 2 },
+ 4: { shadowColor: color, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.12, shadowRadius: 8, elevation: 4 },
+ 8: { shadowColor: color, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.15, shadowRadius: 16, elevation: 8 },
+ };
+ return shadows[elevation] || shadows[2];
+};
+
+const spacing = { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48 };
+const radius = { sm: 6, md: 12, lg: 16, xl: 24, full: 999 };
+
+const typography: Typography = {
+ h1: { fontSize: 28, fontWeight: "700", lineHeight: 36 },
+ h2: { fontSize: 22, fontWeight: "600", lineHeight: 30 },
+ h3: { fontSize: 18, fontWeight: "600", lineHeight: 24 },
+ body: { fontSize: 15, fontWeight: "400", lineHeight: 22 },
+ bodyBold: { fontSize: 15, fontWeight: "600", lineHeight: 22 },
+ caption: { fontSize: 13, fontWeight: "400", lineHeight: 18 },
+ captionBold: { fontSize: 13, fontWeight: "600", lineHeight: 18 },
+ label: { fontSize: 11, fontWeight: "500", lineHeight: 16 },
};
const LightTheme: Theme = {
colors: {
- primary: "#2563EB",
- primaryLight: "#DBEAFE",
- secondary: "#059669",
- background: "#FFFFFF",
- surface: "#F9FAFB",
+ primary: "#D4764A",
+ primaryLight: "#FDF0EA",
+ primaryDark: "#B85E34",
+ secondary: "#1A2A3A",
+ secondaryLight: "#E8ECF0",
+ background: "#FDF8F3",
+ surface: "#FFFFFF",
+ surfaceAlt: "#F5F0EB",
card: "#FFFFFF",
- border: "#E5E7EB",
- text: "#111827",
- textSecondary: "#6B7280",
- textTertiary: "#9CA3AF",
+ border: "#E8E2DA",
+ borderLight: "#F0EBE5",
+ text: "#1A1A2E",
+ textSecondary: "#8B7E74",
+ textTertiary: "#B0A69C",
+ textInverse: "#FFFFFF",
error: "#DC2626",
errorLight: "#FEE2E2",
+ success: "#4A8C6F",
+ successLight: "#E8F3EE",
star: "#F59E0B",
+ starLight: "#FEF3C7",
+ tabActive: "#D4764A",
+ tabInactive: "#B0A69C",
},
- spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 },
- radius: { sm: 6, md: 12, lg: 16, xl: 24 },
+ spacing,
+ radius,
+ shadows: {
+ sm: makeShadow(2, "#000000"),
+ md: makeShadow(4, "#000000"),
+ lg: makeShadow(8, "#000000"),
+ button: { shadowColor: "#D4764A", shadowOffset: { width: 0, height: 3 }, shadowOpacity: 0.3, shadowRadius: 6, elevation: 4 },
+ },
+ typography,
};
const DarkTheme: Theme = {
colors: {
- primary: "#60A5FA",
- primaryLight: "#1E3A5F",
- secondary: "#34D399",
- background: "#111827",
- surface: "#1F2937",
- card: "#1F2937",
- border: "#374151",
- text: "#F9FAFB",
+ primary: "#E89B78",
+ primaryLight: "#2D1F18",
+ primaryDark: "#D4764A",
+ secondary: "#E8ECF0",
+ secondaryLight: "#2A3040",
+ background: "#0F0F1A",
+ surface: "#1A1A2E",
+ surfaceAlt: "#222240",
+ card: "#1A1A2E",
+ border: "#2A2A45",
+ borderLight: "#222240",
+ text: "#F0EDE8",
textSecondary: "#9CA3AF",
textTertiary: "#6B7280",
+ textInverse: "#FFFFFF",
error: "#F87171",
errorLight: "#451111",
+ success: "#6EC99B",
+ successLight: "#1A3A2E",
star: "#FBBF24",
+ starLight: "#3A2E1A",
+ tabActive: "#E89B78",
+ tabInactive: "#6B7280",
},
- spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 },
- radius: { sm: 6, md: 12, lg: 16, xl: 24 },
+ spacing,
+ radius,
+ shadows: {
+ sm: makeShadow(2, "#000000"),
+ md: makeShadow(4, "#000000"),
+ lg: makeShadow(8, "#000000"),
+ button: { shadowColor: "#E89B78", shadowOffset: { width: 0, height: 3 }, shadowOpacity: 0.3, shadowRadius: 6, elevation: 4 },
+ },
+ typography,
};
-export type { Theme };
+export type { Theme, ThemeColors, ThemeShadows, Typography };
const ThemeContext = createContext(LightTheme);
@@ -86,4 +183,4 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
export function useTheme() {
return useContext(ThemeContext);
-}
\ No newline at end of file
+}
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
index 2e8f652..3bdc830 100644
--- a/components/ui/badge.tsx
+++ b/components/ui/badge.tsx
@@ -3,28 +3,41 @@ import { useTheme } from "../theme";
interface BadgeProps {
label: string;
- variant?: "default" | "primary" | "success" | "error";
+ variant?: "default" | "primary" | "success" | "error" | "warning";
+ size?: "sm" | "md";
}
-export function Badge({ label, variant = "default" }: BadgeProps) {
+export function Badge({ label, variant = "default", size = "md" }: BadgeProps) {
const theme = useTheme();
- const variants = {
- default: { bg: theme.colors.surface, text: theme.colors.textSecondary },
- primary: { bg: theme.colors.primaryLight, text: theme.colors.primary },
- success: { bg: "#D1FAE5", text: "#059669" },
- error: { bg: theme.colors.errorLight, text: theme.colors.error },
+ const styles = {
+ default: { bg: theme.colors.surfaceAlt, text: theme.colors.textSecondary, border: theme.colors.borderLight },
+ primary: { bg: theme.colors.primaryLight, text: theme.colors.primaryDark, border: theme.colors.primaryLight },
+ success: { bg: theme.colors.successLight, text: theme.colors.success, border: theme.colors.successLight },
+ error: { bg: theme.colors.errorLight, text: theme.colors.error, border: theme.colors.errorLight },
+ warning: { bg: theme.colors.starLight, text: "#92400E", border: theme.colors.starLight },
};
- const { bg, text: textColor } = variants[variant];
+ const { bg, text: textColor, border } = styles[variant];
+ const isSmall = size === "sm";
return (
- {label}
+
+ {label}
+
);
-}
\ No newline at end of file
+}
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
index 5bbf942..0fb53a8 100644
--- a/components/ui/button.tsx
+++ b/components/ui/button.tsx
@@ -1,13 +1,14 @@
import { Pressable, Text, ActivityIndicator, type ViewStyle } from "react-native";
import { useTheme } from "../theme";
-type Variant = "primary" | "secondary" | "outline" | "destructive";
+type Variant = "primary" | "secondary" | "outline" | "ghost" | "destructive";
interface ButtonProps {
variant?: Variant;
loading?: boolean;
size?: "sm" | "md" | "lg";
disabled?: boolean;
+ fullWidth?: boolean;
onPress?: () => void;
children: React.ReactNode;
style?: ViewStyle;
@@ -19,6 +20,7 @@ export function Button({
loading = false,
size = "md",
disabled = false,
+ fullWidth = false,
children,
style,
onPress,
@@ -27,22 +29,24 @@ export function Button({
const theme = useTheme();
const sizeStyles: Record = {
- sm: { paddingVertical: theme.spacing.sm, paddingHorizontal: theme.spacing.md, borderRadius: theme.radius.sm },
- md: { paddingVertical: 12, paddingHorizontal: theme.spacing.lg, borderRadius: theme.radius.md },
- lg: { paddingVertical: 16, paddingHorizontal: theme.spacing.xl, borderRadius: theme.radius.md },
+ sm: { paddingVertical: 8, paddingHorizontal: theme.spacing.md, borderRadius: theme.radius.sm },
+ md: { paddingVertical: 14, paddingHorizontal: theme.spacing.lg, borderRadius: theme.radius.md },
+ lg: { paddingVertical: 18, paddingHorizontal: theme.spacing.xl, borderRadius: theme.radius.lg },
};
const variantStyles: Record = {
- primary: { backgroundColor: theme.colors.primary },
- secondary: { backgroundColor: theme.colors.secondary },
- outline: { backgroundColor: "transparent", borderWidth: 1, borderColor: theme.colors.primary },
+ primary: { backgroundColor: theme.colors.primary, ...theme.shadows.button },
+ secondary: { backgroundColor: theme.colors.surface, borderWidth: 1.5, borderColor: theme.colors.border },
+ outline: { backgroundColor: "transparent", borderWidth: 1.5, borderColor: theme.colors.primary },
+ ghost: { backgroundColor: "transparent" },
destructive: { backgroundColor: theme.colors.error },
};
const textColors: Record = {
- primary: "#FFFFFF",
- secondary: "#FFFFFF",
+ primary: theme.colors.textInverse,
+ secondary: theme.colors.text,
outline: theme.colors.primary,
+ ghost: theme.colors.primary,
destructive: "#FFFFFF",
};
@@ -57,17 +61,30 @@ export function Button({
disabled={disabled || loading}
onPress={onPress}
style={[
- { alignItems: "center", justifyContent: "center" },
+ {
+ alignItems: "center",
+ justifyContent: "center",
+ flexDirection: "row",
+ gap: theme.spacing.sm,
+ },
sizeStyles[size],
variantStyles[variant],
+ fullWidth && { width: "100%" },
disabled && { opacity: 0.5 },
style,
]}
>
{loading ? (
-
+
) : typeof children === "string" ? (
-
+
{children}
) : (
@@ -75,4 +92,4 @@ export function Button({
)}
);
-}
\ No newline at end of file
+}
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
index 63486f1..1f43a56 100644
--- a/components/ui/card.tsx
+++ b/components/ui/card.tsx
@@ -1,28 +1,53 @@
-import { View, type ViewStyle } from "react-native";
+import { View, Pressable, type ViewStyle } from "react-native";
import { useTheme } from "../theme";
interface CardProps {
children: React.ReactNode;
style?: ViewStyle;
onPress?: () => void;
+ variant?: "default" | "elevated" | "outlined";
}
-export function Card({ children, style, onPress }: CardProps) {
+export function Card({ children, style, onPress, variant = "default" }: CardProps) {
const theme = useTheme();
+ const variantStyles: Record = {
+ default: {
+ backgroundColor: theme.colors.card,
+ borderRadius: theme.radius.lg,
+ padding: theme.spacing.md,
+ borderWidth: 1,
+ borderColor: theme.colors.borderLight,
+ },
+ elevated: {
+ backgroundColor: theme.colors.card,
+ borderRadius: theme.radius.lg,
+ padding: theme.spacing.md,
+ ...theme.shadows.md,
+ },
+ outlined: {
+ backgroundColor: "transparent",
+ borderRadius: theme.radius.lg,
+ padding: theme.spacing.md,
+ borderWidth: 1.5,
+ borderColor: theme.colors.border,
+ },
+ };
+
+ if (onPress) {
+ return (
+
+ {children}
+
+ );
+ }
+
return (
-
+
{children}
);
-}
\ No newline at end of file
+}
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
index 4290888..05445eb 100644
--- a/components/ui/input.tsx
+++ b/components/ui/input.tsx
@@ -1,34 +1,48 @@
-import { TextInput, type TextInputProps, type ViewStyle } from "react-native";
+import { View, TextInput, Text, type TextInputProps, type ViewStyle } from "react-native";
import { useTheme } from "../theme";
interface InputProps extends TextInputProps {
error?: string;
+ label?: string;
+ containerStyle?: ViewStyle;
}
-export function Input({ error, style, ...props }: InputProps) {
+export function Input({ error, label, containerStyle, style, ...props }: InputProps) {
const theme = useTheme();
return (
-
+
+ {label && (
+
+ {label}
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
);
-}
\ No newline at end of file
+}
diff --git a/convex/tsconfig.json b/convex/tsconfig.json
new file mode 100644
index 0000000..41bfbb9
--- /dev/null
+++ b/convex/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ /* This TypeScript project config describes the environment that
+ * Convex functions run in and is used to typecheck them.
+ * You can modify it, but some settings are required to use Convex.
+ */
+ "compilerOptions": {
+ /* These settings are not required by Convex and can be modified. */
+ "allowJs": true,
+ "strict": true,
+ "moduleResolution": "Bundler",
+ "jsx": "react-jsx",
+ "skipLibCheck": true,
+ "allowSyntheticDefaultImports": true,
+
+ /* These compiler options are required by Convex */
+ "target": "ESNext",
+ "lib": ["ES2023", "dom"],
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "include": ["./**/*"],
+ "exclude": ["./_generated"]
+}
diff --git a/mise.toml b/mise.toml
new file mode 100644
index 0000000..a8c096f
--- /dev/null
+++ b/mise.toml
@@ -0,0 +1,2 @@
+[tools]
+java = "21.0.2"
diff --git a/package-lock.json b/package-lock.json
index b87dfe5..e346d90 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.0.0",
"dependencies": {
"@expo/vector-icons": "^15.0.2",
+ "@react-native-async-storage/async-storage": "^2.2.0",
"convex": "^1.24.0",
"expo": "~55.0.26",
"expo-constants": "~55.0.16",
@@ -3214,6 +3215,18 @@
}
}
},
+ "node_modules/@react-native-async-storage/async-storage": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
+ "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
+ "license": "MIT",
+ "dependencies": {
+ "merge-options": "^3.0.4"
+ },
+ "peerDependencies": {
+ "react-native": "^0.0.0-0 || >=0.65 <1.0"
+ }
+ },
"node_modules/@react-native/assets-registry": {
"version": "0.83.6",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.83.6.tgz",
@@ -5888,6 +5901,15 @@
"node": ">=0.12.0"
}
},
+ "node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -6352,9 +6374,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -6375,9 +6394,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -6398,9 +6414,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -6421,9 +6434,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -6626,6 +6636,18 @@
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT"
},
+ "node_modules/merge-options": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
+ "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
diff --git a/package.json b/package.json
index 690a4ed..120fcb5 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
},
"dependencies": {
"@expo/vector-icons": "^15.0.2",
+ "@react-native-async-storage/async-storage": "^2.2.0",
"convex": "^1.24.0",
"expo": "~55.0.26",
"expo-constants": "~55.0.16",