420 lines
18 KiB
TypeScript
420 lines
18 KiB
TypeScript
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>
|
||
</>
|
||
);
|
||
}
|