Compare commits
32 commits
content-de
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a4321a531 | ||
|
|
be30e01a50 | ||
|
|
dbd980a6ec | ||
|
|
a68a2b9f8e | ||
|
|
0b1d7a9441 | ||
|
|
446ad7159c | ||
|
|
885ea76d12 | ||
|
|
a57cdb029a | ||
|
|
f1bcc957f9 | ||
|
|
34368e1dde | ||
|
|
29a32da48a | ||
|
|
fbc5ed2f40 | ||
|
|
1599d0e690 | ||
|
|
7fec93e05c | ||
|
|
06b748dade | ||
|
|
c67ee049b6 | ||
|
|
58c1f539b9 | ||
| f4813e7d9b | |||
|
|
f2823e2cd1 | ||
|
|
b640b0d2ad | ||
|
|
88e364f4c5 | ||
|
|
ebf62ec80e | ||
|
|
01026d43cc | ||
|
|
f1efc97c86 | ||
|
|
61fb02cd39 | ||
|
|
1a2a9af335 | ||
|
|
0674a282b0 | ||
|
|
0136d3d8a4 | ||
|
|
9c3942ebbc | ||
|
|
0953628bf5 | ||
|
|
db37bfc733 | ||
|
|
f29196363f |
|
|
@ -35,7 +35,6 @@ data
|
|||
.env
|
||||
load-ids.txt
|
||||
|
||||
server
|
||||
tmp
|
||||
types
|
||||
.git
|
||||
|
|
|
|||
18
Dockerfile
|
|
@ -7,14 +7,22 @@ COPY package.json package-lock.json* pnpm-lock.yaml* npm-shrinkwrap.json* ./
|
|||
|
||||
# Install dependencies
|
||||
RUN if [ -f pnpm-lock.yaml ]; then npm install -g pnpm && pnpm install --frozen-lockfile; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
else npm install; fi
|
||||
elif [ -f package-lock.json ]; then npm ci --legacy-peer-deps; \
|
||||
else npm install --legacy-peer-deps; fi
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the app (frontend + server)
|
||||
RUN npm run build
|
||||
# Build-time env vars (VITE_* are baked into the bundle at build time)
|
||||
ARG VITE_SUPABASE_URL
|
||||
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
|
||||
ENV NODE_ENV=production
|
||||
|
|
@ -24,4 +32,4 @@ ENV PORT=3000
|
|||
EXPOSE 3000
|
||||
|
||||
# Start the server
|
||||
CMD ["npm", "start"]
|
||||
CMD ["npm", "run", "dev"]
|
||||
|
|
|
|||
106
README.md
|
|
@ -1,54 +1,45 @@
|
|||
# AeThex Forge – Local Development Setup
|
||||
# AeThex Forge - Local Development Setup
|
||||
|
||||
## Quick Start Guide
|
||||
|
||||
This guide will help you set up and run the AeThex Developer Platform locally.
|
||||
This guide will help you set up and run the AeThex platform locally on your machine.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Node.js** (v18 or higher)
|
||||
- [Download Node.js](https://nodejs.org/)
|
||||
- Includes npm (Node Package Manager)
|
||||
- Download from: https://nodejs.org/
|
||||
- This will also install npm (Node Package Manager)
|
||||
|
||||
2. **Git** (recommended for updates)
|
||||
- [Download Git](https://git-scm.com/)
|
||||
2. **Git** (optional, if you want to clone updates)
|
||||
- Download from: https://git-scm.com/
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Install Node.js
|
||||
|
||||
- Download and install the LTS version from [nodejs.org](https://nodejs.org/)
|
||||
- Follow the setup wizard
|
||||
- Restart your terminal after installation
|
||||
- Visit https://nodejs.org/ and download the LTS version
|
||||
- Run the installer and follow the setup wizard
|
||||
- Restart your terminal/PowerShell after installation
|
||||
|
||||
### 2. Verify Installation
|
||||
|
||||
Open your terminal and run:
|
||||
|
||||
```bash
|
||||
Open PowerShell or Command Prompt and run:
|
||||
```powershell
|
||||
node --version
|
||||
npm --version
|
||||
```
|
||||
|
||||
You should see version numbers (e.g., v20.x.x and 10.x.x)
|
||||
|
||||
### 3. Install Project Dependencies
|
||||
|
||||
Navigate to the project folder and install dependencies:
|
||||
|
||||
```bash
|
||||
cd /path/to/aethex-forge
|
||||
```powershell
|
||||
cd C:\Users\PCOEM\Downloads\aethex-forge\aethex-forge
|
||||
npm install
|
||||
```
|
||||
|
||||
This may take a few minutes as it downloads all required packages.
|
||||
|
||||
### 4. Set Up Environment Variables
|
||||
|
||||
Create a `.env` file in the root directory (`aethex-forge/`) with the following variables:
|
||||
Create a `.env` file in the root directory (`aethex-forge` folder) with the following variables:
|
||||
|
||||
**Minimum Required (to run the app):**
|
||||
|
||||
```env
|
||||
# Supabase Configuration (Required)
|
||||
VITE_SUPABASE_URL=your_supabase_url_here
|
||||
|
|
@ -61,7 +52,6 @@ VITE_API_BASE=http://localhost:5000
|
|||
```
|
||||
|
||||
**Optional (for full functionality):**
|
||||
|
||||
```env
|
||||
# Discord Integration
|
||||
DISCORD_CLIENT_ID=your_discord_client_id
|
||||
|
|
@ -86,84 +76,76 @@ VITE_GHOST_API_URL=your_ghost_api_url
|
|||
GHOST_ADMIN_API_KEY=your_ghost_admin_key
|
||||
```
|
||||
|
||||
**Note:** You can start with just the Supabase variables to get the app running. Add other credentials as needed for full functionality.
|
||||
**Note:** You can start with just the Supabase variables to get the app running. Other features will work once you add their respective credentials.
|
||||
|
||||
### 5. Run the Development Server
|
||||
|
||||
```bash
|
||||
```powershell
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The application will start on **[http://localhost:5000](http://localhost:5000)** (or the port set in your config).
|
||||
The application will start on **http://localhost:5000**
|
||||
|
||||
Open your browser and navigate to that URL to view the application.
|
||||
|
||||
## Available Commands
|
||||
|
||||
- `npm run dev` – Start development server (default: port 5000)
|
||||
- `npm run build` – Build for production
|
||||
- `npm start` – Start production server
|
||||
- `npm run typecheck` – Check TypeScript types
|
||||
- `npm test` – Run tests
|
||||
- `npm run dev` - Start development server (port 5000)
|
||||
- `npm run build` - Build for production
|
||||
- `npm start` - Start production server
|
||||
- `npm run typecheck` - Check TypeScript types
|
||||
- `npm test` - Run tests
|
||||
|
||||
aethex-forge/
|
||||
aethex-forge/
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
```
|
||||
aethex-forge/
|
||||
├── client/ # React SPA frontend (pages, components, UI)
|
||||
├── server/ # Express backend API
|
||||
├── api/ # API route handlers (modular)
|
||||
├── shared/ # Shared types/interfaces (client/server)
|
||||
├── supabase/ # Database migrations & SQL
|
||||
├── docs/ # Project documentation
|
||||
└── ... # Other supporting folders
|
||||
├── client/ # React frontend (pages, components)
|
||||
├── server/ # Express backend API
|
||||
├── api/ # API route handlers
|
||||
├── shared/ # Shared types between client/server
|
||||
├── discord-bot/ # Discord bot integration
|
||||
└── supabase/ # Database migrations
|
||||
```
|
||||
|
||||
## Getting Supabase Credentials
|
||||
|
||||
If you don't have Supabase credentials yet:
|
||||
|
||||
1. Go to [supabase.com](https://supabase.com/)
|
||||
2. Create a free account and project
|
||||
3. In your project, go to **Settings → API**
|
||||
4. Copy:
|
||||
1. Go to https://supabase.com/
|
||||
2. Create a free account
|
||||
3. Create a new project
|
||||
4. Go to Project Settings → API
|
||||
5. Copy:
|
||||
- Project URL → `VITE_SUPABASE_URL` and `SUPABASE_URL`
|
||||
- `anon` public key → `VITE_SUPABASE_ANON_KEY`
|
||||
- `service_role` secret key → `SUPABASE_SERVICE_ROLE`
|
||||
- `anon` `public` key → `VITE_SUPABASE_ANON_KEY`
|
||||
- `service_role` `secret` key → `SUPABASE_SERVICE_ROLE`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
If port 5000 is already in use, you can change it in `vite.config.ts`:
|
||||
|
||||
```typescript
|
||||
server: {
|
||||
port: 5001, // Change to any available port
|
||||
port: 5001, // Change to any available port
|
||||
}
|
||||
```
|
||||
|
||||
### Module Not Found Errors
|
||||
|
||||
Try deleting `node_modules` and `package-lock.json`, then run `npm install` again:
|
||||
|
||||
```bash
|
||||
rm -rf node_modules package-lock.json
|
||||
```powershell
|
||||
Remove-Item -Recurse -Force node_modules
|
||||
Remove-Item package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
### Environment Variables Not Loading
|
||||
|
||||
- Ensure `.env` is in the root `aethex-forge` directory
|
||||
- Make sure `.env` file is in the root `aethex-forge` directory
|
||||
- Restart the dev server after adding new environment variables
|
||||
- Variables starting with `VITE_` are exposed to the client
|
||||
|
||||
## Need Help?
|
||||
|
||||
- See the `docs/` folder for detailed documentation
|
||||
- Review `AGENTS.md` for architecture and tech stack
|
||||
- See `replit.md` for cloud deployment info
|
||||
- For advanced troubleshooting, check `DEPLOYMENT_CHECKLIST.md` and `PHASE1_IMPLEMENTATION_SUMMARY.md`
|
||||
- Check the `docs/` folder for detailed documentation
|
||||
- Review `AGENTS.md` for architecture details
|
||||
- See `replit.md` for deployment information
|
||||
|
||||
|
|
|
|||
BIN
aethex-logos.zip
|
Before Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 282 KiB |
|
Before Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 267 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 491 B |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1,021 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
|
@ -1,16 +0,0 @@
|
|||
<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>
|
||||
|
Before Width: | Height: | Size: 861 B |
|
Before Width: | Height: | Size: 887 KiB |
|
Before Width: | Height: | Size: 222 KiB |
187
api/admin/analytics.ts
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
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" } });
|
||||
}
|
||||
};
|
||||
245
api/admin/moderation.ts
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
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" } });
|
||||
}
|
||||
};
|
||||
196
api/candidate/interviews.ts
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
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" },
|
||||
});
|
||||
}
|
||||
};
|
||||
136
api/candidate/offers.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
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" },
|
||||
});
|
||||
}
|
||||
};
|
||||
191
api/candidate/profile.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
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" },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -38,9 +38,6 @@ export default async function handler(req: any, res: any) {
|
|||
client_secret: clientSecret,
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
redirect_uri:
|
||||
process.env.DISCORD_ACTIVITY_REDIRECT_URI ||
|
||||
"https://aethex.dev/activity",
|
||||
}).toString(),
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ export default async function handler(req: any, res: any) {
|
|||
if (method === "GET") {
|
||||
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(
|
||||
`*,user_profiles:users(id, full_name, avatar_url, email)`,
|
||||
`
|
||||
*,
|
||||
user_profiles(id, full_name, avatar_url, email)
|
||||
`,
|
||||
{ count: "exact" },
|
||||
);
|
||||
|
||||
|
|
@ -28,10 +30,7 @@ export default async function handler(req: any, res: any) {
|
|||
.order("joined_date", { ascending: false })
|
||||
.range(Number(offset), Number(offset) + Number(limit) - 1);
|
||||
|
||||
if (error) {
|
||||
console.error("[GameForge Team SQL]", error);
|
||||
throw error;
|
||||
}
|
||||
if (error) throw error;
|
||||
return res.json({
|
||||
data: user_id ? data : data,
|
||||
total: count,
|
||||
|
|
|
|||
62
api/staff/announcements.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
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" } });
|
||||
}
|
||||
};
|
||||
100
api/staff/courses.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
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" } });
|
||||
}
|
||||
};
|
||||
|
|
@ -16,15 +16,10 @@ export default async (req: Request) => {
|
|||
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
|
||||
.from("staff_members")
|
||||
.select(`
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
user_id,
|
||||
full_name,
|
||||
|
|
@ -37,11 +32,9 @@ export default async (req: Request) => {
|
|||
location,
|
||||
username,
|
||||
created_at
|
||||
`)
|
||||
.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})`);
|
||||
`,
|
||||
)
|
||||
.order("full_name", { ascending: true });
|
||||
|
||||
if (error) {
|
||||
console.error("Directory fetch error:", error);
|
||||
|
|
|
|||
96
api/staff/expenses.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
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" } });
|
||||
}
|
||||
};
|
||||
46
api/staff/handbook.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
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" } });
|
||||
}
|
||||
};
|
||||
|
|
@ -16,15 +16,10 @@ export default async (req: Request) => {
|
|||
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
|
||||
.from("contractor_invoices")
|
||||
.select(`
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
user_id,
|
||||
invoice_number,
|
||||
|
|
@ -34,12 +29,10 @@ export default async (req: Request) => {
|
|||
due_date,
|
||||
description,
|
||||
created_at
|
||||
`)
|
||||
`,
|
||||
)
|
||||
.eq("user_id", userData.user.id)
|
||||
.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})`);
|
||||
.order("date", { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error("Invoices fetch error:", error);
|
||||
|
|
|
|||
72
api/staff/knowledge-base.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
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" } });
|
||||
}
|
||||
};
|
||||
126
api/staff/marketplace.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
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" } });
|
||||
}
|
||||
};
|
||||
|
|
@ -16,10 +16,10 @@ export default async (req: Request) => {
|
|||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
const { data: staffMember, error } = await supabase
|
||||
.from("staff_members")
|
||||
.select(`
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
user_id,
|
||||
full_name,
|
||||
|
|
@ -31,11 +31,10 @@ export default async (req: Request) => {
|
|||
salary,
|
||||
avatar_url,
|
||||
created_at
|
||||
`)
|
||||
`,
|
||||
)
|
||||
.eq("user_id", userData.user.id)
|
||||
.single();
|
||||
const elapsed = Date.now() - start;
|
||||
console.log(`[staff/me] Query took ${elapsed}ms`);
|
||||
|
||||
if (error && error.code !== "PGRST116") {
|
||||
console.error("Staff member fetch error:", error);
|
||||
|
|
|
|||
|
|
@ -1,64 +1,208 @@
|
|||
import { supabase } from "../_supabase.js";
|
||||
|
||||
export default async (req: Request) => {
|
||||
if (req.method !== "GET") {
|
||||
return new Response("Method not allowed", { status: 405 });
|
||||
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 {
|
||||
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
|
||||
if (!token) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
// GET - Fetch OKRs with key results
|
||||
if (req.method === "GET") {
|
||||
const quarter = url.searchParams.get("quarter");
|
||||
const year = url.searchParams.get("year");
|
||||
const status = url.searchParams.get("status");
|
||||
|
||||
const { data: userData } = await supabase.auth.getUser(token);
|
||||
if (!userData.user) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
let query = supabase
|
||||
.from("staff_okrs")
|
||||
.select(`
|
||||
*,
|
||||
key_results:staff_key_results(*)
|
||||
`)
|
||||
.or(`user_id.eq.${userId},owner_type.in.(team,company)`)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
// 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"));
|
||||
if (quarter) query = query.eq("quarter", parseInt(quarter));
|
||||
if (year) query = query.eq("year", parseInt(year));
|
||||
if (status) query = query.eq("status", status);
|
||||
|
||||
const start = Date.now();
|
||||
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})`);
|
||||
const { data: okrs, error } = await query;
|
||||
if (error) throw error;
|
||||
|
||||
if (error) {
|
||||
console.error("OKRs fetch error:", error);
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
// Calculate stats
|
||||
const myOkrs = okrs?.filter(o => o.user_id === userId) || [];
|
||||
const stats = {
|
||||
total: myOkrs.length,
|
||||
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" },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(okrs || []), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
// POST - Create OKR or Key Result
|
||||
if (req.method === "POST") {
|
||||
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) {
|
||||
return new Response(JSON.stringify({ error: err.message }), {
|
||||
status: 500,
|
||||
});
|
||||
console.error("OKR API error:", err);
|
||||
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
};
|
||||
|
|
|
|||
289
api/staff/onboarding.ts
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
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" },
|
||||
});
|
||||
}
|
||||
};
|
||||
102
api/staff/projects.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
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" } });
|
||||
}
|
||||
};
|
||||
60
api/staff/reviews.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
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" } });
|
||||
}
|
||||
};
|
||||
245
api/staff/time-tracking.ts
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
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" } });
|
||||
}
|
||||
};
|
||||
401
client/App.tsx
|
|
@ -1,11 +1,10 @@
|
|||
import "./global.css";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import { BrowserRouter, Routes, Route, useNavigate } from "react-router-dom";
|
||||
import { useDiscordActivity } from "./contexts/DiscordActivityContext";
|
||||
import { AuthProvider } from "./contexts/AuthContext";
|
||||
import { Web3Provider } from "./contexts/Web3Context";
|
||||
|
|
@ -15,7 +14,6 @@ import { MaintenanceProvider } from "./contexts/MaintenanceContext";
|
|||
import MaintenanceGuard from "./components/MaintenanceGuard";
|
||||
import PageTransition from "./components/PageTransition";
|
||||
import SkipAgentController from "./components/SkipAgentController";
|
||||
import Index from "./pages/Index";
|
||||
import Onboarding from "./pages/Onboarding";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import Login from "./pages/Login";
|
||||
|
|
@ -26,14 +24,9 @@ import ResearchLabs from "./pages/ResearchLabs";
|
|||
import Labs from "./pages/Labs";
|
||||
import GameForge from "./pages/GameForge";
|
||||
import Foundation from "./pages/Foundation";
|
||||
import Corp from "./pages/Corp";
|
||||
import Staff from "./pages/Staff";
|
||||
import Nexus from "./pages/Nexus";
|
||||
import Arms from "./pages/Arms";
|
||||
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 Engage from "./pages/Pricing";
|
||||
import DocsLayout from "@/components/docs/DocsLayout";
|
||||
|
|
@ -56,12 +49,6 @@ import GameJoltIntegration from "./pages/docs/integrations/GameJolt";
|
|||
import ItchIoIntegration from "./pages/docs/integrations/ItchIo";
|
||||
import DocsCurriculum from "./pages/docs/DocsCurriculum";
|
||||
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 ArtistProfile from "./pages/ethos/ArtistProfile";
|
||||
import ArtistSettings from "./pages/ethos/ArtistSettings";
|
||||
|
|
@ -77,7 +64,6 @@ import DevelopersDirectory from "./pages/DevelopersDirectory";
|
|||
import ProfilePassport from "./pages/ProfilePassport";
|
||||
import SubdomainPassport from "./pages/SubdomainPassport";
|
||||
import Profile from "./pages/Profile";
|
||||
import LegacyPassportRedirect from "./pages/LegacyPassportRedirect";
|
||||
import { SubdomainPassportProvider } from "./contexts/SubdomainPassportContext";
|
||||
import About from "./pages/About";
|
||||
import Contact from "./pages/Contact";
|
||||
|
|
@ -86,32 +72,28 @@ import Careers from "./pages/Careers";
|
|||
import Privacy from "./pages/Privacy";
|
||||
import Terms from "./pages/Terms";
|
||||
import Admin from "./pages/Admin";
|
||||
import Feed from "./pages/Feed";
|
||||
import AdminModeration from "./pages/admin/AdminModeration";
|
||||
import AdminAnalytics from "./pages/admin/AdminAnalytics";
|
||||
import AdminFeed from "./pages/AdminFeed";
|
||||
import ProjectsNew from "./pages/ProjectsNew";
|
||||
import Opportunities from "./pages/Opportunities";
|
||||
import Explore from "./pages/Explore";
|
||||
import ResetPassword from "./pages/ResetPassword";
|
||||
import Teams from "./pages/Teams";
|
||||
import Squads from "./pages/Squads";
|
||||
import MenteeHub from "./pages/MenteeHub";
|
||||
import ProjectBoard from "./pages/ProjectBoard";
|
||||
import ProjectDetail from "./pages/ProjectDetail";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import FourOhFourPage from "./pages/404";
|
||||
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 Investors from "./pages/Investors";
|
||||
import NexusDashboard from "./pages/dashboards/NexusDashboard";
|
||||
import LabsDashboard from "./pages/dashboards/LabsDashboard";
|
||||
import GameForgeDashboard from "./pages/dashboards/GameForgeDashboard";
|
||||
import StaffDashboard from "./pages/dashboards/StaffDashboard";
|
||||
import Roadmap from "./pages/Roadmap";
|
||||
import Trust from "./pages/Trust";
|
||||
import PressKit from "./pages/PressKit";
|
||||
import Downloads from "./pages/Downloads";
|
||||
const Downloads = React.lazy(() => import("./pages/Downloads"));
|
||||
import Projects from "./pages/Projects";
|
||||
import ProjectsAdmin from "./pages/ProjectsAdmin";
|
||||
import Directory from "./pages/Directory";
|
||||
|
|
@ -135,13 +117,7 @@ import OpportunitiesHub from "./pages/opportunities/OpportunitiesHub";
|
|||
import OpportunityDetail from "./pages/opportunities/OpportunityDetail";
|
||||
import OpportunityPostForm from "./pages/opportunities/OpportunityPostForm";
|
||||
import MyApplications from "./pages/profile/MyApplications";
|
||||
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";
|
||||
// Hub pages moved to aethex.co (aethex-corp app)
|
||||
import Space1Welcome from "./pages/internal-docs/Space1Welcome";
|
||||
import Space1AxiomModel from "./pages/internal-docs/Space1AxiomModel";
|
||||
import Space1FindYourRole from "./pages/internal-docs/Space1FindYourRole";
|
||||
|
|
@ -160,20 +136,19 @@ import Space4ClientOps from "./pages/internal-docs/Space4ClientOps";
|
|||
import Space4PlatformStrategy from "./pages/internal-docs/Space4PlatformStrategy";
|
||||
import Space5Onboarding from "./pages/internal-docs/Space5Onboarding";
|
||||
import Space5Finance from "./pages/internal-docs/Space5Finance";
|
||||
import Staff from "./pages/Staff";
|
||||
import StaffLogin from "./pages/StaffLogin";
|
||||
import StaffDirectory from "./pages/StaffDirectory";
|
||||
import StaffDashboard from "./pages/dashboards/StaffDashboard";
|
||||
import StaffAdmin from "./pages/StaffAdmin";
|
||||
import StaffChat from "./pages/StaffChat";
|
||||
import StaffDocs from "./pages/StaffDocs";
|
||||
import StaffDirectory from "./pages/StaffDirectory";
|
||||
import StaffAchievements from "./pages/StaffAchievements";
|
||||
import StaffAnnouncements from "./pages/staff/StaffAnnouncements";
|
||||
import StaffExpenseReports from "./pages/staff/StaffExpenseReports";
|
||||
import StaffInternalMarketplace from "./pages/staff/StaffInternalMarketplace";
|
||||
import StaffKnowledgeBase from "./pages/staff/StaffKnowledgeBase";
|
||||
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 StaffTimeTracking from "./pages/staff/StaffTimeTracking";
|
||||
import CandidatePortal from "./pages/candidate/CandidatePortal";
|
||||
import CandidateInterviews from "./pages/candidate/CandidateInterviews";
|
||||
import CandidateOffers from "./pages/candidate/CandidateOffers";
|
||||
import CandidateProfile from "./pages/candidate/CandidateProfile";
|
||||
import DeveloperDashboard from "./pages/dev-platform/DeveloperDashboard";
|
||||
import ApiReference from "./pages/dev-platform/ApiReference";
|
||||
import QuickStart from "./pages/dev-platform/QuickStart";
|
||||
|
|
@ -184,10 +159,24 @@ import MarketplaceItemDetail from "./pages/dev-platform/MarketplaceItemDetail";
|
|||
import CodeExamples from "./pages/dev-platform/CodeExamples";
|
||||
import ExampleDetail from "./pages/dev-platform/ExampleDetail";
|
||||
import DeveloperPlatform from "./pages/dev-platform/DeveloperPlatform";
|
||||
import AethexLang from "./pages/dev-platform/AethexLang";
|
||||
|
||||
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 { isActivity } = useDiscordActivity();
|
||||
|
||||
|
|
@ -208,6 +197,7 @@ const App = () => (
|
|||
<Toaster />
|
||||
<Analytics />
|
||||
<BrowserRouter>
|
||||
<StaffSubdomainRedirect>
|
||||
<DiscordActivityWrapper>
|
||||
<SubdomainPassportProvider>
|
||||
<ArmThemeProvider>
|
||||
|
|
@ -243,20 +233,14 @@ const App = () => (
|
|||
path="/dashboard/dev-link"
|
||||
element={<Navigate to="/dashboard/nexus" replace />}
|
||||
/>
|
||||
<Route
|
||||
path="/hub/client"
|
||||
element={
|
||||
<RequireAccess>
|
||||
<ClientHub />
|
||||
</RequireAccess>
|
||||
}
|
||||
/>
|
||||
{/* Hub routes → aethex.co */}
|
||||
<Route path="/hub/*" element={<ExternalRedirect to="https://aethex.co/hub" />} />
|
||||
<Route path="/realms" element={<Realms />} />
|
||||
<Route path="/investors" element={<Investors />} />
|
||||
<Route path="/roadmap" element={<Roadmap />} />
|
||||
<Route path="/trust" element={<Trust />} />
|
||||
<Route path="/press" element={<PressKit />} />
|
||||
<Route path="/downloads" element={<Downloads />} />
|
||||
<Route path="/downloads" element={<React.Suspense fallback={null}><Downloads /></React.Suspense>} />
|
||||
<Route path="/projects" element={<Projects />} />
|
||||
<Route
|
||||
path="/projects/admin"
|
||||
|
|
@ -266,6 +250,22 @@ const App = () => (
|
|||
<Route path="/admin" element={<Admin />} />
|
||||
<Route path="/admin/feed" element={<AdminFeed />} />
|
||||
<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="/feed" element={<Navigate to="/community/feed" replace />} />
|
||||
<Route path="/teams" element={<Teams />} />
|
||||
|
|
@ -276,6 +276,10 @@ const App = () => (
|
|||
path="/projects/:projectId/board"
|
||||
element={<ProjectBoard />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId"
|
||||
element={<ProjectDetail />}
|
||||
/>
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="/profile/me" element={<Profile />} />
|
||||
<Route
|
||||
|
|
@ -408,143 +412,27 @@ const App = () => (
|
|||
{/* Foundation page with auto-redirect to aethex.foundation (Non-Profit Guardian - Axiom Model) */}
|
||||
<Route path="/foundation" element={<Foundation />} />
|
||||
|
||||
<Route path="/corp" element={<Corp />} />
|
||||
<Route
|
||||
path="/corp/schedule-consultation"
|
||||
element={<CorpScheduleConsultation />}
|
||||
/>
|
||||
<Route
|
||||
path="/corp/view-case-studies"
|
||||
element={<CorpViewCaseStudies />}
|
||||
/>
|
||||
<Route
|
||||
path="/corp/contact-us"
|
||||
element={<CorpContactUs />}
|
||||
/>
|
||||
{/* Corp routes → aethex.co */}
|
||||
<Route path="/corp" element={<ExternalRedirect to="https://aethex.co" />} />
|
||||
<Route path="/corp/*" element={<ExternalRedirect to="https://aethex.co" />} />
|
||||
|
||||
{/* Staff Arm Routes */}
|
||||
{/* Staff routes */}
|
||||
<Route path="/staff" element={<Staff />} />
|
||||
<Route path="/staff/login" element={<StaffLogin />} />
|
||||
|
||||
{/* Staff Dashboard Routes */}
|
||||
<Route
|
||||
path="/staff/dashboard"
|
||||
element={
|
||||
<RequireAccess>
|
||||
<StaffDashboard />
|
||||
</RequireAccess>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Staff Management Routes */}
|
||||
<Route
|
||||
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>
|
||||
}
|
||||
/>
|
||||
<Route path="/staff/dashboard" element={<StaffDashboard />} />
|
||||
<Route path="/staff/admin" element={<StaffAdmin />} />
|
||||
<Route path="/staff/chat" element={<StaffChat />} />
|
||||
<Route path="/staff/docs" element={<StaffDocs />} />
|
||||
<Route path="/staff/directory" element={<StaffDirectory />} />
|
||||
<Route path="/staff/achievements" element={<StaffAchievements />} />
|
||||
<Route path="/staff/time-tracking" element={<StaffTimeTracking />} />
|
||||
{/* Unbuilt staff sub-pages fall back to dashboard */}
|
||||
<Route path="/staff/*" element={<Navigate to="/staff/dashboard" replace />} />
|
||||
{/* Candidate routes */}
|
||||
<Route path="/candidate" element={<CandidatePortal />} />
|
||||
<Route path="/candidate/interviews" element={<CandidateInterviews />} />
|
||||
<Route path="/candidate/offers" element={<CandidateOffers />} />
|
||||
<Route path="/candidate/profile" element={<CandidateProfile />} />
|
||||
|
||||
{/* Dev-Link routes - now redirect to Nexus Opportunities with ecosystem filter */}
|
||||
<Route path="/dev-link" element={<Navigate to="/opportunities?ecosystem=roblox" replace />} />
|
||||
|
|
@ -553,55 +441,8 @@ const App = () => (
|
|||
element={<Navigate to="/opportunities?ecosystem=roblox" replace />}
|
||||
/>
|
||||
|
||||
{/* Client Hub routes */}
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
{/* Client Hub routes → aethex.co */}
|
||||
<Route path="/hub/client/*" element={<ExternalRedirect to="https://aethex.co/hub" />} />
|
||||
|
||||
{/* Nexus routes */}
|
||||
<Route path="/nexus" element={<Nexus />} />
|
||||
|
|
@ -621,6 +462,10 @@ const App = () => (
|
|||
path="curriculum"
|
||||
element={<DocsCurriculum />}
|
||||
/>
|
||||
<Route
|
||||
path="curriculum/ethos"
|
||||
element={<DocsCurriculumEthos />}
|
||||
/>
|
||||
<Route
|
||||
path="getting-started"
|
||||
element={<DocsGettingStarted />}
|
||||
|
|
@ -669,12 +514,6 @@ const App = () => (
|
|||
path="integrations/itchio"
|
||||
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 path="/tutorials" element={<Tutorials />} />
|
||||
<Route path="/community/*" element={<Community />} />
|
||||
|
|
@ -729,88 +568,6 @@ const App = () => (
|
|||
{/* Discord Activity route */}
|
||||
<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 */}
|
||||
<Route
|
||||
path="/internal-docs"
|
||||
|
|
@ -903,7 +660,6 @@ const App = () => (
|
|||
<Route path="/dev-platform/marketplace/:id" element={<MarketplaceItemDetail />} />
|
||||
<Route path="/dev-platform/examples" element={<CodeExamples />} />
|
||||
<Route path="/dev-platform/examples/:id" element={<ExampleDetail />} />
|
||||
<Route path="/lang" element={<AethexLang />} />
|
||||
|
||||
{/* Explicit 404 route for static hosting fallbacks */}
|
||||
<Route path="/404" element={<FourOhFourPage />} />
|
||||
|
|
@ -916,6 +672,7 @@ const App = () => (
|
|||
</ArmThemeProvider>
|
||||
</SubdomainPassportProvider>
|
||||
</DiscordActivityWrapper>
|
||||
</StaffSubdomainRedirect>
|
||||
</BrowserRouter>
|
||||
</TooltipProvider>
|
||||
</DiscordProvider>
|
||||
|
|
|
|||
|
|
@ -68,9 +68,22 @@ const ARMS: Arm[] = [
|
|||
textColor: "text-purple-400",
|
||||
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">Æ</text></svg>')}`;
|
||||
|
||||
const LOGO_URLS: Record<string, string> = {
|
||||
studio: STUDIO_SVG,
|
||||
staff:
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Music,
|
||||
Toggle,
|
||||
ToggleLeft,
|
||||
ToggleRight,
|
||||
ExternalLink,
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ export function ProfileEditor({
|
|||
|
||||
return (
|
||||
<Tabs defaultValue="basic" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-5">
|
||||
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3 md:grid-cols-5">
|
||||
<TabsTrigger value="basic">Basic</TabsTrigger>
|
||||
<TabsTrigger value="social">Social</TabsTrigger>
|
||||
<TabsTrigger value="skills">Skills</TabsTrigger>
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ const BlogEditor = ({ onPublish, initialData }: BlogEditorProps) => {
|
|||
|
||||
const handlePublish = async () => {
|
||||
if (!title.trim() || !html.trim()) {
|
||||
toast.error({ title: "Title and body are required" });
|
||||
toast.error("Title and body are required");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ const BlogEditor = ({ onPublish, initialData }: BlogEditorProps) => {
|
|||
}
|
||||
|
||||
const data = await response.json();
|
||||
toast.success({ title: `Post published: ${data.url}` });
|
||||
toast.success(`Post published: ${data.url}`);
|
||||
onPublish?.(true);
|
||||
|
||||
// Reset form
|
||||
|
|
@ -108,7 +108,7 @@ const BlogEditor = ({ onPublish, initialData }: BlogEditorProps) => {
|
|||
setMetaTitle("");
|
||||
setMetaDescription("");
|
||||
} catch (error: any) {
|
||||
toast.error({ title: error.message || "Failed to publish post" });
|
||||
toast.error(error.message || "Failed to publish post");
|
||||
onPublish?.(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export default function AdminFoundationManager() {
|
|||
const data = await response.json();
|
||||
setMentors(data || []);
|
||||
} catch (error) {
|
||||
aethexToast.error({ title: "Failed to load mentors" });
|
||||
aethexToast.error("Failed to load mentors");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingMentors(false);
|
||||
|
|
@ -116,7 +116,7 @@ export default function AdminFoundationManager() {
|
|||
const data = await response.json();
|
||||
setCourses(data || []);
|
||||
} catch (error) {
|
||||
aethexToast.error({ title: "Failed to load courses" });
|
||||
aethexToast.error("Failed to load courses");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingCourses(false);
|
||||
|
|
@ -133,7 +133,7 @@ export default function AdminFoundationManager() {
|
|||
const data = await response.json();
|
||||
setAchievements(data || []);
|
||||
} catch (error) {
|
||||
aethexToast.error({ title: "Failed to load achievements" });
|
||||
aethexToast.error("Failed to load achievements");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingAchievements(false);
|
||||
|
|
@ -154,14 +154,14 @@ export default function AdminFoundationManager() {
|
|||
);
|
||||
|
||||
if (!response.ok) throw new Error("Failed to update mentor");
|
||||
aethexToast.success({
|
||||
title: `Mentor ${approvalAction === "approve" ? "approved" : "rejected"}`,
|
||||
});
|
||||
aethexToast.success(
|
||||
`Mentor ${approvalAction === "approve" ? "approved" : "rejected"}`,
|
||||
);
|
||||
setApprovalDialogOpen(false);
|
||||
setSelectedMentor(null);
|
||||
fetchMentors();
|
||||
} catch (error) {
|
||||
aethexToast.error({ title: "Failed to update mentor" });
|
||||
aethexToast.error("Failed to update mentor");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
|
@ -178,10 +178,10 @@ export default function AdminFoundationManager() {
|
|||
);
|
||||
|
||||
if (!response.ok) throw new Error("Failed to update course");
|
||||
aethexToast.success({ title: `Course ${publish ? "published" : "unpublished"}` });
|
||||
aethexToast.success(`Course ${publish ? "published" : "unpublished"}`);
|
||||
fetchCourses();
|
||||
} catch (error) {
|
||||
aethexToast.error({ title: "Failed to update course" });
|
||||
aethexToast.error("Failed to update course");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
|
@ -196,10 +196,10 @@ export default function AdminFoundationManager() {
|
|||
);
|
||||
|
||||
if (!response.ok) throw new Error("Failed to delete course");
|
||||
aethexToast.success({ title: "Course deleted" });
|
||||
aethexToast.success("Course deleted");
|
||||
fetchCourses();
|
||||
} catch (error) {
|
||||
aethexToast.error({ title: "Failed to delete course" });
|
||||
aethexToast.error("Failed to delete course");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export default function AdminNexusManager() {
|
|||
const data = await response.json();
|
||||
setOpportunities(data || []);
|
||||
} catch (error) {
|
||||
aethexToast.error({ title: "Failed to load opportunities" });
|
||||
aethexToast.error("Failed to load opportunities");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingOpp(false);
|
||||
|
|
@ -120,7 +120,7 @@ export default function AdminNexusManager() {
|
|||
const data = await response.json();
|
||||
setDisputes(data || []);
|
||||
} catch (error) {
|
||||
aethexToast.error({ title: "Failed to load disputes" });
|
||||
aethexToast.error("Failed to load disputes");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingDisputes(false);
|
||||
|
|
@ -135,7 +135,7 @@ export default function AdminNexusManager() {
|
|||
const data = await response.json();
|
||||
setCommissions(data || []);
|
||||
} catch (error) {
|
||||
aethexToast.error({ title: "Failed to load commissions" });
|
||||
aethexToast.error("Failed to load commissions");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingCommissions(false);
|
||||
|
|
@ -157,10 +157,10 @@ export default function AdminNexusManager() {
|
|||
);
|
||||
|
||||
if (!response.ok) throw new Error("Failed to update opportunity");
|
||||
aethexToast.success({ title: `Opportunity marked as ${status}` });
|
||||
aethexToast.success(`Opportunity marked as ${status}`);
|
||||
fetchOpportunities();
|
||||
} catch (error) {
|
||||
aethexToast.error({ title: "Failed to update opportunity" });
|
||||
aethexToast.error("Failed to update opportunity");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
|
@ -180,12 +180,12 @@ export default function AdminNexusManager() {
|
|||
);
|
||||
|
||||
if (!response.ok) throw new Error("Failed to update opportunity");
|
||||
aethexToast.success({
|
||||
title: `Opportunity ${featured ? "featured" : "unfeatured"}`,
|
||||
});
|
||||
aethexToast.success(
|
||||
`Opportunity ${featured ? "featured" : "unfeatured"}`,
|
||||
);
|
||||
fetchOpportunities();
|
||||
} catch (error) {
|
||||
aethexToast.error({ title: "Failed to update opportunity" });
|
||||
aethexToast.error("Failed to update opportunity");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
|
@ -207,15 +207,15 @@ export default function AdminNexusManager() {
|
|||
);
|
||||
|
||||
if (!response.ok) throw new Error("Failed to update dispute");
|
||||
aethexToast.success({
|
||||
title: `Dispute ${disputeAction === "resolve" ? "resolved" : "escalated"}`,
|
||||
});
|
||||
aethexToast.success(
|
||||
`Dispute ${disputeAction === "resolve" ? "resolved" : "escalated"}`,
|
||||
);
|
||||
setDisputeDialogOpen(false);
|
||||
setSelectedDispute(null);
|
||||
setDisputeResolution("");
|
||||
fetchDisputes();
|
||||
} catch (error) {
|
||||
aethexToast.error({ title: "Failed to update dispute" });
|
||||
aethexToast.error("Failed to update dispute");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export default function AdminStaffAdmin() {
|
|||
</div>
|
||||
|
||||
<Tabs value={adminTab} onValueChange={setAdminTab} className="space-y-4">
|
||||
<TabsList className="grid w-full grid-cols-3 lg:grid-cols-6">
|
||||
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3 lg:grid-cols-6">
|
||||
<TabsTrigger value="users" className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Users</span>
|
||||
|
|
|
|||
|
|
@ -12,20 +12,20 @@ export default function MaintenanceToggle() {
|
|||
|
||||
const handleToggle = async () => {
|
||||
if (!canBypass) {
|
||||
aethexToast.error({ title: "Only admins can toggle maintenance mode" });
|
||||
aethexToast.error("Only admins can toggle maintenance mode");
|
||||
return;
|
||||
}
|
||||
|
||||
setToggling(true);
|
||||
try {
|
||||
await toggleMaintenanceMode();
|
||||
aethexToast.success({
|
||||
title: isMaintenanceMode
|
||||
aethexToast.success(
|
||||
isMaintenanceMode
|
||||
? "Maintenance mode disabled - site is now live"
|
||||
: "Maintenance mode enabled - visitors will see maintenance page"
|
||||
});
|
||||
);
|
||||
} catch (error: any) {
|
||||
aethexToast.error({ title: error?.message || "Failed to toggle maintenance mode" });
|
||||
aethexToast.error(error?.message || "Failed to toggle maintenance mode");
|
||||
} finally {
|
||||
setToggling(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ export const AIChat: React.FC<AIChatProps> = ({
|
|||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
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-[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-[70vh] sm: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`}>
|
||||
<PersonaSelector
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@ export function DevConnectLinkModal({
|
|||
}: DevConnectLinkModalProps) {
|
||||
const [username, setUsername] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const toast = useAethexToast();
|
||||
const { toast } = useAethexToast();
|
||||
|
||||
const handleLink = async () => {
|
||||
if (!username.trim()) {
|
||||
toast.error({ title: "Please enter your DevConnect username" });
|
||||
toast("Please enter your DevConnect username", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -41,16 +41,17 @@ export function DevConnectLinkModal({
|
|||
devconnect_username: username.trim(),
|
||||
devconnect_profile_url: `https://devconnect.sbs/${username.trim()}`,
|
||||
});
|
||||
toast.success({ title: "DevConnect account linked successfully!" });
|
||||
toast("DevConnect account linked successfully!", "success");
|
||||
setUsername("");
|
||||
onOpenChange(false);
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
toast.error({
|
||||
title: error instanceof Error
|
||||
toast(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to link DevConnect account",
|
||||
});
|
||||
"error",
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,18 +21,122 @@ import {
|
|||
User,
|
||||
Menu,
|
||||
X,
|
||||
Zap,
|
||||
FlaskConical,
|
||||
LayoutDashboard,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
|
||||
export interface DevPlatformNavProps {
|
||||
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) {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
|
||||
const [searchOpen, setSearchOpen] = React.useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
// Command palette keyboard shortcut
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
||||
|
|
@ -40,46 +144,12 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
|
|||
setSearchOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
const navLinks = [
|
||||
{
|
||||
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);
|
||||
const isGroupActive = (group: NavGroup) =>
|
||||
group.items.some((item) => location.pathname.startsWith(item.href));
|
||||
|
||||
return (
|
||||
<nav
|
||||
|
|
@ -91,7 +161,7 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
|
|||
<div className="container flex h-16 items-center">
|
||||
{/* Logo */}
|
||||
<Link
|
||||
to="/"
|
||||
to="/dev-platform"
|
||||
className="mr-8 flex items-center space-x-2 transition-opacity hover:opacity-80"
|
||||
>
|
||||
<FileCode className="h-6 w-6 text-primary" />
|
||||
|
|
@ -104,55 +174,68 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
|
|||
<div className="hidden md:flex md:flex-1 md:items-center md:justify-between">
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList>
|
||||
{navLinks.map((link) => (
|
||||
<NavigationMenuItem key={link.href}>
|
||||
<Link to={link.href}>
|
||||
<NavigationMenuLink
|
||||
className={cn(
|
||||
"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"
|
||||
)}
|
||||
>
|
||||
<link.icon className="mr-2 h-4 w-4" />
|
||||
{link.name}
|
||||
{link.comingSoon && (
|
||||
<span className="ml-2 rounded-full bg-primary/20 px-2 py-0.5 text-xs text-primary">
|
||||
Soon
|
||||
</span>
|
||||
)}
|
||||
</NavigationMenuLink>
|
||||
</Link>
|
||||
{NAV_GROUPS.map((group) => (
|
||||
<NavigationMenuItem key={group.label}>
|
||||
<NavigationMenuTrigger
|
||||
className={cn(
|
||||
"h-10 text-sm font-medium",
|
||||
isGroupActive(group) && "text-primary"
|
||||
)}
|
||||
>
|
||||
{group.label}
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<ul className="grid w-[420px] gap-1 p-3">
|
||||
{/* Group header */}
|
||||
<li className="px-2 pb-1">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/60">
|
||||
{group.label}
|
||||
</p>
|
||||
</li>
|
||||
{group.items.map((item) => (
|
||||
<li key={item.href}>
|
||||
<DropdownItem item={item} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</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>
|
||||
</NavigationMenu>
|
||||
|
||||
{/* Right side actions */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Search button */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="relative h-9 w-full justify-start text-sm text-muted-foreground sm:w-64"
|
||||
className="relative h-9 justify-start text-sm text-muted-foreground w-48"
|
||||
onClick={() => setSearchOpen(true)}
|
||||
>
|
||||
<Command className="mr-2 h-4 w-4" />
|
||||
<span className="hidden lg:inline-flex">Search...</span>
|
||||
<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">
|
||||
<Command className="mr-2 h-4 w-4 shrink-0" />
|
||||
<span>Search docs...</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="text-xs">⌘</span>K
|
||||
</kbd>
|
||||
</Button>
|
||||
|
||||
{/* Dashboard link */}
|
||||
<Link to="/dashboard">
|
||||
<Button variant="ghost" size="sm">
|
||||
Dashboard
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{/* User menu */}
|
||||
<Link to="/profile">
|
||||
<Button variant="ghost" size="icon" className="h-9 w-9">
|
||||
<User className="h-4 w-4" />
|
||||
|
|
@ -168,11 +251,7 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
|
|||
size="icon"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -180,41 +259,50 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
|
|||
{/* Mobile Navigation */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="border-t border-border/40 md:hidden">
|
||||
<div className="container space-y-1 py-4">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
to={link.href}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className={cn(
|
||||
"flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground",
|
||||
isActive(link.href) && "bg-accent text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<link.icon className="mr-3 h-4 w-4" />
|
||||
{link.name}
|
||||
{link.comingSoon && (
|
||||
<span className="ml-auto rounded-full bg-primary/20 px-2 py-0.5 text-xs text-primary">
|
||||
Soon
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
<div className="container py-4 space-y-4">
|
||||
{NAV_GROUPS.map((group) => (
|
||||
<div key={group.label}>
|
||||
<p className="px-3 pb-1 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/60">
|
||||
{group.label}
|
||||
</p>
|
||||
{group.items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground",
|
||||
location.pathname.startsWith(item.href) && "bg-accent text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-4 w-4 shrink-0" />
|
||||
<span className="flex-1">{item.name}</span>
|
||||
{item.comingSoon && (
|
||||
<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-4 mt-4">
|
||||
<div className="border-t border-border/40 pt-3">
|
||||
<Link
|
||||
to="/dashboard"
|
||||
to="/dev-platform/dashboard"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
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"
|
||||
>
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
to="/profile"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
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"
|
||||
>
|
||||
<User className="mr-3 h-4 w-4" />
|
||||
<User className="h-4 w-4" />
|
||||
Profile
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -222,20 +310,20 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Command Palette Placeholder - will be implemented separately */}
|
||||
{/* Command Palette */}
|
||||
{searchOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||
onClick={() => setSearchOpen(false)}
|
||||
>
|
||||
<div 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">
|
||||
<p className="text-center text-muted-foreground">
|
||||
Command palette coming soon...
|
||||
</p>
|
||||
<p className="text-center text-sm text-muted-foreground mt-2">
|
||||
Press Esc to close
|
||||
</p>
|
||||
<div
|
||||
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="rounded-xl border bg-background p-8 shadow-2xl min-w-80 text-center space-y-2">
|
||||
<Command className="mx-auto h-8 w-8 text-muted-foreground" />
|
||||
<p className="text-muted-foreground font-medium">Command palette coming soon</p>
|
||||
<p className="text-sm text-muted-foreground/60">Press Esc or click outside to close</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
// Export layout components
|
||||
export { DevPlatformLayout } from './layouts/DevPlatformLayout';
|
||||
export { ThreeColumnLayout } from './layouts/ThreeColumnLayout';
|
||||
export { default as DevPlatformLayout } from './layouts/DevPlatformLayout';
|
||||
export { default as ThreeColumnLayout } from './layouts/ThreeColumnLayout';
|
||||
|
||||
// Export UI components
|
||||
export { CodeBlock } from './ui/CodeBlock';
|
||||
export { Callout } from './ui/Callout';
|
||||
export { StatCard } from './ui/StatCard';
|
||||
export { ApiEndpointCard } from './ui/ApiEndpointCard';
|
||||
export { default as CodeBlock } from './ui/CodeBlock';
|
||||
export { default as Callout } from './ui/Callout';
|
||||
export { default as StatCard } from './ui/StatCard';
|
||||
export { default as ApiEndpointCard } from './ui/ApiEndpointCard';
|
||||
|
||||
// Export feature components
|
||||
export { DevPlatformNav } from './DevPlatformNav';
|
||||
export { DevPlatformFooter } from './DevPlatformFooter';
|
||||
export { Breadcrumbs } from './Breadcrumbs';
|
||||
export { CodeTabs } from './CodeTabs';
|
||||
export { TemplateCard } from './TemplateCard';
|
||||
export { MarketplaceCard } from './MarketplaceCard';
|
||||
export { ExampleCard } from './ExampleCard';
|
||||
export { ApiKeyCard } from './ApiKeyCard';
|
||||
export { CreateApiKeyDialog } from './CreateApiKeyDialog';
|
||||
export { UsageChart } from './UsageChart';
|
||||
export { default as DevPlatformNav } from './DevPlatformNav';
|
||||
export { default as DevPlatformFooter } from './DevPlatformFooter';
|
||||
export { default as Breadcrumbs } from './Breadcrumbs';
|
||||
export { default as CodeTabs } from './CodeTabs';
|
||||
export { default as TemplateCard } from './TemplateCard';
|
||||
export { default as MarketplaceCard } from './MarketplaceCard';
|
||||
export { default as ExampleCard } from './ExampleCard';
|
||||
export { default as ApiKeyCard } from './ApiKeyCard';
|
||||
export { default as CreateApiKeyDialog } from './CreateApiKeyDialog';
|
||||
export { default as UsageChart } from './UsageChart';
|
||||
|
|
|
|||
|
|
@ -26,67 +26,16 @@ interface DocNavItem {
|
|||
description?: string;
|
||||
}
|
||||
|
||||
const docNavigation: DocNavItem[] = [
|
||||
{
|
||||
title: "Overview",
|
||||
path: "/docs",
|
||||
icon: <BookOpen className="h-5 w-5" />,
|
||||
description: "Get started with AeThex",
|
||||
},
|
||||
{
|
||||
title: "Getting Started",
|
||||
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",
|
||||
},
|
||||
const docNavigation: Omit<DocNavItem, "icon">[] = [
|
||||
{ title: "Overview", path: "/docs", description: "Get started with AeThex" },
|
||||
{ title: "Getting Started", path: "/docs/getting-started", description: "Quick start guide" },
|
||||
{ title: "Platform", path: "/docs/platform", description: "Platform architecture & features" },
|
||||
{ title: "API Reference", path: "/docs/api", description: "Complete API documentation" },
|
||||
{ title: "CLI", path: "/docs/cli", description: "Command line tools" },
|
||||
{ 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: "Curriculum", path: "/docs/curriculum", description: "Learning paths" },
|
||||
];
|
||||
|
||||
interface DocsLayoutProps {
|
||||
|
|
@ -109,15 +58,27 @@ function DocsLayoutContent({
|
|||
const location = useLocation();
|
||||
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(() => {
|
||||
if (!searchQuery) return docNavigation;
|
||||
if (!searchQuery) return navWithIcons;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return docNavigation.filter(
|
||||
return navWithIcons.filter(
|
||||
(item) =>
|
||||
item.title.toLowerCase().includes(query) ||
|
||||
item.description?.toLowerCase().includes(query),
|
||||
);
|
||||
}, [searchQuery]);
|
||||
}, [searchQuery, navWithIcons]);
|
||||
|
||||
const isCurrentPage = (path: string) => location.pathname === path;
|
||||
|
||||
|
|
|
|||
166
client/components/ethos/EthosLayout.tsx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -205,7 +205,7 @@ export default function CommentsModal({
|
|||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px] flex flex-col h-[600px]">
|
||||
<DialogContent className="sm:max-w-[500px] flex flex-col h-[80vh] sm:h-[600px] max-h-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<MessageCircle className="h-5 w-5" />
|
||||
|
|
|
|||
184
client/components/gameforge/GameForgeLayout.tsx
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -243,6 +243,7 @@ export default function NotificationBell({
|
|||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-80 border-border/40 bg-background/95 backdrop-blur"
|
||||
style={{ zIndex: 99999 }}
|
||||
>
|
||||
<DropdownMenuLabel className="flex items-center justify-between">
|
||||
<span className="text-sm font-semibold text-foreground">
|
||||
|
|
|
|||
|
|
@ -47,20 +47,20 @@ export const WalletVerification = ({
|
|||
|
||||
const handleConnect = async () => {
|
||||
if (!walletInput.trim()) {
|
||||
aethexToast.warning({ title: "Please enter a wallet address" });
|
||||
aethexToast.warning("Please enter a wallet address");
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = walletInput.trim().toLowerCase();
|
||||
if (!isValidEthereumAddress(normalized)) {
|
||||
aethexToast.warning({
|
||||
title: "Invalid Ethereum address. Must be 0x followed by 40 hexadecimal characters.",
|
||||
});
|
||||
aethexToast.warning(
|
||||
"Invalid Ethereum address. Must be 0x followed by 40 hexadecimal characters.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user?.id) {
|
||||
aethexToast.error({ title: "User not authenticated" });
|
||||
aethexToast.error("User not authenticated");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -86,16 +86,16 @@ export const WalletVerification = ({
|
|||
const data = await response.json();
|
||||
setConnectedWallet(normalized);
|
||||
setWalletInput("");
|
||||
aethexToast.success({ title: "✅ Wallet connected successfully!" });
|
||||
aethexToast.success("✅ Wallet connected successfully!");
|
||||
|
||||
if (onWalletUpdated) {
|
||||
onWalletUpdated(normalized);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("[Wallet Verification] Error:", error?.message);
|
||||
aethexToast.error({
|
||||
title: error?.message || "Failed to connect wallet. Please try again.",
|
||||
});
|
||||
aethexToast.error(
|
||||
error?.message || "Failed to connect wallet. Please try again.",
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -123,16 +123,16 @@ export const WalletVerification = ({
|
|||
}
|
||||
|
||||
setConnectedWallet(null);
|
||||
aethexToast.success({ title: "Wallet disconnected" });
|
||||
aethexToast.success("Wallet disconnected");
|
||||
|
||||
if (onWalletUpdated) {
|
||||
onWalletUpdated(null);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("[Wallet Verification] Error:", error?.message);
|
||||
aethexToast.error({
|
||||
title: error?.message || "Failed to disconnect wallet. Please try again.",
|
||||
});
|
||||
aethexToast.error(
|
||||
error?.message || "Failed to disconnect wallet. Please try again.",
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
checkProfileComplete,
|
||||
} from "@/lib/aethex-database-adapter";
|
||||
|
||||
type SupportedOAuthProvider = "github" | "google" | "discord";
|
||||
type SupportedOAuthProvider = "github" | "google" | "discord" | string;
|
||||
|
||||
interface LinkedProvider {
|
||||
provider: SupportedOAuthProvider;
|
||||
|
|
@ -165,6 +165,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
const [loading, setLoading] = useState(true);
|
||||
const rewardsActivatedRef = 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(() => {
|
||||
let sessionRestored = false;
|
||||
|
|
@ -197,6 +200,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
// - IndexedDB (where Supabase stores sessions)
|
||||
// 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;
|
||||
} catch {
|
||||
storageClearedRef.current = true;
|
||||
|
|
@ -221,6 +230,21 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
data: { session },
|
||||
} = 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
|
||||
// Wait for onAuthStateChange to trigger
|
||||
if (!session && hasAuthTokens()) {
|
||||
|
|
@ -276,17 +300,24 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
}, 50);
|
||||
}
|
||||
|
||||
// Show toast notifications for auth events
|
||||
if (event === "SIGNED_IN") {
|
||||
// Only toast on real user-initiated events, not session restoration on page load.
|
||||
// INITIAL_SESSION fires first on page load (Supabase v2); after that every
|
||||
// SIGNED_IN is a genuine login.
|
||||
const isInitialRestore = !initialEventFired.current;
|
||||
initialEventFired.current = true;
|
||||
|
||||
if (event === "SIGNED_IN" && !isInitialRestore) {
|
||||
aethexToast.success({
|
||||
title: "Welcome back!",
|
||||
description: "Successfully signed in to AeThex OS",
|
||||
title: "Signed in",
|
||||
description: "Welcome back to AeThex OS",
|
||||
});
|
||||
} else if (event === "SIGNED_OUT") {
|
||||
aethexToast.info({
|
||||
title: "Signed out",
|
||||
description: "Come back soon!",
|
||||
});
|
||||
} else if (event === "TOKEN_REFRESHED") {
|
||||
// Silently refresh — no toast
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -684,7 +715,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
}
|
||||
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider,
|
||||
provider: provider as any,
|
||||
options: {
|
||||
redirectTo: `${window.location.origin}/login`,
|
||||
},
|
||||
|
|
@ -982,13 +1013,16 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
// Only clear session for actual auth errors
|
||||
// Only clear session for actual Supabase auth errors — be very specific.
|
||||
// "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 = [
|
||||
"invalid refresh token",
|
||||
"refresh_token_not_found",
|
||||
"session expired",
|
||||
"token_expired",
|
||||
"revoked",
|
||||
"unauthorized",
|
||||
"auth/",
|
||||
"jwt expired",
|
||||
];
|
||||
|
||||
if (authErrorPatterns.some((pattern) => messageStr.includes(pattern))) {
|
||||
|
|
@ -1033,6 +1067,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
// Step 2: Clear localStorage and IndexedDB
|
||||
console.log("Clearing localStorage and IndexedDB...");
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.removeItem("aethex_remember_me");
|
||||
try {
|
||||
window.localStorage.removeItem("onboarding_complete");
|
||||
window.localStorage.removeItem("aethex_onboarding_progress_v1");
|
||||
|
|
|
|||
|
|
@ -284,17 +284,17 @@ export const DiscordActivityProvider: React.FC<
|
|||
// Subscribe to speaking updates if in voice channel
|
||||
if (sdk.channelId) {
|
||||
try {
|
||||
sdk.subscribe("SPEAKING_START", (data: any) => {
|
||||
await sdk.subscribe("SPEAKING_START", (data: any) => {
|
||||
console.log("[Discord Activity] Speaking start:", data);
|
||||
if (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
|
||||
));
|
||||
}
|
||||
}, { channel_id: sdk.channelId });
|
||||
|
||||
sdk.subscribe("SPEAKING_STOP", (data: any) => {
|
||||
await sdk.subscribe("SPEAKING_STOP", (data: any) => {
|
||||
console.log("[Discord Activity] Speaking stop:", data);
|
||||
if (data?.user_id) {
|
||||
setSpeakingUsers(prev => {
|
||||
|
|
@ -302,7 +302,7 @@ export const DiscordActivityProvider: React.FC<
|
|||
next.delete(data.user_id);
|
||||
return next;
|
||||
});
|
||||
setParticipants(prev => prev.map(p =>
|
||||
setParticipants(prev => prev.map(p =>
|
||||
p.id === data.user_id ? { ...p, speaking: false } : p
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,90 +1,183 @@
|
|||
@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");
|
||||
@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");
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
/**
|
||||
* Tailwind CSS theme
|
||||
* tailwind.config.ts expects the following color variables to be expressed as HSL values.
|
||||
* A different format will require also updating the theme in tailwind.config.ts.
|
||||
*
|
||||
* SPACING SYSTEM:
|
||||
* Container: container mx-auto px-4 sm:px-6 lg:px-8
|
||||
* Page Container: + py-8 lg:py-12
|
||||
* Max Widths: max-w-7xl (app), max-w-6xl (content), max-w-4xl (articles)
|
||||
* Vertical Spacing: space-y-8 (sections), space-y-6 (cards), space-y-4 (content)
|
||||
* Gaps: gap-6 (cards), gap-4 (buttons/forms), gap-2 (tags)
|
||||
*/
|
||||
:root {
|
||||
--background: 222 84% 4.9%;
|
||||
/* ── AeThex Cyberpunk Theme — copied verbatim from AeThex-Passport-Engine/client/src/index.css ── */
|
||||
:root {
|
||||
--button-outline: rgba(0, 255, 255, .15);
|
||||
--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%;
|
||||
--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;
|
||||
|
||||
/* Spacing tokens */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 24px;
|
||||
--space-6: 32px;
|
||||
--space-section-y: var(--space-6);
|
||||
--foreground: 210 40% 98%;
|
||||
/* AeThex Brand Colors — cyan palette */
|
||||
--aethex-50: 180 100% 97%;
|
||||
--aethex-100: 180 100% 92%;
|
||||
--aethex-200: 180 100% 80%;
|
||||
--aethex-300: 180 100% 70%;
|
||||
--aethex-400: 180 100% 60%;
|
||||
--aethex-500: 180 100% 50%;
|
||||
--aethex-600: 180 100% 40%;
|
||||
--aethex-700: 180 100% 30%;
|
||||
--aethex-800: 180 100% 20%;
|
||||
--aethex-900: 180 100% 12%;
|
||||
--aethex-950: 180 100% 6%;
|
||||
|
||||
--card: 222 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
/* Neon accent palette */
|
||||
--neon-green: 142 76% 45%;
|
||||
--neon-yellow: 50 100% 65%;
|
||||
|
||||
--popover: 222 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
/* Spacing tokens */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 24px;
|
||||
--space-6: 32px;
|
||||
--space-section-y: var(--space-6);
|
||||
|
||||
--primary: 250 100% 60%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
/* Fallback for older browsers */
|
||||
--sidebar-primary-border: hsl(var(--sidebar-primary));
|
||||
--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);
|
||||
}
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
.dark {
|
||||
--button-outline: rgba(0, 255, 255, .15);
|
||||
--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);
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 250 100% 70%;
|
||||
|
||||
--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%;
|
||||
}
|
||||
--sidebar-primary-border: hsl(var(--sidebar-primary));
|
||||
--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);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
|
@ -93,529 +186,280 @@
|
|||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: "Courier New", "Courier", monospace;
|
||||
letter-spacing: 0.025em;
|
||||
@apply font-sans antialiased bg-background text-foreground;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
scroll-behavior: smooth;
|
||||
/* 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 */
|
||||
* {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
html::-webkit-scrollbar { display: none; }
|
||||
*::-webkit-scrollbar { display: none; }
|
||||
* { scrollbar-width: none; }
|
||||
|
||||
.container {
|
||||
@apply px-4 sm:px-6 lg:px-8;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Elevation system — from AeThex-Passport-Engine ── */
|
||||
@layer utilities {
|
||||
/* Arm Theme Font Classes */
|
||||
.font-labs {
|
||||
font-family: "VT323", "Courier New", monospace;
|
||||
letter-spacing: 0.05em;
|
||||
input[type="search"]::-webkit-search-cancel-button {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.font-gameforge {
|
||||
font-family: "Press Start 2P", "Arial Black", sans-serif;
|
||||
letter-spacing: 0.1em;
|
||||
font-size: 0.875em;
|
||||
[contenteditable][data-placeholder]:empty::before {
|
||||
content: attr(data-placeholder);
|
||||
color: hsl(var(--muted-foreground));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.font-corp {
|
||||
font-family: "Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI",
|
||||
sans-serif;
|
||||
font-weight: 600;
|
||||
.no-default-hover-elevate {}
|
||||
.no-default-active-elevate {}
|
||||
|
||||
.toggle-elevate::before,
|
||||
.toggle-elevate-2::before {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
border-radius: inherit;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.font-foundation {
|
||||
font-family: "Merriweather", "Georgia", serif;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
.toggle-elevate.toggle-elevated::before {
|
||||
background-color: var(--elevate-2);
|
||||
}
|
||||
|
||||
.font-devlink {
|
||||
font-family: "Roboto Mono", "Courier New", monospace;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.02em;
|
||||
.border.toggle-elevate::before { inset: -1px; }
|
||||
|
||||
.hover-elevate:not(.no-default-hover-elevate),
|
||||
.active-elevate:not(.no-default-active-elevate),
|
||||
.hover-elevate-2:not(.no-default-hover-elevate),
|
||||
.active-elevate-2:not(.no-default-active-elevate) {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.font-staff {
|
||||
font-family: "Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI",
|
||||
sans-serif;
|
||||
font-weight: 600;
|
||||
.hover-elevate:not(.no-default-hover-elevate)::after,
|
||||
.active-elevate:not(.no-default-active-elevate)::after,
|
||||
.hover-elevate-2:not(.no-default-hover-elevate)::after,
|
||||
.active-elevate-2:not(.no-default-active-elevate)::after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
border-radius: inherit;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.font-nexus {
|
||||
font-family: "Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI",
|
||||
sans-serif;
|
||||
font-weight: 600;
|
||||
.hover-elevate:hover:not(.no-default-hover-elevate)::after,
|
||||
.active-elevate:active:not(.no-default-active-elevate)::after {
|
||||
background-color: var(--elevate-1);
|
||||
}
|
||||
|
||||
.font-default {
|
||||
font-family: "Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI",
|
||||
sans-serif;
|
||||
.hover-elevate-2:hover:not(.no-default-hover-elevate)::after,
|
||||
.active-elevate-2:active:not(.no-default-active-elevate)::after {
|
||||
background-color: var(--elevate-2);
|
||||
}
|
||||
|
||||
/* Arm Theme Wallpaper Patterns */
|
||||
.border.hover-elevate:not(.no-hover-interaction-elevate)::after,
|
||||
.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 {
|
||||
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-attachment: fixed;
|
||||
}
|
||||
|
||||
.wallpaper-gameforge {
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
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-image:
|
||||
linear-gradient(45deg, 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-position: 0 0, 20px 20px;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
.wallpaper-corp {
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
rgba(59, 130, 246, 0.05) 1px,
|
||||
transparent 1px
|
||||
),
|
||||
linear-gradient(rgba(59, 130, 246, 0.05) 1px, transparent 1px);
|
||||
background-image:
|
||||
linear-gradient(90deg, 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-attachment: fixed;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.wallpaper-devlink {
|
||||
background-image: 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%,
|
||||
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-image:
|
||||
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%),
|
||||
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%);
|
||||
background-size: 50px 50px;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
.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-attachment: fixed;
|
||||
}
|
||||
|
||||
.wallpaper-nexus {
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
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-image:
|
||||
linear-gradient(45deg, 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-position: 0 0, 20px 20px;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
.wallpaper-default {
|
||||
background-image: linear-gradient(
|
||||
135deg,
|
||||
rgba(167, 139, 250, 0.05) 0%,
|
||||
rgba(96, 165, 250, 0.05) 100%
|
||||
);
|
||||
background-image: linear-gradient(135deg, rgba(0,255,255,0.03) 0%, rgba(0,255,255,0.01) 100%);
|
||||
}
|
||||
|
||||
.section-cozy {
|
||||
padding-block: var(--space-section-y);
|
||||
}
|
||||
.gap-cozy {
|
||||
gap: var(--space-5);
|
||||
}
|
||||
.pad-cozy {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
/* ── Font aliases (arm theming) ── */
|
||||
.font-labs { font-family: "VT323", "Courier New", monospace; letter-spacing: 0.05em; }
|
||||
.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; }
|
||||
.font-foundation { font-family: "Merriweather", "Georgia", serif; font-weight: 700; letter-spacing: -0.02em; }
|
||||
.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; }
|
||||
.font-nexus { font-family: "Electrolize", "Source Code Pro", monospace; font-weight: 600; }
|
||||
.font-default { font-family: "Electrolize", "Source Code Pro", monospace; }
|
||||
|
||||
/* ── Text gradients ── */
|
||||
.text-gradient {
|
||||
@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;
|
||||
@apply bg-gradient-to-r from-aethex-300 via-aethex-500 to-neon-purple bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
.text-gradient-purple {
|
||||
@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;
|
||||
@apply bg-gradient-to-r from-neon-purple via-aethex-500 to-aethex-300 bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
.bg-aethex-gradient {
|
||||
@apply bg-gradient-to-br from-aethex-900 via-background to-aethex-800;
|
||||
}
|
||||
|
||||
.border-gradient {
|
||||
@apply relative overflow-hidden;
|
||||
}
|
||||
/* ── Interaction ── */
|
||||
.hover-lift { transition: transform 0.3s ease, box-shadow 0.3s ease; }
|
||||
.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); }
|
||||
|
||||
.border-gradient::before {
|
||||
content: "";
|
||||
@apply absolute inset-0 rounded-[inherit] p-[1px] bg-gradient-to-r from-aethex-400 via-neon-blue to-aethex-600;
|
||||
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;
|
||||
}
|
||||
/* ── Spacing helpers ── */
|
||||
.section-cozy { padding-block: var(--space-section-y); }
|
||||
.gap-cozy { gap: var(--space-5); }
|
||||
.pad-cozy { padding: var(--space-5); }
|
||||
|
||||
/* ── 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 {
|
||||
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;
|
||||
border-right: 3px solid;
|
||||
white-space: nowrap;
|
||||
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 {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.1),
|
||||
transparent
|
||||
);
|
||||
background: linear-gradient(90deg, transparent, rgba(0,255,255,0.06), transparent);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gradient-shift {
|
||||
0%,
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
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;
|
||||
}
|
||||
}
|
||||
/* ── Keyframes ── */
|
||||
@keyframes ax-sweep { 0%{left:-100%} 100%{left:200%} }
|
||||
@keyframes ax-blink { 0%,50%{opacity:1} 51%,100%{opacity:0} }
|
||||
@keyframes fade-in { from{opacity:0} to{opacity:1} }
|
||||
@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)} }
|
||||
@keyframes slide-left { from{opacity:0;transform:translateX(20px)} to{opacity:1;transform:translateX(0)} }
|
||||
@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 skeleton-loading { 0%{background-position:-200% 0} 100%{background-position:200% 0} }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
import { supabase, isSupabaseConfigured } from "@/lib/supabase";
|
||||
import type { Database } from "./database.types";
|
||||
|
||||
// Use the existing database user profile type directly
|
||||
import type { UserProfile } from "./database.types";
|
||||
// Derive UserProfile from the live generated schema
|
||||
type UserProfile = Database["public"]["Tables"]["user_profiles"]["Row"];
|
||||
|
||||
// API Base URL for fetch requests
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
||||
|
|
|
|||
30
client/lib/auth-fetch.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
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 });
|
||||
}
|
||||
|
|
@ -124,14 +124,18 @@ export default function About() {
|
|||
{/* Hero */}
|
||||
<section className="py-16 lg:py-24 border-b border-gray-800">
|
||||
<div className="container mx-auto max-w-6xl px-4">
|
||||
<h1 className="text-4xl lg:text-5xl font-bold mb-6">
|
||||
<h1 className="text-5xl lg:text-7xl font-black mb-6">
|
||||
Building an Integrated{" "}
|
||||
<span className="bg-gradient-to-r from-yellow-300 via-blue-300 to-red-300 bg-clip-text text-transparent">
|
||||
Ecosystem
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-lg text-gray-300 max-w-3xl">
|
||||
Four-pillar ecosystem combining innovation, operations, community, and talent
|
||||
<p className="text-xl text-gray-300 max-w-3xl">
|
||||
AeThex operates as a unified four-pillar organization that
|
||||
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>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1042,7 +1042,7 @@ function PollsTab({ userId, username }: { userId?: string; username?: string })
|
|||
},
|
||||
);
|
||||
|
||||
channel.subscribe().catch(() => {});
|
||||
channel.subscribe();
|
||||
return () => {
|
||||
supabase.removeChannel(channel);
|
||||
};
|
||||
|
|
@ -2657,7 +2657,7 @@ function ChatTab({
|
|||
},
|
||||
);
|
||||
|
||||
channel.subscribe().catch(() => {});
|
||||
channel.subscribe();
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const ARMS: Arm[] = [
|
|||
textColor: "text-purple-400",
|
||||
href: "/staff",
|
||||
icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fc0414efd7af54ef4b821a05d469150d0?format=webp&width=800",
|
||||
tip: "Staff operations & portal",
|
||||
tip: "Staff operations & internal portal",
|
||||
shadowColor: "shadow-purple-500/50",
|
||||
glowColor: "rgba(168, 85, 247, 0.3)",
|
||||
},
|
||||
|
|
@ -37,7 +37,7 @@ const ARMS: Arm[] = [
|
|||
textColor: "text-yellow-400",
|
||||
href: "/labs",
|
||||
icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fd93f7113d34347469e74421c3a3412e5?format=webp&width=800",
|
||||
tip: "R&D and innovation",
|
||||
tip: "R&D pushing innovation boundaries",
|
||||
shadowColor: "shadow-yellow-500/50",
|
||||
glowColor: "rgba(251, 191, 36, 0.3)",
|
||||
},
|
||||
|
|
@ -49,7 +49,7 @@ const ARMS: Arm[] = [
|
|||
textColor: "text-green-400",
|
||||
href: "/gameforge",
|
||||
icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fcd3534c1caa0497abfd44224040c6059?format=webp&width=800",
|
||||
tip: "Ship games monthly",
|
||||
tip: "Games shipped monthly at speed",
|
||||
shadowColor: "shadow-green-500/50",
|
||||
glowColor: "rgba(34, 197, 94, 0.3)",
|
||||
},
|
||||
|
|
@ -61,7 +61,7 @@ const ARMS: Arm[] = [
|
|||
textColor: "text-blue-400",
|
||||
href: "/corp",
|
||||
icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3772073d5b4b49e688ed02480f4cae43?format=webp&width=800",
|
||||
tip: "Enterprise solutions",
|
||||
tip: "Enterprise solutions for scale",
|
||||
shadowColor: "shadow-blue-500/50",
|
||||
glowColor: "rgba(59, 130, 246, 0.3)",
|
||||
},
|
||||
|
|
@ -73,7 +73,7 @@ const ARMS: Arm[] = [
|
|||
textColor: "text-red-400",
|
||||
href: "https://aethex.foundation",
|
||||
icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fc02cb1bf5056479bbb3ea4bd91f0d472?format=webp&width=800",
|
||||
tip: "Community & education",
|
||||
tip: "Community & education initiatives",
|
||||
shadowColor: "shadow-red-500/50",
|
||||
glowColor: "rgba(239, 68, 68, 0.3)",
|
||||
external: true,
|
||||
|
|
@ -86,7 +86,7 @@ const ARMS: Arm[] = [
|
|||
textColor: "text-purple-400",
|
||||
href: "/nexus",
|
||||
icon: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F6df123b87a144b1fb99894d94198d97b?format=webp&width=800",
|
||||
tip: "Talent marketplace",
|
||||
tip: "Talent marketplace & collaboration",
|
||||
shadowColor: "shadow-purple-500/50",
|
||||
glowColor: "rgba(168, 85, 247, 0.3)",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ export default function BotPanel() {
|
|||
</div>
|
||||
)}
|
||||
<Separator className="bg-gray-700" />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Commands</p>
|
||||
<p className="text-lg font-semibold text-white">
|
||||
|
|
@ -379,7 +379,7 @@ export default function BotPanel() {
|
|||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<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-sm text-gray-400">Total Posts</p>
|
||||
|
|
|
|||
|
|
@ -22,25 +22,25 @@ export default function Careers() {
|
|||
icon: <Microscope className="h-6 w-6" />,
|
||||
title: "Innovation First",
|
||||
description:
|
||||
"Push boundaries and explore cutting-edge technologies",
|
||||
"We push boundaries and explore cutting-edge technologies daily",
|
||||
},
|
||||
{
|
||||
icon: <Heart className="h-6 w-6" />,
|
||||
title: "People Matter",
|
||||
description:
|
||||
"Invest in team growth, health, and work-life balance",
|
||||
"We invest in our team's growth, health, and work-life balance",
|
||||
},
|
||||
{
|
||||
icon: <Zap className="h-6 w-6" />,
|
||||
title: "Ship It",
|
||||
description:
|
||||
"Execute over perfection—iterate and learn fast",
|
||||
"We believe in execution over perfection—iterate and learn fast",
|
||||
},
|
||||
{
|
||||
icon: <Users className="h-6 w-6" />,
|
||||
title: "Collaboration",
|
||||
description:
|
||||
"Great ideas come from diverse teams working openly",
|
||||
"Great ideas come from diverse teams working together openly",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -49,8 +49,8 @@ export default function Careers() {
|
|||
"Comprehensive health insurance (medical, dental, vision)",
|
||||
"Unlimited PTO",
|
||||
"Remote-first, work from anywhere",
|
||||
"Equipment budget",
|
||||
"Professional development ($5k/year)",
|
||||
"Equipment budget for your home office",
|
||||
"Professional development fund ($5k/year)",
|
||||
"Team offsites & retreats",
|
||||
"Stock options",
|
||||
"Parental leave",
|
||||
|
|
@ -67,7 +67,7 @@ export default function Careers() {
|
|||
level: "Senior",
|
||||
type: "Full-time",
|
||||
description:
|
||||
"Lead platform architecture and implementation",
|
||||
"Lead architecture and implementation of next-generation platform systems",
|
||||
},
|
||||
{
|
||||
title: "Game Developer",
|
||||
|
|
@ -75,7 +75,7 @@ export default function Careers() {
|
|||
location: "Remote",
|
||||
level: "Mid-level",
|
||||
type: "Full-time",
|
||||
description: "Ship games monthly with world-class team",
|
||||
description: "Ship games monthly with our world-class production team",
|
||||
},
|
||||
{
|
||||
title: "Research Scientist",
|
||||
|
|
@ -84,7 +84,7 @@ export default function Careers() {
|
|||
level: "Senior",
|
||||
type: "Full-time",
|
||||
description:
|
||||
"Explore AI/ML in game development and interactive experiences",
|
||||
"Explore AI/ML applications in game development and interactive experiences",
|
||||
},
|
||||
{
|
||||
title: "Product Manager",
|
||||
|
|
@ -93,7 +93,7 @@ export default function Careers() {
|
|||
level: "Mid-level",
|
||||
type: "Full-time",
|
||||
description:
|
||||
"Shape the future of developer tools and platforms",
|
||||
"Shape the future of our developer tools and platforms",
|
||||
},
|
||||
{
|
||||
title: "UX/UI Designer",
|
||||
|
|
@ -102,7 +102,7 @@ export default function Careers() {
|
|||
level: "Mid-level",
|
||||
type: "Full-time",
|
||||
description:
|
||||
"Design beautiful interfaces for developers",
|
||||
"Design beautiful, intuitive interfaces for millions of developers",
|
||||
},
|
||||
{
|
||||
title: "DevOps Engineer",
|
||||
|
|
@ -110,7 +110,7 @@ export default function Careers() {
|
|||
location: "Remote",
|
||||
level: "Senior",
|
||||
type: "Full-time",
|
||||
description: "Build infrastructure that powers AeThex at scale",
|
||||
description: "Build the infrastructure that powers AeThex at scale",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -66,11 +66,12 @@ export default function Contact() {
|
|||
<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="space-y-3">
|
||||
<h1 className="text-3xl font-bold text-gradient-purple">
|
||||
<h1 className="text-4xl font-bold text-gradient-purple">
|
||||
Contact Us
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
We respond within 1–2 business days
|
||||
Have a project or question? We typically respond within 1–2
|
||||
business days.
|
||||
</p>
|
||||
<Card className="bg-card/50 border-border/50">
|
||||
<CardContent className="p-6 space-y-3">
|
||||
|
|
|
|||
|
|
@ -78,8 +78,8 @@ export default function Corp() {
|
|||
|
||||
const services = [
|
||||
{
|
||||
title: "Custom Software",
|
||||
description: "Enterprise applications",
|
||||
title: "Custom Software Development",
|
||||
description: "Bespoke applications built for enterprise scale",
|
||||
icon: Code,
|
||||
examples: [
|
||||
"Web & mobile applications",
|
||||
|
|
@ -90,8 +90,8 @@ export default function Corp() {
|
|||
color: "from-blue-500 to-cyan-500",
|
||||
},
|
||||
{
|
||||
title: "Tech Consulting",
|
||||
description: "Digital transformation",
|
||||
title: "Technology Consulting",
|
||||
description: "Strategic guidance for digital transformation",
|
||||
icon: Briefcase,
|
||||
examples: [
|
||||
"Architecture design",
|
||||
|
|
@ -102,8 +102,8 @@ export default function Corp() {
|
|||
color: "from-purple-500 to-pink-500",
|
||||
},
|
||||
{
|
||||
title: "Game Development",
|
||||
description: "Metaverse & gaming",
|
||||
title: "Game Development Services",
|
||||
description: "Specialized expertise for gaming companies",
|
||||
icon: Rocket,
|
||||
examples: [
|
||||
"Full game production",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ import {
|
|||
CheckCircle2,
|
||||
Github,
|
||||
Mail,
|
||||
Loader2,
|
||||
Unlink,
|
||||
Link as LinkIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
const DiscordIcon = () => (
|
||||
|
|
@ -146,6 +149,93 @@ 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() {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
|
|
@ -304,14 +394,14 @@ export default function Dashboard() {
|
|||
</div>
|
||||
<Button
|
||||
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 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 text-lg py-6"
|
||||
>
|
||||
Sign In to Dashboard
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => navigate("/onboarding")}
|
||||
variant="outline"
|
||||
className="w-full py-6 border-purple-500/30 text-purple-300 hover:bg-purple-500/10"
|
||||
className="w-full text-lg py-6 border-purple-500/30 text-purple-300 hover:bg-purple-500/10"
|
||||
>
|
||||
Create New Account
|
||||
</Button>
|
||||
|
|
@ -326,15 +416,15 @@ export default function Dashboard() {
|
|||
return (
|
||||
<Layout>
|
||||
<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-12">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12 max-w-6xl space-y-8">
|
||||
{/* Header Section */}
|
||||
<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="space-y-2">
|
||||
<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">
|
||||
<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">
|
||||
Dashboard
|
||||
</h1>
|
||||
<p className="text-gray-400">
|
||||
<p className="text-gray-400 text-lg">
|
||||
Welcome back,{" "}
|
||||
<span className="text-purple-300 font-semibold">
|
||||
{profile?.full_name || user.email?.split("@")[0]}
|
||||
|
|
@ -393,7 +483,7 @@ export default function Dashboard() {
|
|||
onValueChange={setActiveTab}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-4 lg:grid-cols-4 bg-purple-950/30 border border-purple-500/20 p-1">
|
||||
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-4 bg-purple-950/30 border border-purple-500/20 p-1">
|
||||
<TabsTrigger value="realms" className="text-sm md:text-base">
|
||||
<span className="hidden sm:inline">Realms</span>
|
||||
<span className="sm:hidden">Arms</span>
|
||||
|
|
@ -413,15 +503,15 @@ export default function Dashboard() {
|
|||
<TabsContent value="realms" className="space-y-6 animate-fade-in">
|
||||
{/* Developer CTA Card */}
|
||||
{user && (
|
||||
<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-6">
|
||||
<div className="p-4 bg-primary/20 rounded-lg shrink-0">
|
||||
<Card className="p-6 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="p-3 bg-primary/20 rounded-lg shrink-0">
|
||||
<Code className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-semibold mb-3">Building with AeThex?</h3>
|
||||
<p className="text-base text-muted-foreground mb-6">
|
||||
Get API keys and access developer tools
|
||||
<h3 className="text-lg font-semibold mb-2">Building with AeThex?</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Get API keys, access comprehensive documentation, and explore developer tools to integrate AeThex into your applications.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link to="/dev-platform/dashboard">
|
||||
|
|
@ -432,7 +522,12 @@ export default function Dashboard() {
|
|||
</Link>
|
||||
<Link to="/dev-platform/api-reference">
|
||||
<Button size="sm" variant="outline">
|
||||
View Docs
|
||||
View API Docs
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/dev-platform/templates">
|
||||
<Button size="sm" variant="outline">
|
||||
Browse Templates
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -441,7 +536,7 @@ export default function Dashboard() {
|
|||
</Card>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{ARMS.map((arm) => {
|
||||
const IconComponent = arm.icon;
|
||||
return (
|
||||
|
|
@ -460,7 +555,7 @@ export default function Dashboard() {
|
|||
<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`}
|
||||
>
|
||||
<CardContent className="p-8 space-y-6">
|
||||
<CardContent className="p-6 space-y-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -673,7 +768,7 @@ export default function Dashboard() {
|
|||
linkedProviderMap={
|
||||
linkedProviders
|
||||
? Object.fromEntries(
|
||||
linkedProviders.map((p) => [p.provider, p]),
|
||||
linkedProviders.map((p) => [p.provider, p as any]),
|
||||
)
|
||||
: {}
|
||||
}
|
||||
|
|
@ -681,6 +776,9 @@ export default function Dashboard() {
|
|||
onLink={linkProvider}
|
||||
onUnlink={unlinkProvider}
|
||||
/>
|
||||
|
||||
{/* AeThex ID (Authentik SSO) — staff/internal identity */}
|
||||
<AeThexIDConnection user={user} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -84,11 +84,12 @@ export default function Downloads() {
|
|||
<Badge variant="outline" className="mb-4 border-purple-500/50 text-purple-400">
|
||||
Version {CURRENT_VERSION}
|
||||
</Badge>
|
||||
<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">
|
||||
<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">
|
||||
Download AeThex Desktop
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
Desktop Terminal brings full platform access to your computer. Manage projects and stay connected.
|
||||
The AeThex Desktop Terminal brings the full power of the platform to your computer.
|
||||
Access realms, manage projects, and stay connected with your community.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import {
|
|||
TrendingUp,
|
||||
Heart,
|
||||
MessageSquare,
|
||||
User,
|
||||
Avatar,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,32 +1,24 @@
|
|||
import Layout from "@/components/Layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useArmTheme } from "@/contexts/ArmThemeContext";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Heart,
|
||||
BookOpen,
|
||||
Code,
|
||||
Users,
|
||||
Zap,
|
||||
ExternalLink,
|
||||
ArrowRight,
|
||||
GraduationCap,
|
||||
Gamepad2,
|
||||
Users,
|
||||
Code,
|
||||
GraduationCap,
|
||||
Sparkles,
|
||||
Trophy,
|
||||
Compass,
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import LoadingScreen from "@/components/LoadingScreen";
|
||||
import { useArmToast } from "@/hooks/use-arm-toast";
|
||||
|
||||
export default function Foundation() {
|
||||
const navigate = useNavigate();
|
||||
const { theme } = useArmTheme();
|
||||
const armToast = useArmToast();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [showTldr, setShowTldr] = useState(false);
|
||||
const [showExitModal, setShowExitModal] = useState(false);
|
||||
|
|
@ -35,14 +27,31 @@ export default function Foundation() {
|
|||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
if (!toastShownRef.current) {
|
||||
armToast.system("Foundation network connected");
|
||||
toastShownRef.current = true;
|
||||
}
|
||||
}, 900);
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
|
|
@ -96,7 +105,7 @@ export default function Foundation() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 max-w-6xl space-y-24 py-16 lg:py-24">
|
||||
<div className="container mx-auto px-4 max-w-6xl space-y-20 py-16 lg:py-24">
|
||||
{/* Hero Section */}
|
||||
<div className="text-center space-y-8 animate-slide-down">
|
||||
<div className="flex justify-center mb-6">
|
||||
|
|
@ -113,12 +122,12 @@ export default function Foundation() {
|
|||
501(c)(3) Non-Profit Organization
|
||||
</Badge>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
AeThex Foundation
|
||||
</h1>
|
||||
|
||||
<p className="text-lg md:text-xl text-gray-300 max-w-3xl mx-auto leading-relaxed">
|
||||
501(c)(3) non-profit advancing game development
|
||||
<p className="text-xl md:text-2xl text-gray-300 max-w-3xl mx-auto leading-relaxed">
|
||||
Building community, empowering developers, and advancing game development through open-source innovation and mentorship.
|
||||
</p>
|
||||
|
||||
{/* TL;DR Section */}
|
||||
|
|
@ -168,7 +177,7 @@ export default function Foundation() {
|
|||
|
||||
{/* 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">
|
||||
<CardHeader className="pb-3">
|
||||
<CardContent className="pb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Gamepad2 className="h-8 w-8 text-green-400" />
|
||||
<div>
|
||||
|
|
@ -179,311 +188,135 @@ export default function Foundation() {
|
|||
30-day mentorship sprints where developers ship real games
|
||||
</p>
|
||||
</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>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* What is GameForge? */}
|
||||
|
||||
{/* Redirect Notice */}
|
||||
<div className="bg-black/40 rounded-xl p-6 border border-red-500/20 text-center space-y-4">
|
||||
<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">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<Compass className="h-5 w-5 text-green-400" />
|
||||
What is GameForge?
|
||||
</h3>
|
||||
<p className="text-gray-300 leading-relaxed">
|
||||
GameForge is the Foundation's flagship "master-apprentice"
|
||||
mentorship program. It's our "gym" where developers
|
||||
collaborate on focused, high-impact game projects within
|
||||
30-day sprints. Teams of 5 (1 mentor + 4 mentees) tackle real
|
||||
game development challenges and ship playable games to our
|
||||
community arcade.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* The Triple Win */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<Trophy className="h-5 w-5 text-green-400" />
|
||||
Why GameForge Matters
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div className="p-4 bg-black/40 rounded-lg border border-green-500/20 space-y-2">
|
||||
<p className="font-semibold text-green-300">
|
||||
Role 1: Community
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
Our "campfire" where developers meet, collaborate, and
|
||||
build their `aethex.me` passports through real project
|
||||
work.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-black/40 rounded-lg border border-green-500/20 space-y-2">
|
||||
<p className="font-semibold text-green-300">
|
||||
Role 2: Education
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
Learn professional development practices: Code Review
|
||||
(SOP-102), Scope Management (KND-001), and shipping
|
||||
excellence.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-black/40 rounded-lg border border-green-500/20 space-y-2">
|
||||
<p className="font-semibold text-green-300">
|
||||
Role 3: Pipeline
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
Top performers become "Architects" ready to work on
|
||||
high-value projects. Your GameForge portfolio proves you
|
||||
can execute.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* How It Works */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<Zap className="h-5 w-5 text-green-400" />
|
||||
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
|
||||
born—and that the future of gaming lies in collaboration,
|
||||
transparency, and shared knowledge.
|
||||
</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 className="text-lg font-semibold text-white text-center">
|
||||
Foundation Highlights
|
||||
</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 className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<a
|
||||
href="https://aethex.foundation/gameforge"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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"
|
||||
>
|
||||
<div className="p-2 rounded bg-green-500/20 text-green-400">
|
||||
<Gamepad2 className="h-5 w-5" />
|
||||
</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
|
||||
href="https://aethex.foundation/mentorship"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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"
|
||||
>
|
||||
<div className="p-2 rounded bg-red-500/20 text-red-400">
|
||||
<GraduationCap className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-white group-hover:text-red-300 transition-colors">
|
||||
Mentorship Network
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
Learn from industry veterans
|
||||
</p>
|
||||
</div>
|
||||
<ExternalLink className="h-4 w-4 text-gray-500 group-hover:text-red-400" />
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://aethex.foundation/community"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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"
|
||||
>
|
||||
<div className="p-2 rounded bg-blue-500/20 text-blue-400">
|
||||
<Users className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-white group-hover:text-blue-300 transition-colors">
|
||||
Community Hub
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
Connect with developers
|
||||
</p>
|
||||
</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 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>
|
||||
|
||||
{/* Footer Note */}
|
||||
<div className="text-center pt-4 border-t border-red-500/10">
|
||||
<p className="text-sm text-gray-500">
|
||||
The AeThex Foundation is a 501(c)(3) non-profit organization
|
||||
dedicated to advancing game development education and community.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Layout from "@/components/Layout";
|
||||
import GameForgeLayout from "@/components/gameforge/GameForgeLayout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
|
@ -102,7 +102,7 @@ export default function GameForge() {
|
|||
];
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<GameForgeLayout>
|
||||
<div className="relative min-h-screen bg-black text-white overflow-hidden">
|
||||
{/* 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">
|
||||
|
|
@ -154,12 +154,12 @@ export default function GameForge() {
|
|||
Foundation's Game Production Studio
|
||||
</Badge>
|
||||
|
||||
<h1 className={`text-5xl md:text-6xl lg:text-7xl font-bold text-green-300 leading-tight ${theme.fontClass}`}>
|
||||
<h1 className={`text-5xl md:text-6xl lg:text-7xl font-black text-green-300 leading-tight ${theme.fontClass}`}>
|
||||
Ship Games Every Month
|
||||
</h1>
|
||||
|
||||
<p className="text-lg md:text-xl text-green-100/80 max-w-3xl mx-auto leading-relaxed">
|
||||
Ship real games in 30-day sprints
|
||||
<p className="text-xl md:text-2xl 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.
|
||||
</p>
|
||||
|
||||
{/* TL;DR Section */}
|
||||
|
|
@ -478,6 +478,6 @@ export default function GameForge() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Layout>
|
||||
</GameForgeLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,20 +132,20 @@ export default function GetStarted() {
|
|||
|
||||
const platformFeatures = [
|
||||
{
|
||||
title: "XP & Leveling",
|
||||
description: "Earn XP and level up to unlock features",
|
||||
title: "XP & Leveling System",
|
||||
description: "Earn XP for daily logins, completing your profile, creating posts, and earning badges. Level up to unlock new features and recognition.",
|
||||
icon: Trophy,
|
||||
color: "from-yellow-500 to-amber-600",
|
||||
},
|
||||
{
|
||||
title: "AI Agents",
|
||||
description: "10 specialized AI personas for guidance",
|
||||
title: "AI Intelligent Agents",
|
||||
description: "Access 10 specialized AI personas for guidance on networking, game development, ethics, architecture, and more.",
|
||||
icon: Bot,
|
||||
color: "from-purple-500 to-violet-600",
|
||||
},
|
||||
{
|
||||
title: "Creator Passports",
|
||||
description: "Portable profile with achievements and skills",
|
||||
description: "Build a portable profile that aggregates your achievements, verified skills, project history, and mentorship contributions.",
|
||||
icon: IdCard,
|
||||
color: "from-cyan-500 to-blue-600",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { useState } from "react";
|
||||
import SEO from "@/components/SEO";
|
||||
import Layout from "@/components/Layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -27,50 +26,38 @@ const ecosystemPillars = [
|
|||
{
|
||||
icon: Boxes,
|
||||
title: "Six Realms",
|
||||
description: "Specialized APIs for every use case",
|
||||
description: "Nexus, GameForge, Foundation, Labs, Corp, and Staff — each with unique APIs and capabilities",
|
||||
href: "/realms",
|
||||
gradient: "from-purple-500 via-purple-600 to-indigo-600",
|
||||
accentColor: "hsl(var(--primary))",
|
||||
},
|
||||
{
|
||||
icon: Database,
|
||||
title: "Developer APIs",
|
||||
description: "REST APIs for all platforms",
|
||||
description: "Comprehensive REST APIs for users, content, achievements, and more",
|
||||
href: "/dev-platform/api-reference",
|
||||
gradient: "from-blue-500 via-blue-600 to-cyan-600",
|
||||
accentColor: "hsl(var(--primary))",
|
||||
},
|
||||
{
|
||||
icon: Terminal,
|
||||
title: "SDK & Tools",
|
||||
description: "Ship faster with TypeScript SDK",
|
||||
description: "TypeScript SDK, CLI tools, and pre-built templates to ship faster",
|
||||
href: "/dev-platform/quick-start",
|
||||
gradient: "from-cyan-500 via-teal-600 to-emerald-600",
|
||||
accentColor: "hsl(var(--primary))",
|
||||
},
|
||||
{
|
||||
icon: Layers,
|
||||
title: "Marketplace",
|
||||
description: "Premium plugins & integrations",
|
||||
description: "Premium integrations, plugins, and components from the community",
|
||||
href: "/dev-platform/marketplace",
|
||||
gradient: "from-emerald-500 via-green-600 to-lime-600",
|
||||
accentColor: "hsl(var(--primary))",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "Community",
|
||||
description: "12K+ developers building together",
|
||||
description: "Join 12,000+ developers building on AeThex",
|
||||
href: "/community",
|
||||
gradient: "from-amber-500 via-orange-600 to-red-600",
|
||||
accentColor: "hsl(var(--primary))",
|
||||
},
|
||||
{
|
||||
icon: Trophy,
|
||||
title: "Opportunities",
|
||||
description: "Get paid to build",
|
||||
description: "Get paid to build — contracts, bounties, and commissions",
|
||||
href: "/opportunities",
|
||||
gradient: "from-pink-500 via-rose-600 to-red-600",
|
||||
accentColor: "hsl(var(--primary))",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -84,39 +71,40 @@ const stats = [
|
|||
const features = [
|
||||
{
|
||||
icon: Layers,
|
||||
title: "Cross-Platform Integration",
|
||||
description: "One API for all metaverse platforms",
|
||||
title: "Cross-Platform Integration Layer",
|
||||
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",
|
||||
},
|
||||
{
|
||||
icon: Code2,
|
||||
title: "Enterprise Developer Tools",
|
||||
description: "Production-ready SDK and APIs",
|
||||
title: "Enterprise-Grade Developer Tools",
|
||||
description: "TypeScript SDK, REST APIs, unified authentication, cross-platform achievements, content delivery, and CLI tools — all integrated and production-ready",
|
||||
},
|
||||
{
|
||||
icon: Gamepad2,
|
||||
title: "Six Specialized Realms",
|
||||
description: "Unique APIs for every use case",
|
||||
description: "Nexus (social hub), GameForge (games), Foundation (education), Labs (AI/innovation), Corp (business), Staff (governance) — each with unique APIs and tools",
|
||||
},
|
||||
{
|
||||
icon: Trophy,
|
||||
title: "Monetize Your Skills",
|
||||
description: "12K+ developers earning on AeThex",
|
||||
description: "Get paid to build — access contracts, bounties, and commissions. 12K+ developers earning while creating cross-platform games, apps, and integrations",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "Creator Economy",
|
||||
description: "Collaborate and grow your reputation",
|
||||
title: "Thriving Creator Economy",
|
||||
description: "Join squads, collaborate on projects, share assets in the marketplace, and grow your reputation across all six realms",
|
||||
},
|
||||
{
|
||||
icon: Rocket,
|
||||
title: "Ship Fast",
|
||||
description: "150+ examples and one-click deployment",
|
||||
title: "Ship Everywhere, Fast",
|
||||
description: "150+ cross-platform code examples, pre-built templates, OAuth integration, Supabase backend — one-command deployment to every metaverse",
|
||||
},
|
||||
];
|
||||
|
||||
const platforms = ["Roblox", "Minecraft", "Meta Horizon", "Fortnite", "VRChat", "Zepeto"];
|
||||
const platformIcons = [Gamepad2, Boxes, Globe, Zap, Users, Sparkles];
|
||||
|
||||
export default function Index() {
|
||||
const [hoveredCard, setHoveredCard] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<Layout hideFooter>
|
||||
<SEO
|
||||
|
|
@ -129,308 +117,154 @@ export default function Index() {
|
|||
}
|
||||
/>
|
||||
|
||||
{/* Animated Background */}
|
||||
{/* Static background — radial glow only; grid/scanlines come from body::after/::before in global.css */}
|
||||
<div className="fixed inset-0 pointer-events-none overflow-hidden">
|
||||
<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 className="absolute inset-0 bg-[radial-gradient(ellipse_70%_40%_at_50%_-10%,hsl(var(--primary)/0.08),transparent)]" />
|
||||
</div>
|
||||
|
||||
<div className="relative space-y-40 pb-40">
|
||||
<section className="relative min-h-[90vh] flex items-center justify-center overflow-hidden pt-20">
|
||||
<div className="relative text-center max-w-6xl mx-auto space-y-10 px-4">
|
||||
<div className="relative space-y-28 pb-28">
|
||||
|
||||
{/* Hero */}
|
||||
<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
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
initial={{ opacity: 0, y: -12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Badge
|
||||
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" />
|
||||
<Badge className="text-xs px-4 py-1.5 bg-primary/10 border-primary/30 uppercase tracking-widest font-semibold">
|
||||
<Sparkles className="w-3 h-3 mr-1.5 inline" />
|
||||
AeThex Developer Ecosystem
|
||||
</Badge>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
|
||||
<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"
|
||||
>
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-black tracking-tight leading-none">
|
||||
Build on
|
||||
<br />
|
||||
<span className="relative inline-block mt-4">
|
||||
<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)' }}>
|
||||
AeThex
|
||||
</span>
|
||||
<motion.div
|
||||
className="absolute -inset-8 bg-primary blur-3xl opacity-40"
|
||||
animate={{
|
||||
opacity: [0.4, 0.7, 0.4],
|
||||
scale: [1, 1.1, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</h1>
|
||||
</motion.div>
|
||||
|
||||
Build on{" "}
|
||||
<span className="text-primary">AeThex</span>
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
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"
|
||||
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-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.
|
||||
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 Highlights */}
|
||||
|
||||
{/* Platform pills */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
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"
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
className="flex flex-wrap items-center justify-center gap-2 pt-2"
|
||||
>
|
||||
<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>
|
||||
{platforms.map((name, i) => {
|
||||
const Icon = platformIcons[i];
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
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"
|
||||
>
|
||||
<Icon className="w-3.5 h-3.5 text-primary" />
|
||||
<span className="font-medium">{name}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<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
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* CTAs */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.6 }}
|
||||
className="flex flex-wrap gap-4 justify-center pt-8"
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
className="flex flex-wrap gap-3 justify-center pt-4"
|
||||
>
|
||||
<Link to="/dev-platform/quick-start">
|
||||
<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"
|
||||
>
|
||||
<Button size="lg" className="px-8 h-12 font-semibold">
|
||||
Start Building
|
||||
<Rocket className="w-5 h-5 ml-2" />
|
||||
<Rocket className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/dev-platform/api-reference">
|
||||
<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"
|
||||
>
|
||||
<BookOpen className="w-5 h-5 mr-2" />
|
||||
<Button size="lg" variant="outline" className="px-8 h-12 font-semibold">
|
||||
<BookOpen className="w-4 h-4 mr-2" />
|
||||
Explore APIs
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
{/* Stats */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 1, delay: 0.8 }}
|
||||
className="grid grid-cols-2 md:grid-cols-4 gap-8 pt-16 max-w-4xl mx-auto"
|
||||
transition={{ duration: 0.8, delay: 0.6 }}
|
||||
className="grid grid-cols-2 md:grid-cols-4 gap-4 pt-12 max-w-3xl mx-auto"
|
||||
>
|
||||
{stats.map((stat, i) => (
|
||||
<motion.div
|
||||
{stats.map((stat) => (
|
||||
<div
|
||||
key={stat.label}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.8 + i * 0.1 }}
|
||||
className="relative group"
|
||||
className="bg-secondary/40 border border-border rounded-xl p-5 text-center"
|
||||
>
|
||||
<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-4xl md:text-5xl font-black text-primary mb-2">
|
||||
{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>
|
||||
<p className="text-3xl font-black text-primary">{stat.value}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1 font-medium uppercase tracking-wide">{stat.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</motion.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 className="space-y-20 px-4">
|
||||
{/* Ecosystem Pillars */}
|
||||
<section className="space-y-10 px-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="text-center space-y-6"
|
||||
transition={{ duration: 0.6 }}
|
||||
className="text-center space-y-3"
|
||||
>
|
||||
<h2 className="text-5xl md:text-6xl font-black text-primary">
|
||||
The AeThex Ecosystem
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
Six interconnected realms with unique APIs
|
||||
<h2 className="text-4xl md:text-5xl font-black">The AeThex Ecosystem</h2>
|
||||
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||
Six interconnected realms, each with unique capabilities and APIs to power your applications
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4 max-w-6xl mx-auto">
|
||||
{ecosystemPillars.map((pillar, index) => (
|
||||
<motion.div
|
||||
key={pillar.title}
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||
onMouseEnter={() => setHoveredCard(index)}
|
||||
onMouseLeave={() => setHoveredCard(null)}
|
||||
transition={{ duration: 0.5, delay: index * 0.07 }}
|
||||
>
|
||||
<Link to={pillar.href}>
|
||||
<Card className="group relative overflow-hidden h-full border-2 hover:border-transparent transition-all duration-300">
|
||||
<div
|
||||
className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-primary/10"
|
||||
/>
|
||||
|
||||
{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" />
|
||||
<Card className="group h-full border-border hover:border-primary/30 transition-colors duration-200 bg-card">
|
||||
<div className="p-6 space-y-4">
|
||||
<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">
|
||||
<pillar.icon className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-2xl font-bold group-hover:text-primary transition-all duration-300">
|
||||
<div className="space-y-1.5">
|
||||
<h3 className="font-semibold text-foreground group-hover:text-primary transition-colors">
|
||||
{pillar.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{pillar.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-primary group-hover:translate-x-2 transition-transform duration-300">
|
||||
<span className="text-sm font-medium mr-2">Explore</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
<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>
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
@ -440,38 +274,37 @@ export default function Index() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-20 px-4">
|
||||
{/* Why AeThex */}
|
||||
<section className="space-y-10 px-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="text-center space-y-6"
|
||||
transition={{ duration: 0.6 }}
|
||||
className="text-center space-y-3"
|
||||
>
|
||||
<h2 className="text-5xl md:text-6xl font-black text-primary">
|
||||
Why Build on AeThex?
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
Built for creators and developers
|
||||
<h2 className="text-4xl md:text-5xl font-black">Why Build on AeThex?</h2>
|
||||
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||
Join a growing ecosystem designed for creators, developers, and entrepreneurs
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-10 max-w-6xl mx-auto">
|
||||
<div className="grid md:grid-cols-3 gap-4 max-w-6xl mx-auto">
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: index * 0.2 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.08 }}
|
||||
>
|
||||
<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-16 h-16 rounded-2xl bg-primary flex items-center justify-center shadow-2xl shadow-primary/50">
|
||||
<feature.icon className="w-8 h-8 text-primary-foreground" />
|
||||
<Card className="p-6 space-y-4 border-border bg-card h-full">
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 border border-primary/20 flex items-center justify-center">
|
||||
<feature.icon className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-2xl font-bold">{feature.title}</h3>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-foreground">{feature.title}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -481,108 +314,47 @@ export default function Index() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="px-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="relative overflow-hidden rounded-3xl max-w-6xl mx-auto border-2 border-primary/40"
|
||||
transition={{ duration: 0.6 }}
|
||||
className="relative overflow-hidden rounded-2xl max-w-5xl mx-auto border border-primary/20 bg-primary/5"
|
||||
>
|
||||
{/* Animated Background */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/20 via-primary/10 to-background/50 backdrop-blur-xl" />
|
||||
|
||||
{/* Animated Grid */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.05]"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(to right, hsl(var(--primary)) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, hsl(var(--primary)) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: "40px 40px",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 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"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_60%_80%_at_80%_50%,hsl(var(--primary)/0.08),transparent)]" />
|
||||
<div className="relative z-10 p-12 md:p-16 text-center space-y-6">
|
||||
<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" />
|
||||
Start Building Today
|
||||
</Badge>
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-black leading-tight">
|
||||
Ready to Build Something{" "}
|
||||
<span className="text-primary">Epic?</span>
|
||||
</h2>
|
||||
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||
Get your API key and start deploying across{" "}
|
||||
<span className="text-foreground font-medium">5+ metaverse platforms</span> in minutes
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3 justify-center pt-2">
|
||||
<Link to="/dev-platform/dashboard">
|
||||
<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"
|
||||
>
|
||||
<Button size="lg" className="px-8 h-12 font-semibold">
|
||||
Get Your API Key
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/realms">
|
||||
<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"
|
||||
>
|
||||
<Button size="lg" variant="outline" className="px-8 h-12 font-semibold">
|
||||
Explore Realms
|
||||
<Boxes className="w-5 h-5 ml-2" />
|
||||
<Boxes className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,418 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -95,17 +95,17 @@ export default function Investors() {
|
|||
{
|
||||
icon: <Layers className="h-5 w-5" />,
|
||||
title: "Three Engines",
|
||||
desc: "Studios, Platform, and Labs compound value.",
|
||||
desc: "Studios (services), Platform (community), and Labs (R&D) compound value together.",
|
||||
},
|
||||
{
|
||||
icon: <Shield className="h-5 w-5" />,
|
||||
title: "Trust & Quality",
|
||||
desc: "Security-first engineering with measurable delivery.",
|
||||
desc: "Security-first engineering and measurable delivery keep churn low and NPS high.",
|
||||
},
|
||||
{
|
||||
icon: <Target className="h-5 w-5" />,
|
||||
title: "Focused Markets",
|
||||
desc: "Games, real-time apps, and experience platforms.",
|
||||
desc: "High-signal segments: games, real-time apps, and experience platforms.",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -134,11 +134,13 @@ export default function Investors() {
|
|||
<span className="mr-2 inline-flex h-2 w-2 animate-pulse rounded-full bg-red-300" />
|
||||
Investor Relations
|
||||
</Badge>
|
||||
<h1 className="text-3xl font-black tracking-tight text-red-300 sm:text-4xl lg:text-5xl">
|
||||
<h1 className="text-4xl font-black tracking-tight text-red-300 sm:text-5xl lg:text-6xl">
|
||||
AeThex | Building With Conviction
|
||||
</h1>
|
||||
<p className="text-lg text-red-100/90 sm:text-xl">
|
||||
Reliable software and the platform powering creators. Explore our thesis and participation options.
|
||||
We craft reliable, loved software and the platform that powers
|
||||
creators. Explore our thesis, traction, and how to participate
|
||||
in compliant offerings.
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 sm:flex-row">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -154,9 +154,9 @@ export default function Labs() {
|
|||
{/* 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 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]" />
|
||||
<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" />
|
||||
<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 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 bottom-20 right-10 w-96 h-96 bg-yellow-600/10 rounded-full mix-blend-multiply filter blur-3xl animate-pulse" />
|
||||
|
||||
<main className="relative z-10">
|
||||
{/* Hero Section */}
|
||||
|
|
@ -177,12 +177,12 @@ export default function Labs() {
|
|||
Advanced Research & Development
|
||||
</Badge>
|
||||
|
||||
<h1 className={`text-5xl md:text-6xl lg:text-7xl font-bold text-yellow-300 leading-tight ${theme.fontClass}`}>
|
||||
<h1 className={`text-5xl md:text-6xl lg:text-7xl font-black text-yellow-300 leading-tight ${theme.fontClass}`}>
|
||||
The Innovation Engine
|
||||
</h1>
|
||||
|
||||
<p className="text-lg md:text-xl text-yellow-100/80 max-w-3xl mx-auto leading-relaxed">
|
||||
Breakthrough R&D in software, AI, and games
|
||||
<p className="text-xl md:text-2xl 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.
|
||||
</p>
|
||||
|
||||
{/* TL;DR Section */}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ export default function Login() {
|
|||
const [fullName, setFullName] = useState("");
|
||||
const [showReset, setShowReset] = useState(false);
|
||||
const [resetEmail, setResetEmail] = useState("");
|
||||
const [rememberMe, setRememberMe] = useState(true);
|
||||
const [errorFromUrl, setErrorFromUrl] = useState<string | null>(null);
|
||||
const [discordLinkedEmail, setDiscordLinkedEmail] = useState<string | null>(
|
||||
null,
|
||||
|
|
@ -175,6 +176,12 @@ export default function Login() {
|
|||
});
|
||||
} else {
|
||||
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({
|
||||
title: "Signing you in",
|
||||
description: "Redirecting...",
|
||||
|
|
@ -338,6 +345,39 @@ export default function Login() {
|
|||
) : null}
|
||||
{/* Social Login Buttons */}
|
||||
<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">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
Quick Sign In
|
||||
|
|
@ -527,6 +567,8 @@ export default function Login() {
|
|||
<input
|
||||
type="checkbox"
|
||||
className="rounded border-border/50"
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
/>
|
||||
<span className="text-muted-foreground">
|
||||
Remember me
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export default function MaintenancePage() {
|
|||
|
||||
<div className="h-px bg-border" />
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 text-center text-xs">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-center text-xs">
|
||||
<div className="space-y-1">
|
||||
<div className="text-muted-foreground">STATUS</div>
|
||||
<div className="text-blue-400 font-semibold flex items-center justify-center gap-1">
|
||||
|
|
|
|||
|
|
@ -123,12 +123,14 @@ export default function Nexus() {
|
|||
AeThex Nexus
|
||||
</Badge>
|
||||
|
||||
<h1 className="text-4xl font-bold tracking-tight text-purple-300 sm:text-5xl">
|
||||
<h1 className="text-4xl font-black tracking-tight text-purple-300 sm:text-5xl lg:text-6xl">
|
||||
The Talent Nexus
|
||||
</h1>
|
||||
|
||||
<p className="text-base text-purple-100/90">
|
||||
Connect creators with opportunities across all AeThex arms
|
||||
<p className="text-lg text-purple-100/90 sm:text-xl">
|
||||
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>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
|
|
|
|||
|
|
@ -590,7 +590,7 @@ const ProfilePassport = () => {
|
|||
variant="ghost"
|
||||
className="h-8 px-2 text-xs text-aethex-200"
|
||||
>
|
||||
<Link to="/projects/new">
|
||||
<Link to={`/projects/${project.id}`}>
|
||||
View mission
|
||||
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
||||
</Link>
|
||||
|
|
|
|||
280
client/pages/ProjectDetail.tsx
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -71,7 +71,7 @@ export default function Projects() {
|
|||
Projects & Testimonials
|
||||
</h1>
|
||||
<p className="text-muted-foreground max-w-2xl mt-1">
|
||||
AeThex showcase portfolio
|
||||
Studio initiatives across AeThex Platform, Labs, and Studio.
|
||||
</p>
|
||||
</div>
|
||||
{isOwner && (
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ export default function ProjectsAdmin() {
|
|||
value={draft.title}
|
||||
onChange={(e) => setDraft({ ...draft, title: e.target.value })}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<select
|
||||
className="rounded border border-border/40 bg-background/70 px-3 py-2"
|
||||
value={draft.org_unit}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export default function Realms() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12 max-w-6xl relative">
|
||||
{/* Hero Section */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
|
|
@ -91,8 +91,10 @@ export default function Realms() {
|
|||
Choose Your{" "}
|
||||
<span className="text-primary drop-shadow-[0_0_25px_rgba(168,85,247,0.8)]">Realm</span>
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl text-muted-foreground max-w-3xl mx-auto font-light">
|
||||
Unique tools and communities for every role
|
||||
<p className="text-xl md:text-2xl text-muted-foreground max-w-3xl mx-auto font-light">
|
||||
Each realm has unique tools, communities, and opportunities.
|
||||
<br className="hidden md:block" />
|
||||
Your dashboard adapts to your choice.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export default function Squads() {
|
|||
return (
|
||||
<Layout>
|
||||
<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-12">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12 max-w-6xl space-y-8">
|
||||
{/* Header */}
|
||||
<section className="rounded-3xl border border-border/40 bg-background/80 p-6 shadow-2xl backdrop-blur">
|
||||
<div className="flex items-start justify-between">
|
||||
|
|
@ -96,7 +96,8 @@ export default function Squads() {
|
|||
Squads Hub
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Form squads and ship projects together
|
||||
Form squads and ship projects together. Match by skill,
|
||||
timezone, and goals.
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden sm:block p-3 rounded-2xl bg-gradient-to-br from-aethex-500/10 to-neon-blue/10">
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export default function StaffAdmin() {
|
|||
<Card className="bg-slate-900/50 border-purple-500/20">
|
||||
<CardContent className="pt-6">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-5 bg-slate-800/50">
|
||||
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3 md:grid-cols-5 bg-slate-800/50">
|
||||
<TabsTrigger value="users" className="gap-2">
|
||||
<Users className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Users</span>
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export default function StaffChat() {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 h-[600px]">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 h-[calc(100vh-200px)] sm:h-[600px] min-h-[400px]">
|
||||
{/* Channels Sidebar */}
|
||||
<Card className="bg-slate-900/50 border-purple-500/20 lg:col-span-1">
|
||||
<CardHeader>
|
||||
|
|
|
|||