finsh user,car,driver pages also add validator layer to app build trip page
This commit is contained in:
217
hooks/useCars.ts
Normal file
217
hooks/useCars.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
"use client";
|
||||
// hooks/useCars.ts
|
||||
// All custom hooks for the Car module.
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { getStoredToken } from "../lib/auth";
|
||||
import { carService } from "../services/car.service";
|
||||
import { get } from "../services/api";
|
||||
import type {
|
||||
Car,
|
||||
CarImage,
|
||||
CarListResponse,
|
||||
CreateCarPayload,
|
||||
ImageStage,
|
||||
ToastMsg,
|
||||
UpdateCarPayload,
|
||||
} from "../types/car";
|
||||
|
||||
// ── useCars ───────────────────────────────────────────────────────────────────
|
||||
// Manages the paginated car list for the main page.
|
||||
|
||||
export function useCars(page: number, search: string) {
|
||||
const [cars, setCars] = useState<Car[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [pages, setPages] = useState(1);
|
||||
|
||||
const loadCars = useCallback(() => {
|
||||
const token = getStoredToken();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const query = `?page=${page}&limit=12${search ? `&search=${encodeURIComponent(search)}` : ""}`;
|
||||
get<CarListResponse>(`cars${query}`, token)
|
||||
.then((res) => {
|
||||
const payload = (res as unknown as { data: CarListResponse["data"] }).data ?? res;
|
||||
setCars((payload as CarListResponse["data"]).data ?? []);
|
||||
setTotal((payload as CarListResponse["data"]).meta?.total ?? 0);
|
||||
setPages((payload as CarListResponse["data"]).meta?.pages ?? 1);
|
||||
})
|
||||
.catch((err: Error) => setError(err.message))
|
||||
.finally(() => setLoading(false));
|
||||
}, [page, search]);
|
||||
|
||||
useEffect(() => { loadCars(); }, [loadCars]);
|
||||
|
||||
// Optimistic removal after delete
|
||||
const removeCar = useCallback((id: string) => {
|
||||
setCars(prev => prev.filter(c => c.id !== id));
|
||||
setTotal(prev => Math.max(0, prev - 1));
|
||||
}, []);
|
||||
|
||||
return { cars, loading, error, total, pages, loadCars, removeCar, setError };
|
||||
}
|
||||
|
||||
// ── useCarDetail ──────────────────────────────────────────────────────────────
|
||||
// Fetches a single car by ID — used in CarDetailPanel.
|
||||
|
||||
export function useCarDetail(carId: string) {
|
||||
const [car, setCar] = useState<Car | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const token = getStoredToken();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
carService
|
||||
.getById(carId, token)
|
||||
.then(res => setCar((res as unknown as { data: Car }).data))
|
||||
.catch((err: Error) => setError(err.message))
|
||||
.finally(() => setLoading(false));
|
||||
}, [carId]);
|
||||
|
||||
return { car, loading, error };
|
||||
}
|
||||
|
||||
// ── useCarMutations ───────────────────────────────────────────────────────────
|
||||
// Create, update, and delete operations — used in the main page.
|
||||
|
||||
interface UseCarMutationsOptions {
|
||||
onSuccess: (msg: string) => void;
|
||||
onError: (msg: string) => void;
|
||||
onDeleted: (id: string) => void;
|
||||
getEditTarget: () => Car | null;
|
||||
}
|
||||
|
||||
export function useCarMutations({
|
||||
onSuccess,
|
||||
onError,
|
||||
onDeleted,
|
||||
getEditTarget,
|
||||
}: UseCarMutationsOptions) {
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const handleFormSubmit = useCallback(async (
|
||||
payload: CreateCarPayload | UpdateCarPayload,
|
||||
isNew: boolean,
|
||||
): Promise<boolean> => {
|
||||
const token = getStoredToken();
|
||||
try {
|
||||
if (isNew) {
|
||||
await carService.create(payload as CreateCarPayload, token);
|
||||
onSuccess("تم إضافة المركبة بنجاح.");
|
||||
} else {
|
||||
const editTarget = getEditTarget();
|
||||
if (!editTarget) return false;
|
||||
await carService.update(editTarget.id, payload as UpdateCarPayload, token);
|
||||
onSuccess("تم تحديث بيانات المركبة.");
|
||||
}
|
||||
return true;
|
||||
} catch (err: unknown) {
|
||||
onError(err instanceof Error ? err.message : "فشلت العملية.");
|
||||
return false;
|
||||
}
|
||||
}, [onSuccess, onError, getEditTarget]);
|
||||
|
||||
const handleDeleteConfirm = useCallback(async (target: Car) => {
|
||||
setDeleting(true);
|
||||
const token = getStoredToken();
|
||||
try {
|
||||
await carService.delete(target.id, token);
|
||||
onDeleted(target.id);
|
||||
onSuccess(`تم حذف ${target.manufacturer} ${target.model} بنجاح.`);
|
||||
} catch (err: unknown) {
|
||||
onError(err instanceof Error ? err.message : "فشل الحذف.");
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
}, [onSuccess, onError, onDeleted]);
|
||||
|
||||
return { deleting, handleFormSubmit, handleDeleteConfirm };
|
||||
}
|
||||
|
||||
// ── useToast ──────────────────────────────────────────────────────────────────
|
||||
// Simple toast notification state.
|
||||
|
||||
export function useToast(duration = 3500) {
|
||||
const [toast, setToast] = useState<ToastMsg | null>(null);
|
||||
|
||||
const notify = useCallback((t: ToastMsg) => {
|
||||
setToast(t);
|
||||
setTimeout(() => setToast(null), duration);
|
||||
}, [duration]);
|
||||
|
||||
return { toast, notify };
|
||||
}
|
||||
|
||||
// ── useCarImages ──────────────────────────────────────────────────────────────
|
||||
// Manages gallery images — used in CarImageGallery.
|
||||
|
||||
export function useCarImages(carId: string, sortBy: "asc" | "desc") {
|
||||
const [images, setImages] = useState<CarImage[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [deleting, setDeleting] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchImages = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
const res = await carService.getImages(carId, token, { sortBy });
|
||||
const raw = (res as unknown as { data: CarImage[] }).data ?? [];
|
||||
setImages(raw);
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : "تعذّر تحميل الصور");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [carId, sortBy]);
|
||||
|
||||
useEffect(() => { fetchImages(); }, [fetchImages]);
|
||||
|
||||
const uploadImages = useCallback(async (files: File[], stage: ImageStage) => {
|
||||
setUploading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
await carService.uploadImages(carId, files, stage, token);
|
||||
await fetchImages();
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : "فشل رفع الصور");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}, [carId, fetchImages]);
|
||||
|
||||
const deleteImage = useCallback(async (imageId: string) => {
|
||||
setDeleting(imageId);
|
||||
setError(null);
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
await carService.deleteImage(imageId, token);
|
||||
setImages(prev => prev.filter(img => img.id !== imageId));
|
||||
return true;
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : "فشل حذف الصورة");
|
||||
return false;
|
||||
} finally {
|
||||
setDeleting(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
images,
|
||||
loading,
|
||||
uploading,
|
||||
deleting,
|
||||
error,
|
||||
setError,
|
||||
fetchImages,
|
||||
uploadImages,
|
||||
deleteImage,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user