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
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>
This commit is contained in:
parent
06b748dade
commit
7fec93e05c
63 changed files with 25674 additions and 3199 deletions
406
client/App.tsx
406
client/App.tsx
|
|
@ -1,8 +1,6 @@
|
||||||
import "./global.css";
|
import "./global.css";
|
||||||
|
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { createRoot } from "react-dom/client";
|
|
||||||
|
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
|
|
@ -15,7 +13,6 @@ import { MaintenanceProvider } from "./contexts/MaintenanceContext";
|
||||||
import MaintenanceGuard from "./components/MaintenanceGuard";
|
import MaintenanceGuard from "./components/MaintenanceGuard";
|
||||||
import PageTransition from "./components/PageTransition";
|
import PageTransition from "./components/PageTransition";
|
||||||
import SkipAgentController from "./components/SkipAgentController";
|
import SkipAgentController from "./components/SkipAgentController";
|
||||||
import Index from "./pages/Index";
|
|
||||||
import Onboarding from "./pages/Onboarding";
|
import Onboarding from "./pages/Onboarding";
|
||||||
import Dashboard from "./pages/Dashboard";
|
import Dashboard from "./pages/Dashboard";
|
||||||
import Login from "./pages/Login";
|
import Login from "./pages/Login";
|
||||||
|
|
@ -26,14 +23,9 @@ import ResearchLabs from "./pages/ResearchLabs";
|
||||||
import Labs from "./pages/Labs";
|
import Labs from "./pages/Labs";
|
||||||
import GameForge from "./pages/GameForge";
|
import GameForge from "./pages/GameForge";
|
||||||
import Foundation from "./pages/Foundation";
|
import Foundation from "./pages/Foundation";
|
||||||
import Corp from "./pages/Corp";
|
|
||||||
import Staff from "./pages/Staff";
|
|
||||||
import Nexus from "./pages/Nexus";
|
import Nexus from "./pages/Nexus";
|
||||||
import Arms from "./pages/Arms";
|
import Arms from "./pages/Arms";
|
||||||
import ExternalRedirect from "./components/ExternalRedirect";
|
import ExternalRedirect from "./components/ExternalRedirect";
|
||||||
import CorpScheduleConsultation from "./pages/corp/CorpScheduleConsultation";
|
|
||||||
import CorpViewCaseStudies from "./pages/corp/CorpViewCaseStudies";
|
|
||||||
import CorpContactUs from "./pages/corp/CorpContactUs";
|
|
||||||
import RequireAccess from "@/components/RequireAccess";
|
import RequireAccess from "@/components/RequireAccess";
|
||||||
import Engage from "./pages/Pricing";
|
import Engage from "./pages/Pricing";
|
||||||
import DocsLayout from "@/components/docs/DocsLayout";
|
import DocsLayout from "@/components/docs/DocsLayout";
|
||||||
|
|
@ -56,7 +48,6 @@ import GameJoltIntegration from "./pages/docs/integrations/GameJolt";
|
||||||
import ItchIoIntegration from "./pages/docs/integrations/ItchIo";
|
import ItchIoIntegration from "./pages/docs/integrations/ItchIo";
|
||||||
import DocsCurriculum from "./pages/docs/DocsCurriculum";
|
import DocsCurriculum from "./pages/docs/DocsCurriculum";
|
||||||
import DocsCurriculumEthos from "./pages/docs/DocsCurriculumEthos";
|
import DocsCurriculumEthos from "./pages/docs/DocsCurriculumEthos";
|
||||||
import EthosGuild from "./pages/community/EthosGuild";
|
|
||||||
import TrackLibrary from "./pages/ethos/TrackLibrary";
|
import TrackLibrary from "./pages/ethos/TrackLibrary";
|
||||||
import ArtistProfile from "./pages/ethos/ArtistProfile";
|
import ArtistProfile from "./pages/ethos/ArtistProfile";
|
||||||
import ArtistSettings from "./pages/ethos/ArtistSettings";
|
import ArtistSettings from "./pages/ethos/ArtistSettings";
|
||||||
|
|
@ -72,7 +63,6 @@ import DevelopersDirectory from "./pages/DevelopersDirectory";
|
||||||
import ProfilePassport from "./pages/ProfilePassport";
|
import ProfilePassport from "./pages/ProfilePassport";
|
||||||
import SubdomainPassport from "./pages/SubdomainPassport";
|
import SubdomainPassport from "./pages/SubdomainPassport";
|
||||||
import Profile from "./pages/Profile";
|
import Profile from "./pages/Profile";
|
||||||
import LegacyPassportRedirect from "./pages/LegacyPassportRedirect";
|
|
||||||
import { SubdomainPassportProvider } from "./contexts/SubdomainPassportContext";
|
import { SubdomainPassportProvider } from "./contexts/SubdomainPassportContext";
|
||||||
import About from "./pages/About";
|
import About from "./pages/About";
|
||||||
import Contact from "./pages/Contact";
|
import Contact from "./pages/Contact";
|
||||||
|
|
@ -81,28 +71,24 @@ import Careers from "./pages/Careers";
|
||||||
import Privacy from "./pages/Privacy";
|
import Privacy from "./pages/Privacy";
|
||||||
import Terms from "./pages/Terms";
|
import Terms from "./pages/Terms";
|
||||||
import Admin from "./pages/Admin";
|
import Admin from "./pages/Admin";
|
||||||
import Feed from "./pages/Feed";
|
import AdminModeration from "./pages/admin/AdminModeration";
|
||||||
|
import AdminAnalytics from "./pages/admin/AdminAnalytics";
|
||||||
import AdminFeed from "./pages/AdminFeed";
|
import AdminFeed from "./pages/AdminFeed";
|
||||||
import ProjectsNew from "./pages/ProjectsNew";
|
import ProjectsNew from "./pages/ProjectsNew";
|
||||||
import Opportunities from "./pages/Opportunities";
|
|
||||||
import Explore from "./pages/Explore";
|
import Explore from "./pages/Explore";
|
||||||
import ResetPassword from "./pages/ResetPassword";
|
import ResetPassword from "./pages/ResetPassword";
|
||||||
import Teams from "./pages/Teams";
|
import Teams from "./pages/Teams";
|
||||||
import Squads from "./pages/Squads";
|
import Squads from "./pages/Squads";
|
||||||
import MenteeHub from "./pages/MenteeHub";
|
import MenteeHub from "./pages/MenteeHub";
|
||||||
import ProjectBoard from "./pages/ProjectBoard";
|
import ProjectBoard from "./pages/ProjectBoard";
|
||||||
|
import ProjectDetail from "./pages/ProjectDetail";
|
||||||
import { Navigate } from "react-router-dom";
|
import { Navigate } from "react-router-dom";
|
||||||
import FourOhFourPage from "./pages/404";
|
import FourOhFourPage from "./pages/404";
|
||||||
import SignupRedirect from "./pages/SignupRedirect";
|
import SignupRedirect from "./pages/SignupRedirect";
|
||||||
import MentorshipRequest from "./pages/community/MentorshipRequest";
|
|
||||||
import MentorApply from "./pages/community/MentorApply";
|
|
||||||
import MentorProfile from "./pages/community/MentorProfile";
|
|
||||||
import Realms from "./pages/Realms";
|
import Realms from "./pages/Realms";
|
||||||
import Investors from "./pages/Investors";
|
import Investors from "./pages/Investors";
|
||||||
import NexusDashboard from "./pages/dashboards/NexusDashboard";
|
import NexusDashboard from "./pages/dashboards/NexusDashboard";
|
||||||
import LabsDashboard from "./pages/dashboards/LabsDashboard";
|
|
||||||
import GameForgeDashboard from "./pages/dashboards/GameForgeDashboard";
|
import GameForgeDashboard from "./pages/dashboards/GameForgeDashboard";
|
||||||
import StaffDashboard from "./pages/dashboards/StaffDashboard";
|
|
||||||
import Roadmap from "./pages/Roadmap";
|
import Roadmap from "./pages/Roadmap";
|
||||||
import Trust from "./pages/Trust";
|
import Trust from "./pages/Trust";
|
||||||
import PressKit from "./pages/PressKit";
|
import PressKit from "./pages/PressKit";
|
||||||
|
|
@ -130,13 +116,7 @@ import OpportunitiesHub from "./pages/opportunities/OpportunitiesHub";
|
||||||
import OpportunityDetail from "./pages/opportunities/OpportunityDetail";
|
import OpportunityDetail from "./pages/opportunities/OpportunityDetail";
|
||||||
import OpportunityPostForm from "./pages/opportunities/OpportunityPostForm";
|
import OpportunityPostForm from "./pages/opportunities/OpportunityPostForm";
|
||||||
import MyApplications from "./pages/profile/MyApplications";
|
import MyApplications from "./pages/profile/MyApplications";
|
||||||
import ClientHub from "./pages/hub/ClientHub";
|
// Hub pages moved to aethex.co (aethex-corp app)
|
||||||
import ClientProjects from "./pages/hub/ClientProjects";
|
|
||||||
import ClientDashboard from "./pages/hub/ClientDashboard";
|
|
||||||
import ClientInvoices from "./pages/hub/ClientInvoices";
|
|
||||||
import ClientContracts from "./pages/hub/ClientContracts";
|
|
||||||
import ClientReports from "./pages/hub/ClientReports";
|
|
||||||
import ClientSettings from "./pages/hub/ClientSettings";
|
|
||||||
import Space1Welcome from "./pages/internal-docs/Space1Welcome";
|
import Space1Welcome from "./pages/internal-docs/Space1Welcome";
|
||||||
import Space1AxiomModel from "./pages/internal-docs/Space1AxiomModel";
|
import Space1AxiomModel from "./pages/internal-docs/Space1AxiomModel";
|
||||||
import Space1FindYourRole from "./pages/internal-docs/Space1FindYourRole";
|
import Space1FindYourRole from "./pages/internal-docs/Space1FindYourRole";
|
||||||
|
|
@ -155,20 +135,7 @@ import Space4ClientOps from "./pages/internal-docs/Space4ClientOps";
|
||||||
import Space4PlatformStrategy from "./pages/internal-docs/Space4PlatformStrategy";
|
import Space4PlatformStrategy from "./pages/internal-docs/Space4PlatformStrategy";
|
||||||
import Space5Onboarding from "./pages/internal-docs/Space5Onboarding";
|
import Space5Onboarding from "./pages/internal-docs/Space5Onboarding";
|
||||||
import Space5Finance from "./pages/internal-docs/Space5Finance";
|
import Space5Finance from "./pages/internal-docs/Space5Finance";
|
||||||
import StaffLogin from "./pages/StaffLogin";
|
// Staff/Candidate pages moved to staff.aethex.tech (aethex-staff app)
|
||||||
import StaffDirectory from "./pages/StaffDirectory";
|
|
||||||
import StaffAdmin from "./pages/StaffAdmin";
|
|
||||||
import StaffChat from "./pages/StaffChat";
|
|
||||||
import StaffDocs from "./pages/StaffDocs";
|
|
||||||
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 DeveloperDashboard from "./pages/dev-platform/DeveloperDashboard";
|
import DeveloperDashboard from "./pages/dev-platform/DeveloperDashboard";
|
||||||
import ApiReference from "./pages/dev-platform/ApiReference";
|
import ApiReference from "./pages/dev-platform/ApiReference";
|
||||||
import QuickStart from "./pages/dev-platform/QuickStart";
|
import QuickStart from "./pages/dev-platform/QuickStart";
|
||||||
|
|
@ -237,14 +204,8 @@ const App = () => (
|
||||||
path="/dashboard/dev-link"
|
path="/dashboard/dev-link"
|
||||||
element={<Navigate to="/dashboard/nexus" replace />}
|
element={<Navigate to="/dashboard/nexus" replace />}
|
||||||
/>
|
/>
|
||||||
<Route
|
{/* Hub routes → aethex.co */}
|
||||||
path="/hub/client"
|
<Route path="/hub/*" element={<ExternalRedirect to="https://aethex.co/hub" />} />
|
||||||
element={
|
|
||||||
<RequireAccess>
|
|
||||||
<ClientHub />
|
|
||||||
</RequireAccess>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="/realms" element={<Realms />} />
|
<Route path="/realms" element={<Realms />} />
|
||||||
<Route path="/investors" element={<Investors />} />
|
<Route path="/investors" element={<Investors />} />
|
||||||
<Route path="/roadmap" element={<Roadmap />} />
|
<Route path="/roadmap" element={<Roadmap />} />
|
||||||
|
|
@ -286,6 +247,10 @@ const App = () => (
|
||||||
path="/projects/:projectId/board"
|
path="/projects/:projectId/board"
|
||||||
element={<ProjectBoard />}
|
element={<ProjectBoard />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/projects/:projectId"
|
||||||
|
element={<ProjectDetail />}
|
||||||
|
/>
|
||||||
<Route path="/profile" element={<Profile />} />
|
<Route path="/profile" element={<Profile />} />
|
||||||
<Route path="/profile/me" element={<Profile />} />
|
<Route path="/profile/me" element={<Profile />} />
|
||||||
<Route
|
<Route
|
||||||
|
|
@ -418,211 +383,15 @@ const App = () => (
|
||||||
{/* Foundation page with auto-redirect to aethex.foundation (Non-Profit Guardian - Axiom Model) */}
|
{/* Foundation page with auto-redirect to aethex.foundation (Non-Profit Guardian - Axiom Model) */}
|
||||||
<Route path="/foundation" element={<Foundation />} />
|
<Route path="/foundation" element={<Foundation />} />
|
||||||
|
|
||||||
<Route path="/corp" element={<Corp />} />
|
{/* Corp routes → aethex.co */}
|
||||||
<Route
|
<Route path="/corp" element={<ExternalRedirect to="https://aethex.co" />} />
|
||||||
path="/corp/schedule-consultation"
|
<Route path="/corp/*" element={<ExternalRedirect to="https://aethex.co" />} />
|
||||||
element={<CorpScheduleConsultation />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/corp/view-case-studies"
|
|
||||||
element={<CorpViewCaseStudies />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/corp/contact-us"
|
|
||||||
element={<CorpContactUs />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Staff Arm Routes */}
|
{/* Staff + Candidate routes → staff.aethex.tech */}
|
||||||
<Route path="/staff" element={<Staff />} />
|
<Route path="/staff" element={<ExternalRedirect to="https://staff.aethex.tech" />} />
|
||||||
<Route path="/staff/login" element={<StaffLogin />} />
|
<Route path="/staff/*" element={<ExternalRedirect to="https://staff.aethex.tech" />} />
|
||||||
|
<Route path="/candidate" element={<ExternalRedirect to="https://staff.aethex.tech/candidate" />} />
|
||||||
{/* Staff Dashboard Routes */}
|
<Route path="/candidate/*" element={<ExternalRedirect to="https://staff.aethex.tech/candidate" />} />
|
||||||
<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>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Dev-Link routes - now redirect to Nexus Opportunities with ecosystem filter */}
|
{/* Dev-Link routes - now redirect to Nexus Opportunities with ecosystem filter */}
|
||||||
<Route path="/dev-link" element={<Navigate to="/opportunities?ecosystem=roblox" replace />} />
|
<Route path="/dev-link" element={<Navigate to="/opportunities?ecosystem=roblox" replace />} />
|
||||||
|
|
@ -631,55 +400,8 @@ const App = () => (
|
||||||
element={<Navigate to="/opportunities?ecosystem=roblox" replace />}
|
element={<Navigate to="/opportunities?ecosystem=roblox" replace />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Client Hub routes */}
|
{/* Client Hub routes → aethex.co */}
|
||||||
<Route
|
<Route path="/hub/client/*" element={<ExternalRedirect to="https://aethex.co/hub" />} />
|
||||||
path="/hub/client/dashboard"
|
|
||||||
element={
|
|
||||||
<RequireAccess>
|
|
||||||
<ClientDashboard />
|
|
||||||
</RequireAccess>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/hub/client/projects"
|
|
||||||
element={
|
|
||||||
<RequireAccess>
|
|
||||||
<ClientProjects />
|
|
||||||
</RequireAccess>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/hub/client/invoices"
|
|
||||||
element={
|
|
||||||
<RequireAccess>
|
|
||||||
<ClientInvoices />
|
|
||||||
</RequireAccess>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/hub/client/contracts"
|
|
||||||
element={
|
|
||||||
<RequireAccess>
|
|
||||||
<ClientContracts />
|
|
||||||
</RequireAccess>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/hub/client/reports"
|
|
||||||
element={
|
|
||||||
<RequireAccess>
|
|
||||||
<ClientReports />
|
|
||||||
</RequireAccess>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/hub/client/settings"
|
|
||||||
element={
|
|
||||||
<RequireAccess>
|
|
||||||
<ClientSettings />
|
|
||||||
</RequireAccess>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Nexus routes */}
|
{/* Nexus routes */}
|
||||||
<Route path="/nexus" element={<Nexus />} />
|
<Route path="/nexus" element={<Nexus />} />
|
||||||
|
|
@ -699,6 +421,10 @@ const App = () => (
|
||||||
path="curriculum"
|
path="curriculum"
|
||||||
element={<DocsCurriculum />}
|
element={<DocsCurriculum />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="curriculum/ethos"
|
||||||
|
element={<DocsCurriculumEthos />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="getting-started"
|
path="getting-started"
|
||||||
element={<DocsGettingStarted />}
|
element={<DocsGettingStarted />}
|
||||||
|
|
@ -801,88 +527,6 @@ const App = () => (
|
||||||
{/* Discord Activity route */}
|
{/* Discord Activity route */}
|
||||||
<Route path="/activity" element={<Activity />} />
|
<Route path="/activity" element={<Activity />} />
|
||||||
|
|
||||||
{/* Docs routes */}
|
|
||||||
<Route
|
|
||||||
path="/docs"
|
|
||||||
element={
|
|
||||||
<DocsLayout>
|
|
||||||
<DocsOverview />
|
|
||||||
</DocsLayout>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/docs/getting-started"
|
|
||||||
element={
|
|
||||||
<DocsLayout>
|
|
||||||
<DocsGettingStarted />
|
|
||||||
</DocsLayout>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/docs/platform"
|
|
||||||
element={
|
|
||||||
<DocsLayout>
|
|
||||||
<DocsPlatform />
|
|
||||||
</DocsLayout>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/docs/api"
|
|
||||||
element={
|
|
||||||
<DocsLayout>
|
|
||||||
<DocsApiReference />
|
|
||||||
</DocsLayout>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/docs/cli"
|
|
||||||
element={
|
|
||||||
<DocsLayout>
|
|
||||||
<DocsCli />
|
|
||||||
</DocsLayout>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/docs/tutorials"
|
|
||||||
element={
|
|
||||||
<DocsLayout>
|
|
||||||
<DocsTutorials />
|
|
||||||
</DocsLayout>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/docs/examples"
|
|
||||||
element={
|
|
||||||
<DocsLayout>
|
|
||||||
<DocsExamples />
|
|
||||||
</DocsLayout>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/docs/integrations"
|
|
||||||
element={
|
|
||||||
<DocsLayout>
|
|
||||||
<DocsIntegrations />
|
|
||||||
</DocsLayout>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/docs/curriculum"
|
|
||||||
element={
|
|
||||||
<DocsLayout>
|
|
||||||
<DocsCurriculum />
|
|
||||||
</DocsLayout>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/docs/curriculum/ethos"
|
|
||||||
element={
|
|
||||||
<DocsLayout>
|
|
||||||
<DocsCurriculumEthos />
|
|
||||||
</DocsLayout>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Internal Docs Hub Routes */}
|
{/* Internal Docs Hub Routes */}
|
||||||
<Route
|
<Route
|
||||||
path="/internal-docs"
|
path="/internal-docs"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -21,18 +21,122 @@ import {
|
||||||
User,
|
User,
|
||||||
Menu,
|
Menu,
|
||||||
X,
|
X,
|
||||||
|
Zap,
|
||||||
|
FlaskConical,
|
||||||
|
LayoutDashboard,
|
||||||
|
ChevronRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export interface DevPlatformNavProps {
|
export interface DevPlatformNavProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NavEntry {
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
icon: React.ElementType;
|
||||||
|
description: string;
|
||||||
|
comingSoon?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavGroup {
|
||||||
|
label: string;
|
||||||
|
items: NavEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Grouped nav structure ──────────────────────────────────────────────────────
|
||||||
|
const NAV_GROUPS: NavGroup[] = [
|
||||||
|
{
|
||||||
|
label: "Learn",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: "Quick Start",
|
||||||
|
href: "/dev-platform/quick-start",
|
||||||
|
icon: Zap,
|
||||||
|
description: "Up and running in under 5 minutes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Documentation",
|
||||||
|
href: "/docs",
|
||||||
|
icon: BookOpen,
|
||||||
|
description: "Guides, concepts, and deep dives",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Code Examples",
|
||||||
|
href: "/dev-platform/examples",
|
||||||
|
icon: FlaskConical,
|
||||||
|
description: "Copy-paste snippets for common patterns",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Build",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: "API Reference",
|
||||||
|
href: "/dev-platform/api-reference",
|
||||||
|
icon: Code2,
|
||||||
|
description: "Full endpoint docs with live samples",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SDK",
|
||||||
|
href: "/sdk",
|
||||||
|
icon: Package,
|
||||||
|
description: "Client libraries for JS, Python, Go and more",
|
||||||
|
comingSoon: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Templates",
|
||||||
|
href: "/dev-platform/templates",
|
||||||
|
icon: LayoutTemplate,
|
||||||
|
description: "Project starters and boilerplates",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Marketplace",
|
||||||
|
href: "/dev-platform/marketplace",
|
||||||
|
icon: Store,
|
||||||
|
description: "Plugins, integrations, and extensions",
|
||||||
|
comingSoon: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Shared dropdown item component ────────────────────────────────────────────
|
||||||
|
function DropdownItem({ item, onClick }: { item: NavEntry; onClick?: () => void }) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<Link
|
||||||
|
to={item.href}
|
||||||
|
onClick={onClick}
|
||||||
|
className="group flex select-none gap-3 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
||||||
|
>
|
||||||
|
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-border/60 bg-muted/50 group-hover:border-primary/30 group-hover:bg-primary/10 transition-colors">
|
||||||
|
<item.icon className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium leading-none">{item.name}</span>
|
||||||
|
{item.comingSoon && (
|
||||||
|
<span className="rounded-full bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium text-primary leading-none">
|
||||||
|
Soon
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs leading-snug text-muted-foreground line-clamp-2">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function DevPlatformNav({ className }: DevPlatformNavProps) {
|
export function DevPlatformNav({ className }: DevPlatformNavProps) {
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
|
||||||
const [searchOpen, setSearchOpen] = React.useState(false);
|
const [searchOpen, setSearchOpen] = React.useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// Command palette keyboard shortcut
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
||||||
|
|
@ -40,46 +144,12 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
|
||||||
setSearchOpen(true);
|
setSearchOpen(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const navLinks = [
|
const isGroupActive = (group: NavGroup) =>
|
||||||
{
|
group.items.some((item) => location.pathname.startsWith(item.href));
|
||||||
name: "Docs",
|
|
||||||
href: "/docs",
|
|
||||||
icon: BookOpen,
|
|
||||||
description: "Guides, tutorials, and API concepts",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "API Reference",
|
|
||||||
href: "/api-reference",
|
|
||||||
icon: Code2,
|
|
||||||
description: "Complete API documentation",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "SDK",
|
|
||||||
href: "/sdk",
|
|
||||||
icon: Package,
|
|
||||||
description: "Download SDKs for all platforms",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Templates",
|
|
||||||
href: "/templates",
|
|
||||||
icon: LayoutTemplate,
|
|
||||||
description: "Project starters and boilerplates",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Marketplace",
|
|
||||||
href: "/marketplace",
|
|
||||||
icon: Store,
|
|
||||||
description: "Plugins and extensions (coming soon)",
|
|
||||||
comingSoon: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const isActive = (path: string) => location.pathname.startsWith(path);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
|
|
@ -91,7 +161,7 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
|
||||||
<div className="container flex h-16 items-center">
|
<div className="container flex h-16 items-center">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/dev-platform"
|
||||||
className="mr-8 flex items-center space-x-2 transition-opacity hover:opacity-80"
|
className="mr-8 flex items-center space-x-2 transition-opacity hover:opacity-80"
|
||||||
>
|
>
|
||||||
<FileCode className="h-6 w-6 text-primary" />
|
<FileCode className="h-6 w-6 text-primary" />
|
||||||
|
|
@ -104,55 +174,68 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
|
||||||
<div className="hidden md:flex md:flex-1 md:items-center md:justify-between">
|
<div className="hidden md:flex md:flex-1 md:items-center md:justify-between">
|
||||||
<NavigationMenu>
|
<NavigationMenu>
|
||||||
<NavigationMenuList>
|
<NavigationMenuList>
|
||||||
{navLinks.map((link) => (
|
{NAV_GROUPS.map((group) => (
|
||||||
<NavigationMenuItem key={link.href}>
|
<NavigationMenuItem key={group.label}>
|
||||||
<Link to={link.href}>
|
<NavigationMenuTrigger
|
||||||
<NavigationMenuLink
|
|
||||||
className={cn(
|
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",
|
"h-10 text-sm font-medium",
|
||||||
isActive(link.href) &&
|
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"
|
"bg-accent text-accent-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<link.icon className="mr-2 h-4 w-4" />
|
<LayoutDashboard className="mr-2 h-4 w-4" />
|
||||||
{link.name}
|
Dashboard
|
||||||
{link.comingSoon && (
|
|
||||||
<span className="ml-2 rounded-full bg-primary/20 px-2 py-0.5 text-xs text-primary">
|
|
||||||
Soon
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</NavigationMenuLink>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
))}
|
|
||||||
</NavigationMenuList>
|
</NavigationMenuList>
|
||||||
</NavigationMenu>
|
</NavigationMenu>
|
||||||
|
|
||||||
{/* Right side actions */}
|
{/* Right side actions */}
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-3">
|
||||||
{/* Search button */}
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
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)}
|
onClick={() => setSearchOpen(true)}
|
||||||
>
|
>
|
||||||
<Command className="mr-2 h-4 w-4" />
|
<Command className="mr-2 h-4 w-4 shrink-0" />
|
||||||
<span className="hidden lg:inline-flex">Search...</span>
|
<span>Search docs...</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 sm:flex">
|
||||||
<kbd className="pointer-events-none absolute right-2 hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
|
|
||||||
<span className="text-xs">⌘</span>K
|
<span className="text-xs">⌘</span>K
|
||||||
</kbd>
|
</kbd>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Dashboard link */}
|
|
||||||
<Link to="/dashboard">
|
|
||||||
<Button variant="ghost" size="sm">
|
|
||||||
Dashboard
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* User menu */}
|
|
||||||
<Link to="/profile">
|
<Link to="/profile">
|
||||||
<Button variant="ghost" size="icon" className="h-9 w-9">
|
<Button variant="ghost" size="icon" className="h-9 w-9">
|
||||||
<User className="h-4 w-4" />
|
<User className="h-4 w-4" />
|
||||||
|
|
@ -168,11 +251,7 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
>
|
>
|
||||||
{mobileMenuOpen ? (
|
{mobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||||
<X className="h-5 w-5" />
|
|
||||||
) : (
|
|
||||||
<Menu className="h-5 w-5" />
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -180,41 +259,50 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
|
||||||
{/* Mobile Navigation */}
|
{/* Mobile Navigation */}
|
||||||
{mobileMenuOpen && (
|
{mobileMenuOpen && (
|
||||||
<div className="border-t border-border/40 md:hidden">
|
<div className="border-t border-border/40 md:hidden">
|
||||||
<div className="container space-y-1 py-4">
|
<div className="container py-4 space-y-4">
|
||||||
{navLinks.map((link) => (
|
{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
|
<Link
|
||||||
key={link.href}
|
key={item.href}
|
||||||
to={link.href}
|
to={item.href}
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground",
|
"flex items-center gap-3 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"
|
location.pathname.startsWith(item.href) && "bg-accent text-accent-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<link.icon className="mr-3 h-4 w-4" />
|
<item.icon className="h-4 w-4 shrink-0" />
|
||||||
{link.name}
|
<span className="flex-1">{item.name}</span>
|
||||||
{link.comingSoon && (
|
{item.comingSoon && (
|
||||||
<span className="ml-auto rounded-full bg-primary/20 px-2 py-0.5 text-xs text-primary">
|
<span className="rounded-full bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium text-primary">
|
||||||
Soon
|
Soon
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
<ChevronRight className="h-3 w-3 text-muted-foreground/50" />
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
<div className="border-t border-border/40 pt-4 mt-4">
|
<div className="border-t border-border/40 pt-3">
|
||||||
<Link
|
<Link
|
||||||
to="/dashboard"
|
to="/dev-platform/dashboard"
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
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
|
Dashboard
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/profile"
|
to="/profile"
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
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
|
Profile
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -222,20 +310,20 @@ export function DevPlatformNav({ className }: DevPlatformNavProps) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Command Palette Placeholder - will be implemented separately */}
|
{/* Command Palette */}
|
||||||
{searchOpen && (
|
{searchOpen && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||||
onClick={() => setSearchOpen(false)}
|
onClick={() => setSearchOpen(false)}
|
||||||
>
|
>
|
||||||
<div className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
<div
|
||||||
<div className="rounded-lg border bg-background p-8 shadow-lg">
|
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
|
||||||
<p className="text-center text-muted-foreground">
|
onClick={(e) => e.stopPropagation()}
|
||||||
Command palette coming soon...
|
>
|
||||||
</p>
|
<div className="rounded-xl border bg-background p-8 shadow-2xl min-w-80 text-center space-y-2">
|
||||||
<p className="text-center text-sm text-muted-foreground mt-2">
|
<Command className="mx-auto h-8 w-8 text-muted-foreground" />
|
||||||
Press Esc to close
|
<p className="text-muted-foreground font-medium">Command palette coming soon</p>
|
||||||
</p>
|
<p className="text-sm text-muted-foreground/60">Press Esc or click outside to close</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
166
client/components/ethos/EthosLayout.tsx
Normal file
166
client/components/ethos/EthosLayout.tsx
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
import {
|
||||||
|
Music2,
|
||||||
|
Users,
|
||||||
|
FileText,
|
||||||
|
Settings,
|
||||||
|
ChevronLeft,
|
||||||
|
Headphones,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface EthosLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
icon: React.ElementType;
|
||||||
|
memberOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NAV_ITEMS: NavItem[] = [
|
||||||
|
{ name: "Library", href: "/ethos/library", icon: Headphones },
|
||||||
|
{ name: "Artists", href: "/ethos/artists", icon: Users },
|
||||||
|
{ name: "Licensing", href: "/ethos/licensing", icon: FileText, memberOnly: true },
|
||||||
|
{ name: "Settings", href: "/ethos/settings", icon: Settings, memberOnly: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function EthosLayout({ children }: EthosLayoutProps) {
|
||||||
|
const location = useLocation();
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const isActive = (href: string) => location.pathname.startsWith(href);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ minHeight: "100vh", background: "#050505", color: "#e0e0e0" }}>
|
||||||
|
{/* Top bar */}
|
||||||
|
<header style={{
|
||||||
|
position: "sticky", top: 0, zIndex: 50,
|
||||||
|
background: "rgba(5,5,5,0.97)",
|
||||||
|
borderBottom: "1px solid rgba(168,85,247,0.15)",
|
||||||
|
backdropFilter: "blur(12px)",
|
||||||
|
}}>
|
||||||
|
{/* Purple accent stripe */}
|
||||||
|
<div style={{ height: 2, background: "linear-gradient(90deg, #7c3aed 0%, #a855f7 50%, #7c3aed 100%)", opacity: 0.6 }} />
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
maxWidth: 1200, margin: "0 auto",
|
||||||
|
padding: "0 24px",
|
||||||
|
display: "flex", alignItems: "center",
|
||||||
|
height: 52, gap: 0,
|
||||||
|
}}>
|
||||||
|
{/* Back to main site */}
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
style={{
|
||||||
|
display: "flex", alignItems: "center", gap: 6,
|
||||||
|
color: "rgba(168,85,247,0.5)", textDecoration: "none",
|
||||||
|
fontSize: 11, fontFamily: "monospace", letterSpacing: 1,
|
||||||
|
marginRight: 24, flexShrink: 0,
|
||||||
|
transition: "color 0.2s",
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => (e.currentTarget.style.color = "rgba(168,85,247,0.9)")}
|
||||||
|
onMouseLeave={e => (e.currentTarget.style.color = "rgba(168,85,247,0.5)")}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-3.5 w-3.5" />
|
||||||
|
aethex.dev
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Brand */}
|
||||||
|
<Link
|
||||||
|
to="/ethos/library"
|
||||||
|
style={{
|
||||||
|
display: "flex", alignItems: "center", gap: 8,
|
||||||
|
textDecoration: "none", marginRight: 40, flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
width: 28, height: 28,
|
||||||
|
background: "linear-gradient(135deg, #7c3aed, #a855f7)",
|
||||||
|
borderRadius: "50%",
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center",
|
||||||
|
}}>
|
||||||
|
<Music2 className="h-3.5 w-3.5 text-white" />
|
||||||
|
</div>
|
||||||
|
<span style={{
|
||||||
|
fontFamily: "monospace", fontWeight: 700, fontSize: 13,
|
||||||
|
letterSpacing: 3, color: "#a855f7", textTransform: "uppercase",
|
||||||
|
}}>
|
||||||
|
Ethos Guild
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Nav tabs */}
|
||||||
|
<nav style={{ display: "flex", alignItems: "stretch", gap: 2, flex: 1, height: "100%" }}>
|
||||||
|
{NAV_ITEMS.filter(item => !item.memberOnly || user).map(item => (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
to={item.href}
|
||||||
|
style={{
|
||||||
|
display: "flex", alignItems: "center", gap: 6,
|
||||||
|
padding: "0 16px",
|
||||||
|
textDecoration: "none",
|
||||||
|
fontFamily: "monospace", fontSize: 11, letterSpacing: 1,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
color: isActive(item.href) ? "#a855f7" : "rgba(255,255,255,0.4)",
|
||||||
|
borderBottom: isActive(item.href) ? "2px solid #a855f7" : "2px solid transparent",
|
||||||
|
transition: "color 0.2s, border-color 0.2s",
|
||||||
|
marginBottom: -1,
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
if (!isActive(item.href)) e.currentTarget.style.color = "rgba(168,85,247,0.8)";
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
if (!isActive(item.href)) e.currentTarget.style.color = "rgba(255,255,255,0.4)";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<item.icon className="h-3.5 w-3.5" />
|
||||||
|
{item.name}
|
||||||
|
{item.memberOnly && (
|
||||||
|
<span style={{
|
||||||
|
fontSize: 8, padding: "1px 4px",
|
||||||
|
background: "rgba(168,85,247,0.15)",
|
||||||
|
color: "#a855f7", borderRadius: 2,
|
||||||
|
letterSpacing: 1,
|
||||||
|
}}>
|
||||||
|
MEMBER
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Sign in prompt for guests */}
|
||||||
|
{!user && (
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
style={{
|
||||||
|
fontFamily: "monospace", fontSize: 10, letterSpacing: 2,
|
||||||
|
color: "#a855f7", textDecoration: "none",
|
||||||
|
border: "1px solid rgba(168,85,247,0.4)",
|
||||||
|
padding: "5px 12px",
|
||||||
|
transition: "all 0.2s",
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
e.currentTarget.style.background = "rgba(168,85,247,0.1)";
|
||||||
|
e.currentTarget.style.borderColor = "rgba(168,85,247,0.7)";
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
e.currentTarget.style.background = "transparent";
|
||||||
|
e.currentTarget.style.borderColor = "rgba(168,85,247,0.4)";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
JOIN GUILD
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Page content */}
|
||||||
|
<main>{children}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
184
client/components/gameforge/GameForgeLayout.tsx
Normal file
184
client/components/gameforge/GameForgeLayout.tsx
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
import {
|
||||||
|
Gamepad2,
|
||||||
|
LayoutDashboard,
|
||||||
|
FolderKanban,
|
||||||
|
Users2,
|
||||||
|
Box,
|
||||||
|
ChevronLeft,
|
||||||
|
Lock,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface GameForgeLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarSection {
|
||||||
|
label: string;
|
||||||
|
items: {
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
icon: React.ElementType;
|
||||||
|
authRequired?: boolean;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIDEBAR: SidebarSection[] = [
|
||||||
|
{
|
||||||
|
label: "Overview",
|
||||||
|
items: [
|
||||||
|
{ name: "GameForge Home", href: "/gameforge", icon: Gamepad2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Studio",
|
||||||
|
items: [
|
||||||
|
{ name: "Dashboard", href: "/gameforge/manage", icon: LayoutDashboard, authRequired: true },
|
||||||
|
{ name: "Projects", href: "/gameforge/manage/projects", icon: FolderKanban, authRequired: true },
|
||||||
|
{ name: "Team", href: "/gameforge/manage/team", icon: Users2, authRequired: true },
|
||||||
|
{ name: "Assets", href: "/gameforge/manage/assets", icon: Box, authRequired: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function GameForgeLayout({ children }: GameForgeLayoutProps) {
|
||||||
|
const location = useLocation();
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const isActive = (href: string) =>
|
||||||
|
href === "/gameforge"
|
||||||
|
? location.pathname === href
|
||||||
|
: location.pathname.startsWith(href);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ minHeight: "100vh", background: "#050505", color: "#e0e0e0", display: "flex", flexDirection: "column" }}>
|
||||||
|
{/* Top strip */}
|
||||||
|
<div style={{ height: 2, background: "linear-gradient(90deg, #ff6b00, #ff9500, #ff6b00)", opacity: 0.7, flexShrink: 0 }} />
|
||||||
|
|
||||||
|
<div style={{ display: "flex", flex: 1 }}>
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside style={{
|
||||||
|
width: 220, flexShrink: 0,
|
||||||
|
background: "rgba(10,10,10,0.98)",
|
||||||
|
borderRight: "1px solid rgba(255,107,0,0.12)",
|
||||||
|
position: "sticky", top: 0, height: "100vh",
|
||||||
|
display: "flex", flexDirection: "column",
|
||||||
|
padding: "20px 0",
|
||||||
|
}}>
|
||||||
|
{/* Brand */}
|
||||||
|
<div style={{ padding: "0 20px 20px", borderBottom: "1px solid rgba(255,107,0,0.1)" }}>
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
style={{
|
||||||
|
display: "flex", alignItems: "center", gap: 5,
|
||||||
|
color: "rgba(255,107,0,0.45)", textDecoration: "none",
|
||||||
|
fontSize: 10, fontFamily: "monospace", letterSpacing: 1,
|
||||||
|
marginBottom: 14,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-3 w-3" />aethex.dev
|
||||||
|
</Link>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||||
|
<div style={{
|
||||||
|
width: 32, height: 32, background: "linear-gradient(135deg, #ff6b00, #ff9500)",
|
||||||
|
borderRadius: 4,
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center",
|
||||||
|
}}>
|
||||||
|
<Gamepad2 className="h-4 w-4 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontFamily: "monospace", fontWeight: 700, fontSize: 12, letterSpacing: 2, color: "#ff7a00" }}>
|
||||||
|
GAMEFORGE
|
||||||
|
</div>
|
||||||
|
<div style={{ fontFamily: "monospace", fontSize: 9, color: "rgba(255,107,0,0.4)", letterSpacing: 1 }}>
|
||||||
|
STUDIO MANAGEMENT
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nav sections */}
|
||||||
|
<nav style={{ flex: 1, padding: "16px 0", overflowY: "auto" }}>
|
||||||
|
{SIDEBAR.map(section => (
|
||||||
|
<div key={section.label} style={{ marginBottom: 20 }}>
|
||||||
|
<div style={{
|
||||||
|
padding: "0 20px 6px",
|
||||||
|
fontSize: 9, fontFamily: "monospace", letterSpacing: 2,
|
||||||
|
textTransform: "uppercase", color: "rgba(255,107,0,0.3)",
|
||||||
|
}}>
|
||||||
|
{section.label}
|
||||||
|
</div>
|
||||||
|
{section.items.map(item => {
|
||||||
|
const locked = item.authRequired && !user;
|
||||||
|
const active = isActive(item.href);
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
to={locked ? "/login" : item.href}
|
||||||
|
style={{
|
||||||
|
display: "flex", alignItems: "center", gap: 10,
|
||||||
|
padding: "8px 20px",
|
||||||
|
textDecoration: "none",
|
||||||
|
fontFamily: "monospace", fontSize: 11, letterSpacing: 0.5,
|
||||||
|
color: locked
|
||||||
|
? "rgba(255,255,255,0.2)"
|
||||||
|
: active
|
||||||
|
? "#ff7a00"
|
||||||
|
: "rgba(255,255,255,0.5)",
|
||||||
|
background: active ? "rgba(255,107,0,0.07)" : "transparent",
|
||||||
|
borderLeft: active ? "2px solid #ff7a00" : "2px solid transparent",
|
||||||
|
transition: "all 0.15s",
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
if (!active && !locked) {
|
||||||
|
e.currentTarget.style.color = "rgba(255,122,0,0.8)";
|
||||||
|
e.currentTarget.style.background = "rgba(255,107,0,0.04)";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
if (!active && !locked) {
|
||||||
|
e.currentTarget.style.color = "rgba(255,255,255,0.5)";
|
||||||
|
e.currentTarget.style.background = "transparent";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<item.icon className="h-3.5 w-3.5 shrink-0" />
|
||||||
|
<span style={{ flex: 1 }}>{item.name}</span>
|
||||||
|
{locked && <Lock className="h-3 w-3 opacity-40" />}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Footer hint */}
|
||||||
|
{!user && (
|
||||||
|
<div style={{ padding: "16px 20px", borderTop: "1px solid rgba(255,107,0,0.1)" }}>
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
style={{
|
||||||
|
display: "block", textAlign: "center",
|
||||||
|
fontFamily: "monospace", fontSize: 10, letterSpacing: 2,
|
||||||
|
color: "#ff7a00", textDecoration: "none",
|
||||||
|
border: "1px solid rgba(255,107,0,0.4)",
|
||||||
|
padding: "7px 0",
|
||||||
|
transition: "all 0.2s",
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => (e.currentTarget.style.background = "rgba(255,107,0,0.08)")}
|
||||||
|
onMouseLeave={e => (e.currentTarget.style.background = "transparent")}
|
||||||
|
>
|
||||||
|
SIGN IN TO MANAGE
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<main style={{ flex: 1, minWidth: 0 }}>{children}</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -243,6 +243,7 @@ export default function NotificationBell({
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
align="end"
|
||||||
className="w-80 border-border/40 bg-background/95 backdrop-blur"
|
className="w-80 border-border/40 bg-background/95 backdrop-blur"
|
||||||
|
style={{ zIndex: 99999 }}
|
||||||
>
|
>
|
||||||
<DropdownMenuLabel className="flex items-center justify-between">
|
<DropdownMenuLabel className="flex items-center justify-between">
|
||||||
<span className="text-sm font-semibold text-foreground">
|
<span className="text-sm font-semibold text-foreground">
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
checkProfileComplete,
|
checkProfileComplete,
|
||||||
} from "@/lib/aethex-database-adapter";
|
} from "@/lib/aethex-database-adapter";
|
||||||
|
|
||||||
type SupportedOAuthProvider = "github" | "google" | "discord";
|
type SupportedOAuthProvider = "github" | "google" | "discord" | string;
|
||||||
|
|
||||||
interface LinkedProvider {
|
interface LinkedProvider {
|
||||||
provider: SupportedOAuthProvider;
|
provider: SupportedOAuthProvider;
|
||||||
|
|
@ -165,6 +165,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const rewardsActivatedRef = useRef(false);
|
const rewardsActivatedRef = useRef(false);
|
||||||
const storageClearedRef = useRef(false);
|
const storageClearedRef = useRef(false);
|
||||||
|
// True after the very first auth event resolves — distinguishes session
|
||||||
|
// restoration (page load) from a real user-initiated sign-in.
|
||||||
|
const initialEventFired = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let sessionRestored = false;
|
let sessionRestored = false;
|
||||||
|
|
@ -276,17 +279,24 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show toast notifications for auth events
|
// Only toast on real user-initiated events, not session restoration on page load.
|
||||||
if (event === "SIGNED_IN") {
|
// 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({
|
aethexToast.success({
|
||||||
title: "Welcome back!",
|
title: "Signed in",
|
||||||
description: "Successfully signed in to AeThex OS",
|
description: "Welcome back to AeThex OS",
|
||||||
});
|
});
|
||||||
} else if (event === "SIGNED_OUT") {
|
} else if (event === "SIGNED_OUT") {
|
||||||
aethexToast.info({
|
aethexToast.info({
|
||||||
title: "Signed out",
|
title: "Signed out",
|
||||||
description: "Come back soon!",
|
description: "Come back soon!",
|
||||||
});
|
});
|
||||||
|
} else if (event === "TOKEN_REFRESHED") {
|
||||||
|
// Silently refresh — no toast
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -684,7 +694,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||||
provider,
|
provider: provider as any,
|
||||||
options: {
|
options: {
|
||||||
redirectTo: `${window.location.origin}/login`,
|
redirectTo: `${window.location.origin}/login`,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,88 @@
|
||||||
@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 base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
/* ── AeThex Cyberpunk Theme — copied verbatim from AeThex-Passport-Engine/client/src/index.css ── */
|
||||||
/**
|
|
||||||
* 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 {
|
:root {
|
||||||
--background: 222 84% 4.9%;
|
--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;
|
||||||
|
|
||||||
|
/* 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%;
|
||||||
|
|
||||||
|
/* Neon accent palette */
|
||||||
|
--neon-green: 142 76% 45%;
|
||||||
|
--neon-yellow: 50 100% 65%;
|
||||||
|
|
||||||
/* Spacing tokens */
|
/* Spacing tokens */
|
||||||
--space-1: 4px;
|
--space-1: 4px;
|
||||||
|
|
@ -28,63 +92,92 @@
|
||||||
--space-5: 24px;
|
--space-5: 24px;
|
||||||
--space-6: 32px;
|
--space-6: 32px;
|
||||||
--space-section-y: var(--space-6);
|
--space-section-y: var(--space-6);
|
||||||
--foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--card: 222 84% 4.9%;
|
/* Fallback for older browsers */
|
||||||
--card-foreground: 210 40% 98%;
|
--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);
|
||||||
--popover: 222 84% 4.9%;
|
--sidebar-accent-border: hsl(var(--sidebar-accent));
|
||||||
--popover-foreground: 210 40% 98%;
|
--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: 250 100% 60%;
|
--primary-border: hsl(from hsl(var(--primary)) h s calc(l + var(--opaque-button-border-intensity)) / alpha);
|
||||||
--primary-foreground: 210 40% 98%;
|
--secondary-border: hsl(var(--secondary));
|
||||||
|
--secondary-border: hsl(from hsl(var(--secondary)) h s calc(l + var(--opaque-button-border-intensity)) / alpha);
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--muted-border: hsl(var(--muted));
|
||||||
--secondary-foreground: 210 40% 98%;
|
--muted-border: hsl(from hsl(var(--muted)) h s calc(l + var(--opaque-button-border-intensity)) / alpha);
|
||||||
|
--accent-border: hsl(var(--accent));
|
||||||
--muted: 217.2 32.6% 17.5%;
|
--accent-border: hsl(from hsl(var(--accent)) h s calc(l + var(--opaque-button-border-intensity)) / alpha);
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
--destructive-border: hsl(var(--destructive));
|
||||||
|
--destructive-border: hsl(from hsl(var(--destructive)) h s calc(l + var(--opaque-button-border-intensity)) / alpha);
|
||||||
--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%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
|
||||||
|
--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 {
|
@layer base {
|
||||||
|
|
@ -93,529 +186,280 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply font-sans antialiased bg-background text-foreground;
|
||||||
font-family: "Courier New", "Courier", monospace;
|
}
|
||||||
letter-spacing: 0.025em;
|
|
||||||
|
/* Scanline overlay — from AeThex-Passport-Engine */
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-image: repeating-linear-gradient(
|
||||||
|
0deg,
|
||||||
|
transparent,
|
||||||
|
transparent 1px,
|
||||||
|
rgba(0, 255, 255, 0.015) 1px,
|
||||||
|
rgba(0, 255, 255, 0.015) 2px
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid background — from AeThex-Passport-Engine */
|
||||||
|
body::after {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(0, 255, 255, 0.03) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(0, 255, 255, 0.03) 1px, transparent 1px);
|
||||||
|
background-size: 50px 50px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
/* Hide scrollbar while keeping functionality */
|
-ms-overflow-style: none;
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
|
||||||
scrollbar-width: none; /* Firefox */
|
|
||||||
}
|
|
||||||
|
|
||||||
html::-webkit-scrollbar {
|
|
||||||
display: none; /* Chrome, Safari and Opera */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide horizontal scrollbar on all elements */
|
|
||||||
* {
|
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar {
|
html::-webkit-scrollbar { display: none; }
|
||||||
display: none;
|
*::-webkit-scrollbar { display: none; }
|
||||||
}
|
* { scrollbar-width: none; }
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@apply px-4 sm:px-6 lg:px-8;
|
@apply px-4 sm:px-6 lg:px-8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Elevation system — from AeThex-Passport-Engine ── */
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
/* Arm Theme Font Classes */
|
input[type="search"]::-webkit-search-cancel-button {
|
||||||
.font-labs {
|
@apply hidden;
|
||||||
font-family: "VT323", "Courier New", monospace;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-gameforge {
|
[contenteditable][data-placeholder]:empty::before {
|
||||||
font-family: "Press Start 2P", "Arial Black", sans-serif;
|
content: attr(data-placeholder);
|
||||||
letter-spacing: 0.1em;
|
color: hsl(var(--muted-foreground));
|
||||||
font-size: 0.875em;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-corp {
|
.no-default-hover-elevate {}
|
||||||
font-family: "Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI",
|
.no-default-active-elevate {}
|
||||||
sans-serif;
|
|
||||||
font-weight: 600;
|
.toggle-elevate::before,
|
||||||
|
.toggle-elevate-2::before {
|
||||||
|
content: "";
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
inset: 0px;
|
||||||
|
border-radius: inherit;
|
||||||
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-foundation {
|
.toggle-elevate.toggle-elevated::before {
|
||||||
font-family: "Merriweather", "Georgia", serif;
|
background-color: var(--elevate-2);
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-devlink {
|
.border.toggle-elevate::before { inset: -1px; }
|
||||||
font-family: "Roboto Mono", "Courier New", monospace;
|
|
||||||
font-weight: 400;
|
.hover-elevate:not(.no-default-hover-elevate),
|
||||||
letter-spacing: 0.02em;
|
.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 {
|
.hover-elevate:not(.no-default-hover-elevate)::after,
|
||||||
font-family: "Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI",
|
.active-elevate:not(.no-default-active-elevate)::after,
|
||||||
sans-serif;
|
.hover-elevate-2:not(.no-default-hover-elevate)::after,
|
||||||
font-weight: 600;
|
.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 {
|
.hover-elevate:hover:not(.no-default-hover-elevate)::after,
|
||||||
font-family: "Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI",
|
.active-elevate:active:not(.no-default-active-elevate)::after {
|
||||||
sans-serif;
|
background-color: var(--elevate-1);
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-default {
|
.hover-elevate-2:hover:not(.no-default-hover-elevate)::after,
|
||||||
font-family: "Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI",
|
.active-elevate-2:active:not(.no-default-active-elevate)::after {
|
||||||
sans-serif;
|
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 {
|
.wallpaper-labs {
|
||||||
background-image: radial-gradient(
|
background-image: radial-gradient(circle, rgba(251,191,36,0.08) 1px, transparent 1px);
|
||||||
circle,
|
|
||||||
rgba(251, 191, 36, 0.08) 1px,
|
|
||||||
transparent 1px
|
|
||||||
);
|
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallpaper-gameforge {
|
.wallpaper-gameforge {
|
||||||
background-image: linear-gradient(
|
background-image:
|
||||||
45deg,
|
linear-gradient(45deg, rgba(34,197,94,0.06) 25%, transparent 25%, transparent 75%, rgba(34,197,94,0.06) 75%),
|
||||||
rgba(34, 197, 94, 0.06) 25%,
|
linear-gradient(45deg, rgba(34,197,94,0.06) 25%, transparent 25%, transparent 75%, rgba(34,197,94,0.06) 75%);
|
||||||
transparent 25%,
|
|
||||||
transparent 75%,
|
|
||||||
rgba(34, 197, 94, 0.06) 75%
|
|
||||||
),
|
|
||||||
linear-gradient(
|
|
||||||
45deg,
|
|
||||||
rgba(34, 197, 94, 0.06) 25%,
|
|
||||||
transparent 25%,
|
|
||||||
transparent 75%,
|
|
||||||
rgba(34, 197, 94, 0.06) 75%
|
|
||||||
);
|
|
||||||
background-size: 40px 40px;
|
background-size: 40px 40px;
|
||||||
background-position: 0 0, 20px 20px;
|
background-position: 0 0, 20px 20px;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallpaper-corp {
|
.wallpaper-corp {
|
||||||
background-image: linear-gradient(
|
background-image:
|
||||||
90deg,
|
linear-gradient(90deg, rgba(59,130,246,0.05) 1px, transparent 1px),
|
||||||
rgba(59, 130, 246, 0.05) 1px,
|
|
||||||
transparent 1px
|
|
||||||
),
|
|
||||||
linear-gradient(rgba(59,130,246,0.05) 1px, transparent 1px);
|
linear-gradient(rgba(59,130,246,0.05) 1px, transparent 1px);
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallpaper-foundation {
|
.wallpaper-foundation {
|
||||||
background-image: repeating-linear-gradient(
|
background-image: repeating-linear-gradient(0deg, rgba(239,68,68,0.04) 0px, rgba(239,68,68,0.04) 1px, transparent 1px, transparent 2px);
|
||||||
0deg,
|
|
||||||
rgba(239, 68, 68, 0.04) 0px,
|
|
||||||
rgba(239, 68, 68, 0.04) 1px,
|
|
||||||
transparent 1px,
|
|
||||||
transparent 2px
|
|
||||||
);
|
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallpaper-devlink {
|
.wallpaper-devlink {
|
||||||
background-image: linear-gradient(
|
background-image:
|
||||||
0deg,
|
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 24%,
|
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%);
|
||||||
rgba(6, 182, 212, 0.08) 25%,
|
|
||||||
rgba(6, 182, 212, 0.08) 26%,
|
|
||||||
transparent 27%,
|
|
||||||
transparent 74%,
|
|
||||||
rgba(6, 182, 212, 0.08) 75%,
|
|
||||||
rgba(6, 182, 212, 0.08) 76%,
|
|
||||||
transparent 77%,
|
|
||||||
transparent
|
|
||||||
),
|
|
||||||
linear-gradient(
|
|
||||||
90deg,
|
|
||||||
transparent 24%,
|
|
||||||
rgba(6, 182, 212, 0.08) 25%,
|
|
||||||
rgba(6, 182, 212, 0.08) 26%,
|
|
||||||
transparent 27%,
|
|
||||||
transparent 74%,
|
|
||||||
rgba(6, 182, 212, 0.08) 75%,
|
|
||||||
rgba(6, 182, 212, 0.08) 76%,
|
|
||||||
transparent 77%,
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
background-size: 50px 50px;
|
background-size: 50px 50px;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallpaper-staff {
|
.wallpaper-staff {
|
||||||
background-image: radial-gradient(
|
background-image: radial-gradient(circle, rgba(168,85,247,0.08) 1px, transparent 1px);
|
||||||
circle,
|
|
||||||
rgba(168, 85, 247, 0.08) 1px,
|
|
||||||
transparent 1px
|
|
||||||
);
|
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallpaper-nexus {
|
.wallpaper-nexus {
|
||||||
background-image: linear-gradient(
|
background-image:
|
||||||
45deg,
|
linear-gradient(45deg, rgba(236,72,153,0.06) 25%, transparent 25%, transparent 75%, rgba(236,72,153,0.06) 75%),
|
||||||
rgba(236, 72, 153, 0.06) 25%,
|
linear-gradient(45deg, rgba(236,72,153,0.06) 25%, transparent 25%, transparent 75%, rgba(236,72,153,0.06) 75%);
|
||||||
transparent 25%,
|
|
||||||
transparent 75%,
|
|
||||||
rgba(236, 72, 153, 0.06) 75%
|
|
||||||
),
|
|
||||||
linear-gradient(
|
|
||||||
45deg,
|
|
||||||
rgba(236, 72, 153, 0.06) 25%,
|
|
||||||
transparent 25%,
|
|
||||||
transparent 75%,
|
|
||||||
rgba(236, 72, 153, 0.06) 75%
|
|
||||||
);
|
|
||||||
background-size: 40px 40px;
|
background-size: 40px 40px;
|
||||||
background-position: 0 0, 20px 20px;
|
background-position: 0 0, 20px 20px;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallpaper-default {
|
.wallpaper-default {
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(135deg, rgba(0,255,255,0.03) 0%, rgba(0,255,255,0.01) 100%);
|
||||||
135deg,
|
|
||||||
rgba(167, 139, 250, 0.05) 0%,
|
|
||||||
rgba(96, 165, 250, 0.05) 100%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-cozy {
|
/* ── Font aliases (arm theming) ── */
|
||||||
padding-block: var(--space-section-y);
|
.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; }
|
||||||
.gap-cozy {
|
.font-corp { font-family: "Electrolize", "Source Code Pro", monospace; font-weight: 600; }
|
||||||
gap: var(--space-5);
|
.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; }
|
||||||
.pad-cozy {
|
.font-staff { font-family: "Electrolize", "Source Code Pro", monospace; font-weight: 600; }
|
||||||
padding: var(--space-5);
|
.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 {
|
.text-gradient {
|
||||||
@apply bg-gradient-to-r from-aethex-400 via-neon-blue to-aethex-600 bg-clip-text text-transparent;
|
@apply bg-gradient-to-r from-aethex-300 via-aethex-500 to-neon-purple bg-clip-text text-transparent;
|
||||||
background-size: 200% 200%;
|
|
||||||
animation: gradient-shift 3s ease-in-out infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-gradient-purple {
|
.text-gradient-purple {
|
||||||
@apply bg-gradient-to-r from-neon-purple via-aethex-500 to-neon-blue bg-clip-text text-transparent;
|
@apply bg-gradient-to-r from-neon-purple via-aethex-500 to-aethex-300 bg-clip-text text-transparent;
|
||||||
background-size: 200% 200%;
|
|
||||||
animation: gradient-shift 4s ease-in-out infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-aethex-gradient {
|
.bg-aethex-gradient {
|
||||||
@apply bg-gradient-to-br from-aethex-900 via-background to-aethex-800;
|
@apply bg-gradient-to-br from-aethex-900 via-background to-aethex-800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-gradient {
|
/* ── Interaction ── */
|
||||||
@apply relative overflow-hidden;
|
.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 {
|
/* ── Spacing helpers ── */
|
||||||
content: "";
|
.section-cozy { padding-block: var(--space-section-y); }
|
||||||
@apply absolute inset-0 rounded-[inherit] p-[1px] bg-gradient-to-r from-aethex-400 via-neon-blue to-aethex-600;
|
.gap-cozy { gap: var(--space-5); }
|
||||||
mask:
|
.pad-cozy { padding: var(--space-5); }
|
||||||
linear-gradient(#fff 0 0) content-box,
|
|
||||||
linear-gradient(#fff 0 0);
|
|
||||||
mask-composite: xor;
|
|
||||||
background-size: 200% 200%;
|
|
||||||
animation: gradient-shift 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glow-purple {
|
|
||||||
box-shadow:
|
|
||||||
0 0 20px rgba(139, 92, 246, 0.3),
|
|
||||||
0 0 40px rgba(139, 92, 246, 0.2);
|
|
||||||
transition: box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glow-purple:hover {
|
|
||||||
box-shadow:
|
|
||||||
0 0 30px rgba(139, 92, 246, 0.5),
|
|
||||||
0 0 60px rgba(139, 92, 246, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.glow-blue {
|
|
||||||
box-shadow:
|
|
||||||
0 0 20px rgba(59, 130, 246, 0.3),
|
|
||||||
0 0 40px rgba(59, 130, 246, 0.2);
|
|
||||||
transition: box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glow-blue:hover {
|
|
||||||
box-shadow:
|
|
||||||
0 0 30px rgba(59, 130, 246, 0.5),
|
|
||||||
0 0 60px rgba(59, 130, 246, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.glow-green {
|
|
||||||
box-shadow:
|
|
||||||
0 0 20px rgba(34, 197, 94, 0.3),
|
|
||||||
0 0 40px rgba(34, 197, 94, 0.2);
|
|
||||||
transition: box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glow-yellow {
|
|
||||||
box-shadow:
|
|
||||||
0 0 20px rgba(251, 191, 36, 0.3),
|
|
||||||
0 0 40px rgba(251, 191, 36, 0.2);
|
|
||||||
transition: box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-fade-in {
|
|
||||||
animation: fade-in 0.6s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-slide-up {
|
|
||||||
animation: slide-up 0.6s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-slide-down {
|
|
||||||
animation: slide-down 0.6s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-slide-left {
|
|
||||||
animation: slide-left 0.6s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-slide-right {
|
|
||||||
animation: slide-right 0.6s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-scale-in {
|
|
||||||
animation: scale-in 0.4s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-bounce-gentle {
|
|
||||||
animation: bounce-gentle 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-float {
|
|
||||||
animation: float 3s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-pulse-glow {
|
|
||||||
animation: pulse-glow 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/* ── Animations ── */
|
||||||
|
.animate-fade-in { animation: fade-in 0.6s ease-out; }
|
||||||
|
.animate-slide-up { animation: slide-up 0.6s ease-out; }
|
||||||
|
.animate-slide-down { animation: slide-down 0.6s ease-out; }
|
||||||
|
.animate-slide-left { animation: slide-left 0.6s ease-out; }
|
||||||
|
.animate-slide-right { animation: slide-right 0.6s ease-out; }
|
||||||
|
.animate-scale-in { animation: scale-in 0.4s ease-out; }
|
||||||
.animate-typing {
|
.animate-typing {
|
||||||
animation:
|
animation: typing 3s steps(40, end), blink-caret 0.75s step-end infinite;
|
||||||
typing 3s steps(40, end),
|
|
||||||
blink-caret 0.75s step-end infinite;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-right: 3px solid;
|
border-right: 3px solid;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover-lift {
|
|
||||||
transition:
|
|
||||||
transform 0.3s ease,
|
|
||||||
box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-lift:hover {
|
|
||||||
transform: translateY(-8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-glow {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-glow:hover {
|
|
||||||
filter: brightness(1.1) drop-shadow(0 0 10px currentColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.interactive-scale {
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.interactive-scale:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.interactive-scale:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-dots::after {
|
|
||||||
content: "";
|
|
||||||
animation: loading-dots 1.5s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton {
|
.skeleton {
|
||||||
background: linear-gradient(
|
background: linear-gradient(90deg, transparent, rgba(0,255,255,0.06), transparent);
|
||||||
90deg,
|
|
||||||
transparent,
|
|
||||||
rgba(255, 255, 255, 0.1),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
animation: skeleton-loading 1.5s infinite;
|
animation: skeleton-loading 1.5s infinite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes gradient-shift {
|
/* ── Keyframes ── */
|
||||||
0%,
|
@keyframes ax-sweep { 0%{left:-100%} 100%{left:200%} }
|
||||||
100% {
|
@keyframes ax-blink { 0%,50%{opacity:1} 51%,100%{opacity:0} }
|
||||||
background-position: 0% 50%;
|
@keyframes fade-in { from{opacity:0} to{opacity:1} }
|
||||||
}
|
@keyframes slide-up { from{opacity:0;transform:translateY(20px)} to{opacity:1;transform:translateY(0)} }
|
||||||
50% {
|
@keyframes slide-down { from{opacity:0;transform:translateY(-20px)} to{opacity:1;transform:translateY(0)} }
|
||||||
background-position: 100% 50%;
|
@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 fade-in {
|
@keyframes blink-caret { from,to{border-color:transparent} 50%{border-color:currentColor} }
|
||||||
from {
|
@keyframes skeleton-loading { 0%{background-position:-200% 0} 100%{background-position:200% 0} }
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide-up {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(30px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide-down {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-30px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide-left {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(30px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide-right {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-30px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scale-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce-gentle {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: translateY(0px);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateY(-20px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-glow {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.8;
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes typing {
|
|
||||||
from {
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink-caret {
|
|
||||||
from,
|
|
||||||
to {
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
border-color: currentColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes loading-dots {
|
|
||||||
0% {
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
content: ".";
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
content: "..";
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
content: "...";
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes skeleton-loading {
|
|
||||||
0% {
|
|
||||||
background-position: -200% 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 200% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
* {
|
* {
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
import { supabase, isSupabaseConfigured } from "@/lib/supabase";
|
import { supabase, isSupabaseConfigured } from "@/lib/supabase";
|
||||||
import type { Database } from "./database.types";
|
import type { Database } from "./database.types";
|
||||||
|
|
||||||
// Use the existing database user profile type directly
|
// Derive UserProfile from the live generated schema
|
||||||
import type { UserProfile } from "./database.types";
|
type UserProfile = Database["public"]["Tables"]["user_profiles"]["Row"];
|
||||||
|
|
||||||
// API Base URL for fetch requests
|
// API Base URL for fetch requests
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
||||||
|
|
|
||||||
30
client/lib/auth-fetch.ts
Normal file
30
client/lib/auth-fetch.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { supabase } from "@/lib/supabase";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticated fetch wrapper.
|
||||||
|
* Automatically injects `Authorization: Bearer <token>` from the active
|
||||||
|
* Supabase session. Falls back to an unauthenticated request if no session
|
||||||
|
* exists (lets public endpoints still work normally).
|
||||||
|
*
|
||||||
|
* Drop-in replacement for `fetch` — same signature, same return value.
|
||||||
|
*/
|
||||||
|
export async function authFetch(
|
||||||
|
input: RequestInfo | URL,
|
||||||
|
init: RequestInit = {}
|
||||||
|
): Promise<Response> {
|
||||||
|
const {
|
||||||
|
data: { session },
|
||||||
|
} = await supabase.auth.getSession();
|
||||||
|
|
||||||
|
const headers = new Headers(init.headers);
|
||||||
|
|
||||||
|
if (session?.access_token) {
|
||||||
|
headers.set("Authorization", `Bearer ${session.access_token}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (init.body && typeof init.body === "string" && !headers.has("Content-Type")) {
|
||||||
|
headers.set("Content-Type", "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(input, { ...init, headers });
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -45,6 +45,9 @@ import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Github,
|
Github,
|
||||||
Mail,
|
Mail,
|
||||||
|
Loader2,
|
||||||
|
Unlink,
|
||||||
|
Link as LinkIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
const DiscordIcon = () => (
|
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() {
|
export default function Dashboard() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {
|
const {
|
||||||
|
|
@ -678,7 +768,7 @@ export default function Dashboard() {
|
||||||
linkedProviderMap={
|
linkedProviderMap={
|
||||||
linkedProviders
|
linkedProviders
|
||||||
? Object.fromEntries(
|
? 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}
|
onLink={linkProvider}
|
||||||
onUnlink={unlinkProvider}
|
onUnlink={unlinkProvider}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* AeThex ID (Authentik SSO) — staff/internal identity */}
|
||||||
|
<AeThexIDConnection user={user} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import Layout from "@/components/Layout";
|
import GameForgeLayout from "@/components/gameforge/GameForgeLayout";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
@ -102,7 +102,7 @@ export default function GameForge() {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<GameForgeLayout>
|
||||||
<div className="relative min-h-screen bg-black text-white overflow-hidden">
|
<div className="relative min-h-screen bg-black text-white overflow-hidden">
|
||||||
{/* Persistent Info Banner */}
|
{/* Persistent Info Banner */}
|
||||||
<div className="bg-green-500/10 border-b border-green-400/30 py-3 sticky top-0 z-50 backdrop-blur-sm">
|
<div className="bg-green-500/10 border-b border-green-400/30 py-3 sticky top-0 z-50 backdrop-blur-sm">
|
||||||
|
|
@ -478,6 +478,6 @@ export default function GameForge() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</GameForgeLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import SEO from "@/components/SEO";
|
import SEO from "@/components/SEO";
|
||||||
import Layout from "@/components/Layout";
|
import Layout from "@/components/Layout";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -29,48 +28,36 @@ const ecosystemPillars = [
|
||||||
title: "Six Realms",
|
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",
|
href: "/realms",
|
||||||
gradient: "from-purple-500 via-purple-600 to-indigo-600",
|
|
||||||
accentColor: "hsl(var(--primary))",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Database,
|
icon: Database,
|
||||||
title: "Developer APIs",
|
title: "Developer APIs",
|
||||||
description: "Comprehensive REST APIs for users, content, achievements, and more",
|
description: "Comprehensive REST APIs for users, content, achievements, and more",
|
||||||
href: "/dev-platform/api-reference",
|
href: "/dev-platform/api-reference",
|
||||||
gradient: "from-blue-500 via-blue-600 to-cyan-600",
|
|
||||||
accentColor: "hsl(var(--primary))",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Terminal,
|
icon: Terminal,
|
||||||
title: "SDK & Tools",
|
title: "SDK & Tools",
|
||||||
description: "TypeScript SDK, CLI tools, and pre-built templates to ship faster",
|
description: "TypeScript SDK, CLI tools, and pre-built templates to ship faster",
|
||||||
href: "/dev-platform/quick-start",
|
href: "/dev-platform/quick-start",
|
||||||
gradient: "from-cyan-500 via-teal-600 to-emerald-600",
|
|
||||||
accentColor: "hsl(var(--primary))",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Layers,
|
icon: Layers,
|
||||||
title: "Marketplace",
|
title: "Marketplace",
|
||||||
description: "Premium integrations, plugins, and components from the community",
|
description: "Premium integrations, plugins, and components from the community",
|
||||||
href: "/dev-platform/marketplace",
|
href: "/dev-platform/marketplace",
|
||||||
gradient: "from-emerald-500 via-green-600 to-lime-600",
|
|
||||||
accentColor: "hsl(var(--primary))",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Users,
|
icon: Users,
|
||||||
title: "Community",
|
title: "Community",
|
||||||
description: "Join 12,000+ developers building on AeThex",
|
description: "Join 12,000+ developers building on AeThex",
|
||||||
href: "/community",
|
href: "/community",
|
||||||
gradient: "from-amber-500 via-orange-600 to-red-600",
|
|
||||||
accentColor: "hsl(var(--primary))",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Trophy,
|
icon: Trophy,
|
||||||
title: "Opportunities",
|
title: "Opportunities",
|
||||||
description: "Get paid to build — contracts, bounties, and commissions",
|
description: "Get paid to build — contracts, bounties, and commissions",
|
||||||
href: "/opportunities",
|
href: "/opportunities",
|
||||||
gradient: "from-pink-500 via-rose-600 to-red-600",
|
|
||||||
accentColor: "hsl(var(--primary))",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -85,7 +72,7 @@ const features = [
|
||||||
{
|
{
|
||||||
icon: Layers,
|
icon: Layers,
|
||||||
title: "Cross-Platform Integration Layer",
|
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,
|
icon: Code2,
|
||||||
|
|
@ -105,27 +92,19 @@ const features = [
|
||||||
{
|
{
|
||||||
icon: Users,
|
icon: Users,
|
||||||
title: "Thriving Creator Economy",
|
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,
|
icon: Rocket,
|
||||||
title: "Ship Everywhere, Fast",
|
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",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const platforms = ["Roblox", "Minecraft", "Meta Horizon", "Fortnite", "VRChat", "Zepeto"];
|
||||||
|
const platformIcons = [Gamepad2, Boxes, Globe, Zap, Users, Sparkles];
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
|
||||||
const [hoveredCard, setHoveredCard] = useState<number | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
|
||||||
setMousePosition({ x: e.clientX, y: e.clientY });
|
|
||||||
};
|
|
||||||
window.addEventListener("mousemove", handleMouseMove);
|
|
||||||
return () => window.removeEventListener("mousemove", handleMouseMove);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout hideFooter>
|
<Layout hideFooter>
|
||||||
<SEO
|
<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">
|
<div className="fixed inset-0 pointer-events-none overflow-hidden">
|
||||||
<motion.div
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_70%_40%_at_50%_-10%,hsl(var(--primary)/0.08),transparent)]" />
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<div className="relative space-y-32 pb-32">
|
<div className="relative space-y-28 pb-28">
|
||||||
<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">
|
{/* 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
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -20 }}
|
initial={{ opacity: 0, y: -12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.8 }}
|
transition={{ duration: 0.5 }}
|
||||||
>
|
>
|
||||||
<Badge
|
<Badge className="text-xs px-4 py-1.5 bg-primary/10 border-primary/30 uppercase tracking-widest font-semibold">
|
||||||
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-3 h-3 mr-1.5 inline" />
|
||||||
>
|
|
||||||
<Sparkles className="w-4 h-4 mr-2 inline animate-pulse" />
|
|
||||||
AeThex Developer Ecosystem
|
AeThex Developer Ecosystem
|
||||||
</Badge>
|
</Badge>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.h1
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
initial={{ opacity: 0, y: 16 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.8, delay: 0.2 }}
|
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{" "}
|
||||||
Build on
|
<span className="text-primary">AeThex</span>
|
||||||
<br />
|
</motion.h1>
|
||||||
<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>
|
|
||||||
|
|
||||||
<motion.p
|
<motion.p
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 16 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.8, delay: 0.4 }}
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
className="text-2xl md:text-3xl text-muted-foreground max-w-4xl mx-auto leading-relaxed font-light"
|
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.
|
The <span className="text-foreground font-medium">integration layer</span> connecting all metaverse platforms.
|
||||||
<br className="hidden md:block" />
|
Six specialized realms. <span className="text-foreground font-medium">12K+ developers</span>. One powerful ecosystem.
|
||||||
Six specialized realms. <span className="text-primary font-semibold">12K+ developers</span>. One powerful ecosystem.
|
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
{/* Platform Highlights */}
|
{/* Platform pills */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 16 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.8, delay: 0.5 }}
|
transition={{ duration: 0.6, delay: 0.3 }}
|
||||||
className="flex flex-wrap items-center justify-center gap-3 pt-4 text-sm md:text-base max-w-4xl mx-auto"
|
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">
|
{platforms.map((name, i) => {
|
||||||
<Gamepad2 className="w-4 h-4 text-primary drop-shadow-[0_0_8px_rgba(168,85,247,0.8)]" />
|
const Icon = platformIcons[i];
|
||||||
<span className="text-foreground/90 font-bold uppercase tracking-wide">Roblox</span>
|
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>
|
||||||
<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 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">
|
||||||
</div>
|
& More
|
||||||
<div className="flex items-center gap-2 backdrop-blur-xl bg-primary/5 px-4 py-2 rounded-full border-2 border-primary/30 hover:border-primary/60 hover:bg-primary/10 hover:shadow-[0_0_20px_rgba(168,85,247,0.3)] transition-all">
|
|
||||||
<Globe className="w-4 h-4 text-primary drop-shadow-[0_0_8px_rgba(168,85,247,0.8)]" />
|
|
||||||
<span className="text-foreground/90 font-bold uppercase tracking-wide">Meta Horizon</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 backdrop-blur-xl bg-primary/5 px-4 py-2 rounded-full border-2 border-primary/30 hover:border-primary/60 hover:bg-primary/10 hover:shadow-[0_0_20px_rgba(168,85,247,0.3)] transition-all">
|
|
||||||
<Zap className="w-4 h-4 text-primary drop-shadow-[0_0_8px_rgba(168,85,247,0.8)]" />
|
|
||||||
<span className="text-foreground/90 font-bold uppercase tracking-wide">Fortnite</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 backdrop-blur-xl bg-primary/5 px-4 py-2 rounded-full border-2 border-primary/30 hover:border-primary/60 hover:bg-primary/10 hover:shadow-[0_0_20px_rgba(168,85,247,0.3)] transition-all">
|
|
||||||
<Users className="w-4 h-4 text-primary drop-shadow-[0_0_8px_rgba(168,85,247,0.8)]" />
|
|
||||||
<span className="text-foreground/90 font-bold uppercase tracking-wide">Zepeto</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 backdrop-blur-xl bg-primary/10 px-4 py-2 rounded-full border-2 border-primary/40 shadow-[0_0_20px_rgba(168,85,247,0.4)]">
|
|
||||||
<Sparkles className="w-4 h-4 text-primary drop-shadow-[0_0_8px_rgba(168,85,247,0.8)] animate-pulse" />
|
|
||||||
<span className="text-foreground/90 font-black uppercase tracking-wide">& More</span>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
{/* CTAs */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 16 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.8, delay: 0.6 }}
|
transition={{ duration: 0.6, delay: 0.4 }}
|
||||||
className="flex flex-wrap gap-4 justify-center pt-8"
|
className="flex flex-wrap gap-3 justify-center pt-4"
|
||||||
>
|
>
|
||||||
<Link to="/dev-platform/quick-start">
|
<Link to="/dev-platform/quick-start">
|
||||||
<Button
|
<Button size="lg" className="px-8 h-12 font-semibold">
|
||||||
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"
|
|
||||||
>
|
|
||||||
Start Building
|
Start Building
|
||||||
<Rocket className="w-6 h-6 ml-3" />
|
<Rocket className="w-4 h-4 ml-2" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/dev-platform/api-reference">
|
<Link to="/dev-platform/api-reference">
|
||||||
<Button
|
<Button size="lg" variant="outline" className="px-8 h-12 font-semibold">
|
||||||
size="lg"
|
<BookOpen className="w-4 h-4 mr-2" />
|
||||||
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" />
|
|
||||||
Explore APIs
|
Explore APIs
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 1, delay: 0.8 }}
|
transition={{ duration: 0.8, delay: 0.6 }}
|
||||||
className="grid grid-cols-2 md:grid-cols-4 gap-8 pt-16 max-w-4xl mx-auto"
|
className="grid grid-cols-2 md:grid-cols-4 gap-4 pt-12 max-w-3xl mx-auto"
|
||||||
>
|
>
|
||||||
{stats.map((stat, i) => (
|
{stats.map((stat) => (
|
||||||
<motion.div
|
<div
|
||||||
key={stat.label}
|
key={stat.label}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
className="bg-secondary/40 border border-border rounded-xl p-5 text-center"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.6, delay: 0.8 + i * 0.1 }}
|
|
||||||
className="relative group"
|
|
||||||
>
|
>
|
||||||
<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-3xl font-black text-primary">{stat.value}</p>
|
||||||
<p className="text-4xl md:text-5xl font-black text-primary mb-2">
|
<p className="text-xs text-muted-foreground mt-1 font-medium uppercase tracking-wide">{stat.label}</p>
|
||||||
{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>
|
</div>
|
||||||
</motion.div>
|
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1, y: [0, 10, 0] }}
|
|
||||||
transition={{
|
|
||||||
opacity: { delay: 1.5, duration: 0.5 },
|
|
||||||
y: { duration: 2, repeat: Infinity, ease: "easeInOut" },
|
|
||||||
}}
|
|
||||||
className="absolute bottom-8 left-1/2 transform -translate-x-1/2"
|
|
||||||
>
|
|
||||||
<div className="w-6 h-10 border-2 border-primary/30 rounded-full flex items-start justify-center p-2">
|
|
||||||
<motion.div
|
|
||||||
className="w-1 h-2 bg-primary rounded-full"
|
|
||||||
animate={{ y: [0, 12, 0] }}
|
|
||||||
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="space-y-12 px-4">
|
{/* Ecosystem Pillars */}
|
||||||
|
<section className="space-y-10 px-4">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.8 }}
|
transition={{ duration: 0.6 }}
|
||||||
className="text-center space-y-4"
|
className="text-center space-y-3"
|
||||||
>
|
>
|
||||||
<h2 className="text-5xl md:text-6xl font-black text-primary">
|
<h2 className="text-4xl md:text-5xl font-black">The AeThex Ecosystem</h2>
|
||||||
The AeThex Ecosystem
|
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||||
</h2>
|
|
||||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
|
||||||
Six interconnected realms, each with unique capabilities and APIs to power your applications
|
Six interconnected realms, each with unique capabilities and APIs to power your applications
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</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) => (
|
{ecosystemPillars.map((pillar, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={pillar.title}
|
key={pillar.title}
|
||||||
initial={{ opacity: 0, y: 50 }}
|
initial={{ opacity: 0, y: 24 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
transition={{ duration: 0.5, delay: index * 0.07 }}
|
||||||
onMouseEnter={() => setHoveredCard(index)}
|
|
||||||
onMouseLeave={() => setHoveredCard(null)}
|
|
||||||
>
|
>
|
||||||
<Link to={pillar.href}>
|
<Link to={pillar.href}>
|
||||||
<Card className="group relative overflow-hidden h-full border-2 hover:border-transparent transition-all duration-300">
|
<Card className="group h-full border-border hover:border-primary/30 transition-colors duration-200 bg-card">
|
||||||
<div
|
<div className="p-6 space-y-4">
|
||||||
className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-primary/10"
|
<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" />
|
||||||
|
|
||||||
{hoveredCard === index && (
|
|
||||||
<motion.div
|
|
||||||
className="absolute inset-0 blur-xl opacity-30 bg-primary"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 0.3 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="relative p-8 space-y-4 backdrop-blur-sm">
|
|
||||||
<div
|
|
||||||
className={`w-16 h-16 rounded-2xl bg-gradient-to-br ${pillar.gradient} flex items-center justify-center shadow-2xl group-hover:scale-110 transition-transform duration-300`}
|
|
||||||
style={{
|
|
||||||
boxShadow: `0 20px 40px hsl(var(--primary) / 0.4)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<pillar.icon className="w-8 h-8 text-white" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
<div className="space-y-2">
|
<h3 className="font-semibold text-foreground group-hover:text-primary transition-colors">
|
||||||
<h3 className="text-2xl font-bold group-hover:text-primary transition-all duration-300">
|
|
||||||
{pillar.title}
|
{pillar.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-muted-foreground leading-relaxed">
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
{pillar.description}
|
{pillar.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center text-primary/70 group-hover:text-primary group-hover:translate-x-1 transition-all duration-200 text-sm">
|
||||||
<div className="flex items-center text-primary group-hover:translate-x-2 transition-transform duration-300">
|
<span className="font-medium mr-1">Explore</span>
|
||||||
<span className="text-sm font-medium mr-2">Explore</span>
|
<ArrowRight className="w-3.5 h-3.5" />
|
||||||
<ArrowRight className="w-4 h-4" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -449,38 +274,37 @@ export default function Index() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="space-y-12 px-4">
|
{/* Why AeThex */}
|
||||||
|
<section className="space-y-10 px-4">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.8 }}
|
transition={{ duration: 0.6 }}
|
||||||
className="text-center space-y-4"
|
className="text-center space-y-3"
|
||||||
>
|
>
|
||||||
<h2 className="text-5xl md:text-6xl font-black text-primary">
|
<h2 className="text-4xl md:text-5xl font-black">Why Build on AeThex?</h2>
|
||||||
Why Build on AeThex?
|
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||||
</h2>
|
|
||||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
|
||||||
Join a growing ecosystem designed for creators, developers, and entrepreneurs
|
Join a growing ecosystem designed for creators, developers, and entrepreneurs
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</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) => (
|
{features.map((feature, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={feature.title}
|
key={feature.title}
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
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">
|
<Card className="p-6 space-y-4 border-border bg-card h-full">
|
||||||
<div className="w-16 h-16 rounded-2xl bg-primary flex items-center justify-center shadow-2xl shadow-primary/50">
|
<div className="w-10 h-10 rounded-lg bg-primary/10 border border-primary/20 flex items-center justify-center">
|
||||||
<feature.icon className="w-8 h-8 text-primary-foreground" />
|
<feature.icon className="w-5 h-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<h3 className="text-2xl font-bold">{feature.title}</h3>
|
<h3 className="font-semibold text-foreground">{feature.title}</h3>
|
||||||
<p className="text-muted-foreground leading-relaxed">
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
{feature.description}
|
{feature.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -490,108 +314,47 @@ export default function Index() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
<section className="px-4">
|
<section className="px-4">
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 30 }}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
{/* 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
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.6, delay: 0.2 }}
|
transition={{ duration: 0.6 }}
|
||||||
|
className="relative overflow-hidden rounded-2xl max-w-5xl mx-auto border border-primary/20 bg-primary/5"
|
||||||
>
|
>
|
||||||
<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">
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_60%_80%_at_80%_50%,hsl(var(--primary)/0.08),transparent)]" />
|
||||||
<Terminal className="w-4 h-4 mr-2 inline" />
|
<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
|
Start Building Today
|
||||||
</Badge>
|
</Badge>
|
||||||
</motion.div>
|
<h2 className="text-3xl md:text-4xl lg:text-5xl font-black leading-tight">
|
||||||
|
Ready to Build Something{" "}
|
||||||
<motion.h2
|
<span className="text-primary">Epic?</span>
|
||||||
initial={{ opacity: 0, y: 20 }}
|
</h2>
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||||
viewport={{ once: true }}
|
Get your API key and start deploying across{" "}
|
||||||
transition={{ duration: 0.6, delay: 0.3 }}
|
<span className="text-foreground font-medium">5+ metaverse platforms</span> in minutes
|
||||||
className="text-4xl md:text-5xl lg:text-6xl font-black leading-tight"
|
</p>
|
||||||
>
|
<div className="flex flex-wrap gap-3 justify-center pt-2">
|
||||||
Ready to Build Something
|
|
||||||
<br />
|
|
||||||
<span className="text-primary drop-shadow-[0_0_30px_rgba(168,85,247,0.6)]">Epic?</span>
|
|
||||||
</motion.h2>
|
|
||||||
|
|
||||||
<motion.p
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.6, delay: 0.4 }}
|
|
||||||
className="text-xl md:text-2xl text-muted-foreground max-w-3xl mx-auto font-light"
|
|
||||||
>
|
|
||||||
Get your API key and start deploying across <span className="text-primary font-semibold">5+ metaverse platforms</span> in minutes
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.6, delay: 0.5 }}
|
|
||||||
className="flex flex-wrap gap-4 justify-center pt-6"
|
|
||||||
>
|
|
||||||
<Link to="/dev-platform/dashboard">
|
<Link to="/dev-platform/dashboard">
|
||||||
<Button
|
<Button size="lg" className="px-8 h-12 font-semibold">
|
||||||
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"
|
|
||||||
>
|
|
||||||
Get Your API Key
|
Get Your API Key
|
||||||
<ArrowRight className="w-6 h-6 ml-3" />
|
<ArrowRight className="w-4 h-4 ml-2" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/realms">
|
<Link to="/realms">
|
||||||
<Button
|
<Button size="lg" variant="outline" className="px-8 h-12 font-semibold">
|
||||||
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"
|
|
||||||
>
|
|
||||||
Explore Realms
|
Explore Realms
|
||||||
<Boxes className="w-6 h-6 ml-3" />
|
<Boxes className="w-4 h-4 ml-2" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -338,6 +338,39 @@ export default function Login() {
|
||||||
) : null}
|
) : null}
|
||||||
{/* Social Login Buttons */}
|
{/* Social Login Buttons */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
{/* AeThex ID — primary SSO */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||||
|
AeThex Identity
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ax-mono ax-clip w-full"
|
||||||
|
style={{
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center", gap: 10,
|
||||||
|
border: "1px solid rgba(0,255,255,0.5)", color: "#00ffff",
|
||||||
|
padding: "11px 20px", background: "rgba(0,255,255,0.06)",
|
||||||
|
fontSize: 11, letterSpacing: 2, textTransform: "uppercase",
|
||||||
|
cursor: "pointer", transition: "all 0.2s", width: "100%",
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => { e.currentTarget.style.background = "rgba(0,255,255,0.14)"; e.currentTarget.style.boxShadow = "0 0 20px rgba(0,255,255,0.2)"; }}
|
||||||
|
onMouseLeave={e => { e.currentTarget.style.background = "rgba(0,255,255,0.06)"; e.currentTarget.style.boxShadow = "none"; }}
|
||||||
|
onClick={() => {
|
||||||
|
// Server-side OIDC flow — bypass Supabase social auth
|
||||||
|
const redirectTo = encodeURIComponent(location.state?.from?.pathname || "/dashboard");
|
||||||
|
window.location.href = `${API_BASE}/api/auth/authentik/start?redirectTo=${redirectTo}`;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Hex icon */}
|
||||||
|
<svg viewBox="0 0 100 100" width={16} height={16} style={{ flexShrink: 0 }}>
|
||||||
|
<polygon points="50,5 95,27.5 95,72.5 50,95 5,72.5 5,27.5" fill="none" stroke="#00ffff" strokeWidth="3" opacity="0.8"/>
|
||||||
|
<text x="50" y="63" textAnchor="middle" fontFamily="Orbitron" fontSize="38" fontWeight="700" fill="#00ffff">Æ</text>
|
||||||
|
</svg>
|
||||||
|
Sign in with AeThex ID
|
||||||
|
<span style={{ fontSize: 9, color: "rgba(0,255,255,0.4)", letterSpacing: 1 }}>auth.aethex.tech</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||||
Quick Sign In
|
Quick Sign In
|
||||||
|
|
|
||||||
|
|
@ -590,7 +590,7 @@ const ProfilePassport = () => {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 px-2 text-xs text-aethex-200"
|
className="h-8 px-2 text-xs text-aethex-200"
|
||||||
>
|
>
|
||||||
<Link to="/projects/new">
|
<Link to={`/projects/${project.id}`}>
|
||||||
View mission
|
View mission
|
||||||
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
280
client/pages/ProjectDetail.tsx
Normal file
280
client/pages/ProjectDetail.tsx
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useParams, Link } from "react-router-dom";
|
||||||
|
import Layout from "@/components/Layout";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import {
|
||||||
|
Github,
|
||||||
|
ExternalLink,
|
||||||
|
LayoutDashboard,
|
||||||
|
Calendar,
|
||||||
|
Cpu,
|
||||||
|
Activity,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
||||||
|
|
||||||
|
interface Project {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string | null;
|
||||||
|
status?: string | null;
|
||||||
|
technologies?: string[] | null;
|
||||||
|
github_url?: string | null;
|
||||||
|
live_url?: string | null;
|
||||||
|
image_url?: string | null;
|
||||||
|
engine?: string | null;
|
||||||
|
priority?: string | null;
|
||||||
|
progress?: number | null;
|
||||||
|
created_at?: string | null;
|
||||||
|
updated_at?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Owner {
|
||||||
|
id: string;
|
||||||
|
username?: string | null;
|
||||||
|
full_name?: string | null;
|
||||||
|
avatar_url?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_COLORS: Record<string, string> = {
|
||||||
|
planning: "bg-slate-500/20 text-slate-300 border-slate-600",
|
||||||
|
in_progress: "bg-blue-500/20 text-blue-300 border-blue-600",
|
||||||
|
completed: "bg-green-500/20 text-green-300 border-green-600",
|
||||||
|
on_hold: "bg-yellow-500/20 text-yellow-300 border-yellow-600",
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
|
planning: "Planning",
|
||||||
|
in_progress: "In Progress",
|
||||||
|
completed: "Completed",
|
||||||
|
on_hold: "On Hold",
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (v?: string | null) => {
|
||||||
|
if (!v) return null;
|
||||||
|
const d = new Date(v);
|
||||||
|
if (isNaN(d.getTime())) return null;
|
||||||
|
return d.toLocaleDateString(undefined, { dateStyle: "medium" });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ProjectDetail() {
|
||||||
|
const { projectId } = useParams<{ projectId: string }>();
|
||||||
|
const [project, setProject] = useState<Project | null>(null);
|
||||||
|
const [owner, setOwner] = useState<Owner | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [notFound, setNotFound] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!projectId) return;
|
||||||
|
setLoading(true);
|
||||||
|
fetch(`${API_BASE}/api/projects/${projectId}`)
|
||||||
|
.then((r) => {
|
||||||
|
if (r.status === 404) { setNotFound(true); return null; }
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then((body) => {
|
||||||
|
if (!body) return;
|
||||||
|
setProject(body.project);
|
||||||
|
setOwner(body.owner);
|
||||||
|
})
|
||||||
|
.catch(() => setNotFound(true))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
|
<div className="animate-pulse text-slate-400">Loading project…</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notFound || !project) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-4">
|
||||||
|
<p className="text-xl text-slate-300">Project not found.</p>
|
||||||
|
<Button asChild variant="outline">
|
||||||
|
<Link to="/projects">Browse projects</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusKey = project.status ?? "planning";
|
||||||
|
const statusClass = STATUS_COLORS[statusKey] ?? STATUS_COLORS.planning;
|
||||||
|
const statusLabel = STATUS_LABELS[statusKey] ?? statusKey;
|
||||||
|
|
||||||
|
const ownerSlug = owner?.username ?? owner?.id;
|
||||||
|
const ownerName = owner?.full_name || owner?.username || "Unknown";
|
||||||
|
const ownerInitials = ownerName
|
||||||
|
.split(" ")
|
||||||
|
.map((w) => w[0])
|
||||||
|
.join("")
|
||||||
|
.slice(0, 2)
|
||||||
|
.toUpperCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="max-w-4xl mx-auto px-4 py-10 space-y-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<Badge className={`text-xs border ${statusClass}`}>
|
||||||
|
{statusLabel}
|
||||||
|
</Badge>
|
||||||
|
{project.priority && (
|
||||||
|
<Badge variant="outline" className="text-xs border-slate-600 text-slate-400">
|
||||||
|
{project.priority} priority
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl font-bold text-white">{project.title}</h1>
|
||||||
|
{project.description && (
|
||||||
|
<p className="text-slate-300 leading-relaxed text-base max-w-2xl">
|
||||||
|
{project.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<Button asChild>
|
||||||
|
<Link to={`/projects/${project.id}/board`}>
|
||||||
|
<LayoutDashboard className="mr-2 h-4 w-4" />
|
||||||
|
Project Board
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
{project.github_url && (
|
||||||
|
<Button asChild variant="outline">
|
||||||
|
<a href={project.github_url} target="_blank" rel="noopener noreferrer">
|
||||||
|
<Github className="mr-2 h-4 w-4" />
|
||||||
|
Repository
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{project.live_url && (
|
||||||
|
<Button asChild variant="outline">
|
||||||
|
<a href={project.live_url} target="_blank" rel="noopener noreferrer">
|
||||||
|
<ExternalLink className="mr-2 h-4 w-4" />
|
||||||
|
Live
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator className="border-slate-700" />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{/* Meta card */}
|
||||||
|
<Card className="bg-slate-900/60 border-slate-700 md:col-span-1 space-y-0">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm text-slate-400 uppercase tracking-wide">
|
||||||
|
Details
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4 text-sm">
|
||||||
|
{owner && (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Avatar className="h-8 w-8">
|
||||||
|
<AvatarImage src={owner.avatar_url ?? undefined} />
|
||||||
|
<AvatarFallback className="bg-slate-700 text-slate-300 text-xs">
|
||||||
|
{ownerInitials}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-400 text-xs">Owner</p>
|
||||||
|
{ownerSlug ? (
|
||||||
|
<Link
|
||||||
|
to={`/u/${ownerSlug}`}
|
||||||
|
className="text-aethex-300 hover:underline font-medium"
|
||||||
|
>
|
||||||
|
{ownerName}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className="text-slate-200">{ownerName}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{project.engine && (
|
||||||
|
<div className="flex items-start gap-2 text-slate-300">
|
||||||
|
<Cpu className="h-4 w-4 mt-0.5 text-slate-500 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-400 text-xs">Engine</p>
|
||||||
|
<p>{project.engine}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{typeof project.progress === "number" && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2 text-slate-400 text-xs">
|
||||||
|
<Activity className="h-3.5 w-3.5" />
|
||||||
|
<span>Progress — {project.progress}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-slate-700 rounded-full h-1.5">
|
||||||
|
<div
|
||||||
|
className="bg-aethex-500 h-1.5 rounded-full transition-all"
|
||||||
|
style={{ width: `${Math.min(100, project.progress)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(project.created_at || project.updated_at) && (
|
||||||
|
<div className="flex items-start gap-2 text-slate-300">
|
||||||
|
<Calendar className="h-4 w-4 mt-0.5 text-slate-500 shrink-0" />
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{project.created_at && (
|
||||||
|
<p className="text-xs text-slate-400">
|
||||||
|
Created {formatDate(project.created_at)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{project.updated_at && (
|
||||||
|
<p className="text-xs text-slate-400">
|
||||||
|
Updated {formatDate(project.updated_at)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Technologies */}
|
||||||
|
{project.technologies && project.technologies.length > 0 && (
|
||||||
|
<Card className="bg-slate-900/60 border-slate-700 md:col-span-2">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm text-slate-400 uppercase tracking-wide">
|
||||||
|
Technologies
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{project.technologies.map((tech) => (
|
||||||
|
<Badge
|
||||||
|
key={tech}
|
||||||
|
variant="outline"
|
||||||
|
className="border-slate-600 text-slate-300 text-xs"
|
||||||
|
>
|
||||||
|
{tech}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,7 @@ import {
|
||||||
ArrowDownRight,
|
ArrowDownRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
|
||||||
interface Analytics {
|
interface Analytics {
|
||||||
users: {
|
users: {
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
|
||||||
interface Report {
|
interface Report {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Link } from "wouter";
|
import { Link } from "react-router-dom";
|
||||||
import Layout from "@/components/Layout";
|
import Layout from "@/components/Layout";
|
||||||
import SEO from "@/components/SEO";
|
import SEO from "@/components/SEO";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -33,7 +33,7 @@ import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
|
||||||
interface Interview {
|
interface Interview {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Link } from "wouter";
|
import { Link } from "react-router-dom";
|
||||||
import Layout from "@/components/Layout";
|
import Layout from "@/components/Layout";
|
||||||
import SEO from "@/components/SEO";
|
import SEO from "@/components/SEO";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -41,7 +41,7 @@ import {
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
|
||||||
interface Offer {
|
interface Offer {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Link } from "wouter";
|
import { Link } from "react-router-dom";
|
||||||
import Layout from "@/components/Layout";
|
import Layout from "@/components/Layout";
|
||||||
import SEO from "@/components/SEO";
|
import SEO from "@/components/SEO";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -30,7 +30,7 @@ import {
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
|
||||||
interface ProfileData {
|
interface ProfileData {
|
||||||
profile: {
|
profile: {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Link } from "wouter";
|
import { Link } from "react-router-dom";
|
||||||
import Layout from "@/components/Layout";
|
import Layout from "@/components/Layout";
|
||||||
import SEO from "@/components/SEO";
|
import SEO from "@/components/SEO";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -38,7 +38,7 @@ import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
|
||||||
interface WorkHistory {
|
interface WorkHistory {
|
||||||
company: string;
|
company: string;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import Layout from "@/components/Layout";
|
import GameForgeLayout from "@/components/gameforge/GameForgeLayout";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { useArmTheme } from "@/contexts/ArmThemeContext";
|
import { useArmTheme } from "@/contexts/ArmThemeContext";
|
||||||
|
|
@ -154,7 +154,7 @@ export default function GameForgeDashboard() {
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
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="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">
|
<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">
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</GameForgeLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<GameForgeLayout>
|
||||||
<div
|
<div
|
||||||
className={`min-h-screen bg-gradient-to-b from-black to-black py-8 ${theme.fontClass}`}
|
className={`min-h-screen bg-gradient-to-b from-black to-black py-8 ${theme.fontClass}`}
|
||||||
style={{ backgroundImage: theme.wallpaperPattern }}
|
style={{ backgroundImage: theme.wallpaperPattern }}
|
||||||
|
|
@ -505,6 +505,6 @@ export default function GameForgeDashboard() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</GameForgeLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import Layout from "@/components/Layout";
|
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
|
||||||
import SEO from "@/components/SEO";
|
import SEO from "@/components/SEO";
|
||||||
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
|
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
|
||||||
import { ThreeColumnLayout } from "@/components/dev-platform/layouts/ThreeColumnLayout";
|
import { ThreeColumnLayout } from "@/components/dev-platform/layouts/ThreeColumnLayout";
|
||||||
|
|
@ -67,7 +67,7 @@ export default function ApiReference() {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<DevPlatformLayout>
|
||||||
<SEO pageTitle="API Reference" description="Complete documentation for the AeThex Developer API" />
|
<SEO pageTitle="API Reference" description="Complete documentation for the AeThex Developer API" />
|
||||||
<Breadcrumbs className="mb-6" />
|
<Breadcrumbs className="mb-6" />
|
||||||
<ThreeColumnLayout
|
<ThreeColumnLayout
|
||||||
|
|
@ -636,6 +636,6 @@ def make_request(url):
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</ThreeColumnLayout>
|
</ThreeColumnLayout>
|
||||||
</Layout>
|
</DevPlatformLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Layout from "@/components/Layout";
|
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
|
||||||
import SEO from "@/components/SEO";
|
import SEO from "@/components/SEO";
|
||||||
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
|
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
|
||||||
import { ExampleCard } from "@/components/dev-platform/ExampleCard";
|
import { ExampleCard } from "@/components/dev-platform/ExampleCard";
|
||||||
|
|
@ -165,7 +165,7 @@ export default function CodeExamples() {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<DevPlatformLayout>
|
||||||
<SEO pageTitle="Code Examples Repository" description="Production-ready code examples for common use cases and integrations" />
|
<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">
|
<div className="max-w-6xl mx-auto space-y-8">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
|
|
@ -274,6 +274,6 @@ export default function CodeExamples() {
|
||||||
<Button variant="outline">Submit Example</Button>
|
<Button variant="outline">Submit Example</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</DevPlatformLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
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 SEO from "@/components/SEO";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { CodeBlock } from "@/components/dev-platform/ui/CodeBlock";
|
import { CodeBlock } from "@/components/dev-platform/ui/CodeBlock";
|
||||||
|
|
@ -57,7 +57,7 @@ await game.deploy(['roblox', 'fortnite', 'web']);`;
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
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." />
|
<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 */}
|
{/* Hero Section */}
|
||||||
<section className="container py-20 md:py-32">
|
<section className="container py-20 md:py-32">
|
||||||
|
|
@ -314,6 +314,6 @@ await game.deploy(['roblox', 'fortnite', 'web']);`;
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</DevPlatformLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Layout from "@/components/Layout";
|
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
|
||||||
import SEO from "@/components/SEO";
|
import SEO from "@/components/SEO";
|
||||||
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
|
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
|
||||||
import { StatCard } from "@/components/dev-platform/ui/StatCard";
|
import { StatCard } from "@/components/dev-platform/ui/StatCard";
|
||||||
|
|
@ -15,10 +15,10 @@ import {
|
||||||
Activity,
|
Activity,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
Clock,
|
Clock,
|
||||||
AlertTriangle,
|
|
||||||
Loader2,
|
Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { authFetch } from "@/lib/auth-fetch";
|
||||||
|
|
||||||
interface ApiKey {
|
interface ApiKey {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -58,14 +58,14 @@ export default function DeveloperDashboard() {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
// Load API keys
|
// Load API keys
|
||||||
const keysRes = await fetch("/api/developer/keys");
|
const keysRes = await authFetch("/api/developer/keys");
|
||||||
if (keysRes.ok) {
|
if (keysRes.ok) {
|
||||||
const keysData = await keysRes.json();
|
const keysData = await keysRes.json();
|
||||||
setKeys(keysData.keys || []);
|
setKeys(keysData.keys || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load developer profile
|
// Load developer profile
|
||||||
const profileRes = await fetch("/api/developer/profile");
|
const profileRes = await authFetch("/api/developer/profile");
|
||||||
if (profileRes.ok) {
|
if (profileRes.ok) {
|
||||||
const profileData = await profileRes.json();
|
const profileData = await profileRes.json();
|
||||||
setProfile(profileData.profile);
|
setProfile(profileData.profile);
|
||||||
|
|
@ -102,7 +102,7 @@ export default function DeveloperDashboard() {
|
||||||
expiresInDays?: number;
|
expiresInDays?: number;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/developer/keys", {
|
const res = await authFetch("/api/developer/keys", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
|
|
@ -133,7 +133,7 @@ export default function DeveloperDashboard() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/developer/keys/${id}`, {
|
const res = await authFetch(`/api/developer/keys/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -159,7 +159,7 @@ export default function DeveloperDashboard() {
|
||||||
|
|
||||||
const handleToggleActive = async (id: string, isActive: boolean) => {
|
const handleToggleActive = async (id: string, isActive: boolean) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/developer/keys/${id}`, {
|
const res = await authFetch(`/api/developer/keys/${id}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ is_active: isActive }),
|
body: JSON.stringify({ is_active: isActive }),
|
||||||
|
|
@ -196,12 +196,12 @@ export default function DeveloperDashboard() {
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<DevPlatformLayout>
|
||||||
<SEO pageTitle="Developer Dashboard" description="Manage your API keys and monitor usage" />
|
<SEO pageTitle="Developer Dashboard" description="Manage your API keys and monitor usage" />
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</DevPlatformLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,13 +214,13 @@ export default function DeveloperDashboard() {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<DevPlatformLayout>
|
||||||
<SEO pageTitle="Developer Dashboard" description="Manage your API keys and monitor usage" />
|
<SEO pageTitle="Developer Dashboard" description="Manage your API keys and monitor usage" />
|
||||||
<Breadcrumbs className="mb-6" />
|
<Breadcrumbs className="mb-6" />
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Warning for expiring keys */}
|
{/* Warning for expiring keys */}
|
||||||
{expiringSoon.length > 0 && (
|
{expiringSoon.length > 0 && (
|
||||||
<Callout variant="warning">
|
<Callout type="warning">
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{expiringSoon.length} API key{expiringSoon.length > 1 ? "s" : ""} expiring
|
{expiringSoon.length} API key{expiringSoon.length > 1 ? "s" : ""} expiring
|
||||||
soon
|
soon
|
||||||
|
|
@ -240,7 +240,7 @@ export default function DeveloperDashboard() {
|
||||||
icon={Activity}
|
icon={Activity}
|
||||||
trend={
|
trend={
|
||||||
stats?.totalRequests > 0
|
stats?.totalRequests > 0
|
||||||
? { value: 12.5, label: "vs last week" }
|
? { value: 12.5, isPositive: true }
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -252,14 +252,13 @@ export default function DeveloperDashboard() {
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Recently Used"
|
title="Recently Used"
|
||||||
value={stats?.recentlyUsed || 0}
|
value={stats?.recentlyUsed || 0}
|
||||||
subtitle="Last 24 hours"
|
description="Last 24 hours"
|
||||||
icon={Clock}
|
icon={Clock}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Plan"
|
title="Plan"
|
||||||
value={profile?.plan_tier || "free"}
|
value={profile?.plan_tier || "free"}
|
||||||
icon={TrendingUp}
|
icon={TrendingUp}
|
||||||
valueClassName="capitalize"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -295,7 +294,7 @@ export default function DeveloperDashboard() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{keys.length === 0 ? (
|
{keys.length === 0 ? (
|
||||||
<Callout variant="info">
|
<Callout type="info">
|
||||||
<p className="font-medium">No API keys yet</p>
|
<p className="font-medium">No API keys yet</p>
|
||||||
<p className="text-sm mt-1">
|
<p className="text-sm mt-1">
|
||||||
Create your first API key to start building with AeThex. You can
|
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) && (
|
{keys.length >= (profile?.max_api_keys || 3) && (
|
||||||
<Callout variant="warning">
|
<Callout type="warning">
|
||||||
<p className="font-medium">API Key Limit Reached</p>
|
<p className="font-medium">API Key Limit Reached</p>
|
||||||
<p className="text-sm mt-1">
|
<p className="text-sm mt-1">
|
||||||
You've reached the maximum number of API keys for your plan. Delete
|
You've reached the maximum number of API keys for your plan. Delete
|
||||||
|
|
@ -362,7 +361,7 @@ export default function DeveloperDashboard() {
|
||||||
chartType="bar"
|
chartType="bar"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Callout variant="info">
|
<Callout type="info">
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
<strong>Note:</strong> Real-time analytics are coming soon. This
|
<strong>Note:</strong> Real-time analytics are coming soon. This
|
||||||
preview shows sample data.
|
preview shows sample data.
|
||||||
|
|
@ -370,7 +369,7 @@ export default function DeveloperDashboard() {
|
||||||
</Callout>
|
</Callout>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Callout variant="info">
|
<Callout type="info">
|
||||||
<p className="font-medium">No usage data yet</p>
|
<p className="font-medium">No usage data yet</p>
|
||||||
<p className="text-sm mt-1">
|
<p className="text-sm mt-1">
|
||||||
Start making API requests to see your usage analytics here.
|
Start making API requests to see your usage analytics here.
|
||||||
|
|
@ -387,6 +386,6 @@ export default function DeveloperDashboard() {
|
||||||
onOpenChange={setCreateDialogOpen}
|
onOpenChange={setCreateDialogOpen}
|
||||||
onCreateKey={handleCreateKey}
|
onCreateKey={handleCreateKey}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</DevPlatformLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import Layout from "@/components/Layout";
|
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
|
||||||
import SEO from "@/components/SEO";
|
import SEO from "@/components/SEO";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
|
|
@ -95,7 +95,7 @@ const steps = [
|
||||||
|
|
||||||
export default function DeveloperPlatform() {
|
export default function DeveloperPlatform() {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<DevPlatformLayout>
|
||||||
<SEO
|
<SEO
|
||||||
pageTitle="AeThex Developer Platform"
|
pageTitle="AeThex Developer Platform"
|
||||||
description="Everything you need to build powerful applications with AeThex"
|
description="Everything you need to build powerful applications with AeThex"
|
||||||
|
|
@ -313,6 +313,6 @@ export default function DeveloperPlatform() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</DevPlatformLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useParams, Link } from "react-router-dom";
|
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 SEO from "@/components/SEO";
|
||||||
import { CodeBlock } from "@/components/dev-platform/ui/CodeBlock";
|
import { CodeBlock } from "@/components/dev-platform/ui/CodeBlock";
|
||||||
import { CodeTabs } from "@/components/dev-platform/CodeTabs";
|
import { CodeTabs } from "@/components/dev-platform/CodeTabs";
|
||||||
|
|
@ -377,7 +377,7 @@ export default function ExampleDetail() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<DevPlatformLayout>
|
||||||
<SEO pageTitle={example.title} description={example.description} />
|
<SEO pageTitle={example.title} description={example.description} />
|
||||||
<div className="max-w-5xl mx-auto space-y-8">
|
<div className="max-w-5xl mx-auto space-y-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
@ -531,6 +531,6 @@ SUPABASE_SERVICE_KEY=your_service_key`}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</DevPlatformLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Layout from "@/components/Layout";
|
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
|
||||||
import SEO from "@/components/SEO";
|
import SEO from "@/components/SEO";
|
||||||
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
|
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
|
||||||
import { MarketplaceCard } from "@/components/dev-platform/MarketplaceCard";
|
import { MarketplaceCard } from "@/components/dev-platform/MarketplaceCard";
|
||||||
|
|
@ -175,7 +175,7 @@ export default function Marketplace() {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<DevPlatformLayout>
|
||||||
<SEO pageTitle="Developer Marketplace" description="Premium integrations, plugins, and tools to supercharge your projects" />
|
<SEO pageTitle="Developer Marketplace" description="Premium integrations, plugins, and tools to supercharge your projects" />
|
||||||
<div className="max-w-6xl mx-auto space-y-8">
|
<div className="max-w-6xl mx-auto space-y-8">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
|
|
@ -285,6 +285,6 @@ export default function Marketplace() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</DevPlatformLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams, Link } from "react-router-dom";
|
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 SEO from "@/components/SEO";
|
||||||
import { CodeBlock } from "@/components/dev-platform/ui/CodeBlock";
|
import { CodeBlock } from "@/components/dev-platform/ui/CodeBlock";
|
||||||
import { Callout } from "@/components/dev-platform/ui/Callout";
|
import { Callout } from "@/components/dev-platform/ui/Callout";
|
||||||
|
|
@ -111,7 +111,7 @@ export default function MarketplaceItemDetail() {
|
||||||
const item = itemData[id || ""] || itemData["premium-analytics-dashboard"];
|
const item = itemData[id || ""] || itemData["premium-analytics-dashboard"];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<DevPlatformLayout>
|
||||||
<SEO pageTitle={item.name} description={item.description} />
|
<SEO pageTitle={item.name} description={item.description} />
|
||||||
<div className="max-w-6xl mx-auto space-y-8">
|
<div className="max-w-6xl mx-auto space-y-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
@ -441,6 +441,6 @@ npm install @aethex/${item.name.toLowerCase().replace(/\s+/g, "-")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</DevPlatformLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import Layout from "@/components/Layout";
|
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
|
||||||
import SEO from "@/components/SEO";
|
import SEO from "@/components/SEO";
|
||||||
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
|
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
|
||||||
import { ThreeColumnLayout } from "@/components/dev-platform/layouts/ThreeColumnLayout";
|
import { ThreeColumnLayout } from "@/components/dev-platform/layouts/ThreeColumnLayout";
|
||||||
|
|
@ -84,7 +84,7 @@ export default function QuickStart() {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<DevPlatformLayout>
|
||||||
<SEO pageTitle="Quick Start Guide" description="Get up and running with the AeThex API in minutes" />
|
<SEO pageTitle="Quick Start Guide" description="Get up and running with the AeThex API in minutes" />
|
||||||
<Breadcrumbs className="mb-6" />
|
<Breadcrumbs className="mb-6" />
|
||||||
<ThreeColumnLayout sidebar={sidebarContent} aside={asideContent}>
|
<ThreeColumnLayout sidebar={sidebarContent} aside={asideContent}>
|
||||||
|
|
@ -509,6 +509,6 @@ jobs.data.forEach(job => {
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</ThreeColumnLayout>
|
</ThreeColumnLayout>
|
||||||
</Layout>
|
</DevPlatformLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams, Link } from "react-router-dom";
|
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 SEO from "@/components/SEO";
|
||||||
import { CodeBlock } from "@/components/dev-platform/ui/CodeBlock";
|
import { CodeBlock } from "@/components/dev-platform/ui/CodeBlock";
|
||||||
import { CodeTabs } from "@/components/dev-platform/CodeTabs";
|
import { CodeTabs } from "@/components/dev-platform/CodeTabs";
|
||||||
|
|
@ -110,7 +110,7 @@ export default function TemplateDetail() {
|
||||||
const runCommand = "npm run dev";
|
const runCommand = "npm run dev";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<DevPlatformLayout>
|
||||||
<SEO pageTitle={template.name} description={template.description} />
|
<SEO pageTitle={template.name} description={template.description} />
|
||||||
<div className="max-w-6xl mx-auto space-y-8">
|
<div className="max-w-6xl mx-auto space-y-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
@ -423,6 +423,6 @@ async function getUserProfile() {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</DevPlatformLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Layout from "@/components/Layout";
|
import { DevPlatformLayout } from "@/components/dev-platform/layouts/DevPlatformLayout";
|
||||||
import SEO from "@/components/SEO";
|
import SEO from "@/components/SEO";
|
||||||
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
|
import { Breadcrumbs } from "@/components/dev-platform/Breadcrumbs";
|
||||||
import { TemplateCard } from "@/components/dev-platform/TemplateCard";
|
import { TemplateCard } from "@/components/dev-platform/TemplateCard";
|
||||||
|
|
@ -165,7 +165,7 @@ export default function Templates() {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<DevPlatformLayout>
|
||||||
<SEO pageTitle="Templates Gallery" description="Pre-built templates and starter kits to accelerate your development" />
|
<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">
|
<div className="max-w-6xl mx-auto space-y-8">
|
||||||
{/* Search & Filters */}
|
{/* Search & Filters */}
|
||||||
|
|
@ -253,6 +253,6 @@ export default function Templates() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</DevPlatformLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useParams, Link, useNavigate } from "react-router-dom";
|
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 SEO from "@/components/SEO";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
@ -90,17 +90,17 @@ export default function ArtistProfile() {
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<EthosLayout>
|
||||||
<div className="py-20 text-center">Loading artist profile...</div>
|
<div className="py-20 text-center">Loading artist profile...</div>
|
||||||
</Layout>
|
</EthosLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!artist) {
|
if (!artist) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<EthosLayout>
|
||||||
<div className="py-20 text-center">Artist not found</div>
|
<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`}
|
pageTitle={`${artist.user_profiles.full_name} - Ethos Guild Artist`}
|
||||||
description={artist.bio || "Ethos Guild artist profile"}
|
description={artist.bio || "Ethos Guild artist profile"}
|
||||||
/>
|
/>
|
||||||
<Layout>
|
<EthosLayout>
|
||||||
<div className="bg-slate-950 text-foreground min-h-screen">
|
<div className="bg-slate-950 text-foreground min-h-screen">
|
||||||
{/* Profile Header */}
|
{/* Profile Header */}
|
||||||
<section className="border-b border-slate-800 bg-gradient-to-b from-slate-900 to-slate-950 py-12">
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</EthosLayout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import Layout from "@/components/Layout";
|
import EthosLayout from "@/components/ethos/EthosLayout";
|
||||||
import SEO from "@/components/SEO";
|
import SEO from "@/components/SEO";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
@ -356,16 +356,16 @@ export default function ArtistSettings() {
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<EthosLayout>
|
||||||
<div className="py-20 text-center">Loading settings...</div>
|
<div className="py-20 text-center">Loading settings...</div>
|
||||||
</Layout>
|
</EthosLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SEO pageTitle="Artist Settings - Ethos Guild" />
|
<SEO pageTitle="Artist Settings - Ethos Guild" />
|
||||||
<Layout>
|
<EthosLayout>
|
||||||
<div className="bg-slate-950 text-foreground min-h-screen">
|
<div className="bg-slate-950 text-foreground min-h-screen">
|
||||||
<div className="container mx-auto px-4 max-w-4xl py-12">
|
<div className="container mx-auto px-4 max-w-4xl py-12">
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
|
|
@ -778,7 +778,7 @@ export default function ArtistSettings() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</EthosLayout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import Layout from "@/components/Layout";
|
import EthosLayout from "@/components/ethos/EthosLayout";
|
||||||
import SEO from "@/components/SEO";
|
import SEO from "@/components/SEO";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
@ -131,9 +131,9 @@ export default function LicensingDashboard() {
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<EthosLayout>
|
||||||
<div className="py-20 text-center">Loading agreements...</div>
|
<div className="py-20 text-center">Loading agreements...</div>
|
||||||
</Layout>
|
</EthosLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,7 +143,7 @@ export default function LicensingDashboard() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SEO pageTitle="Licensing Dashboard - Ethos Guild" />
|
<SEO pageTitle="Licensing Dashboard - Ethos Guild" />
|
||||||
<Layout>
|
<EthosLayout>
|
||||||
<div className="bg-slate-950 text-foreground min-h-screen">
|
<div className="bg-slate-950 text-foreground min-h-screen">
|
||||||
<div className="container mx-auto px-4 max-w-4xl py-12">
|
<div className="container mx-auto px-4 max-w-4xl py-12">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
@ -280,7 +280,7 @@ export default function LicensingDashboard() {
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</EthosLayout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import Layout from "@/components/Layout";
|
import EthosLayout from "@/components/ethos/EthosLayout";
|
||||||
import SEO from "@/components/SEO";
|
import SEO from "@/components/SEO";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
@ -113,7 +113,7 @@ export default function TrackLibrary() {
|
||||||
pageTitle="Ethos Track Library"
|
pageTitle="Ethos Track Library"
|
||||||
description="Browse music and sound effects from Ethos Guild artists"
|
description="Browse music and sound effects from Ethos Guild artists"
|
||||||
/>
|
/>
|
||||||
<Layout>
|
<EthosLayout>
|
||||||
<div className="bg-slate-950 text-foreground min-h-screen">
|
<div className="bg-slate-950 text-foreground min-h-screen">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="relative border-b border-slate-800 bg-gradient-to-b from-slate-900 to-slate-950 py-16">
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</EthosLayout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -155,19 +155,7 @@ export default function ClientContracts() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="py-12">
|
<div className="container mx-auto max-w-6xl px-4 py-8 space-y-6">
|
||||||
<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>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
|
|
@ -432,6 +420,7 @@ export default function ClientContracts() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -187,31 +187,7 @@ export default function ClientInvoices() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="py-12">
|
<div className="container mx-auto max-w-6xl px-4 py-8 space-y-6">
|
||||||
<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>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
|
|
@ -448,6 +424,7 @@ export default function ClientInvoices() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -335,7 +335,7 @@ export default function ClientReports() {
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -566,7 +566,7 @@ export default function ClientSettings() {
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Bell, Pin, Loader2, Eye, EyeOff } from "lucide-react";
|
import { Bell, Pin, Loader2, Eye, EyeOff } from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
|
||||||
interface Announcement {
|
interface Announcement {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { DollarSign, FileText, Calendar, CheckCircle, AlertCircle, Plus, Loader2 } from "lucide-react";
|
import { DollarSign, FileText, Calendar, CheckCircle, AlertCircle, Plus, Loader2 } from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
|
||||||
interface Expense {
|
interface Expense {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import {
|
||||||
Eye,
|
Eye,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
|
||||||
interface KnowledgeArticle {
|
interface KnowledgeArticle {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import {
|
||||||
Loader2,
|
Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
|
||||||
interface Course {
|
interface Course {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ import {
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
|
||||||
interface KeyResult {
|
interface KeyResult {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import {
|
||||||
Loader2,
|
Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
|
||||||
interface OnboardingData {
|
interface OnboardingData {
|
||||||
progress: {
|
progress: {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import {
|
||||||
Target,
|
Target,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
|
||||||
interface ChecklistItem {
|
interface ChecklistItem {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
Loader2,
|
Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import {
|
||||||
Calendar,
|
Calendar,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import {
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
|
||||||
interface HandbookSection {
|
interface HandbookSection {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ import {
|
||||||
Edit,
|
Edit,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
28
package-lock.json
generated
28
package-lock.json
generated
|
|
@ -22,6 +22,7 @@
|
||||||
"png-to-ico": "^3.0.1",
|
"png-to-ico": "^3.0.1",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"stripe": "^15.12.0",
|
"stripe": "^15.12.0",
|
||||||
|
"wouter": "^3.9.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -13120,6 +13121,11 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/mkdirp": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||||
|
|
@ -14684,6 +14690,14 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
|
@ -16671,7 +16685,6 @@
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"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"
|
"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": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@
|
||||||
"png-to-ico": "^3.0.1",
|
"png-to-ico": "^3.0.1",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"stripe": "^15.12.0",
|
"stripe": "^15.12.0",
|
||||||
|
"wouter": "^3.9.0",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
452
server/index.ts
452
server/index.ts
|
|
@ -4,7 +4,39 @@ import express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import { adminSupabase } from "./supabase";
|
import { adminSupabase } from "./supabase";
|
||||||
import { emailService } from "./email";
|
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 blogIndexHandler from "../api/blog/index";
|
||||||
import blogSlugHandler from "../api/blog/[slug]";
|
import blogSlugHandler from "../api/blog/[slug]";
|
||||||
import aiChatHandler from "../api/ai/chat";
|
import aiChatHandler from "../api/ai/chat";
|
||||||
|
|
@ -384,7 +416,7 @@ export function createServer() {
|
||||||
const { data: user, error } = await adminSupabase
|
const { data: user, error } = await adminSupabase
|
||||||
.from("user_profiles")
|
.from("user_profiles")
|
||||||
.select(
|
.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)
|
.eq("username", username)
|
||||||
.single();
|
.single();
|
||||||
|
|
@ -402,11 +434,10 @@ export function createServer() {
|
||||||
achievement_id,
|
achievement_id,
|
||||||
achievements(
|
achievements(
|
||||||
id,
|
id,
|
||||||
name,
|
title,
|
||||||
description,
|
description,
|
||||||
icon,
|
icon,
|
||||||
category,
|
category
|
||||||
badge_color
|
|
||||||
)
|
)
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
|
|
@ -645,25 +676,19 @@ export function createServer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// First try exact match by name
|
// First try exact match by name
|
||||||
let query = adminSupabase
|
let { data, error } = await (adminSupabase as any)
|
||||||
.from("projects")
|
.from("projects")
|
||||||
.select(
|
.select("id, title, slug, description, user_id, created_at, updated_at, status, image_url, website")
|
||||||
"id, title, slug, description, user_id, created_at, updated_at, status, image_url, website",
|
.eq("slug", projectname)
|
||||||
)
|
.single();
|
||||||
.eq("slug", projectname);
|
|
||||||
|
|
||||||
let { data, error } = await query.single();
|
|
||||||
|
|
||||||
// If not found by slug, try by title (case-insensitive)
|
// If not found by slug, try by title (case-insensitive)
|
||||||
if (error && error.code === "PGRST116") {
|
if (error && error.code === "PGRST116") {
|
||||||
query = adminSupabase
|
const response = await (adminSupabase as any)
|
||||||
.from("projects")
|
.from("projects")
|
||||||
.select(
|
.select("id, title, slug, description, user_id, created_at, updated_at, status, image_url, website")
|
||||||
"id, title, slug, description, user_id, created_at, updated_at, status, image_url, website",
|
|
||||||
)
|
|
||||||
.ilike("title", projectname);
|
.ilike("title", projectname);
|
||||||
|
|
||||||
const response = await query;
|
|
||||||
if (response.data && response.data.length > 0) {
|
if (response.data && response.data.length > 0) {
|
||||||
data = response.data[0];
|
data = response.data[0];
|
||||||
error = null;
|
error = null;
|
||||||
|
|
@ -698,6 +723,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)
|
// DevConnect REST proxy (GET only)
|
||||||
app.get("/api/devconnect/rest/:table", async (req, res) => {
|
app.get("/api/devconnect/rest/:table", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -3212,6 +3266,16 @@ export function createServer() {
|
||||||
.upsert(rows, { onConflict: "user_id,achievement_id" as any });
|
.upsert(rows, { onConflict: "user_id,achievement_id" as any });
|
||||||
if (iErr && iErr.code !== "23505")
|
if (iErr && iErr.code !== "23505")
|
||||||
return res.status(500).json({ error: iErr.message });
|
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 });
|
return res.json({ ok: true, awarded: rows.length });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("[API] achievements/award exception", e);
|
console.error("[API] achievements/award exception", e);
|
||||||
|
|
@ -3257,42 +3321,42 @@ export function createServer() {
|
||||||
const CORE_ACHIEVEMENTS = [
|
const CORE_ACHIEVEMENTS = [
|
||||||
{
|
{
|
||||||
id: "welcome-to-aethex",
|
id: "welcome-to-aethex",
|
||||||
name: "Welcome to AeThex",
|
slug: "welcome-to-aethex",
|
||||||
|
title: "Welcome to AeThex",
|
||||||
description: "Completed onboarding and joined the AeThex network.",
|
description: "Completed onboarding and joined the AeThex network.",
|
||||||
icon: "🎉",
|
icon: "🎉",
|
||||||
badge_color: "#7C3AED",
|
|
||||||
xp_reward: 250,
|
xp_reward: 250,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "aethex-explorer",
|
id: "aethex-explorer",
|
||||||
name: "AeThex Explorer",
|
slug: "aethex-explorer",
|
||||||
|
title: "AeThex Explorer",
|
||||||
description: "Engaged with community initiatives and posted first update.",
|
description: "Engaged with community initiatives and posted first update.",
|
||||||
icon: "🧭",
|
icon: "🧭",
|
||||||
badge_color: "#0EA5E9",
|
|
||||||
xp_reward: 400,
|
xp_reward: 400,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "community-champion",
|
id: "community-champion",
|
||||||
name: "Community Champion",
|
slug: "community-champion",
|
||||||
|
title: "Community Champion",
|
||||||
description: "Contributed feedback, resolved bugs, and mentored squads.",
|
description: "Contributed feedback, resolved bugs, and mentored squads.",
|
||||||
icon: "🏆",
|
icon: "🏆",
|
||||||
badge_color: "#22C55E",
|
|
||||||
xp_reward: 750,
|
xp_reward: 750,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "workshop-architect",
|
id: "workshop-architect",
|
||||||
name: "Workshop Architect",
|
slug: "workshop-architect",
|
||||||
|
title: "Workshop Architect",
|
||||||
description: "Published a high-impact mod or toolkit adopted by teams.",
|
description: "Published a high-impact mod or toolkit adopted by teams.",
|
||||||
icon: "🛠️",
|
icon: "🛠️",
|
||||||
badge_color: "#F97316",
|
|
||||||
xp_reward: 1200,
|
xp_reward: 1200,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "god-mode",
|
id: "god-mode",
|
||||||
name: "GOD Mode",
|
slug: "god-mode",
|
||||||
|
title: "GOD Mode",
|
||||||
description: "Legendary status awarded by AeThex studio leadership.",
|
description: "Legendary status awarded by AeThex studio leadership.",
|
||||||
icon: "⚡",
|
icon: "⚡",
|
||||||
badge_color: "#FACC15",
|
|
||||||
xp_reward: 5000,
|
xp_reward: 5000,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -3317,10 +3381,10 @@ export function createServer() {
|
||||||
const { error } = await adminSupabase.from("achievements").upsert(
|
const { error } = await adminSupabase.from("achievements").upsert(
|
||||||
{
|
{
|
||||||
id: uuidId,
|
id: uuidId,
|
||||||
name: achievement.name,
|
slug: achievement.slug,
|
||||||
|
title: achievement.title,
|
||||||
description: achievement.description,
|
description: achievement.description,
|
||||||
icon: achievement.icon,
|
icon: achievement.icon,
|
||||||
badge_color: achievement.badge_color,
|
|
||||||
xp_reward: achievement.xp_reward,
|
xp_reward: achievement.xp_reward,
|
||||||
},
|
},
|
||||||
{ onConflict: "id", ignoreDuplicates: true },
|
{ onConflict: "id", ignoreDuplicates: true },
|
||||||
|
|
@ -3329,7 +3393,7 @@ export function createServer() {
|
||||||
if (error && error.code !== "23505") {
|
if (error && error.code !== "23505") {
|
||||||
console.error(`Failed to upsert achievement ${achievement.id}:`, error);
|
console.error(`Failed to upsert achievement ${achievement.id}:`, error);
|
||||||
} else {
|
} else {
|
||||||
seededAchievements[achievement.name] = uuidId;
|
seededAchievements[achievement.title] = uuidId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3341,20 +3405,33 @@ export function createServer() {
|
||||||
const awardedAchievementIds: string[] = [];
|
const awardedAchievementIds: string[] = [];
|
||||||
|
|
||||||
if (canAwardToTarget && (targetEmail || targetUsername)) {
|
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) {
|
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) {
|
} 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 (targetProfile?.id) {
|
||||||
|
targetUserId = targetProfile.id;
|
||||||
if (userProfile?.id) {
|
|
||||||
targetUserId = userProfile.id;
|
|
||||||
|
|
||||||
// Check if target user is an admin (for GOD Mode)
|
// 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
|
// Award Welcome achievement to the user
|
||||||
const welcomeId = seededAchievements["Welcome to AeThex"];
|
const welcomeId = seededAchievements["Welcome to AeThex"];
|
||||||
|
|
@ -3633,6 +3710,26 @@ export function createServer() {
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
if (error) return res.status(500).json({ error: error.message });
|
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);
|
res.json(data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
res.status(500).json({ error: e?.message || String(e) });
|
res.status(500).json({ error: e?.message || String(e) });
|
||||||
|
|
@ -3692,6 +3789,23 @@ export function createServer() {
|
||||||
adminSupabase.from("activity_projects").update({ upvotes: adminSupabase.rpc("get_upvote_count", { pid: projectId }) }).eq("id", projectId);
|
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 });
|
res.json({ ok: true });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
res.status(500).json({ error: e?.message || String(e) });
|
res.status(500).json({ error: e?.message || String(e) });
|
||||||
|
|
@ -6527,7 +6641,7 @@ export function createServer() {
|
||||||
verified,
|
verified,
|
||||||
total_downloads,
|
total_downloads,
|
||||||
created_at,
|
created_at,
|
||||||
user_profiles(id, full_name, avatar_url, email)
|
user_profiles(id, full_name, avatar_url)
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.eq("user_id", id)
|
.eq("user_id", id)
|
||||||
|
|
@ -7666,7 +7780,7 @@ export function createServer() {
|
||||||
.from("nexus_applications")
|
.from("nexus_applications")
|
||||||
.select(`
|
.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),
|
creator_profile:nexus_creator_profiles(skills, experience_level, hourly_rate, rating, review_count),
|
||||||
opportunity:nexus_opportunities(id, title)
|
opportunity:nexus_opportunities(id, title)
|
||||||
`)
|
`)
|
||||||
|
|
@ -7710,7 +7824,7 @@ export function createServer() {
|
||||||
.from("nexus_applications")
|
.from("nexus_applications")
|
||||||
.select(`
|
.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)
|
creator_profile:nexus_creator_profiles(skills, experience_level, hourly_rate, rating, review_count)
|
||||||
`)
|
`)
|
||||||
.eq("opportunity_id", opportunityId)
|
.eq("opportunity_id", opportunityId)
|
||||||
|
|
@ -7787,6 +7901,262 @@ 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: "/" });
|
||||||
|
|
||||||
|
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
|
// Blog API routes
|
||||||
app.get("/api/blog", blogIndexHandler);
|
app.get("/api/blog", blogIndexHandler);
|
||||||
app.get("/api/blog/:slug", (req: express.Request, res: express.Response) => {
|
app.get("/api/blog/:slug", (req: express.Request, res: express.Response) => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
import { createServer } from "./index";
|
import { createServer } from "./index";
|
||||||
import * as express from "express";
|
import * as express from "express";
|
||||||
|
|
||||||
|
|
@ -7,7 +8,8 @@ const port = process.env.PORT || 5000;
|
||||||
const host = "0.0.0.0";
|
const host = "0.0.0.0";
|
||||||
|
|
||||||
// In production, serve the built SPA files
|
// 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");
|
const distPath = path.join(__dirname, "../spa");
|
||||||
|
|
||||||
// Serve static files
|
// Serve static files
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { createClient } from "@supabase/supabase-js";
|
import { createClient } from "@supabase/supabase-js";
|
||||||
|
import type { Database } from "../client/lib/database.types";
|
||||||
|
|
||||||
const SUPABASE_URL =
|
const SUPABASE_URL =
|
||||||
process.env.SUPABASE_URL || process.env.VITE_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) {
|
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 },
|
auth: { autoRefreshToken: false, persistSession: false },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const adminSupabase = admin as ReturnType<typeof createClient>;
|
export const adminSupabase = admin as ReturnType<typeof createClient<Database>>;
|
||||||
|
|
|
||||||
32
supabase/migrations/20260412_fix_notifications_schema.sql
Normal file
32
supabase/migrations/20260412_fix_notifications_schema.sql
Normal 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();
|
||||||
|
|
@ -72,11 +72,19 @@ export default {
|
||||||
},
|
},
|
||||||
neon: {
|
neon: {
|
||||||
purple: "hsl(var(--neon-purple))",
|
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))",
|
green: "hsl(var(--neon-green))",
|
||||||
yellow: "hsl(var(--neon-yellow))",
|
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: {
|
borderRadius: {
|
||||||
lg: "var(--radius)",
|
lg: "var(--radius)",
|
||||||
md: "calc(var(--radius) - 2px)",
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { defineConfig, Plugin } from "vite";
|
import { defineConfig, Plugin } from "vite";
|
||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react-swc";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
|
||||||
|
const pkg = JSON.parse(readFileSync(path.resolve(__dirname, "package.json"), "utf-8"));
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
|
|
@ -20,6 +23,9 @@ export default defineConfig(({ mode }) => ({
|
||||||
build: {
|
build: {
|
||||||
outDir: "dist/spa",
|
outDir: "dist/spa",
|
||||||
},
|
},
|
||||||
|
define: {
|
||||||
|
"import.meta.env.VITE_APP_VERSION": JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
plugins: [react(), expressPlugin()],
|
plugins: [react(), expressPlugin()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue