Compare commits

...

18 commits

Author SHA1 Message Date
AeThex
1a4321a531 fix: await Discord SDK voice subscribes and add missing Activity API endpoints
Some checks failed
Build / build (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Lint & Type Check / lint (push) Has been cancelled
Security Scan / semgrep (push) Has been cancelled
Security Scan / dependency-check (push) Has been cancelled
Test / test (18.x) (push) Has been cancelled
Test / test (20.x) (push) Has been cancelled
- Await SPEAKING_START/SPEAKING_STOP subscribes so try-catch handles 4006 scope errors
- Add GET /api/feed and POST /api/feed/:id/like (community posts alias for Activity)
- Add DELETE /api/activity/polls/:id (soft-delete via is_active=false)
- Add POST /api/activity/challenges/:id/claim (marks challenge progress as completed)
- Add GET /api/activity/badges (all badges, no userId required)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 02:24:32 +00:00
AeThex
be30e01a50 fix: remove .catch() from Supabase channel.subscribe() calls
Some checks are pending
Build / build (push) Waiting to run
Deploy / deploy (push) Waiting to run
Lint & Type Check / lint (push) Waiting to run
Security Scan / semgrep (push) Waiting to run
Security Scan / dependency-check (push) Waiting to run
Test / test (18.x) (push) Waiting to run
Test / test (20.x) (push) Waiting to run
RealtimeChannel.subscribe() returns the channel object, not a Promise.
Calling .catch() on it throws at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 02:10:22 +00:00
AeThex
dbd980a6ec fix: pass VITE_* env vars as Docker build args for client bundle
Some checks are pending
Build / build (push) Waiting to run
Deploy / deploy (push) Waiting to run
Lint & Type Check / lint (push) Waiting to run
Security Scan / semgrep (push) Waiting to run
Security Scan / dependency-check (push) Waiting to run
Test / test (18.x) (push) Waiting to run
Test / test (20.x) (push) Waiting to run
VITE_* variables are baked into the bundle at vite build time. Docker's
env_file only applies at runtime, so they were missing from the build.
Pass them as ARGs from docker-compose so the client bundle includes the
correct Supabase URL and anon key.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 02:04:25 +00:00
AeThex
a68a2b9f8e fix: build client in Docker and serve compiled SPA from Express
Some checks are pending
Build / build (push) Waiting to run
Deploy / deploy (push) Waiting to run
Lint & Type Check / lint (push) Waiting to run
Security Scan / semgrep (push) Waiting to run
Security Scan / dependency-check (push) Waiting to run
Test / test (18.x) (push) Waiting to run
Test / test (20.x) (push) Waiting to run
Vite dev mode is incompatible with Discord Activity's iframe CSP — the
HMR WebSocket is blocked which breaks the React JSX dev runtime (_jsxDEV).

- Build client (vite build) during Docker image build so dist/spa/ exists
- Add express.static serving dist/spa/ assets in server/index.ts
- Add SPA catch-all to serve dist/spa/index.html for non-API routes

The Activity now loads the production compiled bundle instead of Vite's
dev-mode TypeScript modules, resolving the _jsxDEV crash.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 01:59:56 +00:00
AeThex
0b1d7a9441 fix: lazy-load Downloads page to prevent module-level JSX crash in Activity
Some checks are pending
Build / build (push) Waiting to run
Deploy / deploy (push) Waiting to run
Lint & Type Check / lint (push) Waiting to run
Security Scan / semgrep (push) Waiting to run
Security Scan / dependency-check (push) Waiting to run
Test / test (18.x) (push) Waiting to run
Test / test (20.x) (push) Waiting to run
Downloads.tsx has JSX at module initialization (icon fields in const array).
Eagerly importing it crashes the entire app in the Discord Activity iframe
before the React JSX runtime is ready. Lazy loading defers evaluation until
the route is actually navigated to.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 01:40:43 +00:00
AeThex
446ad7159c fix: include server/ in Docker image and fix Vite ssrLoadModule path
Some checks are pending
Build / build (push) Waiting to run
Deploy / deploy (push) Waiting to run
Lint & Type Check / lint (push) Waiting to run
Security Scan / semgrep (push) Waiting to run
Security Scan / dependency-check (push) Waiting to run
Test / test (18.x) (push) Waiting to run
Test / test (20.x) (push) Waiting to run
- Remove 'server' from .dockerignore so the Express server is baked into
  the image (was being excluded, causing 'Cannot find module' on startup)
- Switch from import("./server") to server.ssrLoadModule("/server/index.ts")
  so Vite resolves the path from the project root, not the temp compile dir

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 00:36:35 +00:00
AeThex
885ea76d12 fix: use ssrLoadModule to load Express server in Vite dev plugin
Some checks are pending
Build / build (push) Waiting to run
Deploy / deploy (push) Waiting to run
Lint & Type Check / lint (push) Waiting to run
Security Scan / semgrep (push) Waiting to run
Security Scan / dependency-check (push) Waiting to run
Test / test (18.x) (push) Waiting to run
Test / test (20.x) (push) Waiting to run
Dynamic import("./server") resolves relative to Vite's .vite-temp/
compilation dir, not the project root. ssrLoadModule resolves from
/app root and processes TypeScript correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 00:28:32 +00:00
AeThex
a57cdb029a chore: remove vercel.json — site now served from VPS Docker container
Some checks are pending
Build / build (push) Waiting to run
Deploy / deploy (push) Waiting to run
Lint & Type Check / lint (push) Waiting to run
Security Scan / semgrep (push) Waiting to run
Security Scan / dependency-check (push) Waiting to run
Test / test (18.x) (push) Waiting to run
Test / test (20.x) (push) Waiting to run
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 23:54:54 +00:00
AeThex
f1bcc957f9 fix: Discord Activity token exchange, CSP headers, subscription routes, and static asset 404
- Remove redirect_uri from Discord token exchange (Activities use proxy auth, not redirect flow)
- Add Content-Security-Policy with frame-ancestors for Discord embedding (was only in vercel.json)
- Wire up subscription create-checkout and manage routes in Express
- Add Studio arm to ArmSwitcher with external link
- Prevent SPA catch-all from serving HTML for missing static assets (fixes script.js Unexpected token error)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 23:49:50 +00:00
AeThex
34368e1dde fix: server-side OG/Twitter meta injection for crawler visibility
Some checks failed
Security Scan / semgrep (push) Has been cancelled
Security Scan / dependency-check (push) Has been cancelled
Build / build (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Lint & Type Check / lint (push) Has been cancelled
Test / test (18.x) (push) Has been cancelled
Test / test (20.x) (push) Has been cancelled
Crawlers (Twitter, Discord, Slack) don't execute JavaScript, so the
client-side SEO.tsx useEffect was invisible to them. Every page looked
identical — the hardcoded homepage defaults in index.html.

- node-build.ts: replace simple sendFile with async SSR meta middleware
  that injects per-route title/description/og:*/twitter:* before sending
  HTML. Static route map covers ~15 routes; dynamic lookup queries
  Supabase for /projects/:uuid (title, description, image_url) and
  /passport/:username (full_name, bio) so shared project/profile links
  render correct cards in Discord/Twitter/Slack unfurls.
- index.html: add twitter:site @aethexcorp; SSO.tsx useEffect still
  runs for browser tab updates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 14:30:47 +00:00
AeThex
29a32da48a feat: make staff.aethex.tech work as staff section alias
- Add StaffSubdomainRedirect component: when accessed via staff.aethex.tech,
  auto-redirects to /staff prefix so the subdomain serves the right content
- nginx config updated to proxy to aethex-forge (port 5050) instead of
  dead port 5054

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 08:29:20 +00:00
AeThex
fbc5ed2f40 fix: restore staff & candidate routes — remove dead staff.aethex.tech redirects
staff.aethex.tech doesn't exist. Re-wired /staff and /candidate to the
existing page components already in the codebase:

Staff: Staff, StaffLogin, StaffDashboard, StaffAdmin, StaffChat,
       StaffDocs, StaffDirectory, StaffAchievements, StaffTimeTracking
Candidate: CandidatePortal, CandidateInterviews, CandidateOffers,
           CandidateProfile

Unbuilt staff sub-routes (/staff/announcements etc.) now fall back to
/staff/dashboard instead of 404ing through a dead external domain.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 08:26:12 +00:00
AeThex
1599d0e690 fix: prevent false session logouts and wire up remember-me
- Narrow the unhandledrejection error handler: removed "unauthorized"
  and "auth/" patterns which were too broad and cleared sessions on
  unrelated API 401s or any URL containing "auth/". Now only matches
  specific Supabase strings (invalid refresh token, jwt expired, etc.)
- Wire up the Remember Me checkbox in Login — was purely decorative
  before. Defaults to checked, stores aethex_remember_me in localStorage
- Authentik SSO callback now sets a 30-day cookie so SSO sessions
  survive browser restarts (AuthContext promotes it to localStorage)
- AuthContext clears local session on load if remember-me flag is absent
  (respects user's choice to not stay logged in)
- signOut now removes aethex_remember_me from localStorage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 08:15:47 +00:00
AeThex
7fec93e05c feat: Authentik SSO, nav systems, project pages, and schema fixes
Some checks are pending
Build / build (push) Waiting to run
Deploy / deploy (push) Waiting to run
Lint & Type Check / lint (push) Waiting to run
Security Scan / dependency-check (push) Waiting to run
Security Scan / semgrep (push) Waiting to run
Test / test (18.x) (push) Waiting to run
Test / test (20.x) (push) Waiting to run
Auth & SSO
- Wire Authentik (auth.aethex.tech) as OIDC PKCE SSO provider
- Server-side only flow with HMAC-signed stateless state token
- Account linking via authentik_sub in user metadata
- AeThex ID connection card in Dashboard connections tab
- Unlink endpoint POST /api/auth/authentik/unlink
- Fix node:https helper to bypass undici DNS bug on Node 18
- Fix resolv.conf to use 1.1.1.1/8.8.8.8 in container

Schema & types
- Regenerate database.types.ts from live Supabase schema (23k lines)
- Fix 511 TypeScript errors caused by stale 582-line types file
- Fix UserProfile import in aethex-database-adapter.ts
- Add notifications migration (title, message, read columns)

Server fixes
- Remove badge_color from achievements seed/upsert (column doesn't exist)
- Rename name→title, add slug field in achievements seed
- Remove email from all user_profiles select queries (column doesn't exist)
- Fix email-based achievement target lookup via auth.admin.listUsers
- Add GET /api/projects/:projectId endpoint
- Fix import.meta.dirname → fileURLToPath for Node 18 compatibility
- Expose VITE_APP_VERSION from package.json at build time

Navigation systems
- DevPlatformNav: reorganize into Learn/Build grouped dropdowns with descriptions
- Migrate all 11 dev-platform pages from main Layout to DevPlatformLayout
- Remove dead isDevMode context nav swap from main Layout
- EthosLayout: purple-accented tab bar (Library, Artists, Licensing, Settings)
  with member-only gating and guest CTA — migrate 4 Ethos pages
- GameForgeLayout: orange-branded sidebar with Studio section and lock icons
  for unauthenticated users — migrate GameForge + GameForgeDashboard
- SysBar: live latency ping, status dot (green/yellow/red), real version

Layout dropdown
- Role-gate Admin (owner/admin/founder only) and Internal Docs (+ staff)
- Add Internal section label with separator
- Fix settings link from /dashboard?tab=profile#settings to /dashboard?tab=settings

Project pages
- Add ProjectDetail page at /projects/:projectId
- Fix ProfilePassport "View mission" link from /projects/new to /projects/:id

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 05:01:10 +00:00
AeThex
06b748dade fix: move module-level JSX icons inside component to resolve _jsxDEV error
Some checks failed
Build / build (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Lint & Type Check / lint (push) Has been cancelled
Security Scan / semgrep (push) Has been cancelled
Security Scan / dependency-check (push) Has been cancelled
Test / test (18.x) (push) Has been cancelled
Test / test (20.x) (push) Has been cancelled
2026-04-08 02:02:39 +00:00
AeThex
c67ee049b6 fix: resolve broken auth imports and JSX tag mismatches
Some checks are pending
Build / build (push) Waiting to run
Deploy / deploy (push) Waiting to run
Lint & Type Check / lint (push) Waiting to run
Security Scan / semgrep (push) Waiting to run
Security Scan / dependency-check (push) Waiting to run
Test / test (18.x) (push) Waiting to run
Test / test (20.x) (push) Waiting to run
- Fix 14 files importing useAuth from nonexistent @/lib/auth or @/hooks/useAuth → @/contexts/AuthContext
- Fix ClientReports: Button wrapping Card content, add proper Tabs/TabsContent structure
- Fix ClientInvoices, ClientContracts: </div> → </section> tag mismatch
- Fix ClientSettings: orphaned </TabsContent>, add missing Tabs wrapper and profile tab
- Re-enable 12 disabled pages in App.tsx (hub + staff routes)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 01:39:46 +00:00
root
58c1f539b9 fix: disable broken pages, fix Foundation duplicate import and tag mismatch
Some checks are pending
Build / build (push) Waiting to run
Deploy / deploy (push) Waiting to run
Lint & Type Check / lint (push) Waiting to run
Security Scan / semgrep (push) Waiting to run
Security Scan / dependency-check (push) Waiting to run
Test / test (18.x) (push) Waiting to run
Test / test (20.x) (push) Waiting to run
2026-04-07 06:20:52 +00:00
f4813e7d9b
Merge pull request #2 from AeThex-Corporation/claude/find-unfinished-flows-vKjsD
Some checks failed
Build / build (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Lint & Type Check / lint (push) Has been cancelled
Security Scan / semgrep (push) Has been cancelled
Security Scan / dependency-check (push) Has been cancelled
Test / test (18.x) (push) Has been cancelled
Test / test (20.x) (push) Has been cancelled
Claude/find unfinished flows v kjs d
2026-01-26 15:50:51 -07:00
74 changed files with 26415 additions and 3533 deletions

View file

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

View file

@ -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"]

View file

@ -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(),
},
);

View file

@ -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,7 +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 EthosGuild from "./pages/community/EthosGuild";
import TrackLibrary from "./pages/ethos/TrackLibrary";
import ArtistProfile from "./pages/ethos/ArtistProfile";
import ArtistSettings from "./pages/ethos/ArtistSettings";
@ -72,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";
@ -81,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";
@ -130,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";
@ -155,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";
@ -182,6 +162,21 @@ import DeveloperPlatform from "./pages/dev-platform/DeveloperPlatform";
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();
@ -202,6 +197,7 @@ const App = () => (
<Toaster />
<Analytics />
<BrowserRouter>
<StaffSubdomainRedirect>
<DiscordActivityWrapper>
<SubdomainPassportProvider>
<ArmThemeProvider>
@ -237,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"
@ -286,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
@ -418,211 +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 Onboarding Routes */}
<Route
path="/staff/onboarding"
element={
<RequireAccess>
<StaffOnboarding />
</RequireAccess>
}
/>
<Route
path="/staff/onboarding/checklist"
element={
<RequireAccess>
<StaffOnboardingChecklist />
</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/okrs"
element={
<RequireAccess>
<StaffOKRs />
</RequireAccess>
}
/>
<Route
path="/staff/time-tracking"
element={
<RequireAccess>
<StaffTimeTracking />
</RequireAccess>
}
/>
{/* Candidate Portal Routes */}
<Route
path="/candidate"
element={
<RequireAccess>
<CandidatePortal />
</RequireAccess>
}
/>
<Route
path="/candidate/profile"
element={
<RequireAccess>
<CandidateProfile />
</RequireAccess>
}
/>
<Route
path="/candidate/interviews"
element={
<RequireAccess>
<CandidateInterviews />
</RequireAccess>
}
/>
<Route
path="/candidate/offers"
element={
<RequireAccess>
<CandidateOffers />
</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 />} />
@ -631,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 />} />
@ -699,6 +462,10 @@ const App = () => (
path="curriculum"
element={<DocsCurriculum />}
/>
<Route
path="curriculum/ethos"
element={<DocsCurriculumEthos />}
/>
<Route
path="getting-started"
element={<DocsGettingStarted />}
@ -801,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"
@ -987,6 +672,7 @@ const App = () => (
</ArmThemeProvider>
</SubdomainPassportProvider>
</DiscordActivityWrapper>
</StaffSubdomainRedirect>
</BrowserRouter>
</TooltipProvider>
</DiscordProvider>

View file

@ -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">&#198;</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",

File diff suppressed because it is too large Load diff

View file

@ -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>

View file

@ -26,61 +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",
},
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 {
@ -103,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;

View 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>
);
}

View 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>
);
}

View file

@ -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">

View file

@ -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");

View file

@ -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
));
}

View file

@ -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) {
* {

View file

@ -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
View 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 });
}

File diff suppressed because it is too large Load diff

View file

@ -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);

View file

@ -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 {
@ -678,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]),
)
: {}
}
@ -686,6 +776,9 @@ export default function Dashboard() {
onLink={linkProvider}
onUnlink={unlinkProvider}
/>
{/* AeThex ID (Authentik SSO) — staff/internal identity */}
<AeThexIDConnection user={user} />
</CardContent>
</Card>
</TabsContent>

View file

@ -13,7 +13,7 @@ import {
Sparkles,
Trophy,
Compass,
ExternalLink,
} from "lucide-react";
import { useEffect, useState } from "react";
import LoadingScreen from "@/components/LoadingScreen";
@ -177,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>

View file

@ -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">
@ -478,6 +478,6 @@ export default function GameForge() {
</div>
</div>
)}
</Layout>
</GameForgeLayout>
);
}

View file

@ -1,4 +1,3 @@
import { useState, useEffect } 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: "Nexus, GameForge, Foundation, Labs, Corp, and Staff—each with unique APIs and capabilities",
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: "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: "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 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: "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—contracts, bounties, and commissions",
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))",
},
];
@ -85,47 +72,39 @@ const features = [
{
icon: Layers,
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 or gated gardens",
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-Grade Developer Tools",
description: "TypeScript SDK, REST APIs, unified authentication, cross-platform achievements, content delivery, and CLI tools—all integrated and production-ready",
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: "Nexus (social hub), GameForge (games), Foundation (education), Labs (AI/innovation), Corp (business), Staff (governance)—each with unique APIs and tools",
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: "Get paid to build—access contracts, bounties, and commissions. 12K+ developers earning while creating cross-platform games, apps, and integrations",
description: "Get paid to build access contracts, bounties, and commissions. 12K+ developers earning while creating cross-platform games, apps, and integrations",
},
{
icon: Users,
title: "Thriving Creator Economy",
description: "Join squads, collaborate on projects, share assets in the marketplace that work across all platforms, and grow your reputation across all six realms",
description: "Join squads, collaborate on projects, share assets in the marketplace, and grow your reputation across all six realms",
},
{
icon: Rocket,
title: "Ship Everywhere, Fast",
description: "150+ cross-platform code examples, pre-built templates for VRChat, RecRoom, Spatial, Decentraland, The Sandbox, Roblox, and more—OAuth integration, Supabase backend, and one-command deployment to every metaverse",
description: "150+ cross-platform code examples, pre-built templates, OAuth integration, Supabase backend — one-command deployment to every metaverse",
},
];
export default function Index() {
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [hoveredCard, setHoveredCard] = useState<number | null>(null);
const platforms = ["Roblox", "Minecraft", "Meta Horizon", "Fortnite", "VRChat", "Zepeto"];
const platformIcons = [Gamepad2, Boxes, Globe, Zap, Users, Sparkles];
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setMousePosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove);
}, []);
export default function Index() {
return (
<Layout hideFooter>
<SEO
@ -138,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-[800px] h-[800px] rounded-full blur-[128px] opacity-20 bg-primary/30"
style={{
left: mousePosition.x - 400,
top: mousePosition.y - 400,
}}
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-32 pb-32">
<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-7xl md:text-8xl lg:text-9xl 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-2xl md:text-3xl 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-xl px-10 h-16 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-black uppercase tracking-wide border-2 border-primary/50"
>
<Button size="lg" className="px-8 h-12 font-semibold">
Start Building
<Rocket className="w-6 h-6 ml-3" />
<Rocket className="w-4 h-4 ml-2" />
</Button>
</Link>
<Link to="/dev-platform/api-reference">
<Button
size="lg"
variant="outline"
className="text-xl px-10 h-16 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-black uppercase tracking-wide"
>
<BookOpen className="w-6 h-6 mr-3" />
<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-12 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-4"
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">
<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>
@ -449,38 +274,37 @@ export default function Index() {
</div>
</section>
<section className="space-y-12 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-4"
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">
<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-8 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-8 space-y-6 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-3">
<h3 className="text-2xl font-bold">{feature.title}</h3>
<p className="text-muted-foreground leading-relaxed">
<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>
@ -490,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-xl px-10 h-16 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-black 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-6 h-6 ml-3" />
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</Link>
<Link to="/realms">
<Button
size="lg"
variant="outline"
className="text-xl px-10 h-16 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-black uppercase tracking-wide"
>
<Button size="lg" variant="outline" className="px-8 h-12 font-semibold">
Explore Realms
<Boxes className="w-6 h-6 ml-3" />
<Boxes className="w-4 h-4 ml-2" />
</Button>
</Link>
</motion.div>
</div>
</div>
</motion.div>
</section>
</div>
</Layout>
);

View file

@ -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

View file

@ -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>

View 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>
);
}

View file

@ -29,8 +29,8 @@ import {
ArrowUpRight,
ArrowDownRight,
} from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
interface Analytics {
users: {

View file

@ -42,8 +42,8 @@ import {
Ban,
AlertCircle,
} from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
interface Report {
id: string;

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { Link } from "wouter";
import { Link } from "react-router-dom";
import Layout from "@/components/Layout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
@ -32,8 +32,8 @@ import {
XCircle,
AlertCircle,
} from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
interface Interview {

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { Link } from "wouter";
import { Link } from "react-router-dom";
import Layout from "@/components/Layout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
@ -40,8 +40,8 @@ import {
AlertTriangle,
ExternalLink,
} from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
interface Offer {
id: string;

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { Link } from "wouter";
import { Link } from "react-router-dom";
import Layout from "@/components/Layout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
@ -29,8 +29,8 @@ import {
Gift,
TrendingUp,
} from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
interface ProfileData {
profile: {

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { Link } from "wouter";
import { Link } from "react-router-dom";
import Layout from "@/components/Layout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
@ -37,8 +37,8 @@ import {
Save,
CheckCircle2,
} from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
interface WorkHistory {
company: string;

View file

@ -1,6 +1,6 @@
import { useState, useEffect, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import Layout from "@/components/Layout";
import GameForgeLayout from "@/components/gameforge/GameForgeLayout";
import { Button } from "@/components/ui/button";
import { useAuth } from "@/contexts/AuthContext";
import { useArmTheme } from "@/contexts/ArmThemeContext";
@ -154,7 +154,7 @@ export default function GameForgeDashboard() {
if (!user) {
return (
<Layout>
<GameForgeLayout>
<div className="min-h-screen bg-gradient-to-b from-black via-green-950/30 to-black flex items-center justify-center px-4">
<div className="max-w-md text-center space-y-6">
<h1 className="text-4xl font-bold bg-gradient-to-r from-green-300 to-emerald-300 bg-clip-text text-transparent">
@ -169,12 +169,12 @@ export default function GameForgeDashboard() {
</Button>
</div>
</div>
</Layout>
</GameForgeLayout>
);
}
return (
<Layout>
<GameForgeLayout>
<div
className={`min-h-screen bg-gradient-to-b from-black to-black py-8 ${theme.fontClass}`}
style={{ backgroundImage: theme.wallpaperPattern }}
@ -505,6 +505,6 @@ export default function GameForgeDashboard() {
)}
</div>
</div>
</Layout>
</GameForgeLayout>
);
}

View file

@ -1,4 +1,4 @@
import Layout from "@/components/Layout";
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
import SEO from "@/components/SEO";
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
import { ThreeColumnLayout } from "@/components/dev-platform/layouts/ThreeColumnLayout";
@ -67,7 +67,7 @@ export default function ApiReference() {
);
return (
<Layout>
<DevPlatformLayout>
<SEO pageTitle="API Reference" description="Complete documentation for the AeThex Developer API" />
<Breadcrumbs className="mb-6" />
<ThreeColumnLayout
@ -636,6 +636,6 @@ def make_request(url):
</section>
</div>
</ThreeColumnLayout>
</Layout>
</DevPlatformLayout>
);
}

View file

@ -1,5 +1,5 @@
import { useState } from "react";
import Layout from "@/components/Layout";
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
import SEO from "@/components/SEO";
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
import { ExampleCard } from "@/components/dev-platform/ExampleCard";
@ -165,7 +165,7 @@ export default function CodeExamples() {
});
return (
<Layout>
<DevPlatformLayout>
<SEO pageTitle="Code Examples Repository" description="Production-ready code examples for common use cases and integrations" />
<div className="max-w-6xl mx-auto space-y-8">
{/* Hero Section */}
@ -274,6 +274,6 @@ export default function CodeExamples() {
<Button variant="outline">Submit Example</Button>
</div>
</div>
</Layout>
</DevPlatformLayout>
);
}

View file

@ -1,6 +1,6 @@
import React from "react";
import { Link } from "react-router-dom";
import Layout from "@/components/Layout";
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
import { CodeBlock } from "@/components/dev-platform/ui/CodeBlock";
@ -57,7 +57,7 @@ await game.deploy(['roblox', 'fortnite', 'web']);`;
];
return (
<Layout>
<DevPlatformLayout>
<SEO pageTitle="AeThex Developer Platform" description="Build cross-platform games with AeThex. Ship to Roblox, Fortnite, Web, and Mobile from a single codebase." />
{/* Hero Section */}
<section className="container py-20 md:py-32">
@ -314,6 +314,6 @@ await game.deploy(['roblox', 'fortnite', 'web']);`;
</div>
</div>
</section>
</Layout>
</DevPlatformLayout>
);
}

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import Layout from "@/components/Layout";
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
import SEO from "@/components/SEO";
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
import { StatCard } from "@/components/dev-platform/ui/StatCard";
@ -15,10 +15,10 @@ import {
Activity,
TrendingUp,
Clock,
AlertTriangle,
Loader2,
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { authFetch } from "@/lib/auth-fetch";
interface ApiKey {
id: string;
@ -58,14 +58,14 @@ export default function DeveloperDashboard() {
setIsLoading(true);
try {
// Load API keys
const keysRes = await fetch("/api/developer/keys");
const keysRes = await authFetch("/api/developer/keys");
if (keysRes.ok) {
const keysData = await keysRes.json();
setKeys(keysData.keys || []);
}
// Load developer profile
const profileRes = await fetch("/api/developer/profile");
const profileRes = await authFetch("/api/developer/profile");
if (profileRes.ok) {
const profileData = await profileRes.json();
setProfile(profileData.profile);
@ -102,7 +102,7 @@ export default function DeveloperDashboard() {
expiresInDays?: number;
}) => {
try {
const res = await fetch("/api/developer/keys", {
const res = await authFetch("/api/developer/keys", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
@ -133,7 +133,7 @@ export default function DeveloperDashboard() {
}
try {
const res = await fetch(`/api/developer/keys/${id}`, {
const res = await authFetch(`/api/developer/keys/${id}`, {
method: "DELETE",
});
@ -159,7 +159,7 @@ export default function DeveloperDashboard() {
const handleToggleActive = async (id: string, isActive: boolean) => {
try {
const res = await fetch(`/api/developer/keys/${id}`, {
const res = await authFetch(`/api/developer/keys/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ is_active: isActive }),
@ -196,12 +196,12 @@ export default function DeveloperDashboard() {
if (isLoading) {
return (
<Layout>
<DevPlatformLayout>
<SEO pageTitle="Developer Dashboard" description="Manage your API keys and monitor usage" />
<div className="flex items-center justify-center h-64">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
</Layout>
</DevPlatformLayout>
);
}
@ -214,13 +214,13 @@ export default function DeveloperDashboard() {
});
return (
<Layout>
<DevPlatformLayout>
<SEO pageTitle="Developer Dashboard" description="Manage your API keys and monitor usage" />
<Breadcrumbs className="mb-6" />
<div className="space-y-8">
{/* Warning for expiring keys */}
{expiringSoon.length > 0 && (
<Callout variant="warning">
<Callout type="warning">
<p className="font-medium">
{expiringSoon.length} API key{expiringSoon.length > 1 ? "s" : ""} expiring
soon
@ -240,7 +240,7 @@ export default function DeveloperDashboard() {
icon={Activity}
trend={
stats?.totalRequests > 0
? { value: 12.5, label: "vs last week" }
? { value: 12.5, isPositive: true }
: undefined
}
/>
@ -252,14 +252,13 @@ export default function DeveloperDashboard() {
<StatCard
title="Recently Used"
value={stats?.recentlyUsed || 0}
subtitle="Last 24 hours"
description="Last 24 hours"
icon={Clock}
/>
<StatCard
title="Plan"
value={profile?.plan_tier || "free"}
icon={TrendingUp}
valueClassName="capitalize"
/>
</div>
@ -295,7 +294,7 @@ export default function DeveloperDashboard() {
</div>
{keys.length === 0 ? (
<Callout variant="info">
<Callout type="info">
<p className="font-medium">No API keys yet</p>
<p className="text-sm mt-1">
Create your first API key to start building with AeThex. You can
@ -326,7 +325,7 @@ export default function DeveloperDashboard() {
)}
{keys.length >= (profile?.max_api_keys || 3) && (
<Callout variant="warning">
<Callout type="warning">
<p className="font-medium">API Key Limit Reached</p>
<p className="text-sm mt-1">
You've reached the maximum number of API keys for your plan. Delete
@ -362,7 +361,7 @@ export default function DeveloperDashboard() {
chartType="bar"
/>
<Callout variant="info">
<Callout type="info">
<p className="text-sm">
<strong>Note:</strong> Real-time analytics are coming soon. This
preview shows sample data.
@ -370,7 +369,7 @@ export default function DeveloperDashboard() {
</Callout>
</div>
) : (
<Callout variant="info">
<Callout type="info">
<p className="font-medium">No usage data yet</p>
<p className="text-sm mt-1">
Start making API requests to see your usage analytics here.
@ -387,6 +386,6 @@ export default function DeveloperDashboard() {
onOpenChange={setCreateDialogOpen}
onCreateKey={handleCreateKey}
/>
</Layout>
</DevPlatformLayout>
);
}

View file

@ -1,4 +1,4 @@
import Layout from "@/components/Layout";
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
@ -95,7 +95,7 @@ const steps = [
export default function DeveloperPlatform() {
return (
<Layout>
<DevPlatformLayout>
<SEO
pageTitle="AeThex Developer Platform"
description="Everything you need to build powerful applications with AeThex"
@ -313,6 +313,6 @@ export default function DeveloperPlatform() {
</div>
</section>
</div>
</Layout>
</DevPlatformLayout>
);
}

View file

@ -1,5 +1,5 @@
import { useParams, Link } from "react-router-dom";
import Layout from "@/components/Layout";
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
import SEO from "@/components/SEO";
import { CodeBlock } from "@/components/dev-platform/ui/CodeBlock";
import { CodeTabs } from "@/components/dev-platform/CodeTabs";
@ -377,7 +377,7 @@ export default function ExampleDetail() {
};
return (
<Layout>
<DevPlatformLayout>
<SEO pageTitle={example.title} description={example.description} />
<div className="max-w-5xl mx-auto space-y-8">
{/* Header */}
@ -531,6 +531,6 @@ SUPABASE_SERVICE_KEY=your_service_key`}
</Link>
</div>
</div>
</Layout>
</DevPlatformLayout>
);
}

View file

@ -1,5 +1,5 @@
import { useState } from "react";
import Layout from "@/components/Layout";
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
import SEO from "@/components/SEO";
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
import { MarketplaceCard } from "@/components/dev-platform/MarketplaceCard";
@ -175,7 +175,7 @@ export default function Marketplace() {
});
return (
<Layout>
<DevPlatformLayout>
<SEO pageTitle="Developer Marketplace" description="Premium integrations, plugins, and tools to supercharge your projects" />
<div className="max-w-6xl mx-auto space-y-8">
{/* Hero Section */}
@ -285,6 +285,6 @@ export default function Marketplace() {
</Button>
</div>
</div>
</Layout>
</DevPlatformLayout>
);
}

View file

@ -1,6 +1,6 @@
import { useState } from "react";
import { useParams, Link } from "react-router-dom";
import Layout from "@/components/Layout";
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
import SEO from "@/components/SEO";
import { CodeBlock } from "@/components/dev-platform/ui/CodeBlock";
import { Callout } from "@/components/dev-platform/ui/Callout";
@ -111,7 +111,7 @@ export default function MarketplaceItemDetail() {
const item = itemData[id || ""] || itemData["premium-analytics-dashboard"];
return (
<Layout>
<DevPlatformLayout>
<SEO pageTitle={item.name} description={item.description} />
<div className="max-w-6xl mx-auto space-y-8">
{/* Header */}
@ -441,6 +441,6 @@ npm install @aethex/${item.name.toLowerCase().replace(/\s+/g, "-")}
</Link>
</div>
</div>
</Layout>
</DevPlatformLayout>
);
}

View file

@ -1,4 +1,4 @@
import Layout from "@/components/Layout";
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
import SEO from "@/components/SEO";
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
import { ThreeColumnLayout } from "@/components/dev-platform/layouts/ThreeColumnLayout";
@ -84,7 +84,7 @@ export default function QuickStart() {
);
return (
<Layout>
<DevPlatformLayout>
<SEO pageTitle="Quick Start Guide" description="Get up and running with the AeThex API in minutes" />
<Breadcrumbs className="mb-6" />
<ThreeColumnLayout sidebar={sidebarContent} aside={asideContent}>
@ -509,6 +509,6 @@ jobs.data.forEach(job => {
</section>
</div>
</ThreeColumnLayout>
</Layout>
</DevPlatformLayout>
);
}

View file

@ -1,6 +1,6 @@
import { useState } from "react";
import { useParams, Link } from "react-router-dom";
import Layout from "@/components/Layout";
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
import SEO from "@/components/SEO";
import { CodeBlock } from "@/components/dev-platform/ui/CodeBlock";
import { CodeTabs } from "@/components/dev-platform/CodeTabs";
@ -110,7 +110,7 @@ export default function TemplateDetail() {
const runCommand = "npm run dev";
return (
<Layout>
<DevPlatformLayout>
<SEO pageTitle={template.name} description={template.description} />
<div className="max-w-6xl mx-auto space-y-8">
{/* Header */}
@ -423,6 +423,6 @@ async function getUserProfile() {
</Link>
</div>
</div>
</Layout>
</DevPlatformLayout>
);
}

View file

@ -1,5 +1,5 @@
import { useState } from "react";
import Layout from "@/components/Layout";
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
import SEO from "@/components/SEO";
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
import { TemplateCard } from "@/components/dev-platform/TemplateCard";
@ -165,7 +165,7 @@ export default function Templates() {
});
return (
<Layout>
<DevPlatformLayout>
<SEO pageTitle="Templates Gallery" description="Pre-built templates and starter kits to accelerate your development" />
<div className="max-w-6xl mx-auto space-y-8">
{/* Search & Filters */}
@ -253,6 +253,6 @@ export default function Templates() {
</Button>
</div>
</div>
</Layout>
</DevPlatformLayout>
);
}

View file

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useParams, Link, useNavigate } from "react-router-dom";
import Layout from "@/components/Layout";
import EthosLayout from "@/components/ethos/EthosLayout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@ -90,17 +90,17 @@ export default function ArtistProfile() {
if (loading) {
return (
<Layout>
<EthosLayout>
<div className="py-20 text-center">Loading artist profile...</div>
</Layout>
</EthosLayout>
);
}
if (!artist) {
return (
<Layout>
<EthosLayout>
<div className="py-20 text-center">Artist not found</div>
</Layout>
</EthosLayout>
);
}
@ -115,7 +115,7 @@ export default function ArtistProfile() {
pageTitle={`${artist.user_profiles.full_name} - Ethos Guild Artist`}
description={artist.bio || "Ethos Guild artist profile"}
/>
<Layout>
<EthosLayout>
<div className="bg-slate-950 text-foreground min-h-screen">
{/* Profile Header */}
<section className="border-b border-slate-800 bg-gradient-to-b from-slate-900 to-slate-950 py-12">
@ -293,7 +293,7 @@ export default function ArtistProfile() {
</div>
</section>
</div>
</Layout>
</EthosLayout>
</>
);
}

View file

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import Layout from "@/components/Layout";
import EthosLayout from "@/components/ethos/EthosLayout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@ -356,16 +356,16 @@ export default function ArtistSettings() {
if (loading) {
return (
<Layout>
<EthosLayout>
<div className="py-20 text-center">Loading settings...</div>
</Layout>
</EthosLayout>
);
}
return (
<>
<SEO pageTitle="Artist Settings - Ethos Guild" />
<Layout>
<EthosLayout>
<div className="bg-slate-950 text-foreground min-h-screen">
<div className="container mx-auto px-4 max-w-4xl py-12">
<div className="space-y-8">
@ -778,7 +778,7 @@ export default function ArtistSettings() {
</div>
)}
</div>
</Layout>
</EthosLayout>
</>
);
}

View file

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import Layout from "@/components/Layout";
import EthosLayout from "@/components/ethos/EthosLayout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@ -131,9 +131,9 @@ export default function LicensingDashboard() {
if (loading) {
return (
<Layout>
<EthosLayout>
<div className="py-20 text-center">Loading agreements...</div>
</Layout>
</EthosLayout>
);
}
@ -143,7 +143,7 @@ export default function LicensingDashboard() {
return (
<>
<SEO pageTitle="Licensing Dashboard - Ethos Guild" />
<Layout>
<EthosLayout>
<div className="bg-slate-950 text-foreground min-h-screen">
<div className="container mx-auto px-4 max-w-4xl py-12">
{/* Header */}
@ -280,7 +280,7 @@ export default function LicensingDashboard() {
</Tabs>
</div>
</div>
</Layout>
</EthosLayout>
</>
);
}

View file

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import Layout from "@/components/Layout";
import EthosLayout from "@/components/ethos/EthosLayout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@ -113,7 +113,7 @@ export default function TrackLibrary() {
pageTitle="Ethos Track Library"
description="Browse music and sound effects from Ethos Guild artists"
/>
<Layout>
<EthosLayout>
<div className="bg-slate-950 text-foreground min-h-screen">
{/* Hero Section */}
<section className="relative border-b border-slate-800 bg-gradient-to-b from-slate-900 to-slate-950 py-16">
@ -317,7 +317,7 @@ export default function TrackLibrary() {
</div>
</section>
</div>
</Layout>
</EthosLayout>
</>
);
}

View file

@ -155,19 +155,7 @@ export default function ClientContracts() {
</div>
</section>
<section className="py-12">
<div className="container mx-auto max-w-6xl px-4">
<Card className="bg-slate-800/30 border-slate-700">
<CardContent className="p-12 text-center">
<FileText className="h-12 w-12 text-slate-600 mx-auto mb-4" />
<p className="text-slate-400 mb-6">
Contract management coming soon
</p>
</CardContent>
</Card>
</div>
</div>
<div className="container mx-auto max-w-6xl px-4 py-8 space-y-6">
{/* Filters */}
<div className="flex flex-col md:flex-row gap-4">
<div className="relative flex-1">
@ -431,7 +419,8 @@ export default function ClientContracts() {
)}
</div>
)}
</div>
</div>
</main>
</div>
</Layout>
);

View file

@ -187,31 +187,7 @@ export default function ClientInvoices() {
</div>
</section>
<section className="py-12">
<div className="container mx-auto max-w-6xl px-4">
<Card className="bg-slate-800/30 border-slate-700">
<CardContent className="p-12 text-center">
<FileText className="h-12 w-12 text-slate-600 mx-auto mb-4" />
<p className="text-slate-400 mb-6">
Invoice tracking coming soon
</p>
<Button
variant="outline"
onClick={() => navigate("/hub/client")}
>
Back to Portal
</Button>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-red-950/40 to-red-900/20 border-red-500/20">
<CardContent className="p-4">
<p className="text-xs text-gray-400 uppercase">Overdue</p>
<p className="text-2xl font-bold text-red-400">${(stats.overdue / 1000).toFixed(1)}k</p>
</CardContent>
</Card>
</div>
</div>
<div className="container mx-auto max-w-6xl px-4 py-8 space-y-6">
{/* Filters */}
<div className="flex flex-col md:flex-row gap-4">
<div className="relative flex-1">
@ -447,7 +423,8 @@ export default function ClientInvoices() {
)}
</div>
)}
</div>
</div>
</main>
</div>
</Layout>
);

View file

@ -203,61 +203,76 @@ export default function ClientReports() {
<section className="py-12">
<div className="container mx-auto max-w-6xl px-4">
<Card className="bg-slate-800/30 border-slate-700">
<CardContent className="p-12 text-center">
<TrendingUp className="h-12 w-12 text-slate-600 mx-auto mb-4" />
<p className="text-slate-400 mb-6">
Detailed project reports and analytics coming soon
</p>
<Button
variant="outline"
onClick={() => navigate("/hub/client")}
>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle>{project.title}</CardTitle>
<CardDescription>
{new Date(project.start_date).toLocaleDateString()} - {new Date(project.end_date).toLocaleDateString()}
</CardDescription>
</div>
<Badge className={project.status === "active"
? "bg-green-500/20 text-green-300"
: "bg-blue-500/20 text-blue-300"
}>
{project.status}
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div className="p-3 bg-black/30 rounded-lg">
<p className="text-xs text-gray-400">Progress</p>
<p className="text-lg font-bold text-white">{project.progress}%</p>
</div>
<div className="p-3 bg-black/30 rounded-lg">
<p className="text-xs text-gray-400">Budget Spent</p>
<p className="text-lg font-bold text-purple-400">
${(project.budget_spent / 1000).toFixed(0)}k / ${(project.budget_total / 1000).toFixed(0)}k
</p>
</div>
<div className="p-3 bg-black/30 rounded-lg">
<p className="text-xs text-gray-400">Hours Logged</p>
<p className="text-lg font-bold text-cyan-400">
{project.hours_logged} / {project.hours_estimated}
</p>
</div>
<div className="p-3 bg-black/30 rounded-lg">
<p className="text-xs text-gray-400">Team Size</p>
<p className="text-lg font-bold text-white">{project.team_size}</p>
</div>
</div>
<Progress value={project.progress} className="h-2" />
</CardContent>
</Card>
))
)}
</TabsContent>
<Tabs defaultValue="overview" className="space-y-6">
<TabsList className="bg-slate-800/50 border border-slate-700">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="budget">Budget</TabsTrigger>
<TabsTrigger value="time">Time</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-6">
{projects.length === 0 ? (
<Card className="bg-slate-800/30 border-slate-700">
<CardContent className="p-12 text-center">
<TrendingUp className="h-12 w-12 text-slate-600 mx-auto mb-4" />
<p className="text-slate-400 mb-6">
Detailed project reports and analytics coming soon
</p>
<Button
variant="outline"
onClick={() => navigate("/hub/client")}
>
Back to Hub
</Button>
</CardContent>
</Card>
) : (
projects.map((project) => (
<Card key={project.id}>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle>{project.title}</CardTitle>
<CardDescription>
{new Date(project.start_date).toLocaleDateString()} - {new Date(project.end_date).toLocaleDateString()}
</CardDescription>
</div>
<Badge className={project.status === "active"
? "bg-green-500/20 text-green-300"
: "bg-blue-500/20 text-blue-300"
}>
{project.status}
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div className="p-3 bg-black/30 rounded-lg">
<p className="text-xs text-gray-400">Progress</p>
<p className="text-lg font-bold text-white">{project.progress}%</p>
</div>
<div className="p-3 bg-black/30 rounded-lg">
<p className="text-xs text-gray-400">Budget Spent</p>
<p className="text-lg font-bold text-purple-400">
${(project.budget_spent / 1000).toFixed(0)}k / ${(project.budget_total / 1000).toFixed(0)}k
</p>
</div>
<div className="p-3 bg-black/30 rounded-lg">
<p className="text-xs text-gray-400">Hours Logged</p>
<p className="text-lg font-bold text-cyan-400">
{project.hours_logged} / {project.hours_estimated}
</p>
</div>
<div className="p-3 bg-black/30 rounded-lg">
<p className="text-xs text-gray-400">Team Size</p>
<p className="text-lg font-bold text-white">{project.team_size}</p>
</div>
</div>
<Progress value={project.progress} className="h-2" />
</CardContent>
</Card>
))
)}
</TabsContent>
{/* Budget Analysis Tab */}
<TabsContent value="budget" className="space-y-6">
@ -318,7 +333,9 @@ export default function ClientReports() {
</Card>
</TabsContent>
</Tabs>
</div>
</div>
</section>
</main>
</div>
</Layout>
);

View file

@ -284,24 +284,33 @@ export default function ClientSettings() {
<section className="py-12">
<div className="container mx-auto max-w-6xl px-4">
<Card className="bg-slate-800/30 border-slate-700">
<CardContent className="p-12 text-center">
<Settings className="h-12 w-12 text-slate-600 mx-auto mb-4" />
<p className="text-slate-400 mb-6">
Account settings and preferences coming soon
</p>
<Button
variant="outline"
onClick={() => navigate("/hub/client")}
>
Back to Portal
</Button>
</CardContent>
</Card>
</TabsContent>
<Tabs defaultValue="profile" className="space-y-6">
<TabsList className="bg-slate-800/50 border border-slate-700">
<TabsTrigger value="profile">Profile</TabsTrigger>
<TabsTrigger value="team">Team</TabsTrigger>
<TabsTrigger value="notifications">Notifications</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
<TabsTrigger value="security">Security</TabsTrigger>
</TabsList>
<TabsContent value="profile" className="space-y-6">
<Card className="bg-slate-800/30 border-slate-700">
<CardContent className="p-12 text-center">
<Settings className="h-12 w-12 text-slate-600 mx-auto mb-4" />
<p className="text-slate-400 mb-6">
Account settings and preferences coming soon
</p>
<Button
variant="outline"
onClick={() => navigate("/hub/client")}
>
Back to Portal
</Button>
</CardContent>
</Card>
</TabsContent>
{/* Team Tab */}
<TabsContent value="team" className="space-y-6">
{/* Team Tab */}
<TabsContent value="team" className="space-y-6">
<Card className="bg-slate-900/50 border-slate-700">
<CardHeader>
<CardTitle>Team Members</CardTitle>
@ -555,7 +564,9 @@ export default function ClientSettings() {
</Card>
</TabsContent>
</Tabs>
</div>
</div>
</section>
</main>
</div>
</Layout>
);

View file

@ -11,8 +11,8 @@ import {
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Bell, Pin, Loader2, Eye, EyeOff } from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
interface Announcement {
id: string;

View file

@ -10,8 +10,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import { DollarSign, FileText, Calendar, CheckCircle, AlertCircle, Plus, Loader2 } from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
interface Expense {
id: string;

View file

@ -21,8 +21,8 @@ import {
Coins,
} from "lucide-react";
import { Input } from "@/components/ui/input";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
import {
Dialog,
DialogContent,

View file

@ -24,8 +24,8 @@ import {
ThumbsUp,
Eye,
} from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
interface KnowledgeArticle {
id: string;

View file

@ -21,8 +21,8 @@ import {
CheckCircle,
Loader2,
} from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
interface Course {
id: string;

View file

@ -37,8 +37,8 @@ import {
ChevronUp,
Trash2,
} from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
interface KeyResult {
id: string;

View file

@ -27,8 +27,8 @@ import {
Coffee,
Loader2,
} from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
interface OnboardingData {
progress: {

View file

@ -27,8 +27,8 @@ import {
Briefcase,
Target,
} from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
interface ChecklistItem {
id: string;

View file

@ -20,8 +20,8 @@ import {
Users,
Loader2,
} from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
import {
Dialog,
DialogContent,

View file

@ -19,8 +19,8 @@ import {
Plus,
Calendar,
} from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
import {
Dialog,
DialogContent,

View file

@ -22,8 +22,8 @@ import {
ChevronDown,
ChevronUp,
} from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
interface HandbookSection {
id: string;

View file

@ -37,8 +37,8 @@ import {
Trash2,
Edit,
} from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
import { useAuth } from "@/contexts/AuthContext";
import { aethexToast } from "@/lib/aethex-toast";
interface Project {
id: string;

14
docker-compose.yml Normal file
View file

@ -0,0 +1,14 @@
services:
aethex-forge:
build:
context: .
args:
- VITE_SUPABASE_URL=${VITE_SUPABASE_URL}
- VITE_SUPABASE_ANON_KEY=${VITE_SUPABASE_ANON_KEY}
- VITE_AUTHENTIK_PROVIDER=${VITE_AUTHENTIK_PROVIDER}
container_name: aethex-forge
restart: unless-stopped
ports:
- "5050:5000"
env_file: .env
command: npm run dev

View file

@ -77,6 +77,7 @@
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@aethexcorp" />
<meta
name="twitter:title"
content="AeThex — Developer Platform, Projects, Community"

28
package-lock.json generated
View file

@ -22,6 +22,7 @@
"png-to-ico": "^3.0.1",
"sharp": "^0.34.5",
"stripe": "^15.12.0",
"wouter": "^3.9.0",
"zod": "^3.23.8"
},
"devDependencies": {
@ -13120,6 +13121,11 @@
"node": ">=8"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@ -14684,6 +14690,14 @@
"dev": true,
"license": "MIT"
},
"node_modules/regexparam": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz",
"integrity": "sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==",
"engines": {
"node": ">=8"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -16671,7 +16685,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@ -17569,6 +17582,19 @@
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wouter": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/wouter/-/wouter-3.9.0.tgz",
"integrity": "sha512-sF/od/PIgqEQBQcrN7a2x3MX6MQE6nW0ygCfy9hQuUkuB28wEZuu/6M5GyqkrrEu9M6jxdkgE12yDFsQMKos4Q==",
"dependencies": {
"mitt": "^3.0.1",
"regexparam": "^3.0.0",
"use-sync-external-store": "^1.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

View file

@ -46,6 +46,7 @@
"png-to-ico": "^3.0.1",
"sharp": "^0.34.5",
"stripe": "^15.12.0",
"wouter": "^3.9.0",
"zod": "^3.23.8"
},
"devDependencies": {

View file

@ -2,13 +2,48 @@ import "dotenv/config";
import "dotenv/config";
import express from "express";
import cors from "cors";
import path from "path";
import { adminSupabase } from "./supabase";
import { emailService } from "./email";
import { randomUUID, createHash, createVerify, randomBytes } from "crypto";
import { randomUUID, createHash, createVerify, randomBytes, createHmac } from "crypto";
import * as https from "https";
import * as http from "http";
// httpsPost / httpsGet — use Node's https module which respects /etc/hosts (unlike fetch/undici)
function httpsRequest(url: string, options: { method?: string; headers?: Record<string, string>; body?: string }): Promise<{ status: number; text: () => string; json: () => any }> {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const lib = parsed.protocol === "https:" ? https : http;
const reqOpts = {
hostname: parsed.hostname,
port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
path: parsed.pathname + parsed.search,
method: options.method || "GET",
headers: options.headers || {},
};
const req = lib.request(reqOpts, (res) => {
const chunks: Buffer[] = [];
res.on("data", (c) => chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c)));
res.on("end", () => {
const raw = Buffer.concat(chunks).toString("utf8");
resolve({
status: res.statusCode || 0,
text: () => raw,
json: () => JSON.parse(raw),
});
});
});
req.on("error", reject);
if (options.body) req.write(options.body);
req.end();
});
}
import blogIndexHandler from "../api/blog/index";
import blogSlugHandler from "../api/blog/[slug]";
import aiChatHandler from "../api/ai/chat";
import aiTitleHandler from "../api/ai/title";
import createCheckoutHandler from "../api/subscriptions/create-checkout";
import manageSubscriptionHandler from "../api/subscriptions/manage";
// Developer API Keys handlers
import {
@ -308,6 +343,11 @@ export function createServer() {
app.use((req, res, next) => {
// Allow embedding in iframes (Discord Activities need this)
res.setHeader("X-Frame-Options", "ALLOWALL");
// CSP with frame-ancestors for Discord Activity embedding
res.setHeader(
"Content-Security-Policy",
"default-src 'self' https: data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: blob: https:; font-src 'self' data: https:; connect-src 'self' https: wss:; frame-ancestors 'self' https://discord.com https://*.discord.com https://*.discordsays.com",
);
// Allow Discord to access the iframe
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader(
@ -384,7 +424,7 @@ export function createServer() {
const { data: user, error } = await adminSupabase
.from("user_profiles")
.select(
"id, username, full_name, bio, avatar_url, banner_url, location, website_url, github_url, linkedin_url, twitter_url, role, level, total_xp, user_type, experience_level, current_streak, longest_streak, created_at, updated_at",
"id, username, full_name, bio, avatar_url, location, website_url, github_url, linkedin_url, twitter_url, roles, level, total_xp, user_type, experience_level, current_streak, longest_streak, created_at, updated_at",
)
.eq("username", username)
.single();
@ -402,11 +442,10 @@ export function createServer() {
achievement_id,
achievements(
id,
name,
title,
description,
icon,
category,
badge_color
category
)
`,
)
@ -645,25 +684,19 @@ export function createServer() {
}
// First try exact match by name
let query = adminSupabase
let { data, error } = await (adminSupabase as any)
.from("projects")
.select(
"id, title, slug, description, user_id, created_at, updated_at, status, image_url, website",
)
.eq("slug", projectname);
let { data, error } = await query.single();
.select("id, title, slug, description, user_id, created_at, updated_at, status, image_url, website")
.eq("slug", projectname)
.single();
// If not found by slug, try by title (case-insensitive)
if (error && error.code === "PGRST116") {
query = adminSupabase
const response = await (adminSupabase as any)
.from("projects")
.select(
"id, title, slug, description, user_id, created_at, updated_at, status, image_url, website",
)
.select("id, title, slug, description, user_id, created_at, updated_at, status, image_url, website")
.ilike("title", projectname);
const response = await query;
if (response.data && response.data.length > 0) {
data = response.data[0];
error = null;
@ -698,6 +731,35 @@ export function createServer() {
}
});
// Public project detail by ID
app.get("/api/projects/:projectId", async (req, res) => {
try {
const { projectId } = req.params;
const { data: project, error } = await adminSupabase
.from("projects")
.select("id, title, description, status, technologies, github_url, live_url, image_url, engine, priority, progress, created_at, updated_at, user_id, owner_user_id")
.eq("id", projectId)
.single();
if (error || !project) {
return res.status(404).json({ error: "Project not found" });
}
const ownerId = (project as any).owner_user_id || (project as any).user_id;
const { data: owner } = ownerId
? await adminSupabase
.from("user_profiles")
.select("id, username, full_name, avatar_url")
.eq("id", ownerId)
.maybeSingle()
: { data: null };
return res.json({ project, owner: owner ?? null });
} catch (e: any) {
return res.status(500).json({ error: e?.message || "Failed to fetch project" });
}
});
// DevConnect REST proxy (GET only)
app.get("/api/devconnect/rest/:table", async (req, res) => {
try {
@ -1761,9 +1823,6 @@ export function createServer() {
client_secret: clientSecret,
grant_type: "authorization_code",
code,
redirect_uri:
process.env.DISCORD_ACTIVITY_REDIRECT_URI ||
"https://aethex.dev/activity",
}).toString(),
},
);
@ -2758,6 +2817,41 @@ export function createServer() {
}
});
// Activity feed alias (used by Discord Activity)
app.get("/api/feed", async (req, res) => {
const limit = Math.max(1, Math.min(50, Number(req.query.limit) || 10));
try {
const { data, error } = await adminSupabase
.from("community_posts")
.select(`id, title, content, likes_count, author_id, created_at, user_profiles ( username, full_name, avatar_url )`)
.eq("is_published", true)
.order("created_at", { ascending: false })
.limit(limit);
if (error) return res.status(500).json({ error: error.message });
res.json({ data: data || [] });
} catch (e: any) {
res.status(500).json({ error: e?.message || String(e) });
}
});
app.post("/api/feed/:id/like", async (req, res) => {
const postId = req.params.id;
try {
const { data: post } = await adminSupabase
.from("community_posts")
.select("likes_count")
.eq("id", postId)
.single();
await adminSupabase
.from("community_posts")
.update({ likes_count: (post?.likes_count || 0) + 1 })
.eq("id", postId);
res.json({ ok: true });
} catch (e: any) {
res.status(500).json({ error: e?.message || String(e) });
}
});
app.get("/api/user/:id/posts", async (req, res) => {
const userId = req.params.id;
try {
@ -3212,6 +3306,16 @@ export function createServer() {
.upsert(rows, { onConflict: "user_id,achievement_id" as any });
if (iErr && iErr.code !== "23505")
return res.status(500).json({ error: iErr.message });
// Notify user of each achievement awarded
if (rows.length) {
const awardedNames = (achievements || []).map((a: any) => a.name).join(", ");
await adminSupabase.from("notifications").insert({
user_id,
type: "success",
title: `🏆 Achievement${rows.length > 1 ? "s" : ""} unlocked!`,
message: awardedNames,
});
}
return res.json({ ok: true, awarded: rows.length });
} catch (e: any) {
console.error("[API] achievements/award exception", e);
@ -3257,42 +3361,42 @@ export function createServer() {
const CORE_ACHIEVEMENTS = [
{
id: "welcome-to-aethex",
name: "Welcome to AeThex",
slug: "welcome-to-aethex",
title: "Welcome to AeThex",
description: "Completed onboarding and joined the AeThex network.",
icon: "🎉",
badge_color: "#7C3AED",
xp_reward: 250,
},
{
id: "aethex-explorer",
name: "AeThex Explorer",
slug: "aethex-explorer",
title: "AeThex Explorer",
description: "Engaged with community initiatives and posted first update.",
icon: "🧭",
badge_color: "#0EA5E9",
xp_reward: 400,
},
{
id: "community-champion",
name: "Community Champion",
slug: "community-champion",
title: "Community Champion",
description: "Contributed feedback, resolved bugs, and mentored squads.",
icon: "🏆",
badge_color: "#22C55E",
xp_reward: 750,
},
{
id: "workshop-architect",
name: "Workshop Architect",
slug: "workshop-architect",
title: "Workshop Architect",
description: "Published a high-impact mod or toolkit adopted by teams.",
icon: "🛠️",
badge_color: "#F97316",
xp_reward: 1200,
},
{
id: "god-mode",
name: "GOD Mode",
slug: "god-mode",
title: "GOD Mode",
description: "Legendary status awarded by AeThex studio leadership.",
icon: "⚡",
badge_color: "#FACC15",
xp_reward: 5000,
},
];
@ -3317,10 +3421,10 @@ export function createServer() {
const { error } = await adminSupabase.from("achievements").upsert(
{
id: uuidId,
name: achievement.name,
slug: achievement.slug,
title: achievement.title,
description: achievement.description,
icon: achievement.icon,
badge_color: achievement.badge_color,
xp_reward: achievement.xp_reward,
},
{ onConflict: "id", ignoreDuplicates: true },
@ -3329,7 +3433,7 @@ export function createServer() {
if (error && error.code !== "23505") {
console.error(`Failed to upsert achievement ${achievement.id}:`, error);
} else {
seededAchievements[achievement.name] = uuidId;
seededAchievements[achievement.title] = uuidId;
}
}
@ -3341,20 +3445,33 @@ export function createServer() {
const awardedAchievementIds: string[] = [];
if (canAwardToTarget && (targetEmail || targetUsername)) {
let query = adminSupabase.from("user_profiles").select("id, email, username");
let targetProfile: { id: string; username: string | null } | null = null;
if (targetEmail) {
query = query.eq("email", targetEmail);
// Look up by email via auth.admin, then fetch profile by user id
const { data: authList } = await adminSupabase.auth.admin.listUsers();
const authUser = authList?.users?.find((u) => u.email === targetEmail);
if (authUser) {
const { data: prof } = await adminSupabase
.from("user_profiles")
.select("id, username")
.eq("id", authUser.id)
.single();
targetProfile = prof ?? null;
}
} else if (targetUsername) {
query = query.eq("username", targetUsername);
const { data: prof } = await adminSupabase
.from("user_profiles")
.select("id, username")
.eq("username", targetUsername)
.single();
targetProfile = prof ?? null;
}
const { data: userProfile } = await query.single();
if (userProfile?.id) {
targetUserId = userProfile.id;
if (targetProfile?.id) {
targetUserId = targetProfile.id;
// Check if target user is an admin (for GOD Mode)
const isTargetAdmin = userProfile.email === "mrpiglr@gmail.com" || userProfile.username === "mrpiglr";
const isTargetAdmin = targetEmail === "mrpiglr@gmail.com" || targetProfile.username === "mrpiglr";
// Award Welcome achievement to the user
const welcomeId = seededAchievements["Welcome to AeThex"];
@ -3633,6 +3750,26 @@ export function createServer() {
.select()
.single();
if (error) return res.status(500).json({ error: error.message });
// Notify team owner
const { data: team } = await adminSupabase
.from("activity_teams")
.select("owner_id, name")
.eq("id", teamId)
.single();
const { data: applicant } = await adminSupabase
.from("user_profiles")
.select("username, full_name")
.eq("id", user_id)
.single();
if (team?.owner_id && team.owner_id !== user_id) {
const applicantName = (applicant as any)?.full_name || (applicant as any)?.username || "Someone";
await adminSupabase.from("notifications").insert({
user_id: team.owner_id,
type: "info",
title: `📋 New team application`,
message: `${applicantName} applied to join ${(team as any).name}${role_applied ? ` as ${role_applied}` : ""}`,
});
}
res.json(data);
} catch (e: any) {
res.status(500).json({ error: e?.message || String(e) });
@ -3692,6 +3829,23 @@ export function createServer() {
adminSupabase.from("activity_projects").update({ upvotes: adminSupabase.rpc("get_upvote_count", { pid: projectId }) }).eq("id", projectId);
});
// Notify project owner (not self-upvotes, and throttle: only on milestone counts)
const { data: project } = await adminSupabase
.from("activity_projects")
.select("owner_id, title, upvotes")
.eq("id", projectId)
.single();
const upvotes = (project as any)?.upvotes || 0;
const milestones = [5, 10, 25, 50, 100];
if (project && (project as any).owner_id !== user_id && milestones.includes(upvotes)) {
await adminSupabase.from("notifications").insert({
user_id: (project as any).owner_id,
type: "success",
title: `🚀 ${upvotes} upvotes on your project`,
message: `"${(project as any).title}" just hit ${upvotes} upvotes!`,
});
}
res.json({ ok: true });
} catch (e: any) {
res.status(500).json({ error: e?.message || String(e) });
@ -3743,6 +3897,20 @@ export function createServer() {
}
});
app.delete("/api/activity/polls/:id", async (req, res) => {
const pollId = req.params.id;
try {
const { error } = await adminSupabase
.from("activity_polls")
.update({ is_active: false })
.eq("id", pollId);
if (error) return res.status(500).json({ error: error.message });
res.json({ ok: true });
} catch (e: any) {
res.status(500).json({ error: e?.message || String(e) });
}
});
app.post("/api/activity/polls/:id/vote", async (req, res) => {
const pollId = req.params.id;
const { user_id, option_index } = req.body || {};
@ -3823,6 +3991,29 @@ export function createServer() {
}
});
app.post("/api/activity/challenges/:id/claim", async (req, res) => {
const challengeId = req.params.id;
const { user_id } = req.body || {};
if (!user_id) return res.status(400).json({ error: "user_id required" });
try {
const { data, error } = await adminSupabase
.from("activity_challenge_progress")
.upsert({
challenge_id: challengeId,
user_id,
progress: 1,
completed: true,
completed_at: new Date().toISOString(),
}, { onConflict: "challenge_id,user_id" })
.select()
.single();
if (error) return res.status(500).json({ error: error.message });
res.json(data);
} catch (e: any) {
res.status(500).json({ error: e?.message || String(e) });
}
});
app.get("/api/activity/challenges/:id/progress", async (req, res) => {
const challengeId = req.params.id;
const user_id = req.query.user_id as string;
@ -3934,6 +4125,25 @@ export function createServer() {
}
});
// All badges (for Activity badges tab when no userId)
app.get("/api/activity/badges", async (req, res) => {
try {
const { data, error } = await adminSupabase
.from("badges")
.select("*")
.order("created_at", { ascending: true });
if (error) {
if (error.code === "42P01" || error.message?.includes("does not exist")) {
return res.json([]);
}
return res.status(500).json({ error: error.message });
}
res.json(data || []);
} catch (e: any) {
res.status(500).json({ error: e?.message || String(e) });
}
});
// User Badges (real data)
app.get("/api/activity/badges/:userId", async (req, res) => {
const userId = req.params.userId;
@ -6527,7 +6737,7 @@ export function createServer() {
verified,
total_downloads,
created_at,
user_profiles(id, full_name, avatar_url, email)
user_profiles(id, full_name, avatar_url)
`,
)
.eq("user_id", id)
@ -7666,7 +7876,7 @@ export function createServer() {
.from("nexus_applications")
.select(`
*,
creator:user_profiles(id, full_name, avatar_url, email),
creator:user_profiles(id, full_name, avatar_url),
creator_profile:nexus_creator_profiles(skills, experience_level, hourly_rate, rating, review_count),
opportunity:nexus_opportunities(id, title)
`)
@ -7710,7 +7920,7 @@ export function createServer() {
.from("nexus_applications")
.select(`
*,
creator:user_profiles(id, full_name, avatar_url, email),
creator:user_profiles(id, full_name, avatar_url),
creator_profile:nexus_creator_profiles(skills, experience_level, hourly_rate, rating, review_count)
`)
.eq("opportunity_id", opportunityId)
@ -7787,6 +7997,272 @@ export function createServer() {
}
});
// ─── Authentik SSO ────────────────────────────────────────────────────────
// Server-side OIDC PKCE flow against auth.aethex.tech.
// Stateless signed state token — survives server restarts, no in-memory store needed.
// state = base64url(payload).hmac where payload = base64url(JSON({verifier,redirectTo,exp}))
const AK_STATE_SECRET = process.env.AUTHENTIK_CLIENT_SECRET || process.env.SUPABASE_SERVICE_ROLE_KEY || "fallback-secret";
const signAkState = (verifier: string, redirectTo: string): string => {
const payload = Buffer.from(JSON.stringify({ v: verifier, r: redirectTo, exp: Date.now() + 10 * 60 * 1000 })).toString("base64url");
const sig = createHmac("sha256", AK_STATE_SECRET).update(payload).digest("base64url");
return `${payload}.${sig}`;
};
const verifyAkState = (state: string): { verifier: string; redirectTo: string } | null => {
try {
const dot = state.lastIndexOf(".");
if (dot === -1) return null;
const payload = state.slice(0, dot);
const sig = state.slice(dot + 1);
const expected = createHmac("sha256", AK_STATE_SECRET).update(payload).digest("base64url");
if (sig !== expected) return null;
const data = JSON.parse(Buffer.from(payload, "base64url").toString());
if (!data.exp || Date.now() > data.exp) return null;
return { verifier: data.v, redirectTo: data.r || "/dashboard" };
} catch {
return null;
}
};
app.get("/api/auth/authentik/start", (req, res) => {
try {
const clientId = process.env.AUTHENTIK_CLIENT_ID;
const authentikBase = process.env.AUTHENTIK_BASE_URL || "https://auth.aethex.tech";
if (!clientId || clientId === "REPLACE_WITH_YOUR_AUTHENTIK_CLIENT_ID") {
return res.status(500).send("Authentik SSO is not configured yet. Set AUTHENTIK_CLIENT_ID in .env");
}
const baseUrl = process.env.PUBLIC_BASE_URL || process.env.SITE_URL || "https://aethex.dev";
const redirectUri = `${baseUrl}/api/auth/authentik/callback`;
// PKCE
const codeVerifier = randomBytes(48).toString("base64url");
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
const redirectTo = (req.query.redirectTo as string) || "/dashboard";
const state = signAkState(codeVerifier, redirectTo);
const params = new URLSearchParams({
client_id: clientId,
response_type: "code",
redirect_uri: redirectUri,
scope: "openid email profile",
state,
code_challenge: codeChallenge,
code_challenge_method: "S256",
});
const authorizeUrl = `${authentikBase}/application/o/authorize/?${params.toString()}`;
console.log("[Authentik] Starting OIDC flow, redirectUri:", redirectUri);
return res.redirect(302, authorizeUrl);
} catch (e: any) {
console.error("[Authentik] start error:", e);
return res.status(500).json({ error: e?.message || String(e) });
}
});
app.get("/api/auth/authentik/callback", async (req, res) => {
try {
const code = req.query.code as string;
const state = req.query.state as string;
const error = req.query.error as string;
const errorDesc = req.query.error_description as string;
if (error) {
console.error("[Authentik] callback error from provider:", error, errorDesc);
return res.redirect(`/login?error=${encodeURIComponent(error)}&desc=${encodeURIComponent(errorDesc || "")}`);
}
if (!code || !state) {
return res.redirect("/login?error=no_code");
}
// Verify signed state — CSRF-safe, restart-safe
const stateData = verifyAkState(state);
if (!stateData) {
console.error("[Authentik] invalid/expired state");
return res.redirect("/login?error=state_mismatch");
}
const { verifier: codeVerifier, redirectTo } = stateData;
const clientId = process.env.AUTHENTIK_CLIENT_ID!;
const clientSecret = process.env.AUTHENTIK_CLIENT_SECRET!;
const authentikBase = process.env.AUTHENTIK_BASE_URL || "https://auth.aethex.tech";
const providerSlug = process.env.AUTHENTIK_PROVIDER_SLUG || "aethex-forge";
const baseUrl = process.env.PUBLIC_BASE_URL || process.env.SITE_URL || "https://aethex.dev";
const redirectUri = `${baseUrl}/api/auth/authentik/callback`;
// Exchange code for tokens — endpoint from discovery, no provider slug in path
const tokenUrl = `${authentikBase}/application/o/token/`;
const tokenBody = new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: redirectUri,
client_id: clientId,
client_secret: clientSecret,
code_verifier: codeVerifier,
});
const tokenResp = await httpsRequest(tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: tokenBody.toString(),
});
if (tokenResp.status < 200 || tokenResp.status >= 300) {
const tokenErr = tokenResp.text();
console.error("[Authentik] token exchange failed — status:", tokenResp.status, "url:", tokenUrl, "body:", tokenErr, "redirect_uri:", redirectUri, "client_id:", clientId);
return res.redirect(`/login?error=token_exchange_failed`);
}
const tokens = tokenResp.json() as { access_token: string; id_token?: string };
// Fetch user info
const userInfoUrl = `${authentikBase}/application/o/userinfo/`;
const userInfoResp = await httpsRequest(userInfoUrl, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
if (userInfoResp.status < 200 || userInfoResp.status >= 300) {
console.error("[Authentik] userinfo failed:", userInfoResp.text());
return res.redirect("/login?error=userinfo_failed");
}
const userInfo = userInfoResp.json() as {
sub: string;
email: string;
email_verified?: boolean;
name?: string;
preferred_username?: string;
given_name?: string;
family_name?: string;
groups?: string[];
};
if (!userInfo.email) {
console.error("[Authentik] no email in userinfo:", userInfo);
return res.redirect("/login?error=no_email");
}
console.log("[Authentik] userinfo:", { sub: userInfo.sub, email: userInfo.email });
// Find or create Supabase user
// Priority: 1) pre-linked authentik_sub in metadata, 2) email match, 3) create new
const { data: existingUsers, error: lookupError } = await adminSupabase.auth.admin.listUsers({ perPage: 1000 });
if (lookupError) {
console.error("[Authentik] user lookup error:", lookupError);
return res.redirect("/login?error=user_lookup");
}
const allUsers = existingUsers?.users || [];
// Check for pre-linked sub first — this is how existing accounts get tied to Authentik
let supabaseUser = allUsers.find((u: any) => u.user_metadata?.authentik_sub === userInfo.sub)
// Fall back to email match
?? allUsers.find((u: any) => u.email?.toLowerCase() === userInfo.email.toLowerCase());
if (!supabaseUser) {
// Create new user
const { data: newUser, error: createError } = await adminSupabase.auth.admin.createUser({
email: userInfo.email,
email_confirm: true,
user_metadata: {
full_name: userInfo.name || userInfo.preferred_username || userInfo.email.split("@")[0],
authentik_sub: userInfo.sub,
provider: "authentik",
},
});
if (createError || !newUser?.user) {
console.error("[Authentik] user create error:", createError);
return res.redirect("/login?error=user_create_failed");
}
supabaseUser = newUser.user;
console.log("[Authentik] Created new Supabase user:", supabaseUser.id);
} else {
// Update metadata to note Authentik link
await adminSupabase.auth.admin.updateUserById(supabaseUser.id, {
user_metadata: {
...supabaseUser.user_metadata,
authentik_sub: userInfo.sub,
authentik_linked: true,
},
});
console.log("[Authentik] Linked existing Supabase user:", supabaseUser.id);
}
// Generate a Supabase magic link using the MATCHED user's email, not the Authentik email.
// This ensures we sign into mrpiglr@gmail.com even when Authentik says mrpiglr@aethex.dev.
const { data: linkData, error: linkError } = await adminSupabase.auth.admin.generateLink({
type: "magiclink",
email: supabaseUser.email!,
options: {
redirectTo: `${baseUrl}${redirectTo}`,
},
});
if (linkError || !linkData?.properties?.action_link) {
console.error("[Authentik] generateLink error:", linkError);
return res.redirect("/login?error=link_gen_failed");
}
// Clear flow cookies
res.clearCookie("ak_state", { path: "/" });
res.clearCookie("ak_verifier", { path: "/" });
res.clearCookie("ak_redirect", { path: "/" });
// Set remember-me cookie so the client-side AuthContext keeps this SSO
// session alive across browser restarts — SSO users always want to stay logged in.
res.cookie("aethex_sso_remember", "1", {
httpOnly: false, // readable by JS so AuthContext can copy to localStorage
secure: true,
sameSite: "lax",
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
path: "/",
});
console.log("[Authentik] Redirecting user to Supabase action link → dashboard");
return res.redirect(302, linkData.properties.action_link);
} catch (e: any) {
console.error("[Authentik] callback error:", e);
return res.redirect(`/login?error=server_error`);
}
});
// Unlink AeThex ID — removes authentik_sub from user metadata
app.post("/api/auth/authentik/unlink", async (req, res) => {
try {
const authHeader = req.headers.authorization || "";
const token = authHeader.replace(/^Bearer\s+/i, "");
if (!token) return res.status(401).json({ error: "Unauthorized" });
// Verify the session token and get user
const { createClient } = await import("@supabase/supabase-js");
const userClient = createClient(
process.env.SUPABASE_URL || process.env.VITE_SUPABASE_URL || "",
process.env.SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY || "",
);
const { data: { user }, error: authErr } = await userClient.auth.getUser(token);
if (authErr || !user) return res.status(401).json({ error: "Invalid session" });
// Strip authentik fields from metadata
const meta = { ...(user.user_metadata || {}) };
delete meta.authentik_sub;
delete meta.authentik_linked;
const { error: updateErr } = await adminSupabase.auth.admin.updateUserById(user.id, {
user_metadata: meta,
});
if (updateErr) return res.status(500).json({ error: updateErr.message });
console.log("[Authentik] Unlinked user:", user.id);
return res.json({ ok: true });
} catch (e: any) {
return res.status(500).json({ error: e?.message || String(e) });
}
});
// ── End Authentik SSO ──────────────────────────────────────────────────────
// Blog API routes
app.get("/api/blog", blogIndexHandler);
app.get("/api/blog/:slug", (req: express.Request, res: express.Response) => {
@ -7797,5 +8273,28 @@ export function createServer() {
app.post("/api/ai/chat", aiChatHandler);
app.post("/api/ai/title", aiTitleHandler);
// Subscription API routes
app.post("/api/subscriptions/create-checkout", (req: express.Request, res: express.Response) => {
return createCheckoutHandler(req as any, res as any);
});
app.get("/api/subscriptions/manage", (req: express.Request, res: express.Response) => {
return manageSubscriptionHandler(req as any, res as any);
});
app.post("/api/subscriptions/manage", (req: express.Request, res: express.Response) => {
return manageSubscriptionHandler(req as any, res as any);
});
// Serve compiled SPA static assets (built by `npm run build:client`)
const spaDir = path.join(process.cwd(), "dist/spa");
app.use(express.static(spaDir, { index: false }));
// SPA catch-all — serve index.html for all non-API routes
app.get("*", (req: express.Request, res: express.Response) => {
if (req.path.startsWith("/api/")) {
return res.status(404).json({ error: "API endpoint not found" });
}
res.sendFile(path.join(spaDir, "index.html"));
});
return app;
}

View file

@ -1,35 +1,372 @@
import path from "path";
import { fileURLToPath } from "url";
import { readFileSync } from "fs";
import { createServer } from "./index";
import * as express from "express";
import { adminSupabase } from "./supabase";
const app = createServer();
const port = process.env.PORT || 5000;
const host = "0.0.0.0";
// In production, serve the built SPA files
const __dirname = import.meta.dirname;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const distPath = path.join(__dirname, "../spa");
// Serve static files
app.use(express.static(distPath));
// Handle React Router - serve index.html for all non-API routes
app.get("*", (req, res) => {
// Don't serve index.html for API routes
// ── SSR Meta Injection ────────────────────────────────────────────────────────
const BASE_URL = "https://aethex.dev";
const DEFAULT_OG_IMAGE =
"https://docs.aethex.tech/~gitbook/image?url=https%3A%2F%2F1143808467-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Forganizations%252FDhUg3jal6kdpG645FzIl%252Fsites%252Fsite_HeOmR%252Flogo%252FqxDYz8Oj2SnwUTa8t3UB%252FAeThex%2520Origin%2520logo.png%3Falt%3Dmedia%26token%3D200e8ea2-0129-4cbe-b516-4a53f60c512b&width=1200&dpr=1&quality=100&sign=6c7576ce&sv=2";
type RouteMeta = {
title: string;
description: string;
image?: string;
type?: string;
};
/** Static route overrides — matched in order, first match wins. */
const STATIC_META: Array<{ pattern: RegExp; meta: RouteMeta }> = [
{
pattern: /^\/$/,
meta: {
title: "AeThex | Developer Platform for Builders, Creators & Innovation",
description:
"AeThex: an advanced development platform and community for builders. Collaborate on projects, learn, and ship innovation.",
},
},
{
pattern: /^\/projects\/?$/,
meta: {
title: "AeThex | Projects",
description:
"Explore open-source and community projects built on the AeThex platform.",
},
},
{
pattern: /^\/projects\/new/,
meta: {
title: "AeThex | New Project",
description: "Start a new project on AeThex.",
},
},
{
pattern: /^\/gameforge/,
meta: {
title: "AeThex | GameForge Studio",
description:
"GameForge — AeThex's game development studio for indie game creators. Build, manage, and ship games.",
},
},
{
pattern: /^\/ethos/,
meta: {
title: "AeThex | Ethos Guild",
description:
"Ethos Guild — collaborative music creation, licensing, and creative work on AeThex.",
},
},
{
pattern: /^\/dev-platform/,
meta: {
title: "AeThex | Developer Platform",
description:
"Build with AeThex APIs. Documentation, SDKs, examples, and developer tools for modern builders.",
},
},
{
pattern: /^\/feed/,
meta: {
title: "AeThex | Community Feed",
description: "What's happening in the AeThex builder community.",
},
},
{
pattern: /^\/login/,
meta: {
title: "AeThex | Sign In",
description:
"Sign in to your AeThex account to access projects, community, and more.",
},
},
{
pattern: /^\/register/,
meta: {
title: "AeThex | Create Account",
description:
"Join AeThex and start building, learning, and collaborating with the community.",
},
},
{
pattern: /^\/dashboard/,
meta: {
title: "AeThex | Dashboard",
description:
"Your personal AeThex dashboard — manage projects, track progress, and connect.",
},
},
{
pattern: /^\/passport\/me$/,
meta: {
title: "AeThex | My Passport",
description: "View your AeThex developer passport and profile.",
},
},
{
pattern: /^\/docs/,
meta: {
title: "AeThex | Documentation",
description: "Guides, API references, and developer resources for AeThex.",
},
},
{
pattern: /^\/pricing/,
meta: {
title: "AeThex | Pricing",
description:
"Simple, transparent pricing for AeThex — individuals, teams, and enterprises.",
},
},
{
pattern: /^\/about/,
meta: {
title: "AeThex | About",
description:
"Learn about AeThex — our mission, team, and the technology we build.",
},
},
{
pattern: /^\/blog/,
meta: {
title: "AeThex | Blog",
description:
"News, tutorials, and announcements from the AeThex team and community.",
},
},
];
// UUID pattern
const PROJECT_UUID_RE =
/^\/projects\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
// Passport: /passport/<username> (not "me", handled above)
const PASSPORT_USER_RE = /^\/passport\/([^/]+)$/;
function escHtml(s: string): string {
return s
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
/**
* Replaces meta tag content values in the HTML template.
* Handles both inline and multi-line attribute formats (Vite preserves them).
*/
function injectMeta(
html: string,
meta: RouteMeta & { url: string }
): string {
const {
title,
description,
url,
image = DEFAULT_OG_IMAGE,
type = "website",
} = meta;
const t = escHtml(title);
const d = escHtml(description);
const u = escHtml(url);
const img = escHtml(image);
return (
html
// <title>
.replace(/<title>[^<]*<\/title>/, `<title>${t}</title>`)
// meta name="description"
.replace(
/(<meta\s+name="description"\s+content=")[^"]*"/,
`$1${d}"`
)
// og:title
.replace(
/(<meta\s+property="og:title"\s+content=")[^"]*"/,
`$1${t}"`
)
// og:description
.replace(
/(<meta\s+property="og:description"\s+content=")[^"]*"/,
`$1${d}"`
)
// og:url
.replace(
/(<meta\s+property="og:url"\s+content=")[^"]*"/,
`$1${u}"`
)
// og:type
.replace(
/(<meta\s+property="og:type"\s+content=")[^"]*"/,
`$1${type}"`
)
// og:image (first occurrence — the main image tag, not og:image:width/height)
.replace(
/(<meta\s+property="og:image"\s+content=")[^"]*"/,
`$1${img}"`
)
// twitter:title
.replace(
/(<meta\s+name="twitter:title"\s+content=")[^"]*"/,
`$1${t}"`
)
// twitter:description
.replace(
/(<meta\s+name="twitter:description"\s+content=")[^"]*"/,
`$1${d}"`
)
// twitter:image
.replace(
/(<meta\s+name="twitter:image"\s+content=")[^"]*"/,
`$1${img}"`
)
// canonical link
.replace(
/(<link\s+rel="canonical"\s+href=")[^"]*"/,
`$1${u}"`
)
);
}
/** Resolve per-route meta, fetching from DB for dynamic routes. */
async function resolveRouteMeta(
pathname: string
): Promise<RouteMeta & { url: string }> {
const url = `${BASE_URL}${pathname}`;
// Dynamic: project detail page
const projectMatch = PROJECT_UUID_RE.exec(pathname);
if (projectMatch && adminSupabase) {
const projectId = projectMatch[1];
try {
const { data: project } = await (adminSupabase as any)
.from("projects")
.select("title, description, image_url, status")
.eq("id", projectId)
.maybeSingle();
if (project) {
const desc = project.description
? String(project.description).slice(0, 160)
: `View the ${project.title} project on AeThex.`;
return {
title: `${project.title} — AeThex Project`,
description: desc,
image: project.image_url || DEFAULT_OG_IMAGE,
type: "article",
url,
};
}
} catch {
// fall through to default
}
}
// Dynamic: public passport / profile page
const passportMatch = PASSPORT_USER_RE.exec(pathname);
if (passportMatch && adminSupabase) {
const username = passportMatch[1];
try {
const { data: profile } = await adminSupabase
.from("user_profiles")
.select("username, full_name, avatar_url, bio")
.eq("username", username)
.maybeSingle();
if (profile) {
const displayName =
(profile as any).full_name || profile.username || username;
const bio = (profile as any).bio;
const desc = bio
? String(bio).slice(0, 160)
: `View ${displayName}'s developer passport and projects on AeThex.`;
return {
title: `${displayName} — AeThex Passport`,
description: desc,
url,
};
}
} catch {
// fall through
}
}
// Static route map
for (const { pattern, meta } of STATIC_META) {
if (pattern.test(pathname)) {
return { ...meta, url };
}
}
// Default fallback
return {
title: "AeThex | Developer Platform for Builders, Creators & Innovation",
description:
"AeThex: an advanced development platform and community for builders. Collaborate on projects, learn, and ship innovation.",
url,
};
}
// Cache the HTML template (reset on read error so a rebuild is picked up on restart)
let htmlTemplate: string | null = null;
function getTemplate(): string {
if (!htmlTemplate) {
htmlTemplate = readFileSync(path.join(distPath, "index.html"), "utf-8");
}
return htmlTemplate;
}
// ── Route Handler ─────────────────────────────────────────────────────────────
app.get("*", async (req, res) => {
if (req.path.startsWith("/api/") || req.path.startsWith("/health")) {
return res.status(404).json({ error: "API endpoint not found" });
}
res.sendFile(path.join(distPath, "index.html"));
// Don't serve the SPA shell for missing static-asset requests — they should 404
// cleanly rather than returning HTML (which causes "Unexpected token '<'" JS errors).
if (/\.(js|mjs|cjs|css|map|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|webp|avif)$/i.test(req.path)) {
return res.status(404).send("Not found");
}
try {
const template = getTemplate();
const meta = await resolveRouteMeta(req.path);
const html = injectMeta(template, meta);
res.setHeader("Content-Type", "text/html; charset=utf-8");
// Crawlers should not cache — browsers can
res.setHeader(
"Cache-Control",
"public, max-age=60, stale-while-revalidate=300"
);
res.send(html);
} catch (err) {
console.error("[SSR Meta] Error:", err);
// Fallback: send unmodified file
res.sendFile(path.join(distPath, "index.html"));
}
});
// ── Server ────────────────────────────────────────────────────────────────────
app.listen(Number(port), host, () => {
console.log(`🚀 AeThex server running on ${host}:${port}`);
console.log(`📱 Frontend: http://${host}:${port}`);
console.log(`🔧 API: http://${host}:${port}/api`);
});
// Graceful shutdown
process.on("SIGTERM", () => {
console.log("🛑 Received SIGTERM, shutting down gracefully");
process.exit(0);

View file

@ -1,4 +1,5 @@
import { createClient } from "@supabase/supabase-js";
import type { Database } from "../client/lib/database.types";
const SUPABASE_URL =
process.env.SUPABASE_URL || process.env.VITE_SUPABASE_URL || "";
@ -13,11 +14,11 @@ if (!SUPABASE_SERVICE_ROLE) {
);
}
let admin: any = null;
let admin: ReturnType<typeof createClient<Database>> | null = null;
if (SUPABASE_URL && SUPABASE_SERVICE_ROLE) {
admin = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE, {
admin = createClient<Database>(SUPABASE_URL, SUPABASE_SERVICE_ROLE, {
auth: { autoRefreshToken: false, persistSession: false },
});
}
export const adminSupabase = admin as ReturnType<typeof createClient>;
export const adminSupabase = admin as ReturnType<typeof createClient<Database>>;

View file

@ -0,0 +1,32 @@
-- Fix notifications table: add title, message, read columns that server and client expect.
-- The original table only had: id, user_id, type, data (jsonb), is_read, created_at
-- Server inserts use: title, message (and implicitly read=false)
-- Client reads use: title, message, read
ALTER TABLE public.notifications
ADD COLUMN IF NOT EXISTS title TEXT,
ADD COLUMN IF NOT EXISTS message TEXT,
ADD COLUMN IF NOT EXISTS read BOOLEAN DEFAULT false;
-- Backfill read from is_read for any existing rows
UPDATE public.notifications SET read = is_read WHERE read IS NULL;
-- Keep is_read and read in sync
CREATE OR REPLACE FUNCTION public.sync_notification_read()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
IF TG_OP = 'UPDATE' THEN
IF NEW.read IS DISTINCT FROM OLD.read THEN
NEW.is_read := NEW.read;
ELSIF NEW.is_read IS DISTINCT FROM OLD.is_read THEN
NEW.read := NEW.is_read;
END IF;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_sync_notification_read ON public.notifications;
CREATE TRIGGER trg_sync_notification_read
BEFORE UPDATE ON public.notifications
FOR EACH ROW EXECUTE FUNCTION public.sync_notification_read();

View file

@ -72,11 +72,19 @@ export default {
},
neon: {
purple: "hsl(var(--neon-purple))",
blue: "hsl(var(--neon-blue))",
magenta: "hsl(var(--neon-magenta))",
cyan: "hsl(var(--neon-cyan))",
blue: "hsl(var(--neon-cyan))", // alias — neon-blue → cyan
green: "hsl(var(--neon-green))",
yellow: "hsl(var(--neon-yellow))",
},
},
fontFamily: {
sans: ["Electrolize", "Source Code Pro", "monospace"],
mono: ["Source Code Pro", "JetBrains Mono", "monospace"],
display: ["Electrolize", "monospace"],
orbitron: ["Orbitron", "monospace"],
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",

View file

@ -1,160 +0,0 @@
{
"version": 2,
"buildCommand": "npm run build",
"outputDirectory": "dist/spa",
"functions": {
"api/**/*.ts": {
"memory": 1024,
"maxDuration": 30
}
},
"redirects": [
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "aethex.app" }],
"destination": "https://aethex.dev/:path",
"permanent": true
},
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "aethex.locker" }],
"destination": "https://aethex.dev/:path",
"permanent": true
},
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "aethex.studio" }],
"destination": "https://aethex.dev/ethos/:path",
"permanent": true
},
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "aethex.info" }],
"destination": "https://aethex.dev/foundation/:path",
"permanent": true
},
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "aethex.site" }],
"destination": "https://aethex.dev/:path",
"permanent": true
},
{
"source": "/",
"has": [{ "type": "host", "value": "aethex.me" }],
"destination": "https://aethex.dev/",
"permanent": true
},
{
"source": "/",
"has": [{ "type": "host", "value": "aethex.space" }],
"destination": "https://aethex.dev/",
"permanent": true
},
{
"source": "/feed",
"destination": "/community/feed",
"permanent": true
}
],
"rewrites": [
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "(?<proxy>.+)\\.discordsays\\.com" }],
"destination": "/index.html"
},
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "(?<sub>.+)\\.aethex\\.me" }],
"destination": "/index.html"
},
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "(?<sub>.+)\\.aethex\\.space" }],
"destination": "/index.html"
},
{
"source": "/api/:path(.*)",
"destination": "/api/:path"
},
{ "source": "/", "destination": "/index.html" },
{ "source": "/login", "destination": "/index.html" },
{ "source": "/login/:path*", "destination": "/index.html" },
{ "source": "/dashboard", "destination": "/index.html" },
{ "source": "/dashboard/:path*", "destination": "/index.html" },
{ "source": "/profile", "destination": "/index.html" },
{ "source": "/profile/:path*", "destination": "/index.html" },
{ "source": "/activity", "destination": "/index.html" },
{ "source": "/activity/", "destination": "/index.html" },
{ "source": "/activity/:path*", "destination": "/index.html" },
{ "source": "/admin", "destination": "/index.html" },
{ "source": "/admin/:path*", "destination": "/index.html" },
{ "source": "/creators", "destination": "/index.html" },
{ "source": "/creators/:path*", "destination": "/index.html" },
{ "source": "/opportunities", "destination": "/index.html" },
{ "source": "/opportunities/:path*", "destination": "/index.html" },
{ "source": "/nexus", "destination": "/index.html" },
{ "source": "/nexus/:path*", "destination": "/index.html" },
{ "source": "/foundation", "destination": "/index.html" },
{ "source": "/foundation/:path*", "destination": "/index.html" },
{ "source": "/gameforge", "destination": "/index.html" },
{ "source": "/gameforge/:path*", "destination": "/index.html" },
{ "source": "/labs", "destination": "/index.html" },
{ "source": "/labs/:path*", "destination": "/index.html" },
{ "source": "/corp", "destination": "/index.html" },
{ "source": "/corp/:path*", "destination": "/index.html" },
{ "source": "/devlink", "destination": "/index.html" },
{ "source": "/devlink/:path*", "destination": "/index.html" },
{ "source": "/community", "destination": "/index.html" },
{ "source": "/community/:path*", "destination": "/index.html" },
{ "source": "/developers", "destination": "/index.html" },
{ "source": "/developers/:path*", "destination": "/index.html" },
{ "source": "/discord-verify", "destination": "/index.html" },
{ "source": "/discord-verify/:path*", "destination": "/index.html" },
{ "source": "/ethos", "destination": "/index.html" },
{ "source": "/ethos/:path*", "destination": "/index.html" },
{ "source": "/:path*", "destination": "/index.html" }
],
"headers": [
{
"source": "/assets/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
},
{
"source": "/(.*).(css|js|png|jpg|jpeg|gif|svg|webp|ico|woff2)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
},
{
"source": "/api/(.*)",
"headers": [{ "key": "Cache-Control", "value": "no-store" }]
},
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
},
{
"key": "Permissions-Policy",
"value": "geolocation=(), microphone=(), camera=()"
},
{
"key": "Content-Security-Policy",
"value": "default-src 'self' https: data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: blob: https:; font-src 'self' data: https:; connect-src 'self' https: wss:; frame-ancestors 'self' https://discord.com https://*.discord.com https://*.discordsays.com"
}
]
}
]
}

View file

@ -1,6 +1,9 @@
import { defineConfig, Plugin } from "vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";
import { readFileSync } from "fs";
const pkg = JSON.parse(readFileSync(path.resolve(__dirname, "package.json"), "utf-8"));
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
@ -20,6 +23,9 @@ export default defineConfig(({ mode }) => ({
build: {
outDir: "dist/spa",
},
define: {
"import.meta.env.VITE_APP_VERSION": JSON.stringify(pkg.version),
},
plugins: [react(), expressPlugin()],
resolve: {
alias: {
@ -36,7 +42,7 @@ function expressPlugin(): Plugin {
async configureServer(server) {
try {
console.log("[Vite] Loading Express server...");
const { createServer } = await import("./server");
const { createServer } = await server.ssrLoadModule("/server/index.ts");
const app = createServer();
console.log("[Vite] Express server created, mounting...");