- Error boundary with Macedonian fallback screen and retry button wrapping all navigation - Pull-to-refresh on all list/detail screens (home, explore, posts, chat, profile, ad detail, post detail) - Accessibility: labels on all interactive elements, roles on Pressable/Button, selectable text for user data - Deep linking: mojmajstor:// scheme configured in app.json, ad/[id] and chat/[id] routes work via Expo Router auto-routing - Pagination: cursor-based paginated queries for ads, posts, messages, chats, reviews with Load More buttons on frontend - Profile: shows customer posts with PostCard, loading/error states, create-post navigation - Auth error messages in Macedonian (Невалидни акредитиви, итн.) All UI text in Macedonian. TypeScript clean, Convex functions deployed.
160 lines
6.2 KiB
TypeScript
160 lines
6.2 KiB
TypeScript
import { View, Text, ScrollView, Alert } from "react-native";
|
||
import { Stack, useRouter } from "expo-router";
|
||
import { useState } from "react";
|
||
import { 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 { Input } from "../../components/ui/input";
|
||
import { Card } from "../../components/ui/card";
|
||
import { CATEGORIES } from "../../lib/constants";
|
||
|
||
export default function CreatePostScreen() {
|
||
const router = useRouter();
|
||
const theme = useTheme();
|
||
const { token, isAuthenticated } = useAuth();
|
||
|
||
const [title, setTitle] = useState("");
|
||
const [description, setDescription] = useState("");
|
||
const [category, setCategory] = useState<string | null>(null);
|
||
const [location, setLocation] = useState("");
|
||
const [budget, setBudget] = useState("");
|
||
const [submitting, setSubmitting] = useState(false);
|
||
|
||
const createPost = useMutation(api.posts.create);
|
||
|
||
if (!isAuthenticated) {
|
||
return (
|
||
<>
|
||
<Stack.Screen options={{ title: "Ново побарување", headerShown: true, headerBackButtonDisplayMode: "minimal" }} />
|
||
<View style={{ flex: 1, justifyContent: "center", alignItems: "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" as any)} accessibilityLabel="Најави се">Најави се</Button>
|
||
</View>
|
||
</>
|
||
);
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
if (!title.trim()) {
|
||
Alert.alert("Грешка", "Внесете наслов");
|
||
return;
|
||
}
|
||
if (!description.trim()) {
|
||
Alert.alert("Грешка", "Внесете опис");
|
||
return;
|
||
}
|
||
|
||
setSubmitting(true);
|
||
try {
|
||
const postId = await createPost({
|
||
token: token!,
|
||
title: title.trim(),
|
||
description: description.trim(),
|
||
category: category ?? undefined,
|
||
location: location.trim() || undefined,
|
||
budget: budget.trim() || undefined,
|
||
});
|
||
router.replace(`/post/${postId}` as any);
|
||
} catch (e: any) {
|
||
Alert.alert("Грешка", e.message || "Неуспешно креирање");
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<Stack.Screen options={{ title: "Ново побарување", headerShown: true, headerBackButtonDisplayMode: "minimal" }} />
|
||
<ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.md }}>
|
||
<View style={{ gap: theme.spacing.xs }}>
|
||
<Text style={{ fontSize: 14, fontWeight: "600", color: theme.colors.text }}>Наслов *</Text>
|
||
<Input
|
||
placeholder="Што ви треба?"
|
||
value={title}
|
||
onChangeText={setTitle}
|
||
maxLength={100}
|
||
accessibilityLabel="Наслов на побарување"
|
||
/>
|
||
</View>
|
||
|
||
<View style={{ gap: theme.spacing.xs }}>
|
||
<Text style={{ fontSize: 14, fontWeight: "600", color: theme.colors.text }}>Опис *</Text>
|
||
<Input
|
||
placeholder="Опишете го проблемот детално..."
|
||
value={description}
|
||
onChangeText={setDescription}
|
||
multiline
|
||
numberOfLines={4}
|
||
style={{ minHeight: 100, textAlignVertical: "top" }}
|
||
maxLength={2000}
|
||
accessibilityLabel="Опис на побарување"
|
||
/>
|
||
</View>
|
||
|
||
<View style={{ gap: theme.spacing.xs }}>
|
||
<Text style={{ fontSize: 14, fontWeight: "600", color: theme.colors.text }}>Категорија</Text>
|
||
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: theme.spacing.xs }}>
|
||
{CATEGORIES.map((cat) => (
|
||
<View
|
||
key={cat.slug}
|
||
style={{
|
||
backgroundColor: category === cat.slug ? theme.colors.primary : theme.colors.surface,
|
||
borderRadius: theme.radius.md,
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 8,
|
||
borderWidth: 1,
|
||
borderColor: category === cat.slug ? theme.colors.primary : theme.colors.border,
|
||
}}
|
||
>
|
||
<Text
|
||
style={{
|
||
color: category === cat.slug ? "#FFFFFF" : theme.colors.text,
|
||
fontSize: 13,
|
||
fontWeight: "500",
|
||
}}
|
||
onPress={() => setCategory(category === cat.slug ? null : cat.slug)}
|
||
>
|
||
{cat.name}
|
||
</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
|
||
<View style={{ gap: theme.spacing.xs }}>
|
||
<Text style={{ fontSize: 14, fontWeight: "600", color: theme.colors.text }}>Локација</Text>
|
||
<Input
|
||
placeholder="нр. Скопје, Битола..."
|
||
value={location}
|
||
onChangeText={setLocation}
|
||
maxLength={100}
|
||
accessibilityLabel="Локација"
|
||
/>
|
||
</View>
|
||
|
||
<View style={{ gap: theme.spacing.xs }}>
|
||
<Text style={{ fontSize: 14, fontWeight: "600", color: theme.colors.text }}>Буџет</Text>
|
||
<Input
|
||
placeholder="нр. 5000 - 10000 денари"
|
||
value={budget}
|
||
onChangeText={setBudget}
|
||
maxLength={100}
|
||
accessibilityLabel="Буџет"
|
||
/>
|
||
</View>
|
||
|
||
<Button onPress={handleSubmit} loading={submitting} disabled={submitting} accessibilityLabel="Објави побарување">
|
||
Објави побарување
|
||
</Button>
|
||
</ScrollView>
|
||
</>
|
||
);
|
||
} |