new file: aethex-logos/icons/1024x1024.png
106
README.md
|
|
@ -1,45 +1,54 @@
|
|||
# AeThex Forge - Local Development Setup
|
||||
# AeThex Forge – Local Development Setup
|
||||
|
||||
## Quick Start Guide
|
||||
|
||||
This guide will help you set up and run the AeThex platform locally on your machine.
|
||||
This guide will help you set up and run the AeThex Developer Platform locally.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Node.js** (v18 or higher)
|
||||
- Download from: https://nodejs.org/
|
||||
- This will also install npm (Node Package Manager)
|
||||
- [Download Node.js](https://nodejs.org/)
|
||||
- Includes npm (Node Package Manager)
|
||||
|
||||
2. **Git** (optional, if you want to clone updates)
|
||||
- Download from: https://git-scm.com/
|
||||
2. **Git** (recommended for updates)
|
||||
- [Download Git](https://git-scm.com/)
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Install Node.js
|
||||
- Visit https://nodejs.org/ and download the LTS version
|
||||
- Run the installer and follow the setup wizard
|
||||
- Restart your terminal/PowerShell after installation
|
||||
|
||||
- Download and install the LTS version from [nodejs.org](https://nodejs.org/)
|
||||
- Follow the setup wizard
|
||||
- Restart your terminal after installation
|
||||
|
||||
### 2. Verify Installation
|
||||
Open PowerShell or Command Prompt and run:
|
||||
```powershell
|
||||
|
||||
Open your terminal and run:
|
||||
|
||||
```bash
|
||||
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:
|
||||
```powershell
|
||||
cd C:\Users\PCOEM\Downloads\aethex-forge\aethex-forge
|
||||
|
||||
```bash
|
||||
cd /path/to/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` folder) with the following variables:
|
||||
|
||||
Create a `.env` file in the root directory (`aethex-forge/`) with the following variables:
|
||||
|
||||
**Minimum Required (to run the app):**
|
||||
|
||||
```env
|
||||
# Supabase Configuration (Required)
|
||||
VITE_SUPABASE_URL=your_supabase_url_here
|
||||
|
|
@ -52,6 +61,7 @@ VITE_API_BASE=http://localhost:5000
|
|||
```
|
||||
|
||||
**Optional (for full functionality):**
|
||||
|
||||
```env
|
||||
# Discord Integration
|
||||
DISCORD_CLIENT_ID=your_discord_client_id
|
||||
|
|
@ -76,76 +86,84 @@ VITE_GHOST_API_URL=your_ghost_api_url
|
|||
GHOST_ADMIN_API_KEY=your_ghost_admin_key
|
||||
```
|
||||
|
||||
**Note:** You can start with just the Supabase variables to get the app running. Other features will work once you add their respective credentials.
|
||||
**Note:** You can start with just the Supabase variables to get the app running. Add other credentials as needed for full functionality.
|
||||
|
||||
### 5. Run the Development Server
|
||||
```powershell
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The application will start on **http://localhost:5000**
|
||||
The application will start on **[http://localhost:5000](http://localhost:5000)** (or the port set in your config).
|
||||
|
||||
Open your browser and navigate to that URL to view the application.
|
||||
|
||||
## Available Commands
|
||||
|
||||
- `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
|
||||
- `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
|
||||
|
||||
aethex-forge/
|
||||
aethex-forge/
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
```text
|
||||
aethex-forge/
|
||||
├── 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
|
||||
├── 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
|
||||
```
|
||||
|
||||
## Getting Supabase Credentials
|
||||
|
||||
If you don't have Supabase credentials yet:
|
||||
|
||||
1. Go to https://supabase.com/
|
||||
2. Create a free account
|
||||
3. Create a new project
|
||||
4. Go to Project Settings → API
|
||||
5. Copy:
|
||||
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:
|
||||
- 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:
|
||||
```powershell
|
||||
Remove-Item -Recurse -Force node_modules
|
||||
Remove-Item package-lock.json
|
||||
|
||||
```bash
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
### Environment Variables Not Loading
|
||||
- Make sure `.env` file is in the root `aethex-forge` directory
|
||||
|
||||
- Ensure `.env` is in the root `aethex-forge` directory
|
||||
- Restart the dev server after adding new environment variables
|
||||
- Variables starting with `VITE_` are exposed to the client
|
||||
|
||||
## Need Help?
|
||||
|
||||
- Check the `docs/` folder for detailed documentation
|
||||
- Review `AGENTS.md` for architecture details
|
||||
- See `replit.md` for deployment information
|
||||
- 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`
|
||||
|
||||
|
|
|
|||
BIN
aethex-logos.zip
Normal file
BIN
aethex-logos/arms/corp-512.png
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
aethex-logos/arms/devlink-512.png
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
aethex-logos/arms/foundation-512.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
aethex-logos/arms/gameforge-512.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
aethex-logos/arms/labs-512.png
Normal file
|
After Width: | Height: | Size: 282 KiB |
BIN
aethex-logos/arms/nexus-512.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
aethex-logos/arms/staff-512.png
Normal file
|
After Width: | Height: | Size: 267 KiB |
BIN
aethex-logos/icons/1024x1024.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
aethex-logos/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
aethex-logos/icons/16x16.png
Normal file
|
After Width: | Height: | Size: 491 B |
BIN
aethex-logos/icons/256x256.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
aethex-logos/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1,021 B |
BIN
aethex-logos/icons/48x48.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
aethex-logos/icons/512x512.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
aethex-logos/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
aethex-logos/icons/favicon.ico
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
aethex-logos/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 111 KiB |
16
aethex-logos/icons/icon.svg
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#1e3a5f;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0f172a;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="80" fill="url(#bg)"/>
|
||||
<path d="M256 100 L380 380 L256 320 L132 380 Z" fill="url(#accent)" opacity="0.9"/>
|
||||
<path d="M256 140 L350 340 L256 295 L162 340 Z" fill="#0f172a" opacity="0.3"/>
|
||||
<circle cx="256" cy="220" r="30" fill="#fff" opacity="0.9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 861 B |
BIN
aethex-logos/main/aethex-logo-1200.png
Normal file
|
After Width: | Height: | Size: 887 KiB |
BIN
aethex-logos/main/aethex-logo-512.png
Normal file
|
After Width: | Height: | Size: 222 KiB |
|
|
@ -10,7 +10,6 @@ import { Button } from "@/components/ui/button";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Music,
|
||||
Toggle,
|
||||
ToggleLeft,
|
||||
ToggleRight,
|
||||
ExternalLink,
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ const BlogEditor = ({ onPublish, initialData }: BlogEditorProps) => {
|
|||
|
||||
const handlePublish = async () => {
|
||||
if (!title.trim() || !html.trim()) {
|
||||
toast.error("Title and body are required");
|
||||
toast.error({ title: "Title and body are required" });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ const BlogEditor = ({ onPublish, initialData }: BlogEditorProps) => {
|
|||
}
|
||||
|
||||
const data = await response.json();
|
||||
toast.success(`Post published: ${data.url}`);
|
||||
toast.success({ title: `Post published: ${data.url}` });
|
||||
onPublish?.(true);
|
||||
|
||||
// Reset form
|
||||
|
|
@ -108,7 +108,7 @@ const BlogEditor = ({ onPublish, initialData }: BlogEditorProps) => {
|
|||
setMetaTitle("");
|
||||
setMetaDescription("");
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "Failed to publish post");
|
||||
toast.error({ title: 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("Failed to load mentors");
|
||||
aethexToast.error({ title: "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("Failed to load courses");
|
||||
aethexToast.error({ title: "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("Failed to load achievements");
|
||||
aethexToast.error({ title: "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(
|
||||
`Mentor ${approvalAction === "approve" ? "approved" : "rejected"}`,
|
||||
);
|
||||
aethexToast.success({
|
||||
title: `Mentor ${approvalAction === "approve" ? "approved" : "rejected"}`,
|
||||
});
|
||||
setApprovalDialogOpen(false);
|
||||
setSelectedMentor(null);
|
||||
fetchMentors();
|
||||
} catch (error) {
|
||||
aethexToast.error("Failed to update mentor");
|
||||
aethexToast.error({ title: "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(`Course ${publish ? "published" : "unpublished"}`);
|
||||
aethexToast.success({ title: `Course ${publish ? "published" : "unpublished"}` });
|
||||
fetchCourses();
|
||||
} catch (error) {
|
||||
aethexToast.error("Failed to update course");
|
||||
aethexToast.error({ title: "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("Course deleted");
|
||||
aethexToast.success({ title: "Course deleted" });
|
||||
fetchCourses();
|
||||
} catch (error) {
|
||||
aethexToast.error("Failed to delete course");
|
||||
aethexToast.error({ title: "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("Failed to load opportunities");
|
||||
aethexToast.error({ title: "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("Failed to load disputes");
|
||||
aethexToast.error({ title: "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("Failed to load commissions");
|
||||
aethexToast.error({ title: "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(`Opportunity marked as ${status}`);
|
||||
aethexToast.success({ title: `Opportunity marked as ${status}` });
|
||||
fetchOpportunities();
|
||||
} catch (error) {
|
||||
aethexToast.error("Failed to update opportunity");
|
||||
aethexToast.error({ title: "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(
|
||||
`Opportunity ${featured ? "featured" : "unfeatured"}`,
|
||||
);
|
||||
aethexToast.success({
|
||||
title: `Opportunity ${featured ? "featured" : "unfeatured"}`,
|
||||
});
|
||||
fetchOpportunities();
|
||||
} catch (error) {
|
||||
aethexToast.error("Failed to update opportunity");
|
||||
aethexToast.error({ title: "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(
|
||||
`Dispute ${disputeAction === "resolve" ? "resolved" : "escalated"}`,
|
||||
);
|
||||
aethexToast.success({
|
||||
title: `Dispute ${disputeAction === "resolve" ? "resolved" : "escalated"}`,
|
||||
});
|
||||
setDisputeDialogOpen(false);
|
||||
setSelectedDispute(null);
|
||||
setDisputeResolution("");
|
||||
fetchDisputes();
|
||||
} catch (error) {
|
||||
aethexToast.error("Failed to update dispute");
|
||||
aethexToast.error({ title: "Failed to update dispute" });
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,20 +12,20 @@ export default function MaintenanceToggle() {
|
|||
|
||||
const handleToggle = async () => {
|
||||
if (!canBypass) {
|
||||
aethexToast.error("Only admins can toggle maintenance mode");
|
||||
aethexToast.error({ title: "Only admins can toggle maintenance mode" });
|
||||
return;
|
||||
}
|
||||
|
||||
setToggling(true);
|
||||
try {
|
||||
await toggleMaintenanceMode();
|
||||
aethexToast.success(
|
||||
isMaintenanceMode
|
||||
aethexToast.success({
|
||||
title: isMaintenanceMode
|
||||
? "Maintenance mode disabled - site is now live"
|
||||
: "Maintenance mode enabled - visitors will see maintenance page"
|
||||
);
|
||||
});
|
||||
} catch (error: any) {
|
||||
aethexToast.error(error?.message || "Failed to toggle maintenance mode");
|
||||
aethexToast.error({ title: error?.message || "Failed to toggle maintenance mode" });
|
||||
} finally {
|
||||
setToggling(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("Please enter your DevConnect username", "error");
|
||||
toast.error({ title: "Please enter your DevConnect username" });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -41,17 +41,16 @@ export function DevConnectLinkModal({
|
|||
devconnect_username: username.trim(),
|
||||
devconnect_profile_url: `https://devconnect.sbs/${username.trim()}`,
|
||||
});
|
||||
toast("DevConnect account linked successfully!", "success");
|
||||
toast.success({ title: "DevConnect account linked successfully!" });
|
||||
setUsername("");
|
||||
onOpenChange(false);
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
toast(
|
||||
error instanceof Error
|
||||
toast.error({
|
||||
title: error instanceof Error
|
||||
? error.message
|
||||
: "Failed to link DevConnect account",
|
||||
"error",
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
// Export layout components
|
||||
export { default as DevPlatformLayout } from './layouts/DevPlatformLayout';
|
||||
export { default as ThreeColumnLayout } from './layouts/ThreeColumnLayout';
|
||||
export { DevPlatformLayout } from './layouts/DevPlatformLayout';
|
||||
export { ThreeColumnLayout } from './layouts/ThreeColumnLayout';
|
||||
|
||||
// Export UI components
|
||||
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 { CodeBlock } from './ui/CodeBlock';
|
||||
export { Callout } from './ui/Callout';
|
||||
export { StatCard } from './ui/StatCard';
|
||||
export { ApiEndpointCard } from './ui/ApiEndpointCard';
|
||||
|
||||
// Export feature components
|
||||
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';
|
||||
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';
|
||||
|
|
|
|||
|
|
@ -47,20 +47,20 @@ export const WalletVerification = ({
|
|||
|
||||
const handleConnect = async () => {
|
||||
if (!walletInput.trim()) {
|
||||
aethexToast.warning("Please enter a wallet address");
|
||||
aethexToast.warning({ title: "Please enter a wallet address" });
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = walletInput.trim().toLowerCase();
|
||||
if (!isValidEthereumAddress(normalized)) {
|
||||
aethexToast.warning(
|
||||
"Invalid Ethereum address. Must be 0x followed by 40 hexadecimal characters.",
|
||||
);
|
||||
aethexToast.warning({
|
||||
title: "Invalid Ethereum address. Must be 0x followed by 40 hexadecimal characters.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user?.id) {
|
||||
aethexToast.error("User not authenticated");
|
||||
aethexToast.error({ title: "User not authenticated" });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -86,16 +86,16 @@ export const WalletVerification = ({
|
|||
const data = await response.json();
|
||||
setConnectedWallet(normalized);
|
||||
setWalletInput("");
|
||||
aethexToast.success("✅ Wallet connected successfully!");
|
||||
aethexToast.success({ title: "✅ Wallet connected successfully!" });
|
||||
|
||||
if (onWalletUpdated) {
|
||||
onWalletUpdated(normalized);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("[Wallet Verification] Error:", error?.message);
|
||||
aethexToast.error(
|
||||
error?.message || "Failed to connect wallet. Please try again.",
|
||||
);
|
||||
aethexToast.error({
|
||||
title: error?.message || "Failed to connect wallet. Please try again.",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -123,16 +123,16 @@ export const WalletVerification = ({
|
|||
}
|
||||
|
||||
setConnectedWallet(null);
|
||||
aethexToast.success("Wallet disconnected");
|
||||
aethexToast.success({ title: "Wallet disconnected" });
|
||||
|
||||
if (onWalletUpdated) {
|
||||
onWalletUpdated(null);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("[Wallet Verification] Error:", error?.message);
|
||||
aethexToast.error(
|
||||
error?.message || "Failed to disconnect wallet. Please try again.",
|
||||
);
|
||||
aethexToast.error({
|
||||
title: error?.message || "Failed to disconnect wallet. Please try again.",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import {
|
|||
TrendingUp,
|
||||
Heart,
|
||||
MessageSquare,
|
||||
Avatar,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import SEO from "@/components/SEO";
|
||||
import Layout from "@/components/Layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -115,16 +115,7 @@ const features = [
|
|||
];
|
||||
|
||||
export default function Index() {
|
||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
||||
const [hoveredCard, setHoveredCard] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
setMousePosition({ x: e.clientX, y: e.clientY });
|
||||
};
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
return () => window.removeEventListener("mousemove", handleMouseMove);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout hideFooter>
|
||||
|
|
@ -141,10 +132,10 @@ export default function Index() {
|
|||
{/* Animated Background */}
|
||||
<div className="fixed inset-0 pointer-events-none overflow-hidden">
|
||||
<motion.div
|
||||
className="absolute w-[800px] h-[800px] rounded-full blur-[128px] opacity-20 bg-primary/30"
|
||||
className="absolute w-[600px] h-[600px] rounded-full blur-[100px] opacity-15 bg-primary/30"
|
||||
style={{
|
||||
left: mousePosition.x - 400,
|
||||
top: mousePosition.y - 400,
|
||||
left: '10%',
|
||||
top: '20%',
|
||||
}}
|
||||
animate={{
|
||||
x: [0, 50, 0],
|
||||
|
|
|
|||
418
client/pages/Index.tsx.minimal
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
import { useState } from "react";
|
||||
import SEO from "@/components/SEO";
|
||||
import Layout from "@/components/Layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
ArrowRight,
|
||||
Terminal,
|
||||
Copy,
|
||||
Check,
|
||||
BookOpen,
|
||||
Zap,
|
||||
Shield,
|
||||
Globe,
|
||||
Code2,
|
||||
Database,
|
||||
Users,
|
||||
Boxes,
|
||||
Layers,
|
||||
Trophy,
|
||||
Gamepad2,
|
||||
} from "lucide-react";
|
||||
|
||||
const codeExample = `import { AeThex } from '@aethex/sdk';
|
||||
|
||||
const client = new AeThex({ apiKey: process.env.AETHEX_KEY });
|
||||
|
||||
// Authenticate user across platforms
|
||||
const user = await client.passport.authenticate({
|
||||
platform: 'roblox',
|
||||
userId: '123456789'
|
||||
});
|
||||
|
||||
// Sync achievements, inventory, progress
|
||||
await client.sync({
|
||||
achievements: user.achievements,
|
||||
inventory: user.inventory,
|
||||
progress: user.gameProgress
|
||||
});`;
|
||||
|
||||
const ecosystemPillars = [
|
||||
{
|
||||
icon: Boxes,
|
||||
title: "Six Realms",
|
||||
description: "Specialized APIs for every use case",
|
||||
href: "/realms",
|
||||
gradient: "from-purple-500 to-indigo-600",
|
||||
},
|
||||
{
|
||||
icon: Database,
|
||||
title: "Developer APIs",
|
||||
description: "REST APIs for all platforms",
|
||||
href: "/dev-platform/api-reference",
|
||||
gradient: "from-blue-500 to-cyan-600",
|
||||
},
|
||||
{
|
||||
icon: Terminal,
|
||||
title: "SDK & Tools",
|
||||
description: "Ship faster with TypeScript SDK",
|
||||
href: "/dev-platform/quick-start",
|
||||
gradient: "from-cyan-500 to-emerald-600",
|
||||
},
|
||||
{
|
||||
icon: Layers,
|
||||
title: "Marketplace",
|
||||
description: "Premium plugins & integrations",
|
||||
href: "/dev-platform/marketplace",
|
||||
gradient: "from-emerald-500 to-lime-600",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "Community",
|
||||
description: "12K+ developers building together",
|
||||
href: "/community",
|
||||
gradient: "from-amber-500 to-red-600",
|
||||
},
|
||||
{
|
||||
icon: Trophy,
|
||||
title: "Opportunities",
|
||||
description: "Get paid to build",
|
||||
href: "/opportunities",
|
||||
gradient: "from-pink-500 to-red-600",
|
||||
},
|
||||
];
|
||||
|
||||
const stats = [
|
||||
{ value: "12K+", label: "Developers" },
|
||||
{ value: "2.5M+", label: "API Calls/Day" },
|
||||
{ value: "150+", label: "Examples" },
|
||||
{ value: "6", label: "Realms" },
|
||||
];
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: Globe,
|
||||
title: "Cross-Platform Identity",
|
||||
description: "One passport across Roblox, Minecraft, Fortnite, and more.",
|
||||
},
|
||||
{
|
||||
icon: Database,
|
||||
title: "Universal Data Sync",
|
||||
description: "Sync achievements, inventory, and progress with a single API.",
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: "Enterprise Auth",
|
||||
description: "OAuth 2.0, PKCE, JWT. Production-ready out of the box.",
|
||||
},
|
||||
{
|
||||
icon: Gamepad2,
|
||||
title: "Game Integration",
|
||||
description: "Drop-in SDKs for Roblox, Unity, Unreal, and more.",
|
||||
},
|
||||
];
|
||||
|
||||
const platforms = ["Roblox", "Minecraft", "Fortnite", "Meta Horizon", "Zepeto", "Unity", "Unreal"];
|
||||
|
||||
export default function Index() {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyCode = () => {
|
||||
navigator.clipboard.writeText(codeExample);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout hideFooter>
|
||||
<SEO
|
||||
pageTitle="AeThex | Developer Platform"
|
||||
description="Build cross-platform experiences with the AeThex SDK. One API for identity, data sync, and authentication across metaverse platforms."
|
||||
canonical={typeof window !== "undefined" ? window.location.href : undefined}
|
||||
/>
|
||||
|
||||
{/* Subtle background */}
|
||||
<div className="fixed inset-0 pointer-events-none overflow-hidden -z-10">
|
||||
<div className="absolute top-0 right-0 w-[600px] h-[600px] rounded-full bg-primary/5 blur-3xl" />
|
||||
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] rounded-full bg-primary/5 blur-3xl" />
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.02]"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(to right, hsl(var(--primary)) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, hsl(var(--primary)) 1px, transparent 1px)`,
|
||||
backgroundSize: "60px 60px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative min-h-screen">
|
||||
{/* Hero */}
|
||||
<section className="relative pt-20 pb-16 px-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||
{/* Left: Value Prop */}
|
||||
<div className="space-y-6">
|
||||
<Badge variant="outline" className="text-xs font-mono border-primary/30">
|
||||
v2.4.0 — TypeScript SDK
|
||||
</Badge>
|
||||
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight">
|
||||
One API for
|
||||
<br />
|
||||
<span className="text-primary">cross-platform games</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-lg text-muted-foreground max-w-lg">
|
||||
Connect players across Roblox, Minecraft, Fortnite, and more.
|
||||
Sync identity, achievements, and inventory with a single SDK.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-3 pt-2">
|
||||
<Link to="/dev-platform/quick-start">
|
||||
<Button size="lg" className="font-medium shadow-lg shadow-primary/25">
|
||||
Get Started
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/dev-platform/api-reference">
|
||||
<Button size="lg" variant="outline" className="font-medium">
|
||||
<BookOpen className="w-4 h-4 mr-2" />
|
||||
Read Docs
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Install command */}
|
||||
<div className="flex items-center gap-2 pt-4">
|
||||
<code className="flex-1 bg-muted px-4 py-2.5 rounded-lg font-mono text-sm border">
|
||||
npm install @aethex/sdk
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => navigator.clipboard.writeText("npm install @aethex/sdk")}
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats row */}
|
||||
<div className="grid grid-cols-4 gap-4 pt-6">
|
||||
{stats.map((stat) => (
|
||||
<div key={stat.label} className="text-center">
|
||||
<p className="text-2xl md:text-3xl font-bold text-primary">{stat.value}</p>
|
||||
<p className="text-xs text-muted-foreground">{stat.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Code Example */}
|
||||
<div className="relative">
|
||||
<Card className="bg-zinc-950 border-zinc-800 overflow-hidden shadow-2xl">
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-zinc-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500/80" />
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500/80" />
|
||||
<div className="w-3 h-3 rounded-full bg-green-500/80" />
|
||||
</div>
|
||||
<span className="text-xs text-zinc-500 font-mono">app.ts</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 text-zinc-400 hover:text-white"
|
||||
onClick={copyCode}
|
||||
>
|
||||
{copied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="p-4 text-sm overflow-x-auto">
|
||||
<code className="text-zinc-300 font-mono whitespace-pre-wrap">
|
||||
<span className="text-purple-400">import</span>
|
||||
<span className="text-zinc-300"> {"{"} </span>
|
||||
<span className="text-yellow-300">AeThex</span>
|
||||
<span className="text-zinc-300"> {"}"} </span>
|
||||
<span className="text-purple-400">from</span>
|
||||
<span className="text-green-400"> '@aethex/sdk'</span>
|
||||
<span className="text-zinc-300">;</span>
|
||||
{"\n\n"}
|
||||
<span className="text-purple-400">const</span>
|
||||
<span className="text-blue-300"> client</span>
|
||||
<span className="text-zinc-300"> = </span>
|
||||
<span className="text-purple-400">new</span>
|
||||
<span className="text-yellow-300"> AeThex</span>
|
||||
<span className="text-zinc-300">{"({ "}</span>
|
||||
<span className="text-blue-300">apiKey</span>
|
||||
<span className="text-zinc-300">: </span>
|
||||
<span className="text-blue-300">process.env.</span>
|
||||
<span className="text-zinc-300">AETHEX_KEY {"});"}</span>
|
||||
{"\n\n"}
|
||||
<span className="text-zinc-600">// Authenticate user across platforms</span>
|
||||
{"\n"}
|
||||
<span className="text-purple-400">const</span>
|
||||
<span className="text-blue-300"> user</span>
|
||||
<span className="text-zinc-300"> = </span>
|
||||
<span className="text-purple-400">await</span>
|
||||
<span className="text-blue-300"> client</span>
|
||||
<span className="text-zinc-300">.passport.</span>
|
||||
<span className="text-yellow-300">authenticate</span>
|
||||
<span className="text-zinc-300">{"({"}</span>
|
||||
{"\n"}
|
||||
<span className="text-zinc-300">{" "}</span>
|
||||
<span className="text-blue-300">platform</span>
|
||||
<span className="text-zinc-300">: </span>
|
||||
<span className="text-green-400">'roblox'</span>
|
||||
<span className="text-zinc-300">,</span>
|
||||
{"\n"}
|
||||
<span className="text-zinc-300">{" "}</span>
|
||||
<span className="text-blue-300">userId</span>
|
||||
<span className="text-zinc-300">: </span>
|
||||
<span className="text-green-400">'123456789'</span>
|
||||
{"\n"}
|
||||
<span className="text-zinc-300">{"});"}</span>
|
||||
{"\n\n"}
|
||||
<span className="text-zinc-600">// Sync achievements, inventory, progress</span>
|
||||
{"\n"}
|
||||
<span className="text-purple-400">await</span>
|
||||
<span className="text-blue-300"> client</span>
|
||||
<span className="text-zinc-300">.</span>
|
||||
<span className="text-yellow-300">sync</span>
|
||||
<span className="text-zinc-300">{"({"}</span>
|
||||
{"\n"}
|
||||
<span className="text-zinc-300">{" "}</span>
|
||||
<span className="text-blue-300">achievements</span>
|
||||
<span className="text-zinc-300">: </span>
|
||||
<span className="text-blue-300">user</span>
|
||||
<span className="text-zinc-300">.achievements,</span>
|
||||
{"\n"}
|
||||
<span className="text-zinc-300">{" "}</span>
|
||||
<span className="text-blue-300">inventory</span>
|
||||
<span className="text-zinc-300">: </span>
|
||||
<span className="text-blue-300">user</span>
|
||||
<span className="text-zinc-300">.inventory,</span>
|
||||
{"\n"}
|
||||
<span className="text-zinc-300">{" "}</span>
|
||||
<span className="text-blue-300">progress</span>
|
||||
<span className="text-zinc-300">: </span>
|
||||
<span className="text-blue-300">user</span>
|
||||
<span className="text-zinc-300">.gameProgress</span>
|
||||
{"\n"}
|
||||
<span className="text-zinc-300">{"});"}</span>
|
||||
</code>
|
||||
</pre>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Platforms */}
|
||||
<section className="py-6 px-4 border-y border-border/30">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex flex-wrap items-center justify-center gap-6 md:gap-10 text-muted-foreground">
|
||||
<span className="text-sm font-medium">Works with:</span>
|
||||
{platforms.map((platform) => (
|
||||
<span key={platform} className="text-sm font-medium hover:text-foreground transition-colors cursor-default">
|
||||
{platform}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Ecosystem Pillars */}
|
||||
<section className="py-20 px-4">
|
||||
<div className="max-w-6xl mx-auto space-y-12">
|
||||
<div className="text-center space-y-4">
|
||||
<h2 className="text-3xl md:text-4xl font-bold">The AeThex Ecosystem</h2>
|
||||
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||
Six interconnected realms with specialized APIs for every use case
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{ecosystemPillars.map((pillar) => (
|
||||
<Link key={pillar.title} to={pillar.href}>
|
||||
<Card className="group p-6 h-full hover:border-primary/40 transition-all duration-200 hover:shadow-lg hover:shadow-primary/5">
|
||||
<div className="space-y-4">
|
||||
<div className={`w-14 h-14 rounded-xl bg-gradient-to-br ${pillar.gradient} flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform`}>
|
||||
<pillar.icon className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-semibold group-hover:text-primary transition-colors">
|
||||
{pillar.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{pillar.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center text-primary text-sm font-medium group-hover:translate-x-1 transition-transform">
|
||||
Explore <ArrowRight className="w-4 h-4 ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features */}
|
||||
<section className="py-20 px-4 bg-muted/30">
|
||||
<div className="max-w-6xl mx-auto space-y-12">
|
||||
<div className="text-center space-y-4">
|
||||
<h2 className="text-3xl md:text-4xl font-bold">Built for game developers</h2>
|
||||
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||
Everything you need to connect players across platforms
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||
{features.map((feature) => (
|
||||
<Card key={feature.title} className="p-5 space-y-3">
|
||||
<div className="w-11 h-11 rounded-lg bg-primary/10 flex items-center justify-center">
|
||||
<feature.icon className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-semibold">{feature.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{feature.description}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-20 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Card className="p-8 md:p-12 text-center space-y-6 bg-gradient-to-br from-primary/10 via-primary/5 to-background border-primary/20">
|
||||
<Zap className="w-12 h-12 text-primary mx-auto" />
|
||||
<h2 className="text-3xl md:text-4xl font-bold">Start building today</h2>
|
||||
<p className="text-muted-foreground max-w-lg mx-auto">
|
||||
Get your API key and integrate in minutes. Free tier includes 10K API calls/month.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4 justify-center pt-2">
|
||||
<Link to="/dev-platform/dashboard">
|
||||
<Button size="lg" className="shadow-lg shadow-primary/25">
|
||||
Get API Key
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/dev-platform/quick-start">
|
||||
<Button size="lg" variant="outline">
|
||||
View Quick Start
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer spacer */}
|
||||
<div className="pb-16" />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
|
@ -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] 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" />
|
||||
<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" />
|
||||
|
||||
<main className="relative z-10">
|
||||
{/* Hero Section */}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export default function AdminEthosVerification() {
|
|||
setRequests(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Failed to load verification requests");
|
||||
toast.error({ title: "Failed to load verification requests" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -124,11 +124,11 @@ export default function AdminEthosVerification() {
|
|||
|
||||
if (!response.ok) throw new Error(`Failed to ${confirmAction} artist`);
|
||||
|
||||
toast.success(
|
||||
confirmAction === "approve"
|
||||
toast.success({
|
||||
title: confirmAction === "approve"
|
||||
? "Artist verified successfully! Email sent."
|
||||
: "Artist application rejected. Email sent.",
|
||||
);
|
||||
});
|
||||
|
||||
setIsConfirming(false);
|
||||
setSelectedRequest(null);
|
||||
|
|
@ -136,7 +136,7 @@ export default function AdminEthosVerification() {
|
|||
fetchRequests();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error(`Failed to ${confirmAction} artist`);
|
||||
toast.error({ title: `Failed to ${confirmAction} artist` });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
205
server/index.ts
|
|
@ -1038,6 +1038,211 @@ export function createServer() {
|
|||
}
|
||||
});
|
||||
|
||||
// =====================================================
|
||||
// Foundation OAuth Callback Routes
|
||||
// =====================================================
|
||||
const FOUNDATION_URL = process.env.VITE_FOUNDATION_URL || "https://aethex.foundation";
|
||||
const FOUNDATION_CLIENT_ID = process.env.FOUNDATION_OAUTH_CLIENT_ID || "aethex_corp";
|
||||
const FOUNDATION_CLIENT_SECRET = process.env.FOUNDATION_OAUTH_CLIENT_SECRET;
|
||||
const API_BASE = process.env.VITE_API_BASE || "https://aethex.dev";
|
||||
|
||||
// GET /auth/callback - Receives authorization code from Foundation
|
||||
app.get("/auth/callback", async (req, res) => {
|
||||
const { code, state, error, error_description } = req.query;
|
||||
|
||||
// Handle Foundation errors
|
||||
if (error) {
|
||||
const message = error_description
|
||||
? decodeURIComponent(String(error_description))
|
||||
: String(error);
|
||||
return res.redirect(`/login?error=${error}&message=${encodeURIComponent(message)}`);
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
return res.redirect(`/login?error=no_code&message=${encodeURIComponent("No authorization code received")}`);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("[Foundation OAuth] Received authorization code, initiating token exchange");
|
||||
|
||||
// Exchange code for token
|
||||
if (!FOUNDATION_CLIENT_SECRET) {
|
||||
throw new Error("FOUNDATION_OAUTH_CLIENT_SECRET not configured");
|
||||
}
|
||||
|
||||
const tokenEndpoint = `${FOUNDATION_URL}/api/oauth/token`;
|
||||
const params = new URLSearchParams();
|
||||
params.append("grant_type", "authorization_code");
|
||||
params.append("code", String(code));
|
||||
params.append("client_id", FOUNDATION_CLIENT_ID);
|
||||
params.append("client_secret", FOUNDATION_CLIENT_SECRET);
|
||||
params.append("redirect_uri", `${API_BASE}/auth/callback`);
|
||||
|
||||
const tokenResponse = await fetch(tokenEndpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: params.toString(),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorData = await tokenResponse.json().catch(() => ({}));
|
||||
console.error("[Foundation OAuth] Token exchange failed:", errorData);
|
||||
throw new Error(`Token exchange failed: ${tokenResponse.status}`);
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json();
|
||||
if (!tokenData.access_token) {
|
||||
throw new Error("No access token in Foundation response");
|
||||
}
|
||||
|
||||
// Fetch user info from Foundation
|
||||
const userInfoResponse = await fetch(`${FOUNDATION_URL}/api/oauth/userinfo`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenData.access_token}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!userInfoResponse.ok) {
|
||||
throw new Error(`Failed to fetch user info: ${userInfoResponse.status}`);
|
||||
}
|
||||
|
||||
const userInfo = await userInfoResponse.json();
|
||||
if (!userInfo.id || !userInfo.email) {
|
||||
throw new Error("Invalid user info from Foundation");
|
||||
}
|
||||
|
||||
console.log("[Foundation OAuth] User authenticated:", userInfo.id);
|
||||
|
||||
// Sync user to local database
|
||||
const now = new Date().toISOString();
|
||||
const cacheValidUntil = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
await adminSupabase.from("user_profiles").upsert({
|
||||
id: userInfo.id,
|
||||
email: userInfo.email,
|
||||
username: userInfo.username || null,
|
||||
full_name: userInfo.full_name || null,
|
||||
avatar_url: userInfo.avatar_url || null,
|
||||
profile_completed: userInfo.profile_complete || false,
|
||||
foundation_synced_at: now,
|
||||
cache_valid_until: cacheValidUntil,
|
||||
updated_at: now,
|
||||
});
|
||||
|
||||
// Set session cookies
|
||||
res.cookie("foundation_access_token", tokenData.access_token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "strict",
|
||||
maxAge: (tokenData.expires_in || 3600) * 1000,
|
||||
});
|
||||
res.cookie("auth_user_id", userInfo.id, {
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "strict",
|
||||
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
|
||||
});
|
||||
|
||||
// Redirect to dashboard or stored destination
|
||||
const redirectTo = (req.query.redirect_to as string) || "/dashboard";
|
||||
return res.redirect(redirectTo);
|
||||
} catch (err: any) {
|
||||
console.error("[Foundation OAuth] Callback error:", err);
|
||||
const message = err?.message || "Authentication failed";
|
||||
return res.redirect(`/login?error=auth_failed&message=${encodeURIComponent(message)}`);
|
||||
}
|
||||
});
|
||||
|
||||
// POST /auth/callback/exchange - Exchange code for token (called from frontend)
|
||||
app.post("/auth/callback/exchange", async (req, res) => {
|
||||
const { code } = req.body || {};
|
||||
|
||||
if (!code) {
|
||||
return res.status(400).json({ error: "Authorization code is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
if (!FOUNDATION_CLIENT_SECRET) {
|
||||
throw new Error("FOUNDATION_OAUTH_CLIENT_SECRET not configured");
|
||||
}
|
||||
|
||||
const tokenEndpoint = `${FOUNDATION_URL}/api/oauth/token`;
|
||||
const params = new URLSearchParams();
|
||||
params.append("grant_type", "authorization_code");
|
||||
params.append("code", code);
|
||||
params.append("client_id", FOUNDATION_CLIENT_ID);
|
||||
params.append("client_secret", FOUNDATION_CLIENT_SECRET);
|
||||
params.append("redirect_uri", `${API_BASE}/auth/callback`);
|
||||
|
||||
const tokenResponse = await fetch(tokenEndpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: params.toString(),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorData = await tokenResponse.json().catch(() => ({}));
|
||||
throw new Error(`Token exchange failed: ${tokenResponse.status}`);
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json();
|
||||
if (!tokenData.access_token) {
|
||||
throw new Error("No access token in Foundation response");
|
||||
}
|
||||
|
||||
// Fetch user info from Foundation
|
||||
const userInfoResponse = await fetch(`${FOUNDATION_URL}/api/oauth/userinfo`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenData.access_token}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!userInfoResponse.ok) {
|
||||
throw new Error(`Failed to fetch user info: ${userInfoResponse.status}`);
|
||||
}
|
||||
|
||||
const userInfo = await userInfoResponse.json();
|
||||
|
||||
// Sync user to local database
|
||||
const now = new Date().toISOString();
|
||||
const cacheValidUntil = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
await adminSupabase.from("user_profiles").upsert({
|
||||
id: userInfo.id,
|
||||
email: userInfo.email,
|
||||
username: userInfo.username || null,
|
||||
full_name: userInfo.full_name || null,
|
||||
avatar_url: userInfo.avatar_url || null,
|
||||
profile_completed: userInfo.profile_complete || false,
|
||||
foundation_synced_at: now,
|
||||
cache_valid_until: cacheValidUntil,
|
||||
updated_at: now,
|
||||
});
|
||||
|
||||
// Set session cookies
|
||||
res.cookie("foundation_access_token", tokenData.access_token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "strict",
|
||||
maxAge: (tokenData.expires_in || 3600) * 1000,
|
||||
});
|
||||
res.cookie("auth_user_id", userInfo.id, {
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "strict",
|
||||
maxAge: 30 * 24 * 60 * 60 * 1000,
|
||||
});
|
||||
|
||||
console.log("[Foundation OAuth] Token exchange successful for:", userInfo.id);
|
||||
return res.json({ accessToken: tokenData.access_token, user: userInfo });
|
||||
} catch (err: any) {
|
||||
console.error("[Foundation OAuth] Token exchange error:", err);
|
||||
return res.status(400).json({ error: err?.message || "Token exchange failed" });
|
||||
}
|
||||
});
|
||||
|
||||
// Admin-backed API (service role)
|
||||
try {
|
||||
const ownerEmail = (
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
v2.72.7
|
||||
v2.75.0
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
-- Update OAuth client redirect URIs to include localhost:8080 for local dev
|
||||
-- This enables Foundation login to work in both production and development
|
||||
|
||||
UPDATE public.oauth_clients
|
||||
SET redirect_uris = '["https://aethex.dev/auth/callback", "http://localhost:8080/auth/callback", "http://localhost:3000/auth/callback", "http://localhost:5173/auth/callback"]'::jsonb,
|
||||
updated_at = NOW()
|
||||
WHERE client_id = 'aethex_corp';
|
||||