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

327 lines
13 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, RefreshControl, Alert } from "react-native";
import { useRouter } from "expo-router";
import { useState, useCallback } from "react";
import { Ionicons } from "@expo/vector-icons";
import { useQuery, useMutation, usePaginatedQuery } 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 { Loading } from "../../../components/ui/loading";
import { Card } from "../../../components/ui/card";
import { Avatar } from "../../../components/ui/avatar";
import { ThemeToggleRow } from "../../../components/ui/theme-toggle";
import { AdCard } from "../../../components/ad-card";
import { PostCard } from "../../../components/post-card";
export default function ProfileScreen() {
const router = useRouter();
const theme = useTheme();
const { token, userId, isAuthenticated, logout } = useAuth();
const [refreshing, setRefreshing] = useState(false);
const user = useQuery(api.users.getCurrentUser, isAuthenticated ? { token: token! } : "skip");
const myAdsResult = usePaginatedQuery(
api.ads.getByHandymanPaginated,
isAuthenticated && userId ? { handymanId: userId as any } : "skip",
{ initialNumItems: 20 }
);
const myPostsResult = usePaginatedQuery(
api.posts.getByCustomerPaginated,
isAuthenticated && userId ? { customerId: userId as any } : "skip",
{ initialNumItems: 20 }
);
const deleteAd = useMutation(api.ads.remove);
const logoutMutation = useMutation(api.auth.logout);
const onRefresh = useCallback(() => {
setRefreshing(true);
setTimeout(() => setRefreshing(false), 800);
}, []);
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, backgroundColor: theme.colors.background }}>
<View style={{ paddingHorizontal: theme.spacing.md, paddingTop: 60, paddingBottom: theme.spacing.md, backgroundColor: theme.colors.surface, ...theme.shadows.xs }}>
<Text style={{ ...theme.typography.h2, color: theme.colors.text }}>
Профил
</Text>
</View>
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", padding: theme.spacing.lg }}>
<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="person" size={30} color={theme.colors.primary} />
</View>
<Text style={{ ...theme.typography.bodyBold, color: theme.colors.text, marginBottom: theme.spacing.xs }}>
Потребна најава
</Text>
<Text style={{ ...theme.typography.caption, color: theme.colors.textSecondary, textAlign: "center", marginBottom: theme.spacing.lg }}>
Најавете се за да го видите вашиот профил
</Text>
<Button onPress={() => router.push("/(auth)/login")}>
Најави се
</Button>
</View>
</View>
);
}
if (!user) {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: theme.colors.background }}>
<Loading />
</View>
);
}
const isHandyman = user.role === "handyman";
const myAds = myAdsResult.results;
const myPosts = myPostsResult.results;
return (
<View style={{ flex: 1, backgroundColor: theme.colors.background }}>
<View style={{ paddingTop: 60, paddingBottom: theme.spacing.lg, paddingHorizontal: theme.spacing.md, backgroundColor: theme.colors.surface, ...theme.shadows.xs }}>
<View style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.md }}>
<Avatar uri={user.avatarId ?? null} name={user.name || "Корисник"} size={68} borderColor={theme.colors.primaryLight} />
<View style={{ flex: 1 }}>
<Text selectable style={{ ...theme.typography.h3, color: theme.colors.text }}>
{user.name || "Корисник"}
</Text>
<View style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.sm, marginTop: 4 }}>
<Badge label={isHandyman ? "Мајстор" : "Клиент"} variant={isHandyman ? "primary" : "default"} />
{(user.email || user.phone) && (
<Text style={{ ...theme.typography.caption, color: theme.colors.textSecondary }}>
{user.email || user.phone}
</Text>
)}
</View>
</View>
<Pressable
accessible
accessibilityLabel="Подесувања"
accessibilityRole="button"
onPress={() => {}}
style={{
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: theme.colors.surfaceAlt,
alignItems: "center",
justifyContent: "center",
}}
>
<Ionicons name="settings-outline" size={20} color={theme.colors.textSecondary} />
</Pressable>
</View>
</View>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.colors.primary} />
}
contentContainerStyle={{ padding: theme.spacing.md, gap: theme.spacing.md, paddingBottom: 100 }}
>
{isHandyman && (
<>
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
<Text style={{ ...theme.typography.h4, color: theme.colors.text }}>
Мои огласи ({myAds.length})
</Text>
<Pressable
accessible
accessibilityLabel="Креирај нов оглас"
accessibilityRole="button"
onPress={() => router.push("/ad/create" as any)}
style={{ flexDirection: "row", alignItems: "center", gap: 4 }}
>
<Ionicons name="add-circle" size={20} color={theme.colors.primary} />
<Text style={{ ...theme.typography.bodyBold, color: theme.colors.primary, fontSize: 13 }}>
Нов оглас
</Text>
</Pressable>
</View>
{myAdsResult.status === "LoadingFirstPage" ? (
<Loading skeleton />
) : myAds.length === 0 ? (
<Card variant="flat" style={{ alignItems: "center", paddingVertical: theme.spacing.xl }}>
<View
style={{
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: theme.colors.border,
alignItems: "center",
justifyContent: "center",
marginBottom: theme.spacing.sm,
}}
>
<Ionicons name="document-text" size={24} color={theme.colors.textTertiary} />
</View>
<Text style={{ ...theme.typography.caption, color: theme.colors.textSecondary }}>
Сè уште немате огласи
</Text>
</Card>
) : (
<View style={{ gap: theme.spacing.sm }}>
{myAds.map((ad) => (
<AdCard key={ad._id} ad={ad} />
))}
</View>
)}
{myAdsResult.status === "CanLoadMore" && (
<Pressable
accessible
accessibilityLabel="Вчитај повеќе"
accessibilityRole="button"
onPress={() => myAdsResult.loadMore(20)}
style={{
backgroundColor: theme.colors.surface,
borderRadius: theme.radius.lg,
paddingVertical: 12,
alignItems: "center",
borderWidth: 1,
borderColor: theme.colors.border,
...theme.shadows.xs,
}}
>
<Text style={{ color: theme.colors.primary, fontWeight: "600", fontSize: 13 }}>
Вчитај повеќе
</Text>
</Pressable>
)}
</>
)}
{!isHandyman && (
<>
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
<Text style={{ ...theme.typography.h4, color: theme.colors.text }}>
Мои побарувања ({myPosts.length})
</Text>
<Pressable
accessible
accessibilityLabel="Креирај ново побарување"
accessibilityRole="button"
onPress={() => router.push("/post/create" as any)}
style={{ flexDirection: "row", alignItems: "center", gap: 4 }}
>
<Ionicons name="add-circle" size={20} color={theme.colors.primary} />
<Text style={{ ...theme.typography.bodyBold, color: theme.colors.primary, fontSize: 13 }}>
Ново
</Text>
</Pressable>
</View>
{myPostsResult.status === "LoadingFirstPage" ? (
<Loading skeleton />
) : myPosts.length === 0 ? (
<Card variant="flat" style={{ alignItems: "center", paddingVertical: theme.spacing.xl }}>
<View
style={{
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: theme.colors.border,
alignItems: "center",
justifyContent: "center",
marginBottom: theme.spacing.sm,
}}
>
<Ionicons name="document-text" size={24} color={theme.colors.textTertiary} />
</View>
<Text style={{ ...theme.typography.caption, color: theme.colors.textSecondary }}>
Сè уште немате побарувања
</Text>
</Card>
) : (
<View style={{ gap: theme.spacing.sm }}>
{myPosts.map((post) => (
<PostCard key={post._id} post={post} />
))}
</View>
)}
{myPostsResult.status === "CanLoadMore" && (
<Pressable
accessible
accessibilityLabel="Вчитај повеќе"
accessibilityRole="button"
onPress={() => myPostsResult.loadMore(20)}
style={{
backgroundColor: theme.colors.surface,
borderRadius: theme.radius.lg,
paddingVertical: 12,
alignItems: "center",
borderWidth: 1,
borderColor: theme.colors.border,
...theme.shadows.xs,
}}
>
<Text style={{ color: theme.colors.primary, fontWeight: "600", fontSize: 13 }}>
Вчитај повеќе
</Text>
</Pressable>
)}
</>
)}
<View style={{ marginTop: theme.spacing.sm }}>
<Card variant="flat" style={{ padding: 0, overflow: "hidden" }}>
<ThemeToggleRow />
</Card>
</View>
<Button variant="destructive" onPress={handleLogout} fullWidth>
<Ionicons name="log-out" size={18} color={theme.colors.textInverse} style={{ marginRight: 6 }} />
Одјави се
</Button>
</ScrollView>
</View>
);
}