diff --git a/app/games/[slug]/page.tsx b/app/games/[slug]/page.tsx deleted file mode 100644 index 5b46079..0000000 --- a/app/games/[slug]/page.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import type { Metadata } from "next" -import { notFound } from "next/navigation" -import { Blocks, Gamepad2, Sparkles, Leaf, Pickaxe, Wrench } from "lucide-react" -import { getTranslations } from "next-intl/server" -import { GameHero } from "@/packages/ui/components/Layouts/Games/game-hero" -import { GameFeatures } from "@/packages/ui/components/Layouts/Games/game-features" -import { GamePricing } from "@/packages/ui/components/Layouts/Games/game-pricing" -import { GameFAQ } from "@/packages/ui/components/Layouts/Games/game-faq" -import { getGamePlans } from "@/packages/core/products/billing-service" -import { resolveGameDisplayConfig } from "@/packages/core/products/catalog-config" -import { getCategoryHub } from "@/packages/core/lib/bytepay" -import { GAME_HUB_SLUGS } from "@/packages/core/constants/catalog-hubs" - -const HEADER_ICON_MAP = { Blocks, Gamepad2, Sparkles, Leaf, Pickaxe, Wrench } - -interface PageProps { - params: Promise<{ slug: string }> -} - -async function resolveCategory(slug: string) { - const hub = await getCategoryHub(GAME_HUB_SLUGS) - return hub?.children.find((c) => c.slug === slug) ?? null -} - -export async function generateStaticParams() { - const hub = await getCategoryHub(GAME_HUB_SLUGS) - return (hub?.children ?? []).map((c) => ({ slug: c.slug })) -} - -export async function generateMetadata({ params }: PageProps): Promise { - const { slug } = await params - const category = await resolveCategory(slug) - if (!category) return {} - - const config = resolveGameDisplayConfig(category) - return { - title: `${config.name} Server Hosting`, - description: config.description, - } -} - -export default async function GameCategoryPage({ params }: PageProps) { - const { slug } = await params - const category = await resolveCategory(slug) - if (!category) notFound() - - const t = await getTranslations() - const config = resolveGameDisplayConfig(category) - const rawPlans = await getGamePlans(slug) - - const plans = rawPlans.map((plan) => { - const display = config.resolvePlanDisplay(plan) - return { - name: display.name, - description: display.description, - priceGBP: plan.priceGBP, - prices: plan.prices, - period: t("pricing.perMonth"), - popular: plan.popular, - location: plan.location, - stock: plan.stock, - features: display.features, - url: plan.url, - } - }) - - const HeaderIcon = HEADER_ICON_MAP[config.iconName] - - return ( - <> - - } - headerGradient={config.headerGradient} - headerIconBg={config.headerIconBg} - /> - - - - ) -} diff --git a/app/games/page.tsx b/app/games/page.tsx index 5a5b9cc..59d75c4 100644 --- a/app/games/page.tsx +++ b/app/games/page.tsx @@ -1,176 +1,21 @@ -import { Card } from "@/packages/ui/components/ui/card" -import { Button } from "@/packages/ui/components/ui/button" -import { GamePrice } from "@/packages/ui/components/ui/game-price" -import { Gamepad2, ArrowRight, Check, Blocks, Sparkles, Leaf, Pickaxe, Wrench } from "lucide-react" -import type { LucideIcon } from "lucide-react" -import Image from "next/image" -import Link from "next/link" -import { getTranslations } from "next-intl/server" -import { cn } from "@/lib/utils" import type { Metadata } from "next" +import { GameHub } from "@/packages/ui/components/Layouts/Games/game-hub" +import { getGamePlans } from "@/packages/core/products/billing-service" import { getCategoryHub } from "@/packages/core/lib/bytepay" import { GAME_HUB_SLUGS } from "@/packages/core/constants/catalog-hubs" -import { getGamePlans } from "@/packages/core/products/billing-service" -import { resolveGameDisplayConfig } from "@/packages/core/products/catalog-config" -const ICON_MAP: Record = { Blocks, Gamepad2, Sparkles, Leaf, Pickaxe, Wrench } - -export async function generateMetadata(): Promise { - const t = await getTranslations() - return { - title: `${t("gamesPage.title")}`, - description: t("gamesPage.description"), - } +export const metadata: Metadata = { + title: "Game Server Hosting", + description: + "One set of plans for every game we support. Instant setup, enterprise DDoS protection, NVMe SSD storage, and 24/7 support — pick your game at checkout.", } export default async function GamesPage() { - const t = await getTranslations() const hub = await getCategoryHub(GAME_HUB_SLUGS) const children = hub?.children ?? [] - const games = await Promise.all( - children.map(async (category) => { - const config = resolveGameDisplayConfig(category) - const plans = await getGamePlans(category.slug) - const comingSoon = plans.length === 0 - const startingPriceGBP = comingSoon ? 0 : Math.min(...plans.map((p) => p.priceGBP)) - return { - slug: category.slug, - name: config.name, - description: config.description, - banner: config.banner, - tag: config.tag, - tagColor: config.tagColor, - icon: ICON_MAP[config.iconName], - features: config.heroFeatures, - comingSoon, - startingPriceGBP, - } - }), - ) - - return ( -
- {/* Background */} -
-
- - {/* Animated orbs */} -
-
-
-
- -
- {/* Header */} -
-
- - {t("games.badge")} -
-

- {t("games.title")}{" "} - - {t("games.titleHighlight")} - -

-

- {t("games.description")} -

-
- - {/* Games Grid */} -
- {games.map((game) => ( - - {/* Banner */} -
- {game.name} -
- - {/* Tag */} -
- {game.tag} -
- - {/* Icon */} -
- -
-
- -
- {/* Title & Price */} -
-

{game.name}

- {game.comingSoon ? ( - - Coming Soon - - ) : game.startingPriceGBP ? ( - - ) : null} -
- -

{game.description}

- - {/* Features */} -
    - {game.features.map((feature) => ( -
  • - - {feature} -
  • - ))} -
- - {/* CTA */} - -
- - ))} -
+ const plansByCategory = await Promise.all(children.map((c) => getGamePlans(c.slug))) + const plans = plansByCategory.flat() - {/* Bottom CTA */} -
-

- {t("games.customSolution")}{" "} - - {t("games.contactUs")} - {" "} - {t("games.forCustom")} -

-
-
-
- ) + return } diff --git a/app/sitemap.ts b/app/sitemap.ts index 8a2e2ad..59431ac 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -10,9 +10,6 @@ const staticRoutes: Array<{ }> = [ { path: "", priority: 1.0, changeFrequency: "weekly" }, { path: "/games", priority: 0.9, changeFrequency: "weekly" }, - { path: "/games/minecraft", priority: 0.9, changeFrequency: "weekly" }, - { path: "/games/rust", priority: 0.9, changeFrequency: "weekly" }, - { path: "/games/hytale", priority: 0.8, changeFrequency: "monthly" }, { path: "/vps", priority: 0.9, changeFrequency: "weekly" }, { path: "/vps/amd", priority: 0.9, changeFrequency: "weekly" }, { path: "/vps/intel", priority: 0.8, changeFrequency: "monthly" }, diff --git a/packages/core/constants/game/gmod.ts b/packages/core/constants/game/gmod.ts deleted file mode 100644 index 2b4c529..0000000 --- a/packages/core/constants/game/gmod.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** Static features shared across all Garry's Mod plans (for future use). */ -export const GMOD_PLAN_STATIC_FEATURES = [ - "AMD Ryzen™ 9 5900X", - "DDoS Protection", - "BytePanel", - "99.9% Uptime SLA", -] as const - -/** Inline feature data — maps directly to GameFeatures props. */ -export const GMOD_FEATURES = [ - { - title: "Workshop & Addon Support", - description: - "Full Steam Workshop integration lets you install and auto-download addons for your players at server start, keeping everyone in sync.", - icon: "Settings" as const, - highlights: [ - "Steam Workshop support", - "Addon auto-download", - "Collection support", - "Resource manager", - ], - }, - { - title: "Gamemode Flexibility", - description: - "Run any gamemode — DarkRP, TTT, Prophunt, Murder, Sandbox, and more. Switch gamemodes without reinstalling or rebuilding your server.", - icon: "Gamepad2" as const, - highlights: [ - "DarkRP & TTT ready", - "Any custom gamemode", - "Sandbox support", - "Easy config switching", - ], - }, - { - title: "MySQL Integration", - description: - "Integrated MySQL databases for data-heavy gamemodes like DarkRP. Store player data, inventories, and economy without needing a separate host.", - icon: "Server" as const, - highlights: [ - "Bundled MySQL databases", - "DarkRP optimised", - "Easy database access", - "Regular backups", - ], - }, - { - title: "DDoS Protection", - description: - "Enterprise-grade DDoS mitigation keeps your Garry's Mod server online and your community protected at all times.", - icon: "Shield" as const, - highlights: [ - "Always-on protection", - "Layer 3/4/7 filtering", - "Zero downtime", - "Global POPs", - ], - }, - { - title: "High Performance", - description: - "Low latency hardware chosen for the demanding addon and workshop workloads that Garry's Mod servers generate.", - icon: "Cpu" as const, - highlights: [ - "High-clock AMD CPUs", - "NVMe SSD storage", - "Low latency networking", - "DDR4 ECC memory", - ], - }, - { - title: "Full File Access", - description: - "Complete FTP and web-based file manager access. Upload custom addons, Lua scripts, sounds, and models without restrictions.", - icon: "Users" as const, - highlights: [ - "Web file manager", - "FTP access", - "Lua script support", - "No upload restrictions", - ], - }, -] as const - -/** Inline FAQ data — maps directly to GameFAQ props. */ -export const GMOD_FAQS = [ - { - question: "Can I install addons and gamemodes from the Steam Workshop?", - answer: - "Yes! Full Steam Workshop integration is included. You can add Workshop collections to your server config and addons will download automatically for your players on join.", - }, - { - question: "Which gamemodes are supported?", - answer: - "All gamemodes are supported — DarkRP, TTT, Prophunt, Murder, Sandbox, and any custom ones. You have full file access to install and configure whatever your community needs.", - }, - { - question: "Do you provide MySQL databases for DarkRP?", - answer: - "Yes. MySQL databases are included with our Garry's Mod plans so you can run DarkRP and other database-backed gamemodes without needing an external database host.", - }, - { - question: "Can I run multiple gamemodes or server instances?", - answer: - "Each plan covers one server instance. To run multiple gamemodes simultaneously, you would need separate plans for each server.", - }, - { - question: "Can I choose my server location?", - answer: - "Yes! You can select your preferred data centre location at checkout. We offer multiple locations across Europe and the Americas for the best latency.", - }, -] as const - -/** Static hero feature pills. */ -export const GMOD_HERO_FEATURES = [ - "Workshop Addons", - "DarkRP Ready", - "MySQL Included", - "DDoS Protection", -] as const - -/** Visual + display config for the Garry's Mod hosting page. */ -export const GMOD_CONFIG = { - name: "Garry's Mod", - description: - "High-performance Garry's Mod server hosting with full Steam Workshop support, MySQL integration, and enterprise DDoS protection. Built for any gamemode.", - banner: "/games/gmod.png", - tag: "Coming Soon", - /** String name matching a lucide-react icon — instantiate in the page component. */ - iconName: "Wrench" as const, - tagColor: "bg-orange-500/15 text-orange-400 border border-orange-500/20", - headerGradient: "from-orange-500/20 via-orange-500/10 to-primary/5", - headerIconBg: "bg-orange-500/10 text-orange-400", -} as const diff --git a/packages/core/constants/game/hytale.ts b/packages/core/constants/game/hytale.ts deleted file mode 100644 index 9b8197c..0000000 --- a/packages/core/constants/game/hytale.ts +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Hytale doesn't have translation keys yet — names/descriptions are stored - * here directly instead of being keyed through i18n. - */ -export const HYTALE_PLAN_DISPLAY = { - "hytale-starter": { - name: "Starter", - description: "Perfect for small communities and testing.", - }, - "hytale-standard": { - name: "Standard", - description: "Perfect for growing communities and performance.", - }, - "hytale-performance": { - name: "Performance", - description: "Perfect for large communities and high performance.", - }, -} as const satisfies Record - -/** - * Static features shared across all Hytale plans. - * The page appends the parametric RAM/storage strings built from each plan's spec. - */ -export const HYTALE_PLAN_STATIC_FEATURES = [ - "AMD Ryzen™ 9 5900X", - "10 MySQL Databases", - "DDoS Protection", - "BytePanel", - "99.9% Uptime SLA", -] as const - -/** Hytale doesn't have translation keys yet — feature data is stored inline. */ -export const HYTALE_FEATURES = [ - { - title: "Instant Setup", - description: - "Once purchased, the server will install instantly with a performance plugin for optimal performance.", - icon: "Zap" as const, - highlights: [ - "Fast install", - "Optimized configurations", - "Pre-built templates", - "Quick deployment", - ], - }, - { - title: "Mod Support", - description: - "Full support for Hytale's modding capabilities. Create and host your custom experiences.", - icon: "Settings" as const, - highlights: [ - "Custom mod support", - "Easy mod management", - "Auto-updates", - "Full file access", - ], - }, - { - title: "High Performance", - description: - "Enterprise grade hardware ready to deliver smooth gameplay for your Hytale community.", - icon: "Cpu" as const, - highlights: [ - "Latest gen CPUs", - "NVMe SSD storage", - "High-speed networking", - "Low latency", - ], - }, - { - title: "DDoS Protection", - description: - "Your server will be protected by enterprise-grade DDoS mitigation from day one.", - icon: "Shield" as const, - highlights: [ - "Always-on protection", - "Automated network filtering", - "Zero downtime", - "Global POPs", - ], - }, - { - title: "Global Data Centers", - description: - "Servers hosted in strategically located data centers for excellent latency wherever your players are.", - icon: "Globe" as const, - highlights: [ - "Multiple locations", - "Low latency routing", - "Premium network", - "Global coverage", - ], - }, - { - title: "24/7 Support", - description: - "Our expert support team will be ready to help you with any Hytale hosting questions.", - icon: "Server" as const, - highlights: [ - "24/7 availability", - "Game experts", - "Fast response times", - "Discord support", - ], - }, -] as const - -/** Hytale doesn't have translation keys yet — FAQ data is stored inline. */ -export const HYTALE_FAQS = [ - { - question: "What features are supported?", - answer: - "We support all Hytale server features including mods, custom worlds, and multiplayer.", - }, - { - question: "Will there be mod support?", - answer: "Yes, we support Hytale's modding capabilities.", - }, - { - question: "What regions will be available?", - answer: - "We'll offer Hytale hosting across multiple data center locations for low latency and great coverage wherever your players are.", - }, - { - question: "Can I choose my server location?", - answer: - "Yes! You can select your preferred data centre location at checkout. We offer multiple locations to ensure the best latency for you and your players.", - }, -] as const - -/** Static hero feature pills. */ -export const HYTALE_HERO_FEATURES = [ - "Mod Support", - "Custom Maps", - "DDoS Protection", - "24/7 Support", -] as const - -export const HYTALE_CONFIG = { - name: "Hytale", - description: - "Set out on an adventure built for both creation and play. Hytale blends the freedom of a sandbox with the momentum of an RPG: explore a procedurally generated world full of dungeons, secrets, and a variety of creatures, then shape it block by block.", - banner: "/games/hytale.png", - tag: "Early Access Game", - /** String name matching a lucide-react icon — instantiate in the page component */ - iconName: "Sparkles", - tagColor: "bg-amber-500/15 text-amber-400 border border-amber-500/20", - headerGradient: "from-amber-500/20 via-amber-500/10 to-primary/5", - headerIconBg: "bg-amber-500/10 text-amber-400", -} as const diff --git a/packages/core/constants/game/index.ts b/packages/core/constants/game/index.ts deleted file mode 100644 index b707dc6..0000000 --- a/packages/core/constants/game/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./minecraft" -export * from "./rust" -export * from "./hytale" -export * from "./terraria" -export * from "./gmod" -export * from "./palworld" diff --git a/packages/core/constants/game/minecraft.ts b/packages/core/constants/game/minecraft.ts deleted file mode 100644 index 703706f..0000000 --- a/packages/core/constants/game/minecraft.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Minecraft doesn't need translation keys — plan/feature/FAQ copy is stored - * here directly, same pattern as Hytale/Terraria/Gmod/Palworld. Plan-level - * content always prefers the live billing panel data first; this is only a - * fallback for the 5 originally-curated plan ids. - */ -export const MINECRAFT_PLAN_DISPLAY = { - ember: { name: "Ember", description: "Perfect for small servers and testing" }, - blaze: { name: "Blaze", description: "Great for growing communities" }, - inferno: { name: "Inferno", description: "Ideal for medium sized communities" }, - firestorm: { name: "Firestorm", description: "Built for large and active communities" }, - supernova: { name: "Supernova", description: "Maximum power for massive servers" }, -} as const satisfies Record - -/** Static features shared across all Minecraft plans; RAM/storage are appended parametrically per plan. */ -export const MINECRAFT_PLAN_STATIC_FEATURES = [ - "High Performance CPU", - "10 MySQL Databases", - "DDoS Protection", - "BytePanel", - "Auto/Pre Installed Jars", - "99.6% Uptime SLA", -] as const - -export const MINECRAFT_FEATURES = [ - { - title: "One-Click Mod Loaders", - description: "Install Forge, Fabric, Paper, Spigot, and more with a single click from our control panel.", - icon: "Settings" as const, - highlights: ["Forge & Fabric support", "Paper & Spigot servers", "Bukkit compatibility", "Custom JAR uploads"], - }, - { - title: "High Performance Hardware", - description: "Enterprise grade processors with NVMe storage for blazing fast performance.", - icon: "Cpu" as const, - highlights: ["High performance CPUs", "NVMe SSD storage", "DDR4 ECC memory", "High clock speed processors"], - }, - { - title: "DDoS Protection", - description: "Enterprise grade DDoS mitigation keeps your server online even during the largest attacks.", - icon: "Shield" as const, - highlights: ["Layer 3/4/7 protection", "Enterprise network filtering", "Zero downtime mitigation", "Enterprise POPs"], - }, - { - title: "Instant Setup", - description: "Your server is deployed within seconds. Start playing immediately after purchase.", - icon: "Zap" as const, - highlights: ["Automated provisioning", "Pre configured settings", "Ready in under 60 seconds", "No technical knowledge needed"], - }, - { - title: "Full FTP Access", - description: "Complete file access via FTP/SFTP. Upload worlds, plugins, and configurations with ease.", - icon: "HardDrive" as const, - highlights: ["SFTP file access", "Web based file manager", "Drag & drop uploads", "Automatic backups"], - }, - { - title: "Unlimited Slots", - description: "No artificial player limits. Host as many players as your hardware can handle.", - icon: "Users" as const, - highlights: ["No slot restrictions", "Scalable resources", "Upgrade anytime", "Fair resource allocation"], - }, -] as const - -export const MINECRAFT_FAQS = [ - { - question: "What Minecraft versions do you support?", - answer: "We support all Minecraft versions from 1.7.10 to the latest release, including snapshots. Both Java Edition and Bedrock Edition servers are available.", - }, - { - question: "Can I install mods and plugins?", - answer: "Yes! We support all major mod loaders including Forge, Fabric, and NeoForge. For plugins, we support Paper, Spigot, Bukkit, and Purpur. You can also upload custom JARs.", - }, - { - question: "How do I upload my existing world?", - answer: "You can upload your world files via our web based file manager or through SFTP. Simply drag and drop your world folder and it will be ready to use.", - }, - { - question: "Is there a player limit?", - answer: "No, we don't impose artificial player limits. Your server can host as many players as your allocated resources can handle.", - }, - { - question: "Can I upgrade my plan later?", - answer: "Absolutely! You can upgrade or downgrade your plan at any time from our billing panel. Changes take effect immediately with no downtime.", - }, - { - question: "Do you offer refunds?", - answer: "Yes, we offer a 48 hour money back guarantee on all new purchases. If you're not satisfied, contact support for a full refund.", - }, - { - question: "Can I choose my server location?", - answer: "Yes! You can select your preferred data centre location at checkout. We offer multiple locations to ensure the best latency for you and your players.", - }, -] as const - -/** Static hero feature pills. */ -export const MINECRAFT_HERO_FEATURES = [ - "Forge & Fabric", - "Unlimited Players", - "DDoS Protection", - "24/7 Support", -] as const - -/** Visual + display config for the Minecraft hosting pages */ -export const MINECRAFT_CONFIG = { - name: "Minecraft", - description: - "High performance Minecraft server hosting with instant setup, one click mod loaders, and enterprise grade DDoS protection. Java & Bedrock support.", - banner: "/games/minecraft.png", - tag: "Most Popular", - /** String name matching a lucide-react icon — instantiate in the page component */ - iconName: "Blocks", - tagColor: "bg-primary/10 border border-primary/20 text-primary", - headerGradient: "from-primary/20 via-primary/10 to-accent/5", - headerIconBg: "bg-primary/10 text-primary", -} as const diff --git a/packages/core/constants/game/palworld.ts b/packages/core/constants/game/palworld.ts deleted file mode 100644 index 82addbd..0000000 --- a/packages/core/constants/game/palworld.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** Static features shared across all Palworld plans (for future use). */ -export const PALWORLD_PLAN_STATIC_FEATURES = [ - "AMD Ryzen™ 9 5900X", - "10 MySQL Databases", - "DDoS Protection", - "BytePanel", - "99.9% Uptime SLA", -] as const - -/** Inline feature data — maps directly to GameFeatures props. */ -export const PALWORLD_FEATURES = [ - { - title: "Custom World Configuration", - description: - "Take full control of your Palworld server settings — adjust spawn rates, difficulty, XP multipliers, and more to suit your community.", - icon: "Settings" as const, - highlights: [ - "Difficulty settings", - "Spawn rate control", - "XP & drop multipliers", - "Time & weather config", - ], - }, - { - title: "Pal Data Persistence", - description: - "Your Pals, bases, and world progress are kept safe with reliable save management and automated backups.", - icon: "HardDrive" as const, - highlights: [ - "Reliable save management", - "Automated backups", - "Easy save restores", - "World seed support", - ], - }, - { - title: "Mod Support", - description: - "Install community mods to expand your Palworld experience with new Pals, items, maps, and gameplay tweaks.", - icon: "Zap" as const, - highlights: [ - "Community mod support", - "Easy mod installation", - "Full file access", - "FTP & file manager", - ], - }, - { - title: "DDoS Protection", - description: - "Enterprise-grade DDoS mitigation keeps your Palworld server online and your community protected around the clock.", - icon: "Shield" as const, - highlights: [ - "Always-on protection", - "Layer 3/4/7 filtering", - "Zero downtime", - "Global POPs", - ], - }, - { - title: "High Performance", - description: - "Powerful hardware ensures smooth gameplay for all players, even during intense base raids or large Pal battles.", - icon: "Cpu" as const, - highlights: [ - "High-clock AMD CPUs", - "NVMe SSD storage", - "Low latency networking", - "DDR4 ECC memory", - ], - }, - { - title: "Auto Backups", - description: - "Automated world backups protect your Palworld progress. Restore to any backup point with a single click from the control panel.", - icon: "Server" as const, - highlights: [ - "Scheduled backups", - "One-click restore", - "Multiple restore points", - "Safe world management", - ], - }, -] as const - -/** Inline FAQ data — maps directly to GameFAQ props. */ -export const PALWORLD_FAQS = [ - { - question: "How many players can join my Palworld server?", - answer: - "Palworld dedicated servers support up to 32 players by default. Our plans are provisioned with enough resources to keep gameplay smooth for all your players.", - }, - { - question: "Can I install mods on my Palworld server?", - answer: - "Yes! You can install community mods via FTP or the file manager. We provide full file access so you can customise your Palworld experience however you like.", - }, - { - question: "Is my Pal and world data backed up automatically?", - answer: - "Absolutely. We run automated backups of your world saves on a regular schedule. You can restore to any backup point from within the control panel.", - }, - { - question: "Can I customise world settings like spawn rates and difficulty?", - answer: - "Yes! All Palworld server configuration options are available to you, including difficulty levels, Pal spawn rates, XP multipliers, drop rates, and more.", - }, - { - question: "Can I transfer my single-player save to my dedicated server?", - answer: - "Yes, it is possible to transfer a single-player world save to a dedicated server. Our support team can assist you with the process if needed.", - }, - { - question: "What version of Palworld will you support?", - answer: - "We will keep servers updated with the latest stable Palworld dedicated server releases and will notify you of any updates that require server restarts.", - }, - { - question: "Can I choose my server location?", - answer: - "Yes! You can select your preferred data centre location at checkout. We offer multiple locations to ensure the best latency for you and your players.", - }, -] as const - -/** Static hero feature pills. */ -export const PALWORLD_HERO_FEATURES = [ - "Custom World", - "Mod Support", - "Auto Backups", - "DDoS Protection", -] as const - -/** Visual + display config for the Palworld hosting page. */ -export const PALWORLD_CONFIG = { - name: "Palworld", - description: - "High-performance Palworld server hosting with full world customisation, mod support, automatic backups, and enterprise DDoS protection. The perfect home for your Pal-catching community.", - banner: "/games/palworld.png", - tag: "Coming Soon", - /** String name matching a lucide-react icon — instantiate in the page component. */ - iconName: "Leaf" as const, - tagColor: "bg-green-500/15 text-green-400 border border-green-500/20", - headerGradient: "from-green-500/20 via-green-500/10 to-primary/5", - headerIconBg: "bg-green-500/10 text-green-400", -} as const diff --git a/packages/core/constants/game/rust.ts b/packages/core/constants/game/rust.ts deleted file mode 100644 index c4698df..0000000 --- a/packages/core/constants/game/rust.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Rust doesn't need translation keys — plan/feature/FAQ copy is stored here - * directly, same pattern as Hytale/Terraria/Gmod/Palworld. Plan-level content - * always prefers the live billing panel data first; this is only a fallback - * for the 4 originally-curated plan ids. - */ -export const RUST_PLAN_DISPLAY = { - starter: { name: "Starter", description: "Recommended for 40 Players" }, - standard: { name: "Standard", description: "Recommended for 75 Players" }, - performance: { name: "Performance", description: "Recommended for 100 Players" }, - premium: { name: "Premium", description: "Recommended for 150+ Players" }, -} as const satisfies Record - -/** Static features shared across all Rust plans; RAM/storage are appended parametrically per plan. */ -export const RUST_PLAN_STATIC_FEATURES = [ - "High Performance CPU", - "DDoS Protection", - "Multiple Locations", - "10 MySQL Databases", - "BytePanel (GSM)", - "Oxide/Umod Supported", - "Rust+ Supported", - "99.6% Uptime SLA", -] as const - -export const RUST_FEATURES = [ - { - title: "Oxide/uMod Support", - description: "Full support for Oxide and uMod plugins. Install and manage plugins directly from our control panel.", - icon: "Settings" as const, - highlights: ["One-click Oxide install", "Plugin manager", "Auto updates available", "Permission management"], - }, - { - title: "Custom Maps", - description: "Use procedurally generated maps or upload your own custom maps. Full map customization support.", - icon: "Map" as const, - highlights: ["Procedural generation", "Custom map uploads", "Map size control", "Seed customization"], - }, - { - title: "High Performance", - description: "Rust demands powerful hardware. Our servers use high performance CPUs and NVMe storage for a smooth experience.", - icon: "Cpu" as const, - highlights: ["High performance CPUs", "NVMe SSD storage", "High single thread performance", "Low-latency networking"], - }, - { - title: "DDoS Protection", - description: "Enterprise grade DDoS mitigation through multiple network POPs protects your server from attacks 24/7.", - icon: "Shield" as const, - highlights: ["Enterprise network filtering", "Global POPs", "Layer 3/4/7 protection", "Zero downtime"], - }, - { - title: "Wipe Scheduler", - description: "Automated wipe scheduling to keep your server fresh. Configure weekly, bi-weekly, or monthly wipes.", - icon: "Zap" as const, - highlights: ["Automated wipes", "Blueprint wipe options", "Map wipe scheduling", "Discord notifications"], - }, - { - title: "Full RCON Access", - description: "Complete remote console access for server management. Execute commands from anywhere.", - icon: "Server" as const, - highlights: ["Web-based RCON", "Command scheduling", "Player management", "Real-time logs"], - }, -] as const - -export const RUST_FAQS = [ - { - question: "Do you support Oxide/uMod plugins?", - answer: "Yes! We fully support Oxide and uMod. You can install Oxide with one click from our control panel and manage plugins through our plugin manager or via FTP.", - }, - { - question: "Can I use custom maps?", - answer: "Absolutely! You can use procedurally generated maps with custom seeds and sizes, or upload your own custom map files via FTP.", - }, - { - question: "How does the wipe scheduler work?", - answer: "Our wipe scheduler lets you automate server wipes on a schedule you choose. You can configure map-only wipes or full blueprint wipes, and optionally send Discord notifications.", - }, - { - question: "What's the server tick rate?", - answer: "Our Rust servers run at the default 30 tick rate. Our high-performance hardware ensures consistent performance even with many players online.", - }, - { - question: "Can I access RCON?", - answer: "Yes, you get full RCON access. You can use our web-based RCON console or connect with any standard RCON client.", - }, - { - question: "Do you support modded servers?", - answer: "Yes, we support both vanilla and modded Rust servers. Install Oxide and add any plugins you need to create your perfect modded experience.", - }, - { - question: "Can I choose my server location?", - answer: "Yes! You can select your preferred data centre location at checkout. We offer multiple locations to ensure the best latency for you and your players.", - }, -] as const - -/** Static hero feature pills. */ -export const RUST_HERO_FEATURES = [ - "Oxide/uMod", - "Custom Maps", - "Wipe Scheduler", - "RCON Access", -] as const - -/** Visual + display config for the Rust hosting pages */ -export const RUST_CONFIG = { - name: "Rust", - description: - "High-performance Rust server hosting with Oxide/uMod support, custom maps, wipe scheduling, and enterprise-grade DDoS protection.", - banner: "/games/rust.png", - tag: "High Performance", - /** String name matching a lucide-react icon — instantiate in the page component */ - iconName: "Gamepad2", - tagColor: "bg-accent/10 border border-accent/20 text-accent", - headerGradient: "from-accent/20 via-accent/10 to-primary/5", - headerIconBg: "bg-accent/10 text-accent", -} as const diff --git a/packages/core/constants/game/terraria.ts b/packages/core/constants/game/terraria.ts deleted file mode 100644 index f01ead4..0000000 --- a/packages/core/constants/game/terraria.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** Static features shared across all Terraria plans (for future use). */ -export const TERRARIA_PLAN_STATIC_FEATURES = [ - "AMD Ryzen™ 9 5900X", - "DDoS Protection", - "BytePanel", - "99.9% Uptime SLA", -] as const - -/** Inline feature data — maps directly to GameFeatures props. */ -export const TERRARIA_FEATURES = [ - { - title: "Mod Support", - description: - "Full tModLoader support so you and your players can install and manage mods directly from the control panel with zero hassle.", - icon: "Settings" as const, - highlights: [ - "tModLoader ready", - "Mod manager", - "Auto-restart on crash", - "Easy mod updates", - ], - }, - { - title: "Custom World Generation", - description: - "Configure world size, difficulty, seed, and game mode at creation. Reset or create multiple worlds at any time with a single click.", - icon: "Map" as const, - highlights: [ - "Small / Medium / Large worlds", - "Custom seeds", - "Multiple worlds", - "Journey, Classic & Expert modes", - ], - }, - { - title: "Automatic Backups", - description: - "Scheduled world and character backups protect your progress. Restore to any snapshot with one click in case of accidents or corruption.", - icon: "Server" as const, - highlights: [ - "Scheduled backups", - "One-click restore", - "Multiple restore points", - "World file download", - ], - }, - { - title: "DDoS Protection", - description: - "Enterprise-grade DDoS mitigation keeps your Terraria server online and your adventurers safe at all times.", - icon: "Shield" as const, - highlights: [ - "Always-on protection", - "Layer 3/4/7 filtering", - "Zero downtime", - "Global POPs", - ], - }, - { - title: "High Performance", - description: - "Low latency hardware chosen for smooth Terraria workloads, keeping your world responsive even during boss events and large build projects.", - icon: "Cpu" as const, - highlights: [ - "High-clock AMD CPUs", - "NVMe SSD storage", - "Low latency networking", - "DDR4 ECC memory", - ], - }, - { - title: "Simple Management", - description: - "BytePanel gives you full control — start, stop, restart, manage files, view console output, and configure your server all in one place.", - icon: "Users" as const, - highlights: [ - "Web-based control panel", - "Live console", - "File manager", - "Player & ban management", - ], - }, -] as const - -/** Inline FAQ data — maps directly to GameFAQ props. */ -export const TERRARIA_FAQS = [ - { - question: "Does my server support tModLoader?", - answer: - "Yes! We fully support tModLoader so you can install and run mods directly. You can switch between vanilla and tModLoader from your control panel.", - }, - { - question: "How many players can join my Terraria server?", - answer: - "Terraria officially supports up to 255 simultaneous players. Our plans are sized to handle typical community servers comfortably.", - }, - { - question: "Can I create multiple worlds on one server?", - answer: - "Yes, you can store and switch between multiple world files. Simply upload your worlds via FTP or the file manager and select the active world in the config.", - }, - { - question: "Will my world be backed up automatically?", - answer: - "Yes. Automatic scheduled backups are included. You can restore to any snapshot from your control panel in case of corruption or accidental changes.", - }, - { - question: "Can I choose my server location?", - answer: - "Yes! You can select your preferred data centre location at checkout. We offer multiple locations across Europe and the Americas for the best latency.", - }, -] as const - -/** Static hero feature pills. */ -export const TERRARIA_HERO_FEATURES = [ - "tModLoader", - "Custom Worlds", - "Auto Backups", - "DDoS Protection", -] as const - -/** Visual + display config for the Terraria hosting page. */ -export const TERRARIA_CONFIG = { - name: "Terraria", - description: - "High-performance Terraria server hosting with full tModLoader support, custom world configuration, automatic backups, and enterprise DDoS protection. Built for adventurers.", - banner: "/games/terraria.png", - tag: "Coming Soon", - /** String name matching a lucide-react icon — instantiate in the page component. */ - iconName: "Pickaxe" as const, - tagColor: "bg-lime-500/15 text-lime-400 border border-lime-500/20", - headerGradient: "from-lime-500/20 via-lime-500/10 to-primary/5", - headerIconBg: "bg-lime-500/10 text-lime-400", -} as const diff --git a/packages/core/constants/partners.ts b/packages/core/constants/partners.ts index a46634b..c262da6 100644 --- a/packages/core/constants/partners.ts +++ b/packages/core/constants/partners.ts @@ -28,6 +28,14 @@ export interface PartnerEntry { } export const PARTNERS: PartnerEntry[] = [ + { + id: "fyfeweb", + name: "FyfeWeb", + tier: "partner", + logo: "/partners/fyfeweb.png", + url: "https://fyfeweb.com", + description: "From a single website to racks of your own hardware: one provider, one network, one support team.", + }, { id: "poliberry", name: "Poliberry", @@ -68,6 +76,15 @@ export const PARTNERS: PartnerEntry[] = [ url: "https://antiraid.xyz", description: "From basic moderation to advanced threat protection, AntiRaid handles it all so you can focus on growing your community.", }, + { + id: "fluxrp", + name: "Flux.LT", + tier: "community", + category: "FiveM Community", + logo: "/partners/fluxrp.png", + url: "https://discord.gg/XAKudt7C82", + description: "Lithuania's premier FiveM roleplay community, bringing immersive story-driven RP and a tight-knit Baltic player base together.", + }, { id: "smphub", name: "SMP Hub", diff --git a/packages/core/constants/vps/amd.ts b/packages/core/constants/vps/amd.ts index ffcedec..85eec72 100644 --- a/packages/core/constants/vps/amd.ts +++ b/packages/core/constants/vps/amd.ts @@ -14,6 +14,7 @@ export const AMD_PLANS: VpsPlanSpec[] = [ description: "Perfect for small projects, dev environments, and personal sites.", cpuModel: "AMD Ryzen™ 7 1700X", priceGBP: 4.50, + setupFeeGBP: 0, cpu: 1, ramGB: 2, storageGB: 25, @@ -31,6 +32,7 @@ export const AMD_PLANS: VpsPlanSpec[] = [ description: "Great for growing web apps, APIs, and small databases.", cpuModel: "AMD Ryzen™ 7 1700X", priceGBP: 9, + setupFeeGBP: 0, cpu: 2, ramGB: 4, storageGB: 50, @@ -48,6 +50,7 @@ export const AMD_PLANS: VpsPlanSpec[] = [ description: "Ideal for production workloads, game backends, and high-traffic sites.", cpuModel: "AMD Ryzen™ 7 1700X", priceGBP: 18, + setupFeeGBP: 0, cpu: 4, ramGB: 8, storageGB: 100, @@ -66,6 +69,7 @@ export const AMD_PLANS: VpsPlanSpec[] = [ description: "Maximum power for demanding applications and resource-heavy services.", cpuModel: "AMD Ryzen™ 7 1700X", priceGBP: 40, + setupFeeGBP: 0, cpu: 4, ramGB: 16, storageGB: 200, @@ -84,6 +88,7 @@ export const AMD_PLANS: VpsPlanSpec[] = [ description: "Maximum power for demanding applications and resource-heavy services.", cpuModel: "AMD Ryzen™ 7 1700X", priceGBP: 45, + setupFeeGBP: 0, cpu: 8, ramGB: 32, storageGB: 500, @@ -102,6 +107,7 @@ export const AMD_PLANS: VpsPlanSpec[] = [ description: "Maximum power for demanding applications and resource-heavy services.", cpuModel: "AMD Ryzen™ 7 1700X", priceGBP: 60, + setupFeeGBP: 0, cpu: 8, ramGB: 64, storageGB: 1000, diff --git a/packages/core/lib/bytepay.ts b/packages/core/lib/bytepay.ts index be34034..046377d 100644 --- a/packages/core/lib/bytepay.ts +++ b/packages/core/lib/bytepay.ts @@ -429,6 +429,30 @@ export function getPricesMap(product: BillingProduct): Record { return map } +/** Resolve the GBP one-time setup fee from the product's recurring plan (0 if none). */ +export function getSetupFeeGBP(product: BillingProduct): number { + const plan = + product.plans.find((p) => p.type === "recurring" && p.billingPeriod === 1 && p.billingUnit === "month") + ?? product.plans.find((p) => p.type === "recurring") + ?? product.plans[0] + if (!plan) return 0 + return plan.prices.find((p) => p.currencyCode === "GBP")?.setupFee ?? 0 +} + +/** Return a map of currency code → one-time setup fee from the product's recurring plan. */ +export function getSetupFeesMap(product: BillingProduct): Record { + const plan = + product.plans.find((p) => p.type === "recurring" && p.billingPeriod === 1 && p.billingUnit === "month") + ?? product.plans.find((p) => p.type === "recurring") + ?? product.plans[0] + if (!plan) return {} + const map: Record = {} + for (const price of plan.prices) { + map[price.currencyCode] = price.setupFee + } + return map +} + /** Map billing stock to the website StockStatus type. */ export function getStockStatus( product: BillingProduct, diff --git a/packages/core/products/billing-service.ts b/packages/core/products/billing-service.ts index a6d0064..de6535e 100644 --- a/packages/core/products/billing-service.ts +++ b/packages/core/products/billing-service.ts @@ -7,6 +7,8 @@ import { getProductsByCategory, getGbpPrice, getPricesMap, + getSetupFeeGBP, + getSetupFeesMap, getStockStatus, getBillingUrl, } from "@/packages/core/lib/bytepay" @@ -58,6 +60,7 @@ export async function getGamePlans(categorySlug: string): Promise popular: POPULAR_SLUGS.has(`${categorySlug}/${product.slug}`), priceGBP: getGbpPrice(product), prices: getPricesMap(product), + setupFeeGBP: getSetupFeeGBP(product), + setupFees: getSetupFeesMap(product), stock: getStockStatus(product), url: getBillingUrl(categorySlug, product.slug), } satisfies VpsPlanSpec, diff --git a/packages/core/products/catalog-config.ts b/packages/core/products/catalog-config.ts deleted file mode 100644 index a9e34a1..0000000 --- a/packages/core/products/catalog-config.ts +++ /dev/null @@ -1,247 +0,0 @@ -/** - * Resolves the display config + plan-card content for a game category page. - * - * Every game category discovered from Paymenter gets a working page for - * free (name/description from the category, specs parsed from the product - * description, generic marketing copy). The handful of games we've actually - * curated (banner art, hand-picked feature lists) override that generic - * output — see GAME_OVERRIDES below. Plan-level content (name/description) - * always prefers live billing data first; curated per-plan copy is only a - * fallback for the originally-curated plan ids. - */ - -import type { GamePlanSpec } from "@/packages/core/types/servers/game" -import type { CategoryInfo } from "@/packages/core/lib/bytepay" -import { LINKS } from "@/packages/core/constants/links" -import { - MINECRAFT_CONFIG, - MINECRAFT_PLAN_DISPLAY, - MINECRAFT_PLAN_STATIC_FEATURES, - MINECRAFT_FEATURES, - MINECRAFT_FAQS, - MINECRAFT_HERO_FEATURES, -} from "@/packages/core/constants/game/minecraft" -import { - RUST_CONFIG, - RUST_PLAN_DISPLAY, - RUST_PLAN_STATIC_FEATURES, - RUST_FEATURES, - RUST_FAQS, - RUST_HERO_FEATURES, -} from "@/packages/core/constants/game/rust" -import { - HYTALE_CONFIG, - HYTALE_PLAN_DISPLAY, - HYTALE_PLAN_STATIC_FEATURES, - HYTALE_FEATURES, - HYTALE_FAQS, - HYTALE_HERO_FEATURES, -} from "@/packages/core/constants/game/hytale" -import { - TERRARIA_CONFIG, - TERRARIA_FEATURES, - TERRARIA_FAQS, - TERRARIA_HERO_FEATURES, -} from "@/packages/core/constants/game/terraria" -import { - GMOD_CONFIG, - GMOD_FEATURES, - GMOD_FAQS, - GMOD_HERO_FEATURES, -} from "@/packages/core/constants/game/gmod" -import { - PALWORLD_CONFIG, - PALWORLD_FEATURES, - PALWORLD_FAQS, - PALWORLD_HERO_FEATURES, -} from "@/packages/core/constants/game/palworld" - -export type GameIconName = "Blocks" | "Gamepad2" | "Sparkles" | "Leaf" | "Pickaxe" | "Wrench" -export type FeatureIconName = - | "Settings" | "Cpu" | "Shield" | "Zap" | "HardDrive" | "Users" | "Server" | "Map" | "Globe" | "Sparkles" | "Gamepad2" - -export interface GamePageFeature { - title: string - description: string - icon: FeatureIconName - highlights: string[] -} - -export interface GameFaq { - question: string - answer: string -} - -export interface GamePlanDisplay { - name: string - description: string - features: string[] -} - -export interface GameDisplayConfig { - name: string - description: string - banner: string - iconName: GameIconName - tag: string - tagColor: string - headerGradient: string - headerIconBg: string - billingUrl: string - heroFeatures: string[] - pageFeatures: GamePageFeature[] - faqs: GameFaq[] - resolvePlanDisplay: (plan: GamePlanSpec) => GamePlanDisplay -} - -// ─── Generic fallback (any category with no curated override) ────────────── - -const GENERIC_ICON_ROTATION: GameIconName[] = ["Gamepad2", "Sparkles", "Blocks", "Pickaxe", "Wrench", "Leaf"] -const GENERIC_PALETTES = [ - { tagColor: "bg-primary/10 border border-primary/20 text-primary", headerGradient: "from-primary/20 via-primary/10 to-accent/5", headerIconBg: "bg-primary/10 text-primary" }, - { tagColor: "bg-blue-500/15 text-blue-400 border border-blue-500/20", headerGradient: "from-blue-500/20 via-blue-500/10 to-primary/5", headerIconBg: "bg-blue-500/10 text-blue-400" }, - { tagColor: "bg-violet-500/15 text-violet-400 border border-violet-500/20", headerGradient: "from-violet-500/20 via-violet-500/10 to-primary/5", headerIconBg: "bg-violet-500/10 text-violet-400" }, - { tagColor: "bg-rose-500/15 text-rose-400 border border-rose-500/20", headerGradient: "from-rose-500/20 via-rose-500/10 to-primary/5", headerIconBg: "bg-rose-500/10 text-rose-400" }, -] - -/** Deterministic small hash so the same category always gets the same generic palette/icon. */ -function hashSlug(slug: string): number { - let hash = 0 - for (let i = 0; i < slug.length; i++) hash = (hash * 31 + slug.charCodeAt(i)) >>> 0 - return hash -} - -function formatStorage(gb: number): string { - return gb >= 1024 ? `${gb / 1024} TB` : `${gb} GB` -} - -/** Plan display built straight from live billing data — used as the generic fallback, and for any plan added to a curated category that has no curated entry. */ -function buildGenericPlanDisplay(plan: GamePlanSpec): GamePlanDisplay { - const storageLabel = plan.storageLabel ?? "Storage Array" - const ramLabel = `${plan.ramGB} GB ${plan.ramType ? plan.ramType + " " : ""}RAM` - return { - name: plan.name || plan.id, - description: plan.description || `${formatStorage(plan.storageGB)} ${storageLabel}, ${ramLabel}.`, - features: [ - ramLabel, - `${formatStorage(plan.storageGB)} ${storageLabel}`, - "Enterprise DDoS Protection", - "BytePanel Control Panel", - ], - } -} - -function buildGenericDisplayConfig(category: CategoryInfo): GameDisplayConfig { - const hash = hashSlug(category.slug) - const icon = GENERIC_ICON_ROTATION[hash % GENERIC_ICON_ROTATION.length] - const palette = GENERIC_PALETTES[hash % GENERIC_PALETTES.length] - - return { - name: category.name, - description: - category.description || - `High-performance ${category.name} server hosting with instant setup, enterprise DDoS protection, and 24/7 support.`, - banner: "/games/generic.png", - iconName: icon, - tag: "Game Server", - ...palette, - billingUrl: `${LINKS.billing.root}/products/${category.slug}`, - heroFeatures: ["Instant Setup", "DDoS Protection", "24/7 Support", "Upgrade Anytime"], - pageFeatures: [ - { title: "Instant Setup", description: `Your ${category.name} server deploys automatically the moment your order completes — no waiting on manual provisioning.`, icon: "Zap", highlights: ["Automated deployment", "No setup fees", "Ready in minutes", "Zero manual steps"] }, - { title: "Enterprise Hardware", description: "Servers run on enterprise-grade CPUs with NVMe SSD storage for consistently fast, low-latency performance.", icon: "Cpu", highlights: ["NVMe SSD storage", "High clock speed CPUs", "Low latency networking", "DDR4 ECC memory"] }, - { title: "DDoS Protection", description: "Enterprise-grade DDoS mitigation is included on every plan, keeping your server online during attacks.", icon: "Shield", highlights: ["Always-on protection", "Layer 3/4/7 filtering", "Zero downtime", "Global POPs"] }, - { title: "24/7 Support", description: "Our support team is available around the clock to help with setup, configuration, or troubleshooting.", icon: "Server", highlights: ["24/7 availability", "Knowledgeable staff", "Fast response times", "Discord & ticket support"] }, - ], - faqs: [ - { question: "How quickly will my server be online?", answer: "Your server is provisioned automatically as soon as your order completes — usually within a couple of minutes." }, - { question: "Can I upgrade my plan later?", answer: "Yes, you can upgrade or downgrade your plan at any time from the billing panel." }, - { question: "Is DDoS protection included?", answer: "Yes, enterprise-grade DDoS protection is included on every plan at no extra cost." }, - ], - resolvePlanDisplay: buildGenericPlanDisplay, - } -} - -// ─── Curated overrides ─────────────────────────────────────────────────────── - -/** Build a resolvePlanDisplay for a curated game — curated copy for known plan ids, live data for anything else. */ -function curatedPlanDisplay( - display: Record, - staticFeatures: readonly string[], -): (plan: GamePlanSpec) => GamePlanDisplay { - return (plan) => { - const entry = display[plan.id] - if (!entry) return buildGenericPlanDisplay(plan) - return { - name: entry.name, - description: entry.description, - features: [ - ...staticFeatures, - `${plan.ramGB}GB ${plan.ramType ? plan.ramType + " " : ""}RAM`, - `${plan.storageGB}GB ${plan.storageLabel ?? "Storage Array"}`, - ], - } - } -} - -/** - * Curated display config, keyed by the Paymenter category slug. A category - * without an entry here falls back to buildGenericDisplayConfig() — nothing - * needs to be added here for a new game/category to work. - */ -const GAME_OVERRIDES: Record GameDisplayConfig> = { - minecraft: () => ({ - ...MINECRAFT_CONFIG, - billingUrl: LINKS.billing.minecraftHosting, - heroFeatures: [...MINECRAFT_HERO_FEATURES], - pageFeatures: MINECRAFT_FEATURES.map((f) => ({ ...f, highlights: [...f.highlights] })), - faqs: [...MINECRAFT_FAQS], - resolvePlanDisplay: curatedPlanDisplay(MINECRAFT_PLAN_DISPLAY, MINECRAFT_PLAN_STATIC_FEATURES), - }), - rust: () => ({ - ...RUST_CONFIG, - billingUrl: LINKS.billing.rustHosting, - heroFeatures: [...RUST_HERO_FEATURES], - pageFeatures: RUST_FEATURES.map((f) => ({ ...f, highlights: [...f.highlights] })), - faqs: [...RUST_FAQS], - resolvePlanDisplay: curatedPlanDisplay(RUST_PLAN_DISPLAY, RUST_PLAN_STATIC_FEATURES), - }), - hytale: () => ({ - ...HYTALE_CONFIG, - billingUrl: LINKS.billing.hytaleHosting, - heroFeatures: [...HYTALE_HERO_FEATURES], - pageFeatures: HYTALE_FEATURES.map((f) => ({ ...f, highlights: [...f.highlights] })), - faqs: [...HYTALE_FAQS], - resolvePlanDisplay: curatedPlanDisplay(HYTALE_PLAN_DISPLAY, HYTALE_PLAN_STATIC_FEATURES), - }), - terraria: () => ({ - ...TERRARIA_CONFIG, - billingUrl: LINKS.billing.terrariaHosting, - heroFeatures: [...TERRARIA_HERO_FEATURES], - pageFeatures: TERRARIA_FEATURES.map((f) => ({ ...f, highlights: [...f.highlights] })), - faqs: [...TERRARIA_FAQS], - resolvePlanDisplay: buildGenericPlanDisplay, - }), - gmod: () => ({ - ...GMOD_CONFIG, - billingUrl: LINKS.billing.gmodHosting, - heroFeatures: [...GMOD_HERO_FEATURES], - pageFeatures: GMOD_FEATURES.map((f) => ({ ...f, highlights: [...f.highlights] })), - faqs: [...GMOD_FAQS], - resolvePlanDisplay: buildGenericPlanDisplay, - }), - palworld: () => ({ - ...PALWORLD_CONFIG, - billingUrl: LINKS.billing.palworldHosting, - heroFeatures: [...PALWORLD_HERO_FEATURES], - pageFeatures: PALWORLD_FEATURES.map((f) => ({ ...f, highlights: [...f.highlights] })), - faqs: [...PALWORLD_FAQS], - resolvePlanDisplay: buildGenericPlanDisplay, - }), -} - -/** Resolve the display config for a game category — curated override if one exists, else auto-generated. */ -export function resolveGameDisplayConfig(category: CategoryInfo): GameDisplayConfig { - const override = GAME_OVERRIDES[category.slug] - return override ? override() : buildGenericDisplayConfig(category) -} diff --git a/packages/core/types/servers/dedicated.ts b/packages/core/types/servers/dedicated.ts index af15269..7cf76d6 100644 --- a/packages/core/types/servers/dedicated.ts +++ b/packages/core/types/servers/dedicated.ts @@ -19,6 +19,10 @@ export interface DedicatedPlanSpec { cpuModel?: string hardware?: "amd" | "intel" priceGBP: number + /** One-time setup fee in GBP (0 if none). */ + setupFeeGBP: number + /** Native one-time setup fees per currency code. */ + setupFees?: Record /** Physical CPU cores. May be undefined if not listed in the product description. */ cores?: number ramGB: number diff --git a/packages/core/types/servers/game.ts b/packages/core/types/servers/game.ts index 961bbca..3759fee 100644 --- a/packages/core/types/servers/game.ts +++ b/packages/core/types/servers/game.ts @@ -18,9 +18,15 @@ export interface GamePlanSpec { id: string /** Raw product name from the billing panel — used as the display name for auto-generated (non-curated) game pages */ name?: string + /** Paymenter category slug this plan was fetched from, e.g. "minecraft" — plans are shown in one unified grid regardless, this is just for search/de-duplication. */ + category: string description?: string cpuModel?: string priceGBP: number + /** One-time setup fee in GBP (0 if none). */ + setupFeeGBP: number + /** Native one-time setup fees per currency code. */ + setupFees?: Record ramGB: number /** RAM generation if the description names one, e.g. "DDR4" — omitted when unspecified */ ramType?: string diff --git a/packages/core/types/servers/vps.ts b/packages/core/types/servers/vps.ts index d57769b..11d7d56 100644 --- a/packages/core/types/servers/vps.ts +++ b/packages/core/types/servers/vps.ts @@ -19,6 +19,10 @@ export interface VpsPlanSpec { description?: string cpuModel?: string priceGBP: number + /** One-time setup fee in GBP (0 if none). */ + setupFeeGBP: number + /** Native one-time setup fees per currency code. */ + setupFees?: Record cpu: number ramGB: number storageGB: number diff --git a/packages/ui/components/Layouts/Dedicated/dedicated-hub.tsx b/packages/ui/components/Layouts/Dedicated/dedicated-hub.tsx index 32bc72a..d92e971 100644 --- a/packages/ui/components/Layouts/Dedicated/dedicated-hub.tsx +++ b/packages/ui/components/Layouts/Dedicated/dedicated-hub.tsx @@ -11,6 +11,7 @@ import { Zap, X, ArrowRight, + ChevronDown, Star, Lock, PackageX, @@ -24,6 +25,8 @@ import { SelectTrigger, SelectValue, } 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 Link from "next/link" import { cn } from "@/lib/utils" import type { DedicatedPlanSpec } from "@/packages/core/types/servers/dedicated" @@ -48,6 +51,7 @@ function formatStorage(plan: DedicatedPlanSpec): string { function PlanCard({ plan }: { plan: DedicatedPlanSpec }) { const outOfStock = plan.stock === "out_of_stock" + const [infoOpen, setInfoOpen] = useState(false) return (
+ {/* Server info */} + + + + + + 0 ? : "None"} + /> + {plan.uplink && ( + + )} + {plan.location && } + + + - -
- - - - - ) -} diff --git a/packages/ui/components/Layouts/Games/game-features.tsx b/packages/ui/components/Layouts/Games/game-features.tsx deleted file mode 100644 index 7646391..0000000 --- a/packages/ui/components/Layouts/Games/game-features.tsx +++ /dev/null @@ -1,113 +0,0 @@ -"use client" - -import { Card } from "@/packages/ui/components/ui/card" -import { - CheckCircle2, - Sparkles, - Settings, - Cpu, - Shield, - Zap, - HardDrive, - Users, - Server, - Map, - Globe, - Gamepad2, -} from "lucide-react" -import { cn } from "@/lib/utils" -import { useTranslations } from "next-intl" - -const iconMap = { - Settings: Settings, - Cpu: Cpu, - Shield: Shield, - Zap: Zap, - HardDrive: HardDrive, - Users: Users, - Server: Server, - Map: Map, - Globe: Globe, - Sparkles: Sparkles, - Gamepad2: Gamepad2, -} - -interface Feature { - title: string - description: string - icon: keyof typeof iconMap - highlights: string[] -} - -interface GameFeaturesProps { - gameName: string - features: Feature[] -} - -export function GameFeatures({ gameName, features }: GameFeaturesProps) { - const t = useTranslations() - - return ( -
- {/* Background */} -
- -
- {/* Header */} -
-
- - {t("gamePage.features.badge")} -
-

- {t("gamePage.features.title")}{" "} - - {gameName} {t("gamePage.features.titleSuffix")} - -

-

- {t("gamePage.features.description", { game: gameName })} -

-
- - {/* Features Grid */} -
- {features.map((feature, index) => ( - -
- {/* Icon */} -
- {(() => { - const IconComponent = iconMap[feature.icon] - return IconComponent ? : null - })()} -
- - {/* Content */} -

{feature.title}

-

{feature.description}

- - {/* Highlights */} -
    - {feature.highlights.map((highlight, i) => ( -
  • - - {highlight} -
  • - ))} -
-
-
- ))} -
-
-
- ) -} diff --git a/packages/ui/components/Layouts/Games/game-hero.tsx b/packages/ui/components/Layouts/Games/game-hero.tsx deleted file mode 100644 index d4cd368..0000000 --- a/packages/ui/components/Layouts/Games/game-hero.tsx +++ /dev/null @@ -1,157 +0,0 @@ -"use client" - -import { Button } from "@/packages/ui/components/ui/button" -import { ArrowRight, ExternalLink, Star, Blocks, Gamepad2, Sparkles, Leaf, Pickaxe, Wrench } from "lucide-react" -import Image from "next/image" -import Link from "next/link" -import { cn } from "@/lib/utils" -import { useTranslations } from "next-intl" -import { LINKS } from "@/packages/core/constants/links" - -const iconMap = { - Blocks: Blocks, - Gamepad2: Gamepad2, - Sparkles: Sparkles, - Leaf: Leaf, - Pickaxe: Pickaxe, - Wrench: Wrench, -} - -interface GameHeroProps { - name: string - description: string - banner: string - icon: keyof typeof iconMap - tag: string - tagColor: string - billingUrl: string - features: string[] - comingSoon?: boolean -} - -export function GameHero({ - name, - description, - banner, - icon, - tag, - tagColor, - billingUrl, - features, - comingSoon, -}: GameHeroProps) { - const t = useTranslations() - const Icon = iconMap[icon] - - return ( -
- {/* Background */} -
- - {/* Banner overlay */} -
- {name} -
-
- - {/* Grid pattern */} -
- -
-
- {/* Left Content */} -
- {/* Tag */} -
- - {tag} -
- - {/* Heading */} -

- {name}{" "} - - {t("games.hosting")} - -

- -

- {description} -

- - {/* Quick Features */} -
- {features.slice(0, 4).map((feature) => ( -
- - {feature} -
- ))} -
- - {/* CTAs */} -
- {comingSoon ? ( - - ) : ( - <> - - - - )} -
-
- - {/* Right - Banner Image */} -
-
- {name} - {comingSoon && ( -
-
-

{t("gamePage.hero.comingSoon")}

-

{t("gamePage.hero.joinDiscordForUpdates")}

-
-
- )} -
-
-
-
-
- ) -} diff --git a/packages/ui/components/Layouts/Games/game-hub.tsx b/packages/ui/components/Layouts/Games/game-hub.tsx new file mode 100644 index 0000000..05387b8 --- /dev/null +++ b/packages/ui/components/Layouts/Games/game-hub.tsx @@ -0,0 +1,309 @@ +"use client" + +import { useState } from "react" +import { + Server, + Search, + MemoryStick, + HardDrive, + Network, + Shield, + Zap, + X, + ArrowRight, + ChevronDown, + Star, + Gamepad2, + PackageX, +} from "lucide-react" +import { Button } from "@/packages/ui/components/ui/button" +import { Input } from "@/packages/ui/components/ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} 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 Link from "next/link" +import { cn } from "@/lib/utils" +import type { GamePlanSpec } from "@/packages/core/types/servers/game" +import { Price } from "@/packages/ui/components/ui/price" + +type SortKey = "default" | "asc" | "desc" + +function formatBandwidth(plan: GamePlanSpec): string { + if (!plan.bandwidth) return "Unmetered" + return `${plan.bandwidth.amount} ${plan.bandwidth.unit}` +} + +// ─── PlanCard ───────────────────────────────────────────────────────────────── + +function PlanCard({ plan }: { plan: GamePlanSpec }) { + const outOfStock = plan.stock === "out_of_stock" + const [infoOpen, setInfoOpen] = useState(false) + + return ( +
+ {plan.popular && ( +
+ + Most Popular +
+ )} + +
+ {/* Header row */} +
+
+

+ {plan.name ?? plan.id} +

+ {plan.description && ( +

{plan.description}

+ )} +
+
+ +

/month

+
+
+ + {outOfStock && ( +
+ + Out of Stock + +
+ )} + +
+ + {/* Specs grid */} +
+
+ + + {plan.ramGB} GB {plan.ramType ? plan.ramType : ""} RAM + +
+
+ + {plan.storageGB} GB {plan.storageLabel ?? "Storage"} +
+
+ + {formatBandwidth(plan)} +
+
+ + {/* Included features */} +
+ {[ + { icon: Shield, text: "DDoS Protection" }, + { icon: Zap, text: "Instant Setup" }, + { icon: Server, text: "Control Panel" }, + ].map(({ icon: Icon, text }) => ( + + + {text} + + ))} +
+ + {/* Server info */} + + + + + + 0 ? : "None"} + /> + {plan.cpuModel && } + {plan.uplink && ( + + )} + + + + +
+
+ ) +} + +// ─── Hub ───────────────────────────────────────────────────────────────────── + +interface GameHubProps { + plans: GamePlanSpec[] +} + +export function GameHub({ plans }: GameHubProps) { + const [search, setSearch] = useState("") + const [sort, setSort] = useState("default") + + const filtered = (() => { + let result = [...plans] + const q = search.trim().toLowerCase() + if (q) { + result = result.filter( + (p) => + (p.name ?? p.id).toLowerCase().includes(q) || + p.description?.toLowerCase().includes(q), + ) + } + if (sort === "asc") result.sort((a, b) => a.priceGBP - b.priceGBP) + if (sort === "desc") result.sort((a, b) => b.priceGBP - a.priceGBP) + return result + })() + + const hasActiveFilters = search !== "" + + function clearFilters() { + setSearch("") + setSort("default") + } + + return ( +
+ {/* Background */} +
+
+
+ +
+ + {/* ── Hero ─────────────────────────────────────────────────────────── */} +
+
+ + Game Server Hosting +
+

+ Choose Your{" "} + + Game Server + +

+

+ One set of plans for every game we support. Pick a tier below, then choose which game to deploy at checkout. +

+
+ + {/* ── Filter bar ───────────────────────────────────────────────────── */} +
+
+
+ + setSearch(e.target.value)} + className="pl-9 bg-card/30 border-border/50" + /> +
+ + + {hasActiveFilters && ( + + )} +
+
+ + {/* ── Plan grid ────────────────────────────────────────────────────── */} +
+ {plans.length === 0 ? ( +
+ +

No game server plans in stock right now

+

Check back soon, or get in touch for a custom configuration.

+
+ ) : filtered.length === 0 ? ( +
+ +

No plans match your search

+

Try adjusting or clearing your search.

+ +
+ ) : ( + <> +

+ Showing {filtered.length} of {plans.length} plan{plans.length !== 1 ? "s" : ""} +

+
+ {filtered.map((plan) => ( + + ))} +
+ + )} +
+ + {/* ── Custom / Enterprise CTA ───────────────────────────────────────── */} +
+
+
+
+

Don't see the game you're after?

+

+ Let us know what you're looking to run and we'll help get it set up. +

+
+ +
+
+
+ +
+
+ ) +} diff --git a/packages/ui/components/Layouts/Games/game-pricing.tsx b/packages/ui/components/Layouts/Games/game-pricing.tsx deleted file mode 100644 index c2c644b..0000000 --- a/packages/ui/components/Layouts/Games/game-pricing.tsx +++ /dev/null @@ -1,327 +0,0 @@ -"use client" - -import { useState } from "react" -import { Button } from "@/packages/ui/components/ui/button" -import { Card } from "@/packages/ui/components/ui/card" -import { Input } from "@/packages/ui/components/ui/input" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/packages/ui/components/ui/select" -import { Zap, ArrowRight, ExternalLink, PackageX, Search, X, Check, Cpu, HardDrive, MemoryStick, Shield, Database, Monitor, Package, Activity, MapPin } from "lucide-react" -import Link from "next/link" -import { cn } from "@/lib/utils" -import { Price } from "@/packages/ui/components/ui/price" -import { useTranslations } from "next-intl" -import { LINKS } from "@/packages/core/constants/links" -import type { ReactNode } from "react" - -const DEFAULT_HEADER_ICON = - -function getFeatureIcon(feature: string) { - const f = feature.toLowerCase() - if (f.includes("ram") || f.includes("ddr") || f.includes("memory")) return - if (f.includes("storage") || f.includes("ssd") || f.includes("nvme") || f.includes("disk")) return - if (f.includes("ryzen") || f.includes("intel") || f.includes("cpu") || f.includes("processor")) return - if (f.includes("ddos") || f.includes("protection") || f.includes("firewall")) return - if (f.includes("database") || f.includes("mysql") || f.includes("mariadb")) return - if (f.includes("panel") || f.includes("control") || f.includes("dashboard")) return - if (f.includes("jar") || f.includes("plugin") || f.includes("mod") || f.includes("oxide") || f.includes("umod")) return - if (f.includes("uptime") || f.includes("sla")) return - return -} - -interface PricingPlan { - name: string - description: string - /** Price in GBP (base currency) - just the number */ - priceGBP: number - period: string - features: string[] - /** Data centre location e.g. "Newcastle, United Kingdom" */ - location?: string - popular?: boolean - url?: string - /** Availability status — defaults to in_stock */ - stock?: "in_stock" | "out_of_stock" | "coming_soon" - /** Native billing prices per currency code; when present, used instead of converting priceGBP */ - prices?: Record -} - -interface GamePricingProps { - gameName: string - billingUrl: string - plans: PricingPlan[] - comingSoon?: boolean - /** Show an out-of-stock state instead of pricing cards (also triggered when plans is empty) */ - outOfStock?: boolean - /** Icon node rendered in the card gradient header */ - headerIcon?: ReactNode - /** Tailwind gradient classes for the card header background */ - headerGradient?: string - /** Tailwind classes for the icon background/colour inside the header */ - headerIconBg?: string -} - -export function GamePricing({ - gameName, - billingUrl, - plans, - comingSoon, - outOfStock, - headerIcon = DEFAULT_HEADER_ICON, - headerGradient = "from-primary/20 via-primary/10 to-accent/5", - headerIconBg = "bg-primary/10 text-primary", -}: GamePricingProps) { - const isOutOfStock = outOfStock || plans.length === 0 - const t = useTranslations() - - const [search, setSearch] = useState("") - const [sortOrder, setSortOrder] = useState<"default" | "asc" | "desc">("default") - - const hasActiveFilters = search !== "" || sortOrder !== "default" - - const filteredPlans = [...plans] - .filter((p) => !search || p.name.toLowerCase().includes(search.toLowerCase())) - .sort((a, b) => { - if (sortOrder === "asc") return a.priceGBP - b.priceGBP - if (sortOrder === "desc") return b.priceGBP - a.priceGBP - return 0 - }) - - function clearFilters() { - setSearch("") - setSortOrder("default") - } - - return ( -
- {/* Background */} -
- -
- {/* Header */} -
-
- - {t("gamePage.pricing.badge")} -
-

- {gameName}{" "} - - {t("gamePage.pricing.title")} - -

-

- {t("gamePage.pricing.description")} -

-
- - {comingSoon ? ( - -
-
- -
-

{t("gamePage.pricing.comingSoon")}

-

- {t("gamePage.pricing.comingSoonDesc", { game: gameName })} -

- -
-
- ) : isOutOfStock ? ( -
-
- -
-

{t("gamePage.pricing.outOfStock")}

-

- {t("gamePage.pricing.outOfStockDesc", { game: gameName })} -

-

- {t("gamePage.pricing.outOfStockNote")} -

- -
-
- ) : ( - <> - {/* Filter Bar */} -
-
- - setSearch(e.target.value)} - placeholder={t("gamePage.pricing.filters.search")} - className="pl-9 h-9 bg-background/50" - /> -
- - - - {hasActiveFilters && ( - - )} -
- - {/* Pricing Grid */} - {filteredPlans.length === 0 ? ( - -
-
- -
-

{t("gamePage.pricing.filters.noResults")}

-

{t("gamePage.pricing.filters.noResultsDesc")}

- -
-
- ) : ( -
- {filteredPlans.map((plan) => ( - - {/* Gradient header with icon */} -
-
-
- {headerIcon} -
-
- - {plan.stock === "out_of_stock" ? ( -
- {t("pricing.outOfStock")} -
- ) : plan.stock === "coming_soon" ? ( -
- {t("pricing.comingSoon")} -
- ) : plan.popular ? ( -
- {t("gamePage.pricing.mostPopular")} -
- ) : null} - -
-
- -
- {/* Name + price row */} -
-

{plan.name}

-
-
- - /{plan.period} -
-
-
- - {/* Description */} -

{plan.description}

- - {/* Location */} - {plan.location && ( -
- - {plan.location} -
- )} - - {/* Features */} -
    - {plan.features.map((feature, i) => ( -
  • - {getFeatureIcon(feature)} - {feature} -
  • - ))} -
- - {plan.stock === "out_of_stock" ? ( - - ) : plan.stock === "coming_soon" ? ( - - ) : ( - - )} -
- - ))} -
- )} - - {/* CTA */} -
-

- {t("gamePage.pricing.customSolution")}{" "} - - {t("gamePage.pricing.contactUs")} - -

- -
- - )} -
-
- ) -} diff --git a/packages/ui/components/Layouts/VPS/vps-hub.tsx b/packages/ui/components/Layouts/VPS/vps-hub.tsx index 1c83d28..33491db 100644 --- a/packages/ui/components/Layouts/VPS/vps-hub.tsx +++ b/packages/ui/components/Layouts/VPS/vps-hub.tsx @@ -12,6 +12,7 @@ import { SlidersHorizontal, X, ArrowRight, + ChevronDown, Star, PackageX, } from "lucide-react" @@ -25,6 +26,8 @@ import { SelectTrigger, SelectValue, } 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 Link from "next/link" import { cn } from "@/lib/utils" import type { VpsPlanSpec } from "@/packages/core/types/servers/vps" @@ -88,6 +91,7 @@ function PlanCard({ plan }: { plan: VpsPlanSpec }) { const lineup = plan.lineup ? LINEUP_META[plan.lineup] ?? null : null const series = plan.series ? SERIES_META[plan.series] ?? null : null const outOfStock = plan.stock === "out_of_stock" + const [infoOpen, setInfoOpen] = useState(false) return (
+ {/* Server info */} + + + + + + 0 ? : "None"} + /> + {series && } + {plan.uplink && ( + + )} + {plan.ddos && ( + + )} + {plan.location && } + + + {/* CTA */}
+ ) +} diff --git a/public/partners/fluxrp.png b/public/partners/fluxrp.png new file mode 100644 index 0000000..8bc1f28 Binary files /dev/null and b/public/partners/fluxrp.png differ diff --git a/public/partners/fyfeweb.png b/public/partners/fyfeweb.png new file mode 100644 index 0000000..a54ce8b Binary files /dev/null and b/public/partners/fyfeweb.png differ