[TSM.ID].[11031972] PXE : Platform X Ecosystem I [118 Module -LIVE-]
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export async function GET() {
|
||||
const cookieStore = await cookies();
|
||||
const tokenString = cookieStore.get("jumpa_token")?.value;
|
||||
|
||||
if (!tokenString) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const payloadBase64 = tokenString.split(".")[1];
|
||||
const payloadBuffer = Buffer.from(payloadBase64, "base64");
|
||||
const user = JSON.parse(payloadBuffer.toString("utf-8"));
|
||||
return NextResponse.json(user);
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid Token" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const ENGINE_FILE_PATH = path.join(process.cwd(), 'engine.json');
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
if (fs.existsSync(ENGINE_FILE_PATH)) {
|
||||
const data = fs.readFileSync(ENGINE_FILE_PATH, 'utf-8');
|
||||
const json = JSON.parse(data);
|
||||
return NextResponse.json({ engine: json.engine || 'xcu ULTRA' });
|
||||
}
|
||||
return NextResponse.json({ engine: 'xcu ULTRA' });
|
||||
} catch {
|
||||
return NextResponse.json({ engine: 'xcu ULTRA' });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
if (body.engine === 'xcu ULTRA' || body.engine === 'XCU Ultra' || body.engine === 'XCU_DIRECTOR') {
|
||||
fs.writeFileSync(ENGINE_FILE_PATH, JSON.stringify({ engine: body.engine }), 'utf-8');
|
||||
return NextResponse.json({ success: true, engine: body.engine });
|
||||
}
|
||||
return NextResponse.json({ success: false, error: 'Invalid engine' }, { status: 400 });
|
||||
} catch {
|
||||
return NextResponse.json({ success: false, error: 'Failed to update engine' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { roomName } = body;
|
||||
|
||||
if (!roomName) {
|
||||
return NextResponse.json({ error: "Room name is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Call the PM2 Vanguard AI webhook
|
||||
// Assume Vanguard AI runs on localhost:3060 on Gamma
|
||||
const response = await fetch('http://127.0.0.1:3060/api/summon', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ roomName })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errData = await response.json().catch(() => ({}));
|
||||
return NextResponse.json({ error: "Vanguard failed to join", details: errData }, { status: 500 });
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json({ success: true, message: data.message });
|
||||
|
||||
} catch (err) {
|
||||
console.error("Summon Vanguard Error:", err);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function POST() {
|
||||
// FASE 82: XCU Ultra Eradicated
|
||||
return NextResponse.json({ success: true, message: "Cohost privileges granted via XCU Protocol" });
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// FASE 99: REAL-TIME QUANTUM TELEMETRY
|
||||
// Menghubungi Engine XCU Ultra di port 8081 (Alpha Node)
|
||||
const telemetryRes = await fetch('http://160.187.143.253:8081/api/v1/telemetry/snapshot', {
|
||||
next: { revalidate: 0 },
|
||||
cache: 'no-store'
|
||||
});
|
||||
|
||||
const telemetry = await telemetryRes.json();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
rooms: [
|
||||
{
|
||||
id: "XCU_QUANTUM_CORE_ALPHA",
|
||||
status: telemetry.status || "BROADCASTING",
|
||||
name: "The Cassandra Matrix",
|
||||
type: "MoQ / WebTransport",
|
||||
participants: telemetry.active_connections || 0,
|
||||
metrics: {
|
||||
cpu: telemetry.cpu_usage,
|
||||
ram: telemetry.ram_usage,
|
||||
threats: telemetry.threats_blocked
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "SANDBOX_ALPHA",
|
||||
status: "ONLINE",
|
||||
name: "Quantum Sandbox",
|
||||
type: "XCU Ultra Engine",
|
||||
participants: Math.floor((telemetry.active_connections || 0) / 10)
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("XCU Engine Unreachable:", error);
|
||||
return NextResponse.json({ success: false, error: "Engine Offline" }, { status: 503 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import crypto from 'crypto';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const room = req.nextUrl.searchParams.get("room");
|
||||
const username = req.nextUrl.searchParams.get("username");
|
||||
|
||||
if (!room || !username) {
|
||||
return NextResponse.json({ error: 'Missing "room" or "username" query parameter' }, { status: 400 });
|
||||
}
|
||||
|
||||
// FASE 82: THE GREAT PURGE
|
||||
// XCU Ultra has been eradicated. We now generate a pure XCU Quantum Token.
|
||||
const tokenPayload = {
|
||||
room,
|
||||
username,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + 3600,
|
||||
matrix_id: crypto.randomUUID()
|
||||
};
|
||||
|
||||
const token = Buffer.from(JSON.stringify(tokenPayload)).toString('base64');
|
||||
|
||||
return NextResponse.json({
|
||||
token: `XCU_${token}`,
|
||||
engineStrategy: 'XCU_DIRECTOR'
|
||||
});
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,96 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-brand: #0b5cff;
|
||||
--color-brand-glow: rgba(11, 92, 255, 0.4);
|
||||
--color-dark-100: #f5f5f5;
|
||||
--color-dark-200: #eaeaea;
|
||||
--color-dark-300: #d4d4d4;
|
||||
|
||||
--font-sans: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #f4f5f9;
|
||||
--foreground: #222222;
|
||||
--glass-bg: rgba(255, 255, 255, 0.85);
|
||||
--glass-border: rgba(0, 0, 0, 0.08);
|
||||
--panel-bg: #ffffff;
|
||||
--panel-border: rgba(0, 0, 0, 0.05);
|
||||
--color-brand: #0b5cff;
|
||||
--safe-top: env(safe-area-inset-top);
|
||||
--safe-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--background: #050b14; /* Deep space navy */
|
||||
--foreground: #f5f5f5;
|
||||
--glass-bg: rgba(10, 16, 29, 0.85);
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--panel-bg: #111b21;
|
||||
--panel-border: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* HarmonyOS, Samsung, Safari & Global Browser Parity Optimization */
|
||||
html, body {
|
||||
height: 100%;
|
||||
min-height: -webkit-fill-available;
|
||||
overflow-x: hidden;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-moz-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
scroll-behavior: smooth;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-sans);
|
||||
margin: 0;
|
||||
padding: var(--safe-top) 0 var(--safe-bottom) 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
touch-action: manipulation;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
/* Touch-Optimized Interactions */
|
||||
button, a, input, select, textarea {
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar for Premium Feel - Cross Browser */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.1); border-radius: 10px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.2); }
|
||||
|
||||
[data-theme="dark"] ::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); }
|
||||
[data-theme="dark"] ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); }
|
||||
|
||||
/* Firefox Scrollbar */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
|
||||
}
|
||||
|
||||
/* Android/Huawei/Samsung Input Reset */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: var(--foreground) !important;
|
||||
-webkit-box-shadow: 0 0 0px 1000px var(--background) inset !important;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
.text-gradient {
|
||||
background: linear-gradient(to right, var(--color-brand), #00d2ff);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import "./globals.css";
|
||||
|
||||
|
||||
|
||||
import type { Viewport } from "next";
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: "#111b21",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "JUMPA.ID | Enterprise Video Conference",
|
||||
description: "Secure B2B WebRTC Gateway",
|
||||
manifest: "/manifest.json",
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: "black-translucent",
|
||||
title: "JUMPA VC",
|
||||
},
|
||||
};
|
||||
|
||||
import crypto from "crypto";
|
||||
|
||||
import { OmniSyncProvider } from "../components/OmniSyncProvider";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
// TSM Versioning Format: [TSM.ID].hh.mm.ss.DD.MM.YYYY.XXXX
|
||||
const date = new Date();
|
||||
const format2 = (n: number) => n.toString().padStart(2, '0');
|
||||
const hh = format2(date.getUTCHours());
|
||||
const mm = format2(date.getUTCMinutes());
|
||||
const ss = format2(date.getUTCSeconds());
|
||||
const DD = format2(date.getUTCDate());
|
||||
const MM = format2(date.getUTCMonth() + 1);
|
||||
const YYYY = date.getUTCFullYear();
|
||||
const XXXX = crypto.randomBytes(2).toString('hex').toUpperCase();
|
||||
const tsmVersion = `[TSM.ID].${hh}.${mm}.${ss}.${DD}.${MM}.${YYYY}.${XXXX}`;
|
||||
|
||||
return (
|
||||
<html lang="id">
|
||||
<head>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" href="/icon512_maskable.png" />
|
||||
<script dangerouslySetInnerHTML={{__html: `
|
||||
// PKX NUCLEAR CACHE BUSTER: Unregister all Service Workers & clear all caches
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations().then(function(registrations) {
|
||||
for (var i = 0; i < registrations.length; i++) {
|
||||
registrations[i].unregister();
|
||||
}
|
||||
});
|
||||
}
|
||||
if ('caches' in window) {
|
||||
caches.keys().then(function(names) {
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
caches.delete(names[i]);
|
||||
}
|
||||
});
|
||||
}
|
||||
`}} />
|
||||
</head>
|
||||
<body className={`antialiased font-sans bg-[#050B14] text-white`}>
|
||||
<OmniSyncProvider initialLocale="id">
|
||||
{children}
|
||||
{/* TSM PERMANENT WATERMARK */}
|
||||
<div className="fixed bottom-1 right-1 z-[9999] opacity-30 pointer-events-none select-none">
|
||||
<span className="text-[8px] font-mono text-white tracking-widest drop-shadow-[0_0_5px_rgba(255,255,255,0.8)]">
|
||||
{tsmVersion}
|
||||
</span>
|
||||
</div>
|
||||
</OmniSyncProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
// [TSM.ID].[11031972] — All Rights Reserved. Proprietary & Confidential.
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const router = useRouter();
|
||||
const [isVerifying, setIsVerifying] = useState(true);
|
||||
const [username, setUsername] = useState("");
|
||||
const [engineStrategy, setEngineStrategy] = useState("XCU_DIRECTOR");
|
||||
|
||||
// States for Join Modal
|
||||
const [showJoinModal, setShowJoinModal] = useState(false);
|
||||
const [roomName, setRoomName] = useState("");
|
||||
|
||||
// State for Chronos Smart Scheduler
|
||||
const [showChronosModal, setShowChronosModal] = useState(false);
|
||||
const [roomMode, setRoomMode] = useState<"standard" | "webinar" | "podcast">("standard");
|
||||
const [chronoDate, setChronoDate] = useState("");
|
||||
const [chronoName, setChronoName] = useState("");
|
||||
const [chronoResult, setChronoResult] = useState<{link: string, text: string} | null>(null);
|
||||
const [schedules, setSchedules] = useState<{id: string, title: string, mode: string, date: string, link: string}[]>([]);
|
||||
|
||||
// State for The Vault
|
||||
const [showVaultModal, setShowVaultModal] = useState(false);
|
||||
const [vaultVideoUrl, setVaultVideoUrl] = useState<string | null>(null);
|
||||
const [vaultVideoName, setVaultVideoName] = useState<string>("");
|
||||
|
||||
// States for Copy Alert
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const resp = await fetch("/vc/api/auth/me");
|
||||
const data = await resp.json();
|
||||
if (data.error) {
|
||||
// Auth failed - show as guest instead of redirecting (prevents loop)
|
||||
setUsername("Guest");
|
||||
setIsVerifying(false);
|
||||
} else {
|
||||
setUsername(data.email); // Full email as display name
|
||||
if (data.mediaEngineStrategy) {
|
||||
setEngineStrategy(data.mediaEngineStrategy);
|
||||
}
|
||||
setIsVerifying(false);
|
||||
}
|
||||
} catch {
|
||||
// Network error - show as guest instead of redirecting (prevents loop)
|
||||
setUsername("Guest");
|
||||
setIsVerifying(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const handleNewMeeting = async () => {
|
||||
// TAHAP NANO: Generate Random Room ID dengan Kriptografi Murni
|
||||
const uuid = crypto.randomUUID().toUpperCase().split('-');
|
||||
const newRoomId = `JMP-${uuid[1]}-${uuid[2]}`;
|
||||
const inviteLink = typeof window !== 'undefined' ? `${window.location.origin}/vc/room/${newRoomId}` : `/vc/room/${newRoomId}`;
|
||||
|
||||
// Copy to clipboard
|
||||
try {
|
||||
await navigator.clipboard.writeText(`Join JUMPA.ID Meeting:\n${inviteLink}`);
|
||||
setCopySuccess(true);
|
||||
setTimeout(() => {
|
||||
router.push(`/room/${newRoomId}`);
|
||||
}, 1500); // Tunggu 1.5 detik agar user melihat notif tercopy sebelum dipindah
|
||||
} catch {
|
||||
// Fallback jika clipboard API tidak diizinkan
|
||||
router.push(`/room/${newRoomId}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleJoinSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (roomName) {
|
||||
router.push(`/room/${roomName.toUpperCase()}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (isVerifying) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[#0a101d]">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-12 h-12 border-4 border-brand border-t-transparent rounded-full animate-spin"></div>
|
||||
<p className="text-brand font-medium">Securing Connection...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#050b14] text-white selection:bg-red-500/30 relative overflow-x-hidden pb-24 md:pb-10">
|
||||
{/* Abstract Background Decorators */}
|
||||
<div className="fixed inset-0 z-0 pointer-events-none">
|
||||
<div className="absolute top-[-10%] left-[-10%] w-[500px] h-[500px] bg-red-600/10 rounded-full blur-[150px] animate-pulse"></div>
|
||||
<div className="absolute bottom-[-10%] right-[-10%] w-[400px] h-[400px] bg-blue-600/10 rounded-full blur-[120px]"></div>
|
||||
</div>
|
||||
|
||||
{/* Top Navbar - Ultra Refined */}
|
||||
<header className="h-16 border-b border-white/5 flex items-center justify-between px-4 md:px-8 backdrop-blur-xl bg-black/40 relative z-50">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => window.location.href = '/dashboard'}
|
||||
className="flex items-center gap-2 p-2 rounded-xl bg-white/5 hover:bg-white/10 text-gray-400 hover:text-white transition-all border border-white/5 group"
|
||||
title="Back to Hub"
|
||||
>
|
||||
<svg className="w-5 h-5 group-hover:-translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||||
<span className="hidden md:inline font-bold text-xs uppercase tracking-widest">Master Hub</span>
|
||||
</button>
|
||||
|
||||
<div className="h-6 w-[1px] bg-white/10 mx-2 hidden md:block"></div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center shadow-lg ${engineStrategy.includes('XCU') ? 'bg-red-600 shadow-red-600/20' : 'bg-blue-600 shadow-blue-600/20'}`}>
|
||||
<svg className="w-5 h-5 text-black" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2L2 22h20L12 2zm0 4.5l6.5 13h-13L12 6.5zM11 16h2v2h-2v-2zm0-5h2v4h-2v-4z"/></svg>
|
||||
</div>
|
||||
<span className={`font-black text-sm md:text-lg tracking-tighter uppercase ${engineStrategy.includes('XCU') ? 'text-red-500' : 'text-blue-500'}`}>
|
||||
JUMPA <span className="text-white">MEET</span>
|
||||
<span className="hidden lg:inline ml-2 text-[10px] text-gray-600 font-mono tracking-widest">XCU-DIRECT-LINK</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="hidden sm:flex flex-col items-end mr-2">
|
||||
<span className="text-[10px] text-gray-500 uppercase font-bold">Authenticated as</span>
|
||||
<span className="text-xs text-white font-mono">{username}</span>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-full bg-linear-to-br from-gray-800 to-black border border-white/10 flex items-center justify-center font-black text-brand shadow-xl">
|
||||
{username.substring(0,2).toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="max-w-6xl mx-auto px-4 md:px-6 py-10 md:py-20 relative z-10">
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-12 lg:gap-20 items-center lg:items-start">
|
||||
|
||||
{/* Left Side: Dynamic Status & Info */}
|
||||
<div className="flex-1 text-center lg:text-left">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-red-500/10 border border-red-500/20 text-red-500 text-[10px] font-black uppercase tracking-widest mb-6 animate-pulse">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-red-500"></div>
|
||||
XCU Engine Status: Ultra Optimal
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-8xl font-black tracking-tighter mb-4 leading-none">
|
||||
{new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl text-gray-400 font-medium mb-10">
|
||||
{new Date().toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' })}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 max-w-xl">
|
||||
<div className="p-5 rounded-2xl bg-black/40 border border-white/5 backdrop-blur-md">
|
||||
<h4 className="text-[10px] font-black text-red-500 uppercase tracking-widest mb-2">Protocol</h4>
|
||||
<p className="text-xs text-gray-400">WebTransport HTTP/3 QUIC (0ms Latency Kernel Bypass)</p>
|
||||
</div>
|
||||
<div className="p-5 rounded-2xl bg-black/40 border border-white/5 backdrop-blur-md">
|
||||
<h4 className="text-[10px] font-black text-blue-500 uppercase tracking-widest mb-2">Encryption</h4>
|
||||
<p className="text-xs text-gray-400">Quantum-Safe AES-GCM 256-bit (E2EE Active)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side: High-Density Action Grid */}
|
||||
<div className="w-full max-w-md">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
||||
{/* New Meeting - The Big Button */}
|
||||
<button
|
||||
onClick={handleNewMeeting}
|
||||
className="col-span-2 group relative p-8 rounded-3xl border border-red-500/30 bg-red-950/20 hover:bg-red-900/30 transition-all duration-500 overflow-hidden shadow-2xl shadow-red-950/20"
|
||||
>
|
||||
<div className="absolute inset-0 bg-linear-to-br from-red-600/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<div className="relative flex flex-col items-center gap-4">
|
||||
<div className="w-20 h-20 rounded-2xl bg-red-600 flex items-center justify-center text-black shadow-[0_0_30px_rgba(220,38,38,0.5)] group-hover:shadow-[0_0_50px_rgba(220,38,38,0.8)] transition-all group-hover:scale-110">
|
||||
<svg className="w-10 h-10" fill="currentColor" viewBox="0 0 24 24"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"></path></svg>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<span className="block font-black text-lg tracking-tighter text-white uppercase">Mulai Rapat Baru</span>
|
||||
<span className="text-[10px] text-red-500 font-mono font-bold uppercase tracking-widest">Direct Ultra Link</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Join Button */}
|
||||
<button onClick={() => setShowJoinModal(true)} className="group p-6 rounded-3xl border border-white/5 bg-white/5 hover:bg-white/10 transition-all flex flex-col items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-blue-600/20 flex items-center justify-center text-blue-500 group-hover:bg-blue-600 group-hover:text-white transition-all">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4"></path></svg>
|
||||
</div>
|
||||
<span className="font-bold text-xs uppercase tracking-widest">Join</span>
|
||||
</button>
|
||||
|
||||
{/* Chronos Button */}
|
||||
<button onClick={() => setShowChronosModal(true)} className="group p-6 rounded-3xl border border-white/5 bg-white/5 hover:bg-white/10 transition-all flex flex-col items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-pink-600/20 flex items-center justify-center text-pink-500 group-hover:bg-pink-600 group-hover:text-white transition-all">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7v-5z"/></svg>
|
||||
</div>
|
||||
<span className="font-bold text-xs uppercase tracking-widest">Chronos</span>
|
||||
</button>
|
||||
|
||||
{/* Vault Button */}
|
||||
<button onClick={() => setShowVaultModal(true)} className="group p-6 rounded-3xl border border-white/5 bg-white/5 hover:bg-white/10 transition-all flex flex-col items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-teal-600/20 flex items-center justify-center text-teal-500 group-hover:bg-teal-600 group-hover:text-white transition-all">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/></svg>
|
||||
</div>
|
||||
<span className="font-bold text-xs uppercase tracking-widest">The Vault</span>
|
||||
</button>
|
||||
|
||||
{/* Tenant Master Button */}
|
||||
<button
|
||||
onClick={() => router.push(`/room/JMP-TENANT-MATRIX`)}
|
||||
className="group p-6 rounded-3xl border border-amber-500/20 bg-amber-500/5 hover:bg-amber-500/10 transition-all flex flex-col items-center gap-3"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-amber-500/20 flex items-center justify-center text-amber-500 group-hover:bg-amber-500 group-hover:text-black transition-all">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>
|
||||
</div>
|
||||
<span className="font-bold text-[10px] uppercase tracking-tighter text-center">Master Room</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Mobile Bottom Navigation - 100% Mobile Compatible */}
|
||||
<nav className="fixed bottom-4 left-4 right-4 md:hidden glass-panel rounded-2xl p-2 z-50 border-white/10 shadow-[0_-10px_40px_rgba(0,0,0,0.5)] flex justify-around items-center">
|
||||
<button onClick={() => window.location.href = '/dashboard'} className="flex flex-col items-center gap-1 p-2 text-gray-400">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"></path></svg>
|
||||
<span className="text-[9px] font-bold uppercase">Home</span>
|
||||
</button>
|
||||
<button onClick={() => window.location.href = '/c'} className="flex flex-col items-center gap-1 p-2 text-gray-400">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg>
|
||||
<span className="text-[9px] font-bold uppercase">Chat</span>
|
||||
</button>
|
||||
<button onClick={() => window.location.href = '/vc'} className="flex flex-col items-center gap-1 p-2 text-red-500">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
||||
<span className="text-[9px] font-bold uppercase">Meet</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* Ultra Footer */}
|
||||
<footer className="text-center py-10 opacity-30 mt-auto">
|
||||
<p className="text-[10px] font-mono tracking-[0.2em] uppercase">Jumpa.ID Meet • Absolute Zero Engine v2.4</p>
|
||||
</footer>
|
||||
|
||||
{/* Copy Success Alert */}
|
||||
{copySuccess && (
|
||||
<div className="absolute top-20 left-1/2 -translate-x-1/2 bg-green-500/90 text-white px-6 py-3 rounded-full font-medium shadow-xl backdrop-blur-md flex items-center gap-2 animate-in fade-in slide-in-from-top-5 z-50">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path></svg>
|
||||
Invite Link Copied! Joining meeting...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Join Modal */}
|
||||
{showJoinModal && (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md">
|
||||
<div className="bg-[#111b21] p-8 rounded-3xl border border-white/10 shadow-2xl w-[400px] animate-in zoom-in-95 duration-200">
|
||||
<h2 className="text-2xl font-bold mb-2 text-white">Join Meeting</h2>
|
||||
<p className="text-gray-400 text-sm mb-6">Enter meeting ID or personal link name</p>
|
||||
<form onSubmit={handleJoinSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
value={roomName}
|
||||
onChange={(e) => setRoomName(e.target.value.toUpperCase())}
|
||||
placeholder="Meeting ID (e.g. BOARD-ROOM)"
|
||||
className="w-full bg-[#0a101d] text-white border border-white/10 rounded-xl px-4 py-3.5 mb-6 focus:outline-none focus:border-brand focus:ring-1 focus:ring-brand"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
<div className="flex gap-3">
|
||||
<button type="button" onClick={() => setShowJoinModal(false)} className="flex-1 py-3 text-gray-300 font-medium bg-white/5 hover:bg-white/10 rounded-xl transition-colors">Cancel</button>
|
||||
<button type="submit" className="flex-1 py-3 text-black font-bold bg-brand hover:bg-brand/90 rounded-xl transition-colors">Join</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CHRONOS SMART SCHEDULER MODAL */}
|
||||
{showChronosModal && (
|
||||
<div className="fixed inset-0 z-600 flex items-center justify-center bg-black/90 backdrop-blur-md p-4">
|
||||
<div className="bg-[#111b21] w-full max-w-[700px] h-[80vh] rounded-2xl shadow-2xl border border-pink-500/50 flex flex-col overflow-hidden animate-in zoom-in duration-200">
|
||||
<div className="bg-[#202c33] px-6 py-4 border-b border-gray-800 flex justify-between items-center">
|
||||
<h3 className="text-pink-400 font-bold text-lg flex items-center gap-2">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7v-5z"/></svg>
|
||||
Chronos Smart Scheduler (Tenant Center)
|
||||
</h3>
|
||||
<button title="Aksi" onClick={() => setShowChronosModal(false)} className="text-gray-400 hover:text-white transition-colors">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 overflow-y-auto flex-1 custom-scroll">
|
||||
<div className="grid grid-cols-3 gap-4 mb-8">
|
||||
<div onClick={() => setRoomMode('standard')} className={`cursor-pointer p-4 rounded-xl border flex flex-col items-center gap-2 transition-all ${roomMode === 'standard' ? 'bg-blue-600/20 border-blue-500 text-blue-400' : 'bg-[#202c33] border-gray-600 text-gray-400 hover:border-gray-400'}`}>
|
||||
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>
|
||||
<span className="font-bold text-xs md:text-sm text-center">Standard VC</span>
|
||||
</div>
|
||||
<div onClick={() => setRoomMode('webinar')} className={`cursor-pointer p-4 rounded-xl border flex flex-col items-center gap-2 transition-all ${roomMode === 'webinar' ? 'bg-pink-600/20 border-pink-500 text-pink-400' : 'bg-[#202c33] border-gray-600 text-gray-400 hover:border-gray-400'}`}>
|
||||
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM8 15h8v-2H8v2zm0-4h8V9H8v2z"/></svg>
|
||||
<span className="font-bold text-xs md:text-sm text-center">Holo-Stage Webinar</span>
|
||||
</div>
|
||||
<div onClick={() => setRoomMode('podcast')} className={`cursor-pointer p-4 rounded-xl border flex flex-col items-center gap-2 transition-all ${roomMode === 'podcast' ? 'bg-amber-600/20 border-amber-500 text-amber-500' : 'bg-[#202c33] border-gray-600 text-gray-400 hover:border-gray-400'}`}>
|
||||
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24"><path d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z"/></svg>
|
||||
<span className="font-bold text-xs md:text-sm text-center">Studio Podcast</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#202c33] p-4 rounded-xl border border-gray-600 mb-6">
|
||||
<h4 className="text-white font-bold mb-3">Jadwalkan Ruangan (Smart Timeline)</h4>
|
||||
{!chronoResult ? (
|
||||
<div className="flex flex-col md:flex-row gap-2">
|
||||
<input title="Masukan"
|
||||
type="datetime-local"
|
||||
value={chronoDate}
|
||||
onChange={(e) => setChronoDate(e.target.value)}
|
||||
className="flex-1 bg-[#111b21] text-white px-4 py-2 rounded-lg border border-gray-700 focus:outline-none focus:border-pink-500"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Nama Ruangan..."
|
||||
value={chronoName}
|
||||
onChange={(e) => setChronoName(e.target.value)}
|
||||
className="flex-1 bg-[#111b21] text-white px-4 py-2 rounded-lg border border-gray-700 focus:outline-none focus:border-pink-500"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (chronoDate && chronoName) {
|
||||
const uuid = crypto.randomUUID().toUpperCase().split('-');
|
||||
const newRoomId = `JMP-SCH-${uuid[1]}-${uuid[2]}`;
|
||||
const inviteLink = `/vc/room/${newRoomId}`;
|
||||
const inviteText = `Panggilan JUMPA.ID Terjadwal\nTopik: ${chronoName}\nWaktu: ${chronoDate}\n\nTautan Rapat:\n${inviteLink}\nMeeting ID: ${newRoomId}`;
|
||||
setChronoResult({ link: inviteLink, text: inviteText });
|
||||
|
||||
// Tambahkan ke state schedules untuk ditampilkan di list bawah
|
||||
setSchedules(prev => [...prev, {
|
||||
id: newRoomId,
|
||||
title: chronoName,
|
||||
mode: roomMode,
|
||||
date: chronoDate,
|
||||
link: inviteLink
|
||||
}]);
|
||||
} else {
|
||||
alert("Mohon isi tanggal dan nama ruangan.");
|
||||
}
|
||||
}}
|
||||
className="bg-pink-600 hover:bg-pink-500 text-white px-6 py-2 rounded-lg font-bold"
|
||||
>
|
||||
JADWALKAN
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-green-500/10 border border-green-500/30 p-4 rounded-lg mt-2">
|
||||
<h5 className="text-green-400 font-bold mb-2 text-sm flex items-center gap-2">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path></svg>
|
||||
Berhasil Dijadwalkan!
|
||||
</h5>
|
||||
<textarea title="Teks"
|
||||
readOnly
|
||||
value={chronoResult.text}
|
||||
className="w-full h-24 bg-black/50 text-gray-300 text-xs p-2 rounded-lg border border-white/5 resize-none outline-none custom-scroll mb-2"></textarea>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => {
|
||||
navigator.clipboard.writeText(chronoResult.text);
|
||||
alert("Teks undangan berhasil disalin!");
|
||||
}} className="flex-1 py-1.5 text-black font-bold bg-brand hover:bg-brand/90 rounded-lg text-xs">Copy Invitation</button>
|
||||
<button onClick={() => { setChronoResult(null); setChronoDate(""); setChronoName(""); }} className="px-4 py-1.5 text-white font-medium bg-white/10 hover:bg-white/20 rounded-lg text-xs">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-gray-400 font-bold text-sm">3D TIMELINE SCHEDULES</h4>
|
||||
{schedules.length === 0 ? (
|
||||
<div className="text-center py-6 text-gray-500 text-sm">Belum ada ruangan yang dijadwalkan.</div>
|
||||
) : (
|
||||
schedules.map((sch) => (
|
||||
<div key={sch.id} className="bg-linear-to-r from-[#202c33] to-[#111b21] p-4 rounded-xl border border-gray-700 flex justify-between items-center group">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="bg-pink-500 text-white text-[10px] px-2 py-0.5 rounded font-bold uppercase">{sch.mode}</span>
|
||||
<span className="text-white font-bold">{sch.title}</span>
|
||||
</div>
|
||||
<div className="text-gray-400 text-sm mt-1">{new Date(sch.date).toLocaleString('id-ID')}</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => { navigator.clipboard.writeText(`Gabung Rapat: ${sch.link}`); alert('Link disalin!'); }} className="bg-white/10 hover:bg-white/20 text-white px-3 py-2 rounded-lg text-xs font-bold md:opacity-0 group-hover:opacity-100 transition-opacity">COPY LINK</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowChronosModal(false);
|
||||
router.push(`/room/${sch.id}`);
|
||||
}}
|
||||
className="bg-brand/20 hover:bg-brand text-brand hover:text-black border border-brand/50 px-3 py-2 rounded-lg text-xs font-bold transition-all"
|
||||
>
|
||||
START
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* THE VAULT (RECORDINGS) MODAL */}
|
||||
{showVaultModal && (
|
||||
<div className="fixed inset-0 z-600 flex items-center justify-center bg-black/90 backdrop-blur-md p-4">
|
||||
<div className="bg-[#111b21] w-full max-w-[800px] h-[85vh] rounded-2xl shadow-2xl border border-teal-500/50 flex flex-col overflow-hidden animate-in zoom-in duration-200">
|
||||
<div className="bg-[#202c33] px-6 py-4 border-b border-gray-800 flex justify-between items-center">
|
||||
<h3 className="text-teal-400 font-bold text-lg flex items-center gap-2">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/></svg>
|
||||
The Vault (Quantum Recordings)
|
||||
</h3>
|
||||
<button title="Aksi" onClick={() => setShowVaultModal(false)} className="text-gray-400 hover:text-white transition-colors">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 overflow-y-auto flex-1 custom-scroll flex flex-col gap-4">
|
||||
|
||||
<div className="bg-[#202c33] rounded-xl overflow-hidden border border-gray-700 hover:border-teal-500 transition-colors flex flex-col items-center justify-center text-center p-6 border-dashed relative">
|
||||
<svg className="w-12 h-12 text-teal-500 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
||||
<h4 className="text-white font-bold mb-2">Secure Local Player</h4>
|
||||
<p className="text-gray-400 text-sm mb-4">Pilih file rekaman hasil <strong className="text-teal-400">Quantum Record</strong> (.webm / .mp4) dari perangkat Anda. Video diputar 100% lokal tanpa diunggah ke internet.</p>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
accept="video/webm, video/mp4, video/x-matroska"
|
||||
className="hidden"
|
||||
id="vault-file-upload"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
if (vaultVideoUrl) URL.revokeObjectURL(vaultVideoUrl);
|
||||
const url = URL.createObjectURL(file);
|
||||
setVaultVideoUrl(url);
|
||||
setVaultVideoName(file.name);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor="vault-file-upload"
|
||||
className="cursor-pointer bg-teal-600 hover:bg-teal-500 text-white px-6 py-2.5 rounded-lg font-bold shadow-lg shadow-teal-500/20 transition-all"
|
||||
>
|
||||
Buka Rekaman Lokal
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{vaultVideoUrl && (
|
||||
<div className="bg-[#111b21] rounded-xl overflow-hidden border border-teal-500 shadow-xl shadow-teal-500/10 flex flex-col animate-in fade-in zoom-in-95">
|
||||
<div className="bg-teal-900/20 px-4 py-3 border-b border-teal-500/30 flex justify-between items-center">
|
||||
<span className="text-teal-300 font-mono text-xs truncate max-w-[70%]">📄 {vaultVideoName}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
URL.revokeObjectURL(vaultVideoUrl);
|
||||
setVaultVideoUrl(null);
|
||||
setVaultVideoName("");
|
||||
}}
|
||||
className="text-red-400 hover:text-red-300 text-xs font-bold uppercase"
|
||||
>
|
||||
Tutup Video
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-black aspect-video flex items-center justify-center">
|
||||
<video
|
||||
src={vaultVideoUrl}
|
||||
controls
|
||||
autoPlay
|
||||
className="w-full h-full object-contain outline-none"
|
||||
></video>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
// [TSM.ID].[11031972] — All Rights Reserved. Proprietary & Confidential.
|
||||
"use client";
|
||||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { use, useEffect, useState } from "react";
|
||||
import { XCURoom } from "../../../components/xcuRoom";
|
||||
|
||||
export default function RoomPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ roomName: string }>;
|
||||
}) {
|
||||
const resolvedParams = use(params);
|
||||
const roomName = resolvedParams.roomName;
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const isVoiceCall = searchParams.get("audioOnly") === "true";
|
||||
|
||||
const [token, setToken] = useState("");
|
||||
const [serverUrl, setServerUrl] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [isInitializing, setIsInitializing] = useState(true);
|
||||
const [username, setUsername] = useState("");
|
||||
|
||||
// Guest Lobby State
|
||||
const [showGuestLobby, setShowGuestLobby] = useState(false);
|
||||
const [guestName, setGuestName] = useState("");
|
||||
const [guestLoading, setGuestLoading] = useState(false);
|
||||
const [guestError, setGuestError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const authResp = await fetch(`/vc/api/auth/me?_cb=${Date.now()}`);
|
||||
const authData = await authResp.json();
|
||||
if (authData.error) {
|
||||
// User tidak login → tampilkan Guest Lobby (seperti Zoom)
|
||||
setShowGuestLobby(true);
|
||||
setIsInitializing(false);
|
||||
return;
|
||||
}
|
||||
const userEmail = authData.email;
|
||||
setUsername(userEmail);
|
||||
|
||||
const qResp = await fetch(`/api/auth/quantum_token?_cb=${Date.now()}`, { credentials: 'include' });
|
||||
if (!qResp.ok) {
|
||||
throw new Error("Otorisasi JUMPA.ID Ditolak: Gagal memverifikasi Lisensi Tenant.");
|
||||
}
|
||||
const qData = await qResp.json();
|
||||
if (qData.error || !qData.token) {
|
||||
throw new Error("Akses Ilegal Terdeteksi: Harap masuk melalui Dasbor JUMPA.ID terlebih dahulu.");
|
||||
}
|
||||
|
||||
setToken(qData.token);
|
||||
setServerUrl(process.env.NEXT_PUBLIC_XCU_SERVER_URL || "/xcu-engine");
|
||||
setIsInitializing(false);
|
||||
|
||||
} catch (e: unknown) {
|
||||
// Auth failed but not critical → show guest lobby
|
||||
setShowGuestLobby(true);
|
||||
setIsInitializing(false);
|
||||
}
|
||||
})();
|
||||
}, [roomName]);
|
||||
|
||||
// Handle Guest Join
|
||||
const handleGuestJoin = async () => {
|
||||
if (!guestName.trim() || guestName.trim().length < 2) {
|
||||
setGuestError("Masukkan nama Anda (minimal 2 karakter).");
|
||||
return;
|
||||
}
|
||||
setGuestLoading(true);
|
||||
setGuestError("");
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth/guest-token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ roomName, displayName: guestName.trim() }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok || data.error) {
|
||||
setGuestError(data.error || "Gagal masuk sebagai tamu.");
|
||||
setGuestLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Guest token received — set state and enter room
|
||||
setToken(data.token);
|
||||
setUsername(data.displayName);
|
||||
setServerUrl(process.env.NEXT_PUBLIC_XCU_SERVER_URL || "/xcu-engine");
|
||||
setShowGuestLobby(false);
|
||||
|
||||
} catch (e) {
|
||||
setGuestError("Koneksi gagal. Periksa jaringan Anda.");
|
||||
setGuestLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// GUEST LOBBY UI (Zoom-Like)
|
||||
// ═══════════════════════════════════════
|
||||
if (showGuestLobby) {
|
||||
return (
|
||||
<div className="h-dvh flex items-center justify-center bg-[#0a101d] overflow-hidden relative">
|
||||
{/* Background Animation */}
|
||||
<div className="absolute inset-0 z-0 opacity-10 pointer-events-none">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[120vw] h-[120vw] rounded-full border border-blue-500/20 animate-pulse"></div>
|
||||
<div className="absolute top-1/4 right-1/4 w-64 h-64 bg-blue-600/10 rounded-full blur-3xl"></div>
|
||||
<div className="absolute bottom-1/4 left-1/4 w-80 h-80 bg-purple-600/10 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-md mx-4">
|
||||
<div className="bg-[#111827]/90 backdrop-blur-2xl border border-white/10 rounded-3xl shadow-2xl overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="px-8 pt-10 pb-6 text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-5 rounded-2xl bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-blue-500/30">
|
||||
<svg className="w-8 h-8 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M4 4h10a2 2 0 0 1 2 2v3.5l4-3v11l-4-3V18a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-black text-white tracking-tight mb-1">
|
||||
Gabung Rapat
|
||||
</h1>
|
||||
<p className="text-sm text-gray-400">
|
||||
Room: <span className="text-blue-400 font-mono font-bold">{decodeURIComponent(roomName)}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="px-8 pb-8 space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-widest mb-2">
|
||||
Nama Tampilan
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={guestName}
|
||||
onChange={(e) => { setGuestName(e.target.value); setGuestError(""); }}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleGuestJoin()}
|
||||
placeholder="Masukkan nama Anda..."
|
||||
maxLength={50}
|
||||
autoFocus
|
||||
className="w-full px-4 py-3.5 bg-white/5 border border-white/10 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{guestError && (
|
||||
<div className="px-4 py-3 bg-red-500/10 border border-red-500/20 rounded-xl text-red-400 text-sm font-medium flex items-center gap-2">
|
||||
<svg className="w-4 h-4 shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{guestError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleGuestJoin}
|
||||
disabled={guestLoading || guestName.trim().length < 2}
|
||||
className={`w-full py-3.5 rounded-xl font-bold text-base transition-all flex items-center justify-center gap-2 ${
|
||||
guestLoading || guestName.trim().length < 2
|
||||
? 'bg-gray-700 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:shadow-lg hover:shadow-blue-500/30 hover:-translate-y-0.5 active:translate-y-0'
|
||||
}`}
|
||||
>
|
||||
{guestLoading ? (
|
||||
<>
|
||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
Menghubungkan...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Gabung sebagai Tamu
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="text-center pt-2">
|
||||
<p className="text-xs text-gray-500 mb-3">
|
||||
Sudah punya akun JUMPA.ID?
|
||||
</p>
|
||||
<a
|
||||
href="/"
|
||||
className="text-sm text-blue-400 hover:text-blue-300 font-semibold transition-colors"
|
||||
>
|
||||
Masuk dengan Akun →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-8 py-4 bg-white/[0.02] border-t border-white/5 text-center">
|
||||
<p className="text-[10px] text-gray-600 font-mono">
|
||||
Powered by JUMPA.ID • XCom ULTRA Engine • [TSM.ID].[11031972]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="h-dvh flex items-center justify-center bg-[#0a101d]">
|
||||
<div className="bg-red-950/40 p-8 rounded-3xl text-center border border-red-500/30 max-w-md">
|
||||
<h2 className="text-red-500 font-bold text-xl mb-2">
|
||||
Akses Ditolak
|
||||
</h2>
|
||||
<p className="text-gray-400 mb-6">{error}</p>
|
||||
<button
|
||||
onClick={() => (window.location.href = "/")}
|
||||
className="px-6 py-2.5 bg-red-600 text-white font-bold rounded-xl"
|
||||
>
|
||||
Kembali ke Dasbor
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isInitializing) {
|
||||
return (
|
||||
<div className="h-dvh flex items-center justify-center bg-[#0a101d] overflow-hidden relative">
|
||||
<div className="absolute inset-0 z-0 opacity-20 pointer-events-none flex items-center justify-center">
|
||||
<div className="w-[80vw] h-[80vw] rounded-full border border-blue-500/30 animate-ping absolute"></div>
|
||||
<div className="w-[60vw] h-[60vw] rounded-full border border-blue-400/20 animate-ping absolute [animation-delay:0.5s]"></div>
|
||||
<div className="w-[40vw] h-[40vw] rounded-full border border-blue-300/10 animate-ping absolute [animation-delay:1s]"></div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center z-10 p-8 bg-black/40 backdrop-blur-md rounded-3xl border border-white/5">
|
||||
<div className="mb-8 relative w-24 h-24 flex items-center justify-center">
|
||||
<div className="absolute inset-0 rounded-full border-t-2 border-blue-500 animate-spin"></div>
|
||||
<div className="absolute inset-2 rounded-full border-r-2 border-green-500 animate-spin [animation-direction:reverse]"></div>
|
||||
<svg className="w-8 h-8 text-blue-400 animate-pulse" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2L2 22h20L12 2zm0 4.5l6.5 13h-13L12 6.5zM11 16h2v2h-2v-2zm0-5h2v4h-2v-4z"/></svg>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-white font-black text-xl tracking-widest uppercase mb-1">
|
||||
Initializing JUMPA.ID Engine
|
||||
</p>
|
||||
<p className="text-blue-400 font-mono text-xs">
|
||||
ESTABLISHING SECURE CONNECTION FOR {username.toUpperCase()}...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// THE ABSOLUTE XCU CORE ENGINE
|
||||
// 100% XCU Ultra Node [TSM.ID].[11031972]
|
||||
return (
|
||||
<ErrorBoundaryWrapper roomName={roomName}>
|
||||
<XCURoom
|
||||
roomName={roomName}
|
||||
token={token}
|
||||
serverUrl={serverUrl}
|
||||
isVoiceCall={isVoiceCall}
|
||||
initialCameraOn={false}
|
||||
initialMicOn={false}
|
||||
username={username}
|
||||
/>
|
||||
</ErrorBoundaryWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom Error Boundary to catch & display the EXACT crash reason
|
||||
import React from "react";
|
||||
class ErrorBoundaryWrapper extends React.Component<{roomName: string, children: React.ReactNode}, {hasError: boolean, error: string}> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: "" };
|
||||
}
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { hasError: true, error: `${error.name}: ${error.message}\n${error.stack}` };
|
||||
}
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="h-dvh flex items-center justify-center bg-[#0a101d] p-4">
|
||||
<div className="bg-red-950/40 p-8 rounded-3xl text-center border border-red-500/30 max-w-2xl w-full">
|
||||
<h2 className="text-red-500 font-bold text-xl mb-4">XCU Engine Crash Report</h2>
|
||||
<pre className="text-left text-xs text-gray-300 bg-black/60 p-4 rounded-xl overflow-auto max-h-[60vh] whitespace-pre-wrap break-all">{this.state.error}</pre>
|
||||
<button onClick={() => window.location.reload()} className="mt-6 px-6 py-2.5 bg-red-600 text-white font-bold rounded-xl">Reload</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { XCUQuantumBridge } from "../../components/xcuQuantumBridge";
|
||||
|
||||
export default function SupremeCommandPage() {
|
||||
const [activeRooms, setActiveRooms] = useState<{
|
||||
id: string,
|
||||
status: string,
|
||||
name: string,
|
||||
type: string,
|
||||
participants: number,
|
||||
metrics?: { cpu: number, ram: number, threats: number }
|
||||
}[]>([]);
|
||||
const [glitch, setGlitch] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState("");
|
||||
const [realLatency, setRealLatency] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let timeInterval: NodeJS.Timeout;
|
||||
setTimeout(() => {
|
||||
setCurrentTime(new Date().toLocaleTimeString('en-US', { hour12: false }));
|
||||
timeInterval = setInterval(() => {
|
||||
setCurrentTime(new Date().toLocaleTimeString('en-US', { hour12: false }));
|
||||
}, 1000);
|
||||
}, 0);
|
||||
|
||||
const fetchRooms = async () => {
|
||||
try {
|
||||
const startTime = performance.now();
|
||||
const res = await fetch('/vc/api/xcu/rooms');
|
||||
const endTime = performance.now();
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
setGlitch(true);
|
||||
setTimeout(() => setGlitch(false), 200);
|
||||
setActiveRooms(data.rooms);
|
||||
setRealLatency(Math.floor(endTime - startTime));
|
||||
}
|
||||
} catch(e) {
|
||||
console.error("Gagal menarik data dari Omniversal API", e);
|
||||
}
|
||||
};
|
||||
|
||||
fetchRooms();
|
||||
const interval = setInterval(fetchRooms, 3000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
if (timeInterval) clearInterval(timeInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#050505] text-green-500 font-mono overflow-y-auto relative selection:bg-green-500 selection:text-black p-4 md:p-8">
|
||||
{/* CRT Scanline Effect */}
|
||||
<div className="absolute inset-0 pointer-events-none bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,0,0,0.25)_50%),linear-gradient(90deg,rgba(255,0,0,0.06),rgba(0,255,0,0.02),rgba(0,0,255,0.06))] bg-size-[100%_4px,3px_100%] z-50 mix-blend-screen opacity-30"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto relative z-10">
|
||||
{/* Header */}
|
||||
<header className="flex flex-col md:flex-row justify-between items-start md:items-end border-b border-green-500/30 pb-6 mb-12">
|
||||
<div className="relative mb-4 md:mb-0">
|
||||
<div className="absolute -inset-4 bg-green-500/10 blur-3xl rounded-full animate-pulse"></div>
|
||||
<h1 className={`text-4xl md:text-7xl font-black uppercase tracking-[0.25em] text-transparent bg-clip-text bg-linear-to-r from-green-400 via-emerald-500 to-green-700 drop-shadow-[0_0_20px_rgba(34,197,94,0.4)] ${glitch ? 'translate-x-1' : ''}`}>SUPREME COMMAND</h1>
|
||||
<div className="flex items-center gap-3 mt-3">
|
||||
<div className="flex items-center gap-1.5 bg-green-500/10 border border-green-500/20 px-2 py-0.5 rounded shadow-[0_0_10px_rgba(0,255,0,0.1)]">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full animate-ping"></span>
|
||||
<span className="text-[10px] text-green-400 tracking-widest font-bold uppercase">System Active</span>
|
||||
</div>
|
||||
<span className="text-green-600/50 text-[9px] tracking-tighter uppercase font-medium">// Omniversal CCTV Matrix // Node-Alpha-01</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-8 items-end">
|
||||
<div className="hidden lg:block w-48">
|
||||
<XCUQuantumBridge />
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-3xl md:text-5xl font-black tracking-tighter text-white tabular-nums">{currentTime || "00:00:00"}</div>
|
||||
<div className="flex flex-col items-end mt-2">
|
||||
<span className="text-green-400/80 text-[10px] tracking-widest font-bold">ALPHA LATENCY: {realLatency}ms</span>
|
||||
<div className="w-32 h-1 bg-green-900/50 rounded-full mt-1 overflow-hidden">
|
||||
<div className="h-full bg-green-400 transition-all duration-300" style={{ width: `${Math.min(100, realLatency * 2)}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{activeRooms.map((room) => (
|
||||
<div key={room.id} className="relative group">
|
||||
<div className="absolute -inset-1 bg-green-500/5 rounded-2xl blur opacity-0 group-hover:opacity-100 transition duration-500"></div>
|
||||
<div className="relative border border-green-500/20 bg-[#0A0A0A] p-6 rounded-2xl shadow-[0_0_40px_rgba(0,0,0,0.5)] group-hover:border-green-400/50 transition-all duration-300 flex flex-col justify-between min-h-[260px]">
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] uppercase font-bold tracking-[0.2em] text-green-500/60 mb-1">Matrix ID</span>
|
||||
<span className="text-xs font-mono text-white bg-green-500/5 border border-green-500/10 px-2 py-0.5 rounded">{room.id}</span>
|
||||
</div>
|
||||
<span className={`text-[10px] uppercase font-black tracking-widest px-3 py-1 rounded-full border shadow-[0_0_15px_rgba(0,0,0,0.2)] ${room.status === 'BROADCASTING' ? 'bg-red-500/10 text-red-400 border-red-500/20 animate-pulse' : 'bg-green-500/10 text-green-400 border-green-500/20'}`}>
|
||||
{room.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-black text-white group-hover:text-green-400 transition-colors tracking-tight leading-none mb-2">{room.name}</h2>
|
||||
<p className="text-green-700 text-[10px] uppercase tracking-[0.3em] font-bold">{room.type}</p>
|
||||
</div>
|
||||
|
||||
{room.metrics && (
|
||||
<div className="grid grid-cols-3 gap-2 pt-2 border-t border-green-500/10 mt-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[8px] text-green-700 uppercase">CPU Core</span>
|
||||
<span className="text-xs text-white font-mono">{(room.metrics.cpu || 0).toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[8px] text-green-700 uppercase">Neural RAM</span>
|
||||
<span className="text-xs text-white font-mono">{(room.metrics.ram || 0).toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[8px] text-green-700 uppercase">Threats</span>
|
||||
<span className="text-xs text-red-500 font-mono">{(room.metrics.threats || 0).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-end mt-8">
|
||||
<div className="flex items-end gap-2">
|
||||
<span className="text-4xl font-black text-white tracking-tighter leading-none">{room.participants}</span>
|
||||
<span className="text-[9px] text-green-600 uppercase font-bold tracking-widest mb-1">Entities</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => window.open(`/vc/room/${room.id}`, '_blank')}
|
||||
className="group/btn relative px-6 py-2 bg-green-500 text-black font-black text-[10px] uppercase tracking-widest rounded-lg transition-all hover:bg-white active:scale-95 shadow-[0_0_20px_rgba(34,197,94,0.3)]"
|
||||
>
|
||||
<span className="relative z-10">Infiltrate Matrix</span>
|
||||
<div className="absolute inset-0 bg-white/20 scale-x-0 group-hover/btn:scale-x-100 transition-transform origin-left rounded-lg"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Dashboard Footer/Status Bar */}
|
||||
<footer className="mt-20 flex flex-col md:flex-row justify-between items-center gap-4 text-[9px] text-green-900 font-bold tracking-[0.4em] uppercase border-t border-green-500/10 pt-8 pb-12">
|
||||
<div className="flex items-center gap-6">
|
||||
<span>Encryption: Post-Quantum AES-XCU</span>
|
||||
<span>Layer: eBPF/XDP Kernel-Bypass</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-green-500 animate-pulse">●</span>
|
||||
<span>All Neural Nodes Nominal</span>
|
||||
</div>
|
||||
<div className="hover:text-green-400 transition-colors cursor-pointer">
|
||||
Secure Terminal 0.1.0-alpha
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user