This commit is contained in:
gpatruno
2026-07-18 17:40:39 +02:00
parent 2168998679
commit d0f868ffa5
19 changed files with 406 additions and 28 deletions
+2 -2
View File
@@ -7,7 +7,7 @@ export const ACHIEVEMENT_DEFS = [
{
key: 'welcome',
name: 'Welcome Aboard',
description: 'Create your CaseForge account.',
description: 'Create your CaseOrion account.',
icon: '★',
sortOrder: 1,
},
@@ -70,7 +70,7 @@ export const ACHIEVEMENT_DEFS = [
{
key: 'playtime_hour',
name: 'Hourglass',
description: 'Spend 1 hour playing on CaseForge.',
description: 'Spend 1 hour playing on CaseOrion.',
icon: '⌛',
sortOrder: 10,
},
+54 -1
View File
@@ -1,4 +1,6 @@
/** Suggested item categories (admin can still type custom ones). */
import { prisma } from './db.js';
/** Default categories seeded on boot / sync. */
export const ITEM_CATEGORIES = [
'Rifle',
'Pistol',
@@ -16,3 +18,54 @@ export function normalizeCategory(value) {
const cat = String(value ?? '').trim();
return cat || 'Misc';
}
/** Ensure a single category row exists; returns normalized name. */
export async function ensureCategory(name) {
const normalized = normalizeCategory(name);
await prisma.category.upsert({
where: { name: normalized },
create: { name: normalized },
update: {},
});
return normalized;
}
/**
* Seed defaults + any category strings already used on items.
*/
export async function ensureCategories() {
const fromItems = await prisma.item.findMany({
select: { category: true },
distinct: ['category'],
});
const names = [
...new Set([
...ITEM_CATEGORIES,
...fromItems.map((r) => normalizeCategory(r.category)),
]),
];
for (const name of names) {
await prisma.category.upsert({
where: { name },
create: { name },
update: {},
});
}
}
export async function listCategoriesWithCounts() {
const [categories, grouped] = await Promise.all([
prisma.category.findMany({ orderBy: { name: 'asc' } }),
prisma.item.groupBy({
by: ['category'],
_count: { _all: true },
}),
]);
const countMap = new Map(grouped.map((g) => [g.category, g._count._all]));
return categories.map((c) => ({
id: c.id,
name: c.name,
itemCount: countMap.get(c.name) || 0,
createdAt: c.createdAt,
}));
}
+2
View File
@@ -27,6 +27,7 @@ import { recoverDuelSpins, getDuelRoom, listDuelRooms, startEmptyDuelRoomPurge }
import { getJackpotState } from './services/jackpot.js';
import { ensureAdminFromEnv } from './ensureAdmin.js';
import { ensureAchievements, checkAchievements } from './achievements.js';
import { ensureCategories } from './categories.js';
import { trackConnect, trackDisconnect } from './presence.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -239,6 +240,7 @@ async function bootstrap() {
try {
await ensureAdminFromEnv();
await ensureAchievements();
await ensureCategories();
} catch (err) {
console.error(err.message || err);
process.exit(1);
+88 -12
View File
@@ -6,7 +6,12 @@ import { isValidRarity, RARITIES } from '../rarities.js';
import { createUploader } from '../upload.js';
import { onlineUserCount, totalSocketCount } from '../presence.js';
import { ACHIEVEMENT_DEFS } from '../achievements.js';
import { ITEM_CATEGORIES, normalizeCategory } from '../categories.js';
import {
ITEM_CATEGORIES,
normalizeCategory,
ensureCategory,
listCategoriesWithCounts,
} from '../categories.js';
const router = Router();
const upload = createUploader('asset');
@@ -42,22 +47,93 @@ router.get('/rarities', requireAdmin, (_req, res) => {
router.get('/categories', requireAdmin, async (_req, res) => {
try {
const rows = await prisma.item.findMany({
select: { category: true },
distinct: ['category'],
orderBy: { category: 'asc' },
const categories = await listCategoriesWithCounts();
res.json({
categories,
names: categories.map((c) => c.name),
suggested: ITEM_CATEGORIES,
});
const fromDb = rows.map((r) => r.category).filter(Boolean);
const categories = [...new Set([...ITEM_CATEGORIES, ...fromDb])].sort((a, b) =>
a.localeCompare(b)
);
res.json({ categories, suggested: ITEM_CATEGORIES });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to load categories' });
}
});
router.post('/categories', requireAdmin, async (req, res) => {
try {
const name = normalizeCategory(req.body.name);
const existing = await prisma.category.findUnique({ where: { name } });
if (existing) {
return res.status(400).json({ error: 'Category already exists' });
}
const category = await prisma.category.create({ data: { name } });
res.status(201).json({
category: { ...category, itemCount: 0 },
});
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to create category' });
}
});
router.put('/categories/:id', requireAdmin, async (req, res) => {
try {
const id = Number(req.params.id);
const name = normalizeCategory(req.body.name);
const current = await prisma.category.findUnique({ where: { id } });
if (!current) {
return res.status(404).json({ error: 'Category not found' });
}
if (name !== current.name) {
const clash = await prisma.category.findUnique({ where: { name } });
if (clash) {
return res.status(400).json({ error: 'Category name already taken' });
}
}
const updated = await prisma.$transaction(async (tx) => {
const category = await tx.category.update({
where: { id },
data: { name },
});
if (name !== current.name) {
await tx.item.updateMany({
where: { category: current.name },
data: { category: name },
});
}
const itemCount = await tx.item.count({ where: { category: name } });
return { ...category, itemCount };
});
res.json({ category: updated });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to update category' });
}
});
router.delete('/categories/:id', requireAdmin, async (req, res) => {
try {
const id = Number(req.params.id);
const current = await prisma.category.findUnique({ where: { id } });
if (!current) {
return res.status(404).json({ error: 'Category not found' });
}
const itemCount = await prisma.item.count({ where: { category: current.name } });
if (itemCount > 0) {
return res.status(400).json({
error: `Cannot delete: ${itemCount} item${itemCount === 1 ? '' : 's'} still use this category`,
});
}
await prisma.category.delete({ where: { id } });
res.json({ ok: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to delete category' });
}
});
router.get('/stats', requireAdmin, async (_req, res) => {
try {
const [
@@ -348,7 +424,7 @@ router.post('/items', requireAdmin, async (req, res) => {
const name = String(req.body.name || '').trim();
const imageUrl = String(req.body.imageUrl || '');
const rarity = String(req.body.rarity || '');
const category = normalizeCategory(req.body.category);
const category = await ensureCategory(req.body.category);
const marketValue = Number(req.body.marketValue);
if (!name) return res.status(400).json({ error: 'Name is required' });
@@ -382,7 +458,7 @@ router.put('/items/:id', requireAdmin, async (req, res) => {
data.rarity = req.body.rarity;
}
if (req.body.category !== undefined) {
data.category = normalizeCategory(req.body.category);
data.category = await ensureCategory(req.body.category);
}
if (req.body.marketValue !== undefined) {
const marketValue = Number(req.body.marketValue);