fix: restore complete admin feature dependencies

This commit is contained in:
Simo committed 2026-07-11 21:15:54 +02:00
1 parent 5b4228261a
commit 0cd753c735
115 files changed
+18565 -4930

No files matched your search

@@ -0,0 +1,218 @@
'use client'
import { Crown, Image as ImageIcon, Package, Sparkles } from 'lucide-react'
import type { CatalogItemData } from '@/app/admin/catalog/[id]/catalog-items-table/types'
import { getPointsLabel } from '@/app/admin/catalog/[id]/catalog-items-table/types'
import { Badge } from '@/components/ui/badge'
import { getHabboCdnIconUrl, getLocalIconUrl } from '@/lib/furni/classname'
import { cn } from '@/lib/utils'
interface ItemsShopPreviewProps {
items: CatalogItemData[]
furniRevisionMap: Record<number, { classname: string; revision: number }>
layout: string
canEdit: boolean
onItemClick?: (item: CatalogItemData) => void
}
export function ItemsShopPreview({
items,
furniRevisionMap,
layout,
canEdit,
onItemClick,
}: ItemsShopPreviewProps) {
if (items.length === 0) {
return (
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-12 text-center">
<Package className="h-10 w-10 text-muted-foreground/40" />
<p className="mt-2 text-sm text-muted-foreground">No items in this page yet</p>
</div>
)
}
const gridClass = getGridClass(layout)
const tileSize = getTileSize(layout)
return (
<div className="rounded-lg border bg-gradient-to-b from-sky-50 to-sky-100 dark:from-slate-900 dark:to-slate-950 p-4">
<div className="mb-3 flex items-center justify-between text-xs text-muted-foreground">
<span className="font-medium">Shop preview · {layout}</span>
<span>{items.length} items</span>
</div>
<div className={cn('grid gap-2', gridClass)}>
{items.map((item) => {
const firstId = parseFirstItemId(item.itemIds)
const furni = firstId != null ? furniRevisionMap[firstId] : undefined
const points = getPointsLabel(item.pointsType)
const isClub = item.clubOnly === '1'
const isOffer = item.haveOffer === '1' && item.offerId > 0
return (
<button
key={item.id}
type="button"
onClick={() => onItemClick?.(item)}
disabled={!canEdit && !onItemClick}
className={cn(
'group relative flex flex-col overflow-hidden rounded-md border bg-card shadow-sm transition-all',
canEdit && 'hover:border-primary hover:shadow-md hover:scale-[1.02] cursor-pointer',
!canEdit && 'cursor-default',
)}
title={item.catalogName || item.baseName}
>
{/* Tile icon */}
<div
className="relative flex items-center justify-center bg-gradient-to-br from-muted/30 to-muted/60"
style={{ height: tileSize }}
>
<ShopFurniIcon
classname={furni?.classname ?? ''}
revision={furni?.revision ?? 0}
size={Math.floor(tileSize * 0.7)}
/>
{/* Badges overlay */}
<div className="absolute right-1 top-1 flex flex-col gap-0.5">
{isClub && (
<Badge className="h-4 gap-0.5 border-0 bg-yellow-500/90 px-1 text-[8px] text-white">
<Crown className="h-2.5 w-2.5" />
HC
</Badge>
)}
{isOffer && (
<Badge className="h-4 gap-0.5 border-0 bg-rose-500/90 px-1 text-[8px] text-white">
<Sparkles className="h-2.5 w-2.5" />
OFFER
</Badge>
)}
</div>
{/* Amount badge */}
{item.amount > 1 && (
<Badge className="absolute left-1 top-1 h-4 border-0 bg-black/70 px-1 text-[9px] text-white">
x{item.amount}
</Badge>
)}
{/* Limited */}
{item.limitedStack > 0 && (
<Badge className="absolute bottom-1 left-1 h-4 border-0 bg-purple-600/90 px-1 text-[8px] text-white">
LTD {item.limitedSells}/{item.limitedStack}
</Badge>
)}
</div>
{/* Name + price */}
<div className="border-t bg-background/95 px-2 py-1.5">
<p className="truncate text-xs font-medium leading-tight">
{item.catalogName || item.baseName || `#${item.id}`}
</p>
<div className="mt-0.5 flex items-center gap-1.5 text-[10px] font-mono">
{item.costCredits > 0 && (
<span className="text-yellow-600 dark:text-yellow-500">
{item.costCredits}c
</span>
)}
{item.costPoints > 0 && (
<span className={points.color}>
{item.costPoints} {points.label.slice(0, 3)}
</span>
)}
{item.costCredits === 0 && item.costPoints === 0 && (
<span className="text-muted-foreground">Free</span>
)}
</div>
</div>
</button>
)
})}
</div>
</div>
)
}
// ── Helpers ─────────────────────────────────────────────────────
function parseFirstItemId(itemIds: string): number | null {
if (!itemIds) return null
const first = itemIds.split(';')[0]?.trim()
const n = parseInt(first, 10)
return Number.isNaN(n) ? null : n
}
function getGridClass(layout: string): string {
switch (layout) {
case 'spaces_new':
return 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4'
case 'frontpage':
return 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3'
case 'single_bundle':
case 'room_bundle':
return 'grid-cols-1 sm:grid-cols-2'
case 'pets':
case 'pets2':
case 'pets3':
return 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4'
default:
return 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5'
}
}
function getTileSize(layout: string): number {
switch (layout) {
case 'frontpage':
case 'single_bundle':
case 'room_bundle':
return 140
case 'spaces_new':
return 110
default:
return 96
}
}
// ── Shop tile furni icon (resizable) ────────────────────────────
function ShopFurniIcon({
classname,
revision,
size,
}: {
classname: string
revision: number
size: number
}) {
if (!classname) {
return (
<ImageIcon
className="text-muted-foreground/40"
style={{ width: size * 0.5, height: size * 0.5 }}
/>
)
}
const primary = getLocalIconUrl(classname)
function handleError(e: React.SyntheticEvent<HTMLImageElement>) {
const img = e.target as HTMLImageElement
if (!img.dataset.fallback && revision > 0) {
img.dataset.fallback = 'cdn'
img.src = getHabboCdnIconUrl(classname, revision)
} else {
img.style.opacity = '0'
}
}
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={primary}
alt={classname}
className="object-contain"
style={{ imageRendering: 'pixelated', maxWidth: size, maxHeight: size }}
onError={handleError}
loading="lazy"
/>
)
}