Use Habbo currency PNGs for admin credits, duckets, and diamonds icons.
Local Build and Deploy / deploy (push) Successful in 56s
Local Build and Deploy / deploy (push) Successful in 56s
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
b42a313825
commit
d7405238ea
7 files changed
+118
-26
No files matched your search
@@ -1,7 +1,8 @@
|
|||||||
import { ArrowLeftRight, Coins, TrendingDown, TrendingUp } from "lucide-react";
|
import { ArrowLeftRight, TrendingDown, TrendingUp } from "lucide-react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { RevenueChart } from "@/components/admin/revenue-chart";
|
import { RevenueChart } from "@/components/admin/revenue-chart";
|
||||||
import { StatsCard } from "@/components/admin/stats-card";
|
import { StatsCard } from "@/components/admin/stats-card";
|
||||||
|
import { CurrencyIcon } from "@/components/shared/currency-icon";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
@@ -161,19 +162,19 @@ export default async function EconomyPage() {
|
|||||||
title="Credits in Circulation"
|
title="Credits in Circulation"
|
||||||
value={data.totalCredits.toLocaleString()}
|
value={data.totalCredits.toLocaleString()}
|
||||||
description="Total credits across all users"
|
description="Total credits across all users"
|
||||||
icon={<Coins className="h-4 w-4" />}
|
icon={<CurrencyIcon kind="credits" size={16} />}
|
||||||
/>
|
/>
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Pixels in Circulation"
|
title="Pixels in Circulation"
|
||||||
value={data.totalPixels.toLocaleString()}
|
value={data.totalPixels.toLocaleString()}
|
||||||
description="Total pixels across all users"
|
description="Total pixels across all users"
|
||||||
icon={<TrendingUp className="h-4 w-4" />}
|
icon={<CurrencyIcon kind="pixels" size={16} />}
|
||||||
/>
|
/>
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Points in Circulation"
|
title="Points in Circulation"
|
||||||
value={data.totalPoints.toLocaleString()}
|
value={data.totalPoints.toLocaleString()}
|
||||||
description="Total points across all users"
|
description="Total points across all users"
|
||||||
icon={<TrendingDown className="h-4 w-4" />}
|
icon={<CurrencyIcon kind="diamonds" size={16} />}
|
||||||
/>
|
/>
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Weekly Trades"
|
title="Weekly Trades"
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ import {
|
|||||||
import { BulkImportItems } from "@/components/admin/catalog/bulk-import-items";
|
import { BulkImportItems } from "@/components/admin/catalog/bulk-import-items";
|
||||||
import { ItemsShopPreview } from "@/components/admin/catalog/items-shop-preview";
|
import { ItemsShopPreview } from "@/components/admin/catalog/items-shop-preview";
|
||||||
import { QuickAddFurni } from "@/components/admin/catalog/quick-add-furni";
|
import { QuickAddFurni } from "@/components/admin/catalog/quick-add-furni";
|
||||||
|
import {
|
||||||
|
CurrencyIcon,
|
||||||
|
currencyKindFromPointsType,
|
||||||
|
} from "@/components/shared/currency-icon";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
@@ -1257,15 +1261,24 @@ export function CatalogItemsTable({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs">
|
<div className="text-xs flex flex-wrap items-center gap-1.5">
|
||||||
{item.costCredits > 0 && (
|
{item.costCredits > 0 && (
|
||||||
<span className="text-[var(--admin-warning)]">
|
<span className="inline-flex items-center gap-0.5 text-[var(--admin-warning)]">
|
||||||
{item.costCredits}c{" "}
|
<CurrencyIcon kind="credits" size={12} />
|
||||||
|
{item.costCredits}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{item.costPoints > 0 && (
|
{item.costPoints > 0 && (
|
||||||
<span className={pts.color}>
|
<span
|
||||||
{item.costPoints} {pts.label}{" "}
|
className={`inline-flex items-center gap-0.5 ${pts.color}`}
|
||||||
|
>
|
||||||
|
<CurrencyIcon
|
||||||
|
kind={currencyKindFromPointsType(
|
||||||
|
item.pointsType,
|
||||||
|
)}
|
||||||
|
size={12}
|
||||||
|
/>
|
||||||
|
{item.costPoints}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{item.costCredits === 0 &&
|
{item.costCredits === 0 &&
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
Coins,
|
|
||||||
Copy,
|
Copy,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
MessageCircle,
|
MessageCircle,
|
||||||
@@ -19,6 +18,7 @@ import {
|
|||||||
sendCredits,
|
sendCredits,
|
||||||
unmuteUser,
|
unmuteUser,
|
||||||
} from "@/actions/users";
|
} from "@/actions/users";
|
||||||
|
import { CurrencyIcon } from "@/components/shared/currency-icon";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
@@ -229,7 +229,8 @@ export function UserQuickActions({
|
|||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<Coins className="mr-1.5 h-3.5 w-3.5" /> Send Credits
|
<CurrencyIcon kind="credits" size={14} className="mr-1.5" />{" "}
|
||||||
|
Send Credits
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
ArrowLeftRight,
|
ArrowLeftRight,
|
||||||
Coins,
|
|
||||||
Diamond,
|
|
||||||
DoorOpen,
|
DoorOpen,
|
||||||
Eye,
|
Eye,
|
||||||
Gem,
|
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Pencil,
|
Pencil,
|
||||||
Shield,
|
Shield,
|
||||||
Sparkles,
|
|
||||||
Wifi,
|
Wifi,
|
||||||
WifiOff,
|
WifiOff,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
|
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
|
||||||
import { UserWatchToggle } from "@/components/admin/user-watch-toggle";
|
import { UserWatchToggle } from "@/components/admin/user-watch-toggle";
|
||||||
|
import { CurrencyIcon } from "@/components/shared/currency-icon";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { getAvatarUrl } from "@/lib/imager";
|
import { getAvatarUrl } from "@/lib/imager";
|
||||||
@@ -159,28 +156,28 @@ export function UserHero({
|
|||||||
{/* Currency row */}
|
{/* Currency row */}
|
||||||
<div className="flex flex-wrap gap-3 pt-1">
|
<div className="flex flex-wrap gap-3 pt-1">
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
<Coins className="h-4 w-4 text-[var(--admin-warning)]" />
|
<CurrencyIcon kind="credits" size={18} />
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{user.credits.toLocaleString()}
|
{user.credits.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground text-xs">credits</span>
|
<span className="text-muted-foreground text-xs">credits</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
<Diamond className="h-4 w-4 text-[var(--admin-info)]" />
|
<CurrencyIcon kind="diamonds" size={18} />
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{diamonds.toLocaleString()}
|
{diamonds.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground text-xs">diamonds</span>
|
<span className="text-muted-foreground text-xs">diamonds</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
<Sparkles className="h-4 w-4 text-[var(--admin-accent-text)]" />
|
<CurrencyIcon kind="duckets" size={18} />
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{duckets.toLocaleString()}
|
{duckets.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground text-xs">duckets</span>
|
<span className="text-muted-foreground text-xs">duckets</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
<Gem className="h-4 w-4 text-[var(--admin-error)]" />
|
<CurrencyIcon kind="pixels" size={18} />
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{user.pixels.toLocaleString()}
|
{user.pixels.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
bulkUnban,
|
bulkUnban,
|
||||||
} from "@/actions/bulk-users";
|
} from "@/actions/bulk-users";
|
||||||
import { DataTable } from "@/components/admin/data-table";
|
import { DataTable } from "@/components/admin/data-table";
|
||||||
|
import { CurrencyIcon } from "@/components/shared/currency-icon";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -436,9 +437,22 @@ export function UsersTable({ data, canEdit, canBan }: UsersTableProps) {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="credits">Credits</SelectItem>
|
<SelectItem value="credits">
|
||||||
<SelectItem value="pixels">Pixels (Duckets)</SelectItem>
|
<span className="inline-flex items-center gap-2">
|
||||||
<SelectItem value="points">Points (Diamonds)</SelectItem>
|
<CurrencyIcon kind="credits" size={14} /> Credits
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="pixels">
|
||||||
|
<span className="inline-flex items-center gap-2">
|
||||||
|
<CurrencyIcon kind="pixels" size={14} /> Pixels (Duckets)
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="points">
|
||||||
|
<span className="inline-flex items-center gap-2">
|
||||||
|
<CurrencyIcon kind="diamonds" size={14} /> Points
|
||||||
|
(Diamonds)
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
import { Crown, Image as ImageIcon, Package, Sparkles } from "lucide-react";
|
import { Crown, Image as ImageIcon, Package, Sparkles } from "lucide-react";
|
||||||
import type { CatalogItemData } from "@/app/admin/catalog/[id]/catalog-items-table/types";
|
import type { CatalogItemData } from "@/app/admin/catalog/[id]/catalog-items-table/types";
|
||||||
import { getPointsLabel } from "@/app/admin/catalog/[id]/catalog-items-table/types";
|
import { getPointsLabel } from "@/app/admin/catalog/[id]/catalog-items-table/types";
|
||||||
|
import {
|
||||||
|
CurrencyIcon,
|
||||||
|
currencyKindFromPointsType,
|
||||||
|
} from "@/components/shared/currency-icon";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { getHabboCdnIconUrl, getLocalIconUrl } from "@/lib/furni/classname";
|
import { getHabboCdnIconUrl, getLocalIconUrl } from "@/lib/furni/classname";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -113,13 +117,20 @@ export function ItemsShopPreview({
|
|||||||
</p>
|
</p>
|
||||||
<div className="mt-0.5 flex items-center gap-1.5 text-[10px] font-mono">
|
<div className="mt-0.5 flex items-center gap-1.5 text-[10px] font-mono">
|
||||||
{item.costCredits > 0 && (
|
{item.costCredits > 0 && (
|
||||||
<span className="text-[var(--admin-warning)] dark:text-[var(--admin-warning)]">
|
<span className="inline-flex items-center gap-0.5 text-[var(--admin-warning)] dark:text-[var(--admin-warning)]">
|
||||||
{item.costCredits}c
|
<CurrencyIcon kind="credits" size={12} />
|
||||||
|
{item.costCredits}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{item.costPoints > 0 && (
|
{item.costPoints > 0 && (
|
||||||
<span className={points.color}>
|
<span
|
||||||
{item.costPoints} {points.label.slice(0, 3)}
|
className={`inline-flex items-center gap-0.5 ${points.color}`}
|
||||||
|
>
|
||||||
|
<CurrencyIcon
|
||||||
|
kind={currencyKindFromPointsType(item.pointsType)}
|
||||||
|
size={12}
|
||||||
|
/>
|
||||||
|
{item.costPoints}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{item.costCredits === 0 && item.costPoints === 0 && (
|
{item.costCredits === 0 && item.costPoints === 0 && (
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* Habbo-style currency icons used across admin (and reusable on public).
|
||||||
|
* Files live at /assets/images/icons/currency/{name}.png
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type CurrencyKind =
|
||||||
|
| "credits"
|
||||||
|
| "duckets"
|
||||||
|
| "diamonds"
|
||||||
|
| "pixels"
|
||||||
|
| "points";
|
||||||
|
|
||||||
|
const CURRENCY_FILES: Record<CurrencyKind, string> = {
|
||||||
|
credits: "credits",
|
||||||
|
duckets: "duckets",
|
||||||
|
diamonds: "diamonds",
|
||||||
|
// No dedicated pixels asset — activity points share the duckets icon.
|
||||||
|
pixels: "duckets",
|
||||||
|
points: "duckets",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Map emulator `points_type` (catalog / users_currency) to an icon. */
|
||||||
|
export function currencyKindFromPointsType(type: number): CurrencyKind {
|
||||||
|
if (type === 5) return "diamonds";
|
||||||
|
return "duckets";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currencyIconSrc(kind: CurrencyKind): string {
|
||||||
|
return `/assets/images/icons/currency/${CURRENCY_FILES[kind]}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CurrencyIcon({
|
||||||
|
kind,
|
||||||
|
size = 16,
|
||||||
|
className = "",
|
||||||
|
alt,
|
||||||
|
}: {
|
||||||
|
kind: CurrencyKind;
|
||||||
|
size?: number;
|
||||||
|
className?: string;
|
||||||
|
alt?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
// biome-ignore lint/performance/noImgElement: small pixel art asset, not Next Image optimized
|
||||||
|
<img
|
||||||
|
src={currencyIconSrc(kind)}
|
||||||
|
alt={alt ?? kind}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={`inline-block shrink-0 object-contain image-pixelated ${className}`.trim()}
|
||||||
|
style={{ width: size, height: size, imageRendering: "pixelated" }}
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user