Compare commits

..

11 commits

Author SHA1 Message Date
MrPiglr
0e605c10d4
new file: aethex-dev-docs.zip 2026-02-11 19:00:29 +00:00
MrPiglr
dff8b2ecb1
new file: aethex-logos/icons/1024x1024.png 2026-02-05 10:04:49 +00:00
MrPiglr
4216430546
modified: api/staff/me.ts 2026-01-28 04:52:03 +00:00
MrPiglr
07dd8be820
modified: client/pages/Foundation.tsx 2026-01-27 21:39:22 +00:00
MrPiglr
e19d4ba859
Batch 4: Fix Arms, Investors content density
- Simplified ARM descriptions and tips
- Reduced Investors heading size (text-4xl→text-3xl)
- Normalized verbose descriptions 40-50%

TypeScript validation: No new errors
2026-01-11 04:03:03 +00:00
MrPiglr
b80c0c5d19
Batch 3: Fix public pages content density
- Reduced heading sizes (text-4xl→text-3xl)
- Simplified descriptions 40-60%
- Normalized verbose text
- Fixed: Trust, Downloads, Careers

TypeScript validation: No new errors
2026-01-11 03:54:41 +00:00
MrPiglr
28d9410a9f
Batch 2: Fix documentation pages content density
- Reduced heading sizes (text-3xl→text-2xl)
- Simplified descriptions 50-60%
- Normalized verbose technical text
- Fixed: DocsGettingStarted, DocsPlatform, DocsApiReference, DocsCli, DocsExamples, DocsIntegrations, DocsCurriculum

TypeScript validation: No new errors introduced
2026-01-11 03:52:55 +00:00
MrPiglr
e6b1617b43
Batch 1: Fix About, Contact, GetStarted, Nexus, Corp, Opportunities
 Tested with TypeScript (no new errors)

- About: text-5xl/7xl → text-4xl/5xl, simplified hero text
- Contact: text-4xl → text-3xl heading
- GetStarted: Simplified feature descriptions (60% text reduction)
- Nexus: text-4xl/6xl → text-4xl/5xl, simplified description
- Corp: Shortened service titles and descriptions
- OpportunitiesHub: text-4xl/5xl → text-4xl, simplified text

All changes tested and ready!
2026-01-11 03:30:44 +00:00
MrPiglr
e633079933
Fix button and text sizing issues
- Reduced button sizes: h-16 → h-12, text-xl → text-base
- Reduced padding: px-10 → px-8
- Fixed icon sizes: w-6 h-6 → w-5 h-5
- Normalized font weights: font-black → font-bold
- Fixed hero headings: text-7xl/8xl/9xl → text-5xl/6xl/7xl
- Fixed subheadings: text-2xl/3xl → text-xl/2xl
- Fixed section headings: text-5xl/6xl → text-4xl/5xl
- Removed excessive text-lg from buttons

Pages fixed:
 Homepage
 Dashboard
 Realms
 Labs
 GameForge
 Foundation

Much more readable and balanced!
2026-01-11 03:23:48 +00:00
MrPiglr
5a9487f148
Content density fixes: All 19 high-traffic pages
 Homepage + Dashboard (already committed)
 Realms, Labs, GameForge, Foundation
 Teams, Squads, Projects
 Dev Platform landing

Changes across all pages:
- Cut text 50-60% (paragraphs → sentences)
- Increased spacing (space-y-8 → space-y-12/20)
- Increased padding (p-6 → p-8/10)
- Removed verbose descriptions
- Simplified CTAs

EASY TO RESTORE:
  git checkout main && git branch -D content-density-fixes
2026-01-11 02:50:26 +00:00
MrPiglr
859f69a245
Content density fix: Homepage + Dashboard
- Homepage: Reduce card descriptions 60% (20-30 words → 5-8 words)
- Homepage: Simplify features from paragraphs to single lines
- Homepage: Increase section spacing (space-y-32 → space-y-40)
- Homepage: Increase card padding and spacing
- Dashboard: Simplify Developer CTA (cut text 50%, removed extra button)
- Dashboard: Increase card padding (p-6 → p-8)
- Dashboard: Increase grid spacing (gap-4 → gap-6)
- Dashboard: Increase section spacing (space-y-8 → space-y-12)

All changes on 'content-density-fixes' branch - easy to revert with:
  git checkout main  # restore everything
  git branch -D content-density-fixes  # delete branch
2026-01-11 02:32:10 +00:00
198 changed files with 8367 additions and 40438 deletions

View file

@ -35,6 +35,7 @@ data
.env .env
load-ids.txt load-ids.txt
server
tmp tmp
types types
.git .git

View file

@ -7,22 +7,14 @@ COPY package.json package-lock.json* pnpm-lock.yaml* npm-shrinkwrap.json* ./
# Install dependencies # Install dependencies
RUN if [ -f pnpm-lock.yaml ]; then npm install -g pnpm && pnpm install --frozen-lockfile; \ RUN if [ -f pnpm-lock.yaml ]; then npm install -g pnpm && pnpm install --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci --legacy-peer-deps; \ elif [ -f package-lock.json ]; then npm ci; \
else npm install --legacy-peer-deps; fi else npm install; fi
# Copy source code # Copy source code
COPY . . COPY . .
# Build-time env vars (VITE_* are baked into the bundle at build time) # Build the app (frontend + server)
ARG VITE_SUPABASE_URL RUN npm run build
ARG VITE_SUPABASE_ANON_KEY
ARG VITE_AUTHENTIK_PROVIDER
ENV VITE_SUPABASE_URL=$VITE_SUPABASE_URL
ENV VITE_SUPABASE_ANON_KEY=$VITE_SUPABASE_ANON_KEY
ENV VITE_AUTHENTIK_PROVIDER=$VITE_AUTHENTIK_PROVIDER
# Build the client so the Activity gets compiled JS (no Vite dev mode in Discord iframe)
RUN npm run build:client
# Set environment # Set environment
ENV NODE_ENV=production ENV NODE_ENV=production
@ -32,4 +24,4 @@ ENV PORT=3000
EXPOSE 3000 EXPOSE 3000
# Start the server # Start the server
CMD ["npm", "run", "dev"] CMD ["npm", "start"]

106
README.md
View file

@ -1,45 +1,54 @@
# AeThex Forge - Local Development Setup # AeThex Forge Local Development Setup
## Quick Start Guide ## Quick Start Guide
This guide will help you set up and run the AeThex platform locally on your machine. This guide will help you set up and run the AeThex Developer Platform locally.
## Prerequisites ## Prerequisites
1. **Node.js** (v18 or higher) 1. **Node.js** (v18 or higher)
- Download from: https://nodejs.org/ - [Download Node.js](https://nodejs.org/)
- This will also install npm (Node Package Manager) - Includes npm (Node Package Manager)
2. **Git** (optional, if you want to clone updates) 2. **Git** (recommended for updates)
- Download from: https://git-scm.com/ - [Download Git](https://git-scm.com/)
## Installation Steps ## Installation Steps
### 1. Install Node.js ### 1. Install Node.js
- Visit https://nodejs.org/ and download the LTS version
- Run the installer and follow the setup wizard - Download and install the LTS version from [nodejs.org](https://nodejs.org/)
- Restart your terminal/PowerShell after installation - Follow the setup wizard
- Restart your terminal after installation
### 2. Verify Installation ### 2. Verify Installation
Open PowerShell or Command Prompt and run:
```powershell Open your terminal and run:
```bash
node --version node --version
npm --version npm --version
``` ```
You should see version numbers (e.g., v20.x.x and 10.x.x) You should see version numbers (e.g., v20.x.x and 10.x.x)
### 3. Install Project Dependencies ### 3. Install Project Dependencies
Navigate to the project folder and install dependencies: Navigate to the project folder and install dependencies:
```powershell
cd C:\Users\PCOEM\Downloads\aethex-forge\aethex-forge ```bash
cd /path/to/aethex-forge
npm install npm install
``` ```
This may take a few minutes as it downloads all required packages. This may take a few minutes as it downloads all required packages.
### 4. Set Up Environment Variables ### 4. Set Up Environment Variables
Create a `.env` file in the root directory (`aethex-forge` folder) with the following variables:
Create a `.env` file in the root directory (`aethex-forge/`) with the following variables:
**Minimum Required (to run the app):** **Minimum Required (to run the app):**
```env ```env
# Supabase Configuration (Required) # Supabase Configuration (Required)
VITE_SUPABASE_URL=your_supabase_url_here VITE_SUPABASE_URL=your_supabase_url_here
@ -52,6 +61,7 @@ VITE_API_BASE=http://localhost:5000
``` ```
**Optional (for full functionality):** **Optional (for full functionality):**
```env ```env
# Discord Integration # Discord Integration
DISCORD_CLIENT_ID=your_discord_client_id DISCORD_CLIENT_ID=your_discord_client_id
@ -76,76 +86,84 @@ VITE_GHOST_API_URL=your_ghost_api_url
GHOST_ADMIN_API_KEY=your_ghost_admin_key GHOST_ADMIN_API_KEY=your_ghost_admin_key
``` ```
**Note:** You can start with just the Supabase variables to get the app running. Other features will work once you add their respective credentials. **Note:** You can start with just the Supabase variables to get the app running. Add other credentials as needed for full functionality.
### 5. Run the Development Server ### 5. Run the Development Server
```powershell
```bash
npm run dev npm run dev
``` ```
The application will start on **http://localhost:5000** The application will start on **[http://localhost:5000](http://localhost:5000)** (or the port set in your config).
Open your browser and navigate to that URL to view the application. Open your browser and navigate to that URL to view the application.
## Available Commands ## Available Commands
- `npm run dev` - Start development server (port 5000) - `npm run dev` Start development server (default: port 5000)
- `npm run build` - Build for production - `npm run build` Build for production
- `npm start` - Start production server - `npm start` Start production server
- `npm run typecheck` - Check TypeScript types - `npm run typecheck` Check TypeScript types
- `npm test` - Run tests - `npm test` Run tests
aethex-forge/
aethex-forge/
## Project Structure ## Project Structure
``` ```text
aethex-forge/ aethex-forge/
├── client/ # React frontend (pages, components) ├── client/ # React SPA frontend (pages, components, UI)
├── server/ # Express backend API ├── server/ # Express backend API
├── api/ # API route handlers ├── api/ # API route handlers (modular)
├── shared/ # Shared types between client/server ├── shared/ # Shared types/interfaces (client/server)
├── discord-bot/ # Discord bot integration ├── supabase/ # Database migrations & SQL
└── supabase/ # Database migrations ├── docs/ # Project documentation
└── ... # Other supporting folders
``` ```
## Getting Supabase Credentials ## Getting Supabase Credentials
If you don't have Supabase credentials yet: If you don't have Supabase credentials yet:
1. Go to https://supabase.com/ 1. Go to [supabase.com](https://supabase.com/)
2. Create a free account 2. Create a free account and project
3. Create a new project 3. In your project, go to **Settings → API**
4. Go to Project Settings → API 4. Copy:
5. Copy:
- Project URL → `VITE_SUPABASE_URL` and `SUPABASE_URL` - Project URL → `VITE_SUPABASE_URL` and `SUPABASE_URL`
- `anon` `public` key → `VITE_SUPABASE_ANON_KEY` - `anon` public key → `VITE_SUPABASE_ANON_KEY`
- `service_role` `secret` key → `SUPABASE_SERVICE_ROLE` - `service_role` secret key → `SUPABASE_SERVICE_ROLE`
## Troubleshooting ## Troubleshooting
### Port Already in Use ### Port Already in Use
If port 5000 is already in use, you can change it in `vite.config.ts`: If port 5000 is already in use, you can change it in `vite.config.ts`:
```typescript ```typescript
server: { server: {
port: 5001, // Change to any available port port: 5001, // Change to any available port
} }
``` ```
### Module Not Found Errors ### Module Not Found Errors
Try deleting `node_modules` and `package-lock.json`, then run `npm install` again: Try deleting `node_modules` and `package-lock.json`, then run `npm install` again:
```powershell
Remove-Item -Recurse -Force node_modules ```bash
Remove-Item package-lock.json rm -rf node_modules package-lock.json
npm install npm install
``` ```
### Environment Variables Not Loading ### Environment Variables Not Loading
- Make sure `.env` file is in the root `aethex-forge` directory
- Ensure `.env` is in the root `aethex-forge` directory
- Restart the dev server after adding new environment variables - Restart the dev server after adding new environment variables
- Variables starting with `VITE_` are exposed to the client - Variables starting with `VITE_` are exposed to the client
## Need Help? ## Need Help?
- Check the `docs/` folder for detailed documentation - See the `docs/` folder for detailed documentation
- Review `AGENTS.md` for architecture details - Review `AGENTS.md` for architecture and tech stack
- See `replit.md` for deployment information - See `replit.md` for cloud deployment info
- For advanced troubleshooting, check `DEPLOYMENT_CHECKLIST.md` and `PHASE1_IMPLEMENTATION_SUMMARY.md`

BIN
aethex-dev-docs.zip Normal file

Binary file not shown.

BIN
aethex-logos.zip Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,021 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
aethex-logos/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View file

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1e3a5f;stop-opacity:1" />
<stop offset="100%" style="stop-color:#0f172a;stop-opacity:1" />
</linearGradient>
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="512" height="512" rx="80" fill="url(#bg)"/>
<path d="M256 100 L380 380 L256 320 L132 380 Z" fill="url(#accent)" opacity="0.9"/>
<path d="M256 140 L350 340 L256 295 L162 340 Z" fill="#0f172a" opacity="0.3"/>
<circle cx="256" cy="220" r="30" fill="#fff" opacity="0.9"/>
</svg>

After

Width:  |  Height:  |  Size: 861 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

View file

@ -1,187 +0,0 @@
import { supabase } from "../_supabase.js";
export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
// Check if user is admin
const { data: profile } = await supabase
.from("profiles")
.select("role")
.eq("id", userData.user.id)
.single();
if (!profile || profile.role !== "admin") {
return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403, headers: { "Content-Type": "application/json" } });
}
const url = new URL(req.url);
const period = url.searchParams.get("period") || "30"; // days
try {
if (req.method === "GET") {
const daysAgo = new Date();
daysAgo.setDate(daysAgo.getDate() - parseInt(period));
// Get total users and growth
const { count: totalUsers } = await supabase
.from("profiles")
.select("*", { count: "exact", head: true });
const { count: newUsersThisPeriod } = await supabase
.from("profiles")
.select("*", { count: "exact", head: true })
.gte("created_at", daysAgo.toISOString());
// Get active users (logged in within period)
const { count: activeUsers } = await supabase
.from("profiles")
.select("*", { count: "exact", head: true })
.gte("last_login_at", daysAgo.toISOString());
// Get opportunities stats
const { count: totalOpportunities } = await supabase
.from("aethex_opportunities")
.select("*", { count: "exact", head: true });
const { count: openOpportunities } = await supabase
.from("aethex_opportunities")
.select("*", { count: "exact", head: true })
.eq("status", "open");
const { count: newOpportunities } = await supabase
.from("aethex_opportunities")
.select("*", { count: "exact", head: true })
.gte("created_at", daysAgo.toISOString());
// Get applications stats
const { count: totalApplications } = await supabase
.from("aethex_applications")
.select("*", { count: "exact", head: true });
const { count: newApplications } = await supabase
.from("aethex_applications")
.select("*", { count: "exact", head: true })
.gte("created_at", daysAgo.toISOString());
// Get contracts stats
const { count: totalContracts } = await supabase
.from("nexus_contracts")
.select("*", { count: "exact", head: true });
const { count: activeContracts } = await supabase
.from("nexus_contracts")
.select("*", { count: "exact", head: true })
.eq("status", "active");
// Get community stats
const { count: totalPosts } = await supabase
.from("community_posts")
.select("*", { count: "exact", head: true });
const { count: newPosts } = await supabase
.from("community_posts")
.select("*", { count: "exact", head: true })
.gte("created_at", daysAgo.toISOString());
// Get creator stats
const { count: totalCreators } = await supabase
.from("aethex_creators")
.select("*", { count: "exact", head: true });
// Get daily signups for trend (last 30 days)
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const { data: signupTrend } = await supabase
.from("profiles")
.select("created_at")
.gte("created_at", thirtyDaysAgo.toISOString())
.order("created_at");
// Group signups by date
const signupsByDate: Record<string, number> = {};
signupTrend?.forEach((user) => {
const date = new Date(user.created_at).toISOString().split("T")[0];
signupsByDate[date] = (signupsByDate[date] || 0) + 1;
});
const dailySignups = Object.entries(signupsByDate).map(([date, count]) => ({
date,
count
}));
// Revenue data (if available)
const { data: revenueData } = await supabase
.from("nexus_payments")
.select("amount, created_at")
.eq("status", "completed")
.gte("created_at", daysAgo.toISOString());
const totalRevenue = revenueData?.reduce((sum, p) => sum + (p.amount || 0), 0) || 0;
// Top performing opportunities
const { data: topOpportunities } = await supabase
.from("aethex_opportunities")
.select(`
id,
title,
aethex_applications(count)
`)
.eq("status", "open")
.order("created_at", { ascending: false })
.limit(5);
return new Response(JSON.stringify({
users: {
total: totalUsers || 0,
new: newUsersThisPeriod || 0,
active: activeUsers || 0,
creators: totalCreators || 0
},
opportunities: {
total: totalOpportunities || 0,
open: openOpportunities || 0,
new: newOpportunities || 0
},
applications: {
total: totalApplications || 0,
new: newApplications || 0
},
contracts: {
total: totalContracts || 0,
active: activeContracts || 0
},
community: {
posts: totalPosts || 0,
newPosts: newPosts || 0
},
revenue: {
total: totalRevenue,
period: `${period} days`
},
trends: {
dailySignups,
topOpportunities: topOpportunities?.map(o => ({
id: o.id,
title: o.title,
applications: o.aethex_applications?.[0]?.count || 0
})) || []
},
period: parseInt(period)
}), { headers: { "Content-Type": "application/json" } });
}
return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
} catch (err: any) {
console.error("Analytics API error:", err);
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { "Content-Type": "application/json" } });
}
};

View file

@ -1,245 +0,0 @@
import { supabase } from "../_supabase.js";
export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
// Check if user is admin
const { data: profile } = await supabase
.from("profiles")
.select("role")
.eq("id", userData.user.id)
.single();
if (!profile || profile.role !== "admin") {
return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403, headers: { "Content-Type": "application/json" } });
}
const url = new URL(req.url);
try {
// GET - Fetch reports and stats
if (req.method === "GET") {
const status = url.searchParams.get("status") || "open";
const type = url.searchParams.get("type"); // report, dispute, user
// Get content reports
let reportsQuery = supabase
.from("moderation_reports")
.select(`
*,
reporter:profiles!moderation_reports_reporter_id_fkey(id, full_name, email, avatar_url)
`)
.order("created_at", { ascending: false })
.limit(50);
if (status !== "all") {
reportsQuery = reportsQuery.eq("status", status);
}
if (type && type !== "all") {
reportsQuery = reportsQuery.eq("target_type", type);
}
const { data: reports, error: reportsError } = await reportsQuery;
if (reportsError) console.error("Reports error:", reportsError);
// Get disputes
let disputesQuery = supabase
.from("nexus_disputes")
.select(`
*,
reporter:profiles!nexus_disputes_reported_by_fkey(id, full_name, email)
`)
.order("created_at", { ascending: false })
.limit(50);
if (status !== "all") {
disputesQuery = disputesQuery.eq("status", status);
}
const { data: disputes, error: disputesError } = await disputesQuery;
if (disputesError) console.error("Disputes error:", disputesError);
// Get flagged users (users with warnings/bans)
const { data: flaggedUsers } = await supabase
.from("profiles")
.select("id, full_name, email, avatar_url, is_banned, warning_count, created_at")
.or("is_banned.eq.true,warning_count.gt.0")
.order("created_at", { ascending: false })
.limit(50);
// Calculate stats
const { count: openReports } = await supabase
.from("moderation_reports")
.select("*", { count: "exact", head: true })
.eq("status", "open");
const { count: openDisputes } = await supabase
.from("nexus_disputes")
.select("*", { count: "exact", head: true })
.eq("status", "open");
const { count: resolvedToday } = await supabase
.from("moderation_reports")
.select("*", { count: "exact", head: true })
.eq("status", "resolved")
.gte("updated_at", new Date(new Date().setHours(0, 0, 0, 0)).toISOString());
const stats = {
openReports: openReports || 0,
openDisputes: openDisputes || 0,
resolvedToday: resolvedToday || 0,
flaggedUsers: flaggedUsers?.length || 0
};
return new Response(JSON.stringify({
reports: reports || [],
disputes: disputes || [],
flaggedUsers: flaggedUsers || [],
stats
}), { headers: { "Content-Type": "application/json" } });
}
// POST - Take moderation action
if (req.method === "POST") {
const body = await req.json();
// Resolve/ignore report
if (body.action === "update_report") {
const { report_id, status, resolution_notes } = body;
const { data, error } = await supabase
.from("moderation_reports")
.update({
status,
resolution_notes,
resolved_by: userData.user.id,
updated_at: new Date().toISOString()
})
.eq("id", report_id)
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ report: data }), { headers: { "Content-Type": "application/json" } });
}
// Resolve dispute
if (body.action === "update_dispute") {
const { dispute_id, status, resolution_notes } = body;
const { data, error } = await supabase
.from("nexus_disputes")
.update({
status,
resolution_notes,
resolved_by: userData.user.id,
resolved_at: new Date().toISOString()
})
.eq("id", dispute_id)
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ dispute: data }), { headers: { "Content-Type": "application/json" } });
}
// Ban/warn user
if (body.action === "moderate_user") {
const { user_id, action_type, reason } = body;
if (action_type === "ban") {
const { data, error } = await supabase
.from("profiles")
.update({
is_banned: true,
ban_reason: reason,
banned_at: new Date().toISOString(),
banned_by: userData.user.id
})
.eq("id", user_id)
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ user: data, action: "banned" }), { headers: { "Content-Type": "application/json" } });
}
if (action_type === "warn") {
const { data: currentUser } = await supabase
.from("profiles")
.select("warning_count")
.eq("id", user_id)
.single();
const { data, error } = await supabase
.from("profiles")
.update({
warning_count: (currentUser?.warning_count || 0) + 1,
last_warning_at: new Date().toISOString(),
last_warning_reason: reason
})
.eq("id", user_id)
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ user: data, action: "warned" }), { headers: { "Content-Type": "application/json" } });
}
if (action_type === "unban") {
const { data, error } = await supabase
.from("profiles")
.update({
is_banned: false,
ban_reason: null,
unbanned_at: new Date().toISOString(),
unbanned_by: userData.user.id
})
.eq("id", user_id)
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ user: data, action: "unbanned" }), { headers: { "Content-Type": "application/json" } });
}
}
// Delete content
if (body.action === "delete_content") {
const { content_type, content_id } = body;
const tableMap: Record<string, string> = {
post: "community_posts",
comment: "community_comments",
project: "projects",
opportunity: "aethex_opportunities"
};
const table = tableMap[content_type];
if (!table) {
return new Response(JSON.stringify({ error: "Invalid content type" }), { status: 400, headers: { "Content-Type": "application/json" } });
}
const { error } = await supabase.from(table).delete().eq("id", content_id);
if (error) throw error;
return new Response(JSON.stringify({ success: true, deleted: content_type }), { headers: { "Content-Type": "application/json" } });
}
return new Response(JSON.stringify({ error: "Invalid action" }), { status: 400, headers: { "Content-Type": "application/json" } });
}
return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
} catch (err: any) {
console.error("Moderation API error:", err);
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { "Content-Type": "application/json" } });
}
};

View file

@ -1,196 +0,0 @@
import { supabase } from "../_supabase.js";
export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const userId = userData.user.id;
const url = new URL(req.url);
try {
// GET - Fetch interviews
if (req.method === "GET") {
const status = url.searchParams.get("status");
const upcoming = url.searchParams.get("upcoming") === "true";
let query = supabase
.from("candidate_interviews")
.select(
`
*,
employer:profiles!candidate_interviews_employer_id_fkey(
full_name,
avatar_url,
email
)
`,
)
.eq("candidate_id", userId)
.order("scheduled_at", { ascending: true });
if (status) {
query = query.eq("status", status);
}
if (upcoming) {
query = query
.gte("scheduled_at", new Date().toISOString())
.in("status", ["scheduled", "rescheduled"]);
}
const { data: interviews, error } = await query;
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
// Group by status
const grouped = {
upcoming: interviews?.filter(
(i) =>
["scheduled", "rescheduled"].includes(i.status) &&
new Date(i.scheduled_at) >= new Date(),
) || [],
past: interviews?.filter(
(i) =>
i.status === "completed" ||
new Date(i.scheduled_at) < new Date(),
) || [],
cancelled: interviews?.filter((i) => i.status === "cancelled") || [],
};
return new Response(
JSON.stringify({
interviews: interviews || [],
grouped,
total: interviews?.length || 0,
}),
{
headers: { "Content-Type": "application/json" },
},
);
}
// POST - Create interview (for self-scheduling or employer invites)
if (req.method === "POST") {
const body = await req.json();
const {
application_id,
employer_id,
opportunity_id,
scheduled_at,
duration_minutes,
meeting_link,
meeting_type,
notes,
} = body;
if (!scheduled_at || !employer_id) {
return new Response(
JSON.stringify({ error: "scheduled_at and employer_id are required" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
const { data, error } = await supabase
.from("candidate_interviews")
.insert({
application_id,
candidate_id: userId,
employer_id,
opportunity_id,
scheduled_at,
duration_minutes: duration_minutes || 30,
meeting_link,
meeting_type: meeting_type || "video",
notes,
status: "scheduled",
})
.select()
.single();
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ interview: data }), {
status: 201,
headers: { "Content-Type": "application/json" },
});
}
// PATCH - Update interview (feedback, reschedule)
if (req.method === "PATCH") {
const body = await req.json();
const { id, candidate_feedback, status, scheduled_at } = body;
if (!id) {
return new Response(JSON.stringify({ error: "Interview id is required" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const updateData: Record<string, any> = {};
if (candidate_feedback !== undefined)
updateData.candidate_feedback = candidate_feedback;
if (status !== undefined) updateData.status = status;
if (scheduled_at !== undefined) {
updateData.scheduled_at = scheduled_at;
updateData.status = "rescheduled";
}
const { data, error } = await supabase
.from("candidate_interviews")
.update(updateData)
.eq("id", id)
.eq("candidate_id", userId)
.select()
.single();
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ interview: data }), {
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
headers: { "Content-Type": "application/json" },
});
} catch (err: any) {
console.error("Candidate interviews API error:", err);
return new Response(JSON.stringify({ error: err.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
};

View file

@ -1,136 +0,0 @@
import { supabase } from "../_supabase.js";
export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const userId = userData.user.id;
try {
// GET - Fetch offers
if (req.method === "GET") {
const { data: offers, error } = await supabase
.from("candidate_offers")
.select(
`
*,
employer:profiles!candidate_offers_employer_id_fkey(
full_name,
avatar_url,
email
)
`,
)
.eq("candidate_id", userId)
.order("created_at", { ascending: false });
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
// Group by status
const grouped = {
pending: offers?.filter((o) => o.status === "pending") || [],
accepted: offers?.filter((o) => o.status === "accepted") || [],
declined: offers?.filter((o) => o.status === "declined") || [],
expired: offers?.filter((o) => o.status === "expired") || [],
withdrawn: offers?.filter((o) => o.status === "withdrawn") || [],
};
return new Response(
JSON.stringify({
offers: offers || [],
grouped,
total: offers?.length || 0,
}),
{
headers: { "Content-Type": "application/json" },
},
);
}
// PATCH - Respond to offer (accept/decline)
if (req.method === "PATCH") {
const body = await req.json();
const { id, status, notes } = body;
if (!id) {
return new Response(JSON.stringify({ error: "Offer id is required" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
if (!["accepted", "declined"].includes(status)) {
return new Response(
JSON.stringify({ error: "Status must be accepted or declined" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
const { data, error } = await supabase
.from("candidate_offers")
.update({
status,
notes,
candidate_response_at: new Date().toISOString(),
})
.eq("id", id)
.eq("candidate_id", userId)
.eq("status", "pending") // Can only respond to pending offers
.select()
.single();
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
if (!data) {
return new Response(
JSON.stringify({ error: "Offer not found or already responded" }),
{
status: 404,
headers: { "Content-Type": "application/json" },
},
);
}
return new Response(JSON.stringify({ offer: data }), {
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
headers: { "Content-Type": "application/json" },
});
} catch (err: any) {
console.error("Candidate offers API error:", err);
return new Response(JSON.stringify({ error: err.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
};

View file

@ -1,191 +0,0 @@
import { supabase } from "../_supabase.js";
interface ProfileData {
headline?: string;
bio?: string;
resume_url?: string;
portfolio_urls?: string[];
work_history?: WorkHistory[];
education?: Education[];
skills?: string[];
availability?: string;
desired_rate?: number;
rate_type?: string;
location?: string;
remote_preference?: string;
is_public?: boolean;
}
interface WorkHistory {
company: string;
position: string;
start_date: string;
end_date?: string;
current: boolean;
description?: string;
}
interface Education {
institution: string;
degree: string;
field: string;
start_year: number;
end_year?: number;
current: boolean;
}
export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const userId = userData.user.id;
try {
// GET - Fetch candidate profile
if (req.method === "GET") {
const { data: profile, error } = await supabase
.from("candidate_profiles")
.select("*")
.eq("user_id", userId)
.single();
if (error && error.code !== "PGRST116") {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
// Get user info for basic profile
const { data: userProfile } = await supabase
.from("profiles")
.select("full_name, avatar_url, email")
.eq("id", userId)
.single();
// Get application stats
const { data: applications } = await supabase
.from("aethex_applications")
.select("id, status")
.eq("applicant_id", userId);
const stats = {
total_applications: applications?.length || 0,
pending: applications?.filter((a) => a.status === "pending").length || 0,
reviewed: applications?.filter((a) => a.status === "reviewed").length || 0,
accepted: applications?.filter((a) => a.status === "accepted").length || 0,
rejected: applications?.filter((a) => a.status === "rejected").length || 0,
};
return new Response(
JSON.stringify({
profile: profile || null,
user: userProfile,
stats,
}),
{
headers: { "Content-Type": "application/json" },
},
);
}
// POST - Create or update profile
if (req.method === "POST") {
const body: ProfileData = await req.json();
// Check if profile exists
const { data: existing } = await supabase
.from("candidate_profiles")
.select("id")
.eq("user_id", userId)
.single();
if (existing) {
// Update existing profile
const { data, error } = await supabase
.from("candidate_profiles")
.update({
...body,
portfolio_urls: body.portfolio_urls
? JSON.stringify(body.portfolio_urls)
: undefined,
work_history: body.work_history
? JSON.stringify(body.work_history)
: undefined,
education: body.education
? JSON.stringify(body.education)
: undefined,
})
.eq("user_id", userId)
.select()
.single();
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ profile: data }), {
headers: { "Content-Type": "application/json" },
});
} else {
// Create new profile
const { data, error } = await supabase
.from("candidate_profiles")
.insert({
user_id: userId,
...body,
portfolio_urls: body.portfolio_urls
? JSON.stringify(body.portfolio_urls)
: "[]",
work_history: body.work_history
? JSON.stringify(body.work_history)
: "[]",
education: body.education
? JSON.stringify(body.education)
: "[]",
})
.select()
.single();
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ profile: data }), {
status: 201,
headers: { "Content-Type": "application/json" },
});
}
}
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
headers: { "Content-Type": "application/json" },
});
} catch (err: any) {
console.error("Candidate profile API error:", err);
return new Response(JSON.stringify({ error: err.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
};

View file

@ -38,6 +38,9 @@ export default async function handler(req: any, res: any) {
client_secret: clientSecret, client_secret: clientSecret,
grant_type: "authorization_code", grant_type: "authorization_code",
code, code,
redirect_uri:
process.env.DISCORD_ACTIVITY_REDIRECT_URI ||
"https://aethex.dev/activity",
}).toString(), }).toString(),
}, },
); );

View file

@ -13,11 +13,9 @@ export default async function handler(req: any, res: any) {
if (method === "GET") { if (method === "GET") {
const { user_id, project_id, role, limit = 50, offset = 0 } = query; const { user_id, project_id, role, limit = 50, offset = 0 } = query;
// Fix: Use correct join syntax for Supabase/Postgres foreign table
let dbQuery = supabase.from("gameforge_team_members").select( let dbQuery = supabase.from("gameforge_team_members").select(
` `*,user_profiles:users(id, full_name, avatar_url, email)`,
*,
user_profiles(id, full_name, avatar_url, email)
`,
{ count: "exact" }, { count: "exact" },
); );
@ -30,7 +28,10 @@ export default async function handler(req: any, res: any) {
.order("joined_date", { ascending: false }) .order("joined_date", { ascending: false })
.range(Number(offset), Number(offset) + Number(limit) - 1); .range(Number(offset), Number(offset) + Number(limit) - 1);
if (error) throw error; if (error) {
console.error("[GameForge Team SQL]", error);
throw error;
}
return res.json({ return res.json({
data: user_id ? data : data, data: user_id ? data : data,
total: count, total: count,

View file

@ -1,62 +0,0 @@
import { supabase } from "../_supabase.js";
export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const userId = userData.user.id;
try {
if (req.method === "GET") {
const { data: announcements, error } = await supabase
.from("staff_announcements")
.select(`*, author:profiles!staff_announcements_author_id_fkey(full_name, avatar_url)`)
.or(`expires_at.is.null,expires_at.gt.${new Date().toISOString()}`)
.order("is_pinned", { ascending: false })
.order("published_at", { ascending: false });
if (error) throw error;
// Mark read status
const withReadStatus = announcements?.map(a => ({
...a,
is_read: a.read_by?.includes(userId) || false
}));
return new Response(JSON.stringify({ announcements: withReadStatus || [] }), { headers: { "Content-Type": "application/json" } });
}
if (req.method === "POST") {
const body = await req.json();
// Mark as read
if (body.action === "mark_read" && body.id) {
const { data: current } = await supabase
.from("staff_announcements")
.select("read_by")
.eq("id", body.id)
.single();
const readBy = current?.read_by || [];
if (!readBy.includes(userId)) {
await supabase
.from("staff_announcements")
.update({ read_by: [...readBy, userId] })
.eq("id", body.id);
}
return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } });
}
}
return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
} catch (err: any) {
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { "Content-Type": "application/json" } });
}
};

View file

@ -1,100 +0,0 @@
import { supabase } from "../_supabase.js";
export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const userId = userData.user.id;
try {
if (req.method === "GET") {
// Get all courses
const { data: courses, error: coursesError } = await supabase
.from("staff_courses")
.select("*")
.order("title");
if (coursesError) throw coursesError;
// Get user's progress
const { data: progress, error: progressError } = await supabase
.from("staff_course_progress")
.select("*")
.eq("user_id", userId);
if (progressError) throw progressError;
// Merge progress with courses
const coursesWithProgress = courses?.map(course => {
const userProgress = progress?.find(p => p.course_id === course.id);
return {
...course,
progress: userProgress?.progress_percent || 0,
status: userProgress?.status || "available",
started_at: userProgress?.started_at,
completed_at: userProgress?.completed_at
};
});
const stats = {
total: courses?.length || 0,
completed: coursesWithProgress?.filter(c => c.status === "completed").length || 0,
in_progress: coursesWithProgress?.filter(c => c.status === "in_progress").length || 0,
required: courses?.filter(c => c.is_required).length || 0
};
return new Response(JSON.stringify({ courses: coursesWithProgress || [], stats }), { headers: { "Content-Type": "application/json" } });
}
if (req.method === "POST") {
const body = await req.json();
const { course_id, action, progress } = body;
if (action === "start") {
const { data, error } = await supabase
.from("staff_course_progress")
.upsert({
user_id: userId,
course_id,
status: "in_progress",
progress_percent: 0,
started_at: new Date().toISOString()
}, { onConflict: "user_id,course_id" })
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ progress: data }), { headers: { "Content-Type": "application/json" } });
}
if (action === "update_progress") {
const isComplete = progress >= 100;
const { data, error } = await supabase
.from("staff_course_progress")
.upsert({
user_id: userId,
course_id,
progress_percent: Math.min(progress, 100),
status: isComplete ? "completed" : "in_progress",
completed_at: isComplete ? new Date().toISOString() : null
}, { onConflict: "user_id,course_id" })
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ progress: data }), { headers: { "Content-Type": "application/json" } });
}
}
return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
} catch (err: any) {
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { "Content-Type": "application/json" } });
}
};

View file

@ -16,10 +16,15 @@ export default async (req: Request) => {
return new Response("Unauthorized", { status: 401 }); return new Response("Unauthorized", { status: 401 });
} }
// Pagination support
const url = new URL(req.url);
const limit = Math.max(1, Math.min(100, parseInt(url.searchParams.get("limit") || "50")));
const offset = Math.max(0, parseInt(url.searchParams.get("offset") || "0"));
const start = Date.now();
const { data: directory, error } = await supabase const { data: directory, error } = await supabase
.from("staff_members") .from("staff_members")
.select( .select(`
`
id, id,
user_id, user_id,
full_name, full_name,
@ -32,9 +37,11 @@ export default async (req: Request) => {
location, location,
username, username,
created_at created_at
`, `)
) .order("full_name", { ascending: true })
.order("full_name", { ascending: true }); .range(offset, offset + limit - 1);
const elapsed = Date.now() - start;
console.log(`[staff/directory] Query took ${elapsed}ms (limit=${limit}, offset=${offset})`);
if (error) { if (error) {
console.error("Directory fetch error:", error); console.error("Directory fetch error:", error);

View file

@ -1,96 +0,0 @@
import { supabase } from "../_supabase.js";
export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const userId = userData.user.id;
try {
if (req.method === "GET") {
const { data: expenses, error } = await supabase
.from("staff_expense_reports")
.select("*")
.eq("user_id", userId)
.order("created_at", { ascending: false });
if (error) throw error;
const stats = {
total: expenses?.length || 0,
pending: expenses?.filter(e => e.status === "pending").length || 0,
approved: expenses?.filter(e => e.status === "approved").length || 0,
reimbursed: expenses?.filter(e => e.status === "reimbursed").length || 0,
total_amount: expenses?.reduce((sum, e) => sum + parseFloat(e.amount), 0) || 0,
pending_amount: expenses?.filter(e => e.status === "pending").reduce((sum, e) => sum + parseFloat(e.amount), 0) || 0
};
return new Response(JSON.stringify({ expenses: expenses || [], stats }), { headers: { "Content-Type": "application/json" } });
}
if (req.method === "POST") {
const body = await req.json();
const { title, description, amount, category, receipt_url } = body;
const { data, error } = await supabase
.from("staff_expense_reports")
.insert({
user_id: userId,
title,
description,
amount,
category,
receipt_url,
status: "pending",
submitted_at: new Date().toISOString()
})
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ expense: data }), { status: 201, headers: { "Content-Type": "application/json" } });
}
if (req.method === "PATCH") {
const body = await req.json();
const { id, ...updates } = body;
const { data, error } = await supabase
.from("staff_expense_reports")
.update(updates)
.eq("id", id)
.eq("user_id", userId)
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ expense: data }), { headers: { "Content-Type": "application/json" } });
}
if (req.method === "DELETE") {
const url = new URL(req.url);
const id = url.searchParams.get("id");
const { error } = await supabase
.from("staff_expense_reports")
.delete()
.eq("id", id)
.eq("user_id", userId)
.in("status", ["draft", "pending"]);
if (error) throw error;
return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } });
}
return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
} catch (err: any) {
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { "Content-Type": "application/json" } });
}
};

View file

@ -1,46 +0,0 @@
import { supabase } from "../_supabase.js";
export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
try {
if (req.method === "GET") {
const { data: sections, error } = await supabase
.from("staff_handbook_sections")
.select("*")
.order("category")
.order("order_index");
if (error) throw error;
// Group by category
const grouped = sections?.reduce((acc, section) => {
if (!acc[section.category]) {
acc[section.category] = [];
}
acc[section.category].push(section);
return acc;
}, {} as Record<string, typeof sections>);
const categories = Object.keys(grouped || {});
return new Response(JSON.stringify({
sections: sections || [],
grouped: grouped || {},
categories
}), { headers: { "Content-Type": "application/json" } });
}
return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
} catch (err: any) {
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { "Content-Type": "application/json" } });
}
};

View file

@ -16,10 +16,15 @@ export default async (req: Request) => {
return new Response("Unauthorized", { status: 401 }); return new Response("Unauthorized", { status: 401 });
} }
// Add a limit to prevent timeouts
const url = new URL(req.url);
const limit = Math.max(1, Math.min(100, parseInt(url.searchParams.get("limit") || "50")));
const offset = Math.max(0, parseInt(url.searchParams.get("offset") || "0"));
const start = Date.now();
const { data: invoices, error } = await supabase const { data: invoices, error } = await supabase
.from("contractor_invoices") .from("contractor_invoices")
.select( .select(`
`
id, id,
user_id, user_id,
invoice_number, invoice_number,
@ -29,10 +34,12 @@ export default async (req: Request) => {
due_date, due_date,
description, description,
created_at created_at
`, `)
)
.eq("user_id", userData.user.id) .eq("user_id", userData.user.id)
.order("date", { ascending: false }); .order("date", { ascending: false })
.range(offset, offset + limit - 1);
const elapsed = Date.now() - start;
console.log(`[staff/invoices] Query took ${elapsed}ms (limit=${limit}, offset=${offset})`);
if (error) { if (error) {
console.error("Invoices fetch error:", error); console.error("Invoices fetch error:", error);

View file

@ -1,72 +0,0 @@
import { supabase } from "../_supabase.js";
export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const url = new URL(req.url);
try {
if (req.method === "GET") {
const category = url.searchParams.get("category");
const search = url.searchParams.get("search");
let query = supabase
.from("staff_knowledge_articles")
.select(`*, author:profiles!staff_knowledge_articles_author_id_fkey(full_name, avatar_url)`)
.eq("is_published", true)
.order("views", { ascending: false });
if (category && category !== "all") {
query = query.eq("category", category);
}
if (search) {
query = query.or(`title.ilike.%${search}%,content.ilike.%${search}%`);
}
const { data: articles, error } = await query;
if (error) throw error;
// Get unique categories
const { data: allArticles } = await supabase
.from("staff_knowledge_articles")
.select("category")
.eq("is_published", true);
const categories = [...new Set(allArticles?.map(a => a.category) || [])];
return new Response(JSON.stringify({ articles: articles || [], categories }), { headers: { "Content-Type": "application/json" } });
}
if (req.method === "POST") {
const body = await req.json();
// Increment view count
if (body.action === "view" && body.id) {
await supabase.rpc("increment_kb_views", { article_id: body.id });
return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } });
}
// Mark as helpful
if (body.action === "helpful" && body.id) {
await supabase
.from("staff_knowledge_articles")
.update({ helpful_count: supabase.rpc("increment") })
.eq("id", body.id);
return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } });
}
}
return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
} catch (err: any) {
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { "Content-Type": "application/json" } });
}
};

View file

@ -1,126 +0,0 @@
import { supabase } from "../_supabase.js";
export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const userId = userData.user.id;
try {
if (req.method === "GET") {
// Get items
const { data: items, error: itemsError } = await supabase
.from("staff_marketplace_items")
.select("*")
.eq("is_available", true)
.order("points_cost");
if (itemsError) throw itemsError;
// Get user's points
let { data: points } = await supabase
.from("staff_points")
.select("*")
.eq("user_id", userId)
.single();
// Create points record if doesn't exist
if (!points) {
const { data: newPoints } = await supabase
.from("staff_points")
.insert({ user_id: userId, balance: 1000, lifetime_earned: 1000 })
.select()
.single();
points = newPoints;
}
// Get user's orders
const { data: orders } = await supabase
.from("staff_marketplace_orders")
.select(`*, item:staff_marketplace_items(name, image_url)`)
.eq("user_id", userId)
.order("created_at", { ascending: false });
return new Response(JSON.stringify({
items: items || [],
points: points || { balance: 0, lifetime_earned: 0 },
orders: orders || []
}), { headers: { "Content-Type": "application/json" } });
}
if (req.method === "POST") {
const body = await req.json();
const { item_id, quantity, shipping_address } = body;
// Get item
const { data: item } = await supabase
.from("staff_marketplace_items")
.select("*")
.eq("id", item_id)
.single();
if (!item) {
return new Response(JSON.stringify({ error: "Item not found" }), { status: 404, headers: { "Content-Type": "application/json" } });
}
// Check stock
if (item.stock_count !== null && item.stock_count < (quantity || 1)) {
return new Response(JSON.stringify({ error: "Insufficient stock" }), { status: 400, headers: { "Content-Type": "application/json" } });
}
// Check points
const { data: points } = await supabase
.from("staff_points")
.select("balance")
.eq("user_id", userId)
.single();
const totalCost = item.points_cost * (quantity || 1);
if (!points || points.balance < totalCost) {
return new Response(JSON.stringify({ error: "Insufficient points" }), { status: 400, headers: { "Content-Type": "application/json" } });
}
// Create order
const { data: order, error: orderError } = await supabase
.from("staff_marketplace_orders")
.insert({
user_id: userId,
item_id,
quantity: quantity || 1,
shipping_address,
status: "pending"
})
.select()
.single();
if (orderError) throw orderError;
// Deduct points
await supabase
.from("staff_points")
.update({ balance: points.balance - totalCost })
.eq("user_id", userId);
// Update stock if applicable
if (item.stock_count !== null) {
await supabase
.from("staff_marketplace_items")
.update({ stock_count: item.stock_count - (quantity || 1) })
.eq("id", item_id);
}
return new Response(JSON.stringify({ order }), { status: 201, headers: { "Content-Type": "application/json" } });
}
return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
} catch (err: any) {
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { "Content-Type": "application/json" } });
}
};

View file

@ -16,10 +16,10 @@ export default async (req: Request) => {
return new Response("Unauthorized", { status: 401 }); return new Response("Unauthorized", { status: 401 });
} }
const start = Date.now();
const { data: staffMember, error } = await supabase const { data: staffMember, error } = await supabase
.from("staff_members") .from("staff_members")
.select( .select(`
`
id, id,
user_id, user_id,
full_name, full_name,
@ -31,10 +31,11 @@ export default async (req: Request) => {
salary, salary,
avatar_url, avatar_url,
created_at created_at
`, `)
)
.eq("user_id", userData.user.id) .eq("user_id", userData.user.id)
.single(); .single();
const elapsed = Date.now() - start;
console.log(`[staff/me] Query took ${elapsed}ms`);
if (error && error.code !== "PGRST116") { if (error && error.code !== "PGRST116") {
console.error("Staff member fetch error:", error); console.error("Staff member fetch error:", error);

View file

@ -1,208 +1,64 @@
import { supabase } from "../_supabase.js"; import { supabase } from "../_supabase.js";
export default async (req: Request) => { export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", ""); if (req.method !== "GET") {
if (!token) { return new Response("Method not allowed", { status: 405 });
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
} }
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const userId = userData.user.id;
const url = new URL(req.url);
try { try {
// GET - Fetch OKRs with key results const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (req.method === "GET") { if (!token) {
const quarter = url.searchParams.get("quarter"); return new Response("Unauthorized", { status: 401 });
const year = url.searchParams.get("year"); }
const status = url.searchParams.get("status");
let query = supabase const { data: userData } = await supabase.auth.getUser(token);
.from("staff_okrs") if (!userData.user) {
.select(` return new Response("Unauthorized", { status: 401 });
*, }
key_results:staff_key_results(*)
`)
.or(`user_id.eq.${userId},owner_type.in.(team,company)`)
.order("created_at", { ascending: false });
if (quarter) query = query.eq("quarter", parseInt(quarter)); // Add a limit to prevent timeouts
if (year) query = query.eq("year", parseInt(year)); const url = new URL(req.url);
if (status) query = query.eq("status", status); const limit = Math.max(1, Math.min(100, parseInt(url.searchParams.get("limit") || "50")));
const offset = Math.max(0, parseInt(url.searchParams.get("offset") || "0"));
const { data: okrs, error } = await query; const start = Date.now();
if (error) throw error; const { data: okrs, error } = await supabase
.from("staff_okrs")
.select(`
id,
user_id,
objective,
description,
status,
quarter,
year,
key_results(
id,
title,
progress,
target_value
),
created_at
`)
.eq("user_id", userData.user.id)
.order("created_at", { ascending: false })
.range(offset, offset + limit - 1);
const elapsed = Date.now() - start;
console.log(`[staff/okrs] Query took ${elapsed}ms (limit=${limit}, offset=${offset})`);
// Calculate stats if (error) {
const myOkrs = okrs?.filter(o => o.user_id === userId) || []; console.error("OKRs fetch error:", error);
const stats = { return new Response(JSON.stringify({ error: error.message }), {
total: myOkrs.length, status: 500,
active: myOkrs.filter(o => o.status === "active").length,
completed: myOkrs.filter(o => o.status === "completed").length,
avgProgress: myOkrs.length > 0
? Math.round(myOkrs.reduce((sum, o) => sum + (o.progress || 0), 0) / myOkrs.length)
: 0
};
return new Response(JSON.stringify({ okrs: okrs || [], stats }), {
headers: { "Content-Type": "application/json" },
}); });
} }
// POST - Create OKR or Key Result return new Response(JSON.stringify(okrs || []), {
if (req.method === "POST") { headers: { "Content-Type": "application/json" },
const body = await req.json(); });
// Create new OKR
if (body.action === "create_okr") {
const { objective, description, quarter, year, team, owner_type } = body;
const { data: okr, error } = await supabase
.from("staff_okrs")
.insert({
user_id: userId,
objective,
description,
quarter,
year,
team,
owner_type: owner_type || "individual",
status: "draft"
})
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ okr }), { status: 201, headers: { "Content-Type": "application/json" } });
}
// Add key result to OKR
if (body.action === "add_key_result") {
const { okr_id, title, description, target_value, metric_type, unit, due_date } = body;
const { data: keyResult, error } = await supabase
.from("staff_key_results")
.insert({
okr_id,
title,
description,
target_value,
metric_type: metric_type || "percentage",
unit,
due_date
})
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ key_result: keyResult }), { status: 201, headers: { "Content-Type": "application/json" } });
}
// Update key result progress
if (body.action === "update_key_result") {
const { key_result_id, current_value, status } = body;
// Get target value to calculate progress
const { data: kr } = await supabase
.from("staff_key_results")
.select("target_value, start_value")
.eq("id", key_result_id)
.single();
const progress = kr ? Math.min(100, Math.round(((current_value - (kr.start_value || 0)) / (kr.target_value - (kr.start_value || 0))) * 100)) : 0;
const { data: keyResult, error } = await supabase
.from("staff_key_results")
.update({
current_value,
progress: Math.max(0, progress),
status: status || (progress >= 100 ? "completed" : progress >= 70 ? "on_track" : progress >= 40 ? "at_risk" : "behind"),
updated_at: new Date().toISOString()
})
.eq("id", key_result_id)
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ key_result: keyResult }), { headers: { "Content-Type": "application/json" } });
}
// Add check-in
if (body.action === "add_checkin") {
const { okr_id, notes, progress_snapshot } = body;
const { data: checkin, error } = await supabase
.from("staff_okr_checkins")
.insert({
okr_id,
user_id: userId,
notes,
progress_snapshot
})
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ checkin }), { status: 201, headers: { "Content-Type": "application/json" } });
}
return new Response(JSON.stringify({ error: "Invalid action" }), { status: 400, headers: { "Content-Type": "application/json" } });
}
// PUT - Update OKR
if (req.method === "PUT") {
const body = await req.json();
const { id, objective, description, status, quarter, year } = body;
const { data: okr, error } = await supabase
.from("staff_okrs")
.update({
objective,
description,
status,
quarter,
year,
updated_at: new Date().toISOString()
})
.eq("id", id)
.eq("user_id", userId)
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ okr }), { headers: { "Content-Type": "application/json" } });
}
// DELETE - Delete OKR or Key Result
if (req.method === "DELETE") {
const id = url.searchParams.get("id");
const type = url.searchParams.get("type") || "okr";
if (type === "key_result") {
const { error } = await supabase
.from("staff_key_results")
.delete()
.eq("id", id);
if (error) throw error;
} else {
const { error } = await supabase
.from("staff_okrs")
.delete()
.eq("id", id)
.eq("user_id", userId);
if (error) throw error;
}
return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } });
}
return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
} catch (err: any) { } catch (err: any) {
console.error("OKR API error:", err); return new Response(JSON.stringify({ error: err.message }), {
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { "Content-Type": "application/json" } }); status: 500,
});
} }
}; };

View file

@ -1,289 +0,0 @@
import { supabase } from "../_supabase.js";
interface ChecklistItem {
id: string;
checklist_item: string;
phase: string;
completed: boolean;
completed_at: string | null;
notes: string | null;
}
interface OnboardingMetadata {
start_date: string;
manager_id: string | null;
department: string | null;
role_title: string | null;
onboarding_completed: boolean;
}
// Default checklist items for new staff
const DEFAULT_CHECKLIST_ITEMS = [
// Day 1
{ item: "Complete HR paperwork", phase: "day1" },
{ item: "Set up workstation", phase: "day1" },
{ item: "Join Discord server", phase: "day1" },
{ item: "Meet your manager", phase: "day1" },
{ item: "Review company handbook", phase: "day1" },
{ item: "Set up email and accounts", phase: "day1" },
// Week 1
{ item: "Complete security training", phase: "week1" },
{ item: "Set up development environment", phase: "week1" },
{ item: "Review codebase architecture", phase: "week1" },
{ item: "Attend team standup", phase: "week1" },
{ item: "Complete first small task", phase: "week1" },
{ item: "Meet team members", phase: "week1" },
// Month 1
{ item: "Complete onboarding course", phase: "month1" },
{ item: "Contribute to first sprint", phase: "month1" },
{ item: "30-day check-in with manager", phase: "month1" },
{ item: "Set Q1 OKRs", phase: "month1" },
{ item: "Shadow a senior team member", phase: "month1" },
];
export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const userId = userData.user.id;
const url = new URL(req.url);
try {
// GET - Fetch onboarding progress
if (req.method === "GET") {
// Check for admin view (managers viewing team progress)
if (url.pathname.endsWith("/admin")) {
// Get team members for this manager
const { data: teamMembers, error: teamError } = await supabase
.from("staff_members")
.select("user_id, full_name, email, avatar_url, start_date")
.eq("manager_id", userId);
if (teamError) {
return new Response(JSON.stringify({ error: teamError.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
if (!teamMembers || teamMembers.length === 0) {
return new Response(JSON.stringify({ team: [] }), {
headers: { "Content-Type": "application/json" },
});
}
// Get progress for all team members
const userIds = teamMembers.map((m) => m.user_id);
const { data: progressData } = await supabase
.from("staff_onboarding_progress")
.select("*")
.in("user_id", userIds);
// Calculate completion for each team member
const teamProgress = teamMembers.map((member) => {
const memberProgress = progressData?.filter(
(p) => p.user_id === member.user_id,
);
const completed =
memberProgress?.filter((p) => p.completed).length || 0;
const total = DEFAULT_CHECKLIST_ITEMS.length;
return {
...member,
progress_completed: completed,
progress_total: total,
progress_percentage: Math.round((completed / total) * 100),
};
});
return new Response(JSON.stringify({ team: teamProgress }), {
headers: { "Content-Type": "application/json" },
});
}
// Regular user view - get own progress
const { data: progress, error: progressError } = await supabase
.from("staff_onboarding_progress")
.select("*")
.eq("user_id", userId)
.order("created_at", { ascending: true });
// Get or create metadata
let { data: metadata, error: metadataError } = await supabase
.from("staff_onboarding_metadata")
.select("*")
.eq("user_id", userId)
.single();
// If no metadata exists, create it
if (!metadata && metadataError?.code === "PGRST116") {
const { data: newMetadata } = await supabase
.from("staff_onboarding_metadata")
.insert({ user_id: userId })
.select()
.single();
metadata = newMetadata;
}
// Get staff member info for name/department
const { data: staffMember } = await supabase
.from("staff_members")
.select("full_name, department, role, avatar_url")
.eq("user_id", userId)
.single();
// Get manager info if exists
let managerInfo = null;
if (metadata?.manager_id) {
const { data: manager } = await supabase
.from("staff_members")
.select("full_name, email, avatar_url")
.eq("user_id", metadata.manager_id)
.single();
managerInfo = manager;
}
// If no progress exists, initialize with default items
let progressItems = progress || [];
if (!progress || progress.length === 0) {
const itemsToInsert = DEFAULT_CHECKLIST_ITEMS.map((item) => ({
user_id: userId,
checklist_item: item.item,
phase: item.phase,
completed: false,
}));
const { data: insertedItems } = await supabase
.from("staff_onboarding_progress")
.insert(itemsToInsert)
.select();
progressItems = insertedItems || [];
}
// Group by phase
const groupedProgress = {
day1: progressItems.filter((p) => p.phase === "day1"),
week1: progressItems.filter((p) => p.phase === "week1"),
month1: progressItems.filter((p) => p.phase === "month1"),
};
// Calculate overall progress
const completed = progressItems.filter((p) => p.completed).length;
const total = progressItems.length;
return new Response(
JSON.stringify({
progress: groupedProgress,
metadata: metadata || { start_date: new Date().toISOString() },
staff_member: staffMember,
manager: managerInfo,
summary: {
completed,
total,
percentage: total > 0 ? Math.round((completed / total) * 100) : 0,
},
}),
{
headers: { "Content-Type": "application/json" },
},
);
}
// POST - Mark item complete/incomplete
if (req.method === "POST") {
const body = await req.json();
const { checklist_item, completed, notes } = body;
if (!checklist_item) {
return new Response(
JSON.stringify({ error: "checklist_item is required" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
// Upsert the progress item
const { data, error } = await supabase
.from("staff_onboarding_progress")
.upsert(
{
user_id: userId,
checklist_item,
phase:
DEFAULT_CHECKLIST_ITEMS.find((i) => i.item === checklist_item)
?.phase || "day1",
completed: completed ?? true,
completed_at: completed ? new Date().toISOString() : null,
notes: notes || null,
},
{
onConflict: "user_id,checklist_item",
},
)
.select()
.single();
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
// Check if all items are complete
const { data: allProgress } = await supabase
.from("staff_onboarding_progress")
.select("completed")
.eq("user_id", userId);
const allCompleted = allProgress?.every((p) => p.completed);
// Update metadata if all completed
if (allCompleted) {
await supabase
.from("staff_onboarding_metadata")
.update({
onboarding_completed: true,
onboarding_completed_at: new Date().toISOString(),
})
.eq("user_id", userId);
}
return new Response(
JSON.stringify({
item: data,
all_completed: allCompleted,
}),
{
headers: { "Content-Type": "application/json" },
},
);
}
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
headers: { "Content-Type": "application/json" },
});
} catch (err: any) {
console.error("Onboarding API error:", err);
return new Response(JSON.stringify({ error: err.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
};

View file

@ -1,102 +0,0 @@
import { supabase } from "../_supabase.js";
export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const userId = userData.user.id;
try {
if (req.method === "GET") {
// Get projects where user is lead or team member
const { data: projects, error } = await supabase
.from("staff_projects")
.select(`
*,
lead:profiles!staff_projects_lead_id_fkey(full_name, avatar_url)
`)
.or(`lead_id.eq.${userId},team_members.cs.{${userId}}`)
.order("updated_at", { ascending: false });
if (error) throw error;
// Get tasks for each project
const projectIds = projects?.map(p => p.id) || [];
const { data: tasks } = await supabase
.from("staff_project_tasks")
.select("*")
.in("project_id", projectIds);
// Attach tasks to projects
const projectsWithTasks = projects?.map(project => ({
...project,
tasks: tasks?.filter(t => t.project_id === project.id) || [],
task_stats: {
total: tasks?.filter(t => t.project_id === project.id).length || 0,
done: tasks?.filter(t => t.project_id === project.id && t.status === "done").length || 0
}
}));
const stats = {
total: projects?.length || 0,
active: projects?.filter(p => p.status === "active").length || 0,
completed: projects?.filter(p => p.status === "completed").length || 0
};
return new Response(JSON.stringify({ projects: projectsWithTasks || [], stats }), { headers: { "Content-Type": "application/json" } });
}
if (req.method === "POST") {
const body = await req.json();
// Update task status
if (body.action === "update_task") {
const { task_id, status } = body;
const { data, error } = await supabase
.from("staff_project_tasks")
.update({
status,
completed_at: status === "done" ? new Date().toISOString() : null
})
.eq("id", task_id)
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ task: data }), { headers: { "Content-Type": "application/json" } });
}
// Create task
if (body.action === "create_task") {
const { project_id, title, description, due_date, priority } = body;
const { data, error } = await supabase
.from("staff_project_tasks")
.insert({
project_id,
title,
description,
due_date,
priority,
assignee_id: userId,
status: "todo"
})
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ task: data }), { status: 201, headers: { "Content-Type": "application/json" } });
}
}
return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
} catch (err: any) {
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { "Content-Type": "application/json" } });
}
};

View file

@ -1,60 +0,0 @@
import { supabase } from "../_supabase.js";
export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const userId = userData.user.id;
try {
if (req.method === "GET") {
const { data: reviews, error } = await supabase
.from("staff_performance_reviews")
.select(`
*,
reviewer:profiles!staff_performance_reviews_reviewer_id_fkey(full_name, avatar_url)
`)
.eq("employee_id", userId)
.order("created_at", { ascending: false });
if (error) throw error;
const stats = {
total: reviews?.length || 0,
pending: reviews?.filter(r => r.status === "pending").length || 0,
completed: reviews?.filter(r => r.status === "completed").length || 0,
average_rating: reviews?.filter(r => r.overall_rating).reduce((sum, r) => sum + r.overall_rating, 0) / (reviews?.filter(r => r.overall_rating).length || 1) || 0
};
return new Response(JSON.stringify({ reviews: reviews || [], stats }), { headers: { "Content-Type": "application/json" } });
}
if (req.method === "POST") {
const body = await req.json();
const { review_id, employee_comments } = body;
// Employee can only add their comments
const { data, error } = await supabase
.from("staff_performance_reviews")
.update({ employee_comments })
.eq("id", review_id)
.eq("employee_id", userId)
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ review: data }), { headers: { "Content-Type": "application/json" } });
}
return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
} catch (err: any) {
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { "Content-Type": "application/json" } });
}
};

View file

@ -1,245 +0,0 @@
import { supabase } from "../_supabase.js";
export default async (req: Request) => {
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const { data: userData } = await supabase.auth.getUser(token);
if (!userData.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
}
const userId = userData.user.id;
const url = new URL(req.url);
try {
// GET - Fetch time entries and timesheets
if (req.method === "GET") {
const startDate = url.searchParams.get("start_date");
const endDate = url.searchParams.get("end_date");
const view = url.searchParams.get("view") || "week"; // week, month, all
// Calculate default date range based on view
const now = new Date();
let defaultStart: string;
let defaultEnd: string;
if (view === "week") {
const dayOfWeek = now.getDay();
const weekStart = new Date(now);
weekStart.setDate(now.getDate() - dayOfWeek);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
defaultStart = weekStart.toISOString().split("T")[0];
defaultEnd = weekEnd.toISOString().split("T")[0];
} else if (view === "month") {
defaultStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split("T")[0];
defaultEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().split("T")[0];
} else {
defaultStart = new Date(now.getFullYear(), 0, 1).toISOString().split("T")[0];
defaultEnd = new Date(now.getFullYear(), 11, 31).toISOString().split("T")[0];
}
const rangeStart = startDate || defaultStart;
const rangeEnd = endDate || defaultEnd;
// Get time entries
const { data: entries, error: entriesError } = await supabase
.from("staff_time_entries")
.select(`
*,
project:staff_projects(id, name),
task:staff_project_tasks(id, title)
`)
.eq("user_id", userId)
.gte("date", rangeStart)
.lte("date", rangeEnd)
.order("date", { ascending: false })
.order("start_time", { ascending: false });
if (entriesError) throw entriesError;
// Get projects for dropdown
const { data: projects } = await supabase
.from("staff_projects")
.select("id, name")
.or(`lead_id.eq.${userId},team_members.cs.{${userId}}`)
.eq("status", "active");
// Calculate stats
const totalMinutes = entries?.reduce((sum, e) => sum + (e.duration_minutes || 0), 0) || 0;
const billableMinutes = entries?.filter(e => e.is_billable).reduce((sum, e) => sum + (e.duration_minutes || 0), 0) || 0;
const stats = {
totalHours: Math.round((totalMinutes / 60) * 10) / 10,
billableHours: Math.round((billableMinutes / 60) * 10) / 10,
entriesCount: entries?.length || 0,
avgHoursPerDay: entries?.length ? Math.round((totalMinutes / 60 / new Set(entries.map(e => e.date)).size) * 10) / 10 : 0
};
return new Response(JSON.stringify({
entries: entries || [],
projects: projects || [],
stats,
dateRange: { start: rangeStart, end: rangeEnd }
}), { headers: { "Content-Type": "application/json" } });
}
// POST - Create time entry or actions
if (req.method === "POST") {
const body = await req.json();
// Create time entry
if (body.action === "create_entry") {
const { project_id, task_id, description, date, start_time, end_time, duration_minutes, is_billable, notes } = body;
// Calculate duration if start/end provided
let calculatedDuration = duration_minutes;
if (start_time && end_time && !duration_minutes) {
const [sh, sm] = start_time.split(":").map(Number);
const [eh, em] = end_time.split(":").map(Number);
calculatedDuration = (eh * 60 + em) - (sh * 60 + sm);
}
const { data: entry, error } = await supabase
.from("staff_time_entries")
.insert({
user_id: userId,
project_id,
task_id,
description,
date: date || new Date().toISOString().split("T")[0],
start_time,
end_time,
duration_minutes: calculatedDuration || 0,
is_billable: is_billable !== false,
notes
})
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ entry }), { status: 201, headers: { "Content-Type": "application/json" } });
}
// Start timer (quick entry)
if (body.action === "start_timer") {
const { project_id, description } = body;
const now = new Date();
const { data: entry, error } = await supabase
.from("staff_time_entries")
.insert({
user_id: userId,
project_id,
description: description || "Time tracking",
date: now.toISOString().split("T")[0],
start_time: now.toTimeString().split(" ")[0].substring(0, 5),
duration_minutes: 0,
is_billable: true
})
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ entry }), { status: 201, headers: { "Content-Type": "application/json" } });
}
// Stop timer
if (body.action === "stop_timer") {
const { entry_id } = body;
const now = new Date();
const endTime = now.toTimeString().split(" ")[0].substring(0, 5);
// Get the entry to calculate duration
const { data: existing } = await supabase
.from("staff_time_entries")
.select("start_time")
.eq("id", entry_id)
.single();
if (existing?.start_time) {
const [sh, sm] = existing.start_time.split(":").map(Number);
const [eh, em] = endTime.split(":").map(Number);
const duration = (eh * 60 + em) - (sh * 60 + sm);
const { data: entry, error } = await supabase
.from("staff_time_entries")
.update({
end_time: endTime,
duration_minutes: Math.max(0, duration),
updated_at: new Date().toISOString()
})
.eq("id", entry_id)
.eq("user_id", userId)
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ entry }), { headers: { "Content-Type": "application/json" } });
}
}
return new Response(JSON.stringify({ error: "Invalid action" }), { status: 400, headers: { "Content-Type": "application/json" } });
}
// PUT - Update time entry
if (req.method === "PUT") {
const body = await req.json();
const { id, project_id, task_id, description, date, start_time, end_time, duration_minutes, is_billable, notes } = body;
// Calculate duration if times provided
let calculatedDuration = duration_minutes;
if (start_time && end_time) {
const [sh, sm] = start_time.split(":").map(Number);
const [eh, em] = end_time.split(":").map(Number);
calculatedDuration = (eh * 60 + em) - (sh * 60 + sm);
}
const { data: entry, error } = await supabase
.from("staff_time_entries")
.update({
project_id,
task_id,
description,
date,
start_time,
end_time,
duration_minutes: calculatedDuration,
is_billable,
notes,
updated_at: new Date().toISOString()
})
.eq("id", id)
.eq("user_id", userId)
.eq("status", "draft")
.select()
.single();
if (error) throw error;
return new Response(JSON.stringify({ entry }), { headers: { "Content-Type": "application/json" } });
}
// DELETE - Delete time entry
if (req.method === "DELETE") {
const id = url.searchParams.get("id");
const { error } = await supabase
.from("staff_time_entries")
.delete()
.eq("id", id)
.eq("user_id", userId)
.eq("status", "draft");
if (error) throw error;
return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } });
}
return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
} catch (err: any) {
console.error("Time tracking API error:", err);
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { "Content-Type": "application/json" } });
}
};

View file

@ -1,10 +1,11 @@
import "./global.css"; import "./global.css";
import React, { useEffect } from "react";
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
import { createRoot } from "react-dom/client";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route, useNavigate } from "react-router-dom"; import { BrowserRouter, Routes, Route } from "react-router-dom";
import { useDiscordActivity } from "./contexts/DiscordActivityContext"; import { useDiscordActivity } from "./contexts/DiscordActivityContext";
import { AuthProvider } from "./contexts/AuthContext"; import { AuthProvider } from "./contexts/AuthContext";
import { Web3Provider } from "./contexts/Web3Context"; import { Web3Provider } from "./contexts/Web3Context";
@ -14,6 +15,7 @@ import { MaintenanceProvider } from "./contexts/MaintenanceContext";
import MaintenanceGuard from "./components/MaintenanceGuard"; import MaintenanceGuard from "./components/MaintenanceGuard";
import PageTransition from "./components/PageTransition"; import PageTransition from "./components/PageTransition";
import SkipAgentController from "./components/SkipAgentController"; import SkipAgentController from "./components/SkipAgentController";
import Index from "./pages/Index";
import Onboarding from "./pages/Onboarding"; import Onboarding from "./pages/Onboarding";
import Dashboard from "./pages/Dashboard"; import Dashboard from "./pages/Dashboard";
import Login from "./pages/Login"; import Login from "./pages/Login";
@ -24,9 +26,14 @@ import ResearchLabs from "./pages/ResearchLabs";
import Labs from "./pages/Labs"; import Labs from "./pages/Labs";
import GameForge from "./pages/GameForge"; import GameForge from "./pages/GameForge";
import Foundation from "./pages/Foundation"; import Foundation from "./pages/Foundation";
import Corp from "./pages/Corp";
import Staff from "./pages/Staff";
import Nexus from "./pages/Nexus"; import Nexus from "./pages/Nexus";
import Arms from "./pages/Arms"; import Arms from "./pages/Arms";
import ExternalRedirect from "./components/ExternalRedirect"; import ExternalRedirect from "./components/ExternalRedirect";
import CorpScheduleConsultation from "./pages/corp/CorpScheduleConsultation";
import CorpViewCaseStudies from "./pages/corp/CorpViewCaseStudies";
import CorpContactUs from "./pages/corp/CorpContactUs";
import RequireAccess from "@/components/RequireAccess"; import RequireAccess from "@/components/RequireAccess";
import Engage from "./pages/Pricing"; import Engage from "./pages/Pricing";
import DocsLayout from "@/components/docs/DocsLayout"; import DocsLayout from "@/components/docs/DocsLayout";
@ -49,6 +56,12 @@ import GameJoltIntegration from "./pages/docs/integrations/GameJolt";
import ItchIoIntegration from "./pages/docs/integrations/ItchIo"; import ItchIoIntegration from "./pages/docs/integrations/ItchIo";
import DocsCurriculum from "./pages/docs/DocsCurriculum"; import DocsCurriculum from "./pages/docs/DocsCurriculum";
import DocsCurriculumEthos from "./pages/docs/DocsCurriculumEthos"; import DocsCurriculumEthos from "./pages/docs/DocsCurriculumEthos";
import DocsLangOverview from "./pages/docs/lang/DocsLangOverview";
import DocsLangQuickstart from "./pages/docs/lang/DocsLangQuickstart";
import DocsLangSyntax from "./pages/docs/lang/DocsLangSyntax";
import DocsLangCli from "./pages/docs/lang/DocsLangCli";
import DocsLangExamples from "./pages/docs/lang/DocsLangExamples";
import EthosGuild from "./pages/community/EthosGuild";
import TrackLibrary from "./pages/ethos/TrackLibrary"; import TrackLibrary from "./pages/ethos/TrackLibrary";
import ArtistProfile from "./pages/ethos/ArtistProfile"; import ArtistProfile from "./pages/ethos/ArtistProfile";
import ArtistSettings from "./pages/ethos/ArtistSettings"; import ArtistSettings from "./pages/ethos/ArtistSettings";
@ -64,6 +77,7 @@ import DevelopersDirectory from "./pages/DevelopersDirectory";
import ProfilePassport from "./pages/ProfilePassport"; import ProfilePassport from "./pages/ProfilePassport";
import SubdomainPassport from "./pages/SubdomainPassport"; import SubdomainPassport from "./pages/SubdomainPassport";
import Profile from "./pages/Profile"; import Profile from "./pages/Profile";
import LegacyPassportRedirect from "./pages/LegacyPassportRedirect";
import { SubdomainPassportProvider } from "./contexts/SubdomainPassportContext"; import { SubdomainPassportProvider } from "./contexts/SubdomainPassportContext";
import About from "./pages/About"; import About from "./pages/About";
import Contact from "./pages/Contact"; import Contact from "./pages/Contact";
@ -72,28 +86,32 @@ import Careers from "./pages/Careers";
import Privacy from "./pages/Privacy"; import Privacy from "./pages/Privacy";
import Terms from "./pages/Terms"; import Terms from "./pages/Terms";
import Admin from "./pages/Admin"; import Admin from "./pages/Admin";
import AdminModeration from "./pages/admin/AdminModeration"; import Feed from "./pages/Feed";
import AdminAnalytics from "./pages/admin/AdminAnalytics";
import AdminFeed from "./pages/AdminFeed"; import AdminFeed from "./pages/AdminFeed";
import ProjectsNew from "./pages/ProjectsNew"; import ProjectsNew from "./pages/ProjectsNew";
import Opportunities from "./pages/Opportunities";
import Explore from "./pages/Explore"; import Explore from "./pages/Explore";
import ResetPassword from "./pages/ResetPassword"; import ResetPassword from "./pages/ResetPassword";
import Teams from "./pages/Teams"; import Teams from "./pages/Teams";
import Squads from "./pages/Squads"; import Squads from "./pages/Squads";
import MenteeHub from "./pages/MenteeHub"; import MenteeHub from "./pages/MenteeHub";
import ProjectBoard from "./pages/ProjectBoard"; import ProjectBoard from "./pages/ProjectBoard";
import ProjectDetail from "./pages/ProjectDetail";
import { Navigate } from "react-router-dom"; import { Navigate } from "react-router-dom";
import FourOhFourPage from "./pages/404"; import FourOhFourPage from "./pages/404";
import SignupRedirect from "./pages/SignupRedirect"; import SignupRedirect from "./pages/SignupRedirect";
import MentorshipRequest from "./pages/community/MentorshipRequest";
import MentorApply from "./pages/community/MentorApply";
import MentorProfile from "./pages/community/MentorProfile";
import Realms from "./pages/Realms"; import Realms from "./pages/Realms";
import Investors from "./pages/Investors"; import Investors from "./pages/Investors";
import NexusDashboard from "./pages/dashboards/NexusDashboard"; import NexusDashboard from "./pages/dashboards/NexusDashboard";
import LabsDashboard from "./pages/dashboards/LabsDashboard";
import GameForgeDashboard from "./pages/dashboards/GameForgeDashboard"; import GameForgeDashboard from "./pages/dashboards/GameForgeDashboard";
import StaffDashboard from "./pages/dashboards/StaffDashboard";
import Roadmap from "./pages/Roadmap"; import Roadmap from "./pages/Roadmap";
import Trust from "./pages/Trust"; import Trust from "./pages/Trust";
import PressKit from "./pages/PressKit"; import PressKit from "./pages/PressKit";
const Downloads = React.lazy(() => import("./pages/Downloads")); import Downloads from "./pages/Downloads";
import Projects from "./pages/Projects"; import Projects from "./pages/Projects";
import ProjectsAdmin from "./pages/ProjectsAdmin"; import ProjectsAdmin from "./pages/ProjectsAdmin";
import Directory from "./pages/Directory"; import Directory from "./pages/Directory";
@ -117,7 +135,13 @@ import OpportunitiesHub from "./pages/opportunities/OpportunitiesHub";
import OpportunityDetail from "./pages/opportunities/OpportunityDetail"; import OpportunityDetail from "./pages/opportunities/OpportunityDetail";
import OpportunityPostForm from "./pages/opportunities/OpportunityPostForm"; import OpportunityPostForm from "./pages/opportunities/OpportunityPostForm";
import MyApplications from "./pages/profile/MyApplications"; import MyApplications from "./pages/profile/MyApplications";
// Hub pages moved to aethex.co (aethex-corp app) import ClientHub from "./pages/hub/ClientHub";
import ClientProjects from "./pages/hub/ClientProjects";
import ClientDashboard from "./pages/hub/ClientDashboard";
import ClientInvoices from "./pages/hub/ClientInvoices";
import ClientContracts from "./pages/hub/ClientContracts";
import ClientReports from "./pages/hub/ClientReports";
import ClientSettings from "./pages/hub/ClientSettings";
import Space1Welcome from "./pages/internal-docs/Space1Welcome"; import Space1Welcome from "./pages/internal-docs/Space1Welcome";
import Space1AxiomModel from "./pages/internal-docs/Space1AxiomModel"; import Space1AxiomModel from "./pages/internal-docs/Space1AxiomModel";
import Space1FindYourRole from "./pages/internal-docs/Space1FindYourRole"; import Space1FindYourRole from "./pages/internal-docs/Space1FindYourRole";
@ -136,19 +160,20 @@ import Space4ClientOps from "./pages/internal-docs/Space4ClientOps";
import Space4PlatformStrategy from "./pages/internal-docs/Space4PlatformStrategy"; import Space4PlatformStrategy from "./pages/internal-docs/Space4PlatformStrategy";
import Space5Onboarding from "./pages/internal-docs/Space5Onboarding"; import Space5Onboarding from "./pages/internal-docs/Space5Onboarding";
import Space5Finance from "./pages/internal-docs/Space5Finance"; import Space5Finance from "./pages/internal-docs/Space5Finance";
import Staff from "./pages/Staff";
import StaffLogin from "./pages/StaffLogin"; import StaffLogin from "./pages/StaffLogin";
import StaffDashboard from "./pages/dashboards/StaffDashboard"; import StaffDirectory from "./pages/StaffDirectory";
import StaffAdmin from "./pages/StaffAdmin"; import StaffAdmin from "./pages/StaffAdmin";
import StaffChat from "./pages/StaffChat"; import StaffChat from "./pages/StaffChat";
import StaffDocs from "./pages/StaffDocs"; import StaffDocs from "./pages/StaffDocs";
import StaffDirectory from "./pages/StaffDirectory";
import StaffAchievements from "./pages/StaffAchievements"; import StaffAchievements from "./pages/StaffAchievements";
import StaffTimeTracking from "./pages/staff/StaffTimeTracking"; import StaffAnnouncements from "./pages/staff/StaffAnnouncements";
import CandidatePortal from "./pages/candidate/CandidatePortal"; import StaffExpenseReports from "./pages/staff/StaffExpenseReports";
import CandidateInterviews from "./pages/candidate/CandidateInterviews"; import StaffInternalMarketplace from "./pages/staff/StaffInternalMarketplace";
import CandidateOffers from "./pages/candidate/CandidateOffers"; import StaffKnowledgeBase from "./pages/staff/StaffKnowledgeBase";
import CandidateProfile from "./pages/candidate/CandidateProfile"; import StaffLearningPortal from "./pages/staff/StaffLearningPortal";
import StaffPerformanceReviews from "./pages/staff/StaffPerformanceReviews";
import StaffProjectTracking from "./pages/staff/StaffProjectTracking";
import StaffTeamHandbook from "./pages/staff/StaffTeamHandbook";
import DeveloperDashboard from "./pages/dev-platform/DeveloperDashboard"; import DeveloperDashboard from "./pages/dev-platform/DeveloperDashboard";
import ApiReference from "./pages/dev-platform/ApiReference"; import ApiReference from "./pages/dev-platform/ApiReference";
import QuickStart from "./pages/dev-platform/QuickStart"; import QuickStart from "./pages/dev-platform/QuickStart";
@ -159,24 +184,10 @@ import MarketplaceItemDetail from "./pages/dev-platform/MarketplaceItemDetail";
import CodeExamples from "./pages/dev-platform/CodeExamples"; import CodeExamples from "./pages/dev-platform/CodeExamples";
import ExampleDetail from "./pages/dev-platform/ExampleDetail"; import ExampleDetail from "./pages/dev-platform/ExampleDetail";
import DeveloperPlatform from "./pages/dev-platform/DeveloperPlatform"; import DeveloperPlatform from "./pages/dev-platform/DeveloperPlatform";
import AethexLang from "./pages/dev-platform/AethexLang";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
// Detects staff.aethex.tech and navigates to /staff inside the SPA.
// Must be inside BrowserRouter so useNavigate works.
const StaffSubdomainRedirect = ({ children }: { children: React.ReactNode }) => {
const navigate = useNavigate();
useEffect(() => {
if (
window.location.hostname === "staff.aethex.tech" &&
!window.location.pathname.startsWith("/staff")
) {
navigate("/staff", { replace: true });
}
}, [navigate]);
return <>{children}</>;
};
const DiscordActivityWrapper = ({ children }: { children: React.ReactNode }) => { const DiscordActivityWrapper = ({ children }: { children: React.ReactNode }) => {
const { isActivity } = useDiscordActivity(); const { isActivity } = useDiscordActivity();
@ -197,7 +208,6 @@ const App = () => (
<Toaster /> <Toaster />
<Analytics /> <Analytics />
<BrowserRouter> <BrowserRouter>
<StaffSubdomainRedirect>
<DiscordActivityWrapper> <DiscordActivityWrapper>
<SubdomainPassportProvider> <SubdomainPassportProvider>
<ArmThemeProvider> <ArmThemeProvider>
@ -233,14 +243,20 @@ const App = () => (
path="/dashboard/dev-link" path="/dashboard/dev-link"
element={<Navigate to="/dashboard/nexus" replace />} element={<Navigate to="/dashboard/nexus" replace />}
/> />
{/* Hub routes → aethex.co */} <Route
<Route path="/hub/*" element={<ExternalRedirect to="https://aethex.co/hub" />} /> path="/hub/client"
element={
<RequireAccess>
<ClientHub />
</RequireAccess>
}
/>
<Route path="/realms" element={<Realms />} /> <Route path="/realms" element={<Realms />} />
<Route path="/investors" element={<Investors />} /> <Route path="/investors" element={<Investors />} />
<Route path="/roadmap" element={<Roadmap />} /> <Route path="/roadmap" element={<Roadmap />} />
<Route path="/trust" element={<Trust />} /> <Route path="/trust" element={<Trust />} />
<Route path="/press" element={<PressKit />} /> <Route path="/press" element={<PressKit />} />
<Route path="/downloads" element={<React.Suspense fallback={null}><Downloads /></React.Suspense>} /> <Route path="/downloads" element={<Downloads />} />
<Route path="/projects" element={<Projects />} /> <Route path="/projects" element={<Projects />} />
<Route <Route
path="/projects/admin" path="/projects/admin"
@ -250,22 +266,6 @@ const App = () => (
<Route path="/admin" element={<Admin />} /> <Route path="/admin" element={<Admin />} />
<Route path="/admin/feed" element={<AdminFeed />} /> <Route path="/admin/feed" element={<AdminFeed />} />
<Route path="/admin/docs-sync" element={<DocsSync />} /> <Route path="/admin/docs-sync" element={<DocsSync />} />
<Route
path="/admin/moderation"
element={
<RequireAccess>
<AdminModeration />
</RequireAccess>
}
/>
<Route
path="/admin/analytics"
element={
<RequireAccess>
<AdminAnalytics />
</RequireAccess>
}
/>
<Route path="/arms" element={<Arms />} /> <Route path="/arms" element={<Arms />} />
<Route path="/feed" element={<Navigate to="/community/feed" replace />} /> <Route path="/feed" element={<Navigate to="/community/feed" replace />} />
<Route path="/teams" element={<Teams />} /> <Route path="/teams" element={<Teams />} />
@ -276,10 +276,6 @@ const App = () => (
path="/projects/:projectId/board" path="/projects/:projectId/board"
element={<ProjectBoard />} element={<ProjectBoard />}
/> />
<Route
path="/projects/:projectId"
element={<ProjectDetail />}
/>
<Route path="/profile" element={<Profile />} /> <Route path="/profile" element={<Profile />} />
<Route path="/profile/me" element={<Profile />} /> <Route path="/profile/me" element={<Profile />} />
<Route <Route
@ -412,27 +408,143 @@ const App = () => (
{/* Foundation page with auto-redirect to aethex.foundation (Non-Profit Guardian - Axiom Model) */} {/* Foundation page with auto-redirect to aethex.foundation (Non-Profit Guardian - Axiom Model) */}
<Route path="/foundation" element={<Foundation />} /> <Route path="/foundation" element={<Foundation />} />
{/* Corp routes → aethex.co */} <Route path="/corp" element={<Corp />} />
<Route path="/corp" element={<ExternalRedirect to="https://aethex.co" />} /> <Route
<Route path="/corp/*" element={<ExternalRedirect to="https://aethex.co" />} /> path="/corp/schedule-consultation"
element={<CorpScheduleConsultation />}
/>
<Route
path="/corp/view-case-studies"
element={<CorpViewCaseStudies />}
/>
<Route
path="/corp/contact-us"
element={<CorpContactUs />}
/>
{/* Staff routes */} {/* Staff Arm Routes */}
<Route path="/staff" element={<Staff />} /> <Route path="/staff" element={<Staff />} />
<Route path="/staff/login" element={<StaffLogin />} /> <Route path="/staff/login" element={<StaffLogin />} />
<Route path="/staff/dashboard" element={<StaffDashboard />} />
<Route path="/staff/admin" element={<StaffAdmin />} /> {/* Staff Dashboard Routes */}
<Route path="/staff/chat" element={<StaffChat />} /> <Route
<Route path="/staff/docs" element={<StaffDocs />} /> path="/staff/dashboard"
<Route path="/staff/directory" element={<StaffDirectory />} /> element={
<Route path="/staff/achievements" element={<StaffAchievements />} /> <RequireAccess>
<Route path="/staff/time-tracking" element={<StaffTimeTracking />} /> <StaffDashboard />
{/* Unbuilt staff sub-pages fall back to dashboard */} </RequireAccess>
<Route path="/staff/*" element={<Navigate to="/staff/dashboard" replace />} /> }
{/* Candidate routes */} />
<Route path="/candidate" element={<CandidatePortal />} />
<Route path="/candidate/interviews" element={<CandidateInterviews />} /> {/* Staff Management Routes */}
<Route path="/candidate/offers" element={<CandidateOffers />} /> <Route
<Route path="/candidate/profile" element={<CandidateProfile />} /> path="/staff/directory"
element={
<RequireAccess>
<StaffDirectory />
</RequireAccess>
}
/>
<Route
path="/staff/admin"
element={
<RequireAccess>
<StaffAdmin />
</RequireAccess>
}
/>
{/* Staff Tools & Resources */}
<Route
path="/staff/chat"
element={
<RequireAccess>
<StaffChat />
</RequireAccess>
}
/>
<Route
path="/staff/docs"
element={
<RequireAccess>
<StaffDocs />
</RequireAccess>
}
/>
<Route
path="/staff/achievements"
element={
<RequireAccess>
<StaffAchievements />
</RequireAccess>
}
/>
{/* Staff Admin Pages */}
<Route
path="/staff/announcements"
element={
<RequireAccess>
<StaffAnnouncements />
</RequireAccess>
}
/>
<Route
path="/staff/expense-reports"
element={
<RequireAccess>
<StaffExpenseReports />
</RequireAccess>
}
/>
<Route
path="/staff/marketplace"
element={
<RequireAccess>
<StaffInternalMarketplace />
</RequireAccess>
}
/>
<Route
path="/staff/knowledge-base"
element={
<RequireAccess>
<StaffKnowledgeBase />
</RequireAccess>
}
/>
<Route
path="/staff/learning-portal"
element={
<RequireAccess>
<StaffLearningPortal />
</RequireAccess>
}
/>
<Route
path="/staff/performance-reviews"
element={
<RequireAccess>
<StaffPerformanceReviews />
</RequireAccess>
}
/>
<Route
path="/staff/project-tracking"
element={
<RequireAccess>
<StaffProjectTracking />
</RequireAccess>
}
/>
<Route
path="/staff/team-handbook"
element={
<RequireAccess>
<StaffTeamHandbook />
</RequireAccess>
}
/>
{/* Dev-Link routes - now redirect to Nexus Opportunities with ecosystem filter */} {/* Dev-Link routes - now redirect to Nexus Opportunities with ecosystem filter */}
<Route path="/dev-link" element={<Navigate to="/opportunities?ecosystem=roblox" replace />} /> <Route path="/dev-link" element={<Navigate to="/opportunities?ecosystem=roblox" replace />} />
@ -441,8 +553,55 @@ const App = () => (
element={<Navigate to="/opportunities?ecosystem=roblox" replace />} element={<Navigate to="/opportunities?ecosystem=roblox" replace />}
/> />
{/* Client Hub routes → aethex.co */} {/* Client Hub routes */}
<Route path="/hub/client/*" element={<ExternalRedirect to="https://aethex.co/hub" />} /> <Route
path="/hub/client/dashboard"
element={
<RequireAccess>
<ClientDashboard />
</RequireAccess>
}
/>
<Route
path="/hub/client/projects"
element={
<RequireAccess>
<ClientProjects />
</RequireAccess>
}
/>
<Route
path="/hub/client/invoices"
element={
<RequireAccess>
<ClientInvoices />
</RequireAccess>
}
/>
<Route
path="/hub/client/contracts"
element={
<RequireAccess>
<ClientContracts />
</RequireAccess>
}
/>
<Route
path="/hub/client/reports"
element={
<RequireAccess>
<ClientReports />
</RequireAccess>
}
/>
<Route
path="/hub/client/settings"
element={
<RequireAccess>
<ClientSettings />
</RequireAccess>
}
/>
{/* Nexus routes */} {/* Nexus routes */}
<Route path="/nexus" element={<Nexus />} /> <Route path="/nexus" element={<Nexus />} />
@ -462,10 +621,6 @@ const App = () => (
path="curriculum" path="curriculum"
element={<DocsCurriculum />} element={<DocsCurriculum />}
/> />
<Route
path="curriculum/ethos"
element={<DocsCurriculumEthos />}
/>
<Route <Route
path="getting-started" path="getting-started"
element={<DocsGettingStarted />} element={<DocsGettingStarted />}
@ -514,6 +669,12 @@ const App = () => (
path="integrations/itchio" path="integrations/itchio"
element={<ItchIoIntegration />} element={<ItchIoIntegration />}
/> />
{/* AeThex Language Docs */}
<Route path="lang" element={<DocsLangOverview />} />
<Route path="lang/quickstart" element={<DocsLangQuickstart />} />
<Route path="lang/syntax" element={<DocsLangSyntax />} />
<Route path="lang/cli" element={<DocsLangCli />} />
<Route path="lang/examples" element={<DocsLangExamples />} />
</Route> </Route>
<Route path="/tutorials" element={<Tutorials />} /> <Route path="/tutorials" element={<Tutorials />} />
<Route path="/community/*" element={<Community />} /> <Route path="/community/*" element={<Community />} />
@ -568,6 +729,88 @@ const App = () => (
{/* Discord Activity route */} {/* Discord Activity route */}
<Route path="/activity" element={<Activity />} /> <Route path="/activity" element={<Activity />} />
{/* Docs routes */}
<Route
path="/docs"
element={
<DocsLayout>
<DocsOverview />
</DocsLayout>
}
/>
<Route
path="/docs/getting-started"
element={
<DocsLayout>
<DocsGettingStarted />
</DocsLayout>
}
/>
<Route
path="/docs/platform"
element={
<DocsLayout>
<DocsPlatform />
</DocsLayout>
}
/>
<Route
path="/docs/api"
element={
<DocsLayout>
<DocsApiReference />
</DocsLayout>
}
/>
<Route
path="/docs/cli"
element={
<DocsLayout>
<DocsCli />
</DocsLayout>
}
/>
<Route
path="/docs/tutorials"
element={
<DocsLayout>
<DocsTutorials />
</DocsLayout>
}
/>
<Route
path="/docs/examples"
element={
<DocsLayout>
<DocsExamples />
</DocsLayout>
}
/>
<Route
path="/docs/integrations"
element={
<DocsLayout>
<DocsIntegrations />
</DocsLayout>
}
/>
<Route
path="/docs/curriculum"
element={
<DocsLayout>
<DocsCurriculum />
</DocsLayout>
}
/>
<Route
path="/docs/curriculum/ethos"
element={
<DocsLayout>
<DocsCurriculumEthos />
</DocsLayout>
}
/>
{/* Internal Docs Hub Routes */} {/* Internal Docs Hub Routes */}
<Route <Route
path="/internal-docs" path="/internal-docs"
@ -660,6 +903,7 @@ const App = () => (
<Route path="/dev-platform/marketplace/:id" element={<MarketplaceItemDetail />} /> <Route path="/dev-platform/marketplace/:id" element={<MarketplaceItemDetail />} />
<Route path="/dev-platform/examples" element={<CodeExamples />} /> <Route path="/dev-platform/examples" element={<CodeExamples />} />
<Route path="/dev-platform/examples/:id" element={<ExampleDetail />} /> <Route path="/dev-platform/examples/:id" element={<ExampleDetail />} />
<Route path="/lang" element={<AethexLang />} />
{/* Explicit 404 route for static hosting fallbacks */} {/* Explicit 404 route for static hosting fallbacks */}
<Route path="/404" element={<FourOhFourPage />} /> <Route path="/404" element={<FourOhFourPage />} />
@ -672,7 +916,6 @@ const App = () => (
</ArmThemeProvider> </ArmThemeProvider>
</SubdomainPassportProvider> </SubdomainPassportProvider>
</DiscordActivityWrapper> </DiscordActivityWrapper>
</StaffSubdomainRedirect>
</BrowserRouter> </BrowserRouter>
</TooltipProvider> </TooltipProvider>
</DiscordProvider> </DiscordProvider>

View file

@ -68,22 +68,9 @@ const ARMS: Arm[] = [
textColor: "text-purple-400", textColor: "text-purple-400",
href: "/staff", href: "/staff",
}, },
{
id: "studio",
name: "AeThex | Studio",
label: "Studio",
color: "#00ffff",
bgColor: "bg-cyan-500/20",
textColor: "text-cyan-400",
href: "https://aethex.studio",
external: true,
},
]; ];
const STUDIO_SVG = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><rect width="512" height="512" rx="96" fill="%23050505"/><polygon points="256,48 444,152 444,360 256,464 68,360 68,152" fill="none" stroke="%2300ffff" stroke-width="18" opacity="0.9"/><text x="256" y="320" text-anchor="middle" font-family="Orbitron,monospace" font-size="220" font-weight="700" fill="%2300ffff">&#198;</text></svg>')}`;
const LOGO_URLS: Record<string, string> = { const LOGO_URLS: Record<string, string> = {
studio: STUDIO_SVG,
staff: staff:
"https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fc0414efd7af54ef4b821a05d469150d0?format=webp&width=800", "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fc0414efd7af54ef4b821a05d469150d0?format=webp&width=800",
labs: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fd93f7113d34347469e74421c3a3412e5?format=webp&width=800", labs: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fd93f7113d34347469e74421c3a3412e5?format=webp&width=800",

View file

@ -10,7 +10,6 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { import {
Music, Music,
Toggle,
ToggleLeft, ToggleLeft,
ToggleRight, ToggleRight,
ExternalLink, ExternalLink,

File diff suppressed because it is too large Load diff

View file

@ -196,7 +196,7 @@ export function ProfileEditor({
return ( return (
<Tabs defaultValue="basic" className="w-full"> <Tabs defaultValue="basic" className="w-full">
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3 md:grid-cols-5"> <TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="basic">Basic</TabsTrigger> <TabsTrigger value="basic">Basic</TabsTrigger>
<TabsTrigger value="social">Social</TabsTrigger> <TabsTrigger value="social">Social</TabsTrigger>
<TabsTrigger value="skills">Skills</TabsTrigger> <TabsTrigger value="skills">Skills</TabsTrigger>

View file

@ -67,7 +67,7 @@ const BlogEditor = ({ onPublish, initialData }: BlogEditorProps) => {
const handlePublish = async () => { const handlePublish = async () => {
if (!title.trim() || !html.trim()) { if (!title.trim() || !html.trim()) {
toast.error("Title and body are required"); toast.error({ title: "Title and body are required" });
return; return;
} }
@ -95,7 +95,7 @@ const BlogEditor = ({ onPublish, initialData }: BlogEditorProps) => {
} }
const data = await response.json(); const data = await response.json();
toast.success(`Post published: ${data.url}`); toast.success({ title: `Post published: ${data.url}` });
onPublish?.(true); onPublish?.(true);
// Reset form // Reset form
@ -108,7 +108,7 @@ const BlogEditor = ({ onPublish, initialData }: BlogEditorProps) => {
setMetaTitle(""); setMetaTitle("");
setMetaDescription(""); setMetaDescription("");
} catch (error: any) { } catch (error: any) {
toast.error(error.message || "Failed to publish post"); toast.error({ title: error.message || "Failed to publish post" });
onPublish?.(false); onPublish?.(false);
} finally { } finally {
setIsLoading(false); setIsLoading(false);

View file

@ -101,7 +101,7 @@ export default function AdminFoundationManager() {
const data = await response.json(); const data = await response.json();
setMentors(data || []); setMentors(data || []);
} catch (error) { } catch (error) {
aethexToast.error("Failed to load mentors"); aethexToast.error({ title: "Failed to load mentors" });
console.error(error); console.error(error);
} finally { } finally {
setLoadingMentors(false); setLoadingMentors(false);
@ -116,7 +116,7 @@ export default function AdminFoundationManager() {
const data = await response.json(); const data = await response.json();
setCourses(data || []); setCourses(data || []);
} catch (error) { } catch (error) {
aethexToast.error("Failed to load courses"); aethexToast.error({ title: "Failed to load courses" });
console.error(error); console.error(error);
} finally { } finally {
setLoadingCourses(false); setLoadingCourses(false);
@ -133,7 +133,7 @@ export default function AdminFoundationManager() {
const data = await response.json(); const data = await response.json();
setAchievements(data || []); setAchievements(data || []);
} catch (error) { } catch (error) {
aethexToast.error("Failed to load achievements"); aethexToast.error({ title: "Failed to load achievements" });
console.error(error); console.error(error);
} finally { } finally {
setLoadingAchievements(false); setLoadingAchievements(false);
@ -154,14 +154,14 @@ export default function AdminFoundationManager() {
); );
if (!response.ok) throw new Error("Failed to update mentor"); if (!response.ok) throw new Error("Failed to update mentor");
aethexToast.success( aethexToast.success({
`Mentor ${approvalAction === "approve" ? "approved" : "rejected"}`, title: `Mentor ${approvalAction === "approve" ? "approved" : "rejected"}`,
); });
setApprovalDialogOpen(false); setApprovalDialogOpen(false);
setSelectedMentor(null); setSelectedMentor(null);
fetchMentors(); fetchMentors();
} catch (error) { } catch (error) {
aethexToast.error("Failed to update mentor"); aethexToast.error({ title: "Failed to update mentor" });
console.error(error); console.error(error);
} }
}; };
@ -178,10 +178,10 @@ export default function AdminFoundationManager() {
); );
if (!response.ok) throw new Error("Failed to update course"); if (!response.ok) throw new Error("Failed to update course");
aethexToast.success(`Course ${publish ? "published" : "unpublished"}`); aethexToast.success({ title: `Course ${publish ? "published" : "unpublished"}` });
fetchCourses(); fetchCourses();
} catch (error) { } catch (error) {
aethexToast.error("Failed to update course"); aethexToast.error({ title: "Failed to update course" });
console.error(error); console.error(error);
} }
}; };
@ -196,10 +196,10 @@ export default function AdminFoundationManager() {
); );
if (!response.ok) throw new Error("Failed to delete course"); if (!response.ok) throw new Error("Failed to delete course");
aethexToast.success("Course deleted"); aethexToast.success({ title: "Course deleted" });
fetchCourses(); fetchCourses();
} catch (error) { } catch (error) {
aethexToast.error("Failed to delete course"); aethexToast.error({ title: "Failed to delete course" });
console.error(error); console.error(error);
} }
}; };

View file

@ -105,7 +105,7 @@ export default function AdminNexusManager() {
const data = await response.json(); const data = await response.json();
setOpportunities(data || []); setOpportunities(data || []);
} catch (error) { } catch (error) {
aethexToast.error("Failed to load opportunities"); aethexToast.error({ title: "Failed to load opportunities" });
console.error(error); console.error(error);
} finally { } finally {
setLoadingOpp(false); setLoadingOpp(false);
@ -120,7 +120,7 @@ export default function AdminNexusManager() {
const data = await response.json(); const data = await response.json();
setDisputes(data || []); setDisputes(data || []);
} catch (error) { } catch (error) {
aethexToast.error("Failed to load disputes"); aethexToast.error({ title: "Failed to load disputes" });
console.error(error); console.error(error);
} finally { } finally {
setLoadingDisputes(false); setLoadingDisputes(false);
@ -135,7 +135,7 @@ export default function AdminNexusManager() {
const data = await response.json(); const data = await response.json();
setCommissions(data || []); setCommissions(data || []);
} catch (error) { } catch (error) {
aethexToast.error("Failed to load commissions"); aethexToast.error({ title: "Failed to load commissions" });
console.error(error); console.error(error);
} finally { } finally {
setLoadingCommissions(false); setLoadingCommissions(false);
@ -157,10 +157,10 @@ export default function AdminNexusManager() {
); );
if (!response.ok) throw new Error("Failed to update opportunity"); if (!response.ok) throw new Error("Failed to update opportunity");
aethexToast.success(`Opportunity marked as ${status}`); aethexToast.success({ title: `Opportunity marked as ${status}` });
fetchOpportunities(); fetchOpportunities();
} catch (error) { } catch (error) {
aethexToast.error("Failed to update opportunity"); aethexToast.error({ title: "Failed to update opportunity" });
console.error(error); console.error(error);
} }
}; };
@ -180,12 +180,12 @@ export default function AdminNexusManager() {
); );
if (!response.ok) throw new Error("Failed to update opportunity"); if (!response.ok) throw new Error("Failed to update opportunity");
aethexToast.success( aethexToast.success({
`Opportunity ${featured ? "featured" : "unfeatured"}`, title: `Opportunity ${featured ? "featured" : "unfeatured"}`,
); });
fetchOpportunities(); fetchOpportunities();
} catch (error) { } catch (error) {
aethexToast.error("Failed to update opportunity"); aethexToast.error({ title: "Failed to update opportunity" });
console.error(error); console.error(error);
} }
}; };
@ -207,15 +207,15 @@ export default function AdminNexusManager() {
); );
if (!response.ok) throw new Error("Failed to update dispute"); if (!response.ok) throw new Error("Failed to update dispute");
aethexToast.success( aethexToast.success({
`Dispute ${disputeAction === "resolve" ? "resolved" : "escalated"}`, title: `Dispute ${disputeAction === "resolve" ? "resolved" : "escalated"}`,
); });
setDisputeDialogOpen(false); setDisputeDialogOpen(false);
setSelectedDispute(null); setSelectedDispute(null);
setDisputeResolution(""); setDisputeResolution("");
fetchDisputes(); fetchDisputes();
} catch (error) { } catch (error) {
aethexToast.error("Failed to update dispute"); aethexToast.error({ title: "Failed to update dispute" });
console.error(error); console.error(error);
} }
}; };

View file

@ -80,7 +80,7 @@ export default function AdminStaffAdmin() {
</div> </div>
<Tabs value={adminTab} onValueChange={setAdminTab} className="space-y-4"> <Tabs value={adminTab} onValueChange={setAdminTab} className="space-y-4">
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3 lg:grid-cols-6"> <TabsList className="grid w-full grid-cols-3 lg:grid-cols-6">
<TabsTrigger value="users" className="flex items-center gap-2"> <TabsTrigger value="users" className="flex items-center gap-2">
<Users className="w-4 h-4" /> <Users className="w-4 h-4" />
<span className="hidden sm:inline">Users</span> <span className="hidden sm:inline">Users</span>

View file

@ -12,20 +12,20 @@ export default function MaintenanceToggle() {
const handleToggle = async () => { const handleToggle = async () => {
if (!canBypass) { if (!canBypass) {
aethexToast.error("Only admins can toggle maintenance mode"); aethexToast.error({ title: "Only admins can toggle maintenance mode" });
return; return;
} }
setToggling(true); setToggling(true);
try { try {
await toggleMaintenanceMode(); await toggleMaintenanceMode();
aethexToast.success( aethexToast.success({
isMaintenanceMode title: isMaintenanceMode
? "Maintenance mode disabled - site is now live" ? "Maintenance mode disabled - site is now live"
: "Maintenance mode enabled - visitors will see maintenance page" : "Maintenance mode enabled - visitors will see maintenance page"
); });
} catch (error: any) { } catch (error: any) {
aethexToast.error(error?.message || "Failed to toggle maintenance mode"); aethexToast.error({ title: error?.message || "Failed to toggle maintenance mode" });
} finally { } finally {
setToggling(false); setToggling(false);
} }

View file

@ -209,7 +209,7 @@ export const AIChat: React.FC<AIChatProps> = ({
animate={{ opacity: 1, y: 0, scale: 1 }} animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }} exit={{ opacity: 0, y: 20, scale: 0.95 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }} transition={{ type: 'spring', damping: 25, stiffness: 300 }}
className="fixed bottom-4 right-4 md:bottom-6 md:right-6 w-[calc(100vw-2rem)] md:w-[450px] h-[70vh] sm:h-[600px] max-h-[80vh] bg-background border border-border rounded-2xl shadow-2xl z-50 flex flex-col overflow-hidden" className="fixed bottom-4 right-4 md:bottom-6 md:right-6 w-[calc(100vw-2rem)] md:w-[450px] h-[600px] max-h-[80vh] bg-background border border-border rounded-2xl shadow-2xl z-50 flex flex-col overflow-hidden"
> >
<div className={`flex items-center justify-between p-4 border-b border-border bg-gradient-to-r ${currentPersona.theme.gradient} bg-opacity-10`}> <div className={`flex items-center justify-between p-4 border-b border-border bg-gradient-to-r ${currentPersona.theme.gradient} bg-opacity-10`}>
<PersonaSelector <PersonaSelector

View file

@ -27,11 +27,11 @@ export function DevConnectLinkModal({
}: DevConnectLinkModalProps) { }: DevConnectLinkModalProps) {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { toast } = useAethexToast(); const toast = useAethexToast();
const handleLink = async () => { const handleLink = async () => {
if (!username.trim()) { if (!username.trim()) {
toast("Please enter your DevConnect username", "error"); toast.error({ title: "Please enter your DevConnect username" });
return; return;
} }
@ -41,17 +41,16 @@ export function DevConnectLinkModal({
devconnect_username: username.trim(), devconnect_username: username.trim(),
devconnect_profile_url: `https://devconnect.sbs/${username.trim()}`, devconnect_profile_url: `https://devconnect.sbs/${username.trim()}`,
}); });
toast("DevConnect account linked successfully!", "success"); toast.success({ title: "DevConnect account linked successfully!" });
setUsername(""); setUsername("");
onOpenChange(false); onOpenChange(false);
onSuccess?.(); onSuccess?.();
} catch (error) { } catch (error) {
toast( toast.error({
error instanceof Error title: error instanceof Error
? error.message ? error.message
: "Failed to link DevConnect account", : "Failed to link DevConnect account",
"error", });
);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }

View file

@ -21,122 +21,18 @@ import {
User, User,
Menu, Menu,
X, X,
Zap,
FlaskConical,
LayoutDashboard,
ChevronRight,
} from "lucide-react"; } from "lucide-react";
export interface DevPlatformNavProps { export interface DevPlatformNavProps {
className?: string; className?: string;
} }
interface NavEntry {
name: string;
href: string;
icon: React.ElementType;
description: string;
comingSoon?: boolean;
}
interface NavGroup {
label: string;
items: NavEntry[];
}
// ── Grouped nav structure ──────────────────────────────────────────────────────
const NAV_GROUPS: NavGroup[] = [
{
label: "Learn",
items: [
{
name: "Quick Start",
href: "/dev-platform/quick-start",
icon: Zap,
description: "Up and running in under 5 minutes",
},
{
name: "Documentation",
href: "/docs",
icon: BookOpen,
description: "Guides, concepts, and deep dives",
},
{
name: "Code Examples",
href: "/dev-platform/examples",
icon: FlaskConical,
description: "Copy-paste snippets for common patterns",
},
],
},
{
label: "Build",
items: [
{
name: "API Reference",
href: "/dev-platform/api-reference",
icon: Code2,
description: "Full endpoint docs with live samples",
},
{
name: "SDK",
href: "/sdk",
icon: Package,
description: "Client libraries for JS, Python, Go and more",
comingSoon: true,
},
{
name: "Templates",
href: "/dev-platform/templates",
icon: LayoutTemplate,
description: "Project starters and boilerplates",
},
{
name: "Marketplace",
href: "/dev-platform/marketplace",
icon: Store,
description: "Plugins, integrations, and extensions",
comingSoon: true,
},
],
},
];
// ── Shared dropdown item component ────────────────────────────────────────────
function DropdownItem({ item, onClick }: { item: NavEntry; onClick?: () => void }) {
return (
<NavigationMenuLink asChild>
<Link
to={item.href}
onClick={onClick}
className="group flex select-none gap-3 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-border/60 bg-muted/50 group-hover:border-primary/30 group-hover:bg-primary/10 transition-colors">
<item.icon className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium leading-none">{item.name}</span>
{item.comingSoon && (
<span className="rounded-full bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium text-primary leading-none">
Soon
</span>
)}
</div>
<p className="mt-1 text-xs leading-snug text-muted-foreground line-clamp-2">
{item.description}
</p>
</div>
</Link>
</NavigationMenuLink>
);
}
export function DevPlatformNav({ className }: DevPlatformNavProps) { export function DevPlatformNav({ className }: DevPlatformNavProps) {
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false); const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
const [searchOpen, setSearchOpen] = React.useState(false); const [searchOpen, setSearchOpen] = React.useState(false);
const location = useLocation(); const location = useLocation();
// Command palette keyboard shortcut
React.useEffect(() => { React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") { if ((e.metaKey || e.ctrlKey) && e.key === "k") {
@ -144,12 +40,46 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
setSearchOpen(true); setSearchOpen(true);
} }
}; };
document.addEventListener("keydown", handleKeyDown); document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown);
}, []); }, []);
const isGroupActive = (group: NavGroup) => const navLinks = [
group.items.some((item) => location.pathname.startsWith(item.href)); {
name: "Docs",
href: "/docs",
icon: BookOpen,
description: "Guides, tutorials, and API concepts",
},
{
name: "API Reference",
href: "/api-reference",
icon: Code2,
description: "Complete API documentation",
},
{
name: "SDK",
href: "/sdk",
icon: Package,
description: "Download SDKs for all platforms",
},
{
name: "Templates",
href: "/templates",
icon: LayoutTemplate,
description: "Project starters and boilerplates",
},
{
name: "Marketplace",
href: "/marketplace",
icon: Store,
description: "Plugins and extensions (coming soon)",
comingSoon: true,
},
];
const isActive = (path: string) => location.pathname.startsWith(path);
return ( return (
<nav <nav
@ -161,7 +91,7 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
<div className="container flex h-16 items-center"> <div className="container flex h-16 items-center">
{/* Logo */} {/* Logo */}
<Link <Link
to="/dev-platform" to="/"
className="mr-8 flex items-center space-x-2 transition-opacity hover:opacity-80" className="mr-8 flex items-center space-x-2 transition-opacity hover:opacity-80"
> >
<FileCode className="h-6 w-6 text-primary" /> <FileCode className="h-6 w-6 text-primary" />
@ -174,68 +104,55 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
<div className="hidden md:flex md:flex-1 md:items-center md:justify-between"> <div className="hidden md:flex md:flex-1 md:items-center md:justify-between">
<NavigationMenu> <NavigationMenu>
<NavigationMenuList> <NavigationMenuList>
{NAV_GROUPS.map((group) => ( {navLinks.map((link) => (
<NavigationMenuItem key={group.label}> <NavigationMenuItem key={link.href}>
<NavigationMenuTrigger <Link to={link.href}>
className={cn( <NavigationMenuLink
"h-10 text-sm font-medium", className={cn(
isGroupActive(group) && "text-primary" "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50",
)} isActive(link.href) &&
> "bg-accent text-accent-foreground"
{group.label} )}
</NavigationMenuTrigger> >
<NavigationMenuContent> <link.icon className="mr-2 h-4 w-4" />
<ul className="grid w-[420px] gap-1 p-3"> {link.name}
{/* Group header */} {link.comingSoon && (
<li className="px-2 pb-1"> <span className="ml-2 rounded-full bg-primary/20 px-2 py-0.5 text-xs text-primary">
<p className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/60"> Soon
{group.label} </span>
</p> )}
</li> </NavigationMenuLink>
{group.items.map((item) => ( </Link>
<li key={item.href}>
<DropdownItem item={item} />
</li>
))}
</ul>
</NavigationMenuContent>
</NavigationMenuItem> </NavigationMenuItem>
))} ))}
{/* Standalone Dashboard link */}
<NavigationMenuItem>
<NavigationMenuLink asChild>
<Link
to="/dev-platform/dashboard"
className={cn(
"group inline-flex h-10 items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:outline-none",
location.pathname.startsWith("/dev-platform/dashboard") &&
"bg-accent text-accent-foreground"
)}
>
<LayoutDashboard className="mr-2 h-4 w-4" />
Dashboard
</Link>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList> </NavigationMenuList>
</NavigationMenu> </NavigationMenu>
{/* Right side actions */} {/* Right side actions */}
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-4">
{/* Search button */}
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="relative h-9 justify-start text-sm text-muted-foreground w-48" className="relative h-9 w-full justify-start text-sm text-muted-foreground sm:w-64"
onClick={() => setSearchOpen(true)} onClick={() => setSearchOpen(true)}
> >
<Command className="mr-2 h-4 w-4 shrink-0" /> <Command className="mr-2 h-4 w-4" />
<span>Search docs...</span> <span className="hidden lg:inline-flex">Search...</span>
<kbd className="pointer-events-none absolute right-2 hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium sm:flex"> <span className="inline-flex lg:hidden">Search</span>
<kbd className="pointer-events-none absolute right-2 hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
<span className="text-xs"></span>K <span className="text-xs"></span>K
</kbd> </kbd>
</Button> </Button>
{/* Dashboard link */}
<Link to="/dashboard">
<Button variant="ghost" size="sm">
Dashboard
</Button>
</Link>
{/* User menu */}
<Link to="/profile"> <Link to="/profile">
<Button variant="ghost" size="icon" className="h-9 w-9"> <Button variant="ghost" size="icon" className="h-9 w-9">
<User className="h-4 w-4" /> <User className="h-4 w-4" />
@ -251,7 +168,11 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
size="icon" size="icon"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)} onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
> >
{mobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />} {mobileMenuOpen ? (
<X className="h-5 w-5" />
) : (
<Menu className="h-5 w-5" />
)}
</Button> </Button>
</div> </div>
</div> </div>
@ -259,50 +180,41 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
{/* Mobile Navigation */} {/* Mobile Navigation */}
{mobileMenuOpen && ( {mobileMenuOpen && (
<div className="border-t border-border/40 md:hidden"> <div className="border-t border-border/40 md:hidden">
<div className="container py-4 space-y-4"> <div className="container space-y-1 py-4">
{NAV_GROUPS.map((group) => ( {navLinks.map((link) => (
<div key={group.label}> <Link
<p className="px-3 pb-1 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/60"> key={link.href}
{group.label} to={link.href}
</p> onClick={() => setMobileMenuOpen(false)}
{group.items.map((item) => ( className={cn(
<Link "flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground",
key={item.href} isActive(link.href) && "bg-accent text-accent-foreground"
to={item.href} )}
onClick={() => setMobileMenuOpen(false)} >
className={cn( <link.icon className="mr-3 h-4 w-4" />
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground", {link.name}
location.pathname.startsWith(item.href) && "bg-accent text-accent-foreground" {link.comingSoon && (
)} <span className="ml-auto rounded-full bg-primary/20 px-2 py-0.5 text-xs text-primary">
> Soon
<item.icon className="h-4 w-4 shrink-0" /> </span>
<span className="flex-1">{item.name}</span> )}
{item.comingSoon && ( </Link>
<span className="rounded-full bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium text-primary">
Soon
</span>
)}
<ChevronRight className="h-3 w-3 text-muted-foreground/50" />
</Link>
))}
</div>
))} ))}
<div className="border-t border-border/40 pt-3"> <div className="border-t border-border/40 pt-4 mt-4">
<Link <Link
to="/dev-platform/dashboard" to="/dashboard"
onClick={() => setMobileMenuOpen(false)} onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground" className="flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground"
> >
<LayoutDashboard className="h-4 w-4" />
Dashboard Dashboard
</Link> </Link>
<Link <Link
to="/profile" to="/profile"
onClick={() => setMobileMenuOpen(false)} onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground" className="flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground"
> >
<User className="h-4 w-4" /> <User className="mr-3 h-4 w-4" />
Profile Profile
</Link> </Link>
</div> </div>
@ -310,20 +222,20 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
</div> </div>
)} )}
{/* Command Palette */} {/* Command Palette Placeholder - will be implemented separately */}
{searchOpen && ( {searchOpen && (
<div <div
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm" className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
onClick={() => setSearchOpen(false)} onClick={() => setSearchOpen(false)}
> >
<div <div className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" <div className="rounded-lg border bg-background p-8 shadow-lg">
onClick={(e) => e.stopPropagation()} <p className="text-center text-muted-foreground">
> Command palette coming soon...
<div className="rounded-xl border bg-background p-8 shadow-2xl min-w-80 text-center space-y-2"> </p>
<Command className="mx-auto h-8 w-8 text-muted-foreground" /> <p className="text-center text-sm text-muted-foreground mt-2">
<p className="text-muted-foreground font-medium">Command palette coming soon</p> Press Esc to close
<p className="text-sm text-muted-foreground/60">Press Esc or click outside to close</p> </p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,21 +1,21 @@
// Export layout components // Export layout components
export { default as DevPlatformLayout } from './layouts/DevPlatformLayout'; export { DevPlatformLayout } from './layouts/DevPlatformLayout';
export { default as ThreeColumnLayout } from './layouts/ThreeColumnLayout'; export { ThreeColumnLayout } from './layouts/ThreeColumnLayout';
// Export UI components // Export UI components
export { default as CodeBlock } from './ui/CodeBlock'; export { CodeBlock } from './ui/CodeBlock';
export { default as Callout } from './ui/Callout'; export { Callout } from './ui/Callout';
export { default as StatCard } from './ui/StatCard'; export { StatCard } from './ui/StatCard';
export { default as ApiEndpointCard } from './ui/ApiEndpointCard'; export { ApiEndpointCard } from './ui/ApiEndpointCard';
// Export feature components // Export feature components
export { default as DevPlatformNav } from './DevPlatformNav'; export { DevPlatformNav } from './DevPlatformNav';
export { default as DevPlatformFooter } from './DevPlatformFooter'; export { DevPlatformFooter } from './DevPlatformFooter';
export { default as Breadcrumbs } from './Breadcrumbs'; export { Breadcrumbs } from './Breadcrumbs';
export { default as CodeTabs } from './CodeTabs'; export { CodeTabs } from './CodeTabs';
export { default as TemplateCard } from './TemplateCard'; export { TemplateCard } from './TemplateCard';
export { default as MarketplaceCard } from './MarketplaceCard'; export { MarketplaceCard } from './MarketplaceCard';
export { default as ExampleCard } from './ExampleCard'; export { ExampleCard } from './ExampleCard';
export { default as ApiKeyCard } from './ApiKeyCard'; export { ApiKeyCard } from './ApiKeyCard';
export { default as CreateApiKeyDialog } from './CreateApiKeyDialog'; export { CreateApiKeyDialog } from './CreateApiKeyDialog';
export { default as UsageChart } from './UsageChart'; export { UsageChart } from './UsageChart';

View file

@ -26,16 +26,67 @@ interface DocNavItem {
description?: string; description?: string;
} }
const docNavigation: Omit<DocNavItem, "icon">[] = [ const docNavigation: DocNavItem[] = [
{ title: "Overview", path: "/docs", description: "Get started with AeThex" }, {
{ title: "Getting Started", path: "/docs/getting-started", description: "Quick start guide" }, title: "Overview",
{ title: "Platform", path: "/docs/platform", description: "Platform architecture & features" }, path: "/docs",
{ title: "API Reference", path: "/docs/api", description: "Complete API documentation" }, icon: <BookOpen className="h-5 w-5" />,
{ title: "CLI", path: "/docs/cli", description: "Command line tools" }, description: "Get started with AeThex",
{ title: "Tutorials", path: "/docs/tutorials", description: "Step-by-step guides" }, },
{ title: "Examples", path: "/docs/examples", description: "Code examples" }, {
{ title: "Integrations", path: "/docs/integrations", description: "Third-party integrations" }, title: "Getting Started",
{ title: "Curriculum", path: "/docs/curriculum", description: "Learning paths" }, path: "/docs/getting-started",
icon: <Zap className="h-5 w-5" />,
description: "Quick start guide",
},
{
title: "Platform",
path: "/docs/platform",
icon: <Layers className="h-5 w-5" />,
description: "Platform architecture & features",
},
{
title: "API Reference",
path: "/docs/api",
icon: <Code2 className="h-5 w-5" />,
description: "Complete API documentation",
},
{
title: "CLI",
path: "/docs/cli",
icon: <GitBranch className="h-5 w-5" />,
description: "Command line tools",
},
{
title: "Tutorials",
path: "/docs/tutorials",
icon: <BookMarked className="h-5 w-5" />,
description: "Step-by-step guides",
},
{
title: "Examples",
path: "/docs/examples",
icon: <FileText className="h-5 w-5" />,
description: "Code examples",
},
{
title: "Integrations",
path: "/docs/integrations",
icon: <Zap className="h-5 w-5" />,
description: "Third-party integrations",
},
{
title: "Curriculum",
path: "/docs/curriculum",
icon: <BookOpen className="h-5 w-5" />,
description: "Learning paths",
},
{
title: "AeThex Language",
path: "/docs/lang",
icon: <Code2 className="h-5 w-5" />,
description: "AeThex programming language",
},
]; ];
interface DocsLayoutProps { interface DocsLayoutProps {
@ -58,27 +109,15 @@ function DocsLayoutContent({
const location = useLocation(); const location = useLocation();
const { colors, toggleTheme, theme } = useDocsTheme(); const { colors, toggleTheme, theme } = useDocsTheme();
const navWithIcons: DocNavItem[] = useMemo(() => [
{ ...docNavigation[0], icon: <BookOpen className="h-5 w-5" /> },
{ ...docNavigation[1], icon: <Zap className="h-5 w-5" /> },
{ ...docNavigation[2], icon: <Layers className="h-5 w-5" /> },
{ ...docNavigation[3], icon: <Code2 className="h-5 w-5" /> },
{ ...docNavigation[4], icon: <GitBranch className="h-5 w-5" /> },
{ ...docNavigation[5], icon: <BookMarked className="h-5 w-5" /> },
{ ...docNavigation[6], icon: <FileText className="h-5 w-5" /> },
{ ...docNavigation[7], icon: <Zap className="h-5 w-5" /> },
{ ...docNavigation[8], icon: <BookOpen className="h-5 w-5" /> },
], []);
const filteredNav = useMemo(() => { const filteredNav = useMemo(() => {
if (!searchQuery) return navWithIcons; if (!searchQuery) return docNavigation;
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
return navWithIcons.filter( return docNavigation.filter(
(item) => (item) =>
item.title.toLowerCase().includes(query) || item.title.toLowerCase().includes(query) ||
item.description?.toLowerCase().includes(query), item.description?.toLowerCase().includes(query),
); );
}, [searchQuery, navWithIcons]); }, [searchQuery]);
const isCurrentPage = (path: string) => location.pathname === path; const isCurrentPage = (path: string) => location.pathname === path;

View file

@ -1,166 +0,0 @@
import { Link, useLocation } from "react-router-dom";
import { cn } from "@/lib/utils";
import { useAuth } from "@/contexts/AuthContext";
import {
Music2,
Users,
FileText,
Settings,
ChevronLeft,
Headphones,
} from "lucide-react";
interface EthosLayoutProps {
children: React.ReactNode;
}
interface NavItem {
name: string;
href: string;
icon: React.ElementType;
memberOnly?: boolean;
}
const NAV_ITEMS: NavItem[] = [
{ name: "Library", href: "/ethos/library", icon: Headphones },
{ name: "Artists", href: "/ethos/artists", icon: Users },
{ name: "Licensing", href: "/ethos/licensing", icon: FileText, memberOnly: true },
{ name: "Settings", href: "/ethos/settings", icon: Settings, memberOnly: true },
];
export default function EthosLayout({ children }: EthosLayoutProps) {
const location = useLocation();
const { user } = useAuth();
const isActive = (href: string) => location.pathname.startsWith(href);
return (
<div style={{ minHeight: "100vh", background: "#050505", color: "#e0e0e0" }}>
{/* Top bar */}
<header style={{
position: "sticky", top: 0, zIndex: 50,
background: "rgba(5,5,5,0.97)",
borderBottom: "1px solid rgba(168,85,247,0.15)",
backdropFilter: "blur(12px)",
}}>
{/* Purple accent stripe */}
<div style={{ height: 2, background: "linear-gradient(90deg, #7c3aed 0%, #a855f7 50%, #7c3aed 100%)", opacity: 0.6 }} />
<div style={{
maxWidth: 1200, margin: "0 auto",
padding: "0 24px",
display: "flex", alignItems: "center",
height: 52, gap: 0,
}}>
{/* Back to main site */}
<Link
to="/"
style={{
display: "flex", alignItems: "center", gap: 6,
color: "rgba(168,85,247,0.5)", textDecoration: "none",
fontSize: 11, fontFamily: "monospace", letterSpacing: 1,
marginRight: 24, flexShrink: 0,
transition: "color 0.2s",
}}
onMouseEnter={e => (e.currentTarget.style.color = "rgba(168,85,247,0.9)")}
onMouseLeave={e => (e.currentTarget.style.color = "rgba(168,85,247,0.5)")}
>
<ChevronLeft className="h-3.5 w-3.5" />
aethex.dev
</Link>
{/* Brand */}
<Link
to="/ethos/library"
style={{
display: "flex", alignItems: "center", gap: 8,
textDecoration: "none", marginRight: 40, flexShrink: 0,
}}
>
<div style={{
width: 28, height: 28,
background: "linear-gradient(135deg, #7c3aed, #a855f7)",
borderRadius: "50%",
display: "flex", alignItems: "center", justifyContent: "center",
}}>
<Music2 className="h-3.5 w-3.5 text-white" />
</div>
<span style={{
fontFamily: "monospace", fontWeight: 700, fontSize: 13,
letterSpacing: 3, color: "#a855f7", textTransform: "uppercase",
}}>
Ethos Guild
</span>
</Link>
{/* Nav tabs */}
<nav style={{ display: "flex", alignItems: "stretch", gap: 2, flex: 1, height: "100%" }}>
{NAV_ITEMS.filter(item => !item.memberOnly || user).map(item => (
<Link
key={item.href}
to={item.href}
style={{
display: "flex", alignItems: "center", gap: 6,
padding: "0 16px",
textDecoration: "none",
fontFamily: "monospace", fontSize: 11, letterSpacing: 1,
textTransform: "uppercase",
color: isActive(item.href) ? "#a855f7" : "rgba(255,255,255,0.4)",
borderBottom: isActive(item.href) ? "2px solid #a855f7" : "2px solid transparent",
transition: "color 0.2s, border-color 0.2s",
marginBottom: -1,
}}
onMouseEnter={e => {
if (!isActive(item.href)) e.currentTarget.style.color = "rgba(168,85,247,0.8)";
}}
onMouseLeave={e => {
if (!isActive(item.href)) e.currentTarget.style.color = "rgba(255,255,255,0.4)";
}}
>
<item.icon className="h-3.5 w-3.5" />
{item.name}
{item.memberOnly && (
<span style={{
fontSize: 8, padding: "1px 4px",
background: "rgba(168,85,247,0.15)",
color: "#a855f7", borderRadius: 2,
letterSpacing: 1,
}}>
MEMBER
</span>
)}
</Link>
))}
</nav>
{/* Sign in prompt for guests */}
{!user && (
<Link
to="/login"
style={{
fontFamily: "monospace", fontSize: 10, letterSpacing: 2,
color: "#a855f7", textDecoration: "none",
border: "1px solid rgba(168,85,247,0.4)",
padding: "5px 12px",
transition: "all 0.2s",
}}
onMouseEnter={e => {
e.currentTarget.style.background = "rgba(168,85,247,0.1)";
e.currentTarget.style.borderColor = "rgba(168,85,247,0.7)";
}}
onMouseLeave={e => {
e.currentTarget.style.background = "transparent";
e.currentTarget.style.borderColor = "rgba(168,85,247,0.4)";
}}
>
JOIN GUILD
</Link>
)}
</div>
</header>
{/* Page content */}
<main>{children}</main>
</div>
);
}

View file

@ -205,7 +205,7 @@ export default function CommentsModal({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px] flex flex-col h-[80vh] sm:h-[600px] max-h-[600px]"> <DialogContent className="sm:max-w-[500px] flex flex-col h-[600px]">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<MessageCircle className="h-5 w-5" /> <MessageCircle className="h-5 w-5" />

View file

@ -1,184 +0,0 @@
import { Link, useLocation } from "react-router-dom";
import { cn } from "@/lib/utils";
import { useAuth } from "@/contexts/AuthContext";
import {
Gamepad2,
LayoutDashboard,
FolderKanban,
Users2,
Box,
ChevronLeft,
Lock,
} from "lucide-react";
interface GameForgeLayoutProps {
children: React.ReactNode;
}
interface SidebarSection {
label: string;
items: {
name: string;
href: string;
icon: React.ElementType;
authRequired?: boolean;
}[];
}
const SIDEBAR: SidebarSection[] = [
{
label: "Overview",
items: [
{ name: "GameForge Home", href: "/gameforge", icon: Gamepad2 },
],
},
{
label: "Studio",
items: [
{ name: "Dashboard", href: "/gameforge/manage", icon: LayoutDashboard, authRequired: true },
{ name: "Projects", href: "/gameforge/manage/projects", icon: FolderKanban, authRequired: true },
{ name: "Team", href: "/gameforge/manage/team", icon: Users2, authRequired: true },
{ name: "Assets", href: "/gameforge/manage/assets", icon: Box, authRequired: true },
],
},
];
export default function GameForgeLayout({ children }: GameForgeLayoutProps) {
const location = useLocation();
const { user } = useAuth();
const isActive = (href: string) =>
href === "/gameforge"
? location.pathname === href
: location.pathname.startsWith(href);
return (
<div style={{ minHeight: "100vh", background: "#050505", color: "#e0e0e0", display: "flex", flexDirection: "column" }}>
{/* Top strip */}
<div style={{ height: 2, background: "linear-gradient(90deg, #ff6b00, #ff9500, #ff6b00)", opacity: 0.7, flexShrink: 0 }} />
<div style={{ display: "flex", flex: 1 }}>
{/* Sidebar */}
<aside style={{
width: 220, flexShrink: 0,
background: "rgba(10,10,10,0.98)",
borderRight: "1px solid rgba(255,107,0,0.12)",
position: "sticky", top: 0, height: "100vh",
display: "flex", flexDirection: "column",
padding: "20px 0",
}}>
{/* Brand */}
<div style={{ padding: "0 20px 20px", borderBottom: "1px solid rgba(255,107,0,0.1)" }}>
<Link
to="/"
style={{
display: "flex", alignItems: "center", gap: 5,
color: "rgba(255,107,0,0.45)", textDecoration: "none",
fontSize: 10, fontFamily: "monospace", letterSpacing: 1,
marginBottom: 14,
}}
>
<ChevronLeft className="h-3 w-3" />aethex.dev
</Link>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div style={{
width: 32, height: 32, background: "linear-gradient(135deg, #ff6b00, #ff9500)",
borderRadius: 4,
display: "flex", alignItems: "center", justifyContent: "center",
}}>
<Gamepad2 className="h-4 w-4 text-white" />
</div>
<div>
<div style={{ fontFamily: "monospace", fontWeight: 700, fontSize: 12, letterSpacing: 2, color: "#ff7a00" }}>
GAMEFORGE
</div>
<div style={{ fontFamily: "monospace", fontSize: 9, color: "rgba(255,107,0,0.4)", letterSpacing: 1 }}>
STUDIO MANAGEMENT
</div>
</div>
</div>
</div>
{/* Nav sections */}
<nav style={{ flex: 1, padding: "16px 0", overflowY: "auto" }}>
{SIDEBAR.map(section => (
<div key={section.label} style={{ marginBottom: 20 }}>
<div style={{
padding: "0 20px 6px",
fontSize: 9, fontFamily: "monospace", letterSpacing: 2,
textTransform: "uppercase", color: "rgba(255,107,0,0.3)",
}}>
{section.label}
</div>
{section.items.map(item => {
const locked = item.authRequired && !user;
const active = isActive(item.href);
return (
<Link
key={item.href}
to={locked ? "/login" : item.href}
style={{
display: "flex", alignItems: "center", gap: 10,
padding: "8px 20px",
textDecoration: "none",
fontFamily: "monospace", fontSize: 11, letterSpacing: 0.5,
color: locked
? "rgba(255,255,255,0.2)"
: active
? "#ff7a00"
: "rgba(255,255,255,0.5)",
background: active ? "rgba(255,107,0,0.07)" : "transparent",
borderLeft: active ? "2px solid #ff7a00" : "2px solid transparent",
transition: "all 0.15s",
}}
onMouseEnter={e => {
if (!active && !locked) {
e.currentTarget.style.color = "rgba(255,122,0,0.8)";
e.currentTarget.style.background = "rgba(255,107,0,0.04)";
}
}}
onMouseLeave={e => {
if (!active && !locked) {
e.currentTarget.style.color = "rgba(255,255,255,0.5)";
e.currentTarget.style.background = "transparent";
}
}}
>
<item.icon className="h-3.5 w-3.5 shrink-0" />
<span style={{ flex: 1 }}>{item.name}</span>
{locked && <Lock className="h-3 w-3 opacity-40" />}
</Link>
);
})}
</div>
))}
</nav>
{/* Footer hint */}
{!user && (
<div style={{ padding: "16px 20px", borderTop: "1px solid rgba(255,107,0,0.1)" }}>
<Link
to="/login"
style={{
display: "block", textAlign: "center",
fontFamily: "monospace", fontSize: 10, letterSpacing: 2,
color: "#ff7a00", textDecoration: "none",
border: "1px solid rgba(255,107,0,0.4)",
padding: "7px 0",
transition: "all 0.2s",
}}
onMouseEnter={e => (e.currentTarget.style.background = "rgba(255,107,0,0.08)")}
onMouseLeave={e => (e.currentTarget.style.background = "transparent")}
>
SIGN IN TO MANAGE
</Link>
</div>
)}
</aside>
{/* Main content */}
<main style={{ flex: 1, minWidth: 0 }}>{children}</main>
</div>
</div>
);
}

View file

@ -243,7 +243,6 @@ export default function NotificationBell({
<DropdownMenuContent <DropdownMenuContent
align="end" align="end"
className="w-80 border-border/40 bg-background/95 backdrop-blur" className="w-80 border-border/40 bg-background/95 backdrop-blur"
style={{ zIndex: 99999 }}
> >
<DropdownMenuLabel className="flex items-center justify-between"> <DropdownMenuLabel className="flex items-center justify-between">
<span className="text-sm font-semibold text-foreground"> <span className="text-sm font-semibold text-foreground">

View file

@ -47,20 +47,20 @@ export const WalletVerification = ({
const handleConnect = async () => { const handleConnect = async () => {
if (!walletInput.trim()) { if (!walletInput.trim()) {
aethexToast.warning("Please enter a wallet address"); aethexToast.warning({ title: "Please enter a wallet address" });
return; return;
} }
const normalized = walletInput.trim().toLowerCase(); const normalized = walletInput.trim().toLowerCase();
if (!isValidEthereumAddress(normalized)) { if (!isValidEthereumAddress(normalized)) {
aethexToast.warning( aethexToast.warning({
"Invalid Ethereum address. Must be 0x followed by 40 hexadecimal characters.", title: "Invalid Ethereum address. Must be 0x followed by 40 hexadecimal characters.",
); });
return; return;
} }
if (!user?.id) { if (!user?.id) {
aethexToast.error("User not authenticated"); aethexToast.error({ title: "User not authenticated" });
return; return;
} }
@ -86,16 +86,16 @@ export const WalletVerification = ({
const data = await response.json(); const data = await response.json();
setConnectedWallet(normalized); setConnectedWallet(normalized);
setWalletInput(""); setWalletInput("");
aethexToast.success("✅ Wallet connected successfully!"); aethexToast.success({ title: "✅ Wallet connected successfully!" });
if (onWalletUpdated) { if (onWalletUpdated) {
onWalletUpdated(normalized); onWalletUpdated(normalized);
} }
} catch (error: any) { } catch (error: any) {
console.error("[Wallet Verification] Error:", error?.message); console.error("[Wallet Verification] Error:", error?.message);
aethexToast.error( aethexToast.error({
error?.message || "Failed to connect wallet. Please try again.", title: error?.message || "Failed to connect wallet. Please try again.",
); });
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -123,16 +123,16 @@ export const WalletVerification = ({
} }
setConnectedWallet(null); setConnectedWallet(null);
aethexToast.success("Wallet disconnected"); aethexToast.success({ title: "Wallet disconnected" });
if (onWalletUpdated) { if (onWalletUpdated) {
onWalletUpdated(null); onWalletUpdated(null);
} }
} catch (error: any) { } catch (error: any) {
console.error("[Wallet Verification] Error:", error?.message); console.error("[Wallet Verification] Error:", error?.message);
aethexToast.error( aethexToast.error({
error?.message || "Failed to disconnect wallet. Please try again.", title: error?.message || "Failed to disconnect wallet. Please try again.",
); });
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }

View file

@ -21,7 +21,7 @@ import {
checkProfileComplete, checkProfileComplete,
} from "@/lib/aethex-database-adapter"; } from "@/lib/aethex-database-adapter";
type SupportedOAuthProvider = "github" | "google" | "discord" | string; type SupportedOAuthProvider = "github" | "google" | "discord";
interface LinkedProvider { interface LinkedProvider {
provider: SupportedOAuthProvider; provider: SupportedOAuthProvider;
@ -165,9 +165,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const rewardsActivatedRef = useRef(false); const rewardsActivatedRef = useRef(false);
const storageClearedRef = useRef(false); const storageClearedRef = useRef(false);
// True after the very first auth event resolves — distinguishes session
// restoration (page load) from a real user-initiated sign-in.
const initialEventFired = useRef(false);
useEffect(() => { useEffect(() => {
let sessionRestored = false; let sessionRestored = false;
@ -200,12 +197,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
// - IndexedDB (where Supabase stores sessions) // - IndexedDB (where Supabase stores sessions)
// Clearing these breaks session persistence across page reloads/redirects! // Clearing these breaks session persistence across page reloads/redirects!
// If the server set the SSO remember-me cookie (Authentik login), promote
// it to localStorage so the session survives across browser restarts.
if (document.cookie.includes("aethex_sso_remember=1")) {
window.localStorage.setItem("aethex_remember_me", "1");
}
storageClearedRef.current = true; storageClearedRef.current = true;
} catch { } catch {
storageClearedRef.current = true; storageClearedRef.current = true;
@ -230,21 +221,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
data: { session }, data: { session },
} = await supabase.auth.getSession(); } = await supabase.auth.getSession();
// If "remember me" was NOT checked when the user last signed in, clear
// the persisted session so closing the browser actually logs them out.
// SSO (Authentik) logins always set this flag, so this only affects
// email/password logins where the user explicitly unchecked it.
if (session?.user) {
const rememberMe = window.localStorage.getItem("aethex_remember_me");
if (rememberMe === null) {
// No flag — user didn't ask to be remembered; clear local session.
await supabase.auth.signOut({ scope: "local" });
sessionRestored = true;
setLoading(false);
return;
}
}
// If no session but tokens exist, the session might not have restored yet // If no session but tokens exist, the session might not have restored yet
// Wait for onAuthStateChange to trigger // Wait for onAuthStateChange to trigger
if (!session && hasAuthTokens()) { if (!session && hasAuthTokens()) {
@ -300,24 +276,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
}, 50); }, 50);
} }
// Only toast on real user-initiated events, not session restoration on page load. // Show toast notifications for auth events
// INITIAL_SESSION fires first on page load (Supabase v2); after that every if (event === "SIGNED_IN") {
// SIGNED_IN is a genuine login.
const isInitialRestore = !initialEventFired.current;
initialEventFired.current = true;
if (event === "SIGNED_IN" && !isInitialRestore) {
aethexToast.success({ aethexToast.success({
title: "Signed in", title: "Welcome back!",
description: "Welcome back to AeThex OS", description: "Successfully signed in to AeThex OS",
}); });
} else if (event === "SIGNED_OUT") { } else if (event === "SIGNED_OUT") {
aethexToast.info({ aethexToast.info({
title: "Signed out", title: "Signed out",
description: "Come back soon!", description: "Come back soon!",
}); });
} else if (event === "TOKEN_REFRESHED") {
// Silently refresh — no toast
} }
}); });
@ -715,7 +684,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
} }
const { data, error } = await supabase.auth.signInWithOAuth({ const { data, error } = await supabase.auth.signInWithOAuth({
provider: provider as any, provider,
options: { options: {
redirectTo: `${window.location.origin}/login`, redirectTo: `${window.location.origin}/login`,
}, },
@ -1013,16 +982,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
return; return;
} }
// Only clear session for actual Supabase auth errors — be very specific. // Only clear session for actual auth errors
// "unauthorized" and "auth/" were removed: they're too broad and match
// normal API 401s or any URL containing "auth/", which falsely logs users out.
const authErrorPatterns = [ const authErrorPatterns = [
"invalid refresh token", "invalid refresh token",
"refresh_token_not_found",
"session expired", "session expired",
"token_expired",
"revoked", "revoked",
"jwt expired", "unauthorized",
"auth/",
]; ];
if (authErrorPatterns.some((pattern) => messageStr.includes(pattern))) { if (authErrorPatterns.some((pattern) => messageStr.includes(pattern))) {
@ -1067,7 +1033,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
// Step 2: Clear localStorage and IndexedDB // Step 2: Clear localStorage and IndexedDB
console.log("Clearing localStorage and IndexedDB..."); console.log("Clearing localStorage and IndexedDB...");
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
window.localStorage.removeItem("aethex_remember_me");
try { try {
window.localStorage.removeItem("onboarding_complete"); window.localStorage.removeItem("onboarding_complete");
window.localStorage.removeItem("aethex_onboarding_progress_v1"); window.localStorage.removeItem("aethex_onboarding_progress_v1");

View file

@ -284,17 +284,17 @@ export const DiscordActivityProvider: React.FC<
// Subscribe to speaking updates if in voice channel // Subscribe to speaking updates if in voice channel
if (sdk.channelId) { if (sdk.channelId) {
try { try {
await sdk.subscribe("SPEAKING_START", (data: any) => { sdk.subscribe("SPEAKING_START", (data: any) => {
console.log("[Discord Activity] Speaking start:", data); console.log("[Discord Activity] Speaking start:", data);
if (data?.user_id) { if (data?.user_id) {
setSpeakingUsers(prev => new Set(prev).add(data.user_id)); setSpeakingUsers(prev => new Set(prev).add(data.user_id));
setParticipants(prev => prev.map(p => setParticipants(prev => prev.map(p =>
p.id === data.user_id ? { ...p, speaking: true } : p p.id === data.user_id ? { ...p, speaking: true } : p
)); ));
} }
}, { channel_id: sdk.channelId }); }, { channel_id: sdk.channelId });
await sdk.subscribe("SPEAKING_STOP", (data: any) => { sdk.subscribe("SPEAKING_STOP", (data: any) => {
console.log("[Discord Activity] Speaking stop:", data); console.log("[Discord Activity] Speaking stop:", data);
if (data?.user_id) { if (data?.user_id) {
setSpeakingUsers(prev => { setSpeakingUsers(prev => {
@ -302,7 +302,7 @@ export const DiscordActivityProvider: React.FC<
next.delete(data.user_id); next.delete(data.user_id);
return next; return next;
}); });
setParticipants(prev => prev.map(p => setParticipants(prev => prev.map(p =>
p.id === data.user_id ? { ...p, speaking: false } : p p.id === data.user_id ? { ...p, speaking: false } : p
)); ));
} }

View file

@ -1,183 +1,90 @@
@import url("https://fonts.googleapis.com/css2?family=Electrolize&family=Orbitron:wght@400;600;700;900&family=Share+Tech+Mono&family=Source+Code+Pro:wght@300;400;500;600&family=VT323&family=Press+Start+2P&family=Merriweather:wght@400;700&display=swap"); @import url("https://fonts.googleapis.com/css2?family=VT323&family=Press+Start+2P&family=Merriweather:wght@400;700&family=Roboto+Mono:wght@300;400;500&display=swap");
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* ── AeThex Cyberpunk Theme — copied verbatim from AeThex-Passport-Engine/client/src/index.css ── */ @layer base {
:root { /**
--button-outline: rgba(0, 255, 255, .15); * Tailwind CSS theme
--badge-outline: rgba(0, 255, 255, .08); * tailwind.config.ts expects the following color variables to be expressed as HSL values.
--opaque-button-border-intensity: 8; * A different format will require also updating the theme in tailwind.config.ts.
--elevate-1: rgba(0, 255, 255, .04); *
--elevate-2: rgba(0, 255, 255, .08); * SPACING SYSTEM:
--background: 0 0% 2%; * Container: container mx-auto px-4 sm:px-6 lg:px-8
--foreground: 0 0% 95%; * Page Container: + py-8 lg:py-12
--border: 180 100% 50% / 0.2; * Max Widths: max-w-7xl (app), max-w-6xl (content), max-w-4xl (articles)
--card: 0 0% 5%; * Vertical Spacing: space-y-8 (sections), space-y-6 (cards), space-y-4 (content)
--card-foreground: 0 0% 95%; * Gaps: gap-6 (cards), gap-4 (buttons/forms), gap-2 (tags)
--card-border: 180 100% 50% / 0.15; */
--sidebar: 0 0% 4%; :root {
--sidebar-foreground: 0 0% 90%; --background: 222 84% 4.9%;
--sidebar-border: 180 100% 50% / 0.15;
--sidebar-primary: 180 100% 50%;
--sidebar-primary-foreground: 0 0% 0%;
--sidebar-accent: 0 0% 8%;
--sidebar-accent-foreground: 0 0% 90%;
--sidebar-ring: 180 100% 50%;
--popover: 0 0% 5%;
--popover-foreground: 0 0% 95%;
--popover-border: 180 100% 50% / 0.2;
--primary: 180 100% 50%;
--primary-foreground: 0 0% 0%;
--secondary: 0 0% 10%;
--secondary-foreground: 0 0% 85%;
--muted: 0 0% 8%;
--muted-foreground: 0 0% 55%;
--accent: 195 100% 45%;
--accent-foreground: 0 0% 0%;
--destructive: 340 100% 50%;
--destructive-foreground: 0 0% 98%;
--input: 0 0% 12%;
--ring: 180 100% 50%;
--chart-1: 180 100% 50%;
--chart-2: 300 100% 50%;
--chart-3: 142 76% 45%;
--chart-4: 340 100% 50%;
--chart-5: 260 100% 65%;
--neon-purple: 270 100% 65%;
--neon-magenta: 300 100% 55%;
--neon-cyan: 180 100% 50%;
--gameforge-green: 142 76% 45%;
--gameforge-dark: 142 30% 6%;
--font-sans: 'Electrolize', 'Source Code Pro', monospace;
--font-serif: Georgia, serif;
--font-mono: 'Source Code Pro', 'JetBrains Mono', monospace;
--font-display: 'Electrolize', monospace;
--font-pixel: Oxanium, sans-serif;
--radius: 0rem;
--shadow-2xs: 0px 2px 0px 0px hsl(0 0% 0% / 0.00);
--shadow-xs: 0px 2px 0px 0px hsl(0 0% 0% / 0.00);
--shadow-sm: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
--shadow: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
--shadow-md: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
--shadow-lg: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
--shadow-xl: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
--shadow-2xl: 0px 2px 0px 0px hsl(0 0% 0% / 0.00);
--tracking-normal: 0em;
--spacing: 0.25rem;
/* AeThex Brand Colors — cyan palette */ /* Spacing tokens */
--aethex-50: 180 100% 97%; --space-1: 4px;
--aethex-100: 180 100% 92%; --space-2: 8px;
--aethex-200: 180 100% 80%; --space-3: 12px;
--aethex-300: 180 100% 70%; --space-4: 16px;
--aethex-400: 180 100% 60%; --space-5: 24px;
--aethex-500: 180 100% 50%; --space-6: 32px;
--aethex-600: 180 100% 40%; --space-section-y: var(--space-6);
--aethex-700: 180 100% 30%; --foreground: 210 40% 98%;
--aethex-800: 180 100% 20%;
--aethex-900: 180 100% 12%;
--aethex-950: 180 100% 6%;
/* Neon accent palette */ --card: 222 84% 4.9%;
--neon-green: 142 76% 45%; --card-foreground: 210 40% 98%;
--neon-yellow: 50 100% 65%;
/* Spacing tokens */ --popover: 222 84% 4.9%;
--space-1: 4px; --popover-foreground: 210 40% 98%;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 24px;
--space-6: 32px;
--space-section-y: var(--space-6);
/* Fallback for older browsers */ --primary: 250 100% 60%;
--sidebar-primary-border: hsl(var(--sidebar-primary)); --primary-foreground: 210 40% 98%;
--sidebar-primary-border: hsl(from hsl(var(--sidebar-primary)) h s calc(l + var(--opaque-button-border-intensity)) / alpha);
--sidebar-accent-border: hsl(var(--sidebar-accent));
--sidebar-accent-border: hsl(from hsl(var(--sidebar-accent)) h s calc(l + var(--opaque-button-border-intensity)) / alpha);
--primary-border: hsl(var(--primary));
--primary-border: hsl(from hsl(var(--primary)) h s calc(l + var(--opaque-button-border-intensity)) / alpha);
--secondary-border: hsl(var(--secondary));
--secondary-border: hsl(from hsl(var(--secondary)) h s calc(l + var(--opaque-button-border-intensity)) / alpha);
--muted-border: hsl(var(--muted));
--muted-border: hsl(from hsl(var(--muted)) h s calc(l + var(--opaque-button-border-intensity)) / alpha);
--accent-border: hsl(var(--accent));
--accent-border: hsl(from hsl(var(--accent)) h s calc(l + var(--opaque-button-border-intensity)) / alpha);
--destructive-border: hsl(var(--destructive));
--destructive-border: hsl(from hsl(var(--destructive)) h s calc(l + var(--opaque-button-border-intensity)) / alpha);
}
.dark { --secondary: 217.2 32.6% 17.5%;
--button-outline: rgba(0, 255, 255, .15); --secondary-foreground: 210 40% 98%;
--badge-outline: rgba(0, 255, 255, .08);
--opaque-button-border-intensity: 8;
--elevate-1: rgba(0, 255, 255, .04);
--elevate-2: rgba(0, 255, 255, .08);
--background: 0 0% 2%;
--foreground: 0 0% 95%;
--border: 180 100% 50% / 0.2;
--card: 0 0% 5%;
--card-foreground: 0 0% 95%;
--card-border: 180 100% 50% / 0.15;
--sidebar: 0 0% 4%;
--sidebar-foreground: 0 0% 90%;
--sidebar-border: 180 100% 50% / 0.15;
--sidebar-primary: 180 100% 50%;
--sidebar-primary-foreground: 0 0% 0%;
--sidebar-accent: 0 0% 8%;
--sidebar-accent-foreground: 0 0% 90%;
--sidebar-ring: 180 100% 50%;
--popover: 0 0% 5%;
--popover-foreground: 0 0% 95%;
--popover-border: 180 100% 50% / 0.2;
--primary: 180 100% 50%;
--primary-foreground: 0 0% 0%;
--secondary: 0 0% 10%;
--secondary-foreground: 0 0% 85%;
--muted: 0 0% 8%;
--muted-foreground: 0 0% 55%;
--accent: 195 100% 45%;
--accent-foreground: 0 0% 0%;
--destructive: 340 100% 50%;
--destructive-foreground: 0 0% 98%;
--input: 0 0% 12%;
--ring: 180 100% 50%;
--chart-1: 180 100% 50%;
--chart-2: 300 100% 50%;
--chart-3: 142 76% 45%;
--chart-4: 340 100% 50%;
--chart-5: 260 100% 65%;
--neon-purple: 270 100% 65%;
--neon-magenta: 300 100% 55%;
--neon-cyan: 180 100% 50%;
--gameforge-green: 142 76% 45%;
--gameforge-dark: 142 30% 6%;
--shadow-2xs: 0px 2px 0px 0px hsl(0 0% 0% / 0.00);
--shadow-xs: 0px 2px 0px 0px hsl(0 0% 0% / 0.00);
--shadow-sm: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
--shadow: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
--shadow-md: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
--shadow-lg: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
--shadow-xl: 0px 2px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
--shadow-2xl: 0px 2px 0px 0px hsl(0 0% 0% / 0.00);
--sidebar-primary-border: hsl(var(--sidebar-primary)); --muted: 217.2 32.6% 17.5%;
--sidebar-primary-border: hsl(from hsl(var(--sidebar-primary)) h s calc(l + var(--opaque-button-border-intensity)) / alpha); --muted-foreground: 215 20.2% 65.1%;
--sidebar-accent-border: hsl(var(--sidebar-accent));
--sidebar-accent-border: hsl(from hsl(var(--sidebar-accent)) h s calc(l + var(--opaque-button-border-intensity)) / alpha); --accent: 217.2 32.6% 17.5%;
--primary-border: hsl(var(--primary)); --accent-foreground: 210 40% 98%;
--primary-border: hsl(from hsl(var(--primary)) h s calc(l + var(--opaque-button-border-intensity)) / alpha);
--secondary-border: hsl(var(--secondary)); --destructive: 0 62.8% 30.6%;
--secondary-border: hsl(from hsl(var(--secondary)) h s calc(l + var(--opaque-button-border-intensity)) / alpha); --destructive-foreground: 210 40% 98%;
--muted-border: hsl(var(--muted));
--muted-border: hsl(from hsl(var(--muted)) h s calc(l + var(--opaque-button-border-intensity)) / alpha); --border: 217.2 32.6% 17.5%;
--accent-border: hsl(var(--accent)); --input: 217.2 32.6% 17.5%;
--accent-border: hsl(from hsl(var(--accent)) h s calc(l + var(--opaque-button-border-intensity)) / alpha); --ring: 250 100% 70%;
--destructive-border: hsl(var(--destructive));
--destructive-border: hsl(from hsl(var(--destructive)) h s calc(l + var(--opaque-button-border-intensity)) / alpha); --radius: 0.5rem;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 250 100% 60%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
/* AeThex Brand Colors */
--aethex-50: 250 100% 97%;
--aethex-100: 250 100% 95%;
--aethex-200: 250 100% 90%;
--aethex-300: 250 100% 80%;
--aethex-400: 250 100% 70%;
--aethex-500: 250 100% 60%;
--aethex-600: 250 100% 50%;
--aethex-700: 250 100% 40%;
--aethex-800: 250 100% 30%;
--aethex-900: 250 100% 20%;
--aethex-950: 250 100% 10%;
/* Neon Colors for Accents */
--neon-purple: 280 100% 70%;
--neon-blue: 210 100% 70%;
--neon-green: 120 100% 70%;
--neon-yellow: 50 100% 70%;
}
} }
@layer base { @layer base {
@ -186,280 +93,529 @@
} }
body { body {
@apply font-sans antialiased bg-background text-foreground; @apply bg-background text-foreground;
} font-family: "Courier New", "Courier", monospace;
letter-spacing: 0.025em;
/* Scanline overlay — from AeThex-Passport-Engine */
body::before {
content: '';
position: fixed;
inset: 0;
background-image: repeating-linear-gradient(
0deg,
transparent,
transparent 1px,
rgba(0, 255, 255, 0.015) 1px,
rgba(0, 255, 255, 0.015) 2px
);
pointer-events: none;
z-index: 9999;
}
/* Grid background — from AeThex-Passport-Engine */
body::after {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(0, 255, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 255, 255, 0.03) 1px, transparent 1px);
background-size: 50px 50px;
pointer-events: none;
z-index: -1;
} }
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
-ms-overflow-style: none; /* Hide scrollbar while keeping functionality */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
html::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
/* Hide horizontal scrollbar on all elements */
* {
scrollbar-width: none; scrollbar-width: none;
} }
html::-webkit-scrollbar { display: none; } *::-webkit-scrollbar {
*::-webkit-scrollbar { display: none; } display: none;
* { scrollbar-width: none; } }
.container { .container {
@apply px-4 sm:px-6 lg:px-8; @apply px-4 sm:px-6 lg:px-8;
} }
} }
/* ── Elevation system — from AeThex-Passport-Engine ── */
@layer utilities { @layer utilities {
input[type="search"]::-webkit-search-cancel-button { /* Arm Theme Font Classes */
@apply hidden; .font-labs {
font-family: "VT323", "Courier New", monospace;
letter-spacing: 0.05em;
} }
[contenteditable][data-placeholder]:empty::before { .font-gameforge {
content: attr(data-placeholder); font-family: "Press Start 2P", "Arial Black", sans-serif;
color: hsl(var(--muted-foreground)); letter-spacing: 0.1em;
pointer-events: none; font-size: 0.875em;
} }
.no-default-hover-elevate {} .font-corp {
.no-default-active-elevate {} font-family: "Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI",
sans-serif;
.toggle-elevate::before, font-weight: 600;
.toggle-elevate-2::before {
content: "";
pointer-events: none;
position: absolute;
inset: 0px;
border-radius: inherit;
z-index: -1;
} }
.toggle-elevate.toggle-elevated::before { .font-foundation {
background-color: var(--elevate-2); font-family: "Merriweather", "Georgia", serif;
font-weight: 700;
letter-spacing: -0.02em;
} }
.border.toggle-elevate::before { inset: -1px; } .font-devlink {
font-family: "Roboto Mono", "Courier New", monospace;
.hover-elevate:not(.no-default-hover-elevate), font-weight: 400;
.active-elevate:not(.no-default-active-elevate), letter-spacing: 0.02em;
.hover-elevate-2:not(.no-default-hover-elevate),
.active-elevate-2:not(.no-default-active-elevate) {
position: relative;
z-index: 0;
} }
.hover-elevate:not(.no-default-hover-elevate)::after, .font-staff {
.active-elevate:not(.no-default-active-elevate)::after, font-family: "Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI",
.hover-elevate-2:not(.no-default-hover-elevate)::after, sans-serif;
.active-elevate-2:not(.no-default-active-elevate)::after { font-weight: 600;
content: "";
pointer-events: none;
position: absolute;
inset: 0px;
border-radius: inherit;
z-index: 999;
} }
.hover-elevate:hover:not(.no-default-hover-elevate)::after, .font-nexus {
.active-elevate:active:not(.no-default-active-elevate)::after { font-family: "Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI",
background-color: var(--elevate-1); sans-serif;
font-weight: 600;
} }
.hover-elevate-2:hover:not(.no-default-hover-elevate)::after, .font-default {
.active-elevate-2:active:not(.no-default-active-elevate)::after { font-family: "Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI",
background-color: var(--elevate-2); sans-serif;
} }
.border.hover-elevate:not(.no-hover-interaction-elevate)::after, /* Arm Theme Wallpaper Patterns */
.border.active-elevate:not(.no-active-interaction-elevate)::after,
.border.hover-elevate-2:not(.no-hover-interaction-elevate)::after,
.border.active-elevate-2:not(.no-active-interaction-elevate)::after {
inset: -1px;
}
}
/* ── AeThex OS brand utilities ── */
@layer utilities {
.ax-orbitron { font-family: "Orbitron", monospace !important; }
.ax-mono { font-family: "Share Tech Mono", monospace !important; }
.ax-electrolize { font-family: "Electrolize", monospace !important; }
.ax-corner-bracket { position: relative; }
.ax-corner-bracket::before,
.ax-corner-bracket::after {
content: ""; position: absolute; width: 14px; height: 14px; pointer-events: none;
}
.ax-corner-bracket::before {
top: -1px; left: -1px;
border-top: 1px solid rgba(0,255,255,0.5);
border-left: 1px solid rgba(0,255,255,0.5);
}
.ax-corner-bracket::after {
bottom: -1px; right: -1px;
border-bottom: 1px solid rgba(0,255,255,0.5);
border-right: 1px solid rgba(0,255,255,0.5);
}
.ax-card-sweep { position: relative; overflow: hidden; }
.ax-card-sweep::after {
content: ""; position: absolute; top: 0; left: -100%;
width: 50%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(0,255,255,0.04), transparent);
animation: ax-sweep 6s infinite; pointer-events: none;
}
.ax-clip {
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 8px 100%, 0 calc(100% - 8px));
}
.ax-vignette::after {
content: ""; position: fixed; inset: 0;
background: radial-gradient(ellipse at center, transparent 50%, rgba(0,0,0,0.55) 100%);
pointer-events: none; z-index: 9989;
}
/* ── Arm wallpaper patterns ── */
.wallpaper-labs { .wallpaper-labs {
background-image: radial-gradient(circle, rgba(251,191,36,0.08) 1px, transparent 1px); background-image: radial-gradient(
circle,
rgba(251, 191, 36, 0.08) 1px,
transparent 1px
);
background-size: 20px 20px; background-size: 20px 20px;
background-attachment: fixed; background-attachment: fixed;
} }
.wallpaper-gameforge { .wallpaper-gameforge {
background-image: background-image: linear-gradient(
linear-gradient(45deg, rgba(34,197,94,0.06) 25%, transparent 25%, transparent 75%, rgba(34,197,94,0.06) 75%), 45deg,
linear-gradient(45deg, rgba(34,197,94,0.06) 25%, transparent 25%, transparent 75%, rgba(34,197,94,0.06) 75%); rgba(34, 197, 94, 0.06) 25%,
transparent 25%,
transparent 75%,
rgba(34, 197, 94, 0.06) 75%
),
linear-gradient(
45deg,
rgba(34, 197, 94, 0.06) 25%,
transparent 25%,
transparent 75%,
rgba(34, 197, 94, 0.06) 75%
);
background-size: 40px 40px; background-size: 40px 40px;
background-position: 0 0, 20px 20px; background-position: 0 0, 20px 20px;
background-attachment: fixed; background-attachment: fixed;
} }
.wallpaper-corp { .wallpaper-corp {
background-image: background-image: linear-gradient(
linear-gradient(90deg, rgba(59,130,246,0.05) 1px, transparent 1px), 90deg,
linear-gradient(rgba(59,130,246,0.05) 1px, transparent 1px); rgba(59, 130, 246, 0.05) 1px,
transparent 1px
),
linear-gradient(rgba(59, 130, 246, 0.05) 1px, transparent 1px);
background-size: 20px 20px; background-size: 20px 20px;
background-attachment: fixed; background-attachment: fixed;
} }
.wallpaper-foundation { .wallpaper-foundation {
background-image: repeating-linear-gradient(0deg, rgba(239,68,68,0.04) 0px, rgba(239,68,68,0.04) 1px, transparent 1px, transparent 2px); background-image: repeating-linear-gradient(
0deg,
rgba(239, 68, 68, 0.04) 0px,
rgba(239, 68, 68, 0.04) 1px,
transparent 1px,
transparent 2px
);
background-attachment: fixed; background-attachment: fixed;
} }
.wallpaper-devlink { .wallpaper-devlink {
background-image: background-image: linear-gradient(
linear-gradient(0deg, transparent 24%, rgba(6,182,212,0.08) 25%, rgba(6,182,212,0.08) 26%, transparent 27%, transparent 74%, rgba(6,182,212,0.08) 75%, rgba(6,182,212,0.08) 76%, transparent 77%), 0deg,
linear-gradient(90deg, transparent 24%, rgba(6,182,212,0.08) 25%, rgba(6,182,212,0.08) 26%, transparent 27%, transparent 74%, rgba(6,182,212,0.08) 75%, rgba(6,182,212,0.08) 76%, transparent 77%); transparent 24%,
rgba(6, 182, 212, 0.08) 25%,
rgba(6, 182, 212, 0.08) 26%,
transparent 27%,
transparent 74%,
rgba(6, 182, 212, 0.08) 75%,
rgba(6, 182, 212, 0.08) 76%,
transparent 77%,
transparent
),
linear-gradient(
90deg,
transparent 24%,
rgba(6, 182, 212, 0.08) 25%,
rgba(6, 182, 212, 0.08) 26%,
transparent 27%,
transparent 74%,
rgba(6, 182, 212, 0.08) 75%,
rgba(6, 182, 212, 0.08) 76%,
transparent 77%,
transparent
);
background-size: 50px 50px; background-size: 50px 50px;
background-attachment: fixed; background-attachment: fixed;
} }
.wallpaper-staff { .wallpaper-staff {
background-image: radial-gradient(circle, rgba(168,85,247,0.08) 1px, transparent 1px); background-image: radial-gradient(
circle,
rgba(168, 85, 247, 0.08) 1px,
transparent 1px
);
background-size: 20px 20px; background-size: 20px 20px;
background-attachment: fixed; background-attachment: fixed;
} }
.wallpaper-nexus { .wallpaper-nexus {
background-image: background-image: linear-gradient(
linear-gradient(45deg, rgba(236,72,153,0.06) 25%, transparent 25%, transparent 75%, rgba(236,72,153,0.06) 75%), 45deg,
linear-gradient(45deg, rgba(236,72,153,0.06) 25%, transparent 25%, transparent 75%, rgba(236,72,153,0.06) 75%); rgba(236, 72, 153, 0.06) 25%,
transparent 25%,
transparent 75%,
rgba(236, 72, 153, 0.06) 75%
),
linear-gradient(
45deg,
rgba(236, 72, 153, 0.06) 25%,
transparent 25%,
transparent 75%,
rgba(236, 72, 153, 0.06) 75%
);
background-size: 40px 40px; background-size: 40px 40px;
background-position: 0 0, 20px 20px; background-position: 0 0, 20px 20px;
background-attachment: fixed; background-attachment: fixed;
} }
.wallpaper-default { .wallpaper-default {
background-image: linear-gradient(135deg, rgba(0,255,255,0.03) 0%, rgba(0,255,255,0.01) 100%); background-image: linear-gradient(
135deg,
rgba(167, 139, 250, 0.05) 0%,
rgba(96, 165, 250, 0.05) 100%
);
} }
/* ── Font aliases (arm theming) ── */ .section-cozy {
.font-labs { font-family: "VT323", "Courier New", monospace; letter-spacing: 0.05em; } padding-block: var(--space-section-y);
.font-gameforge { font-family: "Press Start 2P", "Arial Black", sans-serif; letter-spacing: 0.1em; font-size: 0.875em; } }
.font-corp { font-family: "Electrolize", "Source Code Pro", monospace; font-weight: 600; } .gap-cozy {
.font-foundation { font-family: "Merriweather", "Georgia", serif; font-weight: 700; letter-spacing: -0.02em; } gap: var(--space-5);
.font-devlink { font-family: "Source Code Pro", "Electrolize", monospace; font-weight: 400; letter-spacing: 0.02em; } }
.font-staff { font-family: "Electrolize", "Source Code Pro", monospace; font-weight: 600; } .pad-cozy {
.font-nexus { font-family: "Electrolize", "Source Code Pro", monospace; font-weight: 600; } padding: var(--space-5);
.font-default { font-family: "Electrolize", "Source Code Pro", monospace; } }
/* ── Text gradients ── */
.text-gradient { .text-gradient {
@apply bg-gradient-to-r from-aethex-300 via-aethex-500 to-neon-purple bg-clip-text text-transparent; @apply bg-gradient-to-r from-aethex-400 via-neon-blue to-aethex-600 bg-clip-text text-transparent;
background-size: 200% 200%;
animation: gradient-shift 3s ease-in-out infinite;
} }
.text-gradient-purple { .text-gradient-purple {
@apply bg-gradient-to-r from-neon-purple via-aethex-500 to-aethex-300 bg-clip-text text-transparent; @apply bg-gradient-to-r from-neon-purple via-aethex-500 to-neon-blue bg-clip-text text-transparent;
background-size: 200% 200%;
animation: gradient-shift 4s ease-in-out infinite;
} }
.bg-aethex-gradient { .bg-aethex-gradient {
@apply bg-gradient-to-br from-aethex-900 via-background to-aethex-800; @apply bg-gradient-to-br from-aethex-900 via-background to-aethex-800;
} }
/* ── Interaction ── */ .border-gradient {
.hover-lift { transition: transform 0.3s ease, box-shadow 0.3s ease; } @apply relative overflow-hidden;
.hover-lift:hover { transform: translateY(-4px); } }
.hover-glow { transition: all 0.3s ease; }
.hover-glow:hover { filter: brightness(1.1) drop-shadow(0 0 8px rgba(0,255,255,0.4)); }
.interactive-scale { transition: transform 0.2s ease; }
.interactive-scale:hover { transform: scale(1.03); }
.interactive-scale:active { transform: scale(0.98); }
/* ── Spacing helpers ── */ .border-gradient::before {
.section-cozy { padding-block: var(--space-section-y); } content: "";
.gap-cozy { gap: var(--space-5); } @apply absolute inset-0 rounded-[inherit] p-[1px] bg-gradient-to-r from-aethex-400 via-neon-blue to-aethex-600;
.pad-cozy { padding: var(--space-5); } mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: xor;
background-size: 200% 200%;
animation: gradient-shift 2s ease-in-out infinite;
}
.glow-purple {
box-shadow:
0 0 20px rgba(139, 92, 246, 0.3),
0 0 40px rgba(139, 92, 246, 0.2);
transition: box-shadow 0.3s ease;
}
.glow-purple:hover {
box-shadow:
0 0 30px rgba(139, 92, 246, 0.5),
0 0 60px rgba(139, 92, 246, 0.3);
}
.glow-blue {
box-shadow:
0 0 20px rgba(59, 130, 246, 0.3),
0 0 40px rgba(59, 130, 246, 0.2);
transition: box-shadow 0.3s ease;
}
.glow-blue:hover {
box-shadow:
0 0 30px rgba(59, 130, 246, 0.5),
0 0 60px rgba(59, 130, 246, 0.3);
}
.glow-green {
box-shadow:
0 0 20px rgba(34, 197, 94, 0.3),
0 0 40px rgba(34, 197, 94, 0.2);
transition: box-shadow 0.3s ease;
}
.glow-yellow {
box-shadow:
0 0 20px rgba(251, 191, 36, 0.3),
0 0 40px rgba(251, 191, 36, 0.2);
transition: box-shadow 0.3s ease;
}
.animate-fade-in {
animation: fade-in 0.6s ease-out;
}
.animate-slide-up {
animation: slide-up 0.6s ease-out;
}
.animate-slide-down {
animation: slide-down 0.6s ease-out;
}
.animate-slide-left {
animation: slide-left 0.6s ease-out;
}
.animate-slide-right {
animation: slide-right 0.6s ease-out;
}
.animate-scale-in {
animation: scale-in 0.4s ease-out;
}
.animate-bounce-gentle {
animation: bounce-gentle 2s ease-in-out infinite;
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
.animate-pulse-glow {
animation: pulse-glow 2s ease-in-out infinite;
}
/* ── Animations ── */
.animate-fade-in { animation: fade-in 0.6s ease-out; }
.animate-slide-up { animation: slide-up 0.6s ease-out; }
.animate-slide-down { animation: slide-down 0.6s ease-out; }
.animate-slide-left { animation: slide-left 0.6s ease-out; }
.animate-slide-right { animation: slide-right 0.6s ease-out; }
.animate-scale-in { animation: scale-in 0.4s ease-out; }
.animate-typing { .animate-typing {
animation: typing 3s steps(40, end), blink-caret 0.75s step-end infinite; animation:
typing 3s steps(40, end),
blink-caret 0.75s step-end infinite;
overflow: hidden; overflow: hidden;
border-right: 3px solid; border-right: 3px solid;
white-space: nowrap; white-space: nowrap;
margin: 0 auto; margin: 0 auto;
} }
.hover-lift {
transition:
transform 0.3s ease,
box-shadow 0.3s ease;
}
.hover-lift:hover {
transform: translateY(-8px);
}
.hover-glow {
transition: all 0.3s ease;
}
.hover-glow:hover {
filter: brightness(1.1) drop-shadow(0 0 10px currentColor);
}
.interactive-scale {
transition: transform 0.2s ease;
}
.interactive-scale:hover {
transform: scale(1.05);
}
.interactive-scale:active {
transform: scale(0.98);
}
.loading-dots::after {
content: "";
animation: loading-dots 1.5s infinite;
}
.skeleton { .skeleton {
background: linear-gradient(90deg, transparent, rgba(0,255,255,0.06), transparent); background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.1),
transparent
);
background-size: 200% 100%; background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite; animation: skeleton-loading 1.5s infinite;
} }
} }
/* ── Keyframes ── */ @keyframes gradient-shift {
@keyframes ax-sweep { 0%{left:-100%} 100%{left:200%} } 0%,
@keyframes ax-blink { 0%,50%{opacity:1} 51%,100%{opacity:0} } 100% {
@keyframes fade-in { from{opacity:0} to{opacity:1} } background-position: 0% 50%;
@keyframes slide-up { from{opacity:0;transform:translateY(20px)} to{opacity:1;transform:translateY(0)} } }
@keyframes slide-down { from{opacity:0;transform:translateY(-20px)} to{opacity:1;transform:translateY(0)} } 50% {
@keyframes slide-left { from{opacity:0;transform:translateX(20px)} to{opacity:1;transform:translateX(0)} } background-position: 100% 50%;
@keyframes slide-right { from{opacity:0;transform:translateX(-20px)} to{opacity:1;transform:translateX(0)} } }
@keyframes scale-in { from{opacity:0;transform:scale(0.95)} to{opacity:1;transform:scale(1)} } }
@keyframes typing { from{width:0} to{width:100%} }
@keyframes blink-caret { from,to{border-color:transparent} 50%{border-color:currentColor} } @keyframes fade-in {
@keyframes skeleton-loading { 0%{background-position:-200% 0} 100%{background-position:200% 0} } from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-down {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-left {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slide-right {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes bounce-gentle {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
@keyframes float {
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-20px);
}
}
@keyframes pulse-glow {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.8;
transform: scale(1.05);
}
}
@keyframes typing {
from {
width: 0;
}
to {
width: 100%;
}
}
@keyframes blink-caret {
from,
to {
border-color: transparent;
}
50% {
border-color: currentColor;
}
}
@keyframes loading-dots {
0% {
content: "";
}
25% {
content: ".";
}
50% {
content: "..";
}
75% {
content: "...";
}
100% {
content: "";
}
}
@keyframes skeleton-loading {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
* { * {

View file

@ -3,8 +3,8 @@
import { supabase, isSupabaseConfigured } from "@/lib/supabase"; import { supabase, isSupabaseConfigured } from "@/lib/supabase";
import type { Database } from "./database.types"; import type { Database } from "./database.types";
// Derive UserProfile from the live generated schema // Use the existing database user profile type directly
type UserProfile = Database["public"]["Tables"]["user_profiles"]["Row"]; import type { UserProfile } from "./database.types";
// API Base URL for fetch requests // API Base URL for fetch requests
const API_BASE = import.meta.env.VITE_API_BASE || ""; const API_BASE = import.meta.env.VITE_API_BASE || "";

View file

@ -1,30 +0,0 @@
import { supabase } from "@/lib/supabase";
/**
* Authenticated fetch wrapper.
* Automatically injects `Authorization: Bearer <token>` from the active
* Supabase session. Falls back to an unauthenticated request if no session
* exists (lets public endpoints still work normally).
*
* Drop-in replacement for `fetch` same signature, same return value.
*/
export async function authFetch(
input: RequestInfo | URL,
init: RequestInit = {}
): Promise<Response> {
const {
data: { session },
} = await supabase.auth.getSession();
const headers = new Headers(init.headers);
if (session?.access_token) {
headers.set("Authorization", `Bearer ${session.access_token}`);
}
if (init.body && typeof init.body === "string" && !headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
return fetch(input, { ...init, headers });
}

File diff suppressed because it is too large Load diff

View file

@ -124,18 +124,14 @@ export default function About() {
{/* Hero */} {/* Hero */}
<section className="py-16 lg:py-24 border-b border-gray-800"> <section className="py-16 lg:py-24 border-b border-gray-800">
<div className="container mx-auto max-w-6xl px-4"> <div className="container mx-auto max-w-6xl px-4">
<h1 className="text-5xl lg:text-7xl font-black mb-6"> <h1 className="text-4xl lg:text-5xl font-bold mb-6">
Building an Integrated{" "} Building an Integrated{" "}
<span className="bg-gradient-to-r from-yellow-300 via-blue-300 to-red-300 bg-clip-text text-transparent"> <span className="bg-gradient-to-r from-yellow-300 via-blue-300 to-red-300 bg-clip-text text-transparent">
Ecosystem Ecosystem
</span> </span>
</h1> </h1>
<p className="text-xl text-gray-300 max-w-3xl"> <p className="text-lg text-gray-300 max-w-3xl">
AeThex operates as a unified four-pillar organization that Four-pillar ecosystem combining innovation, operations, community, and talent
combines speculative innovation, profitable operations, community
impact, and specialized talent acquisition. This structure creates
multiple reinforcing competitive moats while managing risk and
maintaining investor confidence.
</p> </p>
</div> </div>
</section> </section>

View file

@ -1042,7 +1042,7 @@ function PollsTab({ userId, username }: { userId?: string; username?: string })
}, },
); );
channel.subscribe(); channel.subscribe().catch(() => {});
return () => { return () => {
supabase.removeChannel(channel); supabase.removeChannel(channel);
}; };
@ -2657,7 +2657,7 @@ function ChatTab({
}, },
); );
channel.subscribe(); channel.subscribe().catch(() => {});
return () => { return () => {
clearInterval(interval); clearInterval(interval);

View file

@ -25,7 +25,7 @@ const ARMS: Arm[] = [
textColor: "text-purple-400", textColor: "text-purple-400",
href: "/staff", href: "/staff",
icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fc0414efd7af54ef4b821a05d469150d0?format=webp&width=800", icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fc0414efd7af54ef4b821a05d469150d0?format=webp&width=800",
tip: "Staff operations & internal portal", tip: "Staff operations & portal",
shadowColor: "shadow-purple-500/50", shadowColor: "shadow-purple-500/50",
glowColor: "rgba(168, 85, 247, 0.3)", glowColor: "rgba(168, 85, 247, 0.3)",
}, },
@ -37,7 +37,7 @@ const ARMS: Arm[] = [
textColor: "text-yellow-400", textColor: "text-yellow-400",
href: "/labs", href: "/labs",
icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fd93f7113d34347469e74421c3a3412e5?format=webp&width=800", icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fd93f7113d34347469e74421c3a3412e5?format=webp&width=800",
tip: "R&D pushing innovation boundaries", tip: "R&D and innovation",
shadowColor: "shadow-yellow-500/50", shadowColor: "shadow-yellow-500/50",
glowColor: "rgba(251, 191, 36, 0.3)", glowColor: "rgba(251, 191, 36, 0.3)",
}, },
@ -49,7 +49,7 @@ const ARMS: Arm[] = [
textColor: "text-green-400", textColor: "text-green-400",
href: "/gameforge", href: "/gameforge",
icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fcd3534c1caa0497abfd44224040c6059?format=webp&width=800", icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fcd3534c1caa0497abfd44224040c6059?format=webp&width=800",
tip: "Games shipped monthly at speed", tip: "Ship games monthly",
shadowColor: "shadow-green-500/50", shadowColor: "shadow-green-500/50",
glowColor: "rgba(34, 197, 94, 0.3)", glowColor: "rgba(34, 197, 94, 0.3)",
}, },
@ -61,7 +61,7 @@ const ARMS: Arm[] = [
textColor: "text-blue-400", textColor: "text-blue-400",
href: "/corp", href: "/corp",
icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3772073d5b4b49e688ed02480f4cae43?format=webp&width=800", icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3772073d5b4b49e688ed02480f4cae43?format=webp&width=800",
tip: "Enterprise solutions for scale", tip: "Enterprise solutions",
shadowColor: "shadow-blue-500/50", shadowColor: "shadow-blue-500/50",
glowColor: "rgba(59, 130, 246, 0.3)", glowColor: "rgba(59, 130, 246, 0.3)",
}, },
@ -73,7 +73,7 @@ const ARMS: Arm[] = [
textColor: "text-red-400", textColor: "text-red-400",
href: "https://aethex.foundation", href: "https://aethex.foundation",
icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fc02cb1bf5056479bbb3ea4bd91f0d472?format=webp&width=800", icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fc02cb1bf5056479bbb3ea4bd91f0d472?format=webp&width=800",
tip: "Community & education initiatives", tip: "Community & education",
shadowColor: "shadow-red-500/50", shadowColor: "shadow-red-500/50",
glowColor: "rgba(239, 68, 68, 0.3)", glowColor: "rgba(239, 68, 68, 0.3)",
external: true, external: true,
@ -86,7 +86,7 @@ const ARMS: Arm[] = [
textColor: "text-purple-400", textColor: "text-purple-400",
href: "/nexus", href: "/nexus",
icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F6df123b87a144b1fb99894d94198d97b?format=webp&width=800", icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F6df123b87a144b1fb99894d94198d97b?format=webp&width=800",
tip: "Talent marketplace & collaboration", tip: "Talent marketplace",
shadowColor: "shadow-purple-500/50", shadowColor: "shadow-purple-500/50",
glowColor: "rgba(168, 85, 247, 0.3)", glowColor: "rgba(168, 85, 247, 0.3)",
}, },

View file

@ -347,7 +347,7 @@ export default function BotPanel() {
</div> </div>
)} )}
<Separator className="bg-gray-700" /> <Separator className="bg-gray-700" />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<p className="text-sm text-gray-400">Commands</p> <p className="text-sm text-gray-400">Commands</p>
<p className="text-lg font-semibold text-white"> <p className="text-lg font-semibold text-white">
@ -379,7 +379,7 @@ export default function BotPanel() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<div className="text-center p-4 bg-gray-700/30 rounded-lg"> <div className="text-center p-4 bg-gray-700/30 rounded-lg">
<p className="text-2xl font-bold text-white">{feedStats?.totalPosts || 0}</p> <p className="text-2xl font-bold text-white">{feedStats?.totalPosts || 0}</p>
<p className="text-sm text-gray-400">Total Posts</p> <p className="text-sm text-gray-400">Total Posts</p>

View file

@ -22,25 +22,25 @@ export default function Careers() {
icon: <Microscope className="h-6 w-6" />, icon: <Microscope className="h-6 w-6" />,
title: "Innovation First", title: "Innovation First",
description: description:
"We push boundaries and explore cutting-edge technologies daily", "Push boundaries and explore cutting-edge technologies",
}, },
{ {
icon: <Heart className="h-6 w-6" />, icon: <Heart className="h-6 w-6" />,
title: "People Matter", title: "People Matter",
description: description:
"We invest in our team's growth, health, and work-life balance", "Invest in team growth, health, and work-life balance",
}, },
{ {
icon: <Zap className="h-6 w-6" />, icon: <Zap className="h-6 w-6" />,
title: "Ship It", title: "Ship It",
description: description:
"We believe in execution over perfection—iterate and learn fast", "Execute over perfection—iterate and learn fast",
}, },
{ {
icon: <Users className="h-6 w-6" />, icon: <Users className="h-6 w-6" />,
title: "Collaboration", title: "Collaboration",
description: description:
"Great ideas come from diverse teams working together openly", "Great ideas come from diverse teams working openly",
}, },
]; ];
@ -49,8 +49,8 @@ export default function Careers() {
"Comprehensive health insurance (medical, dental, vision)", "Comprehensive health insurance (medical, dental, vision)",
"Unlimited PTO", "Unlimited PTO",
"Remote-first, work from anywhere", "Remote-first, work from anywhere",
"Equipment budget for your home office", "Equipment budget",
"Professional development fund ($5k/year)", "Professional development ($5k/year)",
"Team offsites & retreats", "Team offsites & retreats",
"Stock options", "Stock options",
"Parental leave", "Parental leave",
@ -67,7 +67,7 @@ export default function Careers() {
level: "Senior", level: "Senior",
type: "Full-time", type: "Full-time",
description: description:
"Lead architecture and implementation of next-generation platform systems", "Lead platform architecture and implementation",
}, },
{ {
title: "Game Developer", title: "Game Developer",
@ -75,7 +75,7 @@ export default function Careers() {
location: "Remote", location: "Remote",
level: "Mid-level", level: "Mid-level",
type: "Full-time", type: "Full-time",
description: "Ship games monthly with our world-class production team", description: "Ship games monthly with world-class team",
}, },
{ {
title: "Research Scientist", title: "Research Scientist",
@ -84,7 +84,7 @@ export default function Careers() {
level: "Senior", level: "Senior",
type: "Full-time", type: "Full-time",
description: description:
"Explore AI/ML applications in game development and interactive experiences", "Explore AI/ML in game development and interactive experiences",
}, },
{ {
title: "Product Manager", title: "Product Manager",
@ -93,7 +93,7 @@ export default function Careers() {
level: "Mid-level", level: "Mid-level",
type: "Full-time", type: "Full-time",
description: description:
"Shape the future of our developer tools and platforms", "Shape the future of developer tools and platforms",
}, },
{ {
title: "UX/UI Designer", title: "UX/UI Designer",
@ -102,7 +102,7 @@ export default function Careers() {
level: "Mid-level", level: "Mid-level",
type: "Full-time", type: "Full-time",
description: description:
"Design beautiful, intuitive interfaces for millions of developers", "Design beautiful interfaces for developers",
}, },
{ {
title: "DevOps Engineer", title: "DevOps Engineer",
@ -110,7 +110,7 @@ export default function Careers() {
location: "Remote", location: "Remote",
level: "Senior", level: "Senior",
type: "Full-time", type: "Full-time",
description: "Build the infrastructure that powers AeThex at scale", description: "Build infrastructure that powers AeThex at scale",
}, },
]; ];

View file

@ -66,12 +66,11 @@ export default function Contact() {
<div className="container mx-auto px-4 max-w-5xl space-y-10"> <div className="container mx-auto px-4 max-w-5xl space-y-10">
<div className="grid md:grid-cols-2 gap-8 items-start"> <div className="grid md:grid-cols-2 gap-8 items-start">
<div className="space-y-3"> <div className="space-y-3">
<h1 className="text-4xl font-bold text-gradient-purple"> <h1 className="text-3xl font-bold text-gradient-purple">
Contact Us Contact Us
</h1> </h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Have a project or question? We typically respond within 12 We respond within 12 business days
business days.
</p> </p>
<Card className="bg-card/50 border-border/50"> <Card className="bg-card/50 border-border/50">
<CardContent className="p-6 space-y-3"> <CardContent className="p-6 space-y-3">

View file

@ -78,8 +78,8 @@ export default function Corp() {
const services = [ const services = [
{ {
title: "Custom Software Development", title: "Custom Software",
description: "Bespoke applications built for enterprise scale", description: "Enterprise applications",
icon: Code, icon: Code,
examples: [ examples: [
"Web & mobile applications", "Web & mobile applications",
@ -90,8 +90,8 @@ export default function Corp() {
color: "from-blue-500 to-cyan-500", color: "from-blue-500 to-cyan-500",
}, },
{ {
title: "Technology Consulting", title: "Tech Consulting",
description: "Strategic guidance for digital transformation", description: "Digital transformation",
icon: Briefcase, icon: Briefcase,
examples: [ examples: [
"Architecture design", "Architecture design",
@ -102,8 +102,8 @@ export default function Corp() {
color: "from-purple-500 to-pink-500", color: "from-purple-500 to-pink-500",
}, },
{ {
title: "Game Development Services", title: "Game Development",
description: "Specialized expertise for gaming companies", description: "Metaverse & gaming",
icon: Rocket, icon: Rocket,
examples: [ examples: [
"Full game production", "Full game production",

View file

@ -45,9 +45,6 @@ import {
CheckCircle2, CheckCircle2,
Github, Github,
Mail, Mail,
Loader2,
Unlink,
Link as LinkIcon,
} from "lucide-react"; } from "lucide-react";
const DiscordIcon = () => ( const DiscordIcon = () => (
@ -149,93 +146,6 @@ const OAUTH_PROVIDERS: readonly ProviderDescriptor[] = [
}, },
]; ];
const API_BASE = import.meta.env.VITE_API_BASE || window.location.origin;
function AeThexIDConnection({ user }: { user: any }) {
const isLinked = !!user?.user_metadata?.authentik_linked;
const sub = user?.user_metadata?.authentik_sub as string | undefined;
const [unlinking, setUnlinking] = useState(false);
const handleLink = () => {
window.location.href = `${API_BASE}/api/auth/authentik/start?redirectTo=/dashboard?tab=connections`;
};
const handleUnlink = async () => {
setUnlinking(true);
try {
const res = await fetch(`${API_BASE}/api/auth/authentik/unlink`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${(await import("@/lib/supabase")).supabase.auth.getSession().then(s => s.data.session?.access_token || "")}` },
});
if (res.ok) {
aethexToast.success({ title: "AeThex ID unlinked", description: "You can re-link at any time." });
setTimeout(() => window.location.reload(), 800);
} else {
aethexToast.error({ title: "Unlink failed", description: "Try again." });
}
} catch {
aethexToast.error({ title: "Unlink failed", description: "Try again." });
} finally {
setUnlinking(false);
}
};
return (
<section
className={`flex flex-col gap-4 rounded-xl border p-4 md:flex-row md:items-center md:justify-between mt-4 ${
isLinked ? "border-cyan-500/40 bg-cyan-500/5" : "border-border/50 bg-background/20"
}`}
>
<div className="flex flex-1 items-start gap-4">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg" style={{ background: "linear-gradient(135deg, rgba(0,255,255,0.2), rgba(0,255,255,0.05))", border: "1px solid rgba(0,255,255,0.3)" }}>
<svg viewBox="0 0 100 100" width={28} height={28}>
<polygon points="50,5 95,27.5 95,72.5 50,95 5,72.5 5,27.5" fill="none" stroke="#00ffff" strokeWidth="4" opacity="0.9"/>
<text x="50" y="63" textAnchor="middle" fontFamily="Orbitron" fontSize="36" fontWeight="700" fill="#00ffff">Æ</text>
</svg>
</div>
<div className="flex-1 space-y-2">
<div className="flex flex-col gap-1 md:flex-row md:items-center md:gap-3">
<h3 className="text-lg font-semibold text-foreground">AeThex ID</h3>
{isLinked ? (
<span className="inline-flex items-center gap-1 rounded-full bg-cyan-600/80 px-2 py-0.5 text-xs font-medium text-white">
<Shield className="h-3 w-3" /> Linked
</span>
) : (
<span className="inline-flex items-center rounded-full border border-border/50 px-2 py-0.5 text-xs text-muted-foreground">
Not linked
</span>
)}
<span className="inline-flex items-center rounded-full bg-amber-500/10 border border-amber-500/30 px-2 py-0.5 text-xs text-amber-400">
AeThex Staff
</span>
</div>
<p className="text-sm text-muted-foreground">
Single sign-on via <span className="text-cyan-400 font-mono text-xs">auth.aethex.tech</span> for AeThex employees and internal team members.
</p>
{isLinked && sub && (
<p className="text-xs text-muted-foreground font-mono truncate">
<span className="text-foreground font-medium">Identity:</span> {sub.slice(0, 16)}
</p>
)}
</div>
</div>
<div className="flex items-center gap-3 md:self-center">
{isLinked ? (
<Button variant="outline" className="flex items-center gap-2" disabled={unlinking} onClick={handleUnlink} type="button">
{unlinking ? <Loader2 className="h-4 w-4 animate-spin" /> : <Unlink className="h-4 w-4" />}
Unlink
</Button>
) : (
<Button className="flex items-center gap-2" style={{ background: "rgba(0,255,255,0.15)", border: "1px solid rgba(0,255,255,0.4)", color: "#00ffff" }} onClick={handleLink} type="button">
<LinkIcon className="h-4 w-4" />
Link AeThex ID
</Button>
)}
</div>
</section>
);
}
export default function Dashboard() { export default function Dashboard() {
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
@ -394,14 +304,14 @@ export default function Dashboard() {
</div> </div>
<Button <Button
onClick={() => navigate("/login")} onClick={() => navigate("/login")}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white text-lg py-6" className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white py-6"
> >
Sign In to Dashboard Sign In to Dashboard
</Button> </Button>
<Button <Button
onClick={() => navigate("/onboarding")} onClick={() => navigate("/onboarding")}
variant="outline" variant="outline"
className="w-full text-lg py-6 border-purple-500/30 text-purple-300 hover:bg-purple-500/10" className="w-full py-6 border-purple-500/30 text-purple-300 hover:bg-purple-500/10"
> >
Create New Account Create New Account
</Button> </Button>
@ -416,15 +326,15 @@ export default function Dashboard() {
return ( return (
<Layout> <Layout>
<div className="min-h-screen bg-gradient-to-b from-black via-purple-950/20 to-black"> <div className="min-h-screen bg-gradient-to-b from-black via-purple-950/20 to-black">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12 max-w-6xl space-y-8"> <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12 max-w-6xl space-y-12">
{/* Header Section */} {/* Header Section */}
<div className="space-y-4 animate-slide-down"> <div className="space-y-4 animate-slide-down">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="space-y-2"> <div className="space-y-2">
<h1 className="text-5xl md:text-6xl font-bold bg-gradient-to-r from-purple-300 via-blue-300 to-purple-300 bg-clip-text text-transparent"> <h1 className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-purple-300 via-blue-300 to-purple-300 bg-clip-text text-transparent">
Dashboard Dashboard
</h1> </h1>
<p className="text-gray-400 text-lg"> <p className="text-gray-400">
Welcome back,{" "} Welcome back,{" "}
<span className="text-purple-300 font-semibold"> <span className="text-purple-300 font-semibold">
{profile?.full_name || user.email?.split("@")[0]} {profile?.full_name || user.email?.split("@")[0]}
@ -483,7 +393,7 @@ export default function Dashboard() {
onValueChange={setActiveTab} onValueChange={setActiveTab}
className="w-full" className="w-full"
> >
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-4 bg-purple-950/30 border border-purple-500/20 p-1"> <TabsList className="grid w-full grid-cols-4 lg:grid-cols-4 bg-purple-950/30 border border-purple-500/20 p-1">
<TabsTrigger value="realms" className="text-sm md:text-base"> <TabsTrigger value="realms" className="text-sm md:text-base">
<span className="hidden sm:inline">Realms</span> <span className="hidden sm:inline">Realms</span>
<span className="sm:hidden">Arms</span> <span className="sm:hidden">Arms</span>
@ -503,15 +413,15 @@ export default function Dashboard() {
<TabsContent value="realms" className="space-y-6 animate-fade-in"> <TabsContent value="realms" className="space-y-6 animate-fade-in">
{/* Developer CTA Card */} {/* Developer CTA Card */}
{user && ( {user && (
<Card className="p-6 bg-gradient-to-br from-primary/10 to-primary/5 border-primary/20 hover:border-primary/40 transition-all"> <Card className="p-8 bg-gradient-to-br from-primary/10 to-primary/5 border-primary/20 hover:border-primary/40 transition-all">
<div className="flex flex-col md:flex-row items-start gap-4"> <div className="flex flex-col md:flex-row items-start gap-6">
<div className="p-3 bg-primary/20 rounded-lg shrink-0"> <div className="p-4 bg-primary/20 rounded-lg shrink-0">
<Code className="w-6 h-6 text-primary" /> <Code className="w-6 h-6 text-primary" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h3 className="text-lg font-semibold mb-2">Building with AeThex?</h3> <h3 className="text-xl font-semibold mb-3">Building with AeThex?</h3>
<p className="text-sm text-muted-foreground mb-4"> <p className="text-base text-muted-foreground mb-6">
Get API keys, access comprehensive documentation, and explore developer tools to integrate AeThex into your applications. Get API keys and access developer tools
</p> </p>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<Link to="/dev-platform/dashboard"> <Link to="/dev-platform/dashboard">
@ -522,12 +432,7 @@ export default function Dashboard() {
</Link> </Link>
<Link to="/dev-platform/api-reference"> <Link to="/dev-platform/api-reference">
<Button size="sm" variant="outline"> <Button size="sm" variant="outline">
View API Docs View Docs
</Button>
</Link>
<Link to="/dev-platform/templates">
<Button size="sm" variant="outline">
Browse Templates
</Button> </Button>
</Link> </Link>
</div> </div>
@ -536,7 +441,7 @@ export default function Dashboard() {
</Card> </Card>
)} )}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{ARMS.map((arm) => { {ARMS.map((arm) => {
const IconComponent = arm.icon; const IconComponent = arm.icon;
return ( return (
@ -555,7 +460,7 @@ export default function Dashboard() {
<Card <Card
className={`bg-gradient-to-br ${arm.bgGradient} border transition-all duration-300 h-full hover:shadow-lg hover:shadow-purple-500/20 ${arm.borderColor} cursor-pointer`} className={`bg-gradient-to-br ${arm.bgGradient} border transition-all duration-300 h-full hover:shadow-lg hover:shadow-purple-500/20 ${arm.borderColor} cursor-pointer`}
> >
<CardContent className="p-6 space-y-4"> <CardContent className="p-8 space-y-6">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -768,7 +673,7 @@ export default function Dashboard() {
linkedProviderMap={ linkedProviderMap={
linkedProviders linkedProviders
? Object.fromEntries( ? Object.fromEntries(
linkedProviders.map((p) => [p.provider, p as any]), linkedProviders.map((p) => [p.provider, p]),
) )
: {} : {}
} }
@ -776,9 +681,6 @@ export default function Dashboard() {
onLink={linkProvider} onLink={linkProvider}
onUnlink={unlinkProvider} onUnlink={unlinkProvider}
/> />
{/* AeThex ID (Authentik SSO) — staff/internal identity */}
<AeThexIDConnection user={user} />
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>

View file

@ -84,12 +84,11 @@ export default function Downloads() {
<Badge variant="outline" className="mb-4 border-purple-500/50 text-purple-400"> <Badge variant="outline" className="mb-4 border-purple-500/50 text-purple-400">
Version {CURRENT_VERSION} Version {CURRENT_VERSION}
</Badge> </Badge>
<h1 className="text-4xl md:text-5xl font-bold mb-4 bg-gradient-to-r from-white via-purple-400 to-blue-400 bg-clip-text text-transparent"> <h1 className="text-3xl md:text-4xl font-bold mb-4 bg-gradient-to-r from-white via-purple-400 to-blue-400 bg-clip-text text-transparent">
Download AeThex Desktop Download AeThex Desktop
</h1> </h1>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto"> <p className="text-lg text-muted-foreground max-w-2xl mx-auto">
The AeThex Desktop Terminal brings the full power of the platform to your computer. Desktop Terminal brings full platform access to your computer. Manage projects and stay connected.
Access realms, manage projects, and stay connected with your community.
</p> </p>
</div> </div>

View file

@ -26,7 +26,7 @@ import {
TrendingUp, TrendingUp,
Heart, Heart,
MessageSquare, MessageSquare,
Avatar, User,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { import {

View file

@ -1,24 +1,32 @@
import Layout from "@/components/Layout"; import Layout from "@/components/Layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useArmTheme } from "@/contexts/ArmThemeContext";
import { import {
Heart, Heart,
ExternalLink, BookOpen,
ArrowRight,
Gamepad2,
Users,
Code, Code,
Users,
Zap,
ArrowRight,
GraduationCap, GraduationCap,
Gamepad2,
Sparkles, Sparkles,
Trophy, Trophy,
Compass, Compass,
ExternalLink,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useEffect, useState, useRef } from "react";
import LoadingScreen from "@/components/LoadingScreen"; import LoadingScreen from "@/components/LoadingScreen";
import { useArmToast } from "@/hooks/use-arm-toast";
export default function Foundation() { export default function Foundation() {
const navigate = useNavigate();
const { theme } = useArmTheme();
const armToast = useArmToast();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [showTldr, setShowTldr] = useState(false); const [showTldr, setShowTldr] = useState(false);
const [showExitModal, setShowExitModal] = useState(false); const [showExitModal, setShowExitModal] = useState(false);
@ -27,31 +35,14 @@ export default function Foundation() {
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
setIsLoading(false); setIsLoading(false);
if (!toastShownRef.current) {
armToast.system("Foundation network connected");
toastShownRef.current = true;
}
}, 900); }, 900);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, []); }, [armToast]);
// Countdown timer for auto-redirect
useEffect(() => {
if (isLoading) return;
const interval = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
window.location.href = "https://aethex.foundation";
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(interval);
}, [isLoading]);
const handleRedirect = () => {
window.location.href = "https://aethex.foundation";
};
// Exit intent detection // Exit intent detection
useEffect(() => { useEffect(() => {
@ -105,7 +96,7 @@ export default function Foundation() {
</div> </div>
</div> </div>
<div className="container mx-auto px-4 max-w-6xl space-y-20 py-16 lg:py-24"> <div className="container mx-auto px-4 max-w-6xl space-y-24 py-16 lg:py-24">
{/* Hero Section */} {/* Hero Section */}
<div className="text-center space-y-8 animate-slide-down"> <div className="text-center space-y-8 animate-slide-down">
<div className="flex justify-center mb-6"> <div className="flex justify-center mb-6">
@ -122,12 +113,12 @@ export default function Foundation() {
501(c)(3) Non-Profit Organization 501(c)(3) Non-Profit Organization
</Badge> </Badge>
<h1 className="text-5xl md:text-6xl lg:text-7xl font-black bg-gradient-to-r from-red-300 via-pink-300 to-red-300 bg-clip-text text-transparent"> <h1 className="text-5xl md:text-6xl lg:text-7xl font-bold bg-gradient-to-r from-red-300 via-pink-300 to-red-300 bg-clip-text text-transparent">
AeThex Foundation AeThex Foundation
</h1> </h1>
<p className="text-xl md:text-2xl text-gray-300 max-w-3xl mx-auto leading-relaxed"> <p className="text-lg md:text-xl text-gray-300 max-w-3xl mx-auto leading-relaxed">
Building community, empowering developers, and advancing game development through open-source innovation and mentorship. 501(c)(3) non-profit advancing game development
</p> </p>
{/* TL;DR Section */} {/* TL;DR Section */}
@ -177,7 +168,7 @@ export default function Foundation() {
{/* Flagship: GameForge Section */} {/* Flagship: GameForge Section */}
<Card className="bg-gradient-to-br from-green-950/40 via-emerald-950/30 to-green-950/40 border-green-500/40 overflow-hidden"> <Card className="bg-gradient-to-br from-green-950/40 via-emerald-950/30 to-green-950/40 border-green-500/40 overflow-hidden">
<CardContent className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Gamepad2 className="h-8 w-8 text-green-400" /> <Gamepad2 className="h-8 w-8 text-green-400" />
<div> <div>
@ -188,135 +179,311 @@ export default function Foundation() {
30-day mentorship sprints where developers ship real games 30-day mentorship sprints where developers ship real games
</p> </p>
</div> </div>
<Badge className="bg-red-600/50 text-red-100">
Non-Profit Guardian
</Badge>
<h1 className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-red-300 via-pink-300 to-red-300 bg-clip-text text-transparent">
AeThex Foundation
</h1>
<p className="text-xl text-gray-300 max-w-2xl mx-auto leading-relaxed">
The heart of our ecosystem. Dedicated to community, mentorship,
and advancing game development through open-source innovation.
</p>
</div> </div>
</CardHeader>
{/* Redirect Notice */} <CardContent className="space-y-6">
<div className="bg-black/40 rounded-xl p-6 border border-red-500/20 text-center space-y-4"> {/* What is GameForge? */}
<div className="flex items-center justify-center gap-2 text-red-300">
<Sparkles className="h-5 w-5" />
<span className="font-semibold">Foundation Has Moved</span>
</div>
<p className="text-gray-300">
The AeThex Foundation now has its own dedicated home. Visit our
new site for programs, resources, and community updates.
</p>
<Button
onClick={handleRedirect}
className="bg-gradient-to-r from-red-600 to-pink-600 hover:from-red-700 hover:to-pink-700 h-12 px-8 text-base"
>
<ExternalLink className="h-5 w-5 mr-2" />
Visit aethex.foundation
<ArrowRight className="h-5 w-5 ml-2" />
</Button>
<p className="text-sm text-gray-500">
Redirecting automatically in {countdown} seconds...
</p>
</div>
{/* Quick Links */}
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-lg font-semibold text-white text-center"> <h3 className="text-lg font-semibold text-white flex items-center gap-2">
Foundation Highlights <Compass className="h-5 w-5 text-green-400" />
</h2> What is GameForge?
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> </h3>
<a <p className="text-gray-300 leading-relaxed">
href="https://aethex.foundation/gameforge" GameForge is the Foundation's flagship "master-apprentice"
target="_blank" mentorship program. It's our "gym" where developers
rel="noopener noreferrer" collaborate on focused, high-impact game projects within
className="flex items-center gap-3 p-4 rounded-lg bg-black/30 border border-green-500/20 hover:border-green-500/40 transition-all group" 30-day sprints. Teams of 5 (1 mentor + 4 mentees) tackle real
> game development challenges and ship playable games to our
<div className="p-2 rounded bg-green-500/20 text-green-400"> community arcade.
<Gamepad2 className="h-5 w-5" /> </p>
</div> </div>
<div className="flex-1">
<p className="font-semibold text-white group-hover:text-green-300 transition-colors">
GameForge Program
</p>
<p className="text-sm text-gray-400">
30-day mentorship sprints
</p>
</div>
<ExternalLink className="h-4 w-4 text-gray-500 group-hover:text-green-400" />
</a>
<a {/* The Triple Win */}
href="https://aethex.foundation/mentorship" <div className="space-y-3">
target="_blank" <h3 className="text-lg font-semibold text-white flex items-center gap-2">
rel="noopener noreferrer" <Trophy className="h-5 w-5 text-green-400" />
className="flex items-center gap-3 p-4 rounded-lg bg-black/30 border border-red-500/20 hover:border-red-500/40 transition-all group" Why GameForge Matters
> </h3>
<div className="p-2 rounded bg-red-500/20 text-red-400"> <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<GraduationCap className="h-5 w-5" /> <div className="p-4 bg-black/40 rounded-lg border border-green-500/20 space-y-2">
</div> <p className="font-semibold text-green-300">
<div className="flex-1"> Role 1: Community
<p className="font-semibold text-white group-hover:text-red-300 transition-colors"> </p>
Mentorship Network <p className="text-sm text-gray-400">
</p> Our "campfire" where developers meet, collaborate, and
<p className="text-sm text-gray-400"> build their `aethex.me` passports through real project
Learn from industry veterans work.
</p> </p>
</div> </div>
<ExternalLink className="h-4 w-4 text-gray-500 group-hover:text-red-400" /> <div className="p-4 bg-black/40 rounded-lg border border-green-500/20 space-y-2">
</a> <p className="font-semibold text-green-300">
Role 2: Education
<a </p>
href="https://aethex.foundation/community" <p className="text-sm text-gray-400">
target="_blank" Learn professional development practices: Code Review
rel="noopener noreferrer" (SOP-102), Scope Management (KND-001), and shipping
className="flex items-center gap-3 p-4 rounded-lg bg-black/30 border border-blue-500/20 hover:border-blue-500/40 transition-all group" excellence.
> </p>
<div className="p-2 rounded bg-blue-500/20 text-blue-400"> </div>
<Users className="h-5 w-5" /> <div className="p-4 bg-black/40 rounded-lg border border-green-500/20 space-y-2">
</div> <p className="font-semibold text-green-300">
<div className="flex-1"> Role 3: Pipeline
<p className="font-semibold text-white group-hover:text-blue-300 transition-colors"> </p>
Community Hub <p className="text-sm text-gray-400">
</p> Top performers become "Architects" ready to work on
<p className="text-sm text-gray-400"> high-value projects. Your GameForge portfolio proves you
Connect with developers can execute.
</p> </p>
</div> </div>
<ExternalLink className="h-4 w-4 text-gray-500 group-hover:text-blue-400" />
</a>
<a
href="https://aethex.foundation/axiom"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 p-4 rounded-lg bg-black/30 border border-purple-500/20 hover:border-purple-500/40 transition-all group"
>
<div className="p-2 rounded bg-purple-500/20 text-purple-400">
<Code className="h-5 w-5" />
</div>
<div className="flex-1">
<p className="font-semibold text-white group-hover:text-purple-300 transition-colors">
Axiom Protocol
</p>
<p className="text-sm text-gray-400">
Open-source innovation
</p>
</div>
<ExternalLink className="h-4 w-4 text-gray-500 group-hover:text-purple-400" />
</a>
</div> </div>
</div> </div>
{/* Footer Note */} {/* How It Works */}
<div className="text-center pt-4 border-t border-red-500/10"> <div className="space-y-3">
<p className="text-sm text-gray-500"> <h3 className="text-lg font-semibold text-white flex items-center gap-2">
The AeThex Foundation is a 501(c)(3) non-profit organization <Zap className="h-5 w-5 text-green-400" />
dedicated to advancing game development education and community. How It Works
</h3>
<div className="space-y-2">
<div className="flex gap-3 p-3 bg-black/30 rounded-lg border border-green-500/10">
<span className="text-green-400 font-bold shrink-0">
1.
</span>
<div>
<p className="font-semibold text-white text-sm">
Join a 5-Person Team
</p>
<p className="text-xs text-gray-400 mt-0.5">
1 Forge Master (Mentor) + 4 Apprentices (Scripter,
Builder, Sound, Narrative)
</p>
</div>
</div>
<div className="flex gap-3 p-3 bg-black/30 rounded-lg border border-green-500/10">
<span className="text-green-400 font-bold shrink-0">
2.
</span>
<div>
<p className="font-semibold text-white text-sm">
Ship in 30 Days
</p>
<p className="text-xs text-gray-400 mt-0.5">
Focused sprint with a strict 1-paragraph GDD. No scope
creep. Execute with excellence.
</p>
</div>
</div>
<div className="flex gap-3 p-3 bg-black/30 rounded-lg border border-green-500/10">
<span className="text-green-400 font-bold shrink-0">
3.
</span>
<div>
<p className="font-semibold text-white text-sm">
Ship to the Arcade
</p>
<p className="text-xs text-gray-400 mt-0.5">
Your finished game goes live on aethex.fun. Add it to
your Passport portfolio.
</p>
</div>
</div>
<div className="flex gap-3 p-3 bg-black/30 rounded-lg border border-green-500/10">
<span className="text-green-400 font-bold shrink-0">
4.
</span>
<div>
<p className="font-semibold text-white text-sm">
Level Up Your Career
</p>
<p className="text-xs text-gray-400 mt-0.5">
3 shipped games = Architect status. Qualify for premium
opportunities on NEXUS.
</p>
</div>
</div>
</div>
</div>
{/* CTA Button */}
<Button
onClick={() => navigate("/gameforge")}
className="w-full bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 h-12 text-base font-semibold"
>
<Gamepad2 className="h-5 w-5 mr-2" />
Join the Next GameForge Cohort
<ArrowRight className="h-5 w-5 ml-auto" />
</Button>
</CardContent>
</Card>
{/* Foundation Mission & Values */}
<div className="space-y-4">
<h2 className="text-3xl font-bold text-white flex items-center gap-2">
<Heart className="h-8 w-8 text-red-400" />
Our Mission
</h2>
<Card className="bg-gradient-to-br from-red-950/40 to-red-900/20 border-red-500/20">
<CardContent className="p-6 space-y-4">
<p className="text-gray-300 text-lg leading-relaxed">
The AeThex Foundation is a non-profit organization dedicated
to advancing game development through community-driven
mentorship, open-source innovation, and educational
excellence. We believe that great developers are built, not
bornand that the future of gaming lies in collaboration,
transparency, and shared knowledge.
</p> </p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<h3 className="font-semibold text-red-300 flex items-center gap-2">
<Users className="h-5 w-5" />
Community is Our Core
</h3>
<p className="text-sm text-gray-400">
Building lasting relationships and support networks within
game development.
</p>
</div>
<div className="space-y-2">
<h3 className="font-semibold text-red-300 flex items-center gap-2">
<Code className="h-5 w-5" />
Open Innovation
</h3>
<p className="text-sm text-gray-400">
Advancing the industry through open-source Axiom Protocol
and shared tools.
</p>
</div>
<div className="space-y-2">
<h3 className="font-semibold text-red-300 flex items-center gap-2">
<GraduationCap className="h-5 w-5" />
Excellence & Growth
</h3>
<p className="text-sm text-gray-400">
Mentoring developers to ship real products and achieve
their potential.
</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Other Programs */}
<div className="space-y-4">
<h2 className="text-3xl font-bold text-white flex items-center gap-2">
<BookOpen className="h-8 w-8 text-red-400" />
Foundation Programs
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Mentorship Program */}
<Card className="bg-gradient-to-br from-red-950/40 to-red-900/20 border-red-500/20 hover:border-red-500/40 transition">
<CardHeader>
<CardTitle className="text-xl">Mentorship Network</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-gray-300 text-sm leading-relaxed">
Learn from industry veterans. Our mentors bring real-world
experience from studios, indie teams, and AAA development.
</p>
<Button
onClick={() => navigate("/mentorship")}
variant="outline"
className="w-full border-red-500/30 text-red-300 hover:bg-red-500/10"
>
Learn More <ArrowRight className="h-4 w-4 ml-2" />
</Button>
</CardContent>
</Card>
{/* Open Source */}
<Card className="bg-gradient-to-br from-red-950/40 to-red-900/20 border-red-500/20 hover:border-red-500/40 transition">
<CardHeader>
<CardTitle className="text-xl">Axiom Protocol</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-gray-300 text-sm leading-relaxed">
Our open-source protocol for game development. Contribute,
learn, and help shape the future of the industry.
</p>
<Button
onClick={() => navigate("/docs")}
variant="outline"
className="w-full border-red-500/30 text-red-300 hover:bg-red-500/10"
>
Explore Protocol <ArrowRight className="h-4 w-4 ml-2" />
</Button>
</CardContent>
</Card>
{/* Courses */}
<Card className="bg-gradient-to-br from-red-950/40 to-red-900/20 border-red-500/20 hover:border-red-500/40 transition">
<CardHeader>
<CardTitle className="text-xl">Learning Paths</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-gray-300 text-sm leading-relaxed">
Structured curricula covering game design, programming, art,
sound, and narrative design from basics to advanced.
</p>
<Button
onClick={() => navigate("/docs/curriculum")}
variant="outline"
className="w-full border-red-500/30 text-red-300 hover:bg-red-500/10"
>
Start Learning <ArrowRight className="h-4 w-4 ml-2" />
</Button>
</CardContent>
</Card>
{/* Community */}
<Card className="bg-gradient-to-br from-red-950/40 to-red-900/20 border-red-500/20 hover:border-red-500/40 transition">
<CardHeader>
<CardTitle className="text-xl">Community Hub</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-gray-300 text-sm leading-relaxed">
Connect with developers, share projects, get feedback, and
build lasting professional relationships.
</p>
<Button
onClick={() => navigate("/community")}
variant="outline"
className="w-full border-red-500/30 text-red-300 hover:bg-red-500/10"
>
Join Community <ArrowRight className="h-4 w-4 ml-2" />
</Button>
</CardContent>
</Card>
</div>
</div>
{/* Call to Action */}
<Card className="bg-gradient-to-r from-red-600/20 via-pink-600/10 to-red-600/20 border-red-500/40">
<CardContent className="p-12 text-center space-y-6">
<div className="space-y-2">
<h2 className="text-3xl font-bold text-white">
Ready to Join the Foundation?
</h2>
<p className="text-gray-300 text-lg">
Whether you're looking to learn, mentor others, or contribute
to open-source game development, there's a place for you here.
</p>
</div>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
onClick={() => navigate("/gameforge")}
className="bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 h-12 px-8 text-base"
>
<Gamepad2 className="h-5 w-5 mr-2" />
Join GameForge Now
</Button>
<Button
onClick={() => navigate("/login")}
variant="outline"
className="border-red-500/30 text-red-300 hover:bg-red-500/10 h-12 px-8 text-base"
>
Sign In
</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View file

@ -1,4 +1,4 @@
import GameForgeLayout from "@/components/gameforge/GameForgeLayout"; import Layout from "@/components/Layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@ -102,7 +102,7 @@ export default function GameForge() {
]; ];
return ( return (
<GameForgeLayout> <Layout>
<div className="relative min-h-screen bg-black text-white overflow-hidden"> <div className="relative min-h-screen bg-black text-white overflow-hidden">
{/* Persistent Info Banner */} {/* Persistent Info Banner */}
<div className="bg-green-500/10 border-b border-green-400/30 py-3 sticky top-0 z-50 backdrop-blur-sm"> <div className="bg-green-500/10 border-b border-green-400/30 py-3 sticky top-0 z-50 backdrop-blur-sm">
@ -154,12 +154,12 @@ export default function GameForge() {
Foundation's Game Production Studio Foundation's Game Production Studio
</Badge> </Badge>
<h1 className={`text-5xl md:text-6xl lg:text-7xl font-black text-green-300 leading-tight ${theme.fontClass}`}> <h1 className={`text-5xl md:text-6xl lg:text-7xl font-bold text-green-300 leading-tight ${theme.fontClass}`}>
Ship Games Every Month Ship Games Every Month
</h1> </h1>
<p className="text-xl md:text-2xl text-green-100/80 max-w-3xl mx-auto leading-relaxed"> <p className="text-lg md:text-xl text-green-100/80 max-w-3xl mx-auto leading-relaxed">
AeThex GameForge is a master-apprentice mentorship program where teams of 5 developers ship real games in 30-day sprints. Ship real games in 30-day sprints
</p> </p>
{/* TL;DR Section */} {/* TL;DR Section */}
@ -478,6 +478,6 @@ export default function GameForge() {
</div> </div>
</div> </div>
)} )}
</GameForgeLayout> </Layout>
); );
} }

View file

@ -132,20 +132,20 @@ export default function GetStarted() {
const platformFeatures = [ const platformFeatures = [
{ {
title: "XP & Leveling System", title: "XP & Leveling",
description: "Earn XP for daily logins, completing your profile, creating posts, and earning badges. Level up to unlock new features and recognition.", description: "Earn XP and level up to unlock features",
icon: Trophy, icon: Trophy,
color: "from-yellow-500 to-amber-600", color: "from-yellow-500 to-amber-600",
}, },
{ {
title: "AI Intelligent Agents", title: "AI Agents",
description: "Access 10 specialized AI personas for guidance on networking, game development, ethics, architecture, and more.", description: "10 specialized AI personas for guidance",
icon: Bot, icon: Bot,
color: "from-purple-500 to-violet-600", color: "from-purple-500 to-violet-600",
}, },
{ {
title: "Creator Passports", title: "Creator Passports",
description: "Build a portable profile that aggregates your achievements, verified skills, project history, and mentorship contributions.", description: "Portable profile with achievements and skills",
icon: IdCard, icon: IdCard,
color: "from-cyan-500 to-blue-600", color: "from-cyan-500 to-blue-600",
}, },

View file

@ -1,3 +1,4 @@
import { useState } from "react";
import SEO from "@/components/SEO"; import SEO from "@/components/SEO";
import Layout from "@/components/Layout"; import Layout from "@/components/Layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -26,38 +27,50 @@ const ecosystemPillars = [
{ {
icon: Boxes, icon: Boxes,
title: "Six Realms", title: "Six Realms",
description: "Nexus, GameForge, Foundation, Labs, Corp, and Staff — each with unique APIs and capabilities", description: "Specialized APIs for every use case",
href: "/realms", href: "/realms",
gradient: "from-purple-500 via-purple-600 to-indigo-600",
accentColor: "hsl(var(--primary))",
}, },
{ {
icon: Database, icon: Database,
title: "Developer APIs", title: "Developer APIs",
description: "Comprehensive REST APIs for users, content, achievements, and more", description: "REST APIs for all platforms",
href: "/dev-platform/api-reference", href: "/dev-platform/api-reference",
gradient: "from-blue-500 via-blue-600 to-cyan-600",
accentColor: "hsl(var(--primary))",
}, },
{ {
icon: Terminal, icon: Terminal,
title: "SDK & Tools", title: "SDK & Tools",
description: "TypeScript SDK, CLI tools, and pre-built templates to ship faster", description: "Ship faster with TypeScript SDK",
href: "/dev-platform/quick-start", href: "/dev-platform/quick-start",
gradient: "from-cyan-500 via-teal-600 to-emerald-600",
accentColor: "hsl(var(--primary))",
}, },
{ {
icon: Layers, icon: Layers,
title: "Marketplace", title: "Marketplace",
description: "Premium integrations, plugins, and components from the community", description: "Premium plugins & integrations",
href: "/dev-platform/marketplace", href: "/dev-platform/marketplace",
gradient: "from-emerald-500 via-green-600 to-lime-600",
accentColor: "hsl(var(--primary))",
}, },
{ {
icon: Users, icon: Users,
title: "Community", title: "Community",
description: "Join 12,000+ developers building on AeThex", description: "12K+ developers building together",
href: "/community", href: "/community",
gradient: "from-amber-500 via-orange-600 to-red-600",
accentColor: "hsl(var(--primary))",
}, },
{ {
icon: Trophy, icon: Trophy,
title: "Opportunities", title: "Opportunities",
description: "Get paid to build — contracts, bounties, and commissions", description: "Get paid to build",
href: "/opportunities", href: "/opportunities",
gradient: "from-pink-500 via-rose-600 to-red-600",
accentColor: "hsl(var(--primary))",
}, },
]; ];
@ -71,40 +84,39 @@ const stats = [
const features = [ const features = [
{ {
icon: Layers, icon: Layers,
title: "Cross-Platform Integration Layer", title: "Cross-Platform Integration",
description: "One unified API to build across Roblox, VRChat, RecRoom, Spatial, Decentraland, The Sandbox, Minecraft, Meta Horizon, Fortnite, and Zepeto — no more managing separate platform SDKs", description: "One API for all metaverse platforms",
}, },
{ {
icon: Code2, icon: Code2,
title: "Enterprise-Grade Developer Tools", title: "Enterprise Developer Tools",
description: "TypeScript SDK, REST APIs, unified authentication, cross-platform achievements, content delivery, and CLI tools — all integrated and production-ready", description: "Production-ready SDK and APIs",
}, },
{ {
icon: Gamepad2, icon: Gamepad2,
title: "Six Specialized Realms", title: "Six Specialized Realms",
description: "Nexus (social hub), GameForge (games), Foundation (education), Labs (AI/innovation), Corp (business), Staff (governance) — each with unique APIs and tools", description: "Unique APIs for every use case",
}, },
{ {
icon: Trophy, icon: Trophy,
title: "Monetize Your Skills", title: "Monetize Your Skills",
description: "Get paid to build — access contracts, bounties, and commissions. 12K+ developers earning while creating cross-platform games, apps, and integrations", description: "12K+ developers earning on AeThex",
}, },
{ {
icon: Users, icon: Users,
title: "Thriving Creator Economy", title: "Creator Economy",
description: "Join squads, collaborate on projects, share assets in the marketplace, and grow your reputation across all six realms", description: "Collaborate and grow your reputation",
}, },
{ {
icon: Rocket, icon: Rocket,
title: "Ship Everywhere, Fast", title: "Ship Fast",
description: "150+ cross-platform code examples, pre-built templates, OAuth integration, Supabase backend — one-command deployment to every metaverse", description: "150+ examples and one-click deployment",
}, },
]; ];
const platforms = ["Roblox", "Minecraft", "Meta Horizon", "Fortnite", "VRChat", "Zepeto"];
const platformIcons = [Gamepad2, Boxes, Globe, Zap, Users, Sparkles];
export default function Index() { export default function Index() {
const [hoveredCard, setHoveredCard] = useState<number | null>(null);
return ( return (
<Layout hideFooter> <Layout hideFooter>
<SEO <SEO
@ -117,154 +129,308 @@ export default function Index() {
} }
/> />
{/* Static background — radial glow only; grid/scanlines come from body::after/::before in global.css */} {/* Animated Background */}
<div className="fixed inset-0 pointer-events-none overflow-hidden"> <div className="fixed inset-0 pointer-events-none overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_70%_40%_at_50%_-10%,hsl(var(--primary)/0.08),transparent)]" /> <motion.div
className="absolute w-[600px] h-[600px] rounded-full blur-[100px] opacity-15 bg-primary/30"
style={{
left: '10%',
top: '20%',
}}
animate={{
x: [0, 50, 0],
y: [0, -50, 0],
}}
transition={{
duration: 20,
repeat: Infinity,
ease: "easeInOut",
}}
/>
<motion.div
className="absolute w-[600px] h-[600px] rounded-full blur-[128px] opacity-20 bg-primary/40"
style={{
right: -100,
top: 200,
}}
animate={{
x: [0, -30, 0],
y: [0, 40, 0],
}}
transition={{
duration: 15,
repeat: Infinity,
ease: "easeInOut",
}}
/>
<motion.div
className="absolute w-[700px] h-[700px] rounded-full blur-[128px] opacity-15 bg-primary/35"
style={{
left: -100,
bottom: -100,
}}
animate={{
x: [0, 40, 0],
y: [0, -40, 0],
}}
transition={{
duration: 18,
repeat: Infinity,
ease: "easeInOut",
}}
/>
{/* Cyber Grid */}
<div
className="absolute inset-0 opacity-[0.03]"
style={{
backgroundImage: `
linear-gradient(to right, hsl(var(--primary)) 1px, transparent 1px),
linear-gradient(to bottom, hsl(var(--primary)) 1px, transparent 1px)
`,
backgroundSize: "60px 60px",
}}
/>
{/* Scanlines */}
<div
className="absolute inset-0 opacity-[0.03] pointer-events-none"
style={{
backgroundImage: "repeating-linear-gradient(0deg, transparent, transparent 2px, hsl(var(--primary) / 0.1) 2px, hsl(var(--primary) / 0.1) 4px)",
}}
/>
{/* Corner Accents */}
<div className="absolute top-0 left-0 w-64 h-64 border-t-2 border-l-2 border-primary/30" />
<div className="absolute top-0 right-0 w-64 h-64 border-t-2 border-r-2 border-primary/30" />
<div className="absolute bottom-0 left-0 w-64 h-64 border-b-2 border-l-2 border-primary/30" />
<div className="absolute bottom-0 right-0 w-64 h-64 border-b-2 border-r-2 border-primary/30" />
</div> </div>
<div className="relative space-y-28 pb-28"> <div className="relative space-y-40 pb-40">
<section className="relative min-h-[90vh] flex items-center justify-center overflow-hidden pt-20">
{/* Hero */} <div className="relative text-center max-w-6xl mx-auto space-y-10 px-4">
<section className="relative min-h-[88vh] flex items-center justify-center pt-20">
<div className="relative text-center max-w-5xl mx-auto space-y-8 px-4">
<motion.div <motion.div
initial={{ opacity: 0, y: -12 }} initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.8 }}
> >
<Badge className="text-xs px-4 py-1.5 bg-primary/10 border-primary/30 uppercase tracking-widest font-semibold"> <Badge
<Sparkles className="w-3 h-3 mr-1.5 inline" /> className="text-sm px-6 py-2 backdrop-blur-xl bg-primary/10 border-primary/50 shadow-[0_0_30px_rgba(168,85,247,0.4)] hover:shadow-[0_0_50px_rgba(168,85,247,0.6)] transition-all uppercase tracking-wider font-bold"
>
<Sparkles className="w-4 h-4 mr-2 inline animate-pulse" />
AeThex Developer Ecosystem AeThex Developer Ecosystem
</Badge> </Badge>
</motion.div> </motion.div>
<motion.h1
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-6xl md:text-7xl lg:text-8xl font-black tracking-tight leading-none"
>
Build on{" "}
<span className="text-primary">AeThex</span>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-lg md:text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed"
>
The <span className="text-foreground font-medium">integration layer</span> connecting all metaverse platforms.
Six specialized realms. <span className="text-foreground font-medium">12K+ developers</span>. One powerful ecosystem.
</motion.p>
{/* Platform pills */}
<motion.div <motion.div
initial={{ opacity: 0, y: 16 }} initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6, delay: 0.3 }} transition={{ duration: 0.8, delay: 0.2 }}
className="flex flex-wrap items-center justify-center gap-2 pt-2"
> >
{platforms.map((name, i) => { <h1 className="text-5xl md:text-6xl lg:text-7xl font-black tracking-tight leading-none">
const Icon = platformIcons[i]; Build on
return ( <br />
<div <span className="relative inline-block mt-4">
key={name} <span className="relative z-10 text-primary drop-shadow-[0_0_25px_rgba(168,85,247,0.8)]" style={{ textShadow: '0 0 40px rgba(168, 85, 247, 0.6)' }}>
className="flex items-center gap-1.5 bg-secondary/60 px-3 py-1.5 rounded-full border border-border text-sm text-muted-foreground" AeThex
> </span>
<Icon className="w-3.5 h-3.5 text-primary" /> <motion.div
<span className="font-medium">{name}</span> className="absolute -inset-8 bg-primary blur-3xl opacity-40"
</div> animate={{
); opacity: [0.4, 0.7, 0.4],
})} scale: [1, 1.1, 1],
<div className="flex items-center gap-1.5 bg-primary/10 px-3 py-1.5 rounded-full border border-primary/20 text-sm text-primary font-medium"> }}
& More transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
/>
</span>
</h1>
</motion.div>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
className="text-xl md:text-2xl text-muted-foreground max-w-4xl mx-auto leading-relaxed font-light"
>
The <span className="text-primary font-bold">integration layer</span> connecting all metaverse platforms.
<br className="hidden md:block" />
Six specialized realms. <span className="text-primary font-semibold">12K+ developers</span>. One powerful ecosystem.
</motion.p>
{/* Platform Highlights */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5 }}
className="flex flex-wrap items-center justify-center gap-3 pt-4 text-sm md:text-base max-w-4xl mx-auto"
>
<div className="flex items-center gap-2 backdrop-blur-xl bg-primary/5 px-4 py-2 rounded-full border-2 border-primary/30 hover:border-primary/60 hover:bg-primary/10 hover:shadow-[0_0_20px_rgba(168,85,247,0.3)] transition-all">
<Gamepad2 className="w-4 h-4 text-primary drop-shadow-[0_0_8px_rgba(168,85,247,0.8)]" />
<span className="text-foreground/90 font-bold uppercase tracking-wide">Roblox</span>
</div>
<div className="flex items-center gap-2 backdrop-blur-xl bg-primary/5 px-4 py-2 rounded-full border-2 border-primary/30 hover:border-primary/60 hover:bg-primary/10 hover:shadow-[0_0_20px_rgba(168,85,247,0.3)] transition-all">
<Boxes className="w-4 h-4 text-primary drop-shadow-[0_0_8px_rgba(168,85,247,0.8)]" />
<span className="text-foreground/90 font-bold uppercase tracking-wide">Minecraft</span>
</div>
<div className="flex items-center gap-2 backdrop-blur-xl bg-primary/5 px-4 py-2 rounded-full border-2 border-primary/30 hover:border-primary/60 hover:bg-primary/10 hover:shadow-[0_0_20px_rgba(168,85,247,0.3)] transition-all">
<Globe className="w-4 h-4 text-primary drop-shadow-[0_0_8px_rgba(168,85,247,0.8)]" />
<span className="text-foreground/90 font-bold uppercase tracking-wide">Meta Horizon</span>
</div>
<div className="flex items-center gap-2 backdrop-blur-xl bg-primary/5 px-4 py-2 rounded-full border-2 border-primary/30 hover:border-primary/60 hover:bg-primary/10 hover:shadow-[0_0_20px_rgba(168,85,247,0.3)] transition-all">
<Zap className="w-4 h-4 text-primary drop-shadow-[0_0_8px_rgba(168,85,247,0.8)]" />
<span className="text-foreground/90 font-bold uppercase tracking-wide">Fortnite</span>
</div>
<div className="flex items-center gap-2 backdrop-blur-xl bg-primary/5 px-4 py-2 rounded-full border-2 border-primary/30 hover:border-primary/60 hover:bg-primary/10 hover:shadow-[0_0_20px_rgba(168,85,247,0.3)] transition-all">
<Users className="w-4 h-4 text-primary drop-shadow-[0_0_8px_rgba(168,85,247,0.8)]" />
<span className="text-foreground/90 font-bold uppercase tracking-wide">Zepeto</span>
</div>
<div className="flex items-center gap-2 backdrop-blur-xl bg-primary/10 px-4 py-2 rounded-full border-2 border-primary/40 shadow-[0_0_20px_rgba(168,85,247,0.4)]">
<Sparkles className="w-4 h-4 text-primary drop-shadow-[0_0_8px_rgba(168,85,247,0.8)] animate-pulse" />
<span className="text-foreground/90 font-black uppercase tracking-wide">& More</span>
</div> </div>
</motion.div> </motion.div>
{/* CTAs */}
<motion.div <motion.div
initial={{ opacity: 0, y: 16 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }} transition={{ duration: 0.8, delay: 0.6 }}
className="flex flex-wrap gap-3 justify-center pt-4" className="flex flex-wrap gap-4 justify-center pt-8"
> >
<Link to="/dev-platform/quick-start"> <Link to="/dev-platform/quick-start">
<Button size="lg" className="px-8 h-12 font-semibold"> <Button
size="lg"
className="text-base px-8 h-12 bg-primary hover:bg-primary/90 shadow-[0_0_40px_rgba(168,85,247,0.6)] hover:shadow-[0_0_60px_rgba(168,85,247,0.8)] hover:scale-105 transition-all duration-300 font-bold uppercase tracking-wide border-2 border-primary/50"
>
Start Building Start Building
<Rocket className="w-4 h-4 ml-2" /> <Rocket className="w-5 h-5 ml-2" />
</Button> </Button>
</Link> </Link>
<Link to="/dev-platform/api-reference"> <Link to="/dev-platform/api-reference">
<Button size="lg" variant="outline" className="px-8 h-12 font-semibold"> <Button
<BookOpen className="w-4 h-4 mr-2" /> size="lg"
variant="outline"
className="text-base px-8 h-12 backdrop-blur-xl bg-background/50 border-2 border-primary/40 hover:bg-primary/10 hover:border-primary/60 shadow-[0_0_20px_rgba(168,85,247,0.3)] hover:shadow-[0_0_40px_rgba(168,85,247,0.5)] hover:scale-105 transition-all duration-300 font-bold uppercase tracking-wide"
>
<BookOpen className="w-5 h-5 mr-2" />
Explore APIs Explore APIs
</Button> </Button>
</Link> </Link>
</motion.div> </motion.div>
{/* Stats */}
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.6 }} transition={{ duration: 1, delay: 0.8 }}
className="grid grid-cols-2 md:grid-cols-4 gap-4 pt-12 max-w-3xl mx-auto" className="grid grid-cols-2 md:grid-cols-4 gap-8 pt-16 max-w-4xl mx-auto"
> >
{stats.map((stat) => ( {stats.map((stat, i) => (
<div <motion.div
key={stat.label} key={stat.label}
className="bg-secondary/40 border border-border rounded-xl p-5 text-center" initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.8 + i * 0.1 }}
className="relative group"
> >
<p className="text-3xl font-black text-primary">{stat.value}</p> <div className="relative backdrop-blur-xl bg-background/30 border border-primary/20 rounded-2xl p-6 hover:border-primary/40 transition-all duration-300 hover:scale-105">
<p className="text-xs text-muted-foreground mt-1 font-medium uppercase tracking-wide">{stat.label}</p> <p className="text-4xl md:text-5xl font-black text-primary mb-2">
</div> {stat.value}
</p>
<p className="text-sm text-muted-foreground font-medium">{stat.label}</p>
<div className="absolute inset-0 bg-primary/0 group-hover:bg-primary/5 rounded-2xl transition-all duration-300" />
</div>
</motion.div>
))} ))}
</motion.div> </motion.div>
</div> </div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1, y: [0, 10, 0] }}
transition={{
opacity: { delay: 1.5, duration: 0.5 },
y: { duration: 2, repeat: Infinity, ease: "easeInOut" },
}}
className="absolute bottom-8 left-1/2 transform -translate-x-1/2"
>
<div className="w-6 h-10 border-2 border-primary/30 rounded-full flex items-start justify-center p-2">
<motion.div
className="w-1 h-2 bg-primary rounded-full"
animate={{ y: [0, 12, 0] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
/>
</div>
</motion.div>
</section> </section>
{/* Ecosystem Pillars */} <section className="space-y-20 px-4">
<section className="space-y-10 px-4">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.6 }} transition={{ duration: 0.8 }}
className="text-center space-y-3" className="text-center space-y-6"
> >
<h2 className="text-4xl md:text-5xl font-black">The AeThex Ecosystem</h2> <h2 className="text-5xl md:text-6xl font-black text-primary">
<p className="text-muted-foreground max-w-2xl mx-auto"> The AeThex Ecosystem
Six interconnected realms, each with unique capabilities and APIs to power your applications </h2>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
Six interconnected realms with unique APIs
</p> </p>
</motion.div> </motion.div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4 max-w-6xl mx-auto"> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
{ecosystemPillars.map((pillar, index) => ( {ecosystemPillars.map((pillar, index) => (
<motion.div <motion.div
key={pillar.title} key={pillar.title}
initial={{ opacity: 0, y: 24 }} initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.07 }} transition={{ duration: 0.6, delay: index * 0.1 }}
onMouseEnter={() => setHoveredCard(index)}
onMouseLeave={() => setHoveredCard(null)}
> >
<Link to={pillar.href}> <Link to={pillar.href}>
<Card className="group h-full border-border hover:border-primary/30 transition-colors duration-200 bg-card"> <Card className="group relative overflow-hidden h-full border-2 hover:border-transparent transition-all duration-300">
<div className="p-6 space-y-4"> <div
<div className="w-11 h-11 rounded-lg bg-primary/10 border border-primary/20 flex items-center justify-center group-hover:bg-primary/15 transition-colors"> className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-primary/10"
<pillar.icon className="w-5 h-5 text-primary" /> />
{hoveredCard === index && (
<motion.div
className="absolute inset-0 blur-xl opacity-30 bg-primary"
initial={{ opacity: 0 }}
animate={{ opacity: 0.3 }}
exit={{ opacity: 0 }}
/>
)}
<div className="relative p-8 space-y-4 backdrop-blur-sm">
<div
className={`w-16 h-16 rounded-2xl bg-gradient-to-br ${pillar.gradient} flex items-center justify-center shadow-2xl group-hover:scale-110 transition-transform duration-300`}
style={{
boxShadow: `0 20px 40px hsl(var(--primary) / 0.4)`,
}}
>
<pillar.icon className="w-8 h-8 text-white" />
</div> </div>
<div className="space-y-1.5">
<h3 className="font-semibold text-foreground group-hover:text-primary transition-colors"> <div className="space-y-2">
<h3 className="text-2xl font-bold group-hover:text-primary transition-all duration-300">
{pillar.title} {pillar.title}
</h3> </h3>
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-muted-foreground leading-relaxed">
{pillar.description} {pillar.description}
</p> </p>
</div> </div>
<div className="flex items-center text-primary/70 group-hover:text-primary group-hover:translate-x-1 transition-all duration-200 text-sm">
<span className="font-medium mr-1">Explore</span> <div className="flex items-center text-primary group-hover:translate-x-2 transition-transform duration-300">
<ArrowRight className="w-3.5 h-3.5" /> <span className="text-sm font-medium mr-2">Explore</span>
<ArrowRight className="w-4 h-4" />
</div> </div>
</div> </div>
</Card> </Card>
@ -274,37 +440,38 @@ export default function Index() {
</div> </div>
</section> </section>
{/* Why AeThex */} <section className="space-y-20 px-4">
<section className="space-y-10 px-4">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.6 }} transition={{ duration: 0.8 }}
className="text-center space-y-3" className="text-center space-y-6"
> >
<h2 className="text-4xl md:text-5xl font-black">Why Build on AeThex?</h2> <h2 className="text-5xl md:text-6xl font-black text-primary">
<p className="text-muted-foreground max-w-2xl mx-auto"> Why Build on AeThex?
Join a growing ecosystem designed for creators, developers, and entrepreneurs </h2>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
Built for creators and developers
</p> </p>
</motion.div> </motion.div>
<div className="grid md:grid-cols-3 gap-4 max-w-6xl mx-auto"> <div className="grid md:grid-cols-3 gap-10 max-w-6xl mx-auto">
{features.map((feature, index) => ( {features.map((feature, index) => (
<motion.div <motion.div
key={feature.title} key={feature.title}
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.08 }} transition={{ duration: 0.6, delay: index * 0.2 }}
> >
<Card className="p-6 space-y-4 border-border bg-card h-full"> <Card className="p-10 space-y-8 backdrop-blur-xl bg-background/50 border-primary/20 hover:border-primary/40 hover:scale-105 transition-all duration-300 h-full">
<div className="w-10 h-10 rounded-lg bg-primary/10 border border-primary/20 flex items-center justify-center"> <div className="w-16 h-16 rounded-2xl bg-primary flex items-center justify-center shadow-2xl shadow-primary/50">
<feature.icon className="w-5 h-5 text-primary" /> <feature.icon className="w-8 h-8 text-primary-foreground" />
</div> </div>
<div className="space-y-2"> <div className="space-y-4">
<h3 className="font-semibold text-foreground">{feature.title}</h3> <h3 className="text-2xl font-bold">{feature.title}</h3>
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-lg text-muted-foreground">
{feature.description} {feature.description}
</p> </p>
</div> </div>
@ -314,47 +481,108 @@ export default function Index() {
</div> </div>
</section> </section>
{/* CTA */}
<section className="px-4"> <section className="px-4">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.6 }} transition={{ duration: 0.8 }}
className="relative overflow-hidden rounded-2xl max-w-5xl mx-auto border border-primary/20 bg-primary/5" className="relative overflow-hidden rounded-3xl max-w-6xl mx-auto border-2 border-primary/40"
> >
<div className="absolute inset-0 bg-[radial-gradient(ellipse_60%_80%_at_80%_50%,hsl(var(--primary)/0.08),transparent)]" /> {/* Animated Background */}
<div className="relative z-10 p-12 md:p-16 text-center space-y-6"> <div className="absolute inset-0 bg-gradient-to-br from-primary/20 via-primary/10 to-background/50 backdrop-blur-xl" />
<Badge className="text-xs px-4 py-1.5 bg-primary/10 border-primary/30 uppercase tracking-widest font-semibold">
<Terminal className="w-3 h-3 mr-1.5 inline" /> {/* Animated Grid */}
Start Building Today <div
</Badge> className="absolute inset-0 opacity-[0.05]"
<h2 className="text-3xl md:text-4xl lg:text-5xl font-black leading-tight"> style={{
Ready to Build Something{" "} backgroundImage: `
<span className="text-primary">Epic?</span> linear-gradient(to right, hsl(var(--primary)) 1px, transparent 1px),
</h2> linear-gradient(to bottom, hsl(var(--primary)) 1px, transparent 1px)
<p className="text-muted-foreground max-w-2xl mx-auto"> `,
Get your API key and start deploying across{" "} backgroundSize: "40px 40px",
<span className="text-foreground font-medium">5+ metaverse platforms</span> in minutes }}
</p> />
<div className="flex flex-wrap gap-3 justify-center pt-2">
{/* Glowing Orb */}
<motion.div
className="absolute top-0 right-0 w-96 h-96 rounded-full bg-primary/30 blur-[120px]"
animate={{
scale: [1, 1.2, 1],
opacity: [0.3, 0.5, 0.3],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut",
}}
/>
<div className="relative z-10 p-12 md:p-20 text-center space-y-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<Badge className="text-sm px-6 py-2 bg-primary/20 border-2 border-primary/50 shadow-[0_0_30px_rgba(168,85,247,0.4)] uppercase tracking-wider font-bold mb-6">
<Terminal className="w-4 h-4 mr-2 inline" />
Start Building Today
</Badge>
</motion.div>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.3 }}
className="text-4xl md:text-5xl lg:text-6xl font-black leading-tight"
>
Ready to Build Something
<br />
<span className="text-primary drop-shadow-[0_0_30px_rgba(168,85,247,0.6)]">Epic?</span>
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}
className="text-xl md:text-2xl text-muted-foreground max-w-3xl mx-auto font-light"
>
Get your API key and start deploying across <span className="text-primary font-semibold">5+ metaverse platforms</span> in minutes
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.5 }}
className="flex flex-wrap gap-4 justify-center pt-6"
>
<Link to="/dev-platform/dashboard"> <Link to="/dev-platform/dashboard">
<Button size="lg" className="px-8 h-12 font-semibold"> <Button
size="lg"
className="text-base px-8 h-12 bg-primary hover:bg-primary/90 shadow-[0_0_40px_rgba(168,85,247,0.6)] hover:shadow-[0_0_60px_rgba(168,85,247,0.8)] hover:scale-105 transition-all duration-300 font-bold uppercase tracking-wide border-2 border-primary/50"
>
Get Your API Key Get Your API Key
<ArrowRight className="w-4 h-4 ml-2" /> <ArrowRight className="w-5 h-5 ml-2" />
</Button> </Button>
</Link> </Link>
<Link to="/realms"> <Link to="/realms">
<Button size="lg" variant="outline" className="px-8 h-12 font-semibold"> <Button
size="lg"
variant="outline"
className="text-base px-8 h-12 backdrop-blur-xl bg-background/50 border-2 border-primary/40 hover:bg-primary/10 hover:border-primary/60 shadow-[0_0_20px_rgba(168,85,247,0.3)] hover:shadow-[0_0_40px_rgba(168,85,247,0.5)] hover:scale-105 transition-all duration-300 font-bold uppercase tracking-wide"
>
Explore Realms Explore Realms
<Boxes className="w-4 h-4 ml-2" /> <Boxes className="w-5 h-5 ml-2" />
</Button> </Button>
</Link> </Link>
</div> </motion.div>
</div> </div>
</motion.div> </motion.div>
</section> </section>
</div> </div>
</Layout> </Layout>
); );

View file

@ -0,0 +1,418 @@
import { useState } from "react";
import SEO from "@/components/SEO";
import Layout from "@/components/Layout";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Link } from "react-router-dom";
import {
ArrowRight,
Terminal,
Copy,
Check,
BookOpen,
Zap,
Shield,
Globe,
Code2,
Database,
Users,
Boxes,
Layers,
Trophy,
Gamepad2,
} from "lucide-react";
const codeExample = `import { AeThex } from '@aethex/sdk';
const client = new AeThex({ apiKey: process.env.AETHEX_KEY });
// Authenticate user across platforms
const user = await client.passport.authenticate({
platform: 'roblox',
userId: '123456789'
});
// Sync achievements, inventory, progress
await client.sync({
achievements: user.achievements,
inventory: user.inventory,
progress: user.gameProgress
});`;
const ecosystemPillars = [
{
icon: Boxes,
title: "Six Realms",
description: "Specialized APIs for every use case",
href: "/realms",
gradient: "from-purple-500 to-indigo-600",
},
{
icon: Database,
title: "Developer APIs",
description: "REST APIs for all platforms",
href: "/dev-platform/api-reference",
gradient: "from-blue-500 to-cyan-600",
},
{
icon: Terminal,
title: "SDK & Tools",
description: "Ship faster with TypeScript SDK",
href: "/dev-platform/quick-start",
gradient: "from-cyan-500 to-emerald-600",
},
{
icon: Layers,
title: "Marketplace",
description: "Premium plugins & integrations",
href: "/dev-platform/marketplace",
gradient: "from-emerald-500 to-lime-600",
},
{
icon: Users,
title: "Community",
description: "12K+ developers building together",
href: "/community",
gradient: "from-amber-500 to-red-600",
},
{
icon: Trophy,
title: "Opportunities",
description: "Get paid to build",
href: "/opportunities",
gradient: "from-pink-500 to-red-600",
},
];
const stats = [
{ value: "12K+", label: "Developers" },
{ value: "2.5M+", label: "API Calls/Day" },
{ value: "150+", label: "Examples" },
{ value: "6", label: "Realms" },
];
const features = [
{
icon: Globe,
title: "Cross-Platform Identity",
description: "One passport across Roblox, Minecraft, Fortnite, and more.",
},
{
icon: Database,
title: "Universal Data Sync",
description: "Sync achievements, inventory, and progress with a single API.",
},
{
icon: Shield,
title: "Enterprise Auth",
description: "OAuth 2.0, PKCE, JWT. Production-ready out of the box.",
},
{
icon: Gamepad2,
title: "Game Integration",
description: "Drop-in SDKs for Roblox, Unity, Unreal, and more.",
},
];
const platforms = ["Roblox", "Minecraft", "Fortnite", "Meta Horizon", "Zepeto", "Unity", "Unreal"];
export default function Index() {
const [copied, setCopied] = useState(false);
const copyCode = () => {
navigator.clipboard.writeText(codeExample);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<Layout hideFooter>
<SEO
pageTitle="AeThex | Developer Platform"
description="Build cross-platform experiences with the AeThex SDK. One API for identity, data sync, and authentication across metaverse platforms."
canonical={typeof window !== "undefined" ? window.location.href : undefined}
/>
{/* Subtle background */}
<div className="fixed inset-0 pointer-events-none overflow-hidden -z-10">
<div className="absolute top-0 right-0 w-[600px] h-[600px] rounded-full bg-primary/5 blur-3xl" />
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] rounded-full bg-primary/5 blur-3xl" />
<div
className="absolute inset-0 opacity-[0.02]"
style={{
backgroundImage: `linear-gradient(to right, hsl(var(--primary)) 1px, transparent 1px),
linear-gradient(to bottom, hsl(var(--primary)) 1px, transparent 1px)`,
backgroundSize: "60px 60px",
}}
/>
</div>
<div className="relative min-h-screen">
{/* Hero */}
<section className="relative pt-20 pb-16 px-4">
<div className="max-w-6xl mx-auto">
<div className="grid lg:grid-cols-2 gap-12 items-center">
{/* Left: Value Prop */}
<div className="space-y-6">
<Badge variant="outline" className="text-xs font-mono border-primary/30">
v2.4.0 — TypeScript SDK
</Badge>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight">
One API for
<br />
<span className="text-primary">cross-platform games</span>
</h1>
<p className="text-lg text-muted-foreground max-w-lg">
Connect players across Roblox, Minecraft, Fortnite, and more.
Sync identity, achievements, and inventory with a single SDK.
</p>
<div className="flex flex-wrap gap-3 pt-2">
<Link to="/dev-platform/quick-start">
<Button size="lg" className="font-medium shadow-lg shadow-primary/25">
Get Started
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</Link>
<Link to="/dev-platform/api-reference">
<Button size="lg" variant="outline" className="font-medium">
<BookOpen className="w-4 h-4 mr-2" />
Read Docs
</Button>
</Link>
</div>
{/* Install command */}
<div className="flex items-center gap-2 pt-4">
<code className="flex-1 bg-muted px-4 py-2.5 rounded-lg font-mono text-sm border">
npm install @aethex/sdk
</code>
<Button
size="sm"
variant="ghost"
onClick={() => navigator.clipboard.writeText("npm install @aethex/sdk")}
>
<Copy className="w-4 h-4" />
</Button>
</div>
{/* Stats row */}
<div className="grid grid-cols-4 gap-4 pt-6">
{stats.map((stat) => (
<div key={stat.label} className="text-center">
<p className="text-2xl md:text-3xl font-bold text-primary">{stat.value}</p>
<p className="text-xs text-muted-foreground">{stat.label}</p>
</div>
))}
</div>
</div>
{/* Right: Code Example */}
<div className="relative">
<Card className="bg-zinc-950 border-zinc-800 overflow-hidden shadow-2xl">
<div className="flex items-center justify-between px-4 py-2 border-b border-zinc-800">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-500/80" />
<div className="w-3 h-3 rounded-full bg-yellow-500/80" />
<div className="w-3 h-3 rounded-full bg-green-500/80" />
</div>
<span className="text-xs text-zinc-500 font-mono">app.ts</span>
<Button
size="sm"
variant="ghost"
className="h-6 text-zinc-400 hover:text-white"
onClick={copyCode}
>
{copied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
</Button>
</div>
<pre className="p-4 text-sm overflow-x-auto">
<code className="text-zinc-300 font-mono whitespace-pre-wrap">
<span className="text-purple-400">import</span>
<span className="text-zinc-300"> {"{"} </span>
<span className="text-yellow-300">AeThex</span>
<span className="text-zinc-300"> {"}"} </span>
<span className="text-purple-400">from</span>
<span className="text-green-400"> '@aethex/sdk'</span>
<span className="text-zinc-300">;</span>
{"\n\n"}
<span className="text-purple-400">const</span>
<span className="text-blue-300"> client</span>
<span className="text-zinc-300"> = </span>
<span className="text-purple-400">new</span>
<span className="text-yellow-300"> AeThex</span>
<span className="text-zinc-300">{"({ "}</span>
<span className="text-blue-300">apiKey</span>
<span className="text-zinc-300">: </span>
<span className="text-blue-300">process.env.</span>
<span className="text-zinc-300">AETHEX_KEY {"});"}</span>
{"\n\n"}
<span className="text-zinc-600">// Authenticate user across platforms</span>
{"\n"}
<span className="text-purple-400">const</span>
<span className="text-blue-300"> user</span>
<span className="text-zinc-300"> = </span>
<span className="text-purple-400">await</span>
<span className="text-blue-300"> client</span>
<span className="text-zinc-300">.passport.</span>
<span className="text-yellow-300">authenticate</span>
<span className="text-zinc-300">{"({"}</span>
{"\n"}
<span className="text-zinc-300">{" "}</span>
<span className="text-blue-300">platform</span>
<span className="text-zinc-300">: </span>
<span className="text-green-400">'roblox'</span>
<span className="text-zinc-300">,</span>
{"\n"}
<span className="text-zinc-300">{" "}</span>
<span className="text-blue-300">userId</span>
<span className="text-zinc-300">: </span>
<span className="text-green-400">'123456789'</span>
{"\n"}
<span className="text-zinc-300">{"});"}</span>
{"\n\n"}
<span className="text-zinc-600">// Sync achievements, inventory, progress</span>
{"\n"}
<span className="text-purple-400">await</span>
<span className="text-blue-300"> client</span>
<span className="text-zinc-300">.</span>
<span className="text-yellow-300">sync</span>
<span className="text-zinc-300">{"({"}</span>
{"\n"}
<span className="text-zinc-300">{" "}</span>
<span className="text-blue-300">achievements</span>
<span className="text-zinc-300">: </span>
<span className="text-blue-300">user</span>
<span className="text-zinc-300">.achievements,</span>
{"\n"}
<span className="text-zinc-300">{" "}</span>
<span className="text-blue-300">inventory</span>
<span className="text-zinc-300">: </span>
<span className="text-blue-300">user</span>
<span className="text-zinc-300">.inventory,</span>
{"\n"}
<span className="text-zinc-300">{" "}</span>
<span className="text-blue-300">progress</span>
<span className="text-zinc-300">: </span>
<span className="text-blue-300">user</span>
<span className="text-zinc-300">.gameProgress</span>
{"\n"}
<span className="text-zinc-300">{"});"}</span>
</code>
</pre>
</Card>
</div>
</div>
</div>
</section>
{/* Platforms */}
<section className="py-6 px-4 border-y border-border/30">
<div className="max-w-6xl mx-auto">
<div className="flex flex-wrap items-center justify-center gap-6 md:gap-10 text-muted-foreground">
<span className="text-sm font-medium">Works with:</span>
{platforms.map((platform) => (
<span key={platform} className="text-sm font-medium hover:text-foreground transition-colors cursor-default">
{platform}
</span>
))}
</div>
</div>
</section>
{/* Ecosystem Pillars */}
<section className="py-20 px-4">
<div className="max-w-6xl mx-auto space-y-12">
<div className="text-center space-y-4">
<h2 className="text-3xl md:text-4xl font-bold">The AeThex Ecosystem</h2>
<p className="text-muted-foreground max-w-2xl mx-auto">
Six interconnected realms with specialized APIs for every use case
</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-5">
{ecosystemPillars.map((pillar) => (
<Link key={pillar.title} to={pillar.href}>
<Card className="group p-6 h-full hover:border-primary/40 transition-all duration-200 hover:shadow-lg hover:shadow-primary/5">
<div className="space-y-4">
<div className={`w-14 h-14 rounded-xl bg-gradient-to-br ${pillar.gradient} flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform`}>
<pillar.icon className="w-7 h-7 text-white" />
</div>
<div className="space-y-2">
<h3 className="text-xl font-semibold group-hover:text-primary transition-colors">
{pillar.title}
</h3>
<p className="text-muted-foreground text-sm">
{pillar.description}
</p>
</div>
<div className="flex items-center text-primary text-sm font-medium group-hover:translate-x-1 transition-transform">
Explore <ArrowRight className="w-4 h-4 ml-1" />
</div>
</div>
</Card>
</Link>
))}
</div>
</div>
</section>
{/* Features */}
<section className="py-20 px-4 bg-muted/30">
<div className="max-w-6xl mx-auto space-y-12">
<div className="text-center space-y-4">
<h2 className="text-3xl md:text-4xl font-bold">Built for game developers</h2>
<p className="text-muted-foreground max-w-2xl mx-auto">
Everything you need to connect players across platforms
</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-5">
{features.map((feature) => (
<Card key={feature.title} className="p-5 space-y-3">
<div className="w-11 h-11 rounded-lg bg-primary/10 flex items-center justify-center">
<feature.icon className="w-5 h-5 text-primary" />
</div>
<h3 className="font-semibold">{feature.title}</h3>
<p className="text-sm text-muted-foreground">{feature.description}</p>
</Card>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="py-20 px-4">
<div className="max-w-4xl mx-auto">
<Card className="p-8 md:p-12 text-center space-y-6 bg-gradient-to-br from-primary/10 via-primary/5 to-background border-primary/20">
<Zap className="w-12 h-12 text-primary mx-auto" />
<h2 className="text-3xl md:text-4xl font-bold">Start building today</h2>
<p className="text-muted-foreground max-w-lg mx-auto">
Get your API key and integrate in minutes. Free tier includes 10K API calls/month.
</p>
<div className="flex flex-wrap gap-4 justify-center pt-2">
<Link to="/dev-platform/dashboard">
<Button size="lg" className="shadow-lg shadow-primary/25">
Get API Key
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</Link>
<Link to="/dev-platform/quick-start">
<Button size="lg" variant="outline">
View Quick Start
</Button>
</Link>
</div>
</Card>
</div>
</section>
{/* Footer spacer */}
<div className="pb-16" />
</div>
</Layout>
);
}

View file

@ -95,17 +95,17 @@ export default function Investors() {
{ {
icon: <Layers className="h-5 w-5" />, icon: <Layers className="h-5 w-5" />,
title: "Three Engines", title: "Three Engines",
desc: "Studios (services), Platform (community), and Labs (R&D) compound value together.", desc: "Studios, Platform, and Labs compound value.",
}, },
{ {
icon: <Shield className="h-5 w-5" />, icon: <Shield className="h-5 w-5" />,
title: "Trust & Quality", title: "Trust & Quality",
desc: "Security-first engineering and measurable delivery keep churn low and NPS high.", desc: "Security-first engineering with measurable delivery.",
}, },
{ {
icon: <Target className="h-5 w-5" />, icon: <Target className="h-5 w-5" />,
title: "Focused Markets", title: "Focused Markets",
desc: "High-signal segments: games, real-time apps, and experience platforms.", desc: "Games, real-time apps, and experience platforms.",
}, },
]; ];
@ -134,13 +134,11 @@ export default function Investors() {
<span className="mr-2 inline-flex h-2 w-2 animate-pulse rounded-full bg-red-300" /> <span className="mr-2 inline-flex h-2 w-2 animate-pulse rounded-full bg-red-300" />
Investor Relations Investor Relations
</Badge> </Badge>
<h1 className="text-4xl font-black tracking-tight text-red-300 sm:text-5xl lg:text-6xl"> <h1 className="text-3xl font-black tracking-tight text-red-300 sm:text-4xl lg:text-5xl">
AeThex | Building With Conviction AeThex | Building With Conviction
</h1> </h1>
<p className="text-lg text-red-100/90 sm:text-xl"> <p className="text-lg text-red-100/90 sm:text-xl">
We craft reliable, loved software and the platform that powers Reliable software and the platform powering creators. Explore our thesis and participation options.
creators. Explore our thesis, traction, and how to participate
in compliant offerings.
</p> </p>
<div className="flex flex-col gap-4 sm:flex-row"> <div className="flex flex-col gap-4 sm:flex-row">
<Button <Button

View file

@ -154,9 +154,9 @@ export default function Labs() {
{/* Cyberpunk Background Effects */} {/* Cyberpunk Background Effects */}
<div className="pointer-events-none absolute inset-0 opacity-[0.12] [background-image:radial-gradient(circle_at_top,#facc15_0,rgba(0,0,0,0.45)_55%,rgba(0,0,0,0.9)_100%)]" /> <div className="pointer-events-none absolute inset-0 opacity-[0.12] [background-image:radial-gradient(circle_at_top,#facc15_0,rgba(0,0,0,0.45)_55%,rgba(0,0,0,0.9)_100%)]" />
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(transparent_0,transparent_calc(100%-1px),rgba(250,204,21,0.05)_calc(100%-1px))] bg-[length:100%_32px]" /> <div className="pointer-events-none absolute inset-0 bg-[linear-gradient(transparent_0,transparent_calc(100%-1px),rgba(250,204,21,0.05)_calc(100%-1px))] bg-[length:100%_32px]" />
<div className="pointer-events-none absolute inset-0 opacity-[0.08] [background-image:linear-gradient(90deg,rgba(251,191,36,0.1)_1px,transparent_1px),linear-gradient(0deg,rgba(251,191,36,0.1)_1px,transparent_1px)] [background-size:50px_50px] animate-pulse" /> <div className="pointer-events-none absolute inset-0 opacity-[0.08] [background-image:linear-gradient(90deg,rgba(251,191,36,0.1)_1px,transparent_1px),linear-gradient(0deg,rgba(251,191,36,0.1)_1px,transparent_1px)] [background-size:50px_50px]" />
<div className="pointer-events-none absolute top-20 left-10 w-96 h-96 bg-yellow-500/20 rounded-full mix-blend-multiply filter blur-3xl animate-pulse" /> <div className="pointer-events-none absolute top-20 left-10 w-96 h-96 bg-yellow-500/20 rounded-full mix-blend-multiply filter blur-3xl" />
<div className="pointer-events-none absolute bottom-20 right-10 w-96 h-96 bg-yellow-600/10 rounded-full mix-blend-multiply filter blur-3xl animate-pulse" /> <div className="pointer-events-none absolute bottom-20 right-10 w-96 h-96 bg-yellow-600/10 rounded-full mix-blend-multiply filter blur-3xl" />
<main className="relative z-10"> <main className="relative z-10">
{/* Hero Section */} {/* Hero Section */}
@ -177,12 +177,12 @@ export default function Labs() {
Advanced Research & Development Advanced Research & Development
</Badge> </Badge>
<h1 className={`text-5xl md:text-6xl lg:text-7xl font-black text-yellow-300 leading-tight ${theme.fontClass}`}> <h1 className={`text-5xl md:text-6xl lg:text-7xl font-bold text-yellow-300 leading-tight ${theme.fontClass}`}>
The Innovation Engine The Innovation Engine
</h1> </h1>
<p className="text-xl md:text-2xl text-yellow-100/80 max-w-3xl mx-auto leading-relaxed"> <p className="text-lg md:text-xl text-yellow-100/80 max-w-3xl mx-auto leading-relaxed">
Breakthrough R&D pushing the boundaries of what's possible in software, AI, games, and digital experiences. Breakthrough R&D in software, AI, and games
</p> </p>
{/* TL;DR Section */} {/* TL;DR Section */}

View file

@ -65,7 +65,6 @@ export default function Login() {
const [fullName, setFullName] = useState(""); const [fullName, setFullName] = useState("");
const [showReset, setShowReset] = useState(false); const [showReset, setShowReset] = useState(false);
const [resetEmail, setResetEmail] = useState(""); const [resetEmail, setResetEmail] = useState("");
const [rememberMe, setRememberMe] = useState(true);
const [errorFromUrl, setErrorFromUrl] = useState<string | null>(null); const [errorFromUrl, setErrorFromUrl] = useState<string | null>(null);
const [discordLinkedEmail, setDiscordLinkedEmail] = useState<string | null>( const [discordLinkedEmail, setDiscordLinkedEmail] = useState<string | null>(
null, null,
@ -176,12 +175,6 @@ export default function Login() {
}); });
} else { } else {
await signIn(email, password); await signIn(email, password);
// Store remember-me preference — read by AuthContext on next page load
if (rememberMe) {
localStorage.setItem("aethex_remember_me", "1");
} else {
localStorage.removeItem("aethex_remember_me");
}
toastInfo({ toastInfo({
title: "Signing you in", title: "Signing you in",
description: "Redirecting...", description: "Redirecting...",
@ -345,39 +338,6 @@ export default function Login() {
) : null} ) : null}
{/* Social Login Buttons */} {/* Social Login Buttons */}
<div className="space-y-3"> <div className="space-y-3">
{/* AeThex ID — primary SSO */}
<div className="space-y-2">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
AeThex Identity
</p>
<button
type="button"
className="ax-mono ax-clip w-full"
style={{
display: "flex", alignItems: "center", justifyContent: "center", gap: 10,
border: "1px solid rgba(0,255,255,0.5)", color: "#00ffff",
padding: "11px 20px", background: "rgba(0,255,255,0.06)",
fontSize: 11, letterSpacing: 2, textTransform: "uppercase",
cursor: "pointer", transition: "all 0.2s", width: "100%",
}}
onMouseEnter={e => { e.currentTarget.style.background = "rgba(0,255,255,0.14)"; e.currentTarget.style.boxShadow = "0 0 20px rgba(0,255,255,0.2)"; }}
onMouseLeave={e => { e.currentTarget.style.background = "rgba(0,255,255,0.06)"; e.currentTarget.style.boxShadow = "none"; }}
onClick={() => {
// Server-side OIDC flow — bypass Supabase social auth
const redirectTo = encodeURIComponent(location.state?.from?.pathname || "/dashboard");
window.location.href = `${API_BASE}/api/auth/authentik/start?redirectTo=${redirectTo}`;
}}
>
{/* Hex icon */}
<svg viewBox="0 0 100 100" width={16} height={16} style={{ flexShrink: 0 }}>
<polygon points="50,5 95,27.5 95,72.5 50,95 5,72.5 5,27.5" fill="none" stroke="#00ffff" strokeWidth="3" opacity="0.8"/>
<text x="50" y="63" textAnchor="middle" fontFamily="Orbitron" fontSize="38" fontWeight="700" fill="#00ffff">Æ</text>
</svg>
Sign in with AeThex ID
<span style={{ fontSize: 9, color: "rgba(0,255,255,0.4)", letterSpacing: 1 }}>auth.aethex.tech</span>
</button>
</div>
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider"> <p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Quick Sign In Quick Sign In
@ -567,8 +527,6 @@ export default function Login() {
<input <input
type="checkbox" type="checkbox"
className="rounded border-border/50" className="rounded border-border/50"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
/> />
<span className="text-muted-foreground"> <span className="text-muted-foreground">
Remember me Remember me

View file

@ -108,7 +108,7 @@ export default function MaintenancePage() {
<div className="h-px bg-border" /> <div className="h-px bg-border" />
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-center text-xs"> <div className="grid grid-cols-3 gap-4 text-center text-xs">
<div className="space-y-1"> <div className="space-y-1">
<div className="text-muted-foreground">STATUS</div> <div className="text-muted-foreground">STATUS</div>
<div className="text-blue-400 font-semibold flex items-center justify-center gap-1"> <div className="text-blue-400 font-semibold flex items-center justify-center gap-1">

View file

@ -123,14 +123,12 @@ export default function Nexus() {
AeThex Nexus AeThex Nexus
</Badge> </Badge>
<h1 className="text-4xl font-black tracking-tight text-purple-300 sm:text-5xl lg:text-6xl"> <h1 className="text-4xl font-bold tracking-tight text-purple-300 sm:text-5xl">
The Talent Nexus The Talent Nexus
</h1> </h1>
<p className="text-lg text-purple-100/90 sm:text-xl"> <p className="text-base text-purple-100/90">
Connect creators with opportunities across all AeThex arms. Connect creators with opportunities across all AeThex arms
Find talent, post jobs, and build amazing teams in a unified
marketplace powered by both AeThex and DevConnect.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4"> <div className="flex flex-col sm:flex-row gap-4">

View file

@ -590,7 +590,7 @@ const ProfilePassport = () => {
variant="ghost" variant="ghost"
className="h-8 px-2 text-xs text-aethex-200" className="h-8 px-2 text-xs text-aethex-200"
> >
<Link to={`/projects/${project.id}`}> <Link to="/projects/new">
View mission View mission
<ExternalLink className="ml-1 h-3.5 w-3.5" /> <ExternalLink className="ml-1 h-3.5 w-3.5" />
</Link> </Link>

View file

@ -1,280 +0,0 @@
import { useEffect, useState } from "react";
import { useParams, Link } from "react-router-dom";
import Layout from "@/components/Layout";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Github,
ExternalLink,
LayoutDashboard,
Calendar,
Cpu,
Activity,
} from "lucide-react";
const API_BASE = import.meta.env.VITE_API_BASE || "";
interface Project {
id: string;
title: string;
description?: string | null;
status?: string | null;
technologies?: string[] | null;
github_url?: string | null;
live_url?: string | null;
image_url?: string | null;
engine?: string | null;
priority?: string | null;
progress?: number | null;
created_at?: string | null;
updated_at?: string | null;
}
interface Owner {
id: string;
username?: string | null;
full_name?: string | null;
avatar_url?: string | null;
}
const STATUS_COLORS: Record<string, string> = {
planning: "bg-slate-500/20 text-slate-300 border-slate-600",
in_progress: "bg-blue-500/20 text-blue-300 border-blue-600",
completed: "bg-green-500/20 text-green-300 border-green-600",
on_hold: "bg-yellow-500/20 text-yellow-300 border-yellow-600",
};
const STATUS_LABELS: Record<string, string> = {
planning: "Planning",
in_progress: "In Progress",
completed: "Completed",
on_hold: "On Hold",
};
const formatDate = (v?: string | null) => {
if (!v) return null;
const d = new Date(v);
if (isNaN(d.getTime())) return null;
return d.toLocaleDateString(undefined, { dateStyle: "medium" });
};
export default function ProjectDetail() {
const { projectId } = useParams<{ projectId: string }>();
const [project, setProject] = useState<Project | null>(null);
const [owner, setOwner] = useState<Owner | null>(null);
const [loading, setLoading] = useState(true);
const [notFound, setNotFound] = useState(false);
useEffect(() => {
if (!projectId) return;
setLoading(true);
fetch(`${API_BASE}/api/projects/${projectId}`)
.then((r) => {
if (r.status === 404) { setNotFound(true); return null; }
return r.json();
})
.then((body) => {
if (!body) return;
setProject(body.project);
setOwner(body.owner);
})
.catch(() => setNotFound(true))
.finally(() => setLoading(false));
}, [projectId]);
if (loading) {
return (
<Layout>
<div className="flex items-center justify-center min-h-[60vh]">
<div className="animate-pulse text-slate-400">Loading project</div>
</div>
</Layout>
);
}
if (notFound || !project) {
return (
<Layout>
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-4">
<p className="text-xl text-slate-300">Project not found.</p>
<Button asChild variant="outline">
<Link to="/projects">Browse projects</Link>
</Button>
</div>
</Layout>
);
}
const statusKey = project.status ?? "planning";
const statusClass = STATUS_COLORS[statusKey] ?? STATUS_COLORS.planning;
const statusLabel = STATUS_LABELS[statusKey] ?? statusKey;
const ownerSlug = owner?.username ?? owner?.id;
const ownerName = owner?.full_name || owner?.username || "Unknown";
const ownerInitials = ownerName
.split(" ")
.map((w) => w[0])
.join("")
.slice(0, 2)
.toUpperCase();
return (
<Layout>
<div className="max-w-4xl mx-auto px-4 py-10 space-y-8">
{/* Header */}
<div className="space-y-3">
<div className="flex flex-wrap items-center gap-3">
<Badge className={`text-xs border ${statusClass}`}>
{statusLabel}
</Badge>
{project.priority && (
<Badge variant="outline" className="text-xs border-slate-600 text-slate-400">
{project.priority} priority
</Badge>
)}
</div>
<h1 className="text-3xl font-bold text-white">{project.title}</h1>
{project.description && (
<p className="text-slate-300 leading-relaxed text-base max-w-2xl">
{project.description}
</p>
)}
</div>
{/* Action buttons */}
<div className="flex flex-wrap gap-3">
<Button asChild>
<Link to={`/projects/${project.id}/board`}>
<LayoutDashboard className="mr-2 h-4 w-4" />
Project Board
</Link>
</Button>
{project.github_url && (
<Button asChild variant="outline">
<a href={project.github_url} target="_blank" rel="noopener noreferrer">
<Github className="mr-2 h-4 w-4" />
Repository
</a>
</Button>
)}
{project.live_url && (
<Button asChild variant="outline">
<a href={project.live_url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="mr-2 h-4 w-4" />
Live
</a>
</Button>
)}
</div>
<Separator className="border-slate-700" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Meta card */}
<Card className="bg-slate-900/60 border-slate-700 md:col-span-1 space-y-0">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-slate-400 uppercase tracking-wide">
Details
</CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-sm">
{owner && (
<div className="flex items-center gap-3">
<Avatar className="h-8 w-8">
<AvatarImage src={owner.avatar_url ?? undefined} />
<AvatarFallback className="bg-slate-700 text-slate-300 text-xs">
{ownerInitials}
</AvatarFallback>
</Avatar>
<div>
<p className="text-slate-400 text-xs">Owner</p>
{ownerSlug ? (
<Link
to={`/u/${ownerSlug}`}
className="text-aethex-300 hover:underline font-medium"
>
{ownerName}
</Link>
) : (
<span className="text-slate-200">{ownerName}</span>
)}
</div>
</div>
)}
{project.engine && (
<div className="flex items-start gap-2 text-slate-300">
<Cpu className="h-4 w-4 mt-0.5 text-slate-500 shrink-0" />
<div>
<p className="text-slate-400 text-xs">Engine</p>
<p>{project.engine}</p>
</div>
</div>
)}
{typeof project.progress === "number" && (
<div className="space-y-1">
<div className="flex items-center gap-2 text-slate-400 text-xs">
<Activity className="h-3.5 w-3.5" />
<span>Progress {project.progress}%</span>
</div>
<div className="w-full bg-slate-700 rounded-full h-1.5">
<div
className="bg-aethex-500 h-1.5 rounded-full transition-all"
style={{ width: `${Math.min(100, project.progress)}%` }}
/>
</div>
</div>
)}
{(project.created_at || project.updated_at) && (
<div className="flex items-start gap-2 text-slate-300">
<Calendar className="h-4 w-4 mt-0.5 text-slate-500 shrink-0" />
<div className="space-y-0.5">
{project.created_at && (
<p className="text-xs text-slate-400">
Created {formatDate(project.created_at)}
</p>
)}
{project.updated_at && (
<p className="text-xs text-slate-400">
Updated {formatDate(project.updated_at)}
</p>
)}
</div>
</div>
)}
</CardContent>
</Card>
{/* Technologies */}
{project.technologies && project.technologies.length > 0 && (
<Card className="bg-slate-900/60 border-slate-700 md:col-span-2">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-slate-400 uppercase tracking-wide">
Technologies
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{project.technologies.map((tech) => (
<Badge
key={tech}
variant="outline"
className="border-slate-600 text-slate-300 text-xs"
>
{tech}
</Badge>
))}
</div>
</CardContent>
</Card>
)}
</div>
</div>
</Layout>
);
}

View file

@ -71,7 +71,7 @@ export default function Projects() {
Projects & Testimonials Projects & Testimonials
</h1> </h1>
<p className="text-muted-foreground max-w-2xl mt-1"> <p className="text-muted-foreground max-w-2xl mt-1">
Studio initiatives across AeThex Platform, Labs, and Studio. AeThex showcase portfolio
</p> </p>
</div> </div>
{isOwner && ( {isOwner && (

View file

@ -158,7 +158,7 @@ export default function ProjectsAdmin() {
value={draft.title} value={draft.title}
onChange={(e) => setDraft({ ...draft, title: e.target.value })} onChange={(e) => setDraft({ ...draft, title: e.target.value })}
/> />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<select <select
className="rounded border border-border/40 bg-background/70 px-3 py-2" className="rounded border border-border/40 bg-background/70 px-3 py-2"
value={draft.org_unit} value={draft.org_unit}

View file

@ -75,7 +75,7 @@ export default function Realms() {
/> />
</div> </div>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12 max-w-6xl relative"> <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12 max-w-6xl relative space-y-20">
{/* Hero Section */} {/* Hero Section */}
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
@ -91,10 +91,8 @@ export default function Realms() {
Choose Your{" "} Choose Your{" "}
<span className="text-primary drop-shadow-[0_0_25px_rgba(168,85,247,0.8)]">Realm</span> <span className="text-primary drop-shadow-[0_0_25px_rgba(168,85,247,0.8)]">Realm</span>
</h1> </h1>
<p className="text-xl md:text-2xl text-muted-foreground max-w-3xl mx-auto font-light"> <p className="text-lg md:text-xl text-muted-foreground max-w-3xl mx-auto font-light">
Each realm has unique tools, communities, and opportunities. Unique tools and communities for every role
<br className="hidden md:block" />
Your dashboard adapts to your choice.
</p> </p>
</motion.div> </motion.div>

View file

@ -87,7 +87,7 @@ export default function Squads() {
return ( return (
<Layout> <Layout>
<div className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(110,141,255,0.12),transparent_60%)]"> <div className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(110,141,255,0.12),transparent_60%)]">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12 max-w-6xl space-y-8"> <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12 max-w-6xl space-y-12">
{/* Header */} {/* Header */}
<section className="rounded-3xl border border-border/40 bg-background/80 p-6 shadow-2xl backdrop-blur"> <section className="rounded-3xl border border-border/40 bg-background/80 p-6 shadow-2xl backdrop-blur">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
@ -96,8 +96,7 @@ export default function Squads() {
Squads Hub Squads Hub
</h1> </h1>
<p className="mt-1 text-sm text-muted-foreground"> <p className="mt-1 text-sm text-muted-foreground">
Form squads and ship projects together. Match by skill, Form squads and ship projects together
timezone, and goals.
</p> </p>
</div> </div>
<div className="hidden sm:block p-3 rounded-2xl bg-gradient-to-br from-aethex-500/10 to-neon-blue/10"> <div className="hidden sm:block p-3 rounded-2xl bg-gradient-to-br from-aethex-500/10 to-neon-blue/10">

View file

@ -74,7 +74,7 @@ export default function StaffAdmin() {
<Card className="bg-slate-900/50 border-purple-500/20"> <Card className="bg-slate-900/50 border-purple-500/20">
<CardContent className="pt-6"> <CardContent className="pt-6">
<Tabs value={activeTab} onValueChange={setActiveTab}> <Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3 md:grid-cols-5 bg-slate-800/50"> <TabsList className="grid w-full grid-cols-5 bg-slate-800/50">
<TabsTrigger value="users" className="gap-2"> <TabsTrigger value="users" className="gap-2">
<Users className="w-4 h-4" /> <Users className="w-4 h-4" />
<span className="hidden sm:inline">Users</span> <span className="hidden sm:inline">Users</span>

View file

@ -71,7 +71,7 @@ export default function StaffChat() {
</Button> </Button>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 h-[calc(100vh-200px)] sm:h-[600px] min-h-[400px]"> <div className="grid grid-cols-1 lg:grid-cols-4 gap-6 h-[600px]">
{/* Channels Sidebar */} {/* Channels Sidebar */}
<Card className="bg-slate-900/50 border-purple-500/20 lg:col-span-1"> <Card className="bg-slate-900/50 border-purple-500/20 lg:col-span-1">
<CardHeader> <CardHeader>

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