Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
388ca91
feat: scaffold StakeReloadXS.com Next.js frontend with multi-brand de…
claude Mar 18, 2026
f56c22e
feat: v2 — user journeys, design token system, admin GUI, /status, OG…
claude Mar 18, 2026
26fd1bd
fix: rename next.config.ts → next.config.js for Next.js 14 compatibility
claude Mar 18, 2026
25fb89d
Fix: JSDoc comment contains `*/` in cron schedule which prematurely c…
vercel[bot] Mar 18, 2026
086e79b
fix: upgrade Next.js 15, ESLint 9, establish monorepo packages
claude Mar 18, 2026
c372cf4
Merge remote-tracking branch 'origin/claude/define-tech-stack-C37J3' …
claude Mar 18, 2026
e60ce64
fix: add root vercel.json to resolve monorepo Next.js detection
claude Mar 18, 2026
c37b2bb
Fix: New monorepo packages have incorrect "main" field pointing to Ty…
vercel[bot] Mar 18, 2026
e49679d
Fix: JSDoc comment contains `*/15 * * * *` cron expression which prem…
vercel[bot] Mar 18, 2026
154f3a1
Fix: Unvalidated URL redirection: `window.location.href` is set direc…
vercel[bot] Mar 18, 2026
d5ed96c
Fix: XSS vulnerability: gaId environment variable interpolated direct…
vercel[bot] Mar 18, 2026
5ed88ea
Fix: Tawk propertyId and widgetId are interpolated into script conten…
vercel[bot] Mar 18, 2026
f7ff91a
Fix: POST endpoint in cron route lacks authentication check, allowing…
vercel[bot] Mar 18, 2026
3006e75
Fix: Missing server-side validation of packageId, packageName, price,…
vercel[bot] Mar 18, 2026
784bc59
Fix: Cron POST and GET endpoints skip authentication if CRON_SECRET e…
vercel[bot] Mar 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions apps/admin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@fused-gaming/admin",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start -p 3001",
"type-check": "tsc --noEmit"
},
"dependencies": {
"next": "14.2.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"framer-motion": "^11.3.8",
"zustand": "^4.5.4",
"clsx": "^2.1.1"
},
"devDependencies": {
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.6",
"typescript": "^5.5.3"
}
}
39 changes: 39 additions & 0 deletions apps/admin/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--admin-bg: #0a0a0a;
--admin-surface: #111111;
--admin-surface2: #1a1a1a;
--admin-border: #222222;
--admin-primary: #dc2626;
--admin-text: #f3f4f6;
--admin-muted: #9ca3af;
--admin-faint: #4b5563;
--admin-success: #22c55e;
--admin-warning: #f59e0b;
--admin-error: #ef4444;
}

body {
background: var(--admin-bg);
color: var(--admin-text);
font-family: 'Inter', system-ui, sans-serif;
}

/* Live preview iframe isolation */
.brand-preview-frame {
border: 1px solid var(--admin-border);
border-radius: 12px;
overflow: hidden;
}

/* Token color swatch */
.color-swatch {
width: 32px;
height: 32px;
border-radius: 6px;
border: 1px solid rgba(255,255,255,0.1);
flex-shrink: 0;
}
21 changes: 21 additions & 0 deletions apps/admin/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });

export const metadata: Metadata = {
title: "Fused Gaming — Admin",
description: "Brand management and deployment console",
robots: "noindex, nofollow",
};

export default function AdminLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${inter.variable} font-sans antialiased`}>
{children}
</body>
</html>
);
}
5 changes: 5 additions & 0 deletions apps/admin/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import AdminDashboard from "@/components/AdminDashboard";

export default function AdminPage() {
return <AdminDashboard />;
}
119 changes: 119 additions & 0 deletions apps/admin/src/components/AdminDashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"use client";

import { useState } from "react";
import BrandFormPanel from "@/components/panels/BrandFormPanel";
import TokenPanel from "@/components/panels/TokenPanel";
import DeployPanel from "@/components/panels/DeployPanel";
import PreviewPanel from "@/components/panels/PreviewPanel";

type Tab = "form" | "tokens" | "preview" | "deploy" | "status";

const TABS: { id: Tab; label: string; icon: string }[] = [
{ id: "form", label: "Brand", icon: "✦" },
{ id: "tokens", label: "Tokens", icon: "⬡" },
{ id: "preview", label: "Preview", icon: "◈" },
{ id: "deploy", label: "Deploy", icon: "🚀" },
{ id: "status", label: "Status", icon: "◉" },
];

export default function AdminDashboard() {
const [activeTab, setActiveTab] = useState<Tab>("form");
const [sidebarOpen, setSidebarOpen] = useState(true);

return (
<div className="flex h-screen overflow-hidden" style={{ background: "var(--admin-bg)", color: "var(--admin-text)" }}>
{/* Sidebar */}
<aside
className="flex flex-col shrink-0 transition-all duration-300"
style={{
width: sidebarOpen ? "220px" : "60px",
background: "var(--admin-surface)",
borderRight: "1px solid var(--admin-border)",
}}
>
{/* Logo */}
<div className="flex items-center gap-3 px-4 py-5 border-b" style={{ borderColor: "var(--admin-border)" }}>
<button
onClick={() => setSidebarOpen((v) => !v)}
className="text-sm font-black"
style={{ color: "var(--admin-primary)" }}
>
FG
</button>
{sidebarOpen && (
<span className="text-sm font-semibold truncate">Admin Console</span>
)}
</div>

{/* Nav */}
<nav className="flex flex-col gap-1 p-2 flex-grow">
{TABS.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-left transition-all"
style={{
background: activeTab === tab.id ? "rgba(220,38,38,0.15)" : "transparent",
color: activeTab === tab.id ? "var(--admin-primary)" : "var(--admin-muted)",
border: `1px solid ${activeTab === tab.id ? "rgba(220,38,38,0.3)" : "transparent"}`,
}}
>
<span className="text-base shrink-0">{tab.icon}</span>
{sidebarOpen && <span className="truncate">{tab.label}</span>}
</button>
))}
</nav>

{/* Status indicator */}
{sidebarOpen && (
<div className="p-4 border-t text-xs" style={{ borderColor: "var(--admin-border)", color: "var(--admin-faint)" }}>
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse inline-block" />
System Online
</div>
</div>
)}
</aside>

{/* Main content */}
<main className="flex-grow flex flex-col overflow-hidden">
{/* Top bar */}
<div className="flex items-center justify-between px-6 py-4 border-b shrink-0"
style={{ borderColor: "var(--admin-border)", background: "var(--admin-surface)" }}>
<div>
<h1 className="text-lg font-bold">
{TABS.find((t) => t.id === activeTab)?.label}
</h1>
<p className="text-xs mt-0.5" style={{ color: "var(--admin-muted)" }}>
{activeTab === "form" && "Define brand identity and copy"}
{activeTab === "tokens" && "Customize design tokens and CSS variables"}
{activeTab === "preview"&& "Live component preview across breakpoints"}
{activeTab === "deploy" && "Build and deploy to any target"}
{activeTab === "status" && "System health across all domains"}
</p>
</div>
<a
href="/status"
className="text-xs px-3 py-1.5 rounded-md font-medium"
style={{ background: "var(--admin-surface2)", border: "1px solid var(--admin-border)", color: "var(--admin-muted)" }}
>
◉ /status
</a>
</div>

{/* Panel content */}
<div className="flex-grow overflow-hidden">
{activeTab === "form" && <BrandFormPanel />}
{activeTab === "tokens" && <TokenPanel />}
{activeTab === "preview" && <PreviewPanel />}
{activeTab === "deploy" && <DeployPanel />}
{activeTab === "status" && (
<div className="p-6 text-center" style={{ color: "var(--admin-muted)" }}>
<p>Status dashboard — see <code>/apps/web/src/app/status/page.tsx</code></p>
</div>
)}
</div>
</main>
</div>
);
}
172 changes: 172 additions & 0 deletions apps/admin/src/components/panels/BrandFormPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
"use client";

import { useAdminStore } from "@/lib/brandStore";

const COPY_VARIANTS = ["urgency", "exclusivity", "authority", "rebellion", "data"];
const ANIMATION_PRESETS = ["smooth", "fast", "glitch", "static"] as const;

export default function BrandFormPanel() {
const { draft, setDraftField } = useAdminStore();

return (
<div className="p-6 space-y-5 overflow-y-auto h-full">
<div className="text-xs font-semibold uppercase tracking-wider mb-1"
style={{ color: "var(--admin-muted)" }}>
Brand Identity
</div>

{/* ID */}
<Field label="Brand ID" hint="URL-safe, e.g. stakereloadxs">
<input
type="text"
value={draft.id}
onChange={(e) => setDraftField("id", e.target.value)}
placeholder="stakeclaimbot"
className="admin-input"
/>
</Field>

{/* Name */}
<Field label="Brand Name">
<input
type="text"
value={draft.name}
onChange={(e) => setDraftField("name", e.target.value)}
placeholder="StakeClaimBot"
className="admin-input"
/>
</Field>

{/* Domain */}
<Field label="Domain">
<input
type="text"
value={draft.domain}
onChange={(e) => setDraftField("domain", e.target.value)}
placeholder="stakeclaimbot.com"
className="admin-input"
/>
</Field>

{/* Tagline */}
<Field label="Tagline (Hero H1)">
<input
type="text"
value={draft.tagline}
onChange={(e) => setDraftField("tagline", e.target.value)}
placeholder="Automate Your Stake Claims"
className="admin-input"
/>
</Field>

{/* Sub tagline */}
<Field label="Sub Tagline (Hero body)">
<textarea
value={draft.subTagline}
onChange={(e) => setDraftField("subTagline", e.target.value)}
placeholder="One-click automation for Stake bonus cycles."
rows={2}
className="admin-input resize-none"
/>
</Field>

{/* CTA */}
<Field label="Primary CTA Text">
<input
type="text"
value={draft.ctaPrimary}
onChange={(e) => setDraftField("ctaPrimary", e.target.value)}
placeholder="Claim Now"
className="admin-input"
/>
</Field>

{/* Affiliate commission */}
<Field label="Affiliate Commission">
<input
type="text"
value={draft.affiliateCommission}
onChange={(e) => setDraftField("affiliateCommission", e.target.value)}
placeholder="20%"
className="admin-input"
/>
</Field>

{/* Copy variant */}
<Field label="Copy Variant" hint="Psychological framing for all copy">
<div className="flex flex-wrap gap-2">
{COPY_VARIANTS.map((v) => (
<button
key={v}
onClick={() => setDraftField("copyVariant", v)}
className="px-3 py-1.5 rounded-md text-xs font-medium capitalize transition-all"
style={{
background: draft.copyVariant === v ? "var(--admin-primary)" : "var(--admin-surface2)",
color: draft.copyVariant === v ? "white" : "var(--admin-muted)",
border: `1px solid ${draft.copyVariant === v ? "var(--admin-primary)" : "var(--admin-border)"}`,
}}
>
{v}
</button>
))}
</div>
</Field>

{/* Animation */}
<Field label="Animation Preset">
<div className="flex flex-wrap gap-2">
{ANIMATION_PRESETS.map((v) => (
<button
key={v}
onClick={() => setDraftField("animation", v)}
className="px-3 py-1.5 rounded-md text-xs font-medium capitalize transition-all"
style={{
background: draft.animation === v ? "var(--admin-primary)" : "var(--admin-surface2)",
color: draft.animation === v ? "white" : "var(--admin-muted)",
border: `1px solid ${draft.animation === v ? "var(--admin-primary)" : "var(--admin-border)"}`,
}}
>
{v}
</button>
))}
</div>
</Field>

<style>{`
.admin-input {
width: 100%;
padding: 8px 12px;
border-radius: 8px;
font-size: 0.875rem;
outline: none;
transition: border-color 0.15s;
background: var(--admin-surface2);
border: 1px solid var(--admin-border);
color: var(--admin-text);
}
.admin-input:focus {
border-color: var(--admin-primary);
}
.admin-input::placeholder {
color: var(--admin-faint);
}
`}</style>
</div>
);
}

function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) {
return (
<div>
<label className="block text-sm font-medium mb-1.5" style={{ color: "var(--admin-text)" }}>
{label}
{hint && (
<span className="ml-2 text-xs font-normal" style={{ color: "var(--admin-faint)" }}>
{hint}
</span>
)}
</label>
{children}
</div>
);
}
Loading
Loading