aethex-forge/client/pages/dashboards/LabsDashboard.tsx
Claude b640b0d2ad
Mobile optimization pass for responsive layouts
- TabsList: Add responsive grid columns (grid-cols-2/3 on mobile)
- Headers: Stack vertically on mobile with responsive text sizes
- Dialogs: Use viewport-relative heights (70-80vh on mobile)
- Grids: Add sm: breakpoints for single-column mobile layouts
- Tables: Add overflow-x-auto for horizontal scrolling
- Buttons: Full-width on mobile with flex-1 sm:flex-none
- Select triggers: Full-width on mobile

Files updated: 21 component and page files across admin,
staff, dashboards, and hub sections.
2026-01-26 22:46:26 +00:00

833 lines
34 KiB
TypeScript

import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import Layout from "@/components/Layout";
import { Button } from "@/components/ui/button";
import { useAuth } from "@/contexts/AuthContext";
import { useArmTheme } from "@/contexts/ArmThemeContext";
import { supabase } from "@/lib/supabase";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import LoadingScreen from "@/components/LoadingScreen";
import {
Lightbulb,
FileText,
Zap,
Lock,
ExternalLink,
ArrowRight,
AlertCircle,
Send,
Briefcase,
TrendingUp,
Code2,
} from "lucide-react";
const API_BASE = import.meta.env.VITE_API_BASE || "";
interface ResearchTrack {
id: string;
title: string;
description: string;
status: "scoping" | "research" | "in-development" | "testing" | "released";
progress: number;
lead_name: string;
team_size: number;
}
interface IPPortfolioItem {
id: string;
name: string;
type: "patent" | "trademark" | "trade-secret" | "copyright";
status: "filed" | "pending" | "secured" | "expired";
filing_date: string;
licensed_to: string;
}
interface Publication {
id: string;
title: string;
description: string;
status: "drafting" | "review" | "published";
author: string;
published_date: string;
url?: string;
}
interface ResearchBounty {
id: string;
title: string;
description: string;
reward: number;
difficulty: "intermediate" | "advanced" | "expert";
applicants_count: number;
}
export default function LabsDashboard() {
const navigate = useNavigate();
const { user, loading: authLoading } = useAuth();
const { theme } = useArmTheme();
const [activeTab, setActiveTab] = useState("overview");
const [isAccessible, setIsAccessible] = useState(false);
const [researchTracks, setResearchTracks] = useState<ResearchTrack[]>([]);
const [ipPortfolio, setIpPortfolio] = useState<IPPortfolioItem[]>([]);
const [publications, setPublications] = useState<Publication[]>([]);
const [bounties, setBounties] = useState<ResearchBounty[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!authLoading && user) {
checkAccessAndLoadData();
} else if (!authLoading && !user) {
setLoading(false);
}
}, [user, authLoading]);
const checkAccessAndLoadData = async () => {
try {
setLoading(true);
// Check if user has labs affiliation
const {
data: { session },
} = await supabase.auth.getSession();
const token = session?.access_token;
if (!token) throw new Error("No auth token");
// Check arm affiliations
const affiliationRes = await fetch(
`${API_BASE}/api/user/arm-affiliations`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
let hasLabsAccess = false;
if (affiliationRes.ok) {
const data = await affiliationRes.json();
hasLabsAccess =
data.arms?.includes("labs") || data.role === "admin" || data.verified;
}
setIsAccessible(hasLabsAccess);
if (hasLabsAccess) {
// Load research tracks
try {
const tracksRes = await fetch(
`${API_BASE}/api/labs/research-tracks`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
if (tracksRes.ok) {
const data = await tracksRes.json();
setResearchTracks(Array.isArray(data) ? data : []);
}
} catch {
// Silently ignore
}
// Load IP portfolio
try {
const ipRes = await fetch(`${API_BASE}/api/labs/ip-portfolio`, {
headers: { Authorization: `Bearer ${token}` },
});
if (ipRes.ok) {
const data = await ipRes.json();
setIpPortfolio(Array.isArray(data) ? data : []);
}
} catch {
// Silently ignore
}
// Load publications (all)
try {
const pubRes = await fetch(`${API_BASE}/api/labs/publications`, {
headers: { Authorization: `Bearer ${token}` },
});
if (pubRes.ok) {
const data = await pubRes.json();
setPublications(Array.isArray(data) ? data : []);
}
} catch {
// Silently ignore
}
// Load bounties
try {
const bountiesRes = await fetch(`${API_BASE}/api/labs/bounties`, {
headers: { Authorization: `Bearer ${token}` },
});
if (bountiesRes.ok) {
const data = await bountiesRes.json();
setBounties(Array.isArray(data) ? data : []);
}
} catch {
// Silently ignore
}
}
} catch {
// Silently ignore errors
} finally {
setLoading(false);
}
};
if (authLoading || loading) {
return <LoadingScreen message="Loading LABS..." />;
}
if (!user) {
return (
<Layout>
<div className="min-h-screen bg-gradient-to-b from-black via-amber-950/30 to-black flex items-center justify-center px-4">
<div className="max-w-md text-center space-y-6">
<div className="space-y-2">
<Code2 className="h-16 w-16 mx-auto text-amber-400" />
<h1 className="text-4xl font-bold bg-gradient-to-r from-amber-300 to-yellow-300 bg-clip-text text-transparent font-mono">
AeThex LABS
</h1>
</div>
<p className="text-gray-400 text-lg">
Our proprietary R&D skunkworks
</p>
<p className="text-sm text-gray-500">
Access our cutting-edge research, IP portfolio, and publications
</p>
<Button
onClick={() => navigate("/login")}
className="w-full bg-gradient-to-r from-amber-600 to-yellow-600 hover:from-amber-700 hover:to-yellow-700 text-lg py-6"
>
Sign In to Continue
</Button>
</div>
</div>
</Layout>
);
}
if (!isAccessible) {
return (
<Layout>
<div className="min-h-screen bg-gradient-to-b from-black via-amber-950/30 to-black py-8">
<div className="container mx-auto px-4 max-w-2xl">
<Card className="bg-gradient-to-br from-amber-950/40 to-amber-900/20 border-amber-500/30">
<CardContent className="p-12 text-center space-y-6">
<div className="space-y-3">
<Lock className="h-16 w-16 mx-auto text-amber-400" />
<h2 className="text-3xl font-bold text-white font-mono">
Join LABS?
</h2>
<p className="text-gray-400 text-lg">
LABS is our internal R&D department for A-Corp employees
</p>
</div>
<div className="space-y-4 text-left bg-black/30 p-6 rounded-lg border border-amber-500/20">
<h3 className="font-semibold text-white text-sm uppercase tracking-wider">
What is LABS?
</h3>
<p className="text-sm text-gray-400">
LABS is our proprietary, for-profit R&D department that
takes the open-source Axiom Protocol and builds competitive,
closed-source "secret weapons" on top of it.
</p>
<p className="text-sm text-gray-400">
We house active research tracks, manage our IP portfolio,
publish technical whitepapers, and post high-difficulty
research bounties to our elite architect community.
</p>
</div>
<div className="space-y-3">
<Button
onClick={() => navigate("/labs")}
className="w-full bg-gradient-to-r from-amber-600 to-yellow-600 hover:from-amber-700 hover:to-yellow-700 h-12"
>
<ArrowRight className="h-4 w-4 mr-2" />
Explore Published Research
</Button>
<Button
onClick={() => navigate("/labs/join-request")}
variant="outline"
className="w-full border-amber-500/30 text-amber-300 hover:bg-amber-500/10 h-12"
>
Request LABS Access
</Button>
</div>
<p className="text-xs text-gray-500 max-w-sm mx-auto">
To join LABS, you must be a verified A-Corp employee or
architect with proven expertise
</p>
</CardContent>
</Card>
</div>
</div>
</Layout>
);
}
// Main dashboard - user has access
return (
<Layout>
<div
className={`min-h-screen bg-gradient-to-b from-black to-black py-8 ${theme.fontClass}`}
style={{ backgroundImage: theme.wallpaperPattern }}
>
<div className="container mx-auto px-4 max-w-7xl space-y-8">
{/* Header */}
<div className="space-y-4 animate-slide-down">
<div className="flex items-center gap-3">
<Code2 className="h-8 w-8 text-amber-400" />
<h1
className={`text-5xl md:text-6xl font-bold bg-gradient-to-r ${theme.accentColor} bg-clip-text text-transparent font-mono`}
>
LABS
</h1>
</div>
<p className="text-gray-400 text-lg max-w-2xl">
R&D Workshop | Proprietary Research & IP Management
</p>
</div>
{/* Tabs */}
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="w-full"
>
<TabsList
className="grid w-full grid-cols-2 sm:grid-cols-4 bg-amber-950/30 border border-amber-500/20 p-1"
style={{ fontFamily: "Monaco, Courier New, monospace" }}
>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="tracks">Research Tracks</TabsTrigger>
<TabsTrigger value="publications">Publications</TabsTrigger>
<TabsTrigger value="bounties">Bounties</TabsTrigger>
</TabsList>
{/* Overview Tab */}
<TabsContent value="overview" className="space-y-6 animate-fade-in">
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="bg-gradient-to-br from-amber-950/40 to-amber-900/20 border-amber-500/20">
<CardContent className="p-6 space-y-2">
<div className="flex items-center justify-between">
<p className="text-xs text-gray-400 uppercase tracking-wider">
Active Tracks
</p>
<Lightbulb className="h-5 w-5 text-amber-400" />
</div>
<p className="text-3xl font-bold text-white font-mono">
{researchTracks.length}
</p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-yellow-950/40 to-yellow-900/20 border-yellow-500/20">
<CardContent className="p-6 space-y-2">
<div className="flex items-center justify-between">
<p className="text-xs text-gray-400 uppercase tracking-wider">
IP Assets
</p>
<Lock className="h-5 w-5 text-yellow-400" />
</div>
<p className="text-3xl font-bold text-white font-mono">
{ipPortfolio.length}
</p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-orange-950/40 to-orange-900/20 border-orange-500/20">
<CardContent className="p-6 space-y-2">
<div className="flex items-center justify-between">
<p className="text-xs text-gray-400 uppercase tracking-wider">
Publications
</p>
<FileText className="h-5 w-5 text-orange-400" />
</div>
<p className="text-3xl font-bold text-white font-mono">
{publications.length}
</p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-amber-950/40 to-amber-900/20 border-amber-500/20">
<CardContent className="p-6 space-y-2">
<div className="flex items-center justify-between">
<p className="text-xs text-gray-400 uppercase tracking-wider">
Bounties
</p>
<Zap className="h-5 w-5 text-amber-400" />
</div>
<p className="text-3xl font-bold text-white font-mono">
{bounties.length}
</p>
</CardContent>
</Card>
</div>
{/* Featured Research Track */}
{researchTracks.length > 0 && (
<Card className="bg-gradient-to-br from-amber-950/40 to-amber-900/20 border-amber-500/20">
<CardHeader>
<CardTitle>Featured Research Track</CardTitle>
<CardDescription>
Our current flagship R&D initiative
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{(() => {
const featured = researchTracks[0];
return (
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold text-white mb-1">
{featured.title}
</h3>
<p className="text-sm text-gray-400">
{featured.description}
</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<div className="space-y-1">
<p className="text-xs text-gray-500 uppercase">
Lead
</p>
<p className="text-sm font-mono text-white">
{featured.lead_name}
</p>
</div>
<div className="space-y-1">
<p className="text-xs text-gray-500 uppercase">
Team Size
</p>
<p className="text-sm font-mono text-white">
{featured.team_size} members
</p>
</div>
<div className="space-y-1">
<p className="text-xs text-gray-500 uppercase">
Progress
</p>
<p className="text-sm font-mono text-white">
{featured.progress}%
</p>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-gray-400">
Overall Progress
</span>
<span className="font-mono text-amber-400">
{featured.progress}%
</span>
</div>
<div className="w-full bg-black/30 rounded h-2 border border-amber-500/20 overflow-hidden">
<div
className="h-full bg-gradient-to-r from-amber-500 to-yellow-500"
style={{ width: `${featured.progress}%` }}
/>
</div>
</div>
<Badge className="bg-amber-600/50 text-amber-100 capitalize w-fit">
{featured.status.replace("-", " ")}
</Badge>
</div>
);
})()}
</CardContent>
</Card>
)}
{/* Recent Publications */}
{publications.filter((p) => p.status === "published").length >
0 && (
<Card className="bg-gradient-to-br from-orange-950/40 to-orange-900/20 border-orange-500/20">
<CardHeader>
<CardTitle>Recent Publications</CardTitle>
<CardDescription>
Latest technical whitepapers and blog posts
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{publications
.filter((p) => p.status === "published")
.slice(0, 3)
.map((pub) => (
<a
key={pub.id}
href={pub.url}
target="_blank"
rel="noopener noreferrer"
className="p-4 bg-black/30 rounded-lg border border-orange-500/10 hover:border-orange-500/30 transition block group"
>
<div className="flex items-start gap-3">
<FileText className="h-5 w-5 text-orange-400 flex-shrink-0 mt-1" />
<div className="flex-1 min-w-0">
<p className="font-semibold text-white group-hover:text-orange-300 transition truncate">
{pub.title}
</p>
<p className="text-xs text-gray-400 mt-1">
By {pub.author} {" "}
{new Date(
pub.published_date,
).toLocaleDateString()}
</p>
</div>
<ExternalLink className="h-4 w-4 text-gray-500 flex-shrink-0 group-hover:text-orange-400 transition" />
</div>
</a>
))}
</CardContent>
</Card>
)}
{/* CTA Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-gradient-to-br from-amber-600/20 to-yellow-600/20 border-amber-500/40">
<CardContent className="p-8 flex flex-col justify-center h-full space-y-4">
<h3
className="text-xl font-bold text-white font-mono"
style={{ fontFamily: "Monaco, Courier New, monospace" }}
>
Submit Research Proposal
</h3>
<p className="text-sm text-gray-300">
Propose a new R&D initiative for LABS
</p>
<Button
onClick={() => navigate("/labs/submit-proposal")}
className="w-full bg-gradient-to-r from-amber-600 to-yellow-600 hover:from-amber-700 hover:to-yellow-700 justify-center"
>
<Send className="h-4 w-4 mr-2" />
Submit Proposal
</Button>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-yellow-600/20 to-amber-600/20 border-yellow-500/40">
<CardContent className="p-8 flex flex-col justify-center h-full space-y-4">
<h3
className="text-xl font-bold text-white font-mono"
style={{ fontFamily: "Monaco, Courier New, monospace" }}
>
Browse LABS Bounties
</h3>
<p className="text-sm text-gray-300">
High-difficulty research opportunities from NEXUS
</p>
<Button
onClick={() => navigate("/nexus?category=research")}
className="w-full bg-gradient-to-r from-yellow-600 to-amber-600 hover:from-yellow-700 hover:to-amber-700 justify-center"
>
<ArrowRight className="h-4 w-4 mr-2" />
Browse Bounties
</Button>
</CardContent>
</Card>
</div>
</TabsContent>
{/* Research Tracks Tab */}
<TabsContent value="tracks" className="space-y-4 animate-fade-in">
{researchTracks.length === 0 ? (
<Card className="bg-gradient-to-br from-amber-950/40 to-amber-900/20 border-amber-500/20">
<CardContent className="p-12 text-center space-y-4">
<Lightbulb className="h-12 w-12 mx-auto text-amber-500 opacity-50" />
<p className="text-gray-400">No active research tracks</p>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{researchTracks.map((track) => (
<Card
key={track.id}
className="bg-gradient-to-br from-amber-950/40 to-amber-900/20 border-amber-500/20 hover:border-amber-500/40 transition"
>
<CardHeader>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<CardTitle className="text-base truncate">
{track.title}
</CardTitle>
</div>
<Badge className="capitalize shrink-0">
{track.status.replace("-", " ")}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-gray-400">
{track.description}
</p>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<p className="text-xs text-gray-500 uppercase">
Lead
</p>
<p className="text-sm font-mono text-white">
{track.lead_name}
</p>
</div>
<div className="space-y-1">
<p className="text-xs text-gray-500 uppercase">
Team
</p>
<p className="text-sm font-mono text-white">
{track.team_size} members
</p>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-gray-400">Progress</span>
<span className="font-mono text-amber-400">
{track.progress}%
</span>
</div>
<div className="w-full bg-black/30 rounded h-2 border border-amber-500/20 overflow-hidden">
<div
className="h-full bg-gradient-to-r from-amber-500 to-yellow-500"
style={{ width: `${track.progress}%` }}
/>
</div>
</div>
<Button
size="sm"
variant="outline"
className="w-full border-amber-500/30 text-amber-300 hover:bg-amber-500/10"
>
View Details <ArrowRight className="h-3 w-3 ml-2" />
</Button>
</CardContent>
</Card>
))}
</div>
)}
</TabsContent>
{/* Publications Tab */}
<TabsContent
value="publications"
className="space-y-4 animate-fade-in"
>
{publications.length === 0 ? (
<Card className="bg-gradient-to-br from-orange-950/40 to-orange-900/20 border-orange-500/20">
<CardContent className="p-12 text-center space-y-4">
<FileText className="h-12 w-12 mx-auto text-orange-500 opacity-50" />
<p className="text-gray-400">No publications yet</p>
</CardContent>
</Card>
) : (
<div className="space-y-3">
{publications.map((pub) => (
<a
key={pub.id}
href={pub.url}
target="_blank"
rel="noopener noreferrer"
className="p-4 bg-gradient-to-r from-orange-950/40 to-orange-900/20 rounded-lg border border-orange-500/20 hover:border-orange-500/40 transition block group"
>
<div className="flex items-start justify-between gap-4 mb-2">
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-white group-hover:text-orange-300 transition truncate">
{pub.title}
</h4>
</div>
<Badge
className={
pub.status === "published"
? "bg-green-600/50 text-green-100 capitalize shrink-0"
: "bg-blue-600/50 text-blue-100 capitalize shrink-0"
}
>
{pub.status}
</Badge>
</div>
<p className="text-sm text-gray-400 mb-2">
{pub.description}
</p>
<div className="flex items-center justify-between">
<p className="text-xs text-gray-500">
By {pub.author} {" "}
{new Date(pub.published_date).toLocaleDateString()}
</p>
{pub.url && (
<ExternalLink className="h-4 w-4 text-orange-400 group-hover:translate-x-0.5 transition" />
)}
</div>
</a>
))}
</div>
)}
</TabsContent>
{/* Bounties Tab */}
<TabsContent value="bounties" className="space-y-4 animate-fade-in">
{bounties.length === 0 ? (
<Card className="bg-gradient-to-br from-amber-950/40 to-amber-900/20 border-amber-500/20">
<CardContent className="p-12 text-center space-y-4">
<Zap className="h-12 w-12 mx-auto text-amber-500 opacity-50" />
<p className="text-gray-400">
No active research bounties at this time
</p>
<Button
onClick={() => navigate("/nexus")}
variant="outline"
className="border-amber-500/30 text-amber-300 hover:bg-amber-500/10"
>
Browse All NEXUS Bounties
</Button>
</CardContent>
</Card>
) : (
<div className="space-y-3">
{bounties.map((bounty) => (
<Card
key={bounty.id}
className="bg-gradient-to-br from-amber-950/40 to-amber-900/20 border-amber-500/20 hover:border-amber-500/40 transition"
>
<CardContent className="p-4">
<div className="flex items-start justify-between gap-4 mb-3">
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-white truncate">
{bounty.title}
</h4>
</div>
<div className="text-right shrink-0">
<p className="text-lg font-bold text-amber-400 font-mono">
${bounty.reward.toLocaleString()}
</p>
</div>
</div>
<p className="text-sm text-gray-400 mb-3">
{bounty.description}
</p>
<div className="flex items-center justify-between mb-3">
<Badge
variant="outline"
className={
bounty.difficulty === "expert"
? "border-red-500/50 text-red-300"
: bounty.difficulty === "advanced"
? "border-orange-500/50 text-orange-300"
: "border-yellow-500/50 text-yellow-300"
}
>
{bounty.difficulty}
</Badge>
<span className="text-xs text-gray-500">
{bounty.applicants_count} applicants
</span>
</div>
<Button
size="sm"
className="w-full bg-gradient-to-r from-amber-600 to-yellow-600 hover:from-amber-700 hover:to-yellow-700"
>
View & Apply <ArrowRight className="h-3 w-3 ml-2" />
</Button>
</CardContent>
</Card>
))}
</div>
)}
</TabsContent>
{/* IP Portfolio - Admin Only */}
{ipPortfolio.length > 0 && (
<Card className="bg-gradient-to-br from-red-950/40 to-red-900/20 border-red-500/20 mt-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Lock className="h-5 w-5 text-red-500" />
IP Portfolio
</CardTitle>
<CardDescription>
Proprietary intellectual property assets
</CardDescription>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-red-500/20">
<th className="text-left py-3 px-3 font-semibold text-gray-400 uppercase text-xs">
IP Name
</th>
<th className="text-left py-3 px-3 font-semibold text-gray-400 uppercase text-xs">
Type
</th>
<th className="text-left py-3 px-3 font-semibold text-gray-400 uppercase text-xs">
Status
</th>
<th className="text-left py-3 px-3 font-semibold text-gray-400 uppercase text-xs">
Licensed To
</th>
</tr>
</thead>
<tbody>
{ipPortfolio.map((item) => (
<tr
key={item.id}
className="border-b border-red-500/10 hover:bg-red-500/5 transition"
>
<td className="py-3 px-3 text-white font-mono text-xs">
{item.name}
</td>
<td className="py-3 px-3">
<Badge
variant="outline"
className="capitalize text-xs"
>
{item.type.replace("-", " ")}
</Badge>
</td>
<td className="py-3 px-3">
<Badge
className={
item.status === "secured"
? "bg-green-600/50 text-green-100"
: item.status === "filed"
? "bg-blue-600/50 text-blue-100"
: "bg-yellow-600/50 text-yellow-100"
}
>
{item.status}
</Badge>
</td>
<td className="py-3 px-3 text-gray-400 text-xs">
{item.licensed_to}
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
)}
</Tabs>
</div>
</div>
</Layout>
);
}