feat: implement comprehensive item management system with admin UI, API, and asset handling utilities.
All checks were successful
Deploy to Production / test (push) Successful in 44s
All checks were successful
Deploy to Production / test (push) Successful in 44s
This commit is contained in:
306
web/src/hooks/use-items.ts
Normal file
306
web/src/hooks/use-items.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* 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?: 'Common' | 'Uncommon' | 'Rare' | 'Epic' | 'Legendary';
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user