Waseel: driver app, dispatch, POI suggestions, map fixes
This commit is contained in:
+147
-115
@@ -17,7 +17,9 @@ import { CustomButton } from "@/components/custom-button";
|
||||
import { icons, images } from "@/constants";
|
||||
import { SERVICES, type ServiceId } from "@/constants/services";
|
||||
import { ApiError, fetchAPI } from "@/lib/fetch";
|
||||
import { useT } from "@/lib/i18n";
|
||||
import { useSession } from "@/lib/session";
|
||||
import { useTheme } from "@/lib/theme";
|
||||
import { useDriverLocation } from "@/lib/use-driver-location";
|
||||
import { formatTime } from "@/lib/utils";
|
||||
|
||||
@@ -71,6 +73,8 @@ type Dashboard = {
|
||||
|
||||
const DriverHome = () => {
|
||||
const { signOut, user } = useSession();
|
||||
const { isDark } = useTheme();
|
||||
const t = useT();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [profile, setProfile] = useState<Profile | null>(null);
|
||||
const [online, setOnline] = useState(false);
|
||||
@@ -136,7 +140,7 @@ const DriverHome = () => {
|
||||
if (!next) setDashboard(null);
|
||||
} catch (err) {
|
||||
console.log("[DRIVER_TOGGLE]: ", err);
|
||||
Alert.alert("Error", "Could not change your status. Please try again.");
|
||||
Alert.alert(t("driver.home.alertErrorTitle"), t("driver.home.alertToggleBody"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -154,10 +158,10 @@ const DriverHome = () => {
|
||||
} catch (err) {
|
||||
console.log("[DRIVER_RESPOND]: ", err);
|
||||
Alert.alert(
|
||||
"Error",
|
||||
t("driver.activeRide.alertErrorTitle"),
|
||||
action === "accept"
|
||||
? "Could not accept this ride. It may have been taken or expired."
|
||||
: "Could not decline this ride. Please try again.",
|
||||
? t("driver.activeRide.alertAcceptBody")
|
||||
: t("driver.activeRide.alertDeclineBody"),
|
||||
);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
@@ -175,7 +179,7 @@ const DriverHome = () => {
|
||||
await fetchDashboard();
|
||||
} catch (err) {
|
||||
console.log("[DRIVER_ADVANCE]: ", err);
|
||||
Alert.alert("Error", "Could not update the ride. Please try again.");
|
||||
Alert.alert(t("driver.activeRide.alertErrorTitle"), t("driver.activeRide.alertUpdateBody"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -183,8 +187,8 @@ const DriverHome = () => {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-white items-center justify-center">
|
||||
<ActivityIndicator size="large" color="#0286ff" />
|
||||
<SafeAreaView className="flex-1 bg-white dark:bg-neutral-950 items-center justify-center">
|
||||
<ActivityIndicator size="large" color={isDark ? "#0286ff" : "#0286ff"} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@@ -199,20 +203,20 @@ const DriverHome = () => {
|
||||
const rideCount = dashboard?.recent.length ?? 0;
|
||||
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-general-500">
|
||||
<SafeAreaView className="flex-1 bg-general-500 dark:bg-neutral-950">
|
||||
<ScrollView
|
||||
className="flex-1 px-5"
|
||||
contentContainerStyle={{ paddingBottom: 40 }}
|
||||
>
|
||||
<View className="flex-row items-center justify-between my-5">
|
||||
<Text className="text-2xl font-JakartaExtraBold">
|
||||
Driver mode
|
||||
<Text className="text-2xl font-JakartaExtraBold text-black dark:text-white">
|
||||
{t("driver.home.driverMode")}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={signOut}
|
||||
className="w-10 h-10 rounded-full bg-white items-center justify-center"
|
||||
className="w-10 h-10 rounded-full bg-white dark:bg-neutral-900 items-center justify-center"
|
||||
>
|
||||
<Image source={icons.out} className="w-4 h-4" alt="Sign out" />
|
||||
<Image source={icons.out} className="w-4 h-4" alt={t("driver.home.signOutAlt")} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@@ -221,29 +225,29 @@ const DriverHome = () => {
|
||||
onPress={toggleOnline}
|
||||
disabled={busy}
|
||||
className={`rounded-2xl p-5 items-center mb-4 ${
|
||||
online ? "bg-emerald-500" : "bg-neutral-700"
|
||||
online ? "bg-emerald-500" : "bg-neutral-700 dark:bg-neutral-800"
|
||||
}`}
|
||||
>
|
||||
<Text className="text-white text-lg font-JakartaBold">
|
||||
{online ? "● Online — receiving ride requests" : "○ Go online to drive"}
|
||||
{online ? t("driver.home.online") : t("driver.home.goOnline")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Earnings summary */}
|
||||
<View className="bg-white rounded-2xl p-4 mb-4 flex-row justify-between">
|
||||
<View className="bg-white dark:bg-neutral-900 rounded-2xl p-4 mb-4 flex-row justify-between">
|
||||
<View>
|
||||
<Text className="text-general-200 text-xs font-JakartaMedium">
|
||||
Today's earnings
|
||||
<Text className="text-general-200 dark:text-neutral-400 text-xs font-JakartaMedium">
|
||||
{t("driver.home.todaysEarnings")}
|
||||
</Text>
|
||||
<Text className="text-2xl font-JakartaExtraBold">
|
||||
<Text className="text-2xl font-JakartaExtraBold text-black dark:text-white">
|
||||
${(earnings / 100).toFixed(2)}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="items-end">
|
||||
<Text className="text-general-200 text-xs font-JakartaMedium">
|
||||
Completed today
|
||||
<Text className="text-general-200 dark:text-neutral-400 text-xs font-JakartaMedium">
|
||||
{t("driver.home.completedToday")}
|
||||
</Text>
|
||||
<Text className="text-2xl font-JakartaExtraBold">{rideCount}</Text>
|
||||
<Text className="text-2xl font-JakartaExtraBold text-black dark:text-white">{rideCount}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -257,8 +261,10 @@ const DriverHome = () => {
|
||||
) : null}
|
||||
|
||||
{/* Incoming offers */}
|
||||
<Text className="text-xl font-JakartaBold mt-4 mb-3">
|
||||
Incoming requests {online ? "" : "(offline)"}
|
||||
<Text className="text-xl font-JakartaBold mt-4 mb-3 text-black dark:text-white">
|
||||
{online
|
||||
? t("driver.home.incomingRequests")
|
||||
: t("driver.home.incomingRequestsOffline")}
|
||||
</Text>
|
||||
|
||||
{!online ? null : dashboard?.offers.length ? (
|
||||
@@ -272,10 +278,10 @@ const DriverHome = () => {
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<View className="bg-white rounded-2xl p-6 items-center">
|
||||
<View className="bg-white dark:bg-neutral-900 rounded-2xl p-6 items-center">
|
||||
<Image source={images.noResult} className="w-24 h-24" resizeMode="contain" />
|
||||
<Text className="text-general-200 mt-2">
|
||||
{online ? "Waiting for ride requests…" : "Go online to start driving."}
|
||||
<Text className="text-general-200 dark:text-neutral-400 mt-2">
|
||||
{online ? t("driver.home.waitingRequests") : t("driver.home.goOnlineStart")}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -295,6 +301,8 @@ const Onboarding = ({
|
||||
signOut: () => Promise<void>;
|
||||
userName?: string | null;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const { isDark } = useTheme();
|
||||
const [service, setService] = useState<ServiceId>("car");
|
||||
const [carModel, setCarModel] = useState("");
|
||||
const [carSeats, setCarSeats] = useState("4");
|
||||
@@ -303,7 +311,7 @@ const Onboarding = ({
|
||||
const submit = async () => {
|
||||
const seats = Number(carSeats);
|
||||
if (!Number.isInteger(seats) || seats < 1 || seats > 8) {
|
||||
Alert.alert("Invalid seats", "Car seats must be a whole number 1–8.");
|
||||
Alert.alert(t("driver.home.alertSeatsTitle"), t("driver.home.alertSeatsBody"));
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
@@ -320,33 +328,35 @@ const Onboarding = ({
|
||||
await onCreated();
|
||||
} catch (err) {
|
||||
console.log("[DRIVER_ONBOARD]: ", err);
|
||||
Alert.alert("Error", "Could not create your driver profile. Please try again.");
|
||||
Alert.alert(t("driver.home.alertErrorTitle"), t("driver.home.alertCreateBody"));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-white">
|
||||
<SafeAreaView className="flex-1 bg-white dark:bg-neutral-950">
|
||||
<ScrollView className="flex-1 px-5" contentContainerStyle={{ paddingBottom: 40 }}>
|
||||
<View className="flex-row items-center justify-between my-5">
|
||||
<Text className="text-2xl font-JakartaExtraBold">
|
||||
Welcome, {userName?.split(" ")[0] || "driver"}
|
||||
<Text className="text-2xl font-JakartaExtraBold text-black dark:text-white">
|
||||
{t("driver.home.welcome", {
|
||||
name: userName?.split(" ")[0] || t("driver.home.welcomeFallback"),
|
||||
})}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={signOut}
|
||||
className="w-10 h-10 rounded-full bg-neutral-100 items-center justify-center"
|
||||
className="w-10 h-10 rounded-full bg-neutral-100 dark:bg-neutral-900 items-center justify-center"
|
||||
>
|
||||
<Image source={icons.out} className="w-4 h-4" alt="Sign out" />
|
||||
<Image source={icons.out} className="w-4 h-4" alt={t("driver.home.signOutAlt")} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Text className="text-base text-general-200 font-Jakarta mb-4">
|
||||
Set up your driver profile to start receiving ride requests.
|
||||
<Text className="text-base text-general-200 dark:text-neutral-400 font-Jakarta mb-4">
|
||||
{t("driver.home.setupIntro")}
|
||||
</Text>
|
||||
|
||||
<Text className="text-lg font-JakartaSemiBold mb-3">
|
||||
What will you drive?
|
||||
<Text className="text-lg font-JakartaSemiBold mb-3 text-black dark:text-white">
|
||||
{t("driver.home.whatDrive")}
|
||||
</Text>
|
||||
<View className="flex-row gap-2 mb-5">
|
||||
{SERVICES.map((item) => {
|
||||
@@ -358,46 +368,52 @@ const Onboarding = ({
|
||||
className={`flex-1 items-center rounded-2xl border py-3 ${
|
||||
active
|
||||
? "border-primary-500 bg-primary-500/10"
|
||||
: "border-neutral-100 bg-neutral-100"
|
||||
: "border-neutral-100 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-900"
|
||||
}`}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={item.icon}
|
||||
size={24}
|
||||
color={active ? "#0286ff" : "#858585"}
|
||||
color={active ? "#0286ff" : isDark ? "#9ca3af" : "#858585"}
|
||||
/>
|
||||
<Text
|
||||
className={`mt-1.5 text-xs font-JakartaBold ${
|
||||
active ? "text-primary-500" : "text-black"
|
||||
active ? "text-primary-500" : "text-black dark:text-white"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
{t(item.labelKey)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
<Text className="text-lg font-JakartaSemiBold mb-3">Car model</Text>
|
||||
<Text className="text-lg font-JakartaSemiBold mb-3 text-black dark:text-white">
|
||||
{t("driver.home.carModel")}
|
||||
</Text>
|
||||
<TextInput
|
||||
value={carModel}
|
||||
onChangeText={setCarModel}
|
||||
placeholder="e.g. Toyota Camry"
|
||||
className="bg-neutral-100 rounded-full px-4 py-4 font-JakartaSemiBold text-[15px] mb-4"
|
||||
placeholder={t("driver.home.carModelPlaceholder")}
|
||||
placeholderTextColor={isDark ? "#737373" : "#858585"}
|
||||
className="bg-neutral-100 dark:bg-neutral-900 text-black dark:text-white rounded-full px-4 py-4 font-JakartaSemiBold text-[15px] mb-4"
|
||||
autoCapitalize="words"
|
||||
/>
|
||||
|
||||
<Text className="text-lg font-JakartaSemiBold mb-3">Car seats</Text>
|
||||
<Text className="text-lg font-JakartaSemiBold mb-3 text-black dark:text-white">
|
||||
{t("driver.home.carSeats")}
|
||||
</Text>
|
||||
<TextInput
|
||||
value={carSeats}
|
||||
onChangeText={setCarSeats}
|
||||
placeholder="4"
|
||||
placeholder={t("driver.home.carSeatsPlaceholder")}
|
||||
placeholderTextColor={isDark ? "#737373" : "#858585"}
|
||||
keyboardType="number-pad"
|
||||
className="bg-neutral-100 rounded-full px-4 py-4 font-JakartaSemiBold text-[15px] mb-8"
|
||||
className="bg-neutral-100 dark:bg-neutral-900 text-black dark:text-white rounded-full px-4 py-4 font-JakartaSemiBold text-[15px] mb-8"
|
||||
/>
|
||||
|
||||
<CustomButton
|
||||
title={submitting ? "Saving…" : "Start driving"}
|
||||
title={submitting ? t("common.saving") : t("driver.home.startDriving")}
|
||||
onPress={submit}
|
||||
disabled={submitting}
|
||||
/>
|
||||
@@ -418,63 +434,74 @@ const OfferCard = ({
|
||||
busy: boolean;
|
||||
onAccept: () => void;
|
||||
onDecline: () => void;
|
||||
}) => (
|
||||
<View className="bg-white rounded-2xl p-4 mb-3">
|
||||
<View className="flex-row items-center justify-between mb-2">
|
||||
<Text className="text-sm font-JakartaBold text-primary-500">
|
||||
New request · {offer.service}
|
||||
</Text>
|
||||
<Text className="text-xs text-general-200">
|
||||
{offer.payment_status === "cash" ? "💵 Cash" : "💳 Card"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex-row items-center gap-x-2 mb-1">
|
||||
<Image source={icons.to} alt="From" className="w-4 h-4" />
|
||||
<Text className="font-JakartaMedium" numberOfLines={1}>
|
||||
{offer.origin_address}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center gap-x-2 mb-3">
|
||||
<Image source={icons.point} alt="To" className="w-4 h-4" />
|
||||
<Text className="font-JakartaMedium" numberOfLines={1}>
|
||||
{offer.destination_address}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex-row justify-between mb-3">
|
||||
<Text className="text-general-200 text-xs">Trip time</Text>
|
||||
<Text className="font-JakartaMedium text-xs">
|
||||
{formatTime(offer.ride_time)}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-row justify-between mb-3">
|
||||
<Text className="text-general-200 text-xs">Fare</Text>
|
||||
<Text className="font-JakartaMedium text-xs text-emerald-600">
|
||||
${(offer.fare_price / 100).toFixed(2)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex-row gap-3">
|
||||
<TouchableOpacity
|
||||
onPress={onDecline}
|
||||
disabled={busy}
|
||||
className="flex-1 rounded-full py-3 bg-neutral-200 items-center"
|
||||
>
|
||||
<Text className="font-JakartaBold text-neutral-700">Decline</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={onAccept}
|
||||
disabled={busy}
|
||||
className="flex-1 rounded-full py-3 bg-emerald-500 items-center"
|
||||
>
|
||||
<Text className="font-JakartaBold text-white">
|
||||
{busy ? "…" : "Accept"}
|
||||
}) => {
|
||||
const t = useT();
|
||||
return (
|
||||
<View className="bg-white dark:bg-neutral-900 rounded-2xl p-4 mb-3">
|
||||
<View className="flex-row items-center justify-between mb-2">
|
||||
<Text className="text-sm font-JakartaBold text-primary-500">
|
||||
{t("driver.offerCard.newRequest", { service: offer.service })}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Text className="text-xs text-general-200 dark:text-neutral-400">
|
||||
{offer.payment_status === "cash"
|
||||
? t("driver.offerCard.cash")
|
||||
: t("driver.offerCard.card")}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex-row items-center gap-x-2 mb-1">
|
||||
<Image source={icons.to} alt={t("driver.offerCard.fromAlt")} className="w-4 h-4" />
|
||||
<Text className="font-JakartaMedium text-black dark:text-white" numberOfLines={1}>
|
||||
{offer.origin_address}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center gap-x-2 mb-3">
|
||||
<Image source={icons.point} alt={t("driver.offerCard.toAlt")} className="w-4 h-4" />
|
||||
<Text className="font-JakartaMedium text-black dark:text-white" numberOfLines={1}>
|
||||
{offer.destination_address}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex-row justify-between mb-3">
|
||||
<Text className="text-general-200 dark:text-neutral-400 text-xs">
|
||||
{t("driver.offerCard.tripTime")}
|
||||
</Text>
|
||||
<Text className="font-JakartaMedium text-xs text-black dark:text-white">
|
||||
{formatTime(offer.ride_time)}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-row justify-between mb-3">
|
||||
<Text className="text-general-200 dark:text-neutral-400 text-xs">
|
||||
{t("driver.offerCard.fare")}
|
||||
</Text>
|
||||
<Text className="font-JakartaMedium text-xs text-emerald-600 dark:text-emerald-400">
|
||||
${(offer.fare_price / 100).toFixed(2)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex-row gap-3">
|
||||
<TouchableOpacity
|
||||
onPress={onDecline}
|
||||
disabled={busy}
|
||||
className="flex-1 rounded-full py-3 bg-neutral-200 dark:bg-neutral-800 items-center"
|
||||
>
|
||||
<Text className="font-JakartaBold text-neutral-700 dark:text-neutral-200">
|
||||
{t("driver.offerCard.decline")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={onAccept}
|
||||
disabled={busy}
|
||||
className="flex-1 rounded-full py-3 bg-emerald-500 items-center"
|
||||
>
|
||||
<Text className="font-JakartaBold text-white">
|
||||
{busy ? "…" : t("driver.offerCard.accept")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
// --- Active ride card -----------------------------------------------------
|
||||
|
||||
@@ -487,11 +514,12 @@ const ActiveRideCard = ({
|
||||
busy: boolean;
|
||||
onAdvance: (rideId: number, status: "en_route" | "completed") => void;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const statusLabel =
|
||||
ride.status === "accepted"
|
||||
? "Head to pickup"
|
||||
? t("driver.activeRide.headToPickup")
|
||||
: ride.status === "en_route"
|
||||
? "Trip in progress"
|
||||
? t("driver.activeRide.tripInProgress")
|
||||
: ride.status;
|
||||
|
||||
return (
|
||||
@@ -500,36 +528,40 @@ const ActiveRideCard = ({
|
||||
<Text className="text-sm font-JakartaBold text-primary-500">
|
||||
● {statusLabel}
|
||||
</Text>
|
||||
<Text className="text-xs text-general-200">{ride.service}</Text>
|
||||
<Text className="text-xs text-general-200 dark:text-neutral-400">{ride.service}</Text>
|
||||
</View>
|
||||
|
||||
{ride.rider_name ? (
|
||||
<Text className="font-JakartaBold mb-2">{ride.rider_name}</Text>
|
||||
<Text className="font-JakartaBold mb-2 text-black dark:text-white">
|
||||
{t("driver.activeRide.rider", { name: ride.rider_name })}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<View className="flex-row items-center gap-x-2 mb-1">
|
||||
<Image source={icons.to} alt="From" className="w-4 h-4" />
|
||||
<Text className="font-JakartaMedium" numberOfLines={1}>
|
||||
<Image source={icons.to} alt={t("driver.activeRide.fromAlt")} className="w-4 h-4" />
|
||||
<Text className="font-JakartaMedium text-black dark:text-white" numberOfLines={1}>
|
||||
{ride.origin_address}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center gap-x-2 mb-3">
|
||||
<Image source={icons.point} alt="To" className="w-4 h-4" />
|
||||
<Text className="font-JakartaMedium" numberOfLines={1}>
|
||||
<Image source={icons.point} alt={t("driver.activeRide.toAlt")} className="w-4 h-4" />
|
||||
<Text className="font-JakartaMedium text-black dark:text-white" numberOfLines={1}>
|
||||
{ride.destination_address}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex-row justify-between mb-4">
|
||||
<Text className="text-general-200 text-xs">Fare</Text>
|
||||
<Text className="font-JakartaMedium text-xs text-emerald-600">
|
||||
<Text className="text-general-200 dark:text-neutral-400 text-xs">
|
||||
{t("driver.activeRide.fare")}
|
||||
</Text>
|
||||
<Text className="font-JakartaMedium text-xs text-emerald-600 dark:text-emerald-400">
|
||||
${(ride.fare_price / 100).toFixed(2)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{ride.status === "accepted" ? (
|
||||
<CustomButton
|
||||
title={busy ? "…" : "Start trip"}
|
||||
title={busy ? "…" : t("driver.activeRide.startTrip")}
|
||||
bgVariant="success"
|
||||
onPress={() => onAdvance(ride.ride_id, "en_route")}
|
||||
className="mb-2"
|
||||
@@ -537,7 +569,7 @@ const ActiveRideCard = ({
|
||||
) : null}
|
||||
{ride.status === "en_route" ? (
|
||||
<CustomButton
|
||||
title={busy ? "…" : "Complete trip"}
|
||||
title={busy ? "…" : t("driver.activeRide.completeTrip")}
|
||||
bgVariant="success"
|
||||
onPress={() => onAdvance(ride.ride_id, "completed")}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user