portal revamp 12-27-25

This commit is contained in:
jester1181 2025-12-27 21:52:58 +00:00
parent 2caae970ce
commit a1796f200e
101 changed files with 12645 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

16
eslint.config.mjs Normal file
View File

@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

7
next.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

7351
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "zpack-portal",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@iconify/react": "^5.2.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/typography": "^0.5.16",
"autoprefixer": "^10.4.20",
"axios": "^1.7.9",
"clsx": "^2.1.1",
"cors": "^2.8.5",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"next": "15.1.4",
"react": "^18.2.0",
"react-circular-progressbar": "^2.2.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.5.2",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"eslint": "^9",
"eslint-config-next": "15.1.4",
"postcss": "^8.5.1",
"tailwindcss": "^3.4.17",
"typescript": "^5"
}
}

8
postcss.config.mjs Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

1
public/file.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

BIN
public/img/games/rust.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

1
public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

1
public/vercel.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
public/window.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@ -0,0 +1,25 @@
"use client";
import Link from "next/link";
export default function SuspendedPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-black to-darkGray text-white p-6 text-center">
<div className="bg-darkGray p-8 rounded shadow-lg max-w-lg w-full border border-red-500">
<h1 className="text-3xl font-bold text-red-500 mb-4">🚫 Account Suspended</h1>
<p className="mb-4 text-lightGray">
Your account is currently suspended due to a billing issue. All server access has been restricted.
</p>
<p className="mb-6 text-yellow-300">
Please resolve your payment to regain access. Servers may be permanently deleted after 7 days.
</p>
<Link
href="/billing"
className="inline-block bg-electricBlue text-black px-6 py-3 rounded font-bold hover:bg-neonGreen transition"
>
Go to Billing
</Link>
</div>
</div>
);
}

View File

@ -0,0 +1,157 @@
/* eslint-disable */
"use client";
import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import axios from "axios";
import Cookies from "js-cookie";
import { useAuth } from "@/context/authContext";
import SteelLayout from "@/components/layouts/SteelLayout";
const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const router = useRouter();
const { setToken } = useAuth();
const fetchCsrfToken = async () => {
try {
let csrfToken = Cookies.get("XSRF-TOKEN");
if (!csrfToken) {
console.log("⚠️ CSRF Token missing. Fetching a new one...");
const response = await axios.get("https://api.zerolaghub.com/auth/csrf", {
withCredentials: true,
});
csrfToken = response.data.csrfToken;
console.log("✅ CSRF Token Retrieved:", csrfToken);
}
return csrfToken;
} catch (error) {
console.error("❌ Failed to fetch CSRF Token:", error);
setError("CSRF Token could not be retrieved.");
return null;
}
};
useEffect(() => {
fetchCsrfToken();
}, []);
const handleLogin = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setError("");
try {
let csrfToken = Cookies.get("XSRF-TOKEN") || await fetchCsrfToken();
if (!csrfToken) throw new Error("CSRF token missing.");
console.log("✅ Using CSRF Token for Login:", csrfToken);
const response = await axios.post(
"https://api.zerolaghub.com/auth/login",
{ email, password },
{
withCredentials: true,
headers: {
"X-CSRF-Token": csrfToken,
"Content-Type": "application/json"
}
}
);
console.log("✅ Login successful!", response.data);
const token = response.data.token;
setToken(token);
sessionStorage.setItem("sso_token", token);
router.push("/dashboard");
} catch (err: any) {
if (err.response && err.response.status === 403) {
console.warn("⚠️ CSRF Token possibly expired. Retrying login...");
const newCsrfToken = await fetchCsrfToken();
if (newCsrfToken) {
return handleLogin(e);
}
}
console.error("❌ Login failed:", err);
setError("Login failed. Please try again.");
}
};
const handleLogout = async () => {
try {
console.log("✅ Logging out...");
await axios.post("https://api.zerolaghub.com/auth/logout", {}, { withCredentials: true });
console.log("✅ Logged out successfully, clearing CSRF token...");
Cookies.remove("XSRF-TOKEN", { path: "/" });
await fetchCsrfToken();
router.push("/login");
} catch (error) {
console.error("❌ Logout failed:", error);
}
};
return (
<SteelLayout>
<div className="flex items-center justify-center min-h-screen">
<div className="p-8 bg-darkGray rounded-lg shadow-lg max-w-sm text-center">
<Image
src="/images/zlhlogo_enlarged.png"
alt="ZeroLagHub Logo"
width={150}
height={150}
className="mx-auto mb-4"
/>
<h1 className="text-electricBlue text-2xl font-bold mb-4">Login</h1>
{error && (
<p className="text-red-500 bg-red-100 p-2 rounded mb-4">{error}</p>
)}
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-lightGray text-left mb-1">Email:</label>
<input
type="email"
placeholder="Enter your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<div>
<label className="block text-lightGray text-left mb-1">Password:</label>
<input
type="password"
placeholder="Enter your password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<button
type="submit"
className="w-full p-3 bg-electricBlue text-black font-bold rounded hover:bg-neonGreen transition"
>
Login
</button>
</form>
</div>
</div>
</SteelLayout>
);
};
export default Login;

View File

@ -0,0 +1,55 @@
"use client";
import React, { useEffect } from "react";
import { useRouter } from "next/navigation";
import api from "@/lib/api-client";
type ErrorResponse = {
error?: string;
};
function isAxiosError(error: unknown): error is import("axios").AxiosError<ErrorResponse> {
return typeof error === "object" && error !== null && "isAxiosError" in error;
}
const Logout = () => {
const router = useRouter();
useEffect(() => {
const handleLogout = async () => {
try {
const token = localStorage.getItem("token");
if (token) {
await api.post(
"/auth/logout",
{},
{
headers: { Authorization: `Bearer ${token}` },
}
);
}
} catch (err: unknown) {
if (isAxiosError(err)) {
console.error("Logout error:", err.response?.data?.error || err.message);
alert(err.response?.data?.error || "Logout failed. Please try again.");
} else {
console.error("Unexpected logout error:", err);
alert("An unexpected error occurred. Please try again.");
}
} finally {
localStorage.removeItem("token"); // Clear the token regardless of success or failure
router.push("/"); // Redirect to the home page
}
};
handleLogout();
}, [router]);
return (
<div className="min-h-screen bg-transparent px-6 py-10">
<p className="text-lightGray text-lg">Logging out...</p>
</div>
);
};
export default Logout;

View File

@ -0,0 +1,149 @@
"use client";
import React, { useState } from "react";
import api from "@/lib/api-client";
const Register = () => {
const [formData, setFormData] = useState({
username: "",
email: "",
password: "",
confirmPassword: "",
firstName: "",
lastName: "",
});
const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setError("");
setSuccess(false);
if (formData.password !== formData.confirmPassword) {
setError("Passwords do not match!");
return;
}
try {
await api.post("/auth/register", formData);
setSuccess(true);
alert("Registration successful! You can now log in.");
window.location.href = "/login";
} catch (err: unknown) {
if (isAxiosError(err)) {
setError(err.response?.data?.error || "Registration failed. Please try again.");
} else {
console.error("Unexpected registration error:", err);
setError("An unexpected error occurred. Please try again.");
}
}
};
function isAxiosError(error: unknown): error is import("axios").AxiosError<{ error?: string }> {
return typeof error === "object" && error !== null && "isAxiosError" in error;
}
return (
<div className="min-h-screen bg-transparent px-6 py-10">
<div className="p-8 bg-darkGray rounded-lg shadow-lg max-w-md w-full">
<h1 className="text-3xl font-heading text-electricBlue mb-6 text-center">
Create an Account
</h1>
{error && (
<p className="text-red-500 bg-red-100 p-2 rounded mb-4">
{error}
</p>
)}
{success && (
<p className="text-green-500 bg-green-100 p-2 rounded mb-4">
Registration successful!
</p>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-lightGray mb-1">First Name:</label>
<input
type="text"
name="firstName"
value={formData.firstName}
onChange={handleInputChange}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<div>
<label className="block text-lightGray mb-1">Last Name:</label>
<input
type="text"
name="lastName"
value={formData.lastName}
onChange={handleInputChange}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<div>
<label className="block text-lightGray mb-1">Username:</label>
<input
type="text"
name="username"
value={formData.username}
onChange={handleInputChange}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<div>
<label className="block text-lightGray mb-1">Email:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<div>
<label className="block text-lightGray mb-1">Password:</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<div>
<label className="block text-lightGray mb-1">Confirm Password:</label>
<input
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleInputChange}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<button
type="submit"
className="w-full p-3 bg-electricBlue text-black font-bold rounded hover:bg-neonGreen transition"
>
Register
</button>
</form>
</div>
</div>
);
};
export default Register;

View File

@ -0,0 +1,114 @@
"use client";
import { useEffect, useState } from "react";
import api from "@/lib/api-client";
type BillingStatus = "active" | "inactive" | "past_due" | "canceled" | null;
type UserProfile = {
billing_status: BillingStatus;
plan_name: string | null;
plan_price: number | null;
};
export default function BillingPage() {
const [profile, setProfile] = useState<UserProfile | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const token = localStorage.getItem("token");
if (!token) return;
const fetchProfile = async () => {
try {
const res = await api.get("/api/users/profile", {
headers: { Authorization: `Bearer ${token}` },
});
setProfile(res.data);
} catch (error) {
console.error("Failed to load profile:", error);
} finally {
setLoading(false);
}
};
fetchProfile();
}, []);
const handleOpenPortal = async () => {
try {
const token = localStorage.getItem("token");
const res = await api.post("/api/billing/portal", null, {
headers: { Authorization: `Bearer ${token}` },
});
window.location.href = res.data.url;
} catch (err) {
console.error("Error opening portal:", err);
alert("Error opening portal.");
}
};
const handleCheckout = async () => {
try {
const token = localStorage.getItem("token");
const res = await api.post("/api/billing/checkout", null, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
if (res.data?.url) {
window.location.href = res.data.url;
} else {
alert("Could not start checkout session.");
}
} catch (err) {
console.error("Checkout error:", err);
alert("Checkout session failed.");
}
};
return (
<div className="min-h-screen bg-transparent px-6 py-10">
<div className="bg-gray-800 p-8 rounded shadow max-w-md w-full text-center">
<h1 className="text-3xl text-electricBlue font-heading mb-4">Billing</h1>
{loading ? (
<p className="text-lightGray">Loading...</p>
) : profile?.billing_status === "active" ? (
<>
<p className="text-green-400 mb-2 font-medium">
Your account is in good standing.
</p>
<p className="text-lightGray mb-6">
You&apos;re subscribed to{" "}
<strong>{profile.plan_name || "ZerolagHub Basic"}</strong> $
{(profile.plan_price ?? 10).toFixed(2)} / month
</p>
<button
onClick={handleOpenPortal}
className="bg-neonGreen hover:bg-electricBlue transition text-black font-semibold px-5 py-2 rounded"
>
Manage Subscription
</button>
</>
) : (
<>
<p className="text-yellow-400 font-semibold mb-2">
Your account is not active.
</p>
<p className="text-lightGray mb-4">
Subscribe to activate your hosting plan.
</p>
<button
onClick={handleCheckout}
className="bg-electricBlue text-black font-semibold px-5 py-2 rounded hover:bg-neonGreen transition"
>
Subscribe Now $10/month
</button>
</>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,256 @@
"use client";
import { useEffect, useState } from "react";
import { jwtDecode } from "jwt-decode";
import api from "@/lib/api-client";
import Link from "next/link";
import toast from "react-hot-toast";
//import { Icon } from "@iconify/react";
import "react-circular-progressbar/dist/styles.css";
import MessageBoard from "@/components/MessageBoard";
import BillingSummaryCard from "@/components/BillingSummaryCard";
import AuditLogPreview from "@/components/AuditLogPreview";
//import HudPanel from "@/components/HudPanel";
//import HUDBox from "@/components/HUDBox";
//import ServerHUDOverview from "@/components/ServerHUDOverview";
import RadialStat from "@/components/RadialStat";
//import HUDFramedPanel from "@/components/HUDFramedPanel";
//import SystemControlHUD from "@/components/SystemControlHUD";
import WelcomeCoreHUD from "@/components/WelcomeCoreHUD";
import TechFramePanel from "@/components/TechFramePanel";
//import HUDScaffold from "@/components/HUDScaffold";
type JwtPayload = { email: string };
type ServerStatus = {
uuid: string;
name: string;
status: string;
resources?: {
memory_bytes: number;
disk_bytes: number;
cpu_absolute: number;
};
};
const Dashboard = () => {
const [username, setUsername] = useState("User");
const [consoleUrl, setConsoleUrl] = useState("https://panel.zerolaghub.com");
const [serverList, setServerList] = useState<ServerStatus[]>([]);
const [selectedServer, setSelectedServer] = useState<string>("");
const [suspensionDaysRemaining, setSuspensionDaysRemaining] = useState<number | null>(null);
const [accountStatus, setAccountStatus] = useState<string>("active");
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [stats, setStats] = useState({
total: 0,
online: 0,
offline: 0,
});
const selectedStats = serverList.find((s) => s.uuid === selectedServer);
const fetchStatus = async () => {
try {
const token = localStorage.getItem("token");
if (!token) return;
const [serversRes, statusRes] = await Promise.all([
api.get("/api/environment/servers", {
headers: { Authorization: `Bearer ${token}` },
}),
api.get<{ statuses: ServerStatus[] }>("/api/environment/server-status", {
headers: { Authorization: `Bearer ${token}` },
}),
]);
const servers = serversRes.data.servers || [];
const statuses = statusRes.data.statuses || [];
setServerList(statuses);
setStats({
total: servers.length,
online: statuses.filter((s) => s.status === "running").length,
offline: statuses.filter((s) => s.status !== "running").length,
});
} catch {
// silent fail
}
};
const handlePowerAction = async (action: "start" | "stop" | "restart") => {
const token = localStorage.getItem("token");
const toastId = toast.loading(`${action.toUpperCase()}ING all servers...`);
try {
if (!token) return;
await Promise.allSettled(
serverList.map((s) =>
fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/servers/${s.uuid}/${action}`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
})
)
);
toast.success(`${action.toUpperCase()}ED all servers`, { id: toastId });
fetchStatus();
} catch {
toast.error(`Failed to ${action} all servers`, { id: toastId });
}
};
useEffect(() => {
const token = localStorage.getItem("token");
const ssoToken = sessionStorage.getItem("sso_token");
if (ssoToken) {
setConsoleUrl(`https://panel.zerolaghub.com/login/token?access_token=${ssoToken}`);
}
const initialize = async () => {
if (token) {
try {
const decoded = jwtDecode<JwtPayload>(token);
const fallbackName = decoded.email?.split("@")[0] || "User";
const profileRes = await api.get("/api/users/profile", {
headers: { Authorization: `Bearer ${token}` },
});
const nickname = profileRes.data.nickname;
const suspendedAt = profileRes.data.suspended_at;
const status = profileRes.data.billing_status;
setUsername(nickname || fallbackName);
setAccountStatus(status || "active");
if (suspendedAt) {
const suspendedDate = new Date(suspendedAt);
const now = new Date();
const daysElapsed = Math.floor((now.getTime() - suspendedDate.getTime()) / (1000 * 60 * 60 * 24));
const daysLeft = 7 - daysElapsed;
setSuspensionDaysRemaining(daysLeft > 0 ? daysLeft : 0);
} else {
setSuspensionDaysRemaining(null);
}
} catch {
const decoded = jwtDecode<JwtPayload>(token);
const fallbackName = decoded.email?.split("@")[0] || "User";
setUsername(fallbackName);
}
}
try {
const [serversRes, statusRes] = await Promise.all([
api.get("/api/environment/servers", {
headers: { Authorization: `Bearer ${token}` },
}),
api.get<{ statuses: ServerStatus[] }>("/api/environment/server-status", {
headers: { Authorization: `Bearer ${token}` },
}),
]);
const servers = serversRes.data.servers || [];
const statuses = statusRes.data.statuses || [];
setServerList(statuses);
// ✅ Set default selected server to first one
if (statuses.length > 0 && !selectedServer) {
setSelectedServer(statuses[0].uuid);
}
setStats({
total: servers.length,
online: statuses.filter((s) => s.status === "running").length,
offline: statuses.filter((s) => s.status !== "running").length,
});
} catch {
// silent fail
}
const interval = setInterval(fetchStatus, 10000);
return () => clearInterval(interval);
};
initialize();
}, [selectedServer]);
return (
<div className="min-h-screen px-6 pt-24 pb-10">
<div className="max-w-screen-xl mx-auto flex flex-col gap-10 items-center">
{/* Welcome Header */}
<div className="w-full max-w-xl text-center">
<WelcomeCoreHUD username={username} />
</div>
{/* Middle Section */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 w-full items-start">
{/* Account + Activity */}
<TechFramePanel title="Your Account + Recent Activity">
<div className="space-y-6">
<BillingSummaryCard status={accountStatus} daysRemaining={suspensionDaysRemaining ?? undefined} />
<div>
<h3 className="text-xs font-mono text-cyan-300 uppercase mb-2 tracking-widest">Recent Activity</h3>
<AuditLogPreview />
</div>
</div>
</TechFramePanel>
{/* Server Stats */}
<TechFramePanel title="Server Management">
<div className="space-y-6">
<label className="block text-lightGray text-sm mb-2">Select a Server</label>
<select
className="bg-darkGray text-white p-2 rounded w-full"
value={selectedServer}
onChange={(e) => setSelectedServer(e.target.value)}
>
<option value="">-- Select a Server --</option>
{serverList.map((s) => (
<option key={s.uuid} value={s.uuid}>{s.name}</option>
))}
</select>
{selectedStats && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<RadialStat value={selectedStats.resources?.cpu_absolute || 0} label="CPU" color="#00FF88" />
<RadialStat value={(selectedStats.resources?.memory_bytes || 0) / 1024 / 1024} label="Memory" max={4096} color="#40CFFF" />
<RadialStat value={(selectedStats.resources?.disk_bytes || 0) / 1024 / 1024} label="Disk" max={5120} color="#FF6B6B" />
</div>
)}
</div>
</TechFramePanel>
{/* Message Board */}
<TechFramePanel title="📣 Message Board">
<p className="text-sm text-lightGray mb-2">Stay updated with announcements, system alerts, and platform updates.</p>
<MessageBoard />
</TechFramePanel>
</div>
{/* Bottom Quick Actions */}
<div className="w-full">
<TechFramePanel title="⚡ Quick Actions">
<div className="flex flex-wrap gap-4 justify-center mt-2">
<button onClick={() => handlePowerAction("start")} className="bg-green-500 text-black px-4 py-2 rounded hover:bg-green-600 hover:shadow-[0_0_10px_#00FF88]">Start All</button>
<button onClick={() => handlePowerAction("stop")} className="bg-yellow-500 text-black px-4 py-2 rounded hover:bg-yellow-600 hover:shadow-[0_0_10px_#FFD700]">Stop All</button>
<button onClick={() => handlePowerAction("restart")} className="bg-blue-500 text-black px-4 py-2 rounded hover:bg-blue-600 hover:shadow-[0_0_10px_#40CFFF]">Restart All</button>
<Link href="/servers/create" className="bg-gray-700 text-white px-4 py-2 rounded hover:bg-gray-600">Create Server</Link>
<a href={consoleUrl} target="_blank" className="bg-electricBlue text-black px-4 py-2 rounded hover:bg-blue-600">Go to Console</a>
</div>
</TechFramePanel>
</div>
</div>
</div>
);
};
export default Dashboard;

View File

@ -0,0 +1,187 @@
"use client";
import { useEffect, useState } from "react";
import api from "@/lib/api-client";
import toast from "react-hot-toast";
import { jwtDecode } from "jwt-decode";
const Profile = () => {
const [form, setForm] = useState({
email: "",
username: "",
first_name: "",
last_name: "",
nickname: "",
});
const [original, setOriginal] = useState({ ...form });
const [isAdmin, setIsAdmin] = useState(false);
const [loading, setLoading] = useState(true);
// 🧠 Fetch user data
useEffect(() => {
const token = localStorage.getItem("token");
if (!token) return;
// Decode JWT to check is_admin
const decoded = jwtDecode<{ is_admin: number }>(token);
setIsAdmin(decoded?.is_admin === 1);
const fetchProfile = async () => {
try {
const res = await api.get("/api/users/profile", {
headers: { Authorization: `Bearer ${token}` },
});
// ✅ Normalize nulls to empty strings
const clean = {
email: res.data.email || "",
username: res.data.username || "",
first_name: res.data.first_name || "",
last_name: res.data.last_name || "",
nickname: res.data.nickname || "",
};
setForm(clean);
setOriginal(clean);
} catch {
toast.error("Failed to load profile");
} finally {
setLoading(false);
}
};
fetchProfile();
}, []);
// ✏️ Handle input change
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
// 💾 Save handler
const handleSave = async () => {
const token = localStorage.getItem("token");
if (!token) return;
// ✅ Sanitize values
const trimmed = {
...form,
first_name: form.first_name?.trim() || "User",
last_name: form.last_name?.trim() || "Updated",
nickname: form.nickname?.trim() || "",
};
// ✅ Prevent whitespace-only names
if (!trimmed.first_name.trim() || !trimmed.last_name.trim()) {
return toast.error("First and last name cannot be empty.");
}
try {
toast.dismiss();
await api.put("/api/users/profile", trimmed, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success("Profile updated!");
setOriginal(trimmed);
} catch {
toast.error("Failed to save profile");
}
};
const hasChanges = JSON.stringify(form) !== JSON.stringify(original);
return (
<div className="min-h-screen bg-transparent px-6 py-10">
<div className="max-w-screen-md mx-auto bg-gray-800 p-6 rounded shadow">
<h1 className="text-3xl text-electricBlue font-heading mb-6">👤 Profile</h1>
{loading ? (
<p className="text-lightGray">Loading...</p>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
{/* EMAIL */}
<div>
<label className="block text-lightGray mb-1 font-bold">Email</label>
<input
type="text"
name="email"
value={form.email}
onChange={handleChange}
disabled={!isAdmin}
className="w-full p-2 bg-darkGray text-white rounded"
/>
</div>
{/* USERNAME */}
<div>
<label className="block text-lightGray mb-1 font-bold">Username</label>
<input
type="text"
name="username"
value={form.username}
onChange={handleChange}
disabled={!isAdmin}
className="w-full p-2 bg-darkGray text-white rounded"
/>
</div>
{/* FIRST NAME */}
<div>
<label className="block text-lightGray mb-1 font-bold">First Name</label>
<input
type="text"
name="first_name"
value={form.first_name}
onChange={handleChange}
className="w-full p-2 bg-darkGray text-white rounded"
/>
</div>
{/* LAST NAME */}
<div>
<label className="block text-lightGray mb-1 font-bold">Last Name</label>
<input
type="text"
name="last_name"
value={form.last_name}
onChange={handleChange}
className="w-full p-2 bg-darkGray text-white rounded"
/>
</div>
{/* NICKNAME */}
<div className="md:col-span-2">
<label className="block text-lightGray mb-1 font-bold">Nickname (Display Name)</label>
<input
type="text"
name="nickname"
value={form.nickname}
onChange={handleChange}
className="w-full p-2 bg-darkGray text-white rounded"
/>
</div>
</div>
<div className="text-right">
<button
onClick={handleSave}
disabled={!hasChanges}
className={`px-4 py-2 rounded transition font-semibold ${
hasChanges
? "bg-electricBlue text-black hover:bg-neonGreen"
: "bg-gray-600 text-gray-300 cursor-not-allowed"
}`}
>
Save Changes
</button>
</div>
</>
)}
</div>
</div>
);
};
export default Profile;

View File

@ -0,0 +1,11 @@
"use client";
import TerminalView from "@/components/terminal/TerminalView";
export default function ServerConsole() {
return (
<div className="h-[600px] rounded-md bg-black p-2">
<TerminalView />
</div>
);
}

View File

@ -0,0 +1,14 @@
"use client";
export const dynamic = "force-dynamic";
import { Suspense } from "react";
import ServerConsole from "./ServerConsole";
export default function ConsolePageWrapper() {
return (
<Suspense fallback={<div className="text-center text-white p-10">Loading Console...</div>}>
<ServerConsole />
</Suspense>
);
}

View File

@ -0,0 +1,333 @@
"use client";
import React, { useEffect, useState } from "react";
import api from "@/lib/api-client";
import { NESTS_ROUTE, EGGS_ROUTE, CREATE_SERVER_ROUTE, ALLOCATIONS_ROUTE } from "@/services/routes";
// Define types for clarity
type Nest = {
id: number;
name: string;
};
type Egg = {
id: number;
name: string;
};
type Allocation = {
id: number;
ip: string;
port: number;
};
const CreateServer = () => {
const [serverName, setServerName] = useState("");
const [nests, setNests] = useState<Nest[]>([]);
const [selectedNestId, setSelectedNestId] = useState<number | null>(null);
const [eggs, setEggs] = useState<Egg[]>([]);
const [selectedEggId, setSelectedEggId] = useState<number | null>(null);
const [allocations, setAllocations] = useState<Allocation[]>([]);
const [selectedAllocationId, setSelectedAllocationId] = useState<number | null>(null);
const [memory, setMemory] = useState<number>(1024);
const [disk, setDisk] = useState<number>(10240);
const [cpu, setCpu] = useState<number>(100);
const [startup, setStartup] = useState("");
const [game, setGame] = useState<string | null>(null);
const [variant, setVariant] = useState<string | null>(null);
const [adminPassword, setAdminPassword] = useState<string>("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
const token = localStorage.getItem("token");
if (!token) {
setError("User is not authenticated.");
return;
}
try {
const nestsResponse = await api.get<Nest[]>(NESTS_ROUTE, {
headers: { Authorization: `Bearer ${token}` },
});
setNests(nestsResponse.data);
const allocationsResponse = await api.get<{ allocations: Allocation[] }>(ALLOCATIONS_ROUTE, {
headers: { Authorization: `Bearer ${token}` },
});
setAllocations(allocationsResponse.data.allocations || []);
setSelectedAllocationId(null); // Reset allocation on reload
} catch (error) {
console.error("Failed to fetch data:", error);
setError("Failed to load nests or allocations.");
}
};
fetchData();
}, []);
const handleNestChange = async (nestId: number) => {
setSelectedNestId(nestId);
const selectedNest = nests.find((nest) => nest.id === nestId);
setGame(selectedNest ? selectedNest.name : null);
try {
const token = localStorage.getItem("token");
if (!token) {
setError("User is not authenticated.");
return;
}
const eggsResponse = await api.get<Egg[]>(EGGS_ROUTE(nestId), {
headers: { Authorization: `Bearer ${token}` },
});
setEggs(eggsResponse.data);
if (eggsResponse.data.length > 0) {
const firstEgg = eggsResponse.data[0];
setSelectedEggId(firstEgg.id);
setVariant(firstEgg.name);
const startupResponse = await api.get(`${EGGS_ROUTE(nestId)}/${firstEgg.id}`, {
headers: { Authorization: `Bearer ${token}` },
});
setStartup(startupResponse.data.startup_command || "");
}
} catch (error) {
console.error("Error fetching eggs or startup command:", error);
setError("Failed to load server types or startup command.");
}
};
const handleEggChange = async (eggId: number) => {
setSelectedEggId(eggId);
const selectedEgg = eggs.find((egg) => egg.id === eggId);
setVariant(selectedEgg ? selectedEgg.name : null);
if (selectedNestId) {
try {
const token = localStorage.getItem("token");
if (!token) {
setError("User is not authenticated.");
return;
}
const startupResponse = await api.get(`${EGGS_ROUTE(selectedNestId)}/${eggId}`, {
headers: { Authorization: `Bearer ${token}` },
});
setStartup(startupResponse.data.startup_command || "");
} catch (error) {
console.error("Error fetching startup command:", error);
setError("Failed to load startup command.");
}
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!serverName || !selectedNestId || !selectedEggId || !selectedAllocationId || !startup || !game || !variant) {
setError("All required fields must be filled.");
return;
}
setLoading(true);
setError(null);
setSuccess(null);
try {
const payload = {
name: serverName,
nestId: selectedNestId,
eggId: selectedEggId,
allocationId: selectedAllocationId,
memory,
disk,
cpu,
startup,
game,
variant,
...(game === "Project Zomboid" && { environment: { ADMIN_PASSWORD: adminPassword } }),
};
const token = localStorage.getItem("token");
if (!token) {
setError("User is not authenticated.");
return;
}
const response = await api.post(CREATE_SERVER_ROUTE, payload, {
headers: { Authorization: `Bearer ${token}` },
});
if (response.status === 201) {
setSuccess("Server created successfully!");
resetForm();
// Redirect to servers page without reloading
setTimeout(() => {
window.location.href = "/servers";
}, 1000); // Optional delay
}
} catch (error) {
console.error("Error creating server:", error);
setError("Failed to create server. Please try again.");
} finally {
setLoading(false);
}
};
const resetForm = () => {
setServerName("");
setSelectedNestId(null);
setSelectedEggId(null);
setSelectedAllocationId(null);
setMemory(1024);
setDisk(10240);
setCpu(100);
setStartup("");
setGame(null);
setVariant(null);
setAdminPassword("");
};
return (
<>
<div className="min-h-screen bg-transparent px-6 py-10">
<div className="max-w-screen-lg mx-auto">
<h1 className="text-4xl font-heading text-electricBlue mb-6">Create a New Server</h1>
{error && <p className="text-red-500 bg-red-100 p-2 rounded mb-4">{error}</p>}
{success && <p className="text-green-500 bg-green-100 p-2 rounded mb-4">{success}</p>}
<form className="space-y-6" onSubmit={handleSubmit}>
<div>
<label className="block text-lightGray mb-1">Server Name:</label>
<input
type="text"
value={serverName}
onChange={(e) => setServerName(e.target.value)}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<div>
<label className="block text-lightGray mb-1">Game:</label>
<select
value={selectedNestId || ""}
onChange={(e) => handleNestChange(Number(e.target.value))}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
>
<option value="">Select a game</option>
{nests.map((nest) => (
<option key={nest.id} value={nest.id}>
{nest.name}
</option>
))}
</select>
</div>
{selectedNestId && (
<div>
<label className="block text-lightGray mb-1">Server Type:</label>
<select
value={selectedEggId || ""}
onChange={(e) => handleEggChange(Number(e.target.value))}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
>
<option value="">Select a server type</option>
{eggs.map((egg) => (
<option key={egg.id} value={egg.id}>
{egg.name}
</option>
))}
</select>
</div>
)}
<div>
<label className="block text-lightGray mb-1">Allocation:</label>
<select
value={selectedAllocationId || ""}
onChange={(e) => setSelectedAllocationId(Number(e.target.value))}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
>
<option value="">Select an allocation</option>
{allocations.map((allocation) => (
<option key={allocation.id} value={allocation.id}>
{allocation.ip}:{allocation.port}
</option>
))}
</select>
</div>
<div>
<label className="block text-lightGray mb-1">Memory (MB):</label>
<input
type="number"
value={memory}
onChange={(e) => setMemory(Number(e.target.value))}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<div>
<label className="block text-lightGray mb-1">Disk (MB):</label>
<input
type="number"
value={disk}
onChange={(e) => setDisk(Number(e.target.value))}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<div>
<label className="block text-lightGray mb-1">CPU (%):</label>
<input
type="number"
value={cpu}
onChange={(e) => setCpu(Number(e.target.value))}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<div>
<label className="block text-lightGray mb-1">Startup Command:</label>
<input
type="text"
value={startup}
readOnly
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
{game === "Project Zomboid" && (
<div>
<label className="block text-lightGray mb-1">Admin Password:</label>
<input
type="password"
value={adminPassword}
onChange={(e) => setAdminPassword(e.target.value)}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
)}
<button
type="submit"
className="w-full p-3 bg-electricBlue text-black font-bold rounded hover:bg-neonGreen transition"
>
{loading ? "Creating Server..." : "Create Server"}
</button>
</form>
</div>
</div>
</>
);
};
export default CreateServer;

View File

@ -0,0 +1,201 @@
"use client";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import toast from "react-hot-toast";
import api from "@/lib/api-client";
import GroupedServerList from "@/components/GroupedServerList";
import SteelLayout from "@/components/layouts/SteelLayout";
export type Server = {
uuid: string;
name: string;
game: string;
variant?: string;
status?: string;
};
const ServersPage = () => {
const [servers, setServers] = useState<Server[]>([]);
const [grouped, setGrouped] = useState(true);
const router = useRouter();
const actionLabels: Record<string, string> = {
start: "Starting",
stop: "Stopping",
restart: "Restarting",
};
useEffect(() => {
const token = localStorage.getItem("token");
if (!token) {
router.push("/login");
return;
}
const fetchAndMergeServers = async () => {
try {
const [serverRes, statusRes] = await Promise.all([
api.get<{ servers: Server[] }>("/api/environment/servers", {
headers: { Authorization: `Bearer ${token}` },
}),
api.get<{ statuses: { uuid: string; status: string }[] }>(
"/api/environment/server-status",
{ headers: { Authorization: `Bearer ${token}` } }
),
]);
const rawServers = serverRes.data.servers || [];
const statuses = statusRes.data.statuses || [];
const merged = rawServers.map((server) => {
const match = statuses.find((s) => s.uuid === server.uuid);
return {
...server,
status: match?.status || "unknown",
};
});
setServers(merged);
} catch {
toast.error("Failed to update server status.");
}
};
fetchAndMergeServers();
const interval = setInterval(fetchAndMergeServers, 10000);
return () => clearInterval(interval);
}, [router]);
const handleConsole = (uuid: string) => {
const token = sessionStorage.getItem("sso_token");
if (!token) return toast.error("SSO token missing");
const url = `https://panel.zerolaghub.com/login/token?access_token=${token}&redirect=/server/${uuid}`;
window.open(url, "_blank");
};
const handleUpgrade = (uuid: string) => {
router.push(`/servers/upgrade?uuid=${uuid}`);
};
const handleAction = async (uuid: string, action: string) => {
const toastId = toast.loading(`${actionLabels[action]}...`);
try {
const token = localStorage.getItem("token");
const url = `${process.env.NEXT_PUBLIC_API_BASE_URL}/servers/${uuid}/${action}`;
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
if (res.ok) {
toast.success(`${actionLabels[action]} succeeded`, { id: toastId });
} else {
const data = await res.json();
toast.error(data?.error || "Failed", { id: toastId });
}
} catch {
toast.error("Unexpected error", { id: toastId });
}
};
return (
<SteelLayout>
<div className="max-w-screen-lg mx-auto px-6 py-10">
<h1 className="text-4xl font-heading text-electricBlue mb-6 text-center">
Your Servers
</h1>
{/* Toggle View */}
<div className="flex justify-center mb-6">
<button
className={`px-4 py-2 font-semibold rounded-l ${
grouped
? "bg-electricBlue text-black"
: "bg-gray-700 text-white hover:bg-gray-600"
}`}
onClick={() => setGrouped(true)}
>
Group by Game
</button>
<button
className={`px-4 py-2 font-semibold rounded-r ${
!grouped
? "bg-electricBlue text-black"
: "bg-gray-700 text-white hover:bg-gray-600"
}`}
onClick={() => setGrouped(false)}
>
Flat View
</button>
</div>
{servers.length > 0 ? (
grouped ? (
<GroupedServerList
servers={servers}
onAction={handleAction}
onConsole={handleConsole}
onUpgrade={handleUpgrade}
/>
) : (
<ul className="space-y-4">
{servers.map((server) => (
<li
key={server.uuid}
className="bg-darkGray p-4 rounded shadow flex justify-between items-center"
>
<div>
<p className="text-white font-bold">{server.name}</p>
<p className="text-sm text-gray-400">{server.status}</p>
</div>
<div className="flex gap-2">
<button
className="px-4 py-2 bg-electricBlue text-black rounded hover:bg-neonGreen transition"
onClick={() => handleConsole(server.uuid)}
>
Go to Console
</button>
<button
className="px-3 py-2 text-sm bg-blue-700 text-white rounded hover:bg-blue-600 transition"
onClick={() => handleUpgrade(server.uuid)}
>
Upgrade
</button>
{["start", "stop", "restart"].map((action) => (
<button
key={action}
onClick={() => handleAction(server.uuid, action)}
className="px-3 py-2 text-sm bg-gray-700 text-white rounded hover:bg-gray-600 transition"
>
{action.charAt(0).toUpperCase() + action.slice(1)}
</button>
))}
</div>
</li>
))}
</ul>
)
) : (
<p className="text-center text-lightGray text-lg">No servers found.</p>
)}
<div className="text-center mt-10">
<button
className="px-6 py-3 bg-electricBlue text-black font-bold rounded hover:bg-neonGreen transition"
onClick={() => router.push("/servers/create")}
>
Create Server
</button>
</div>
</div>
</SteelLayout>
);
};
export default ServersPage;

View File

@ -0,0 +1,107 @@
"use client";
import React, { useEffect, useState, Suspense } from "react";
import api from "@/lib/api-client";
import { useSearchParams } from "next/navigation";
import toast from "react-hot-toast";
function UpgradeServer() {
const params = useSearchParams();
const uuid = params.get("uuid");
const [memory, setMemory] = useState(1024);
const [disk, setDisk] = useState(10240);
const [cpu, setCpu] = useState(100);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!uuid) return;
const fetchServerInfo = async () => {
try {
const token = localStorage.getItem("token");
const response = await api.get(`/api/servers/${uuid}`, {
headers: { Authorization: `Bearer ${token}` },
});
const { limits } = response.data;
setMemory(limits.memory);
setDisk(limits.disk);
setCpu(limits.cpu);
} catch {
toast.error("Failed to load server info.");
}
};
fetchServerInfo();
}, [uuid]);
const handleUpgrade = async () => {
if (!uuid) return;
setLoading(true);
try {
const token = localStorage.getItem("token");
await api.put(
`/api/servers/${uuid}/upgrade`,
{ memory, disk, cpu },
{ headers: { Authorization: `Bearer ${token}` } }
);
toast.success("Server upgraded successfully!");
} catch {
toast.error("Failed to upgrade server.");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-transparent px-6 py-10">
<div className="max-w-screen-sm mx-auto bg-gray-800 p-6 rounded shadow">
<h1 className="text-3xl font-heading text-electricBlue mb-4 text-center">Upgrade Server</h1>
<label className="block text-lightGray mb-2">Memory (MB):</label>
<input
type="number"
value={memory}
onChange={(e) => setMemory(Number(e.target.value))}
className="w-full p-2 mb-4 rounded bg-black border border-electricBlue"
/>
<label className="block text-lightGray mb-2">Disk (MB):</label>
<input
type="number"
value={disk}
onChange={(e) => setDisk(Number(e.target.value))}
className="w-full p-2 mb-4 rounded bg-black border border-electricBlue"
/>
<label className="block text-lightGray mb-2">CPU (%):</label>
<input
type="number"
value={cpu}
onChange={(e) => setCpu(Number(e.target.value))}
className="w-full p-2 mb-6 rounded bg-black border border-electricBlue"
/>
<button
onClick={handleUpgrade}
disabled={loading}
className="w-full p-3 bg-electricBlue text-black font-bold rounded hover:bg-neonGreen transition"
>
{loading ? "Upgrading..." : "Upgrade Server"}
</button>
</div>
</div>
);
}
// ✅ Wrap with Suspense to avoid Next.js 13 App Router warning
export default function UpgradeServerPageWrapper() {
return (
<Suspense fallback={<div className="text-center text-white p-10">Loading Upgrade Form...</div>}>
<UpgradeServer />
</Suspense>
);
}

View File

@ -0,0 +1,125 @@
"use client";
import React, { useState } from "react";
import api from "@/lib/api-client"; // Import API utility
const Support = () => {
const [formData, setFormData] = useState({
name: "",
email: "",
subject: "",
message: "",
});
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setError(null);
setSuccess(null);
if (!formData.name || !formData.email || !formData.subject || !formData.message) {
setError("All fields are required.");
return;
}
try {
const response = await api.post("api/support/create", formData);
if (response.status === 200) {
setSuccess("Your support request has been submitted successfully.");
setFormData({ name: "", email: "", subject: "", message: "" });
} else {
throw new Error("Failed to submit support request.");
}
} catch (err) {
console.error("Error submitting support request:", err);
setError("Failed to submit your request. Please try again later.");
}
};
return (
<>
<div className="min-h-screen bg-transparent px-6 py-10">
<div className="max-w-screen-md mx-auto">
<h1 className="text-4xl font-heading text-electricBlue mb-6 text-center">
Contact Support
</h1>
{error && (
<p className="text-red-500 bg-red-100 p-2 rounded mb-4 text-center">
{error}
</p>
)}
{success && (
<p className="text-green-500 bg-green-100 p-2 rounded mb-4 text-center">
{success}
</p>
)}
<form className="space-y-6" onSubmit={handleSubmit}>
<div>
<label className="block text-lightGray mb-1">Name:</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<div>
<label className="block text-lightGray mb-1">Email:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<div>
<label className="block text-lightGray mb-1">Subject:</label>
<input
type="text"
name="subject"
value={formData.subject}
onChange={handleInputChange}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<div>
<label className="block text-lightGray mb-1">Message:</label>
<textarea
name="message"
value={formData.message}
onChange={handleInputChange}
rows={5}
required
className="w-full p-3 bg-black border border-electricBlue rounded focus:outline-none focus:border-neonGreen"
/>
</div>
<button
type="submit"
className="w-full p-3 bg-electricBlue text-black font-bold rounded hover:bg-neonGreen transition"
>
Submit
</button>
</form>
</div>
</div>
</>
);
};
export default Support;

28
src/app/about/page.tsx Normal file
View File

@ -0,0 +1,28 @@
const About = () => {
return (
<>
<div className="min-h-screen bg-gradient-to-br from-black to-darkGray text-foreground flex flex-col items-center justify-center px-4">
<div className="max-w-3xl text-center">
<h1 className="text-4xl font-heading text-electricBlue mb-5 text-shadow">
About ZeroLagHub
</h1>
<p className="text-lg text-lightGray leading-relaxed mb-6">
ZeroLagHub is your ultimate solution for hosting open-source, indie, RPG,
and modded game servers. We focus on providing seamless, reliable, and
high-performance server hosting for gamers and developers alike.
</p>
<p className="text-lg text-lightGray leading-relaxed">
Our platform is built with cutting-edge automation, robust server management
capabilities, and an easy-to-use interface to ensure you can focus on
playing or developing while we handle the rest.
</p>
</div>
</div>
</>
);
};
export default About;

43
src/app/faq/page.tsx Normal file
View File

@ -0,0 +1,43 @@
const FAQ = () => {
return (
<>
<div className="min-h-screen bg-transparent px-6 py-10">
<div className="max-w-3xl mx-auto">
<h1 className="text-4xl font-heading text-electricBlue mb-6 text-center">
Frequently Asked Questions
</h1>
<div className="space-y-8">
<div className="border-b border-gray-600 pb-4">
<h2 className="text-2xl font-bold text-foreground mb-2">What is ZeroLagHub?</h2>
<p className="text-lightGray text-lg leading-relaxed">
ZeroLagHub is a hosting platform specializing in open-source,
indie, RPG, and modded game servers.
</p>
</div>
<div className="border-b border-gray-600 pb-4">
<h2 className="text-2xl font-bold text-foreground mb-2">What games are supported?</h2>
<p className="text-lightGray text-lg leading-relaxed">
We support a wide variety of games, including Minecraft, ARK: Survival Evolved,
CS:GO, and more. Custom games can also be hosted upon request.
</p>
</div>
<div className="border-b border-gray-600 pb-4">
<h2 className="text-2xl font-bold text-foreground mb-2">How do I create a server?</h2>
<p className="text-lightGray text-lg leading-relaxed">
Once registered, you can create a server directly from your dashboard.
Choose your game, allocate resources, and start playing!
</p>
</div>
</div>
</div>
</div>
</>
);
};
export default FAQ;

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

70
src/app/features/page.tsx Normal file
View File

@ -0,0 +1,70 @@
const Features = () => {
return (
<>
<div className="min-h-screen bg-transparent px-6 py-10">
<div className="max-w-screen-lg mx-auto text-center">
<h1 className="text-5xl font-heading text-electricBlue mb-6">
Platform Features
</h1>
<p className="text-lightGray text-lg mb-10">
Discover the powerful features of ZeroLagHub that set us apart.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{[
{
title: "Blazing Fast Performance",
description:
"Our servers are optimized for speed and reliability to give you the best experience.",
icon: "🔥",
},
{
title: "Intuitive Dashboard",
description:
"Manage your game servers easily with our user-friendly dashboard.",
icon: "📊",
},
{
title: "24/7 Support",
description:
"Get help anytime with our around-the-clock support team.",
icon: "💡",
},
{
title: "Custom Mod Support",
description:
"Run modded servers with full customization options tailored to your needs.",
icon: "🛠️",
},
{
title: "Scalable Plans",
description:
"Choose a plan that grows with your community, from small servers to large-scale environments.",
icon: "📈",
},
].map((feature, index) => (
<div
key={index}
className="p-6 bg-darkGray rounded-lg shadow-md hover:shadow-xl transition-shadow"
>
<div className="text-4xl mb-4">{feature.icon}</div>
<h2 className="text-electricBlue text-2xl font-bold mb-3">
{feature.title}
</h2>
<p className="text-lightGray">{feature.description}</p>
</div>
))}
</div>
</div>
</div>
</>
);
};
export default Features;

241
src/app/globals.css Normal file
View File

@ -0,0 +1,241 @@
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Reset styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
font-family: var(--font-body, "Roboto", sans-serif);
background-color: #000;
background-image: url('/textures/hdsteel-bg.webp');
background-size: cover;
background-repeat: no-repeat;
background-position: center;
background-attachment: fixed;
color: var(--foreground, #f4f4f8);
min-height: 100vh;
line-height: 1.6;
}
/* Links */
a {
color: var(--electricBlue, #1f8eff);
text-decoration: none;
}
a:hover {
color: var(--neonGreen, #32ff7e);
}
/* Headings */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading, "Geist", sans-serif);
margin-bottom: 1rem;
}
/* Buttons */
button {
font-family: var(--font-body, "Roboto", sans-serif);
background-color: var(--electricBlue, #1f8eff);
color: var(--foreground, #f4f4f8);
border: none;
border-radius: 0.5rem;
padding: 0.75rem 1.5rem;
cursor: pointer;
transition: background-color 0.2s ease;
}
button:hover {
background-color: var(--neonGreen, #32ff7e);
}
/* Inputs */
input,
textarea,
select {
font-family: var(--font-body, "Roboto", sans-serif);
background-color: var(--darkGray, #1a1a1a);
color: var(--foreground, #f4f4f8);
border: 1px solid var(--lightGray, #bbbbbb);
border-radius: 0.25rem;
padding: 0.5rem;
width: 100%;
}
input:focus,
textarea:focus,
select:focus {
border-color: var(--electricBlue, #1f8eff);
}
/* Footer */
footer {
background-color: var(--darkGray, #1a1a1a);
color: var(--foreground, #f4f4f8);
text-align: center;
padding: 1rem;
}
/* Animations */
@keyframes slowShift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
@keyframes hudScanLine {
0%, 100% { transform: scaleX(1); }
50% { transform: scaleX(1.1); }
}
@keyframes pulse-slow {
0%, 100% { opacity: 0.2; }
50% { opacity: 0.6; }
}
.animate-slow-shift {
animation: slowShift 20s ease infinite;
}
.animate-hudScanLine {
animation: hudScanLine 4s infinite ease-in-out;
}
.animate-pulse-slow {
animation: pulse-slow 3s infinite ease-in-out;
}
/* Material Icons */
.material-symbols-rounded {
font-family: 'Material Symbols Rounded';
font-weight: normal;
font-style: normal;
font-size: 1.25rem;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
/* Hover glow effect */
.hover-glow {
transition: box-shadow 0.2s ease;
}
.hover-glow:hover {
box-shadow: 0 0 8px rgba(31, 142, 255, 0.5);
}
/* Steel overlay effect */
.bg-steel-overlay {
background-image: url('/textures/hdsteel-bg.webp');
background-size: cover;
background-position: center;
opacity: 0.4;
filter: brightness(1.25) contrast(1.15) saturate(1.1);
mix-blend-mode: screen;
pointer-events: none;
position: absolute;
inset: 0;
z-index: 0;
}
.bg-steel-texture {
background-image: url('/textures/hdsteel-bg.webp');
background-size: cover;
background-repeat: no-repeat;
background-position: center;
background-attachment: fixed;
opacity: 1;
pointer-events: none;
position: absolute;
inset: 0;
z-index: 0;
}
/* HUD beveled shape */
.clip-bevel-frame {
clip-path: polygon(
12px 0%, 100% 0%, 100% 100%, 12px 100%, 0% 92%, 0% 8%
);
background: linear-gradient(145deg, #0f1114, #0b0d11);
box-shadow:
inset 0 0 0 1px rgba(0, 255, 255, 0.12),
0 0 20px rgba(0, 255, 255, 0.06),
0 0 80px rgba(0, 255, 255, 0.04);
position: relative;
}
/* HUD curved hologram illusion (NEW) */
.curved-holo-backdrop {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%) perspective(1200px) rotateX(10deg);
width: 100%;
height: 100%;
background: radial-gradient(
ellipse at center,
rgba(0, 255, 255, 0.06) 0%,
rgba(0, 255, 255, 0.03) 50%,
transparent 80%
);
border-radius: 60% / 15%;
filter: brightness(1.4) blur(1.5px) contrast(1.4);
opacity: 0.45;
z-index: 0;
pointer-events: none;
}
.hud-scanlines::before {
content: "";
position: absolute;
inset: 0;
background-image: repeating-linear-gradient(
to bottom,
rgba(0, 255, 255, 0.05),
rgba(0, 255, 255, 0.05) 1px,
transparent 2px,
transparent 4px
);
pointer-events: none;
z-index: 1;
}
.hud-curved-shell {
perspective: 1800px;
transform-style: preserve-3d;
transform: scale(1.01);
margin-top: 2rem;
}
.hud-inner-curved {
transform: rotateX(12deg);
transform-origin: center top;
background: radial-gradient(ellipse at center, #0f1114 0%, #0b0d11 100%);
border-radius: 20px;
box-shadow:
inset 0 0 50px rgba(0, 255, 255, 0.05),
0 20px 40px rgba(0, 255, 255, 0.1);
padding: 1rem;
}
/* global.css */
.curved-hud-background {
filter: brightness(1.2) contrast(1.1) saturate(1.1);
mix-blend-mode: screen;
}

31
src/app/layout.tsx Normal file
View File

@ -0,0 +1,31 @@
// src/app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";
import { AuthProvider } from "@/context/authContext";
import { Toaster } from "react-hot-toast";
import ClientLayout from "../components/ClientLayout";
import SteelLayout from "@/components/layouts/SteelLayout"; // ✅ Apply steel wrapper globally
export const metadata: Metadata = {
title: "ZeroLagHub",
description: "Seamless hosting for open-source, indie, RPG, and modded game servers.",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" data-theme="dark">
<body className="antialiased text-foreground min-h-screen flex flex-col">
<AuthProvider>
<Toaster position="top-center" reverseOrder={false} />
<SteelLayout>
<ClientLayout>{children}</ClientLayout>
</SteelLayout>
</AuthProvider>
</body>
</html>
);
}

47
src/app/page.tsx Normal file
View File

@ -0,0 +1,47 @@
"use client";
import Image from "next/image";
export default function Home() {
return (
<div className="min-h-screen bg-transparent px-6 py-10">
<header className="relative flex-grow flex flex-col items-center justify-center text-center px-6 py-20 bg-cover bg-center">
<div className="relative z-10 flex flex-col items-center">
<Image
src="/images/zlhlogo_enlarged.png"
alt="ZeroLagHub Logo"
width={140}
height={140}
className="mb-6"
/>
<h1 className="text-5xl md:text-6xl font-heading text-electricBlue mb-6">
Next-Level Game Hosting
</h1>
<p className="text-lg md:text-xl text-lightGray mb-8 max-w-2xl">
Optimized servers for open-source, indie, RPG, and modded games.
Experience unparalleled speed, scalability, and reliability.
</p>
<div className="flex space-x-4">
<a
href="/register"
className="px-8 py-4 bg-electricBlue text-black font-bold text-lg rounded-lg hover:bg-neonGreen transition transform hover:scale-105 shadow-md"
>
Get Started
</a>
<a
href="/features"
className="px-8 py-4 border border-electricBlue text-electricBlue font-bold text-lg rounded-lg hover:bg-electricBlue hover:text-black transition transform hover:scale-105"
>
Learn More
</a>
</div>
</div>
</header>
</div>
);
}

104
src/app/pricing/page.tsx Normal file
View File

@ -0,0 +1,104 @@
"use client";
import GameCard from "@/components/GameCard"; // adjust path as needed
import { gameInfo } from "@/data/games";
const Pricing = () => {
return (
<>
<div className="min-h-screen bg-transparent px-6 py-10">
<div className="max-w-screen-lg mx-auto text-center">
<h1 className="text-5xl font-heading text-electricBlue mb-10">
Pricing Plans
</h1>
{/* 💳 Pricing Tiers */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{[
{
title: "Basic",
price: "$5/month",
features: ["1 GB RAM", "10 GB Disk", "Community Support"],
popular: false,
},
{
title: "Standard",
price: "$10/month",
features: ["2 GB RAM", "20 GB Disk", "Priority Support"],
popular: true,
},
{
title: "Pro",
price: "$20/month",
features: ["4 GB RAM", "50 GB Disk", "24/7 Support"],
popular: false,
},
].map((plan, index) => (
<div
key={index}
className={`p-6 bg-darkGray rounded-lg shadow-md border border-gray-700 ${
plan.popular
? "border-electricBlue scale-105"
: "hover:scale-105 hover:border-electricBlue"
} transition-transform`}
>
<h2 className="text-2xl font-bold text-electricBlue mb-3">
{plan.title}
</h2>
<p className="text-xl text-neonGreen mb-4">{plan.price}</p>
<ul className="text-lightGray space-y-2">
{plan.features.map((feature, i) => (
<li key={i}>{feature}</li>
))}
</ul>
{plan.popular && (
<div className="mt-4 text-sm font-bold text-neonGreen uppercase">
Most Popular
</div>
)}
</div>
))}
</div>
{/* 🧩 GameCard Preview Section */}
<div className="mt-16 text-center">
<h2 className="text-3xl text-white font-bold mb-6">Supported Games Preview</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 justify-items-center">
{[
"minecraft",
"valheim",
"rust",
"project_zomboid",
"terraria",
"nonexistent_key", // fallback test
].map((key) => (
<GameCard
key={key}
gameKey={key}
serverName={gameInfo[key]?.name || "Unknown Server"}
status="online"
/>
))}
</div>
</div>
{/* 📄 Terms of Service */}
<div className="mt-16 text-sm text-lightGray">
<p>
By using ZeroLagHub, you agree to comply with our{" "}
<a
href="/terms-of-service"
className="text-electricBlue underline hover:text-neonGreen transition"
>
Terms of Service
</a>
.
</p>
</div>
</div>
</div>
</>
);
};
export default Pricing;

View File

@ -0,0 +1,68 @@
"use client";
const PrivacyPolicy = () => {
return (
<>
<div className="min-h-screen bg-transparent px-6 py-10">
<div className="max-w-screen-lg mx-auto">
<h1 className="text-5xl font-heading text-electricBlue mb-10">
Privacy Policy
</h1>
<div className="space-y-6 text-lightGray">
<p>
<strong>Effective Date:</strong> [Insert Date]
</p>
<p>
ZeroLagHub values your privacy. This Privacy Policy explains what
data we collect, how we use it, and your rights regarding your
information.
</p>
<h2 className="text-3xl text-electricBlue mt-8">1. Information We Collect</h2>
<p>
- <strong>Account Information:</strong> Name, email, payment details.
<br />
- <strong>Usage Data:</strong> IP address, browser type, and usage
activity.
<br />
- <strong>Cookies:</strong> Used to improve site performance and
user experience.
</p>
<h2 className="text-3xl text-electricBlue mt-8">2. How We Use Your Information</h2>
<p>
- To provide and maintain the Service.
<br />
- To improve our platform.
<br />
- To process payments.
</p>
<h2 className="text-3xl text-electricBlue mt-8">3. Sharing Your Information</h2>
<p>
- We only share data with third-party services for processing
payments, analytics, or as required by law.
</p>
<h2 className="text-3xl text-electricBlue mt-8">4. Data Security</h2>
<p>
We use industry-standard measures to secure your data but cannot
guarantee absolute security.
</p>
<h2 className="text-3xl text-electricBlue mt-8">5. User Rights</h2>
<p>
You can:
<br />
- Request access to your data.
<br />
- Request data correction or deletion.
<br />
- Opt-out of marketing communications.
</p>
</div>
</div>
</div>
</>
);
};
export default PrivacyPolicy;

View File

@ -0,0 +1,67 @@
"use client";
const TermsOfService = () => {
return (
<>
<div className="min-h-screen bg-transparent px-6 py-10">
<div className="max-w-screen-lg mx-auto">
<h1 className="text-5xl font-heading text-electricBlue mb-10">
Terms of Service
</h1>
<div className="space-y-6 text-lightGray">
<p>
<strong>Effective Date:</strong> [Insert Date]
</p>
<p>
These Terms of Service (&ldquo;Terms&rdquo;) govern your use of
ZeroLagHub&rsquo;s website, services, and hosting platform. By using our
services, you agree to these Terms. Please read them carefully.
</p>
<h2 className="text-3xl text-electricBlue mt-8">1. Definitions</h2>
<p>
- &ldquo;Service&rdquo; refers to game server hosting and related
products provided by ZeroLagHub.
<br />
- &ldquo;User&rdquo; refers to anyone accessing or using the Service.
<br />
- &ldquo;Account&rdquo; refers to a registered profile created by the User.
</p>
<h2 className="text-3xl text-electricBlue mt-8">2. Acceptable Use</h2>
<p>
You agree to:
<br />
- Not use our Service for illegal activities, harassment, or abusive behavior.
<br />
- Not overuse or abuse resources, disrupting service for others.
</p>
<h2 className="text-3xl text-electricBlue mt-8">3. Account Responsibilities</h2>
<p>
- Maintain the confidentiality of your login credentials.
<br />
- Ensure all account information is accurate and up-to-date.
</p>
<h2 className="text-3xl text-electricBlue mt-8">4. Payments and Refunds</h2>
<p>
- Subscriptions are billed monthly.
<br />
- Refunds are not provided for partial periods or mid-cycle
cancellations unless explicitly stated in a promotion.
</p>
<h2 className="text-3xl text-electricBlue mt-8">5. Service Availability</h2>
<p>
- We strive for 99.9% uptime but do not guarantee uninterrupted service.
<br />
- Downtime caused by third-party providers or scheduled maintenance
is not our liability.
</p>
</div>
</div>
</div>
</>
);
};
export default TermsOfService;

View File

@ -0,0 +1,48 @@
"use client";
import { useEffect, useState } from "react";
import api from "@/lib/api-client";
type LogItem = {
id: number;
action: string;
timestamp: string;
created_at: string;
};
const AuditLogPreview = () => {
const [logs, setLogs] = useState<LogItem[]>([]);
useEffect(() => {
const fetchLogs = async () => {
try {
const res = await api.get("/api/audit");
setLogs(res.data.logs || []);
} catch (err) {
console.error("Failed to load audit logs", err);
}
};
fetchLogs();
}, []);
return (
<div className="bg-darkGray p-4 rounded shadow">
<h3 className="text-lg font-bold text-white mb-2">📜 Recent Activity</h3>
{logs.length === 0 ? (
<p className="text-lightGray text-sm">No recent activity found.</p>
) : (
<ul className="text-sm text-lightGray space-y-1">
{logs.slice(0, 5).map((log) => (
<li key={log.id}>
{log.action} <span className="text-xs text-gray-400">({new Date(log.created_at).toLocaleString()})
</span>
</li>
))}
</ul>
)}
</div>
);
};
export default AuditLogPreview;

View File

@ -0,0 +1,24 @@
"use client";
import Modal from "@/components/Modal";
import { useAccessControl } from "@/hooks/useAccessControl";
export default function BillingModal() {
const { isSuspended } = useAccessControl();
if (!isSuspended) return null;
return (
<Modal
isOpen={true}
onClose={() => {}}
onConfirm={() => {
window.location.href = "/billing";
}}
title="Account Suspended"
message="Your account has been suspended due to billing issues. Please update your billing to continue using ZeroLagHub."
confirmText="Go to Billing"
cancelText="Close"
/>
);
}

View File

@ -0,0 +1,25 @@
import Link from "next/link";
const BillingSummaryCard = ({ status, daysRemaining }: { status: string; daysRemaining?: number }) => {
const statusColor = {
active: "text-green-400",
past_due: "text-yellow-400",
suspended: "text-red-500",
}[status] || "text-white";
const message = {
active: "Your account is in good standing.",
past_due: "Your account is past due. Please resolve soon.",
suspended: `Account suspended. ${daysRemaining ? `${daysRemaining} day(s) remaining before full lockout.` : ""}`,
}[status] || "Billing status unknown.";
return (
<div className="bg-darkGray p-4 rounded shadow text-center">
<h3 className="text-lg font-bold text-white mb-2">💳 Billing Summary</h3>
<p className={`${statusColor} font-semibold`}>{message}</p>
<Link href="/billing" className="text-electricBlue underline text-sm mt-2 inline-block">Manage Billing</Link>
</div>
);
};
export default BillingSummaryCard;

View File

@ -0,0 +1,54 @@
"use client";
import { useEffect, useState } from "react";
import HubNavbar from "@/components/HubNavbar";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import { Toaster } from "react-hot-toast";
import { useAuth } from "@/context/authContext";
import { usePathname, useRouter } from "next/navigation";
const bypassRoutes = ["/billing", "/logout", "/account-suspended"];
export default function ClientLayout({ children }: { children: React.ReactNode }) {
const { billingStatus, suspensionDaysRemaining } = useAuth();
const pathname = usePathname();
const router = useRouter();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (!mounted) return;
const isBypassRoute = bypassRoutes.some((route) => pathname?.startsWith(route));
const isSuspended = billingStatus === "suspended" && suspensionDaysRemaining === 0;
if (isSuspended && !isBypassRoute) {
console.warn("🔒 Lockdown triggered. Redirecting to /account-suspended.");
router.replace("/account-suspended");
}
}, [billingStatus, suspensionDaysRemaining, pathname, mounted, router]);
return (
<>
<Toaster position="top-center" reverseOrder={false} />
{/* Show HubNavbar only on /dashboard or /servers */}
{pathname?.startsWith("/dashboard") || pathname?.startsWith("/servers") ? (
<HubNavbar />
) : (
<Navbar />
)}
<main className="flex-grow pt-16">{children}</main>
<footer className="bg-darkGray text-foreground text-center py-4">
<Footer />
</footer>
</>
);
}

View File

@ -0,0 +1,37 @@
"use client";
import React from "react";
type ExpirationModalProps = {
onRefresh: () => void;
onLogout: () => void;
};
const ExpirationModal: React.FC<ExpirationModalProps> = ({ onRefresh, onLogout }) => {
return (
<div className="fixed z-50 bottom-6 right-6 max-w-sm w-full bg-white border border-gray-200 shadow-lg rounded-xl p-4 animate-fade-in">
<h3 className="text-sm font-semibold text-gray-800 mb-2">
Your session is about to expire
</h3>
<p className="text-sm text-gray-600 mb-4">
Youll be logged out soon. Would you like to stay signed in?
</p>
<div className="flex justify-end gap-2">
<button
onClick={onLogout}
className="px-4 py-1.5 text-sm rounded bg-red-500 text-white hover:bg-red-600 transition"
>
Log Out
</button>
<button
onClick={onRefresh}
className="px-4 py-1.5 text-sm rounded bg-blue-600 text-white hover:bg-blue-700 transition"
>
Stay Logged In
</button>
</div>
</div>
);
};
export default ExpirationModal;

17
src/components/Footer.tsx Normal file
View File

@ -0,0 +1,17 @@
// src/components/Footer.tsx
export default function Footer() {
return (
<footer className="bg-darkGray text-center text-gray-500 py-6">
© {new Date().getFullYear()} ZeroLagHub. All rights reserved.
<br />
<a href="/terms-of-service" className="text-electricBlue underline hover:text-neonGreen">
Terms of Service
</a>{" "}
|{" "}
<a href="/privacy-policy" className="text-electricBlue underline hover:text-neonGreen">
Privacy Policy
</a>
</footer>
);
}

View File

@ -0,0 +1,32 @@
// components/FramedHUDBox.tsx
"use client";
import React from "react";
import clsx from "clsx";
type Props = {
title?: string;
children: React.ReactNode;
className?: string;
};
const FramedHUDBox = ({ title, children, className }: Props) => {
return (
<div className={clsx(
"relative border border-cyan-500 bg-darkGray/80 text-white rounded-md p-5 shadow-lg",
"before:absolute before:-top-2 before:left-4 before:h-2 before:w-6 before:bg-cyan-500",
"after:absolute after:-bottom-2 after:right-4 after:h-2 after:w-6 after:bg-cyan-500",
"hover:shadow-cyan-500/50 transition-all duration-300",
className
)}>
{title && (
<h2 className="text-xs uppercase tracking-wide text-cyan-400 mb-3">
{title}
</h2>
)}
{children}
</div>
);
};
export default FramedHUDBox;

View File

@ -0,0 +1,42 @@
import Image from "next/image";
import { gameInfo } from "@/data/games";
type GameCardProps = {
gameKey: string;
serverName?: string;
status?: "online" | "offline" | "installing";
};
const GameCard = ({ gameKey, serverName, status }: GameCardProps) => {
const game = gameInfo[gameKey] || gameInfo["fallback"];
const statusColor = {
online: "text-green-400",
offline: "text-red-400",
installing: "text-yellow-400",
}[status || "offline"];
return (
<div className="bg-gray-800 rounded-lg shadow-md hover:shadow-cyan-500/50 overflow-hidden w-full max-w-xs transition">
<div className="relative w-full h-36">
<Image
src={game.image}
alt={game.name}
fill
className="object-cover rounded-t-lg"
priority
/>
</div>
<div className="p-4 min-h-[90px] flex flex-col justify-center items-center text-center">
<h3 className="text-white text-lg font-semibold break-words leading-tight">
{serverName || game.name}
</h3>
<p className={`text-sm font-medium mt-1 ${statusColor}`}>
{status?.toUpperCase() || "OFFLINE"}
</p>
</div>
</div>
);
};
export default GameCard;

View File

@ -0,0 +1,89 @@
import Image from "next/image";
import { gameInfo } from "@/data/games";
// 🔁 Replace the broken import with a local type
export type Server = {
uuid: string;
name: string;
game: string;
variant?: string;
status?: string;
};
type Props = {
servers: Server[];
onAction: (uuid: string, action: string) => void;
onConsole: (uuid: string) => void;
onUpgrade: (uuid: string) => void;
};
const GroupedServerList = ({ servers, onAction, onConsole, onUpgrade }: Props) => {
const grouped = servers.reduce<Record<string, Server[]>>((acc, server) => {
const game = server.game || "Unknown";
if (!acc[game]) acc[game] = [];
acc[game].push(server);
return acc;
}, {});
return (
<div className="space-y-12">
{Object.entries(grouped).map(([game, servers]) => {
const gameData = gameInfo[game];
return (
<div key={game}>
<div className="flex items-center space-x-3 mb-4">
{gameData?.image && (
<Image
src={gameData.image}
alt={game}
width={64}
height={64}
className="rounded"
/>
)}
<h2 className="text-2xl text-electricBlue font-bold">{game}</h2>
</div>
<ul className="space-y-4">
{servers.map((server) => (
<li
key={server.uuid}
className="bg-darkGray p-4 rounded shadow flex justify-between items-center"
>
<div>
<p className="text-white font-bold">{server.name}</p>
<p className="text-sm text-gray-400">{server.status}</p>
</div>
<div className="flex gap-2">
<button
className="px-4 py-2 bg-electricBlue text-black rounded hover:bg-neonGreen transition"
onClick={() => onConsole(server.uuid)}
>
Go to Console
</button>
<button
className="px-3 py-2 text-sm bg-blue-700 text-white rounded hover:bg-blue-600 transition"
onClick={() => onUpgrade(server.uuid)}
>
Upgrade
</button>
{["start", "stop", "restart"].map((action) => (
<button
key={action}
onClick={() => onAction(server.uuid, action)}
className="px-3 py-2 text-sm bg-gray-700 text-white rounded hover:bg-gray-600 transition"
>
{action.charAt(0).toUpperCase() + action.slice(1)}
</button>
))}
</div>
</li>
))}
</ul>
</div>
);
})}
</div>
);
};
export default GroupedServerList;

33
src/components/HUDBox.tsx Normal file
View File

@ -0,0 +1,33 @@
// src/components/HUDBox.tsx
"use client";
import React from "react";
import clsx from "clsx";
interface HUDBoxProps {
children: React.ReactNode;
className?: string;
}
const HUDBox: React.FC<HUDBoxProps> = ({ children, className }) => {
return (
<div
className={clsx(
"relative bg-black/40 p-6 rounded-lg overflow-hidden border border-cyan-400/20 shadow-[0_0_20px_#00fff733] backdrop-blur-sm",
className
)}
>
{/* Outer ring glow layer */}
<div className="absolute inset-0 rounded-lg border border-cyan-500/10 blur-sm z-0 pointer-events-none" />
{/* Optional additional framing layer */}
<div className="absolute inset-1 rounded-lg border border-cyan-300/10 z-0 pointer-events-none" />
{/* Content */}
<div className="relative z-10">
{children}
</div>
</div>
);
};
export default HUDBox;

View File

@ -0,0 +1,39 @@
"use client";
import React from "react";
import clsx from "clsx";
interface HUDFramedPanelProps {
title?: string;
icon?: React.ReactNode;
children: React.ReactNode;
className?: string;
}
const HUDFramedPanel: React.FC<HUDFramedPanelProps> = ({ title, icon, children, className }) => {
return (
<div
className={clsx(
"relative bg-black/50 rounded-lg overflow-hidden p-6 border border-cyan-400/20 shadow-[0_0_15px_#00fff733] backdrop-blur-sm",
"before:absolute before:inset-0 before:rounded-lg before:border before:border-cyan-300/10 before:blur-sm before:pointer-events-none",
"after:absolute after:-top-2 after:-left-2 after:w-6 after:h-6 after:border-t-2 after:border-l-2 after:border-cyan-500",
"after:content-[''] before:z-0 z-10",
className
)}
>
{(title || icon) && (
<div className="flex items-center gap-2 mb-4 z-10 relative">
{icon && <span className="text-xl text-electricBlue">{icon}</span>}
{title && (
<h2 className="text-[10px] tracking-widest uppercase text-cyan-300 font-mono">
{title}
</h2>
)}
</div>
)}
<div className="relative z-10">{children}</div>
</div>
);
};
export default HUDFramedPanel;

View File

@ -0,0 +1,35 @@
// src/components/HUDScaffold.tsx
"use client";
import React from "react";
const HUDScaffold = ({ children }: { children: React.ReactNode }) => {
return (
<div className="relative w-full h-full overflow-hidden bg-black text-white">
{/* Curved Display Background */}
<div
className="absolute inset-0 bg-no-repeat bg-center bg-cover z-0 pointer-events-none opacity-40"
style={{
backgroundImage: "url('/assets/hud/slices/hud-bg-base.png')",
}}
/>
{/* HUD Glow Line (optional) */}
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-[80%] h-2 bg-gradient-to-r from-cyan-500/20 via-cyan-300/70 to-cyan-500/20 rounded-full blur-sm animate-hudScanLine" />
{/* Frame Ornaments (optional, can be tuned later) */}
<div className="absolute top-0 left-0 w-6 h-6 border-t-2 border-l-2 border-cyan-500/30" />
<div className="absolute top-0 right-0 w-6 h-6 border-t-2 border-r-2 border-cyan-500/30" />
<div className="absolute bottom-0 left-0 w-6 h-6 border-b-2 border-l-2 border-cyan-500/30" />
<div className="absolute bottom-0 right-0 w-6 h-6 border-b-2 border-r-2 border-cyan-500/30" />
{/* HUD Content */}
<div className="relative z-10">
{children}
</div>
</div>
);
};
export default HUDScaffold;

View File

@ -0,0 +1,72 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Icon } from "@iconify/react";
const links = [
{ href: "/", label: "Home" },
{ href: "/features", label: "Features" },
{ href: "/pricing", label: "Pricing" },
{ href: "/dashboard", label: "Dashboard" },
{ href: "/servers", label: "Servers" },
{ href: "/faq", label: "FAQ" },
{ href: "/support", label: "Support" },
{ href: "/about", label: "About" },
{ href: "/profile", label: "Profile" },
];
const HubNavbar = () => {
const [open, setOpen] = useState(false);
const pathname = usePathname();
const isActive = (href: string) => pathname === href;
return (
<>
{/* Floating Logo & Hub Icon Container */}
<div className="fixed top-4 left-4 z-50 flex flex-col items-start space-y-2">
{/* Logo */}
<div className="text-electricBlue text-lg font-bold">ZeroLagHub</div>
{/* Toggle Button */}
<button
onClick={() => setOpen(!open)}
className="p-2 rounded-full bg-electricBlue text-black hover:bg-neonGreen transition shadow-md"
aria-label="Toggle Navigation Menu"
>
<Icon
icon="mdi:hubspot"
width="24"
height="24"
className={open ? "rotate-90 transition-transform duration-300" : "transition-transform duration-300"}
/>
</button>
</div>
{/* Dropdown Menu anchored below logo+button */}
{open && (
<div className="absolute top-[88px] left-4 z-40 w-48 bg-black/90 backdrop-blur border border-electricBlue rounded-md shadow-lg py-3 px-4 space-y-2">
{links.map(({ href, label }) => (
<Link
key={href}
href={href}
onClick={() => setOpen(false)}
className={`block px-4 py-2 rounded text-sm font-medium transition-all duration-200 ${
isActive(href)
? "bg-electricBlue text-black font-bold shadow-[0_0_10px_#1f8eff80]"
: "text-lightGray hover:text-electricBlue hover:bg-darkGray hover:shadow-[0_0_8px_#1f8eff50]"
}`}
>
{label}
</Link>
))}
</div>
)}
</>
);
};
export default HubNavbar;

View File

@ -0,0 +1,64 @@
"use client";
import React from "react";
import clsx from "clsx";
interface HudPanelProps {
title?: string;
icon?: React.ReactNode;
children: React.ReactNode;
className?: string;
}
const HudPanel: React.FC<HudPanelProps> = ({ title, icon, children, className }) => {
return (
<div
className={clsx(
"relative bg-black/30 rounded-2xl p-6 border border-cyan-500/20 backdrop-blur-md overflow-hidden",
"shadow-[0_0_20px_#00fff722]",
className
)}
>
{/* Animated Glow Border */}
<div className="absolute inset-0 rounded-2xl border border-cyan-400/10 animate-borderFlow z-0" />
{/* Signal Dot */}
<div className="absolute top-2 right-2 w-2 h-2 rounded-full bg-cyan-400 animate-ping z-10" />
{/* Title Section */}
{(title || icon) && (
<div className="flex items-center gap-2 mb-4 relative z-10">
{icon && <span className="text-xl text-electricBlue">{icon}</span>}
{title && (
<h2 className="text-[11px] font-mono uppercase tracking-widest text-cyan-300">
{title}
</h2>
)}
</div>
)}
{/* Content */}
<div className="relative z-10">{children}</div>
{/* Keyframe Styles */}
<style jsx>{`
@keyframes borderFlow {
0% {
box-shadow: 0 0 6px rgba(0, 255, 255, 0.1);
}
50% {
box-shadow: 0 0 12px rgba(0, 255, 255, 0.25);
}
100% {
box-shadow: 0 0 6px rgba(0, 255, 255, 0.1);
}
}
.animate-borderFlow {
animation: borderFlow 6s ease-in-out infinite;
}
`}</style>
</div>
);
};
export default HudPanel;

View File

@ -0,0 +1,50 @@
"use client";
import { useEffect, useState } from "react";
import api from "@/lib/api-client";
type Announcement = {
id: number;
content: string; // Stored as HTML
created_at: string;
};
const MessageBoard = () => {
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchAnnouncements = async () => {
try {
const res = await api.get("/api/announcements");
setAnnouncements(res.data.announcements || []);
} catch (err) {
console.error("Failed to load announcements", err);
} finally {
setLoading(false);
}
};
fetchAnnouncements();
}, []);
return (
<div className="text-lightGray space-y-4">
{loading ? (
<p>Loading...</p>
) : announcements.length === 0 ? (
<p>No recent announcements.</p>
) : (
announcements.map((announcement) => (
<div
key={announcement.id}
className="bg-darkGray p-3 rounded"
dangerouslySetInnerHTML={{ __html: announcement.content }}
/>
))
)}
</div>
);
};
export default MessageBoard;

52
src/components/Modal.tsx Normal file
View File

@ -0,0 +1,52 @@
"use client";
import React from "react";
interface ModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm?: () => void;
title?: string;
message?: string;
confirmText?: string;
cancelText?: string;
}
const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
onConfirm,
title = "Are you sure?",
message = "Your session is about to expire.",
confirmText = "Stay Logged In",
cancelText = "Logout",
}) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-darkGray p-6 rounded-lg shadow-lg text-center w-full max-w-sm border border-electricBlue">
<h2 className="text-lg font-bold text-electricBlue mb-4">{title}</h2>
<p className="text-foreground mb-6">{message}</p>
<div className="flex justify-center gap-4">
<button
onClick={onClose}
className="px-4 py-2 rounded bg-red-600 text-white hover:bg-red-700 transition"
>
{cancelText}
</button>
{onConfirm && (
<button
onClick={onConfirm}
className="px-4 py-2 rounded bg-neonGreen text-black font-bold hover:bg-electricBlue transition"
>
{confirmText}
</button>
)}
</div>
</div>
</div>
);
};
export default Modal;

119
src/components/Navbar.tsx Normal file
View File

@ -0,0 +1,119 @@
"use client";
import React, { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import Modal from "@/components/Modal";
import { useAuth } from "@/context/authContext";
import SuspensionBanner from "@/components/SuspensionBanner";
import UserDropdownMenu from "@/components/UserDropdownMenu";
const Navbar: React.FC = () => {
const {
token,
setToken,
showModal,
refreshToken,
logout,
profile,
suspensionDaysRemaining,
} = useAuth();
const [nickname, setNickname] = useState("User");
const pathname = usePathname();
useEffect(() => {
if (profile?.nickname) {
setNickname(profile.nickname);
} else {
setNickname("User");
}
}, [profile]);
const handleLogout = () => {
setToken(null);
window.location.href = "/login";
};
const isActive = (path: string) => pathname === path;
return (
<>
<SuspensionBanner daysRemaining={suspensionDaysRemaining} />
<nav className="bg-black/80 backdrop-blur-sm border-b border-electricBlue text-foreground flex items-center justify-between px-6 py-3 sticky top-0 z-50 shadow-md">
{/* Logo */}
<Link
href="/"
className="text-electricBlue text-xl font-bold hover:text-neonGreen transition"
>
ZeroLagHub
</Link>
{/* Nav Links */}
<div className="hidden md:flex space-x-4 text-sm">
{[
{ href: "/", label: "Home" },
{ href: "/features", label: "Features" },
{ href: "/pricing", label: "Pricing" },
...(token
? [
{ href: "/dashboard", label: "Dashboard" },
{ href: "/servers", label: "Servers" },
]
: []),
{ href: "/faq", label: "FAQ" },
{ href: "/support", label: "Support" },
{ href: "/about", label: "About" },
].map((link) => (
<Link
key={link.href}
href={link.href}
className={`px-2 py-1 rounded-md transition
${
isActive(link.href)
? "text-electricBlue font-bold border-b-2 border-neonGreen"
: "text-lightGray font-medium hover:text-electricBlue hover:bg-darkGray"
}`}
>
{link.label}
</Link>
))}
</div>
{/* Auth Buttons or User Menu */}
{token ? (
<UserDropdownMenu nickname={nickname} onLogout={handleLogout} />
) : (
<div className="hidden md:flex gap-2">
<Link
href="/login"
className="px-3 py-1 rounded-md bg-darkGray text-lightGray font-semibold hover:bg-gray-700 transition"
>
Login
</Link>
<Link
href="/register"
className="px-3 py-1 rounded-md bg-electricBlue text-black font-semibold hover:bg-neonGreen transition"
>
Register
</Link>
</div>
)}
</nav>
{/* Session Modal */}
<Modal
isOpen={showModal}
onClose={logout}
onConfirm={refreshToken}
title="Session Expiring"
message="Your session is about to expire. Would you like to stay logged in?"
confirmText="Stay Logged In"
cancelText="Logout"
/>
</>
);
};
export default Navbar;

View File

@ -0,0 +1,41 @@
"use client";
import React from "react";
import { CircularProgressbar, buildStyles } from "react-circular-progressbar";
import "react-circular-progressbar/dist/styles.css";
interface RadialStatProps {
value: number;
label: string;
color?: string;
max?: number;
}
const RadialStat: React.FC<RadialStatProps> = ({ value, label, color = "#00FFF7", max = 100 }) => {
const percent = Math.min((value / max) * 100, 100);
return (
<div className="relative text-center">
{/* Outer glow ring */}
<div className="absolute inset-0 z-0 rounded-full shadow-[0_0_20px_4px_rgba(0,255,247,0.2)]" />
{/* Ring meter */}
<div className="relative z-10">
<CircularProgressbar
value={percent}
text={`${Math.round(value)}${max === 100 ? "%" : " MB"}`}
styles={buildStyles({
pathColor: color,
trailColor: "#222",
textColor: "#fff",
textSize: "16px"
})}
/>
</div>
<p className="text-xs mt-2 uppercase tracking-wide text-cyan-300 font-mono">{label}</p>
</div>
);
};
export default RadialStat;

View File

@ -0,0 +1,58 @@
import Image from "next/image";
import { gameInfo } from "@/data/games";
type ServerCardProps = {
name: string;
game: string;
status: string;
children: React.ReactNode;
};
const ServerCard = ({ name, game, status, children }: ServerCardProps) => {
const normalizedKey = typeof game === "string"
? game.toLowerCase().replace(/\s+/g, "_")
: "fallback";
const gameData = gameInfo[normalizedKey] || gameInfo["fallback"];
console.log("🧠 Raw game key:", game);
const statusColor = {
online: "text-green-400",
offline: "text-red-500",
starting: "text-yellow-400",
stopping: "text-gray-400",
suspended: "text-orange-400",
unknown: "text-gray-400",
}[status || "unknown"];
return (
<div className="flex items-center bg-darkGray rounded-lg shadow-lg overflow-hidden p-3 w-full">
{/* Game Thumbnail */}
<div className="relative w-52 h-50 rounded overflow-hidden flex-shrink-0">
<Image
src={gameData.image}
alt={gameData.name}
width={428}
height={400}
className="rounded object-cover"
priority
/>
</div>
{/* Server Info */}
<div className="flex flex-col justify-center ml-4 flex-grow">
<p className="text-lightGray font-semibold text-base leading-tight">
{name}
</p>
<span className={`text-sm font-medium mt-1 ${statusColor}`}>
{status.toUpperCase()}
</span>
</div>
{/* Action Buttons */}
<div className="flex gap-2 items-center">{children}</div>
</div>
);
};
export default ServerCard;

View File

@ -0,0 +1,31 @@
// src/components/ServerHUDOverview.tsx
"use client";
import React from "react";
interface Props {
total: number;
online: number;
offline: number;
}
const ServerHUDOverview: React.FC<Props> = ({ total, online, offline }) => {
const StatBlock = ({ label, value, color }: { label: string; value: number; color: string }) => (
<div className="text-center px-4 py-3 bg-black/60 rounded border border-cyan-400/10 shadow-inner">
<h3 className="text-[10px] text-cyan-200 tracking-widest uppercase font-mono mb-1">{label}</h3>
<div className={`text-3xl font-bold`} style={{ color }}>
{value}
</div>
</div>
);
return (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-center">
<StatBlock label="Total Servers" value={total} color="#1f9fff" />
<StatBlock label="Online" value={online} color="#00ff88" />
<StatBlock label="Offline" value={offline} color="#ff4d4d" />
</div>
);
};
export default ServerHUDOverview;

View File

@ -0,0 +1,30 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
import { useAuth } from "@/context/authContext";
import { useEffect, useMemo } from "react";
const bypassRoutes = ["/billing", "/logout", "/account-suspended"];
export default function SuspendedRedirect({ children }: { children: React.ReactNode }) {
const { billingStatus, suspensionDaysRemaining } = useAuth();
const pathname = usePathname();
const router = useRouter();
const isBypassRoute = useMemo(
() => bypassRoutes.some((route) => pathname?.startsWith(route)),
[pathname]
);
useEffect(() => {
const isSuspended = billingStatus === "suspended" && suspensionDaysRemaining === 0;
const hasSeenRedirect = sessionStorage.getItem("seenSuspensionRedirect") === "true";
if (isSuspended && !isBypassRoute && !hasSeenRedirect) {
sessionStorage.setItem("seenSuspensionRedirect", "true");
router.replace("/account-suspended");
}
}, [billingStatus, suspensionDaysRemaining, isBypassRoute, router]);
return <>{children}</>;
}

View File

@ -0,0 +1,21 @@
// components/SuspensionBanner.tsx
"use client";
import React from "react";
interface Props {
daysRemaining: number | null;
}
const SuspensionBanner: React.FC<Props> = ({ daysRemaining }) => {
if (daysRemaining === null || daysRemaining <= 0) return null;
return (
<div className="w-full bg-yellow-500 text-black text-center py-2 font-semibold">
Account suspension in {daysRemaining} day{daysRemaining > 1 ? "s" : ""}
</div>
);
};
export default SuspensionBanner;

View File

@ -0,0 +1,35 @@
"use client";
import React from "react";
import { Icon } from "@iconify/react";
const SystemControlHUD: React.FC = () => {
return (
<div
className="relative bg-black/50 border border-cyan-400/30 shadow-[0_0_25px_#00fff722] p-6 rounded-lg overflow-hidden"
style={{
clipPath:
"polygon(0 10px, 10px 0, calc(100% - 10px) 0, 100% 10px, 100% calc(100% - 10px), calc(100% - 10px) 100%, 10px 100%, 0 calc(100% - 10px))"
}}
>
{/* Animated Glow Ring */}
<div className="absolute inset-0 rounded-lg border-2 border-cyan-300 animate-pulse opacity-10 z-0" />
{/* Animated Background Grid (optional SVG pattern) */}
<div className="absolute inset-0 bg-[radial-gradient(circle,#00fff722_1px,transparent_1px)] bg-[size:20px_20px] opacity-5 z-0" />
{/* Center Radar Ping */}
<div className="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10">
<Icon icon="mdi:radar" fontSize={64} className="text-cyan-400 animate-pulse opacity-20" />
</div>
{/* HUD Content */}
<div className="relative z-20 text-center">
<h2 className="text-[10px] font-mono uppercase tracking-widest text-cyan-300 mb-1">System Control</h2>
<p className="text-sm text-lightGray">Welcome to the central hub interface.</p>
</div>
</div>
);
};
export default SystemControlHUD;

View File

@ -0,0 +1,49 @@
// src/components/TechFramePanel.tsx
import React from "react";
import clsx from "clsx";
interface Props {
title?: string;
children: React.ReactNode;
className?: string;
}
const TechFramePanel = ({ title, children, className }: Props) => {
return (
<div
className={clsx(
"relative p-5 text-white bg-gradient-to-br from-[#0f1114] to-[#0b0d11]",
"border border-cyan-400/20 shadow-[0_0_20px_#00ffff33]",
"backdrop-blur-md overflow-hidden",
"clip-bevel-frame",
"transform-gpu rotate-x-[3deg] hover:scale-[1.02] perspective-[1200px]",
className
)}
>
{/* 🔷 Curved Holographic Illusion */}
<div className="absolute inset-0 z-0 curved-holo-backdrop" />
{/* Glow Frame Overlay */}
<div className="absolute inset-0 z-0 pointer-events-none before:absolute before:inset-0 before:rounded-md before:border-2 before:border-cyan-400/10 before:shadow-[inset_0_0_30px_#00ffff22] before:content-['']" />
{/* Bevel Edge */}
<div className="absolute inset-0 z-0 pointer-events-none after:absolute after:inset-1 after:border-t after:border-l after:border-cyan-500/10 after:rounded-md after:content-['']" />
{/* Optional Scanning Line */}
<div className="absolute inset-0 z-0 bg-gradient-to-r from-transparent via-cyan-500/5 to-transparent animate-scanStripe" />
{/* Content */}
<div className="relative z-10">
{title && (
<h3 className="text-xs uppercase tracking-wider text-cyan-400 font-semibold mb-3">
{title}
</h3>
)}
{children}
</div>
</div>
);
};
export default TechFramePanel;

View File

@ -0,0 +1,7 @@
// components/TopHUDBar.tsx
const TopHUDBar = () => (
<div className="w-full h-3 bg-gradient-to-r from-cyan-500 via-transparent to-cyan-500 opacity-50 blur-sm mt-2 mb-4" />
);
export default TopHUDBar;

View File

@ -0,0 +1,64 @@
// components/UserDropdownMenu.tsx
"use client";
import React, { useState, useRef, useEffect } from "react";
import Link from "next/link";
interface Props {
nickname: string;
onLogout: () => void;
}
const UserDropdownMenu: React.FC<Props> = ({ nickname, onLogout }) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// Close dropdown on outside click
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen((prev) => !prev)}
className="flex items-center justify-center w-10 h-10 rounded-full bg-electricBlue text-black font-bold text-sm hover:bg-neonGreen transition"
>
{nickname.charAt(0).toUpperCase()}
</button>
{isOpen && (
<div className="absolute right-0 mt-2 bg-darkGray text-lightGray rounded shadow-lg w-40">
<Link
href="/profile"
className="block px-4 py-2 hover:bg-electricBlue hover:text-black"
>
Profile
</Link>
<Link
href="/billing"
className="block px-4 py-2 hover:bg-electricBlue hover:text-black"
>
Billing
</Link>
<button
onClick={onLogout}
className="block w-full text-left px-4 py-2 hover:bg-electricBlue hover:text-black"
>
Logout
</button>
</div>
)}
</div>
);
};
export default UserDropdownMenu;

View File

@ -0,0 +1,48 @@
"use client";
import React from "react";
import { Icon } from "@iconify/react";
const WelcomeCoreHUD: React.FC<{ username: string }> = ({ username }) => {
return (
<div className="relative rounded-2xl p-6 border border-cyan-500/20 bg-black/30 shadow-[0_0_20px_#00fff722] backdrop-blur-md overflow-hidden">
{/* Border Flow Animation Layer */}
<div className="absolute inset-0 rounded-2xl pointer-events-none z-0">
<div className="absolute inset-0 rounded-2xl border border-cyan-400/20 animate-borderFlow" />
</div>
{/* HUD Content */}
<div className="relative z-10 text-center">
<div className="flex justify-center items-center gap-2 mb-1">
<Icon icon="mdi:account-circle-outline" fontSize={28} className="text-cyan-300" />
<h2 className="text-[11px] font-mono uppercase tracking-widest text-cyan-200">Welcome Back</h2>
</div>
<h3 className="text-xl font-bold text-electricBlue drop-shadow-[0_0_4px_#1f9fff88]">
{username}
</h3>
<p className="text-sm text-lightGray mt-1">Your hub is connected and stable.</p>
</div>
{/* Keyframe style */}
<style jsx>{`
@keyframes borderFlow {
0% {
box-shadow: 0 0 8px rgba(0, 255, 255, 0.1);
}
50% {
box-shadow: 0 0 12px rgba(0, 255, 255, 0.3);
}
100% {
box-shadow: 0 0 8px rgba(0, 255, 255, 0.1);
}
}
.animate-borderFlow {
animation: borderFlow 6s linear infinite;
}
`}</style>
</div>
);
};
export default WelcomeCoreHUD;

View File

@ -0,0 +1,33 @@
"use client";
import React from "react";
type Props = {
children: React.ReactNode;
};
const SteelLayout = ({ children }: Props) => {
return (
<div className="relative min-h-screen text-foreground overflow-hidden">
{/* Brushed Steel Background */}
<div
className="bg-steel-texture"
aria-hidden="true"
/>
{/* Optional Ambient Glow (subtle radial light effect) */}
<div
className="absolute inset-0 z-0 pointer-events-none"
style={{
background: "radial-gradient(ellipse at center, rgba(0,255,255,0.02), transparent)",
}}
aria-hidden="true"
/>
{/* Content Layer */}
<main className="relative z-10">{children}</main>
</div>
);
};
export default SteelLayout;

View File

@ -0,0 +1,49 @@
"use client";
type ServerControlsProps = {
status?: "online" | "offline" | "starting" | "stopping" | "unknown";
};
export default function ServerControls({ status = "unknown" }: ServerControlsProps) {
return (
<section className="rounded-lg border border-white/10 bg-black/40 p-4 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-xs uppercase tracking-widest text-white/60">
Server Controls
</p>
<p className="mt-1 text-sm text-white/80">Status: {status}</p>
</div>
<div className="flex gap-2">
<button
className="rounded-md border border-white/20 px-3 py-1 text-xs text-white/60"
type="button"
disabled
aria-disabled="true"
>
Start
</button>
<button
className="rounded-md border border-white/20 px-3 py-1 text-xs text-white/60"
type="button"
disabled
aria-disabled="true"
>
Stop
</button>
<button
className="rounded-md border border-white/20 px-3 py-1 text-xs text-white/60"
type="button"
disabled
aria-disabled="true"
>
Restart
</button>
</div>
</div>
<p className="mt-3 text-xs text-white/50">
Actions are disabled until the control plane API is wired in.
</p>
</section>
);
}

View File

@ -0,0 +1,44 @@
"use client";
import { useEffect, useRef } from "react";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import "xterm/css/xterm.css";
type TerminalViewProps = {
welcomeMessage?: string;
};
export default function TerminalView({
welcomeMessage = "Terminal ready. Awaiting connection...",
}: TerminalViewProps) {
const containerRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<Terminal | null>(null);
useEffect(() => {
if (!containerRef.current) {
return;
}
const terminal = new Terminal({
cursorBlink: true,
scrollback: 2000,
fontSize: 13,
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.open(containerRef.current);
fitAddon.fit();
terminal.writeln(welcomeMessage);
terminalRef.current = terminal;
return () => {
terminalRef.current?.dispose();
terminalRef.current = null;
};
}, [welcomeMessage]);
return <div ref={containerRef} className="h-full w-full" />;
}

174
src/context/authContext.tsx Normal file
View File

@ -0,0 +1,174 @@
"use client";
import React, { createContext, useContext, useState, useEffect } from "react";
interface UserProfile {
email: string;
username: string;
first_name: string;
last_name: string;
nickname?: string;
billing_status?: string;
suspended_at?: string | null;
}
interface AuthContextProps {
token: string | null;
setToken: (token: string | null) => void;
isReady: boolean;
tokenRestored: boolean;
showModal: boolean;
refreshToken: () => void;
logout: () => void;
profile: UserProfile | null;
billingStatus: string | null;
suspensionDaysRemaining: number | null;
isLoadingProfile: boolean;
}
const AuthContext = createContext<AuthContextProps | undefined>(undefined);
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [token, setTokenState] = useState<string | null>(null);
const [isReady, setIsReady] = useState(false);
const [tokenRestored, setTokenRestored] = useState(false);
const [showModal, setShowModal] = useState(false);
const [profile, setProfile] = useState<UserProfile | null>(null);
const [billingStatus, setBillingStatus] = useState<string | null>(null);
const [suspensionDaysRemaining, setSuspensionDaysRemaining] = useState<number | null>(null);
const [isLoadingProfile, setIsLoadingProfile] = useState(true);
// Load token from localStorage on initial mount
useEffect(() => {
const storedToken = localStorage.getItem("token");
if (storedToken) {
setTokenState(storedToken);
}
setTokenRestored(true);
setIsReady(true);
}, []);
const setToken = (newToken: string | null) => {
if (newToken) {
localStorage.setItem("token", newToken);
} else {
localStorage.removeItem("token");
}
setTokenState(newToken);
};
const refreshToken = async () => {
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/auth/refresh-token`, {
method: "POST",
credentials: "include",
});
if (!response.ok) {
throw new Error("Token refresh failed");
}
const data = await response.json();
console.log("🔁 Token refreshed:", data.token);
setToken(data.token);
setShowModal(false);
} catch (err) {
console.error("🔒 Refresh failed:", err);
logout();
}
};
const logout = () => {
setToken(null);
setProfile(null);
setBillingStatus(null);
setSuspensionDaysRemaining(null);
localStorage.removeItem("token");
window.location.href = "/login";
};
// Profile Fetch & Suspension Countdown
useEffect(() => {
const fetchProfile = async () => {
if (!token) {
console.warn("⚠️ No token set for profile fetch.");
setIsLoadingProfile(false);
return;
}
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/profile`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error("Failed to fetch profile");
}
const data = await response.json();
console.log("✅ Profile fetched:", data);
setProfile(data);
setBillingStatus(data.billing_status || null);
if (data.suspended_at) {
const suspendedAt = new Date(data.suspended_at);
const now = new Date();
const diffMs = now.getTime() - suspendedAt.getTime();
const elapsedDays = diffMs / (1000 * 60 * 60 * 24); // days as float
const daysRemaining = Math.max(0, Math.floor(7 - elapsedDays));
console.log("📆 suspended_at:", suspendedAt.toISOString());
console.log("⏳ suspensionDaysRemaining (floor):", daysRemaining);
setSuspensionDaysRemaining(daysRemaining);
} else {
console.log(" No suspended_at value found.");
setSuspensionDaysRemaining(null);
}
} catch (error) {
console.error("❌ Failed to fetch profile:", error);
setProfile(null);
setBillingStatus(null);
setSuspensionDaysRemaining(null);
} finally {
setIsLoadingProfile(false);
}
};
if (tokenRestored && token) {
fetchProfile();
}
}, [tokenRestored, token]);
return (
<AuthContext.Provider
value={{
token,
setToken,
isReady,
tokenRestored,
showModal,
refreshToken,
logout,
profile,
billingStatus,
suspensionDaysRemaining,
isLoadingProfile,
}}
>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error("useAuth must be used within an AuthProvider");
return context;
};

29
src/data/games.ts Normal file
View File

@ -0,0 +1,29 @@
export const gameInfo: Record<string, { name: string; image: string }> = {
minecraft: { name: "Minecraft", image: "/img/games/minecraft.png" },
valheim: { name: "Valheim", image: "/img/games/valheim.png" },
project_zomboid: { name: "Project Zomboid", image: "/img/games/project_zomboid.png" },
rust: { name: "Rust", image: "/img/games/rust.png" },
terraria: { name: "Terraria", image: "/img/games/terraria.png" },
// Minecraft variants
paper: { name: "Paper (Minecraft)", image: "/img/games/minecraft.png" },
forge_enhanced: { name: "Forge Enhanced", image: "/img/games/minecraft.png" },
fabric: { name: "Fabric", image: "/img/games/minecraft.png" },
vanilla_bedrock: { name: "Vanilla Bedrock", image: "/img/games/minecraft.png" },
pocketminemp: { name: "PocketmineMP", image: "/img/games/minecraft.png" },
// Valheim mods
valheim_plus_mod: { name: "Valheim Plus Mod", image: "/img/games/valheim.png" },
valheim_bepinex: { name: "Valheim BepInEx", image: "/img/games/valheim.png" },
// Rust variants
rust_autowipe: { name: "Rust Autowipe", image: "/img/games/rust.png" },
rust_staging: { name: "Rust Staging", image: "/img/games/rust.png" },
// Terraria mods
tmodloader: { name: "tModLoader", image: "/img/games/terraria.png" },
tshock: { name: "tShock", image: "/img/games/terraria.png" },
fallback: { name: "Unknown Game", image: "/img/games/fallback.png" },
};

View File

@ -0,0 +1,46 @@
// src/hooks/useAccessControl.ts
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/context/authContext";
import api from "@/lib/api-client"; // ✅ Using Axios instead of fetch
export const useAccessControl = (options = { autoRedirect: false }) => {
const { token } = useAuth();
const router = useRouter();
const [status, setStatus] = useState<"unknown" | "active" | "suspended" | "past_due">("unknown");
useEffect(() => {
const fetchStatus = async () => {
try {
const res = await api.get("/api/users/profile", {
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = res.data;
setStatus(data.billing_status ?? "active"); // fallback to active if missing
} catch (err) {
console.error("❌ Failed to fetch billing status:", err);
}
};
if (token) fetchStatus();
}, [token]);
useEffect(() => {
if (options.autoRedirect && status === "suspended") {
router.push("/billing-issue");
}
}, [status, options.autoRedirect, router]);
return {
billingStatus: status,
isSuspended: status === "suspended",
isActive: status === "active",
};
};

62
src/lib/api-client.ts Normal file
View File

@ -0,0 +1,62 @@
"use client";
import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from "axios";
import { refreshAccessToken } from "@/utils/tokenUtils";
interface RetryableRequest extends InternalAxiosRequestConfig {
_retry?: boolean;
}
const apiBaseUrl =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "https://api.zerolaghub.com";
const apiClient: AxiosInstance = axios.create({
baseURL: apiBaseUrl,
withCredentials: true,
});
const getBrowserToken = () => {
if (typeof window === "undefined") {
return null;
}
return window.localStorage.getItem("token");
};
apiClient.interceptors.request.use((config) => {
const token = getBrowserToken();
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as RetryableRequest | undefined;
if (
error.response?.status === 401 &&
originalRequest &&
!originalRequest._retry
) {
originalRequest._retry = true;
const newToken = await refreshAccessToken();
if (newToken && originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return apiClient(originalRequest);
}
if (typeof window !== "undefined") {
window.localStorage.removeItem("token");
window.location.href = "/login";
}
}
return Promise.reject(error);
}
);
export default apiClient;

14
src/lib/sockets.ts Normal file
View File

@ -0,0 +1,14 @@
"use client";
type SocketConfig = {
url: string;
protocols?: string | string[];
};
export const createSocket = ({ url, protocols }: SocketConfig) => {
if (typeof window === "undefined") {
throw new Error("WebSocket can only be created in the browser.");
}
return new WebSocket(url, protocols);
};

9
src/services/routes.ts Normal file
View File

@ -0,0 +1,9 @@
// src/services/routes.ts
export const NESTS_ROUTE = "/api/nests"; // For fetching nests
export const EGGS_ROUTE = (nestId: number) => `/api/nests/${nestId}/eggs`; // For fetching eggs in a specific nest
export const LOGIN_ROUTE = "/auth/login"; // For user login
export const REGISTER_ROUTE = "/auth/register"; // For user registration
export const ALLOCATIONS_ROUTE = "api/environment/allocations"; // For fetching allocations
export const CREATE_SERVER_ROUTE = "/servers/create"; // For creating a server
export const SERVERS_ROUTE = "/servers"; // For fetching servers

38
src/styles.old/About.css Normal file
View File

@ -0,0 +1,38 @@
/* Page background */
body {
background: linear-gradient(135deg, #000000, #1a1a1a 70%);
margin: 0;
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
}
/* About container styling */
.about-container {
width: 100%;
max-width: 800px;
margin: 40px auto;
padding: 20px;
background: none;
color: #ffffff;
font-family: Arial, sans-serif;
text-align: center;
box-sizing: border-box;
}
/* Title styling */
.about-title {
font-size: 2.5rem;
color: #00b7ff;
margin-bottom: 20px;
text-shadow: 0 0 15px rgba(0, 183, 255, 0.8), 0 0 25px rgba(0, 183, 255, 0.5);
}
/* About text */
.about-text {
font-size: 1rem;
line-height: 1.6;
color: #bbbbbb;
margin-bottom: 30px;
}

59
src/styles.old/App.css Normal file
View File

@ -0,0 +1,59 @@
body {
background-color: #000; /* Set a black background */
color: #00f5d4; /* Adjust text color for contrast */
font-family: 'Arial', sans-serif; /* Replace with your preferred font */
margin: 0;
padding: 0;
height: 100vh;
width: 100%;
}
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
background-color: transparent; /* Ensure the root does not overwrite the body background */
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); /* Optional: Add a subtle shadow */
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
background-color: #111; /* Optional: Add a subtle card background */
border-radius: 10px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.3); /* Subtle shadow for better aesthetics */
}
.read-the-docs {
color: #888;
}

View File

@ -0,0 +1,92 @@
/* General container styling */
.create-server-container {
max-width: 600px;
margin: 20px auto;
padding: 20px;
background-color: #1c1c1c;
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
color: #f5f5f5;
font-family: Arial, sans-serif;
}
/* Form heading */
.create-server-container h1 {
text-align: center;
margin-bottom: 20px;
font-size: 24px;
color: #00aaff;
}
/* Input and select fields styling */
.server-form label {
display: block;
margin-bottom: 10px;
font-weight: bold;
}
.server-form input,
.server-form select {
width: 100%;
padding: 10px;
margin-bottom: 20px;
border: 1px solid #444;
border-radius: 5px;
background-color: #2c2c2c;
color: #f5f5f5;
font-size: 14px;
}
.server-form input:focus,
.server-form select:focus {
outline: none;
border-color: #00aaff;
}
/* Advanced options checkbox */
.server-form input[type="checkbox"] {
width: auto;
margin-right: 10px;
}
/* Button styling */
.server-form button {
width: 100%;
padding: 12px;
border: none;
border-radius: 5px;
background-color: #00aaff;
color: #f5f5f5;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.server-form button:disabled {
background-color: #666;
cursor: not-allowed;
}
.server-form button:hover:not(:disabled) {
background-color: #007acc;
}
/* Success and error messages */
.success-text,
.error-text {
text-align: center;
margin-bottom: 15px;
padding: 10px;
border-radius: 5px;
font-size: 14px;
}
.success-text {
background-color: #28a745;
color: #fff;
}
.error-text {
background-color: #dc3545;
color: #fff;
}

View File

@ -0,0 +1,72 @@
/* Page background */
body {
background: linear-gradient(135deg, #000000, #1a1a1a 70%);
margin: 0;
padding: 0;
height: 100%; /* Ensure the background covers the full height */
display: flex; /* Add flex to center the content vertically if needed */
flex-direction: column;
}
/* General container styling */
.dashboard-container {
width: 100%;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
background: none; /* Remove any conflicting background from the container */
color: #ffffff; /* Light text for contrast */
min-height: calc(100vh - 80px); /* Ensure it spans the full view height minus navbar */
box-sizing: border-box;
}
/* Dashboard title styling */
.dashboard-title {
font-size: 2.5rem;
color: #00b7ff; /* Neon blue for a modern look */
text-align: center;
margin-bottom: 20px;
text-shadow: 0 0 15px rgba(0, 183, 255, 0.8), 0 0 25px rgba(0, 183, 255, 0.5);
}
.dashboard-description {
text-align: center;
font-size: 1rem;
color: #bbbbbb;
margin-top: 10px;
line-height: 1.5;
opacity: 0;
animation: fadeIn 1.5s ease-in forwards;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
/* Subtitle for instructions or descriptions */
.dashboard-subtitle {
font-size: 1rem;
text-align: center;
color: #cccccc; /* Lighter text for contrast */
margin-bottom: 30px;
}
/* Button styles */
.button-container {
text-align: center;
margin-top: 20px;
}
/* Adjustments for mobile and responsive design */
@media (max-width: 768px) {
.dashboard-title {
font-size: 2rem;
}
.dashboard-container {
padding: 10px;
}
}

47
src/styles.old/Faq.css Normal file
View File

@ -0,0 +1,47 @@
/* Page background */
body {
background: linear-gradient(135deg, #000000, #1a1a1a 70%);
margin: 0;
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
}
/* FAQ container */
.faq-container {
width: 100%;
max-width: 800px;
margin: 40px auto;
padding: 20px;
background: none;
color: #ffffff;
font-family: Arial, sans-serif;
box-sizing: border-box;
}
/* Title styling */
.faq-title {
font-size: 2.5rem;
color: #00b7ff;
margin-bottom: 20px;
text-shadow: 0 0 15px rgba(0, 183, 255, 0.8), 0 0 25px rgba(0, 183, 255, 0.5);
}
/* FAQ items */
.faq-item {
margin-bottom: 20px;
border-bottom: 1px solid #444444;
padding-bottom: 10px;
}
.faq-item h2 {
font-size: 1.5rem;
color: #ffffff;
}
.faq-item p {
font-size: 1rem;
color: #bbbbbb;
margin-top: 10px;
}

View File

@ -0,0 +1,55 @@
/* Features Page Styling */
.features-container {
max-width: 1200px;
margin: auto;
padding: 20px;
font-family: Arial, sans-serif;
color: #ffffff;
background: linear-gradient(135deg, #1a1a1a, #000000);
min-height: calc(100vh - 80px); /* Adjust for navbar height */
}
.features-title {
text-align: center;
font-size: 2.5rem;
color: #00b7ff;
text-shadow: 0 0 15px rgba(0, 183, 255, 0.8), 0 0 25px rgba(0, 183, 255, 0.5);
}
.features-subtitle {
text-align: center;
font-size: 1.2rem;
color: #bbbbbb;
margin: 10px 0 30px 0;
}
.features-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.feature-item {
background-color: #121212;
border-radius: 8px;
padding: 20px;
text-align: center;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
transition: transform 0.3s, box-shadow 0.3s;
}
.feature-item:hover {
transform: translateY(-5px);
box-shadow: 0 6px 15px rgba(0, 183, 255, 0.5);
}
.feature-item h2 {
font-size: 1.5rem;
color: #00b7ff;
margin-bottom: 10px;
}
.feature-item p {
color: #bbbbbb;
font-size: 1rem;
}

85
src/styles.old/Home.css Normal file
View File

@ -0,0 +1,85 @@
/* Gradient Background */
body {
background: linear-gradient(135deg, #000000, #1a1a1a 70%);
margin: 0;
padding: 0;
color: #ffffff;
font-family: "Inter", sans-serif; /* Adjust to match your global font */
}
/* Main Container */
.home-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* Main Content */
.home-content {
flex: 1;
text-align: center;
padding: 2rem;
}
.home-title {
font-size: 3rem;
font-weight: 800;
color: #00b7ff; /* Neon blue */
margin-bottom: 1rem;
text-shadow: 0 0 15px rgba(0, 183, 255, 0.8), 0 0 25px rgba(0, 183, 255, 0.5);
}
.home-subtitle {
font-size: 1.2rem;
color: #cccccc; /* Light gray */
margin-bottom: 2rem;
}
/* Buttons */
.home-buttons {
display: flex;
justify-content: center;
gap: 1.5rem;
}
.home-button {
background-color: #00b7ff;
color: #000;
padding: 0.8rem 2rem;
font-size: 1rem;
font-weight: bold;
text-decoration: none;
border-radius: 5px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s, transform 0.3s;
}
.home-button:hover {
background-color: #007bbd;
transform: scale(1.05);
}
.home-button-alt {
background-color: #555555;
color: #ffffff;
padding: 0.8rem 2rem;
font-size: 1rem;
font-weight: bold;
text-decoration: none;
border-radius: 5px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s, transform 0.3s;
}
.home-button-alt:hover {
background-color: #444444;
transform: scale(1.05);
}
/* Footer */
.home-footer {
text-align: center;
padding: 1rem;
font-size: 0.9rem;
color: #aaaaaa; /* Subtle gray for footer */
}

85
src/styles.old/Login.css Normal file
View File

@ -0,0 +1,85 @@
/* Center the login container */
.login-container {
height: 100vh;
display: flex;
justify-content: center; /* Center horizontally */
align-items: center; /* Center vertically */
background: linear-gradient(135deg, #000, #111 70%); /* Subtle gradient background */
}
/* Style the login card */
.login-card {
display: flex; /* Use flexbox to align items within the card */
flex-direction: column; /* Stack items vertically */
justify-content: center; /* Center items vertically within the card */
align-items: center; /* Center items horizontally within the card */
background: #111; /* Dark card background */
padding: 2rem;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 183, 255, 0.8), 0 0 50px rgba(0, 183, 255, 0.4); /* Blue glow effect */
text-align: center;
width: 90%; /* Responsive width */
max-width: 400px; /* Ensure the card doesn't get too large */
}
/* Style the logo */
.login-logo {
width: 150px;
margin-bottom: 1rem;
border-radius: 10px; /* Add rounded corners to the logo */
}
/* Style the login title */
h1 {
color: #00b7ff; /* Neon blue text */
margin-bottom: 1rem;
}
/* Style the form layout */
form {
display: flex;
flex-direction: column;
gap: 1rem;
}
/* Style the form labels */
label {
color: #00b7ff;
text-align: left;
}
/* Style the input fields */
input {
padding: 0.5rem;
border: 1px solid #00b7ff;
border-radius: 5px;
background: #000;
color: #fff;
width: 100%;
transition: border-color 0.3s ease; /* Smooth transition for border */
}
input::placeholder {
color: #666; /* Placeholder text color */
}
input:focus {
border-color: #007bbd; /* Change border color on focus */
outline: none; /* Remove default browser outline */
}
/* Style the login button */
button {
padding: 0.7rem;
background: #00b7ff;
border: none;
border-radius: 5px;
color: #000;
font-weight: bold;
cursor: pointer;
transition: background 0.3s ease;
}
button:hover {
background: #007bbd; /* Slightly darker hover effect */
}

79
src/styles.old/Navbar.css Normal file
View File

@ -0,0 +1,79 @@
/* Navbar container */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #000; /* Black background */
padding: 10px 20px;
border-bottom: 2px solid #007bff; /* Blue border effect */
font-family: Arial, sans-serif;
}
/* Navbar links container */
.navbar-links {
display: flex;
gap: 20px;
}
/* Navbar link styling */
.navbar-link {
color: #007bff; /* Blue text */
text-decoration: none;
background-color: #000; /* Black background for links */
border: 2px solid #007bff; /* Blue border */
border-radius: 5px;
padding: 8px 16px;
font-size: 16px;
font-weight: bold;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
/* Logout button styling */
.logout-button {
color: #007bff;
background-color: #000; /* Match navbar background */
border: 2px solid #007bff;
border-radius: 5px;
padding: 8px 16px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
/* Hover effects */
.navbar-link:hover,
.logout-button:hover {
background-color: #007bff; /* Blue background on hover */
color: #000; /* Black text on hover */
}
/* Active link styling */
.navbar-link.active {
background-color: #007bff; /* Active blue background */
color: #000; /* Black text */
border-color: #000; /* Black border */
font-weight: bold;
text-shadow: 0 0 10px rgba(0, 183, 255, 0.8); /* Add glow for active link */
}
/* Responsive adjustments */
@media (max-width: 768px) {
.navbar {
flex-direction: column;
align-items: flex-start;
padding: 10px;
}
.navbar-links {
flex-direction: column;
gap: 10px;
}
.logout-button {
align-self: flex-start;
margin-top: 10px;
}
}

View File

@ -0,0 +1,20 @@
.terms-section {
margin-top: 40px;
padding: 20px;
background-color: #1a1a1a;
color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
.terms-section h2 {
color: #00b7ff;
margin-bottom: 10px;
}
.terms-section p {
font-size: 1rem;
line-height: 1.6;
color: #bbbbbb;
margin-bottom: 10px;
}

View File

@ -0,0 +1,69 @@
.register-container {
max-width: 400px;
margin: 50px auto;
padding: 20px;
background-color: #1a1a1a;
color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
font-family: Arial, sans-serif;
}
.register-title {
font-size: 1.8rem;
color: #00b7ff;
text-align: center;
margin-bottom: 20px;
text-shadow: 0 0 10px rgba(0, 183, 255, 0.8);
}
.register-form {
display: flex;
flex-direction: column;
}
.register-form label {
margin-bottom: 15px;
font-weight: bold;
}
.register-form input {
width: 100%;
padding: 10px;
margin-top: 5px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #121212;
color: #ffffff;
}
.register-button {
background-color: #00b7ff;
color: #000;
padding: 10px 15px;
border: none;
border-radius: 5px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s, transform 0.3s;
}
.register-button:hover {
background-color: #007bbd;
transform: scale(1.05);
}
.error-text {
color: #ff4d4d;
text-align: center;
font-weight: bold;
margin-bottom: 10px;
}
.success-text {
color: #4caf50;
text-align: center;
font-weight: bold;
margin-bottom: 10px;
}

View File

@ -0,0 +1,96 @@
/* General container styling */
.servers-container {
width: 100%;
max-width: 1200px;
margin: auto;
padding: 20px;
font-family: Arial, sans-serif;
color: #ffffff;
background-color: #121212; /* Matches theme */
}
/* Title styling */
.servers-title {
font-size: 2rem;
color: #00b7ff;
text-align: center;
margin-bottom: 30px;
text-shadow: 0 0 15px rgba(0, 183, 255, 0.8), 0 0 25px rgba(0, 183, 255, 0.5);
}
/* Server list grid */
.servers-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
/* Individual server item */
.server-item {
background-color: #1a1a1a;
padding: 15px;
border-radius: 8px;
text-align: center;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
transition: transform 0.3s, box-shadow 0.3s ease;
}
.server-item:hover {
transform: translateY(-5px);
box-shadow: 0 6px 15px rgba(0, 183, 255, 0.5);
}
/* Console button styling */
.console-button {
background-color: #00b7ff;
color: #000;
padding: 10px 15px;
font-size: 0.9rem;
font-weight: bold;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s, transform 0.3s ease;
}
.console-button:hover {
background-color: #007bbd;
transform: scale(1.05);
}
/* Create Server button styling */
.create-server-button {
background-color: #00b7ff;
color: #000;
border: none;
padding: 12px 20px; /* Slightly larger padding for consistency */
font-size: 1rem;
font-weight: bold;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s, transform 0.3s ease;
margin-top: 20px; /* Adds spacing between the button and the list */
display: block;
margin-left: auto;
margin-right: auto; /* Center the button */
}
.create-server-button:hover {
background-color: #007bbd;
transform: scale(1.05); /* Slightly enlarge on hover */
}
/* No servers message */
.no-servers-text {
text-align: center;
color: #bbbbbb;
font-style: italic;
}
/* Error message */
.error-text {
text-align: center;
color: #ff4d4d;
font-weight: bold;
margin-bottom: 20px;
}

View File

@ -0,0 +1,79 @@
/* Support container */
.support-container {
max-width: 800px;
margin: 40px auto;
padding: 20px;
background-color: #121212; /* Dark background */
color: #ffffff; /* Light text */
font-family: Arial, sans-serif;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
/* Title styling */
.support-container h1 {
font-size: 2.5rem;
text-align: center;
color: #00b7ff;
margin-bottom: 20px;
text-shadow: 0 0 10px rgba(0, 183, 255, 0.8);
}
/* Form styling */
.support-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.support-form label {
font-weight: bold;
margin-bottom: 5px;
display: block;
}
.support-form input,
.support-form textarea {
width: 100%;
padding: 10px;
border: 1px solid #00b7ff;
border-radius: 5px;
background-color: #000; /* Dark input background */
color: #fff; /* Light text */
font-size: 1rem;
}
.support-form textarea {
resize: none; /* Disable resizing */
}
.support-form button {
background-color: #00b7ff;
color: #000;
border: none;
padding: 10px 15px;
border-radius: 5px;
font-weight: bold;
font-size: 1.2rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
.support-form button:hover {
background-color: #007bbd; /* Slightly darker blue */
}
/* Error and success messages */
.error-text {
color: #ff4d4d;
font-size: 1rem;
margin-bottom: 15px;
text-align: center;
}
.success-text {
color: #4caf50;
font-size: 1rem;
margin-bottom: 15px;
text-align: center;
}

44
src/styles.old/Terms.css Normal file
View File

@ -0,0 +1,44 @@
/* Page background */
body {
background: linear-gradient(135deg, #000000, #1a1a1a 70%);
margin: 0;
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
}
/* Terms container */
.terms-container {
width: 100%;
max-width: 800px;
margin: 40px auto;
padding: 20px;
background: none;
color: #ffffff;
font-family: Arial, sans-serif;
box-sizing: border-box;
}
/* Title styling */
.terms-title {
font-size: 2.5rem;
color: #00b7ff;
margin-bottom: 20px;
text-shadow: 0 0 15px rgba(0, 183, 255, 0.8), 0 0 25px rgba(0, 183, 255, 0.5);
}
/* Section titles */
.terms-section-title {
font-size: 1.5rem;
color: #ffffff;
margin-top: 20px;
margin-bottom: 10px;
}
.terms-text {
font-size: 1rem;
line-height: 1.6;
color: #bbbbbb;
margin-bottom: 20px;
}

1
src/types/css.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module "*.css";

10
src/types/js-cookie.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
declare module "js-cookie" {
interface CookiesStatic {
get(name: string): string | undefined;
set(name: string, value: string, options?: { expires?: number | Date; path?: string }): void;
remove(name: string, options?: { path?: string }): void;
}
const Cookies: CookiesStatic;
export default Cookies;
}

46
src/utils/tokenUtils.ts Normal file
View File

@ -0,0 +1,46 @@
// src/utils/tokenUtils.ts
"use client";
import axios from "axios";
const isBrowser = typeof window !== "undefined";
export const getAccessToken = async (): Promise<string | null> => {
if (!isBrowser) {
return null;
}
const storedToken = window.localStorage.getItem("token");
if (!storedToken) return null;
try {
// Optional: validate token here
return storedToken;
} catch (err) {
console.error("Failed to validate token:", err);
return null;
}
};
export const refreshAccessToken = async (): Promise<string | null> => {
if (!isBrowser) {
return null;
}
try {
const res = await axios.post("https://api.zerolaghub.com/oauth/token", {
grant_type: "refresh_token",
refresh_token: window.localStorage.getItem("refresh_token"),
client_id: process.env.NEXT_PUBLIC_CLIENT_ID,
client_secret: process.env.NEXT_PUBLIC_CLIENT_SECRET,
});
const { access_token, refresh_token } = res.data;
window.localStorage.setItem("token", access_token);
window.localStorage.setItem("refresh_token", refresh_token);
return access_token;
} catch (err) {
console.error("❌ Failed to refresh token:", err);
return null;
}
};

61
tailwind.config.ts Normal file
View File

@ -0,0 +1,61 @@
import type { Config } from "tailwindcss";
export default {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
safelist: ["bg-steel-texture"],
theme: {
extend: {
colors: {
background: "#111215",
foreground: "#F8F9FA",
electricBlue: "#1F8EFF",
neonGreen: "#32FF7E",
dangerRed: "#FF5E5E",
darkGray: "#1A1A1A",
lightGray: "#CCCCCC",
slateSatin: "#0f1218",
obsidian: "#121212",
},
fontFamily: {
heading: ["Geist", "sans-serif"],
mono: ["Geist Mono", "monospace"],
body: ["Roboto", "sans-serif"],
},
boxShadow: {
subtle: "0 2px 4px rgba(0, 0, 0, 0.1)",
glow: "0 0 8px rgba(31, 142, 255, 0.8)",
"glow-inner": "inset 0 0 200px rgba(0, 255, 255, 0.02)",
},
backgroundImage: {
'steel-gradient': "linear-gradient(135deg, #101215, #0b0c0f)",
'glow-overlay': "radial-gradient(ellipse at center, rgba(0,255,255,0.03), transparent)",
'steel-texture': "url('/textures/hdsteel-bg.webp')",
},
keyframes: {
slowShift: {
'0%, 100%': { backgroundPosition: '0% 50%' },
'50%': { backgroundPosition: '100% 50%' },
},
},
animation: {
'slow-shift': 'slowShift 20s ease infinite',
},
screens: {
xs: "480px",
},
maxWidth: {
screenLg: "1280px",
screenMd: "1024px",
screenSm: "768px",
},
},
},
darkMode: "class",
plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")],
} satisfies Config;

Some files were not shown because too many files have changed in this diff Show More