307 lines
9.4 KiB
TypeScript
307 lines
9.4 KiB
TypeScript
/**
|
|
* useItems Hook
|
|
* Manages item data fetching and mutations for the items management interface.
|
|
*/
|
|
|
|
import { useState, useCallback, useEffect } from "react";
|
|
import { toast } from "sonner";
|
|
|
|
// Full item type matching the database schema
|
|
export interface ItemWithUsage {
|
|
id: number;
|
|
name: string;
|
|
description: string | null;
|
|
rarity: string | null;
|
|
type: string | null;
|
|
price: bigint | null;
|
|
iconUrl: string | null;
|
|
imageUrl: string | null;
|
|
usageData: {
|
|
consume: boolean;
|
|
effects: Array<{
|
|
type: string;
|
|
[key: string]: any;
|
|
}>;
|
|
} | null;
|
|
}
|
|
|
|
export interface ItemFilters {
|
|
search?: string;
|
|
type?: string;
|
|
rarity?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
}
|
|
|
|
export interface ItemsResponse {
|
|
items: ItemWithUsage[];
|
|
total: number;
|
|
}
|
|
|
|
export interface CreateItemData {
|
|
name: string;
|
|
description?: string | null;
|
|
rarity?: 'C' | 'R' | 'SR' | 'SSR';
|
|
type: 'MATERIAL' | 'CONSUMABLE' | 'EQUIPMENT' | 'QUEST';
|
|
price?: string | null;
|
|
iconUrl?: string;
|
|
imageUrl?: string;
|
|
usageData?: {
|
|
consume: boolean;
|
|
effects: Array<{ type: string;[key: string]: any }>;
|
|
} | null;
|
|
}
|
|
|
|
export interface UpdateItemData extends Partial<CreateItemData> { }
|
|
|
|
export function useItems(initialFilters: ItemFilters = {}) {
|
|
const [items, setItems] = useState<ItemWithUsage[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(true);
|
|
const [filters, setFilters] = useState<ItemFilters>(initialFilters);
|
|
|
|
const fetchItems = useCallback(async (newFilters?: ItemFilters) => {
|
|
setLoading(true);
|
|
try {
|
|
const params = new URLSearchParams();
|
|
const activeFilters = newFilters ?? filters;
|
|
|
|
if (activeFilters.search) params.set("search", activeFilters.search);
|
|
if (activeFilters.type) params.set("type", activeFilters.type);
|
|
if (activeFilters.rarity) params.set("rarity", activeFilters.rarity);
|
|
if (activeFilters.limit) params.set("limit", String(activeFilters.limit));
|
|
if (activeFilters.offset) params.set("offset", String(activeFilters.offset));
|
|
|
|
const response = await fetch(`/api/items?${params.toString()}`);
|
|
if (!response.ok) throw new Error("Failed to fetch items");
|
|
|
|
const data: ItemsResponse = await response.json();
|
|
setItems(data.items);
|
|
setTotal(data.total);
|
|
} catch (error) {
|
|
console.error("Error fetching items:", error);
|
|
toast.error("Failed to load items", {
|
|
description: error instanceof Error ? error.message : "Unknown error"
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [filters]);
|
|
|
|
useEffect(() => {
|
|
fetchItems();
|
|
}, [fetchItems]);
|
|
|
|
const updateFilters = useCallback((newFilters: Partial<ItemFilters>) => {
|
|
setFilters(prev => ({ ...prev, ...newFilters }));
|
|
}, []);
|
|
|
|
const clearFilters = useCallback(() => {
|
|
setFilters({});
|
|
}, []);
|
|
|
|
return {
|
|
items,
|
|
total,
|
|
loading,
|
|
filters,
|
|
fetchItems,
|
|
updateFilters,
|
|
clearFilters,
|
|
};
|
|
}
|
|
|
|
export function useItem(id: number | null) {
|
|
const [item, setItem] = useState<ItemWithUsage | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchItem = useCallback(async () => {
|
|
if (id === null) {
|
|
setItem(null);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await fetch(`/api/items/${id}`);
|
|
if (!response.ok) {
|
|
if (response.status === 404) {
|
|
throw new Error("Item not found");
|
|
}
|
|
throw new Error("Failed to fetch item");
|
|
}
|
|
|
|
const data = await response.json();
|
|
setItem(data);
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
setError(message);
|
|
toast.error("Failed to load item", { description: message });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [id]);
|
|
|
|
useEffect(() => {
|
|
fetchItem();
|
|
}, [fetchItem]);
|
|
|
|
return { item, loading, error, refetch: fetchItem };
|
|
}
|
|
|
|
export function useCreateItem() {
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const createItem = useCallback(async (data: CreateItemData, imageFile?: File): Promise<ItemWithUsage | null> => {
|
|
setLoading(true);
|
|
try {
|
|
let response: Response;
|
|
|
|
if (imageFile) {
|
|
// Multipart form with image
|
|
const formData = new FormData();
|
|
formData.append("data", JSON.stringify(data));
|
|
formData.append("image", imageFile);
|
|
|
|
response = await fetch("/api/items", {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
} else {
|
|
// JSON-only request
|
|
response = await fetch("/api/items", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.error || "Failed to create item");
|
|
}
|
|
|
|
const result = await response.json();
|
|
toast.success("Item created", {
|
|
description: `"${result.item.name}" has been created successfully.`
|
|
});
|
|
|
|
return result.item;
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
toast.error("Failed to create item", { description: message });
|
|
return null;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
return { createItem, loading };
|
|
}
|
|
|
|
export function useUpdateItem() {
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const updateItem = useCallback(async (id: number, data: UpdateItemData): Promise<ItemWithUsage | null> => {
|
|
setLoading(true);
|
|
try {
|
|
const response = await fetch(`/api/items/${id}`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.error || "Failed to update item");
|
|
}
|
|
|
|
const result = await response.json();
|
|
toast.success("Item updated", {
|
|
description: `"${result.item.name}" has been updated successfully.`
|
|
});
|
|
|
|
return result.item;
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
toast.error("Failed to update item", { description: message });
|
|
return null;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
return { updateItem, loading };
|
|
}
|
|
|
|
export function useDeleteItem() {
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const deleteItem = useCallback(async (id: number, name?: string): Promise<boolean> => {
|
|
setLoading(true);
|
|
try {
|
|
const response = await fetch(`/api/items/${id}`, {
|
|
method: "DELETE",
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.error || "Failed to delete item");
|
|
}
|
|
|
|
toast.success("Item deleted", {
|
|
description: name ? `"${name}" has been deleted.` : "Item has been deleted."
|
|
});
|
|
|
|
return true;
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
toast.error("Failed to delete item", { description: message });
|
|
return false;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
return { deleteItem, loading };
|
|
}
|
|
|
|
export function useUploadItemIcon() {
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const uploadIcon = useCallback(async (itemId: number, imageFile: File): Promise<ItemWithUsage | null> => {
|
|
setLoading(true);
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append("image", imageFile);
|
|
|
|
const response = await fetch(`/api/items/${itemId}/icon`, {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.error || "Failed to upload icon");
|
|
}
|
|
|
|
const result = await response.json();
|
|
toast.success("Icon uploaded", {
|
|
description: "Item icon has been updated successfully."
|
|
});
|
|
|
|
return result.item;
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
toast.error("Failed to upload icon", { description: message });
|
|
return null;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
return { uploadIcon, loading };
|
|
}
|