feat(item-card): add 'market'/'catalog' mode prop with defensive price checks

Introduces optional `mode` prop ("market"/"catalog") to conditionally render components like rarity badges. Adds defensive checks for missing price data and priceHistory properties to prevent runtime errors. New icons `Badge` and `Boxes` imported from lucide-react.
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-30 14:10:46 -03:00
1 parent 29ba5aece0
commit 44d7f8ce81
21 files changed
+1181 -714

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
import { NextRequest } from "next/server";
import fs from "node:fs";
import path from "node:path";
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const relPath = searchParams.get("path") || "";
// Permitir apenas caminhos sob data/images para segurança
const baseDir = path.join(process.cwd(), "data", "images");
const resolved = path.join(process.cwd(), relPath);
if (!resolved.startsWith(baseDir)) {
return new Response("Forbidden", { status: 403 });
}
try {
const file = await fs.promises.readFile(resolved);
const ext = path.extname(resolved).toLowerCase();
const contentType =
ext === ".gif"
? "image/gif"
: ext === ".png"
? "image/png"
: ext === ".webp"
? "image/webp"
: ext === ".jpg" || ext === ".jpeg"
? "image/jpeg"
: "application/octet-stream";
return new Response(file, { status: 200, headers: { "Content-Type": contentType } });
} catch {
return new Response("Not Found", { status: 404 });
}
}
+25
View File
@@ -0,0 +1,25 @@
import { NextRequest } from "next/server";
import path from "node:path";
function getDb() {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require("better-sqlite3");
const dbPath = path.join(process.cwd(), "data", "tibiawiki.db");
return new Database(dbPath, { readonly: true, fileMustExist: true });
}
export async function GET(_req: NextRequest) {
try {
const db = getDb();
const tables = db
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name <> 'sqlite_sequence' ORDER BY name")
.all() as { name: string }[];
db.close();
// Retorna nomes das tabelas (PT) para usar como categorias
return Response.json({ categories: tables.map((t) => t.name) });
} catch (e: any) {
return Response.json({ error: e?.message || "Erro" }, { status: 500 });
}
}
+69
View File
@@ -0,0 +1,69 @@
import { NextRequest } from "next/server";
import { searchItems } from "@/lib/items-db";
import { buildPublicItemImageUrl } from "@/lib/item-images";
function parseArray(param: string | null): string[] {
if (!param) return [];
// Suporta formatos: comma-separated (a,b) ou múltiplos parâmetros já combinados
return param
.split(",")
.map((v) => v.trim())
.filter(Boolean);
}
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const q = searchParams.get("q") || undefined;
const categories = parseArray(searchParams.get("category"));
const rarities = parseArray(searchParams.get("rarity"));
const sortParam = (searchParams.get("sort") || "name") as
| "name"
| "price-asc"
| "price-desc"
| "rarity"
| "recent";
const page = Number(searchParams.get("page") || "1");
const pageSize = Number(searchParams.get("pageSize") || "24");
const { items, total } = searchItems({
q,
categories,
rarities,
sort: sortParam,
page,
pageSize,
});
// Enriquecer com URL pública de imagem quando possível
const itemsWithImage = items.map((it: any) => {
const img = (it.image || "").toString();
// Preferir URL absoluta do banco para teste
if (/^https?:\/\//i.test(img)) {
return { ...it, image: img };
}
if (img.startsWith("data/")) {
return { ...it, image: `/api/images?path=${encodeURIComponent(img)}` };
}
const categoryEn = (it.category || "other").toString();
return { ...it, image: buildPublicItemImageUrl(categoryEn, img) };
});
const totalPages = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
return Response.json(
{ items: itemsWithImage, page, pageSize, total, totalPages },
{ status: 200 }
);
} catch (error: any) {
console.error("[api/items] Erro:", error?.message, error?.stack);
return Response.json(
{
error: "Falha ao consultar itens",
message: error?.message || "Erro desconhecido",
},
{ status: 500 }
);
}
}