mojmajstor/app/(tabs)/(profile)/index.tsx
echo ff464b0f9b feat: Phase 3 - ad creation, ad detail, profile my-ads
- Add create/update/remove ad mutations with auth & ownership checks
- Ad detail screen: gallery placeholder, title, description, category
  badge, location, price range, availability, rating, handyman info
  card, start-chat and write-review buttons for non-owners,
  edit/delete for ad owner, auth redirect for anonymous
- Create ad screen: 4-step form (category → title/description →
  location → price/availability/submit), handyman-only guard,
  edit mode via editId param
- Profile screen: user avatar/name/role/email/phone, my-ads
  section for handymen with edit/delete, new-ad button, placeholder
  my-posts section for customers, proper logout mutation

All UI text in Macedonian. TypeScript clean, Convex functions deployed.
2026-05-29 18:28:01 +02:00

213 lines
8.9 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, Pressable, ScrollView, Alert } from "react-native";
import { useRouter } from "expo-router";
import { useQuery, useMutation } from "convex/react";
import { api } from "../../../convex/_generated/api";
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 { getCategoryName } from "../../../lib/constants";
export default function ProfileScreen() {
const router = useRouter();
const theme = useTheme();
const { token, userId, isAuthenticated, logout } = useAuth();
const user = useQuery(api.users.getCurrentUser, isAuthenticated ? { token: token! } : "skip");
const myAds = useQuery(
api.ads.getByHandyman,
isAuthenticated && userId ? { handymanId: userId as any } : "skip"
);
const deleteAd = useMutation(api.ads.remove);
const logoutMutation = useMutation(api.auth.logout);
async function handleLogout() {
if (token) {
try {
await logoutMutation({ token });
} catch {}
}
await logout();
router.replace("/(auth)/login");
}
async function handleDeleteAd(adId: string) {
Alert.alert(
"Избриши оглас",
"Дали сте сигурни дека сакате да го избришете овој оглас?",
[
{ text: "Откажи", style: "cancel" },
{
text: "Избриши",
style: "destructive",
onPress: async () => {
try {
await deleteAd({ token: token!, adId: adId as any });
} catch (e: any) {
Alert.alert("Грешка", e.message || "Неуспешно бришење");
}
},
},
]
);
}
if (!isAuthenticated) {
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: theme.colors.background, padding: theme.spacing.lg }}>
<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", marginBottom: theme.spacing.lg }}>
За да ги видите вашиот профил и податоци.
</Text>
<Button onPress={() => router.push("/(auth)/login")}>Најави се</Button>
</View>
);
}
if (!user) {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: theme.colors.background }}>
<Loading />
</View>
);
}
const isHandyman = user.role === "handyman";
return (
<ScrollView
contentInsetAdjustmentBehavior="automatic"
contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.md, paddingBottom: 80 }}
>
{/* Profile header */}
<View style={{ alignItems: "center", paddingVertical: theme.spacing.lg }}>
<Avatar uri={user.avatarId ?? null} name={user.name || "Корисник"} size={80} />
<Text style={{ fontSize: 20, fontWeight: "700", color: theme.colors.text, marginTop: theme.spacing.md }}>
{user.name || "Корисник"}
</Text>
<Badge label={isHandyman ? "Мајстор" : "Клиент"} variant={isHandyman ? "primary" : "default"} />
{user.email && (
<Text style={{ color: theme.colors.textSecondary, fontSize: 14, marginTop: theme.spacing.xs }}>
{user.email}
</Text>
)}
{user.phone && (
<Text style={{ color: theme.colors.textSecondary, fontSize: 14 }}>
{user.phone}
</Text>
)}
</View>
{/* Handyman sections */}
{isHandyman && (
<>
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
<Text style={{ fontSize: 18, fontWeight: "700", color: theme.colors.text }}>
Мои огласи
</Text>
<Pressable onPress={() => router.push("/ad/create" as any)}>
<View style={{ backgroundColor: theme.colors.primary, borderRadius: theme.radius.md, paddingHorizontal: theme.spacing.md, paddingVertical: theme.spacing.sm }}>
<Text style={{ color: "#FFFFFF", fontWeight: "600", fontSize: 14 }}>+ Нов оглас</Text>
</View>
</Pressable>
</View>
{myAds === undefined ? (
<Loading />
) : myAds.length === 0 ? (
<Card>
<View style={{ alignItems: "center", paddingVertical: theme.spacing.lg }}>
<Text style={{ fontSize: 40, marginBottom: theme.spacing.sm }}>📋</Text>
<Text style={{ color: theme.colors.textSecondary, textAlign: "center", fontSize: 15 }}>
Сеуште немате огласи. Креирајте нов оглас за да се прикажете на мајсторите.
</Text>
</View>
</Card>
) : (
<View style={{ gap: theme.spacing.sm }}>
{myAds.map((ad) => (
<View key={ad._id} style={{ gap: theme.spacing.sm }}>
<AdCard ad={ad} />
<View style={{ flexDirection: "row", gap: theme.spacing.sm }}>
<Pressable
onPress={() => router.push(`/ad/create?editId=${ad._id}` as any)}
style={{
flex: 1,
backgroundColor: theme.colors.surface,
borderRadius: theme.radius.md,
paddingVertical: 10,
alignItems: "center",
borderWidth: 1,
borderColor: theme.colors.border,
}}
>
<Text style={{ color: theme.colors.primary, fontWeight: "600", fontSize: 14 }}>Уреди</Text>
</Pressable>
<Pressable
onPress={() => handleDeleteAd(ad._id)}
style={{
flex: 1,
backgroundColor: theme.colors.errorLight,
borderRadius: theme.radius.md,
paddingVertical: 10,
alignItems: "center",
}}
>
<Text style={{ color: theme.colors.error, fontWeight: "600", fontSize: 14 }}>Избриши</Text>
</Pressable>
</View>
</View>
))}
</View>
)}
</>
)}
{/* Customer sections */}
{!isHandyman && (
<>
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
<Text style={{ fontSize: 18, fontWeight: "700", color: theme.colors.text }}>
Мои побарувања
</Text>
<Pressable>
<View style={{ backgroundColor: theme.colors.primary, borderRadius: theme.radius.md, paddingHorizontal: theme.spacing.md, paddingVertical: theme.spacing.sm }}>
<Text style={{ color: "#FFFFFF", fontWeight: "600", fontSize: 14 }}>+ Ново побарување</Text>
</View>
</Pressable>
</View>
<Card>
<View style={{ alignItems: "center", paddingVertical: theme.spacing.lg }}>
<Text style={{ fontSize: 40, marginBottom: theme.spacing.sm }}>📝</Text>
<Text style={{ color: theme.colors.textSecondary, textAlign: "center", fontSize: 15 }}>
Сеуште немате побарувања. Креирајте ново побарување за да најдете мајстор.
</Text>
</View>
</Card>
</>
)}
{/* Menu items */}
<View style={{ backgroundColor: theme.colors.card, borderRadius: theme.radius.lg, padding: theme.spacing.md, gap: theme.spacing.sm, borderWidth: 1, borderColor: theme.colors.border }}>
<Pressable style={{ paddingVertical: theme.spacing.sm }}>
<Text style={{ color: theme.colors.text, fontSize: 16 }}>Оцени</Text>
</Pressable>
<Pressable style={{ paddingVertical: theme.spacing.sm }}>
<Text style={{ color: theme.colors.text, fontSize: 16 }}>Поставки</Text>
</Pressable>
</View>
<Button variant="destructive" onPress={handleLogout}>
Одјави се
</Button>
</ScrollView>
);
}