From f48028f6ddd130e9cb6ad2a3e8ea9fb31fb81e4e Mon Sep 17 00:00:00 2001 From: TheRealToxicDev Date: Tue, 14 Jul 2026 00:58:49 -0600 Subject: [PATCH 1/2] fix: cleanup hub filter and sorting options --- .../Layouts/Dedicated/dedicated-hub.tsx | 144 ++++++------ .../ui/components/Layouts/Games/game-hub.tsx | 4 +- .../ui/components/Layouts/VPS/vps-hub.tsx | 219 ++++++++---------- packages/ui/components/ui/filter-chip.tsx | 38 +++ 4 files changed, 207 insertions(+), 198 deletions(-) create mode 100644 packages/ui/components/ui/filter-chip.tsx diff --git a/packages/ui/components/Layouts/Dedicated/dedicated-hub.tsx b/packages/ui/components/Layouts/Dedicated/dedicated-hub.tsx index 9446518..4a959c6 100644 --- a/packages/ui/components/Layouts/Dedicated/dedicated-hub.tsx +++ b/packages/ui/components/Layouts/Dedicated/dedicated-hub.tsx @@ -9,6 +9,7 @@ import { Network, Shield, Zap, + SlidersHorizontal, X, ArrowRight, ChevronDown, @@ -17,6 +18,7 @@ import { PackageX, } from "lucide-react" import { Button } from "@/packages/ui/components/ui/button" +import { Badge } from "@/packages/ui/components/ui/badge" import { Input } from "@/packages/ui/components/ui/input" import { Select, @@ -27,6 +29,7 @@ import { } from "@/packages/ui/components/ui/select" import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/packages/ui/components/ui/collapsible" import { PlanInfoRow } from "@/packages/ui/components/ui/plan-info-row" +import { FilterChipRow, FilterChip } from "@/packages/ui/components/ui/filter-chip" import Link from "next/link" import { cn } from "@/lib/utils" import type { DedicatedPlanSpec } from "@/packages/core/types/servers/dedicated" @@ -220,7 +223,8 @@ export function DedicatedHub({ plans }: DedicatedHubProps) { const [ram, setRam] = useState("ALL") const [priceMin, setPriceMin] = useState("") const [priceMax, setPriceMax] = useState("") - const [sort, setSort] = useState("default") + const [sort, setSort] = useState("price-asc") + const [filtersOpen, setFiltersOpen] = useState(false) // Derived from live plan data, not the server name — reliable regardless of naming. const availableRam = Array.from(new Set(plans.map((p) => p.ramGB))).sort((a, b) => a - b) @@ -253,8 +257,10 @@ export function DedicatedHub({ plans }: DedicatedHubProps) { return result })() - const hasActiveFilters = - hardware !== "ALL" || ram !== "ALL" || priceMin !== "" || priceMax !== "" || search !== "" + const activeFilterCount = [ + hardware !== "ALL", ram !== "ALL", priceMin !== "", priceMax !== "", + ].filter(Boolean).length + const hasActiveFilters = activeFilterCount > 0 || search !== "" function clearFilters() { setSearch("") @@ -262,7 +268,7 @@ export function DedicatedHub({ plans }: DedicatedHubProps) { setRam("ALL") setPriceMin("") setPriceMax("") - setSort("default") + setSort("price-asc") } return ( @@ -325,7 +331,7 @@ export function DedicatedHub({ plans }: DedicatedHubProps) { /> -
- setPriceMin(e.target.value)} - className="w-24 bg-card/30 border-border/50" - /> - - setPriceMax(e.target.value)} - className="w-24 bg-card/30 border-border/50" - /> -
- - {/* Hardware filter */} -
- {(["ALL", "amd", "intel"] as const).map((h) => ( - - ))} -
- + {hasActiveFilters && ( - {availableRam.map((r) => ( - - ))} - + + +
+ +
+ setPriceMin(e.target.value)} + className="w-24 h-7 bg-card/30 border-border/50 text-xs" + /> + + setPriceMax(e.target.value)} + className="w-24 h-7 bg-card/30 border-border/50 text-xs" + /> +
+
+ + + setRam("ALL")}>All RAM + {availableRam.map((r) => ( + setRam(ram === r ? "ALL" : r)}> + {r} GB + + ))} + + + + {(["ALL", "amd", "intel"] as const).map((h) => ( + setHardware(h)}> + {h === "ALL" ? "All Hardware" : h.toUpperCase()} + + ))} + +
+
+
{/* ── Plan grid ────────────────────────────────────────────────────── */} diff --git a/packages/ui/components/Layouts/Games/game-hub.tsx b/packages/ui/components/Layouts/Games/game-hub.tsx index 49c03c9..6f67ab8 100644 --- a/packages/ui/components/Layouts/Games/game-hub.tsx +++ b/packages/ui/components/Layouts/Games/game-hub.tsx @@ -177,7 +177,7 @@ interface GameHubProps { export function GameHub({ plans }: GameHubProps) { const [search, setSearch] = useState("") - const [sort, setSort] = useState("default") + const [sort, setSort] = useState("asc") const filtered = (() => { let result = [...plans] @@ -198,7 +198,7 @@ export function GameHub({ plans }: GameHubProps) { function clearFilters() { setSearch("") - setSort("default") + setSort("asc") } return ( diff --git a/packages/ui/components/Layouts/VPS/vps-hub.tsx b/packages/ui/components/Layouts/VPS/vps-hub.tsx index 18d4218..be328f5 100644 --- a/packages/ui/components/Layouts/VPS/vps-hub.tsx +++ b/packages/ui/components/Layouts/VPS/vps-hub.tsx @@ -28,6 +28,7 @@ import { } from "@/packages/ui/components/ui/select" import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/packages/ui/components/ui/collapsible" import { PlanInfoRow } from "@/packages/ui/components/ui/plan-info-row" +import { FilterChipRow, FilterChip } from "@/packages/ui/components/ui/filter-chip" import Link from "next/link" import { cn } from "@/lib/utils" import type { VpsPlanSpec } from "@/packages/core/types/servers/vps" @@ -258,7 +259,8 @@ export function VpsHub({ plans }: VpsHubProps) { const [ram, setRam] = useState("ALL") const [priceMin, setPriceMin] = useState("") const [priceMax, setPriceMax] = useState("") - const [sort, setSort] = useState("default") + const [sort, setSort] = useState("price-asc") + const [filtersOpen, setFiltersOpen] = useState(false) // Derive which lineups/series/RAM tiers actually exist in the plan list — // reliable even for plans whose names don't follow the LINEUP-SERIES-RAM @@ -297,9 +299,11 @@ export function VpsHub({ plans }: VpsHubProps) { return result })() - const hasActiveFilters = - lineup !== "ALL" || series !== "ALL" || hardware !== "ALL" || ram !== "ALL" || - priceMin !== "" || priceMax !== "" || search !== "" + const activeFilterCount = [ + lineup !== "ALL", series !== "ALL", hardware !== "ALL", ram !== "ALL", + priceMin !== "", priceMax !== "", + ].filter(Boolean).length + const hasActiveFilters = activeFilterCount > 0 || search !== "" function clearFilters() { setSearch("") @@ -309,7 +313,7 @@ export function VpsHub({ plans }: VpsHubProps) { setRam("ALL") setPriceMin("") setPriceMax("") - setSort("default") + setSort("price-asc") } return ( @@ -343,7 +347,7 @@ export function VpsHub({ plans }: VpsHubProps) { {/* ── Filter bar ───────────────────────────────────────────────────── */}
- {/* Search + Sort row */} + {/* Search + Sort + Filters toggle row */}
@@ -355,7 +359,7 @@ export function VpsHub({ plans }: VpsHubProps) { />
-
- setPriceMin(e.target.value)} - className="w-24 bg-card/30 border-border/50" - /> - - setPriceMax(e.target.value)} - className="w-24 bg-card/30 border-border/50" - /> -
+ {hasActiveFilters && (
- {/* Filter chips row */} -
- - Filter: - - - {/* RAM tier — derived from live plan data, always reliable regardless of naming */} - - {availableRam.map((r) => ( - - ))} - - - - {/* Hardware brand */} - {(["ALL", "amd", "intel", "arm"] as const).map((h) => ( - - ))} - - - - {/* Lineup chips */} - {(Object.entries(LINEUP_META) as [Lineup, typeof LINEUP_META[Lineup]][]).map(([key, meta]) => ( - availableLineups.includes(key) ? ( - - ) : null - ))} - - - - {/* Series chips — only show series that exist in the plans */} - {availableSeries.map((s) => { - const meta = SERIES_META[s] - return ( - - ) - })} -
+
+ + {/* ── Plan grid ────────────────────────────────────────────────────── */} diff --git a/packages/ui/components/ui/filter-chip.tsx b/packages/ui/components/ui/filter-chip.tsx new file mode 100644 index 0000000..9faca05 --- /dev/null +++ b/packages/ui/components/ui/filter-chip.tsx @@ -0,0 +1,38 @@ +import type { ReactNode } from "react" +import { cn } from "@/lib/utils" + +/** A labeled row of filter chips — label on the left (or stacked above on mobile), chips wrap on the right. */ +export function FilterChipRow({ label, children }: { label: string; children: ReactNode }) { + return ( +
+ {label} +
{children}
+
+ ) +} + +/** A single toggleable filter pill, used inside FilterChipRow. */ +export function FilterChip({ + active, + onClick, + children, +}: { + active: boolean + onClick: () => void + children: ReactNode +}) { + return ( + + ) +} From 9cda3f5aa8c360064c1dc927471e4bf577079d17 Mon Sep 17 00:00:00 2001 From: TheRealToxicDev Date: Tue, 14 Jul 2026 01:10:32 -0600 Subject: [PATCH 2/2] chore: prep for new release --- .github/SECURITY.md | 10 +++--- CHANGELOG.md | 61 ++++++++++++++++++++++++++++++++ package.json | 2 +- packages/core/lib/spec-parser.ts | 5 ++- 4 files changed, 71 insertions(+), 7 deletions(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 0da50e7..434037b 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -107,11 +107,11 @@ Please include: ## Version Support -| Version | Status | Support Until | -|---------|--------|---------------| -| 3.2.x | Current | TBD | -| 3.1.x | Security Only | TBD | -| < 3.1 | Unsupported | - | +| Version | Status | Support Until | +|---------|---------------|---------------| +| 3.7.x | Current | 3.8.x Release | +| 3.6.x | Security Only | ~ Aug 2026 | +| < 3.5 | Unsupported | End of life | **Note**: Versions older than the current major version may have limited security support. diff --git a/CHANGELOG.md b/CHANGELOG.md index 60b2fd6..5dbc4ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,67 @@ All notable changes to the NodeByte Hosting website will be documented in this f The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.7.0] - 2026-07-13 + +### Added + +- **Unified Game Server Hosting** — `/games` now shows one flat plan grid for every supported game instead of a separate curated page per game; game selection moves to a Paymenter checkout config option + - `packages/ui/components/Layouts/Games/game-hub.tsx` — new hub component (search, sort, plan grid) matching the VPS/Dedicated hub pattern + - `app/games/page.tsx` — rewritten to flatten `getGamePlans()` across every child of the Game Servers hub instead of rendering one curated card per category + - `GamePlanSpec.category` field added (which Paymenter category a plan came from, for search/de-dup — not used for grouping) + - `packages/core/constants/supported-games.ts` — manually maintained `SUPPORTED_GAMES` list shown as badges on the `/games` hero; there's no admin API to read Paymenter's config-option choices live, so this is hand-kept in sync with the checkout dropdown + - Removed: `/games/[slug]` dynamic route, `packages/core/products/catalog-config.ts`, `packages/core/constants/game/*` curated per-game content, and the 4 now-unused `Games` layout components (`game-hero`, `game-features`, `game-pricing`, `game-faq`) + +- **Partners & Sponsors Page** — new `/partners` page + - `packages/core/constants/partners.ts` — maintainable data file; entries have a `sponsor` / `partner` / `community` tier and an optional `category` badge (e.g. "Minecraft Server", "FiveM Community") + - `packages/ui/components/Layouts/Partners/partners-page.tsx` — hero, one section per tier (a tier with zero entries doesn't render at all rather than showing an empty state), CTA linking to the existing KB partner program docs + - Nav "Company" dropdown, footer "Company" column, and `sitemap.ts` all link to `/partners` + +- **Expandable "Server Info" panel** on every Game/VPS/Dedicated plan card — a "View Server Info" toggle reveals Setup Fee, CPU Model, Uplink, DDoS layers, Location, Databases, and Backups, all parsed live from the Paymenter product description + - `packages/ui/components/ui/plan-info-row.tsx` — shared label/value row, built on the existing Radix `Collapsible` primitive + - `getSetupFeeGBP` / `getSetupFeesMap` (`bytepay.ts`) — one-time setup fee per plan, mirroring the existing price helpers + - `location` / `databases` / `backups` extraction (`spec-parser.ts`) — e.g. "3x MySQL Databases", "Newcastle, UK or New York, US", "Automatic Daily Backups" + +- **VPS/Dedicated Hub — richer filtering & sorting** + - RAM tier filter chips, built live from the RAM values actually present in the plan list (reliable regardless of naming convention) + - Price range (min/max) filter + - Sort extended from price-only to price / RAM / storage / CPU cores, each ascending or descending; default sort is now price ascending on Games, VPS, and Dedicated + - `packages/ui/components/ui/filter-chip.tsx` — shared `FilterChipRow` / `FilterChip`, replacing duplicated chip markup in both hubs + - Filter bar redesigned: Search + Sort + a "Filters" toggle (with an active-filter-count badge) in one compact row; the actual filter chips live in a collapsible panel grouped into labeled rows (Price / RAM / Hardware / Lineup / Series) instead of one long wrapped cluster — considerably shorter on mobile + +- **Live node discovery** — `/nodes` now reads its node list from status.nodebyte.host's "Nodes" monitor group instead of a hardcoded array, so adding a node to the status page is enough for it to appear on the site + - `getNodeMonitorNames()` (`lib/status.ts`) — filters the status snapshot to the "Nodes" group, excluding aggregate "group"-type rollups + - `NODE_DISPLAY_OVERRIDES` (`constants/status-mapping.ts`) replaces `NODE_MONITOR_MAP` — a small optional table for location/hardware details the status API doesn't carry, keyed by monitor name + +### Changed + +- **Category hub discovery hardened** (`bytepay.ts`) + - `getCategoryHub` now falls back to substring matching (with a console warning) if no exact alias match is found, so a category rename in Paymenter degrades gracefully instead of silently emptying a whole section + - A hub always includes itself as a leaf alongside its real sub-categories, not only when it has none — fixes products filed directly on a hub (e.g. the new generic `GAME-BASE`/`GAME-PLUS` tiers, filed on "Game Servers" itself alongside its Minecraft/Rust/Hytale children) being invisible on the site + - `fetchWithRetry` — every billing panel request now has a 10s timeout and retries transient failures (429/5xx/network errors) with backoff, honouring `Retry-After`; `per_page` increased to reduce pagination round-trips; missing `BYTEPAY_HOST`/`BYTEPAY_TOKEN` now throws a clear error instead of building a URL like `undefined/api/...` + +- **`spec-parser.ts` rewritten to classify description lines by topic instead of matching one exact phrase shape** — tolerates much more variation in how a product description is worded + - Fixed a real bug along the way: `bulletLines()` wasn't splitting on `
  • ` boundaries, so line-based extraction was silently reading across bullet points in Paymenter's default `
    • ` description format + - CPU brand detection broadened: "Ryzen" no longer requires an "AMD" prefix, "Intel i9-9900K" no longer requires the word "Core" + - CPU core count now also catches "8 Dedicated/Pinned Physical Cores" (number not at the start of the bullet) via a bounded per-line pattern, without guessing wrong on CPU model numbers like "Ryzen™ 9 5900X" + - Storage detection catches "Local NVMe Storage" and similar phrasing lacking an explicit SSD/HDD/Disk keyword + - Location extraction handles both "Newcastle, UK" (short code) and "Falkenstein, Germany" (full country name) styles, triggered by a wider set of keywords (location/region/infrastructure/provisioned/hosted/data centre) + +- **Nav Services dropdown simplified** to 3 flat links (Game Servers / VPS Servers / Dedicated Servers), replacing the old sectioned mega-menu with a per-game submenu; the now-dead `gamesNav` prop chain (`app/layout.tsx` → `layout-chrome.tsx` → `navigation.tsx`) was removed along with the billing-panel fetch it required on every page load + +- **Dropped-product alerting** — `getGamePlans` / `getVpsPlans` / `getDedicatedPlans` now log a warning when a live, non-hidden product fails to parse its required specs (instead of silently vanishing from the site), and the dev-only `/api/billing-debug` route lists every such product with its missing fields + +### Fixed + +- **Plan card layout bug** — `h-full` on plan cards was silently fighting the grid's `items-start` sizing, clipping the "Order Now" button on a collapsed sibling card whenever another card's Server Info panel was expanded; removed, cards now size purely to their own content +- Card grids no longer visually stretch every card in a row to match the tallest (expanded) one — `items-start` added to the Game/VPS/Dedicated plan grids + +### Security + +- **Double-unescaping in `spec-parser.ts`'s HTML entity decoding** (CodeQL `js/double-escaping`) — `bulletLines()` unescaped `&` → `&` *before* `>`/`<`/`"`/`'`, so a properly double-escaped literal like `&gt;` (meant to render as the text `>`) was silently corrupted into `>` instead. Fixed by unescaping `&` last, matching the standard safe-unescaping order — the escape character itself must always be resolved after every other entity that depends on it. + +--- + ## [3.6.0] - 2026-07-04 ### Added diff --git a/package.json b/package.json index e49e1de..368ea60 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@nodebyte/hosting-site", "description": "The official website for NodeByte Hosting.", "license": "AGPL-3.0-only", - "version": "3.5.4", + "version": "3.7.0", "scripts": { "build": "next build", "dev": "next dev", diff --git a/packages/core/lib/spec-parser.ts b/packages/core/lib/spec-parser.ts index f9943b0..d9cbddb 100644 --- a/packages/core/lib/spec-parser.ts +++ b/packages/core/lib/spec-parser.ts @@ -51,14 +51,17 @@ function bulletLines(html: string): string[] { const withBreaks = html .replace(/<\/(li|p|div|h[1-6])>/gi, "\n") .replace(//gi, "\n") + // Unescape "&" last — it must never run before the other entities, or + // e.g. a literal "&gt;" in the source (an escaped ampersand followed by + // "gt;") would double-unescape into ">" instead of the correct ">". const text = withBreaks .replace(/<[^>]*>/g, " ") - .replace(/&/g, "&") .replace(/>/g, ">") .replace(/</g, "<") .replace(/"/g, '"') .replace(/�?39;/g, "'") .replace(/ /g, " ") + .replace(/&/g, "&") .replace(/[ \t]+/g, " ") return text .split(/[•\n]/)