diff --git a/src/app/admin/achievements/page.tsx b/src/app/admin/achievements/page.tsx index a0f0547766..13f5ece9fb 100644 --- a/src/app/admin/achievements/page.tsx +++ b/src/app/admin/achievements/page.tsx @@ -1,3 +1,4 @@ +import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -39,47 +40,60 @@ export default async function AdminAchievements() { groups.set(a.category, list); } + // Distinct achievement names (each name can have multiple level rows). + const distinctNames = new Set(achievements.map((a) => a.name)).size; + return (

Achievements

-

+

Read-only ยท {achievements.length} rows across {groups.size} categories.

+
+ + + +
+ {groups.size === 0 ? (

No achievements found.

) : ( [...groups.entries()].map(([category, rows]) => ( -
-

{category}

- - - - - - - - - - - - - {rows.map((a) => ( - - - - - - - +
+

+ {category}{" "} + {rows.length} +

+
+
NameLevelProgress neededReward typeReward amountPoints
{a.name}{a.level}{a.progressNeeded}{a.rewardType}{a.rewardAmount}{a.points ?? 0}
+ + + + + + + + - ))} - -
NameLevelProgress neededReward typeReward amountPoints
-
+ + + {rows.map((a) => ( + + + {a.name} + + {a.level} + {a.progressNeeded} + {a.rewardType} + {a.rewardAmount} + {a.points ?? 0} + + ))} + + + + )) )}
diff --git a/src/app/admin/ads/page.tsx b/src/app/admin/ads/page.tsx index 08af99c610..b7cb534d98 100644 --- a/src/app/admin/ads/page.tsx +++ b/src/app/admin/ads/page.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -14,47 +15,64 @@ export default async function AdminAds() { ads = []; } + const latest = ads[0]?.createdAt + ? ads[0].createdAt.toISOString().slice(0, 10) + : "โ€”"; + return (

Advertisements

- + New ad + + + New ad +
-

+

Website advertisement banners (website_ads). Each ad is an image URL shown across the site.

- - - - - - - - - - {ads.map((ad) => ( - - - - - - - ))} - -
PreviewImage URLCreated -
- {/* eslint-disable-next-line @next/next/no-img-element */} - - - {ad.image} - - {ad.createdAt ? ad.createdAt.toISOString().slice(0, 10) : ""} - - Edit -
- {ads.length === 0 ?

No advertisements yet.

: null} +
+ + +
+ +
+

All ads

+ {ads.length === 0 ? ( +

No advertisements yet.

+ ) : ( + + + + + + + + + + {ads.map((ad) => ( + + + + + + + ))} + +
PreviewImage URLCreated +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + + + {ad.image} + + {ad.createdAt ? ad.createdAt.toISOString().slice(0, 10) : "โ€”"} + + Edit +
+ )} +
); } diff --git a/src/app/admin/alerts/page.tsx b/src/app/admin/alerts/page.tsx index 12e08a8896..78ad306b94 100644 --- a/src/app/admin/alerts/page.tsx +++ b/src/app/admin/alerts/page.tsx @@ -1,8 +1,12 @@ import { sendHotelAlert } from "@/actions/admin-alerts"; +import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; +const CRITICAL = new Set(["critical", "error", "danger"]); +const WARNING = new Set(["warning", "warn"]); + // Read-only feed of alert_logs (prisma.alertLogs delegate): the emulator and // CMS write rows here when hotel/user alerts fire. Auto-gated by the admin // layout (requireStaff); only the mutation re-asserts staff in the action. @@ -64,6 +68,10 @@ export default async function AdminAlerts() { alerts = []; } + const critical = alerts.filter((a) => CRITICAL.has((a.severity || "").toLowerCase())).length; + const warnings = alerts.filter((a) => WARNING.has((a.severity || "").toLowerCase())).length; + const unread = alerts.filter((a) => !a.isRead).length; + return (

Alerts

@@ -71,6 +79,28 @@ export default async function AdminAlerts() { Read-only ยท 100 most recent alert_logs (newest first).

+
+ + 0 ? "danger" : "ok"} + icon="๐Ÿšจ" + /> + 0 ? "warn" : "neutral"} + icon="โš ๏ธ" + /> + 0 ? "warn" : "ok"} + icon="๐Ÿ“จ" + /> +
+

Send hotel alert

diff --git a/src/app/admin/applications/page.tsx b/src/app/admin/applications/page.tsx index a5b276dfbf..4d782681de 100644 --- a/src/app/admin/applications/page.tsx +++ b/src/app/admin/applications/page.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import { dismissApplication } from "@/actions/admin-applications"; +import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -13,7 +14,7 @@ type ApplicationRow = { }; function formatDate(d: Date | null): string { - return d ? d.toISOString().slice(0, 16).replace("T", " ") : ""; + return d ? d.toISOString().slice(0, 16).replace("T", " ") : "โ€”"; } export default async function AdminApplications() { @@ -45,47 +46,62 @@ export default async function AdminApplications() { return (

Staff Applications

+

+ Pending applications from players requesting a staff rank. +

- {applications.length === 0 ? ( -

No pending applications.

- ) : ( -
- {applications.map((a) => { - const username = userMap.get(a.userId); - return ( -
-
-

- {username ? ( - {username} - ) : ( - <>User #{a.userId} - )} -

- Rank {a.rankId} -
-

- {formatDate(a.createdAt)} -

-

{a.content}

- - - - -
- ); - })} -
- )} +
+ 0 ? "warn" : "ok"} + icon="๐Ÿ“จ" + /> +
+ +
+

Applications ({applications.length})

+ {applications.length === 0 ? ( +

No pending applications.

+ ) : ( +
+ {applications.map((a) => { + const username = userMap.get(a.userId); + return ( +
+
+

+ {username ? ( + {username} + ) : ( + <>User #{a.userId} + )} +

+ Rank {a.rankId} +
+

+ {formatDate(a.createdAt)} +

+

{a.content}

+
+ + +
+
+ ); + })} +
+ )} +
); } diff --git a/src/app/admin/articles/page.tsx b/src/app/admin/articles/page.tsx index a3acd7f00c..edaa32ae0a 100644 --- a/src/app/admin/articles/page.tsx +++ b/src/app/admin/articles/page.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -14,32 +15,58 @@ export default async function AdminArticles() { }) .catch(() => []); + const latest = articles[0]?.createdAt + ? articles[0].createdAt.toISOString().slice(0, 10) + : "โ€”"; + return (

Articles

- + New article + + + New article +
- - - - - - - - - - {articles.map((a) => ( - - - - - - ))} - -
TitleSlugCreated
- {a.title} - {a.slug}{a.createdAt ? a.createdAt.toISOString().slice(0, 10) : ""}
+

+ News articles (website_articles) shown on the public site. 50 most recent. +

+ +
+ + +
+ +
+

Recent articles

+ {articles.length === 0 ? ( +

No articles yet.

+ ) : ( + + + + + + + + + + {articles.map((a) => ( + + + + + + ))} + +
TitleSlugCreated
+ {a.title} + + {a.slug} + + {a.createdAt ? a.createdAt.toISOString().slice(0, 10) : "โ€”"} +
+ )} +
); } diff --git a/src/app/admin/bans/page.tsx b/src/app/admin/bans/page.tsx index 1b53def2c1..ef5d709dad 100644 --- a/src/app/admin/bans/page.tsx +++ b/src/app/admin/bans/page.tsx @@ -1,8 +1,13 @@ import { createBan, liftBan } from "@/actions/admin-bans"; +import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; +function fromUnix(ts: number): string { + return new Date(ts * 1000).toISOString().slice(0, 16).replace("T", " "); +} + export default async function AdminBans() { const now = Math.floor(Date.now() / 1000); let bans: Awaited> = []; @@ -16,9 +21,32 @@ export default async function AdminBans() { bans = []; } + // banExpire of 0 means permanent in the emulator schema. + const permanent = bans.filter((b) => b.banExpire === 0).length; + const accountBans = bans.filter((b) => b.type === "account").length; + return (

Bans

+

+ Active bans only (expiry in the future) ยท newest first. +

+ +
+ 0 ? "warn" : "ok"} + icon="๐Ÿ”จ" + /> + 0 ? "danger" : "neutral"} + icon="โ›”" + /> + +

New ban

@@ -32,38 +60,60 @@ export default async function AdminBans() { - +
- - - - - - - - - - - {bans.map((b) => ( - - - - - - - - ))} - -
User IDTypeReasonExpires -
{b.userId}{b.type}{b.banReason}{new Date(b.banExpire * 1000).toISOString().slice(0, 16).replace("T", " ")} -
- - -
-
- {bans.length === 0 ?

No active bans.

: null} +
+

Active bans ({bans.length})

+ {bans.length === 0 ? ( +

No active bans.

+ ) : ( +
+ + + + + + + + + + + {bans.map((b) => ( + + + + + + + + ))} + +
User IDTypeReasonExpires +
{b.userId} + {b.type} + {b.banReason || "โ€”"} + {b.banExpire === 0 ? ( + permanent + ) : ( + + {fromUnix(b.banExpire)} + + )} + +
+ + +
+
+
+ )} +
); } diff --git a/src/app/admin/calendar/page.tsx b/src/app/admin/calendar/page.tsx index 0244f79704..8ecc5423bf 100644 --- a/src/app/admin/calendar/page.tsx +++ b/src/app/admin/calendar/page.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -39,10 +40,12 @@ export default async function AdminCalendar() { // Tally reward counts per campaign in JS (avoid prisma.groupBy). const rewardCounts = new Map(); + let totalRewards = 0; try { const rewards = await prisma.calendarRewards.findMany({ select: { campaignId: true }, }); + totalRewards = rewards.length; for (const r of rewards) { rewardCounts.set(r.campaignId, (rewardCounts.get(r.campaignId) ?? 0) + 1); } @@ -50,19 +53,32 @@ export default async function AdminCalendar() { // leave map empty on failure } + const enabledCount = campaigns.filter((c) => c.enabled === "1").length; + return (

Calendar campaigns

-

+

Read-only ยท {campaigns.length} campaign{campaigns.length === 1 ? "" : "s"}.

+
+ + 0 ? "ok" : "neutral"} + icon="โœ…" + /> + +
+ {campaigns.length === 0 ? (

No calendar campaigns found.

) : (
{campaigns.map((c) => { - const disabled = c.enabled !== "1"; + const enabled = c.enabled === "1"; const count = rewardCounts.get(c.id) ?? 0; return ( )}
-

{c.name || `Campaign #${c.id}`}

+
+

{c.name || `Campaign #${c.id}`}

+ + {enabled ? "enabled" : "disabled"} + +

#{c.id} ยท {c.totalDays} day{c.totalDays === 1 ? "" : "s"} ยท {count} reward {count === 1 ? "" : "s"}

Starts {fmtDate(c.startTimestamp)} - {disabled ? " ยท Disabled" : ""} {c.lockExpired === "1" ? " ยท Locks expired" : ""}

diff --git a/src/app/admin/catalog/page.tsx b/src/app/admin/catalog/page.tsx index a56991413c..a10eefeb88 100644 --- a/src/app/admin/catalog/page.tsx +++ b/src/app/admin/catalog/page.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -39,58 +40,83 @@ export default async function AdminCatalog() { const captionById = new Map(); for (const p of pages) captionById.set(p.id, p.caption); + const visibleCount = pages.filter( + (p) => p.visible === "1" && p.enabled === "1", + ).length; + const hiddenCount = pages.length - visibleCount; + const rootCount = pages.filter((p) => p.parentId <= 0).length; + return (

Catalog

-

Browse catalog pages and the items they contain. Read-only.

- - {pages.length === 0 ? ( -

No catalog pages found.

- ) : ( - - - - - - - - - - - - - - {pages.map((p) => { - const parentName = - p.parentId > 0 ? captionById.get(p.parentId) : null; - const hidden = p.visible !== "1" || p.enabled !== "1"; - return ( - - - - - - - - - - ); - })} - -
IDCaptionParentLayoutMin rankOrderStatus
{p.id} - {p.caption} - - {p.parentId <= 0 - ? "โ€”" - : parentName - ? `${parentName} (#${p.parentId})` - : `#${p.parentId}`} - {p.pageLayout}{p.minRank}{p.orderNum}{hidden ? "Hidden" : "Visible"}
- )} - -

- {pages.length} page{pages.length === 1 ? "" : "s"} +

+ Browse catalog pages and the items they contain. Read-only.

+ +
+ + + 0 ? "warn" : "neutral"} + icon="๐Ÿšซ" + /> + +
+ +
+

Pages ({pages.length})

+ {pages.length === 0 ? ( +

No catalog pages found.

+ ) : ( +
+ + + + + + + + + + + + + + {pages.map((p) => { + const parentName = + p.parentId > 0 ? captionById.get(p.parentId) : null; + const hidden = p.visible !== "1" || p.enabled !== "1"; + return ( + + + + + + + + + + ); + })} + +
IDCaptionParentLayoutMin rankOrderStatus
{p.id} + {p.caption} + + {p.parentId <= 0 + ? "โ€”" + : parentName + ? `${parentName} (#${p.parentId})` + : `#${p.parentId}`} + {p.pageLayout}{p.minRank}{p.orderNum} + + {hidden ? "Hidden" : "Visible"} + +
+
+ )} +
); } diff --git a/src/app/admin/emulator/page.tsx b/src/app/admin/emulator/page.tsx index 29205bc915..c1e193c3ca 100644 --- a/src/app/admin/emulator/page.tsx +++ b/src/app/admin/emulator/page.tsx @@ -1,4 +1,5 @@ import { updateEmulatorSetting, updateEmulatorText } from "@/actions/admin-emulator"; +import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -40,8 +41,16 @@ export default async function AdminEmulator() { on the next emulator read (some keys require an emulator reload).

-

Settings

-

{settings.length} keys.

+
+ + +
+ +
+

Settings

+

+ {settings.length} keys. +

No emulator settings found.

) : null} +
-

Texts

-

- {textsCount} keys{textsTruncated ? ` (showing first ${texts.length})` : ""}. -

+
+

Texts

+

+ {textsCount} keys{textsTruncated ? ` (showing first ${texts.length})` : ""}. +

) : null} +
); } diff --git a/src/app/admin/help-questions/page.tsx b/src/app/admin/help-questions/page.tsx index 12e8e2a058..2958024fa7 100644 --- a/src/app/admin/help-questions/page.tsx +++ b/src/app/admin/help-questions/page.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -16,44 +17,65 @@ export default async function AdminHelpQuestions() { entries = []; } + const smallBoxes = entries.filter((e) => e.smallBox).length; + const withButton = entries.filter((e) => e.buttonText).length; + return (

Help Center

- + New entry + + + New entry +
-

+

Help-center FAQ entries (website_help_center_categories). Shown to visitors on the public help page, ordered by position.

- - - - - - - - - - - {entries.map((e) => ( - - - - - - - - ))} - -
Pos.NameButtonSmall box -
{e.position} - {e.name} - {e.buttonText || "โ€”"}{e.smallBox ? "Yes" : "No"} - Edit -
- {entries.length === 0 ?

No help-center entries yet.

: null} +
+ + + +
+ +
+

Entries

+ {entries.length === 0 ? ( +

No help-center entries yet.

+ ) : ( + + + + + + + + + + + {entries.map((e) => ( + + + + + + + + ))} + +
Pos.NameButtonSmall box +
{e.position} + {e.name} + {e.buttonText || "โ€”"} + + {e.smallBox ? "Yes" : "No"} + + + Edit +
+ )} +
); } diff --git a/src/app/admin/housekeeping/page.tsx b/src/app/admin/housekeeping/page.tsx index 1dad9ed4ff..2996f9f46c 100644 --- a/src/app/admin/housekeeping/page.tsx +++ b/src/app/admin/housekeeping/page.tsx @@ -1,4 +1,5 @@ import { deletePermission, upsertPermission } from "@/actions/admin-housekeeping"; +import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -15,65 +16,91 @@ export default async function AdminHousekeeping() { permissions = []; } + const ranks = permissions.map((p) => p.minRank); + const maxRank = ranks.length ? Math.max(...ranks) : 0; + return (

Housekeeping Permissions

-

+

Minimum rank required to access each housekeeping permission key. Saving an existing key overwrites its min rank and description.

- -

Add / overwrite permission

-
- - - - -
- +
+ + +
- - - - - - - - - - {permissions.map((p) => ( - - - - - - - ))} - -
PermissionDescriptionMin rank -
{p.permission}{p.description ?? "โ€”"} -
- - - - -
-
-
- - -
-
- {permissions.length === 0 ? ( -

No housekeeping permissions defined yet.

- ) : null} +
+

Add / overwrite permission

+
+
+ + + + +
+
+
+ +
+

+ Permissions {permissions.length} +

+ {permissions.length === 0 ? ( +

No housekeeping permissions defined yet.

+ ) : ( +
+ + + + + + + + + + {permissions.map((p) => ( + + + + + + + ))} + +
PermissionDescriptionMin rank +
+ {p.permission} + {p.description ?? "โ€”"} +
+ + + + +
+
+
+ + +
+
+
+ )} +
); } diff --git a/src/app/admin/ip/page.tsx b/src/app/admin/ip/page.tsx index f263dc18c0..acaea2168f 100644 --- a/src/app/admin/ip/page.tsx +++ b/src/app/admin/ip/page.tsx @@ -4,6 +4,7 @@ import { deleteBlacklist, deleteWhitelist, } from "@/actions/admin-ip"; +import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -16,7 +17,7 @@ type IpRow = { }; function formatDate(d: Date | null): string { - return d ? d.toISOString().slice(0, 16).replace("T", " ") : ""; + return d ? d.toISOString().slice(0, 16).replace("T", " ") : "โ€”"; } export default async function AdminIp() { @@ -44,10 +45,28 @@ export default async function AdminIp() { return (

IP Management

+

+ Allow or block IP addresses (and ASNs) at the website edge. +

+ +
+ 0 ? "ok" : "neutral"} + icon="โœ…" + /> + 0 ? "danger" : "neutral"} + icon="๐Ÿšซ" + /> +
- {/* โ”€โ”€ Whitelist โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} -
+ {/* โ”€โ”€ Whitelist โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} +

Whitelist

@@ -65,38 +84,43 @@ export default async function AdminIp() {
- - - - - - - - - - {whitelist.map((w) => ( - - - - - - - ))} - -
IPASNAdded -
{w.ipAddress}{w.asn ?? "โ€”"}{formatDate(w.createdAt)} -
- - -
-
- {whitelist.length === 0 ?

No whitelisted IPs.

: null} + {whitelist.length === 0 ? ( +

No whitelisted IPs.

+ ) : ( +
+ + + + + + + + + + {whitelist.map((w) => ( + + + + + + + ))} + +
IPASNAdded +
{w.ipAddress}{w.asn ?? "โ€”"}{formatDate(w.createdAt)} +
+ + +
+
+
+ )} - {/* โ”€โ”€ Blacklist โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} -
+ {/* โ”€โ”€ Blacklist โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} +

Blacklist

@@ -114,34 +138,39 @@ export default async function AdminIp() {
- - - - - - - - - - {blacklist.map((b) => ( - - - - - - - ))} - -
IPASNAdded -
{b.ipAddress}{b.asn ?? "โ€”"}{formatDate(b.createdAt)} -
- - -
-
- {blacklist.length === 0 ?

No blacklisted IPs.

: null} + {blacklist.length === 0 ? ( +

No blacklisted IPs.

+ ) : ( +
+ + + + + + + + + + {blacklist.map((b) => ( + + + + + + + ))} + +
IPASNAdded +
{b.ipAddress}{b.asn ?? "โ€”"}{formatDate(b.createdAt)} +
+ + +
+
+
+ )}
diff --git a/src/app/admin/logs/page.tsx b/src/app/admin/logs/page.tsx index d2c628898a..d7fb26c2d8 100644 --- a/src/app/admin/logs/page.tsx +++ b/src/app/admin/logs/page.tsx @@ -1,7 +1,10 @@ +import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; +const CRITICAL = new Set(["critical", "error", "danger"]); + // chatlogs_room and commandlogs are @@ignore'd in the schema (no PK), so Prisma // generates no delegate for them โ€” they are read via $queryRaw against the real // emulator columns. alert_logs has a proper delegate (prisma.alertLogs). @@ -76,6 +79,12 @@ export default async function AdminLogs() { Read-only ยท 50 most recent of each.

+
+ + + +
+

Room chat ({chatlogs.length})

{chatlogs.length === 0 ? ( @@ -131,7 +140,11 @@ export default async function AdminLogs() { {String(c.user_id)} {c.command} {c.params} - {c.succes === "yes" ? "โœ“" : "โœ—"} + + + {c.succes === "yes" ? "OK" : "FAIL"} + + ))} @@ -161,9 +174,19 @@ export default async function AdminLogs() { {fromDate(a.createdAt)} {a.type} - {a.severity} + + + {a.severity || "โ€”"} + + {a.message} - {a.isRead ? "โœ“" : "โ—‹"} + + + {a.isRead ? "read" : "new"} + + ))} diff --git a/src/app/admin/maintenance/page.tsx b/src/app/admin/maintenance/page.tsx index baf1599a32..2509058c79 100644 --- a/src/app/admin/maintenance/page.tsx +++ b/src/app/admin/maintenance/page.tsx @@ -1,4 +1,5 @@ import { saveMaintenance } from "@/actions/admin-maintenance"; +import { StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -34,111 +35,120 @@ export default async function AdminMaintenance() { return (

Maintenance Mode

-

+

While maintenance is enabled, only staff (and users at or above the minimum login rank) can log into the hotel. These values are stored in website_settings and read live by the login flow.

- {/* โ”€โ”€ Current state โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} -
-
+ {/* โ”€โ”€ Current state โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} +
+

Current state

+
+ + {enabled ? "ENABLED" : "disabled"} + +
+ + Maintenance is currently {enabled ? "ENABLED" : "disabled"}. + +

+ {enabled + ? `Only users with rank ${minRankValue} or higher can log in right now.` + : "The hotel is open to everyone."} +

+
+
+
+ {/* โ”€โ”€ Edit form โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} -
-

Settings

- - - -
+
+

Settings

+ - -
-
- -