mojmajstor/app/ad/create.tsx
2026-06-06 03:54:22 +02:00

420 lines
18 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, ScrollView, Pressable, Alert } from "react-native";
import { useState } from "react";
import { Stack, useRouter, useLocalSearchParams } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
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 { Input } from "../../components/ui/input";
import { Badge } from "../../components/ui/badge";
import { Loading } from "../../components/ui/loading";
import { Card } from "../../components/ui/card";
import { CATEGORIES, CATEGORY_EMOJI, getCategoryName } from "../../lib/constants";
const STEPS = ["Категорија", "Детали", "Локација", "Цена"];
export default function CreateAdScreen() {
const theme = useTheme();
const router = useRouter();
const { token, isAuthenticated, userId } = useAuth();
const params = useLocalSearchParams<{ editId?: string }>();
const isEditing = !!params.editId;
const existingAd = useQuery(
api.ads.getById,
isEditing ? { id: params.editId as any } : "skip"
);
const user = useQuery(
api.users.getCurrentUser,
isAuthenticated ? { token: token! } : "skip"
);
const createAd = useMutation(api.ads.create);
const updateAd = useMutation(api.ads.update);
const [category, setCategory] = useState<string>("");
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [location, setLocation] = useState("");
const [priceRange, setPriceRange] = useState("");
const [availability, setAvailability] = useState("");
const [submitting, setSubmitting] = useState(false);
const [step, setStep] = useState(0);
const isHandyman = user?.role === "handyman";
const canProceed = step === 0
? !!category
: step === 1
? title.trim().length >= 3 && description.trim().length >= 10
: step === 2
? location.trim().length > 0
: true;
if (!isAuthenticated) {
return (
<>
<Stack.Screen options={{ title: "Нов оглас", headerShown: true, headerBackButtonDisplayMode: "minimal" }} />
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: theme.colors.background, padding: theme.spacing.lg }}>
<View style={{ width: 88, height: 88, borderRadius: 44, backgroundColor: theme.colors.primaryLight, alignItems: "center", justifyContent: "center", marginBottom: 16, ...theme.shadows.sm }}>
<Ionicons name="lock-closed" size={38} color={theme.colors.primary} />
</View>
<Text style={{ ...theme.typography.h3, color: theme.colors.text, marginBottom: 8 }}>
Најава потребна
</Text>
<Text style={{ ...theme.typography.body, color: theme.colors.textSecondary, textAlign: "center", marginBottom: theme.spacing.lg }}>
Треба да бидете најавени за да креирате оглас.
</Text>
<Button onPress={() => router.push("/(auth)/login")} accessibilityLabel="Најави се">Најави се</Button>
</View>
</>
);
}
if (user && !isHandyman) {
return (
<>
<Stack.Screen options={{ title: "Нов оглас", headerShown: true, headerBackButtonDisplayMode: "minimal" }} />
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: theme.colors.background, padding: theme.spacing.lg }}>
<View style={{ width: 88, height: 88, borderRadius: 44, backgroundColor: theme.colors.surfaceAlt, alignItems: "center", justifyContent: "center", marginBottom: 16, ...theme.shadows.sm }}>
<Ionicons name="construct" size={38} color={theme.colors.textTertiary} />
</View>
<Text style={{ ...theme.typography.h3, color: theme.colors.text, marginBottom: 8 }}>
Само за мајстори
</Text>
<Text style={{ ...theme.typography.body, color: theme.colors.textSecondary, textAlign: "center" }}>
Само мајстори можат да креираат огласи.
</Text>
</View>
</>
);
}
if (user === undefined) {
return (
<>
<Stack.Screen options={{ title: "Нов оглас", headerShown: true, headerBackButtonDisplayMode: "minimal" }} />
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Loading />
</View>
</>
);
}
async function handleSubmit() {
if (submitting) return;
setSubmitting(true);
try {
if (isEditing && existingAd) {
await updateAd({
token: token!,
adId: existingAd._id as any,
title: title.trim(),
description: description.trim(),
category,
location: location.trim(),
priceRange: priceRange.trim() || undefined,
availability: availability.trim() || undefined,
});
} else {
const adId = await createAd({
token: token!,
title: title.trim(),
description: description.trim(),
category,
location: location.trim(),
priceRange: priceRange.trim() || undefined,
availability: availability.trim() || undefined,
});
router.replace(`/ad/${adId}`);
return;
}
router.back();
} catch (e: any) {
Alert.alert("Грешка", e.message || "Неуспешно зачувување");
} finally {
setSubmitting(false);
}
}
return (
<>
<Stack.Screen
options={{ title: isEditing ? "Уреди оглас" : "Нов оглас", headerShown: true, headerBackButtonDisplayMode: "minimal" }}
/>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.lg, paddingBottom: 100 }}
keyboardShouldPersistTaps="handled"
>
{/* Progress bar */}
<View style={{ flexDirection: "row", gap: theme.spacing.xs, marginBottom: theme.spacing.sm }}>
{STEPS.map((label, i) => (
<Pressable
key={i}
accessible
accessibilityLabel={`Чекор ${label}`}
accessibilityRole="button"
onPress={() => setStep(i)}
style={{
flex: 1,
height: 6,
borderRadius: 3,
backgroundColor: step >= i ? theme.colors.primary : theme.colors.borderLight,
}}
/>
))}
</View>
<View style={{ flexDirection: "row", gap: theme.spacing.xs, marginBottom: theme.spacing.sm }}>
{STEPS.map((label, i) => (
<Text
key={i}
style={{
flex: 1,
textAlign: "center",
fontSize: 12,
fontWeight: step === i ? "600" : "400",
color: step >= i ? theme.colors.primary : theme.colors.textTertiary,
}}
>
{label}
</Text>
))}
</View>
{/* Step 0: Category */}
{step === 0 && (
<View style={{ gap: theme.spacing.md }}>
<Text style={{ ...theme.typography.h3, color: theme.colors.text, letterSpacing: -0.3 }}>Изберете категорија</Text>
<View style={{ gap: theme.spacing.sm }}>
{CATEGORIES.map((cat) => (
<Pressable
key={cat.slug}
accessible
accessibilityLabel={cat.name}
accessibilityRole="button"
onPress={() => setCategory(cat.slug)}
style={{
flexDirection: "row",
alignItems: "center",
gap: theme.spacing.md,
padding: theme.spacing.lg,
borderRadius: theme.radius.md,
borderWidth: 2,
borderColor: category === cat.slug ? theme.colors.primary : theme.colors.border,
backgroundColor: category === cat.slug ? theme.colors.primaryLight : theme.colors.card,
}}
>
<View style={{
width: 48,
height: 48,
borderRadius: theme.radius.md,
backgroundColor: category === cat.slug ? theme.colors.primary + "18" : theme.colors.surfaceAlt,
alignItems: "center",
justifyContent: "center",
}}>
<Ionicons name={(cat.icon as any) ?? "construct-outline"} size={24} color={category === cat.slug ? theme.colors.primary : theme.colors.textSecondary} />
</View>
<Text style={{ fontSize: 16, fontWeight: "500", color: theme.colors.text, flex: 1 }}>
{cat.name}
</Text>
{category === cat.slug && (
<Ionicons name="checkmark-circle" size={24} color={theme.colors.primary} />
)}
</Pressable>
))}
</View>
</View>
)}
{/* Step 1: Title + Description */}
{step === 1 && (
<View style={{ gap: theme.spacing.md }}>
<Text style={{ ...theme.typography.h3, color: theme.colors.text, letterSpacing: -0.3 }}>Детали за огласот</Text>
<View>
<Text style={{ ...theme.typography.captionBold, color: theme.colors.text, marginBottom: theme.spacing.xs, fontSize: 13 }}>
Наслов *
</Text>
<Input
placeholder="Пр: Водоинсталација на бања"
value={title}
onChangeText={setTitle}
maxLength={100}
accessibilityLabel="Наслов на оглас"
/>
{title.length > 0 && title.length < 3 && (
<Text style={{ color: theme.colors.error, ...theme.typography.caption, marginTop: 4 }}>Насловот треба да има најмалку 3 знаци</Text>
)}
</View>
<View>
<Text style={{ ...theme.typography.captionBold, color: theme.colors.text, marginBottom: theme.spacing.xs, fontSize: 13 }}>
Опис *
</Text>
<Input
placeholder="Опишете ги вашите услуги..."
value={description}
onChangeText={setDescription}
multiline
numberOfLines={5}
style={{ minHeight: 140, textAlignVertical: "top" }}
maxLength={2000}
accessibilityLabel="Опис на оглас"
/>
{description.length > 0 && description.length < 10 && (
<Text style={{ color: theme.colors.error, ...theme.typography.caption, marginTop: 4 }}>Описот треба да има најмалку 10 знаци</Text>
)}
</View>
{category && <Badge label={getCategoryName(category)} variant="primary" />}
</View>
)}
{/* Step 2: Location */}
{step === 2 && (
<View style={{ gap: theme.spacing.md }}>
<Text style={{ ...theme.typography.h3, color: theme.colors.text, letterSpacing: -0.3 }}>Локација</Text>
<View>
<Text style={{ ...theme.typography.captionBold, color: theme.colors.text, marginBottom: theme.spacing.xs, fontSize: 13 }}>
Адреса / град *
</Text>
<Input
placeholder="Пр: Скопје, Чаир"
value={location}
onChangeText={setLocation}
accessibilityLabel="Локација"
/>
</View>
<View
style={{
backgroundColor: theme.colors.surface,
borderRadius: theme.radius.lg,
padding: theme.spacing.lg,
alignItems: "center",
justifyContent: "center",
borderWidth: 1.5,
borderColor: theme.colors.border,
borderStyle: "dashed",
height: 140,
}}
>
<Ionicons name="map-outline" size={36} color={theme.colors.textTertiary} />
<Text style={{ color: theme.colors.textTertiary, ...theme.typography.caption, marginTop: theme.spacing.sm }}>Избор на локација на мапа доаѓа наскоро</Text>
</View>
</View>
)}
{/* Step 3: Price + Availability */}
{step === 3 && (
<View style={{ gap: theme.spacing.md }}>
<Text style={{ ...theme.typography.h3, color: theme.colors.text, letterSpacing: -0.3 }}>Цена и расположивост</Text>
<View>
<Text style={{ ...theme.typography.captionBold, color: theme.colors.text, marginBottom: theme.spacing.xs, fontSize: 13 }}>
Ценовен опсег
</Text>
<Input
placeholder="Пр: 500-1500 ден/час"
value={priceRange}
onChangeText={setPriceRange}
accessibilityLabel="Ценовен опсег"
/>
</View>
<View>
<Text style={{ ...theme.typography.captionBold, color: theme.colors.text, marginBottom: theme.spacing.xs, fontSize: 13 }}>
Расположивост
</Text>
<Input
placeholder="Пр: Достапен Петоци - Недели"
value={availability}
onChangeText={setAvailability}
accessibilityLabel="Расположивост"
/>
</View>
<View>
<Text style={{ ...theme.typography.captionBold, color: theme.colors.text, marginBottom: theme.spacing.xs, fontSize: 13 }}>
Слики
</Text>
<View
style={{
backgroundColor: theme.colors.surface,
borderRadius: theme.radius.lg,
padding: theme.spacing.lg,
alignItems: "center",
justifyContent: "center",
borderWidth: 1.5,
borderColor: theme.colors.border,
borderStyle: "dashed",
height: 140,
}}
>
<Ionicons name="camera-outline" size={36} color={theme.colors.textTertiary} />
<Text style={{ color: theme.colors.textTertiary, ...theme.typography.caption, marginTop: theme.spacing.sm }}>Додавање слики доаѓа наскоро</Text>
</View>
</View>
{/* Summary */}
<Card variant="elevated" style={{ backgroundColor: theme.colors.secondaryLight }}>
<Text style={{ ...theme.typography.captionBold, color: theme.colors.text, marginBottom: theme.spacing.sm, fontSize: 13 }}>Преглед</Text>
<View style={{ gap: theme.spacing.sm }}>
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
<Text style={{ color: theme.colors.textSecondary, ...theme.typography.body }}>Категорија:</Text>
<Text style={{ color: theme.colors.text, ...theme.typography.bodyBold }}>{getCategoryName(category)}</Text>
</View>
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
<Text style={{ color: theme.colors.textSecondary, ...theme.typography.body }}>Наслов:</Text>
<Text style={{ color: theme.colors.text, ...theme.typography.bodyBold }} numberOfLines={1}>{title || "—"}</Text>
</View>
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
<Text style={{ color: theme.colors.textSecondary, ...theme.typography.body }}>Локација:</Text>
<Text style={{ color: theme.colors.text, ...theme.typography.bodyBold }}>{location || "—"}</Text>
</View>
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
<Text style={{ color: theme.colors.textSecondary, ...theme.typography.body }}>Цена:</Text>
<Text style={{ color: theme.colors.text, ...theme.typography.bodyBold }}>{priceRange || "—"}</Text>
</View>
</View>
</Card>
<Button
onPress={handleSubmit}
loading={submitting}
disabled={!title.trim() || !description.trim() || !category || !location.trim()}
size="lg"
fullWidth
>
{isEditing ? "Зачувај промени" : "Објави оглас"}
</Button>
</View>
)}
{/* Navigation buttons */}
{step < 3 && (
<View style={{ flexDirection: "row", gap: theme.spacing.sm }}>
{step > 0 && (
<Button variant="outline" onPress={() => setStep(step - 1)} style={{ flex: 1 }}>
<Ionicons name="arrow-back" size={18} color={theme.colors.primary} style={{ marginRight: 4 }} />
Назад
</Button>
)}
<Button
onPress={() => setStep(step + 1)}
disabled={!canProceed}
style={{ flex: 1 }}
>
Следно
<Ionicons name="arrow-forward" size={18} color={theme.colors.textInverse} style={{ marginLeft: 4 }} />
</Button>
</View>
)}
{step === 3 && step > 0 && (
<Button variant="outline" onPress={() => setStep(step - 1)}>
<Ionicons name="arrow-back" size={18} color={theme.colors.primary} style={{ marginRight: 4 }} />
Назад
</Button>
)}
</ScrollView>
</>
);
}