fix: mobile responsiveness for all admin pages
- DataTable: overflow-x-auto on table wrapper (affects users, rooms, tickets) - Radio: overflow-x-auto wrappers around all 3 tables (shouts, applications, schedules) - Catalog items: responsive grids (grid-cols-1 sm:grid-cols-3) in Add/Edit dialogs - Catalog tabs: TabsList overflow-x-auto for narrow screens - Import clone: pagination flex-wrap to prevent overlap on small screens
This commit is contained in:
1 parent
d189bb5af4
commit
827595ef00
5 files changed
+65
-39
No files matched your search
@@ -69,7 +69,7 @@ export function CatalogDetailTabs({
|
|||||||
}: CatalogDetailTabsProps) {
|
}: CatalogDetailTabsProps) {
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue="settings">
|
<Tabs defaultValue="settings">
|
||||||
<TabsList>
|
<TabsList className="overflow-x-auto flex-nowrap w-full">
|
||||||
<TabsTrigger value="settings" className="gap-2">
|
<TabsTrigger value="settings" className="gap-2">
|
||||||
<Settings className="h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
Settings
|
Settings
|
||||||
|
|||||||
@@ -736,7 +736,7 @@ export function CatalogItemsTable({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">Credits</Label>
|
<Label className="text-xs">Credits</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -777,7 +777,7 @@ export function CatalogItemsTable({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">Amount</Label>
|
<Label className="text-xs">Amount</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -810,7 +810,7 @@ export function CatalogItemsTable({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">Limited Sells</Label>
|
<Label className="text-xs">Limited Sells</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -833,7 +833,7 @@ export function CatalogItemsTable({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">Have Offer</Label>
|
<Label className="text-xs">Have Offer</Label>
|
||||||
<Select
|
<Select
|
||||||
@@ -1276,7 +1276,10 @@ export function CatalogItemsTable({
|
|||||||
<div className="text-xs flex flex-wrap items-center gap-1.5">
|
<div className="text-xs flex flex-wrap items-center gap-1.5">
|
||||||
{item.costCredits > 0 && (
|
{item.costCredits > 0 && (
|
||||||
<span className="inline-flex items-center gap-0.5 text-[var(--admin-warning)]">
|
<span className="inline-flex items-center gap-0.5 text-[var(--admin-warning)]">
|
||||||
<CurrencyIcon kind="credits" size={12} />
|
<CurrencyIcon
|
||||||
|
kind="credits"
|
||||||
|
size={12}
|
||||||
|
/>
|
||||||
{item.costCredits}
|
{item.costCredits}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -1317,7 +1320,9 @@ export function CatalogItemsTable({
|
|||||||
className="h-8 text-xs w-16"
|
className="h-8 text-xs w-16"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs">{item.amount}</span>
|
<span className="text-xs">
|
||||||
|
{item.amount}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -1398,7 +1403,9 @@ export function CatalogItemsTable({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={() => handleDuplicateItem(item)}
|
onClick={() =>
|
||||||
|
handleDuplicateItem(item)
|
||||||
|
}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
title="Duplicate item"
|
title="Duplicate item"
|
||||||
>
|
>
|
||||||
@@ -1418,7 +1425,9 @@ export function CatalogItemsTable({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7 text-destructive"
|
className="h-7 w-7 text-destructive"
|
||||||
onClick={() => handleDeleteItem(item.id)}
|
onClick={() =>
|
||||||
|
handleDeleteItem(item.id)
|
||||||
|
}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
title="Delete"
|
title="Delete"
|
||||||
>
|
>
|
||||||
@@ -1482,7 +1491,7 @@ export function CatalogItemsTable({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{/* Read-only identity fields */}
|
{/* Read-only identity fields */}
|
||||||
<div className="grid grid-cols-2 gap-3 mb-2">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs text-muted-foreground">
|
<Label className="text-xs text-muted-foreground">
|
||||||
Catalog Name
|
Catalog Name
|
||||||
@@ -1526,7 +1535,7 @@ export function CatalogItemsTable({
|
|||||||
|
|
||||||
{/* ── Catalog Settings Tab ── */}
|
{/* ── Catalog Settings Tab ── */}
|
||||||
<TabsContent value="catalog" className="space-y-3 mt-3">
|
<TabsContent value="catalog" className="space-y-3 mt-3">
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">Credits</Label>
|
<Label className="text-xs">Credits</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -1537,7 +1546,8 @@ export function CatalogItemsTable({
|
|||||||
(p) =>
|
(p) =>
|
||||||
p && {
|
p && {
|
||||||
...p,
|
...p,
|
||||||
costCredits: parseInt(e.target.value, 10) || 0,
|
costCredits:
|
||||||
|
parseInt(e.target.value, 10) || 0,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1585,7 +1595,7 @@ export function CatalogItemsTable({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">Amount</Label>
|
<Label className="text-xs">Amount</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -1612,7 +1622,8 @@ export function CatalogItemsTable({
|
|||||||
(p) =>
|
(p) =>
|
||||||
p && {
|
p && {
|
||||||
...p,
|
...p,
|
||||||
orderNumber: parseInt(e.target.value, 10) || 0,
|
orderNumber:
|
||||||
|
parseInt(e.target.value, 10) || 0,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1636,7 +1647,7 @@ export function CatalogItemsTable({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">Limited Sells</Label>
|
<Label className="text-xs">Limited Sells</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -1647,7 +1658,8 @@ export function CatalogItemsTable({
|
|||||||
(p) =>
|
(p) =>
|
||||||
p && {
|
p && {
|
||||||
...p,
|
...p,
|
||||||
limitedSells: parseInt(e.target.value, 10) || 0,
|
limitedSells:
|
||||||
|
parseInt(e.target.value, 10) || 0,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1663,7 +1675,8 @@ export function CatalogItemsTable({
|
|||||||
(p) =>
|
(p) =>
|
||||||
p && {
|
p && {
|
||||||
...p,
|
...p,
|
||||||
limitedStack: parseInt(e.target.value, 10) || 0,
|
limitedStack:
|
||||||
|
parseInt(e.target.value, 10) || 0,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1671,7 +1684,7 @@ export function CatalogItemsTable({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">Have Offer</Label>
|
<Label className="text-xs">Have Offer</Label>
|
||||||
<Select
|
<Select
|
||||||
@@ -1744,7 +1757,7 @@ export function CatalogItemsTable({
|
|||||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
|
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
|
||||||
Identity
|
Identity
|
||||||
</h4>
|
</h4>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">Base ID</Label>
|
<Label className="text-xs">Base ID</Label>
|
||||||
<div className="rounded-md border bg-muted/50 px-3 py-2 text-sm font-mono">
|
<div className="rounded-md border bg-muted/50 px-3 py-2 text-sm font-mono">
|
||||||
@@ -1761,7 +1774,8 @@ export function CatalogItemsTable({
|
|||||||
(p) =>
|
(p) =>
|
||||||
p && {
|
p && {
|
||||||
...p,
|
...p,
|
||||||
spriteId: parseInt(e.target.value, 10) || 0,
|
spriteId:
|
||||||
|
parseInt(e.target.value, 10) || 0,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1784,7 +1798,9 @@ export function CatalogItemsTable({
|
|||||||
<SelectItem value="e">e (Effect)</SelectItem>
|
<SelectItem value="e">e (Effect)</SelectItem>
|
||||||
<SelectItem value="b">b (Badge)</SelectItem>
|
<SelectItem value="b">b (Badge)</SelectItem>
|
||||||
<SelectItem value="r">r (Robot)</SelectItem>
|
<SelectItem value="r">r (Robot)</SelectItem>
|
||||||
<SelectItem value="h">h (Habbo Club)</SelectItem>
|
<SelectItem value="h">
|
||||||
|
h (Habbo Club)
|
||||||
|
</SelectItem>
|
||||||
<SelectItem value="p">p (Pet)</SelectItem>
|
<SelectItem value="p">p (Pet)</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -1964,7 +1980,10 @@ export function CatalogItemsTable({
|
|||||||
onChange={(v) =>
|
onChange={(v) =>
|
||||||
setEditingBase(
|
setEditingBase(
|
||||||
(p) =>
|
(p) =>
|
||||||
p && { ...p, allowMarketplaceSell: v ? 1 : 0 },
|
p && {
|
||||||
|
...p,
|
||||||
|
allowMarketplaceSell: v ? 1 : 0,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -2008,7 +2027,10 @@ export function CatalogItemsTable({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditingBase(
|
setEditingBase(
|
||||||
(p) =>
|
(p) =>
|
||||||
p && { ...p, customparams: e.target.value },
|
p && {
|
||||||
|
...p,
|
||||||
|
customparams: e.target.value,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
@@ -2034,7 +2056,9 @@ export function CatalogItemsTable({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">Effect ID Female</Label>
|
<Label className="text-xs">
|
||||||
|
Effect ID Female
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={editingBase.effectIdFemale}
|
value={editingBase.effectIdFemale}
|
||||||
@@ -2058,7 +2082,10 @@ export function CatalogItemsTable({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditingBase(
|
setEditingBase(
|
||||||
(p) =>
|
(p) =>
|
||||||
p && { ...p, clothingOnWalk: e.target.value },
|
p && {
|
||||||
|
...p,
|
||||||
|
clothingOnWalk: e.target.value,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
@@ -2283,7 +2310,9 @@ export function CatalogItemsTable({
|
|||||||
setBulkPriceValue(parseFloat(e.target.value) || 0)
|
setBulkPriceValue(parseFloat(e.target.value) || 0)
|
||||||
}
|
}
|
||||||
placeholder={
|
placeholder={
|
||||||
bulkPriceMode === "percent" ? "e.g. 10 for +10%" : "e.g. 5"
|
bulkPriceMode === "percent"
|
||||||
|
? "e.g. 10 for +10%"
|
||||||
|
: "e.g. 5"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{bulkPriceMode === "percent" && (
|
{bulkPriceMode === "percent" && (
|
||||||
|
|||||||
@@ -876,7 +876,7 @@ function FurniGrid({ source }: FurniGridProps) {
|
|||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{total > PER_PAGE && (
|
{total > PER_PAGE && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Page {page} / {lastPage}
|
Page {page} / {lastPage}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ type Schedule = {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function formatTime(d: Date | null): string {
|
function formatTime(d: Date | null): string {
|
||||||
return d ? d.toISOString().slice(11, 16) : "\u2014";
|
return d ? d.toISOString().slice(11, 16) : "\u2014";
|
||||||
}
|
}
|
||||||
@@ -108,22 +107,14 @@ export default async function AdminRadioPage() {
|
|||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||||
<StatusCard
|
<StatusCard label={t("shouts")} value={shouts.length} icon="📣" />
|
||||||
label={t("shouts")}
|
|
||||||
value={shouts.length}
|
|
||||||
icon="📣"
|
|
||||||
/>
|
|
||||||
<StatusCard
|
<StatusCard
|
||||||
label={t("pendingApplications")}
|
label={t("pendingApplications")}
|
||||||
value={pendingApps}
|
value={pendingApps}
|
||||||
state={pendingApps > 0 ? "warn" : "ok"}
|
state={pendingApps > 0 ? "warn" : "ok"}
|
||||||
icon="📝"
|
icon="📝"
|
||||||
/>
|
/>
|
||||||
<StatusCard
|
<StatusCard label={t("schedules")} value={schedules.length} icon="🗓️" />
|
||||||
label={t("schedules")}
|
|
||||||
value={schedules.length}
|
|
||||||
icon="🗓️"
|
|
||||||
/>
|
|
||||||
<StatusCard
|
<StatusCard
|
||||||
label={t("activeSchedules")}
|
label={t("activeSchedules")}
|
||||||
value={activeSchedules}
|
value={activeSchedules}
|
||||||
@@ -141,6 +132,7 @@ export default async function AdminRadioPage() {
|
|||||||
{t("noShouts")}
|
{t("noShouts")}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -174,6 +166,7 @@ export default async function AdminRadioPage() {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -186,6 +179,7 @@ export default async function AdminRadioPage() {
|
|||||||
{t("noApplications")}
|
{t("noApplications")}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -222,6 +216,7 @@ export default async function AdminRadioPage() {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -234,6 +229,7 @@ export default async function AdminRadioPage() {
|
|||||||
{t("noSchedules")}
|
{t("noSchedules")}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -266,6 +262,7 @@ export default async function AdminRadioPage() {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ export function DataTable<T extends { id: number | string }>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
Reference in new issue
Block a user