Prettier format pending files

This commit is contained in:
Builder.io 2025-11-08 03:49:51 +00:00
parent b3f92fe96d
commit 0afd536c9b
53 changed files with 1467 additions and 720 deletions

View file

@ -5,10 +5,7 @@ const supabaseServiceRole = process.env.SUPABASE_SERVICE_ROLE || "";
const supabase = createClient(supabaseUrl, supabaseServiceRole); const supabase = createClient(supabaseUrl, supabaseServiceRole);
export async function getMyApplications( export async function getMyApplications(req: Request, userId: string) {
req: Request,
userId: string
) {
const url = new URL(req.url); const url = new URL(req.url);
const page = parseInt(url.searchParams.get("page") || "1"); const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "10"); const limit = parseInt(url.searchParams.get("limit") || "10");
@ -25,7 +22,7 @@ export async function getMyApplications(
if (!creator) { if (!creator) {
return new Response( return new Response(
JSON.stringify({ error: "Creator profile not found" }), JSON.stringify({ error: "Creator profile not found" }),
{ status: 404, headers: { "Content-Type": "application/json" } } { status: 404, headers: { "Content-Type": "application/json" } },
); );
} }
@ -43,7 +40,7 @@ export async function getMyApplications(
updated_at, updated_at,
aethex_opportunities(id, title, arm_affiliation, job_type, posted_by_id, aethex_creators!aethex_opportunities_posted_by_id_fkey(username, avatar_url)) aethex_opportunities(id, title, arm_affiliation, job_type, posted_by_id, aethex_creators!aethex_opportunities_posted_by_id_fkey(username, avatar_url))
`, `,
{ count: "exact" } { count: "exact" },
) )
.eq("creator_id", creator.id); .eq("creator_id", creator.id);
@ -73,20 +70,20 @@ export async function getMyApplications(
{ {
status: 200, status: 200,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
} },
); );
} catch (error) { } catch (error) {
console.error("Error fetching applications:", error); console.error("Error fetching applications:", error);
return new Response( return new Response(
JSON.stringify({ error: "Failed to fetch applications" }), JSON.stringify({ error: "Failed to fetch applications" }),
{ status: 500, headers: { "Content-Type": "application/json" } } { status: 500, headers: { "Content-Type": "application/json" } },
); );
} }
} }
export async function getApplicationsForOpportunity( export async function getApplicationsForOpportunity(
opportunityId: string, opportunityId: string,
userId: string userId: string,
) { ) {
try { try {
// Verify user owns this opportunity // Verify user owns this opportunity
@ -97,10 +94,10 @@ export async function getApplicationsForOpportunity(
.single(); .single();
if (!opportunity) { if (!opportunity) {
return new Response( return new Response(JSON.stringify({ error: "Opportunity not found" }), {
JSON.stringify({ error: "Opportunity not found" }), status: 404,
{ status: 404, headers: { "Content-Type": "application/json" } } headers: { "Content-Type": "application/json" },
); });
} }
const { data: creator } = await supabase const { data: creator } = await supabase
@ -110,10 +107,10 @@ export async function getApplicationsForOpportunity(
.single(); .single();
if (creator?.id !== opportunity.posted_by_id) { if (creator?.id !== opportunity.posted_by_id) {
return new Response( return new Response(JSON.stringify({ error: "Unauthorized" }), {
JSON.stringify({ error: "Unauthorized" }), status: 403,
{ status: 403, headers: { "Content-Type": "application/json" } } headers: { "Content-Type": "application/json" },
); });
} }
const { data, error } = await supabase const { data, error } = await supabase
@ -126,7 +123,7 @@ export async function getApplicationsForOpportunity(
cover_letter, cover_letter,
applied_at, applied_at,
aethex_creators(username, avatar_url, bio, skills) aethex_creators(username, avatar_url, bio, skills)
` `,
) )
.eq("opportunity_id", opportunityId) .eq("opportunity_id", opportunityId)
.order("applied_at", { ascending: false }); .order("applied_at", { ascending: false });
@ -141,15 +138,12 @@ export async function getApplicationsForOpportunity(
console.error("Error fetching opportunity applications:", error); console.error("Error fetching opportunity applications:", error);
return new Response( return new Response(
JSON.stringify({ error: "Failed to fetch applications" }), JSON.stringify({ error: "Failed to fetch applications" }),
{ status: 500, headers: { "Content-Type": "application/json" } } { status: 500, headers: { "Content-Type": "application/json" } },
); );
} }
} }
export async function submitApplication( export async function submitApplication(req: Request, userId: string) {
req: Request,
userId: string
) {
try { try {
const body = await req.json(); const body = await req.json();
const { opportunity_id, cover_letter } = body; const { opportunity_id, cover_letter } = body;
@ -164,7 +158,7 @@ export async function submitApplication(
if (!creator) { if (!creator) {
return new Response( return new Response(
JSON.stringify({ error: "Creator profile not found" }), JSON.stringify({ error: "Creator profile not found" }),
{ status: 404, headers: { "Content-Type": "application/json" } } { status: 404, headers: { "Content-Type": "application/json" } },
); );
} }
@ -179,7 +173,7 @@ export async function submitApplication(
if (!opportunity) { if (!opportunity) {
return new Response( return new Response(
JSON.stringify({ error: "Opportunity not found or closed" }), JSON.stringify({ error: "Opportunity not found or closed" }),
{ status: 404, headers: { "Content-Type": "application/json" } } { status: 404, headers: { "Content-Type": "application/json" } },
); );
} }
@ -193,8 +187,10 @@ export async function submitApplication(
if (existing) { if (existing) {
return new Response( return new Response(
JSON.stringify({ error: "You have already applied to this opportunity" }), JSON.stringify({
{ status: 400, headers: { "Content-Type": "application/json" } } error: "You have already applied to this opportunity",
}),
{ status: 400, headers: { "Content-Type": "application/json" } },
); );
} }
@ -219,7 +215,7 @@ export async function submitApplication(
console.error("Error submitting application:", error); console.error("Error submitting application:", error);
return new Response( return new Response(
JSON.stringify({ error: "Failed to submit application" }), JSON.stringify({ error: "Failed to submit application" }),
{ status: 500, headers: { "Content-Type": "application/json" } } { status: 500, headers: { "Content-Type": "application/json" } },
); );
} }
} }
@ -227,7 +223,7 @@ export async function submitApplication(
export async function updateApplicationStatus( export async function updateApplicationStatus(
req: Request, req: Request,
applicationId: string, applicationId: string,
userId: string userId: string,
) { ) {
try { try {
const body = await req.json(); const body = await req.json();
@ -241,16 +237,16 @@ export async function updateApplicationStatus(
id, id,
opportunity_id, opportunity_id,
aethex_opportunities(posted_by_id) aethex_opportunities(posted_by_id)
` `,
) )
.eq("id", applicationId) .eq("id", applicationId)
.single(); .single();
if (!application) { if (!application) {
return new Response( return new Response(JSON.stringify({ error: "Application not found" }), {
JSON.stringify({ error: "Application not found" }), status: 404,
{ status: 404, headers: { "Content-Type": "application/json" } } headers: { "Content-Type": "application/json" },
); });
} }
const { data: creator } = await supabase const { data: creator } = await supabase
@ -260,10 +256,10 @@ export async function updateApplicationStatus(
.single(); .single();
if (creator?.id !== application.aethex_opportunities.posted_by_id) { if (creator?.id !== application.aethex_opportunities.posted_by_id) {
return new Response( return new Response(JSON.stringify({ error: "Unauthorized" }), {
JSON.stringify({ error: "Unauthorized" }), status: 403,
{ status: 403, headers: { "Content-Type": "application/json" } } headers: { "Content-Type": "application/json" },
); });
} }
const { data, error } = await supabase const { data, error } = await supabase
@ -287,14 +283,14 @@ export async function updateApplicationStatus(
console.error("Error updating application:", error); console.error("Error updating application:", error);
return new Response( return new Response(
JSON.stringify({ error: "Failed to update application" }), JSON.stringify({ error: "Failed to update application" }),
{ status: 500, headers: { "Content-Type": "application/json" } } { status: 500, headers: { "Content-Type": "application/json" } },
); );
} }
} }
export async function withdrawApplication( export async function withdrawApplication(
applicationId: string, applicationId: string,
userId: string userId: string,
) { ) {
try { try {
// Verify user owns this application // Verify user owns this application
@ -305,10 +301,10 @@ export async function withdrawApplication(
.single(); .single();
if (!application) { if (!application) {
return new Response( return new Response(JSON.stringify({ error: "Application not found" }), {
JSON.stringify({ error: "Application not found" }), status: 404,
{ status: 404, headers: { "Content-Type": "application/json" } } headers: { "Content-Type": "application/json" },
); });
} }
const { data: creator } = await supabase const { data: creator } = await supabase
@ -318,10 +314,10 @@ export async function withdrawApplication(
.single(); .single();
if (creator?.id !== application.creator_id) { if (creator?.id !== application.creator_id) {
return new Response( return new Response(JSON.stringify({ error: "Unauthorized" }), {
JSON.stringify({ error: "Unauthorized" }), status: 403,
{ status: 403, headers: { "Content-Type": "application/json" } } headers: { "Content-Type": "application/json" },
); });
} }
const { error } = await supabase const { error } = await supabase
@ -339,7 +335,7 @@ export async function withdrawApplication(
console.error("Error withdrawing application:", error); console.error("Error withdrawing application:", error);
return new Response( return new Response(
JSON.stringify({ error: "Failed to withdraw application" }), JSON.stringify({ error: "Failed to withdraw application" }),
{ status: 500, headers: { "Content-Type": "application/json" } } { status: 500, headers: { "Content-Type": "application/json" } },
); );
} }
} }

View file

@ -28,7 +28,7 @@ export async function getCreators(req: Request) {
created_at, created_at,
aethex_projects(count) aethex_projects(count)
`, `,
{ count: "exact" } { count: "exact" },
) )
.eq("is_discoverable", true) .eq("is_discoverable", true)
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });
@ -38,9 +38,7 @@ export async function getCreators(req: Request) {
} }
if (search) { if (search) {
query = query.or( query = query.or(`username.ilike.%${search}%,bio.ilike.%${search}%`);
`username.ilike.%${search}%,bio.ilike.%${search}%`
);
} }
const start = (page - 1) * limit; const start = (page - 1) * limit;
@ -63,14 +61,14 @@ export async function getCreators(req: Request) {
{ {
status: 200, status: 200,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
} },
); );
} catch (error) { } catch (error) {
console.error("Error fetching creators:", error); console.error("Error fetching creators:", error);
return new Response( return new Response(JSON.stringify({ error: "Failed to fetch creators" }), {
JSON.stringify({ error: "Failed to fetch creators" }), status: 500,
{ status: 500, headers: { "Content-Type": "application/json" } } headers: { "Content-Type": "application/json" },
); });
} }
} }
@ -92,7 +90,7 @@ export async function getCreatorByUsername(username: string) {
updated_at, updated_at,
aethex_projects(id, title, description, url, image_url, tags, is_featured), aethex_projects(id, title, description, url, image_url, tags, is_featured),
aethex_skill_endorsements(skill, count) aethex_skill_endorsements(skill, count)
` `,
) )
.eq("username", username) .eq("username", username)
.eq("is_discoverable", true) .eq("is_discoverable", true)
@ -117,10 +115,7 @@ export async function getCreatorByUsername(username: string) {
} }
} }
export async function createCreatorProfile( export async function createCreatorProfile(req: Request, userId: string) {
req: Request,
userId: string
) {
try { try {
const body = await req.json(); const body = await req.json();
const { const {
@ -158,15 +153,12 @@ export async function createCreatorProfile(
console.error("Error creating creator profile:", error); console.error("Error creating creator profile:", error);
return new Response( return new Response(
JSON.stringify({ error: "Failed to create creator profile" }), JSON.stringify({ error: "Failed to create creator profile" }),
{ status: 500, headers: { "Content-Type": "application/json" } } { status: 500, headers: { "Content-Type": "application/json" } },
); );
} }
} }
export async function updateCreatorProfile( export async function updateCreatorProfile(req: Request, userId: string) {
req: Request,
userId: string
) {
try { try {
const body = await req.json(); const body = await req.json();
const { const {
@ -209,15 +201,12 @@ export async function updateCreatorProfile(
console.error("Error updating creator profile:", error); console.error("Error updating creator profile:", error);
return new Response( return new Response(
JSON.stringify({ error: "Failed to update creator profile" }), JSON.stringify({ error: "Failed to update creator profile" }),
{ status: 500, headers: { "Content-Type": "application/json" } } { status: 500, headers: { "Content-Type": "application/json" } },
); );
} }
} }
export async function addProjectToCreator( export async function addProjectToCreator(req: Request, creatorId: string) {
req: Request,
creatorId: string
) {
try { try {
const body = await req.json(); const body = await req.json();
const { title, description, url, image_url, tags, is_featured } = body; const { title, description, url, image_url, tags, is_featured } = body;
@ -244,17 +233,17 @@ export async function addProjectToCreator(
}); });
} catch (error) { } catch (error) {
console.error("Error adding project:", error); console.error("Error adding project:", error);
return new Response( return new Response(JSON.stringify({ error: "Failed to add project" }), {
JSON.stringify({ error: "Failed to add project" }), status: 500,
{ status: 500, headers: { "Content-Type": "application/json" } } headers: { "Content-Type": "application/json" },
); });
} }
} }
export async function endorseSkill( export async function endorseSkill(
req: Request, req: Request,
creatorId: string, creatorId: string,
endorsedByUserId: string endorsedByUserId: string,
) { ) {
try { try {
const body = await req.json(); const body = await req.json();
@ -270,7 +259,7 @@ export async function endorseSkill(
if (!endorsingCreator) { if (!endorsingCreator) {
return new Response( return new Response(
JSON.stringify({ error: "Endorsing user not found" }), JSON.stringify({ error: "Endorsing user not found" }),
{ status: 404, headers: { "Content-Type": "application/json" } } { status: 404, headers: { "Content-Type": "application/json" } },
); );
} }
@ -292,9 +281,9 @@ export async function endorseSkill(
}); });
} catch (error) { } catch (error) {
console.error("Error endorsing skill:", error); console.error("Error endorsing skill:", error);
return new Response( return new Response(JSON.stringify({ error: "Failed to endorse skill" }), {
JSON.stringify({ error: "Failed to endorse skill" }), status: 500,
{ status: 500, headers: { "Content-Type": "application/json" } } headers: { "Content-Type": "application/json" },
); });
} }
} }

View file

@ -5,10 +5,7 @@ const supabaseServiceRole = process.env.SUPABASE_SERVICE_ROLE || "";
const supabase = createClient(supabaseUrl, supabaseServiceRole); const supabase = createClient(supabaseUrl, supabaseServiceRole);
export async function linkDevConnectAccount( export async function linkDevConnectAccount(req: Request, userId: string) {
req: Request,
userId: string
) {
try { try {
const body = await req.json(); const body = await req.json();
const { devconnect_username, devconnect_profile_url } = body; const { devconnect_username, devconnect_profile_url } = body;
@ -16,7 +13,7 @@ export async function linkDevConnectAccount(
if (!devconnect_username) { if (!devconnect_username) {
return new Response( return new Response(
JSON.stringify({ error: "DevConnect username is required" }), JSON.stringify({ error: "DevConnect username is required" }),
{ status: 400, headers: { "Content-Type": "application/json" } } { status: 400, headers: { "Content-Type": "application/json" } },
); );
} }
@ -29,8 +26,10 @@ export async function linkDevConnectAccount(
if (!creator) { if (!creator) {
return new Response( return new Response(
JSON.stringify({ error: "Creator profile not found. Create profile first." }), JSON.stringify({
{ status: 404, headers: { "Content-Type": "application/json" } } error: "Creator profile not found. Create profile first.",
}),
{ status: 404, headers: { "Content-Type": "application/json" } },
); );
} }
@ -50,7 +49,9 @@ export async function linkDevConnectAccount(
.from("aethex_devconnect_links") .from("aethex_devconnect_links")
.update({ .update({
devconnect_username, devconnect_username,
devconnect_profile_url: devconnect_profile_url || `https://devconnect.com/${devconnect_username}`, devconnect_profile_url:
devconnect_profile_url ||
`https://devconnect.com/${devconnect_username}`,
}) })
.eq("aethex_creator_id", creator.id) .eq("aethex_creator_id", creator.id)
.select() .select()
@ -66,7 +67,9 @@ export async function linkDevConnectAccount(
.insert({ .insert({
aethex_creator_id: creator.id, aethex_creator_id: creator.id,
devconnect_username, devconnect_username,
devconnect_profile_url: devconnect_profile_url || `https://devconnect.com/${devconnect_username}`, devconnect_profile_url:
devconnect_profile_url ||
`https://devconnect.com/${devconnect_username}`,
}) })
.select() .select()
.single(); .single();
@ -89,7 +92,7 @@ export async function linkDevConnectAccount(
console.error("Error linking DevConnect account:", error); console.error("Error linking DevConnect account:", error);
return new Response( return new Response(
JSON.stringify({ error: "Failed to link DevConnect account" }), JSON.stringify({ error: "Failed to link DevConnect account" }),
{ status: 500, headers: { "Content-Type": "application/json" } } { status: 500, headers: { "Content-Type": "application/json" } },
); );
} }
} }
@ -106,7 +109,7 @@ export async function getDevConnectLink(userId: string) {
if (!creator) { if (!creator) {
return new Response( return new Response(
JSON.stringify({ error: "Creator profile not found" }), JSON.stringify({ error: "Creator profile not found" }),
{ status: 404, headers: { "Content-Type": "application/json" } } { status: 404, headers: { "Content-Type": "application/json" } },
); );
} }
@ -128,7 +131,7 @@ export async function getDevConnectLink(userId: string) {
console.error("Error fetching DevConnect link:", error); console.error("Error fetching DevConnect link:", error);
return new Response( return new Response(
JSON.stringify({ error: "Failed to fetch DevConnect link" }), JSON.stringify({ error: "Failed to fetch DevConnect link" }),
{ status: 500, headers: { "Content-Type": "application/json" } } { status: 500, headers: { "Content-Type": "application/json" } },
); );
} }
} }
@ -145,7 +148,7 @@ export async function unlinkDevConnectAccount(userId: string) {
if (!creator) { if (!creator) {
return new Response( return new Response(
JSON.stringify({ error: "Creator profile not found" }), JSON.stringify({ error: "Creator profile not found" }),
{ status: 404, headers: { "Content-Type": "application/json" } } { status: 404, headers: { "Content-Type": "application/json" } },
); );
} }
@ -170,15 +173,12 @@ export async function unlinkDevConnectAccount(userId: string) {
console.error("Error unlinking DevConnect account:", error); console.error("Error unlinking DevConnect account:", error);
return new Response( return new Response(
JSON.stringify({ error: "Failed to unlink DevConnect account" }), JSON.stringify({ error: "Failed to unlink DevConnect account" }),
{ status: 500, headers: { "Content-Type": "application/json" } } { status: 500, headers: { "Content-Type": "application/json" } },
); );
} }
} }
export async function verifyDevConnectLink( export async function verifyDevConnectLink(req: Request, userId: string) {
req: Request,
userId: string
) {
try { try {
const body = await req.json(); const body = await req.json();
const { verification_code } = body; const { verification_code } = body;
@ -193,7 +193,7 @@ export async function verifyDevConnectLink(
if (!creator) { if (!creator) {
return new Response( return new Response(
JSON.stringify({ error: "Creator profile not found" }), JSON.stringify({ error: "Creator profile not found" }),
{ status: 404, headers: { "Content-Type": "application/json" } } { status: 404, headers: { "Content-Type": "application/json" } },
); );
} }
@ -216,7 +216,7 @@ export async function verifyDevConnectLink(
console.error("Error verifying DevConnect link:", error); console.error("Error verifying DevConnect link:", error);
return new Response( return new Response(
JSON.stringify({ error: "Failed to verify DevConnect link" }), JSON.stringify({ error: "Failed to verify DevConnect link" }),
{ status: 500, headers: { "Content-Type": "application/json" } } { status: 500, headers: { "Content-Type": "application/json" } },
); );
} }
} }

View file

@ -34,7 +34,7 @@ export async function getOpportunities(req: Request) {
created_at, created_at,
aethex_applications(count) aethex_applications(count)
`, `,
{ count: "exact" } { count: "exact" },
) )
.eq("status", "open"); .eq("status", "open");
@ -43,9 +43,7 @@ export async function getOpportunities(req: Request) {
} }
if (search) { if (search) {
query = query.or( query = query.or(`title.ilike.%${search}%,description.ilike.%${search}%`);
`title.ilike.%${search}%,description.ilike.%${search}%`
);
} }
if (jobType) { if (jobType) {
@ -80,13 +78,13 @@ export async function getOpportunities(req: Request) {
{ {
status: 200, status: 200,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
} },
); );
} catch (error) { } catch (error) {
console.error("Error fetching opportunities:", error); console.error("Error fetching opportunities:", error);
return new Response( return new Response(
JSON.stringify({ error: "Failed to fetch opportunities" }), JSON.stringify({ error: "Failed to fetch opportunities" }),
{ status: 500, headers: { "Content-Type": "application/json" } } { status: 500, headers: { "Content-Type": "application/json" } },
); );
} }
} }
@ -111,7 +109,7 @@ export async function getOpportunityById(opportunityId: string) {
created_at, created_at,
updated_at, updated_at,
aethex_applications(count) aethex_applications(count)
` `,
) )
.eq("id", opportunityId) .eq("id", opportunityId)
.eq("status", "open") .eq("status", "open")
@ -126,10 +124,7 @@ export async function getOpportunityById(opportunityId: string) {
} }
} }
export async function createOpportunity( export async function createOpportunity(req: Request, userId: string) {
req: Request,
userId: string
) {
try { try {
const body = await req.json(); const body = await req.json();
const { const {
@ -151,8 +146,10 @@ export async function createOpportunity(
if (!creator) { if (!creator) {
return new Response( return new Response(
JSON.stringify({ error: "Creator profile not found. Create profile first." }), JSON.stringify({
{ status: 404, headers: { "Content-Type": "application/json" } } error: "Creator profile not found. Create profile first.",
}),
{ status: 404, headers: { "Content-Type": "application/json" } },
); );
} }
@ -182,7 +179,7 @@ export async function createOpportunity(
console.error("Error creating opportunity:", error); console.error("Error creating opportunity:", error);
return new Response( return new Response(
JSON.stringify({ error: "Failed to create opportunity" }), JSON.stringify({ error: "Failed to create opportunity" }),
{ status: 500, headers: { "Content-Type": "application/json" } } { status: 500, headers: { "Content-Type": "application/json" } },
); );
} }
} }
@ -190,7 +187,7 @@ export async function createOpportunity(
export async function updateOpportunity( export async function updateOpportunity(
req: Request, req: Request,
opportunityId: string, opportunityId: string,
userId: string userId: string,
) { ) {
try { try {
const body = await req.json(); const body = await req.json();
@ -212,10 +209,10 @@ export async function updateOpportunity(
.single(); .single();
if (!opportunity) { if (!opportunity) {
return new Response( return new Response(JSON.stringify({ error: "Opportunity not found" }), {
JSON.stringify({ error: "Opportunity not found" }), status: 404,
{ status: 404, headers: { "Content-Type": "application/json" } } headers: { "Content-Type": "application/json" },
); });
} }
const { data: creator } = await supabase const { data: creator } = await supabase
@ -225,10 +222,10 @@ export async function updateOpportunity(
.single(); .single();
if (creator?.id !== opportunity.posted_by_id) { if (creator?.id !== opportunity.posted_by_id) {
return new Response( return new Response(JSON.stringify({ error: "Unauthorized" }), {
JSON.stringify({ error: "Unauthorized" }), status: 403,
{ status: 403, headers: { "Content-Type": "application/json" } } headers: { "Content-Type": "application/json" },
); });
} }
const { data, error } = await supabase const { data, error } = await supabase
@ -257,15 +254,12 @@ export async function updateOpportunity(
console.error("Error updating opportunity:", error); console.error("Error updating opportunity:", error);
return new Response( return new Response(
JSON.stringify({ error: "Failed to update opportunity" }), JSON.stringify({ error: "Failed to update opportunity" }),
{ status: 500, headers: { "Content-Type": "application/json" } } { status: 500, headers: { "Content-Type": "application/json" } },
); );
} }
} }
export async function closeOpportunity( export async function closeOpportunity(opportunityId: string, userId: string) {
opportunityId: string,
userId: string
) {
try { try {
// Verify user owns this opportunity // Verify user owns this opportunity
const { data: opportunity } = await supabase const { data: opportunity } = await supabase
@ -275,10 +269,10 @@ export async function closeOpportunity(
.single(); .single();
if (!opportunity) { if (!opportunity) {
return new Response( return new Response(JSON.stringify({ error: "Opportunity not found" }), {
JSON.stringify({ error: "Opportunity not found" }), status: 404,
{ status: 404, headers: { "Content-Type": "application/json" } } headers: { "Content-Type": "application/json" },
); });
} }
const { data: creator } = await supabase const { data: creator } = await supabase
@ -288,10 +282,10 @@ export async function closeOpportunity(
.single(); .single();
if (creator?.id !== opportunity.posted_by_id) { if (creator?.id !== opportunity.posted_by_id) {
return new Response( return new Response(JSON.stringify({ error: "Unauthorized" }), {
JSON.stringify({ error: "Unauthorized" }), status: 403,
{ status: 403, headers: { "Content-Type": "application/json" } } headers: { "Content-Type": "application/json" },
); });
} }
const { data, error } = await supabase const { data, error } = await supabase
@ -314,7 +308,7 @@ export async function closeOpportunity(
console.error("Error closing opportunity:", error); console.error("Error closing opportunity:", error);
return new Response( return new Response(
JSON.stringify({ error: "Failed to close opportunity" }), JSON.stringify({ error: "Failed to close opportunity" }),
{ status: 500, headers: { "Content-Type": "application/json" } } { status: 500, headers: { "Content-Type": "application/json" } },
); );
} }
} }

View file

@ -144,7 +144,10 @@ const App = () => (
/> />
<Route path="/profile" element={<Profile />} /> <Route path="/profile" element={<Profile />} />
<Route path="/profile/me" element={<Profile />} /> <Route path="/profile/me" element={<Profile />} />
<Route path="/profile/applications" element={<MyApplications />} /> <Route
path="/profile/applications"
element={<MyApplications />}
/>
<Route path="/developers" element={<DevelopersDirectory />} /> <Route path="/developers" element={<DevelopersDirectory />} />
<Route <Route
@ -183,9 +186,15 @@ const App = () => (
{/* Creator Network routes */} {/* Creator Network routes */}
<Route path="/creators" element={<CreatorDirectory />} /> <Route path="/creators" element={<CreatorDirectory />} />
<Route path="/creators/:username" element={<CreatorProfile />} /> <Route
path="/creators/:username"
element={<CreatorProfile />}
/>
<Route path="/opportunities" element={<OpportunitiesHub />} /> <Route path="/opportunities" element={<OpportunitiesHub />} />
<Route path="/opportunities/:id" element={<OpportunityDetail />} /> <Route
path="/opportunities/:id"
element={<OpportunityDetail />}
/>
{/* Service routes */} {/* Service routes */}
<Route path="/game-development" element={<GameDevelopment />} /> <Route path="/game-development" element={<GameDevelopment />} />

View file

@ -63,7 +63,7 @@ export async function getMyApplications(filters?: {
export async function getApplicationsForOpportunity(opportunityId: string) { export async function getApplicationsForOpportunity(opportunityId: string) {
const response = await fetch( const response = await fetch(
`${API_BASE}/api/opportunities/${opportunityId}/applications` `${API_BASE}/api/opportunities/${opportunityId}/applications`,
); );
if (!response.ok) throw new Error("Failed to fetch applications"); if (!response.ok) throw new Error("Failed to fetch applications");
const data = await response.json(); const data = await response.json();
@ -88,20 +88,28 @@ export async function updateApplicationStatus(
data: { data: {
status: "reviewing" | "accepted" | "rejected"; status: "reviewing" | "accepted" | "rejected";
response_message?: string; response_message?: string;
} },
): Promise<Application> { ): Promise<Application> {
const response = await fetch(`${API_BASE}/api/applications/${applicationId}/status`, { const response = await fetch(
`${API_BASE}/api/applications/${applicationId}/status`,
{
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(data), body: JSON.stringify(data),
}); },
);
if (!response.ok) throw new Error("Failed to update application"); if (!response.ok) throw new Error("Failed to update application");
return response.json(); return response.json();
} }
export async function withdrawApplication(applicationId: string): Promise<void> { export async function withdrawApplication(
const response = await fetch(`${API_BASE}/api/applications/${applicationId}`, { applicationId: string,
): Promise<void> {
const response = await fetch(
`${API_BASE}/api/applications/${applicationId}`,
{
method: "DELETE", method: "DELETE",
}); },
);
if (!response.ok) throw new Error("Failed to withdraw application"); if (!response.ok) throw new Error("Failed to withdraw application");
} }

View file

@ -130,35 +130,50 @@ export async function addProject(data: {
return response.json(); return response.json();
} }
export async function updateProject(projectId: string, data: { export async function updateProject(
projectId: string,
data: {
title?: string; title?: string;
description?: string; description?: string;
url?: string; url?: string;
image_url?: string; image_url?: string;
tags?: string[]; tags?: string[];
is_featured?: boolean; is_featured?: boolean;
}): Promise<Project> { },
const response = await fetch(`${API_BASE}/api/creators/me/projects/${projectId}`, { ): Promise<Project> {
const response = await fetch(
`${API_BASE}/api/creators/me/projects/${projectId}`,
{
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(data), body: JSON.stringify(data),
}); },
);
if (!response.ok) throw new Error("Failed to update project"); if (!response.ok) throw new Error("Failed to update project");
return response.json(); return response.json();
} }
export async function deleteProject(projectId: string): Promise<void> { export async function deleteProject(projectId: string): Promise<void> {
const response = await fetch(`${API_BASE}/api/creators/me/projects/${projectId}`, { const response = await fetch(
`${API_BASE}/api/creators/me/projects/${projectId}`,
{
method: "DELETE", method: "DELETE",
}); },
);
if (!response.ok) throw new Error("Failed to delete project"); if (!response.ok) throw new Error("Failed to delete project");
} }
export async function endorseSkill(creatorId: string, skill: string): Promise<void> { export async function endorseSkill(
const response = await fetch(`${API_BASE}/api/creators/${creatorId}/endorse`, { creatorId: string,
skill: string,
): Promise<void> {
const response = await fetch(
`${API_BASE}/api/creators/${creatorId}/endorse`,
{
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ skill }), body: JSON.stringify({ skill }),
}); },
);
if (!response.ok) throw new Error("Failed to endorse skill"); if (!response.ok) throw new Error("Failed to endorse skill");
} }

View file

@ -36,7 +36,9 @@ export async function unlinkDevConnectAccount(): Promise<void> {
if (!response.ok) throw new Error("Failed to unlink DevConnect account"); if (!response.ok) throw new Error("Failed to unlink DevConnect account");
} }
export async function verifyDevConnectLink(verificationCode: string): Promise<DevConnectLink> { export async function verifyDevConnectLink(
verificationCode: string,
): Promise<DevConnectLink> {
const response = await fetch(`${API_BASE}/api/devconnect/link/verify`, { const response = await fetch(`${API_BASE}/api/devconnect/link/verify`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },

View file

@ -57,7 +57,8 @@ export async function getOpportunities(filters?: {
if (filters?.arm) params.append("arm", filters.arm); if (filters?.arm) params.append("arm", filters.arm);
if (filters?.search) params.append("search", filters.search); if (filters?.search) params.append("search", filters.search);
if (filters?.jobType) params.append("jobType", filters.jobType); if (filters?.jobType) params.append("jobType", filters.jobType);
if (filters?.experienceLevel) params.append("experienceLevel", filters.experienceLevel); if (filters?.experienceLevel)
params.append("experienceLevel", filters.experienceLevel);
if (filters?.sort) params.append("sort", filters.sort); if (filters?.sort) params.append("sort", filters.sort);
if (filters?.page) params.append("page", String(filters.page)); if (filters?.page) params.append("page", String(filters.page));
if (filters?.limit) params.append("limit", String(filters.limit)); if (filters?.limit) params.append("limit", String(filters.limit));
@ -73,7 +74,9 @@ export async function getOpportunityById(id: string): Promise<Opportunity> {
return response.json(); return response.json();
} }
export async function createOpportunity(data: CreateOpportunityData): Promise<Opportunity> { export async function createOpportunity(
data: CreateOpportunityData,
): Promise<Opportunity> {
const response = await fetch(`${API_BASE}/api/opportunities`, { const response = await fetch(`${API_BASE}/api/opportunities`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@ -85,7 +88,7 @@ export async function createOpportunity(data: CreateOpportunityData): Promise<Op
export async function updateOpportunity( export async function updateOpportunity(
id: string, id: string,
data: Partial<CreateOpportunityData> data: Partial<CreateOpportunityData>,
): Promise<Opportunity> { ): Promise<Opportunity> {
const response = await fetch(`${API_BASE}/api/opportunities/${id}`, { const response = await fetch(`${API_BASE}/api/opportunities/${id}`, {
method: "PUT", method: "PUT",
@ -104,7 +107,9 @@ export async function closeOpportunity(id: string): Promise<void> {
} }
export async function getApplicationsForOpportunity(opportunityId: string) { export async function getApplicationsForOpportunity(opportunityId: string) {
const response = await fetch(`${API_BASE}/api/opportunities/${opportunityId}/applications`); const response = await fetch(
`${API_BASE}/api/opportunities/${opportunityId}/applications`,
);
if (!response.ok) throw new Error("Failed to fetch applications"); if (!response.ok) throw new Error("Failed to fetch applications");
return response.json(); return response.json();
} }

View file

@ -35,7 +35,8 @@ export interface ArmBadgeProps {
export function ArmBadge({ arm, size = "md" }: ArmBadgeProps) { export function ArmBadge({ arm, size = "md" }: ArmBadgeProps) {
const colors = ARM_COLORS[arm.toLowerCase()] || ARM_COLORS.labs; const colors = ARM_COLORS[arm.toLowerCase()] || ARM_COLORS.labs;
const sizeClass = size === "sm" ? "text-xs" : size === "lg" ? "text-base" : "text-sm"; const sizeClass =
size === "sm" ? "text-xs" : size === "lg" ? "text-base" : "text-sm";
return ( return (
<Badge className={`${colors.bg} border-0 ${colors.text} ${sizeClass}`}> <Badge className={`${colors.bg} border-0 ${colors.text} ${sizeClass}`}>

View file

@ -19,7 +19,9 @@ export function CreatorCard({ creator }: CreatorCardProps) {
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-4">
<Avatar className="h-16 w-16"> <Avatar className="h-16 w-16">
<AvatarImage src={creator.avatar_url} alt={creator.username} /> <AvatarImage src={creator.avatar_url} alt={creator.username} />
<AvatarFallback>{creator.username.charAt(0).toUpperCase()}</AvatarFallback> <AvatarFallback>
{creator.username.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar> </Avatar>
{creator.devconnect_linked && ( {creator.devconnect_linked && (
<div className="text-xs bg-cyan-500/10 text-cyan-300 px-2 py-1 rounded-full border border-cyan-500/20"> <div className="text-xs bg-cyan-500/10 text-cyan-300 px-2 py-1 rounded-full border border-cyan-500/20">

View file

@ -47,8 +47,10 @@ export function DevConnectLinkModal({
onSuccess?.(); onSuccess?.();
} catch (error) { } catch (error) {
toast( toast(
error instanceof Error ? error.message : "Failed to link DevConnect account", error instanceof Error
"error" ? error.message
: "Failed to link DevConnect account",
"error",
); );
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@ -61,7 +63,8 @@ export function DevConnectLinkModal({
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Link Your DevConnect Account</AlertDialogTitle> <AlertDialogTitle>Link Your DevConnect Account</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Enter your DevConnect username to link your profile. This allows you to showcase your presence on both platforms. Enter your DevConnect username to link your profile. This allows you
to showcase your presence on both platforms.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
@ -79,7 +82,8 @@ export function DevConnectLinkModal({
/> />
</div> </div>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
Your username will be used to create a link to your DevConnect profile Your username will be used to create a link to your DevConnect
profile
</p> </p>
</div> </div>
</div> </div>

View file

@ -29,7 +29,8 @@ export function OpportunityCard({ opportunity }: OpportunityCardProps) {
const formatSalary = (min?: number, max?: number) => { const formatSalary = (min?: number, max?: number) => {
if (!min && !max) return "Not specified"; if (!min && !max) return "Not specified";
if (min && max) return `$${min.toLocaleString()} - $${max.toLocaleString()}`; if (min && max)
return `$${min.toLocaleString()} - $${max.toLocaleString()}`;
if (min) return `$${min.toLocaleString()}+`; if (min) return `$${min.toLocaleString()}+`;
if (max) return `Up to $${max.toLocaleString()}`; if (max) return `Up to $${max.toLocaleString()}`;
}; };
@ -41,11 +42,15 @@ export function OpportunityCard({ opportunity }: OpportunityCardProps) {
<div className="flex items-start gap-3 flex-1"> <div className="flex items-start gap-3 flex-1">
<Avatar className="h-12 w-12"> <Avatar className="h-12 w-12">
<AvatarImage src={poster.avatar_url} alt={poster.username} /> <AvatarImage src={poster.avatar_url} alt={poster.username} />
<AvatarFallback>{poster.username.charAt(0).toUpperCase()}</AvatarFallback> <AvatarFallback>
{poster.username.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar> </Avatar>
<div> <div>
<p className="text-xs text-gray-400">Posted by</p> <p className="text-xs text-gray-400">Posted by</p>
<p className="text-sm font-semibold text-white">@{poster.username}</p> <p className="text-sm font-semibold text-white">
@{poster.username}
</p>
</div> </div>
</div> </div>
<div className="text-xs text-gray-400 whitespace-nowrap"> <div className="text-xs text-gray-400 whitespace-nowrap">
@ -86,7 +91,9 @@ export function OpportunityCard({ opportunity }: OpportunityCardProps) {
<div className="flex items-center text-gray-400"> <div className="flex items-center text-gray-400">
<Clock className="h-4 w-4 mr-2 text-blue-400" /> <Clock className="h-4 w-4 mr-2 text-blue-400" />
{opportunity.aethex_applications.count}{" "} {opportunity.aethex_applications.count}{" "}
{opportunity.aethex_applications.count === 1 ? "applicant" : "applicants"} {opportunity.aethex_applications.count === 1
? "applicant"
: "applicants"}
</div> </div>
)} )}
</div> </div>

View file

@ -15,10 +15,34 @@ interface CreatorProfileProps {
} }
const AVAILABLE_ARMS = [ const AVAILABLE_ARMS = [
{ id: "labs", name: "Labs", description: "R&D & Innovation", color: "from-yellow-500 to-orange-500", icon: Zap }, {
{ id: "gameforge", name: "GameForge", description: "Game Development", color: "from-green-500 to-emerald-500", icon: Code }, id: "labs",
{ id: "corp", name: "Corp", description: "Enterprise Consulting", color: "from-blue-500 to-cyan-500", icon: Users }, name: "Labs",
{ id: "foundation", name: "Foundation", description: "Education & Open Source", color: "from-red-500 to-pink-500", icon: Users }, description: "R&D & Innovation",
color: "from-yellow-500 to-orange-500",
icon: Zap,
},
{
id: "gameforge",
name: "GameForge",
description: "Game Development",
color: "from-green-500 to-emerald-500",
icon: Code,
},
{
id: "corp",
name: "Corp",
description: "Enterprise Consulting",
color: "from-blue-500 to-cyan-500",
icon: Users,
},
{
id: "foundation",
name: "Foundation",
description: "Education & Open Source",
color: "from-red-500 to-pink-500",
icon: Users,
},
]; ];
const SKILL_SUGGESTIONS = [ const SKILL_SUGGESTIONS = [
@ -48,7 +72,11 @@ export default function CreatorProfile({
totalSteps, totalSteps,
}: CreatorProfileProps) { }: CreatorProfileProps) {
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const creatorData = data.creatorProfile || { bio: "", skills: [], primaryArm: "" }; const creatorData = data.creatorProfile || {
bio: "",
skills: [],
primaryArm: "",
};
const canProceed = useMemo(() => { const canProceed = useMemo(() => {
return creatorData.primaryArm && creatorData.skills.length > 0; return creatorData.primaryArm && creatorData.skills.length > 0;
@ -138,7 +166,9 @@ export default function CreatorProfile({
</div> </div>
<div> <div>
<div className="font-semibold text-white">{arm.name}</div> <div className="font-semibold text-white">{arm.name}</div>
<div className="text-xs text-gray-400">{arm.description}</div> <div className="text-xs text-gray-400">
{arm.description}
</div>
</div> </div>
</div> </div>
</button> </button>
@ -150,7 +180,10 @@ export default function CreatorProfile({
{/* Skills Selection */} {/* Skills Selection */}
<div className="space-y-4"> <div className="space-y-4">
<label className="text-sm font-semibold text-gray-200 block"> <label className="text-sm font-semibold text-gray-200 block">
What are your skills? * <span className="text-xs text-gray-400 font-normal">(Add at least 1)</span> What are your skills? *{" "}
<span className="text-xs text-gray-400 font-normal">
(Add at least 1)
</span>
</label> </label>
{/* Skills Input */} {/* Skills Input */}
@ -221,11 +254,7 @@ export default function CreatorProfile({
{/* Navigation */} {/* Navigation */}
<div className="flex gap-3 pt-8 border-t border-gray-800"> <div className="flex gap-3 pt-8 border-t border-gray-800">
<Button <Button onClick={prevStep} variant="outline" className="flex-1">
onClick={prevStep}
variant="outline"
className="flex-1"
>
Back Back
</Button> </Button>
<Button <Button

View file

@ -1,7 +1,16 @@
import Layout from "@/components/Layout"; import Layout from "@/components/Layout";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Beaker, Briefcase, Heart, Network, Lock, TrendingUp, Users, Shield } from "lucide-react"; import {
Beaker,
Briefcase,
Heart,
Network,
Lock,
TrendingUp,
Users,
Shield,
} from "lucide-react";
export default function About() { export default function About() {
const pillars = [ const pillars = [
@ -11,9 +20,16 @@ export default function About() {
color: "from-yellow-500 to-orange-500", color: "from-yellow-500 to-orange-500",
icon: Beaker, icon: Beaker,
tagline: "The Innovation Engine", tagline: "The Innovation Engine",
description: "Bleeding-edge R&D focused on proprietary technology and thought leadership", description:
focus: ["Advanced AI & Machine Learning", "Next-gen Web Architectures", "Procedural Content Generation", "Graphics & Performance Optimization"], "Bleeding-edge R&D focused on proprietary technology and thought leadership",
function: "High-burn speculative research creating lasting competitive advantage", focus: [
"Advanced AI & Machine Learning",
"Next-gen Web Architectures",
"Procedural Content Generation",
"Graphics & Performance Optimization",
],
function:
"High-burn speculative research creating lasting competitive advantage",
}, },
{ {
id: "corp", id: "corp",
@ -21,9 +37,16 @@ export default function About() {
color: "from-blue-500 to-cyan-500", color: "from-blue-500 to-cyan-500",
icon: Briefcase, icon: Briefcase,
tagline: "The Profit Engine", tagline: "The Profit Engine",
description: "High-margin consulting and enterprise solutions subsidizing R&D investment", description:
focus: ["Custom Software Development", "Enterprise Consulting", "Game Development Services", "Digital Transformation"], "High-margin consulting and enterprise solutions subsidizing R&D investment",
function: "Stable revenue generation and financial discipline for investor confidence", focus: [
"Custom Software Development",
"Enterprise Consulting",
"Game Development Services",
"Digital Transformation",
],
function:
"Stable revenue generation and financial discipline for investor confidence",
}, },
{ {
id: "foundation", id: "foundation",
@ -31,9 +54,16 @@ export default function About() {
color: "from-red-500 to-pink-500", color: "from-red-500 to-pink-500",
icon: Heart, icon: Heart,
tagline: "Community & Education", tagline: "Community & Education",
description: "Open-source and educational mission building goodwill and talent pipeline", description:
focus: ["Open-Source Code Repositories", "Educational Curriculum", "Community Workshops", "Talent Development"], "Open-source and educational mission building goodwill and talent pipeline",
function: "Goodwill moat, specialized talent pipeline, and ecosystem growth", focus: [
"Open-Source Code Repositories",
"Educational Curriculum",
"Community Workshops",
"Talent Development",
],
function:
"Goodwill moat, specialized talent pipeline, and ecosystem growth",
}, },
{ {
id: "devlink", id: "devlink",
@ -41,28 +71,38 @@ export default function About() {
color: "from-cyan-500 to-blue-500", color: "from-cyan-500 to-blue-500",
icon: Network, icon: Network,
tagline: "Talent Network", tagline: "Talent Network",
description: "B2B SaaS platform for specialized professional networking and recruitment", description:
focus: ["Recruitment Platform", "Talent Matching", "Professional Network", "Career Development"], "B2B SaaS platform for specialized professional networking and recruitment",
function: "Access-based moat and lock-in effect for specialized human capital", focus: [
"Recruitment Platform",
"Talent Matching",
"Professional Network",
"Career Development",
],
function:
"Access-based moat and lock-in effect for specialized human capital",
}, },
]; ];
const moats = [ const moats = [
{ {
title: "Technological Moat", title: "Technological Moat",
description: "Labs creates proprietary AI and architectural innovations licensed to Corp and Dev-Link", description:
"Labs creates proprietary AI and architectural innovations licensed to Corp and Dev-Link",
icon: Shield, icon: Shield,
color: "bg-yellow-500/20 border-yellow-500/40 text-yellow-300", color: "bg-yellow-500/20 border-yellow-500/40 text-yellow-300",
}, },
{ {
title: "Talent Moat", title: "Talent Moat",
description: "Dev-Link and Foundation establish curated access to specialized immersive developers", description:
"Dev-Link and Foundation establish curated access to specialized immersive developers",
icon: Users, icon: Users,
color: "bg-cyan-500/20 border-cyan-500/40 text-cyan-300", color: "bg-cyan-500/20 border-cyan-500/40 text-cyan-300",
}, },
{ {
title: "Community Moat", title: "Community Moat",
description: "Foundation builds trust and goodwill through open-source and educational mission", description:
"Foundation builds trust and goodwill through open-source and educational mission",
icon: Heart, icon: Heart,
color: "bg-red-500/20 border-red-500/40 text-red-300", color: "bg-red-500/20 border-red-500/40 text-red-300",
}, },
@ -71,22 +111,26 @@ export default function About() {
const benefits = [ const benefits = [
{ {
title: "Risk Segregation", title: "Risk Segregation",
description: "Labs' high-burn R&D is isolated from operational liabilities, protecting core assets", description:
"Labs' high-burn R&D is isolated from operational liabilities, protecting core assets",
icon: Lock, icon: Lock,
}, },
{ {
title: "Tax Optimization", title: "Tax Optimization",
description: "Corp and Dev-Link as profit centers, Foundation as 501(c)(3), optimizing UBIT exposure", description:
"Corp and Dev-Link as profit centers, Foundation as 501(c)(3), optimizing UBIT exposure",
icon: TrendingUp, icon: TrendingUp,
}, },
{ {
title: "Investor Confidence", title: "Investor Confidence",
description: "Low burn multiples demonstrated through profitable Corp operations subsidizing R&D", description:
"Low burn multiples demonstrated through profitable Corp operations subsidizing R&D",
icon: TrendingUp, icon: TrendingUp,
}, },
{ {
title: "IP Centralization", title: "IP Centralization",
description: "Labs acts as IP holding company with licensing to subsidiaries via transfer pricing", description:
"Labs acts as IP holding company with licensing to subsidiaries via transfer pricing",
icon: Shield, icon: Shield,
}, },
]; ];
@ -98,10 +142,17 @@ export default function About() {
<section className="py-16 lg:py-24 border-b border-gray-800"> <section className="py-16 lg:py-24 border-b border-gray-800">
<div className="container mx-auto max-w-6xl px-4"> <div className="container mx-auto max-w-6xl px-4">
<h1 className="text-5xl lg:text-7xl font-black mb-6"> <h1 className="text-5xl lg:text-7xl font-black mb-6">
Building an Integrated <span className="bg-gradient-to-r from-yellow-300 via-blue-300 to-red-300 bg-clip-text text-transparent">Ecosystem</span> Building an Integrated{" "}
<span className="bg-gradient-to-r from-yellow-300 via-blue-300 to-red-300 bg-clip-text text-transparent">
Ecosystem
</span>
</h1> </h1>
<p className="text-xl text-gray-300 max-w-3xl"> <p className="text-xl text-gray-300 max-w-3xl">
AeThex operates as a unified four-pillar organization that combines speculative innovation, profitable operations, community impact, and specialized talent acquisition. This structure creates multiple reinforcing competitive moats while managing risk and maintaining investor confidence. AeThex operates as a unified four-pillar organization that
combines speculative innovation, profitable operations, community
impact, and specialized talent acquisition. This structure creates
multiple reinforcing competitive moats while managing risk and
maintaining investor confidence.
</p> </p>
</div> </div>
</section> </section>
@ -114,29 +165,44 @@ export default function About() {
{pillars.map((pillar) => { {pillars.map((pillar) => {
const Icon = pillar.icon; const Icon = pillar.icon;
return ( return (
<Card key={pillar.id} className="bg-gray-900/50 border-gray-800 hover:border-gray-700 transition-all"> <Card
key={pillar.id}
className="bg-gray-900/50 border-gray-800 hover:border-gray-700 transition-all"
>
<CardHeader> <CardHeader>
<div className={`w-12 h-12 rounded-lg bg-gradient-to-r ${pillar.color} flex items-center justify-center mb-4`}> <div
className={`w-12 h-12 rounded-lg bg-gradient-to-r ${pillar.color} flex items-center justify-center mb-4`}
>
<Icon className="h-6 w-6 text-white" /> <Icon className="h-6 w-6 text-white" />
</div> </div>
<CardTitle> <CardTitle>
<div className="text-2xl font-black">{pillar.name}</div> <div className="text-2xl font-black">{pillar.name}</div>
<div className="text-sm font-normal text-gray-400 mt-1">{pillar.tagline}</div> <div className="text-sm font-normal text-gray-400 mt-1">
{pillar.tagline}
</div>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<p className="text-gray-300">{pillar.description}</p> <p className="text-gray-300">{pillar.description}</p>
<div> <div>
<p className="text-xs font-semibold text-gray-400 mb-2">KEY AREAS</p> <p className="text-xs font-semibold text-gray-400 mb-2">
KEY AREAS
</p>
<ul className="space-y-1"> <ul className="space-y-1">
{pillar.focus.map((item, idx) => ( {pillar.focus.map((item, idx) => (
<li key={idx} className="text-sm text-gray-400"> {item}</li> <li key={idx} className="text-sm text-gray-400">
{item}
</li>
))} ))}
</ul> </ul>
</div> </div>
<div className="pt-4 border-t border-gray-800"> <div className="pt-4 border-t border-gray-800">
<p className="text-xs font-semibold text-gray-400 mb-2">STRATEGIC FUNCTION</p> <p className="text-xs font-semibold text-gray-400 mb-2">
<p className="text-sm text-gray-300">{pillar.function}</p> STRATEGIC FUNCTION
</p>
<p className="text-sm text-gray-300">
{pillar.function}
</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -149,21 +215,30 @@ export default function About() {
{/* Competitive Moats */} {/* Competitive Moats */}
<section className="py-16 border-b border-gray-800"> <section className="py-16 border-b border-gray-800">
<div className="container mx-auto max-w-6xl px-4"> <div className="container mx-auto max-w-6xl px-4">
<h2 className="text-3xl font-bold mb-6">Integrated Competitive Moats</h2> <h2 className="text-3xl font-bold mb-6">
Integrated Competitive Moats
</h2>
<p className="text-gray-300 mb-12 max-w-3xl"> <p className="text-gray-300 mb-12 max-w-3xl">
While competitors can replicate technology, the AeThex structure creates multiple, mutually-reinforcing barriers to entry that are far more resilient and defensible. While competitors can replicate technology, the AeThex structure
creates multiple, mutually-reinforcing barriers to entry that are
far more resilient and defensible.
</p> </p>
<div className="grid md:grid-cols-3 gap-6"> <div className="grid md:grid-cols-3 gap-6">
{moats.map((moat, idx) => { {moats.map((moat, idx) => {
const Icon = moat.icon; const Icon = moat.icon;
return ( return (
<Card key={idx} className={`border ${moat.color.split(" ")[1]} ${moat.color.split(" ")[0]} bg-black/40`}> <Card
key={idx}
className={`border ${moat.color.split(" ")[1]} ${moat.color.split(" ")[0]} bg-black/40`}
>
<CardHeader> <CardHeader>
<Icon className="h-8 w-8 mb-4" /> <Icon className="h-8 w-8 mb-4" />
<CardTitle className="text-lg">{moat.title}</CardTitle> <CardTitle className="text-lg">{moat.title}</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-sm text-gray-300">{moat.description}</p> <p className="text-sm text-gray-300">
{moat.description}
</p>
</CardContent> </CardContent>
</Card> </Card>
); );
@ -185,8 +260,12 @@ export default function About() {
<div className="flex gap-4"> <div className="flex gap-4">
<Icon className="h-6 w-6 text-blue-400 flex-shrink-0" /> <Icon className="h-6 w-6 text-blue-400 flex-shrink-0" />
<div> <div>
<h3 className="font-semibold mb-2">{benefit.title}</h3> <h3 className="font-semibold mb-2">
<p className="text-sm text-gray-400">{benefit.description}</p> {benefit.title}
</h3>
<p className="text-sm text-gray-400">
{benefit.description}
</p>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@ -208,9 +287,14 @@ export default function About() {
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<p className="text-sm text-gray-300"> <p className="text-sm text-gray-300">
Benefit Corporation status legally protects our dual mission of profit and public benefit, enabling strategic investment in research and community regardless of leadership or capital changes. Benefit Corporation status legally protects our dual mission
of profit and public benefit, enabling strategic investment
in research and community regardless of leadership or
capital changes.
</p> </p>
<Badge className="bg-blue-500/20 text-blue-300">Benefit Corp</Badge> <Badge className="bg-blue-500/20 text-blue-300">
Benefit Corp
</Badge>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gray-900/50 border-gray-800"> <Card className="bg-gray-900/50 border-gray-800">
@ -219,9 +303,14 @@ export default function About() {
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<p className="text-sm text-gray-300"> <p className="text-sm text-gray-300">
Wholly-owned C-Corporation subsidiaries with separate boards, bank accounts, and intercompany agreements maintain the corporate veil and protect parent assets from operational liabilities. Wholly-owned C-Corporation subsidiaries with separate
boards, bank accounts, and intercompany agreements maintain
the corporate veil and protect parent assets from
operational liabilities.
</p> </p>
<Badge className="bg-yellow-500/20 text-yellow-300">Risk Segregation</Badge> <Badge className="bg-yellow-500/20 text-yellow-300">
Risk Segregation
</Badge>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gray-900/50 border-gray-800"> <Card className="bg-gray-900/50 border-gray-800">
@ -230,9 +319,14 @@ export default function About() {
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<p className="text-sm text-gray-300"> <p className="text-sm text-gray-300">
Labs acts as IP Holding Company with formal licensing agreements to Corp and Dev-Link at Fair Market Value, enabling tax-efficient transfer pricing and centralized IP control. Labs acts as IP Holding Company with formal licensing
agreements to Corp and Dev-Link at Fair Market Value,
enabling tax-efficient transfer pricing and centralized IP
control.
</p> </p>
<Badge className="bg-green-500/20 text-green-300">IPCo Strategy</Badge> <Badge className="bg-green-500/20 text-green-300">
IPCo Strategy
</Badge>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gray-900/50 border-gray-800"> <Card className="bg-gray-900/50 border-gray-800">
@ -241,9 +335,13 @@ export default function About() {
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<p className="text-sm text-gray-300"> <p className="text-sm text-gray-300">
Independent board majority, Fund Accounting, and related-party transaction safeguards prevent private inurement and maintain 501(c)(3) tax-exempt status. Independent board majority, Fund Accounting, and
related-party transaction safeguards prevent private
inurement and maintain 501(c)(3) tax-exempt status.
</p> </p>
<Badge className="bg-red-500/20 text-red-300">Compliance First</Badge> <Badge className="bg-red-500/20 text-red-300">
Compliance First
</Badge>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View file

@ -1154,29 +1154,56 @@ export default function Admin() {
<Lightbulb className="h-5 w-5 text-yellow-400" /> <Lightbulb className="h-5 w-5 text-yellow-400" />
<CardTitle>Labs - Research & Innovation</CardTitle> <CardTitle>Labs - Research & Innovation</CardTitle>
</div> </div>
<CardDescription>Research initiatives, publications, and innovation pipeline</CardDescription> <CardDescription>
Research initiatives, publications, and innovation
pipeline
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-3"> <div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Active Research Projects</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-yellow-400">12</p> Active Research Projects
<p className="text-xs text-gray-400 mt-2"> 3 new this quarter</p> </p>
<p className="text-2xl font-bold text-yellow-400">
12
</p>
<p className="text-xs text-gray-400 mt-2">
3 new this quarter
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Publications</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-yellow-400">8</p> Publications
<p className="text-xs text-gray-400 mt-2">Peer-reviewed papers</p> </p>
<p className="text-2xl font-bold text-yellow-400">
8
</p>
<p className="text-xs text-gray-400 mt-2">
Peer-reviewed papers
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Research Team Size</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-yellow-400">24</p> Research Team Size
<p className="text-xs text-gray-400 mt-2">PhD & researchers</p> </p>
<p className="text-2xl font-bold text-yellow-400">
24
</p>
<p className="text-xs text-gray-400 mt-2">
PhD & researchers
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Impact Citations</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-yellow-400">342</p> Impact Citations
<p className="text-xs text-gray-400 mt-2">Academic references</p> </p>
<p className="text-2xl font-bold text-yellow-400">
342
</p>
<p className="text-xs text-gray-400 mt-2">
Academic references
</p>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@ -1190,29 +1217,55 @@ export default function Admin() {
<Zap className="h-5 w-5 text-green-400" /> <Zap className="h-5 w-5 text-green-400" />
<CardTitle>GameForge - Game Development</CardTitle> <CardTitle>GameForge - Game Development</CardTitle>
</div> </div>
<CardDescription>Monthly shipping cycles and game production metrics</CardDescription> <CardDescription>
Monthly shipping cycles and game production metrics
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-3"> <div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Games in Production</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-green-400">18</p> Games in Production
<p className="text-xs text-gray-400 mt-2">Active development</p> </p>
<p className="text-2xl font-bold text-green-400">
18
</p>
<p className="text-xs text-gray-400 mt-2">
Active development
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Games Shipped</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-green-400">45</p> Games Shipped
<p className="text-xs text-gray-400 mt-2">This year</p> </p>
<p className="text-2xl font-bold text-green-400">
45
</p>
<p className="text-xs text-gray-400 mt-2">
This year
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Player Base</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-green-400">2.8M</p> Player Base
<p className="text-xs text-gray-400 mt-2">Monthly active</p> </p>
<p className="text-2xl font-bold text-green-400">
2.8M
</p>
<p className="text-xs text-gray-400 mt-2">
Monthly active
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Avg Shipping Velocity</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-green-400">3.2x</p> Avg Shipping Velocity
<p className="text-xs text-gray-400 mt-2">vs industry standard</p> </p>
<p className="text-2xl font-bold text-green-400">
3.2x
</p>
<p className="text-xs text-gray-400 mt-2">
vs industry standard
</p>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@ -1226,29 +1279,55 @@ export default function Admin() {
<Briefcase className="h-5 w-5 text-blue-400" /> <Briefcase className="h-5 w-5 text-blue-400" />
<CardTitle>Corp - Enterprise Services</CardTitle> <CardTitle>Corp - Enterprise Services</CardTitle>
</div> </div>
<CardDescription>Enterprise consulting and business solutions</CardDescription> <CardDescription>
Enterprise consulting and business solutions
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-3"> <div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Active Clients</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-blue-400">34</p> Active Clients
<p className="text-xs text-gray-400 mt-2">Enterprise accounts</p> </p>
<p className="text-2xl font-bold text-blue-400">
34
</p>
<p className="text-xs text-gray-400 mt-2">
Enterprise accounts
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">ARR</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-blue-400">$4.2M</p> ARR
<p className="text-xs text-gray-400 mt-2">Annual recurring</p> </p>
<p className="text-2xl font-bold text-blue-400">
$4.2M
</p>
<p className="text-xs text-gray-400 mt-2">
Annual recurring
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Retention Rate</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-blue-400">94%</p> Retention Rate
<p className="text-xs text-gray-400 mt-2">12-month</p> </p>
<p className="text-2xl font-bold text-blue-400">
94%
</p>
<p className="text-xs text-gray-400 mt-2">
12-month
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Consulting Team</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-blue-400">16</p> Consulting Team
<p className="text-xs text-gray-400 mt-2">Senior consultants</p> </p>
<p className="text-2xl font-bold text-blue-400">
16
</p>
<p className="text-xs text-gray-400 mt-2">
Senior consultants
</p>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@ -1260,51 +1339,93 @@ export default function Admin() {
<CardHeader> <CardHeader>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Heart className="h-5 w-5 text-red-400" /> <Heart className="h-5 w-5 text-red-400" />
<CardTitle>Foundation - Education & Community</CardTitle> <CardTitle>
Foundation - Education & Community
</CardTitle>
</div> </div>
<CardDescription>Educational impact and talent pipeline effectiveness</CardDescription> <CardDescription>
Educational impact and talent pipeline effectiveness
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h4 className="font-semibold text-sm mb-3">Student Metrics</h4> <h4 className="font-semibold text-sm mb-3">
Student Metrics
</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3"> <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Course Completion</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-green-400">87.5%</p> Course Completion
<p className="text-xs text-gray-400 mt-2"> +12% YoY</p> </p>
<p className="text-2xl font-bold text-green-400">
87.5%
</p>
<p className="text-xs text-gray-400 mt-2">
+12% YoY
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Active Learners</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-blue-400">342</p> Active Learners
<p className="text-xs text-gray-400 mt-2"> +28 new</p> </p>
<p className="text-2xl font-bold text-blue-400">
342
</p>
<p className="text-xs text-gray-400 mt-2">
+28 new
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Placement Rate</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-yellow-400">42%</p> Placement Rate
<p className="text-xs text-gray-400 mt-2">54 hired</p> </p>
<p className="text-2xl font-bold text-yellow-400">
42%
</p>
<p className="text-xs text-gray-400 mt-2">
54 hired
</p>
</div> </div>
</div> </div>
</div> </div>
<div> <div>
<h4 className="font-semibold text-sm mb-3">Open Source Impact</h4> <h4 className="font-semibold text-sm mb-3">
Open Source Impact
</h4>
<div className="grid grid-cols-1 md:grid-cols-4 gap-3"> <div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<div className="bg-background/40 rounded-lg p-3 border border-border/40"> <div className="bg-background/40 rounded-lg p-3 border border-border/40">
<p className="text-xs text-gray-400 mb-1">GitHub Forks</p> <p className="text-xs text-gray-400 mb-1">
<p className="text-xl font-bold text-purple-400">2.3K</p> GitHub Forks
</p>
<p className="text-xl font-bold text-purple-400">
2.3K
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-3 border border-border/40"> <div className="bg-background/40 rounded-lg p-3 border border-border/40">
<p className="text-xs text-gray-400 mb-1">PRs</p> <p className="text-xs text-gray-400 mb-1">
<p className="text-xl font-bold text-green-400">184</p> PRs
</p>
<p className="text-xl font-bold text-green-400">
184
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-3 border border-border/40"> <div className="bg-background/40 rounded-lg p-3 border border-border/40">
<p className="text-xs text-gray-400 mb-1">External Usage</p> <p className="text-xs text-gray-400 mb-1">
<p className="text-xl font-bold text-blue-400">452</p> External Usage
</p>
<p className="text-xl font-bold text-blue-400">
452
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-3 border border-border/40"> <div className="bg-background/40 rounded-lg p-3 border border-border/40">
<p className="text-xs text-gray-400 mb-1">Contributors</p> <p className="text-xs text-gray-400 mb-1">
<p className="text-xl font-bold text-cyan-400">67</p> Contributors
</p>
<p className="text-xl font-bold text-cyan-400">
67
</p>
</div> </div>
</div> </div>
</div> </div>
@ -1320,29 +1441,55 @@ export default function Admin() {
<Users className="h-5 w-5 text-purple-400" /> <Users className="h-5 w-5 text-purple-400" />
<CardTitle>Nexus - Talent Marketplace</CardTitle> <CardTitle>Nexus - Talent Marketplace</CardTitle>
</div> </div>
<CardDescription>Creator network and opportunity matching metrics</CardDescription> <CardDescription>
Creator network and opportunity matching metrics
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-3"> <div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Active Creators</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-purple-400">1,240</p> Active Creators
<p className="text-xs text-gray-400 mt-2"> +180 this month</p> </p>
<p className="text-2xl font-bold text-purple-400">
1,240
</p>
<p className="text-xs text-gray-400 mt-2">
+180 this month
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Open Opportunities</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-purple-400">342</p> Open Opportunities
<p className="text-xs text-gray-400 mt-2">Across all arms</p> </p>
<p className="text-2xl font-bold text-purple-400">
342
</p>
<p className="text-xs text-gray-400 mt-2">
Across all arms
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Successful Matches</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-purple-400">87</p> Successful Matches
<p className="text-xs text-gray-400 mt-2">This quarter</p> </p>
<p className="text-2xl font-bold text-purple-400">
87
</p>
<p className="text-xs text-gray-400 mt-2">
This quarter
</p>
</div> </div>
<div className="bg-background/40 rounded-lg p-4 border border-border/40"> <div className="bg-background/40 rounded-lg p-4 border border-border/40">
<p className="text-sm text-muted-foreground mb-2">Match Success Rate</p> <p className="text-sm text-muted-foreground mb-2">
<p className="text-2xl font-bold text-purple-400">68%</p> Match Success Rate
<p className="text-xs text-gray-400 mt-2">Application to hire</p> </p>
<p className="text-2xl font-bold text-purple-400">
68%
</p>
<p className="text-xs text-gray-400 mt-2">
Application to hire
</p>
</div> </div>
</div> </div>
</CardContent> </CardContent>

View file

@ -137,7 +137,11 @@ export default function Corp() {
The Profit Engine The Profit Engine
</h1> </h1>
<p className="text-xl text-blue-100/70 max-w-3xl"> <p className="text-xl text-blue-100/70 max-w-3xl">
AeThex Corp delivers high-margin enterprise consulting and specialized software development. We leverage proprietary technologies from Labs to create cutting-edge solutions while generating stable, benchmarkable revenue that funds our ambitious R&D roadmap. AeThex Corp delivers high-margin enterprise consulting and
specialized software development. We leverage proprietary
technologies from Labs to create cutting-edge solutions while
generating stable, benchmarkable revenue that funds our
ambitious R&D roadmap.
</p> </p>
</div> </div>
@ -160,7 +164,9 @@ export default function Corp() {
{/* Creator Network CTAs */} {/* Creator Network CTAs */}
<div className="mt-8 pt-8 border-t border-blue-400/20"> <div className="mt-8 pt-8 border-t border-blue-400/20">
<p className="text-sm text-blue-200/70 mb-4">Explore our creator community:</p> <p className="text-sm text-blue-200/70 mb-4">
Explore our creator community:
</p>
<div className="flex flex-col sm:flex-row gap-3"> <div className="flex flex-col sm:flex-row gap-3">
<Button <Button
size="sm" size="sm"
@ -221,7 +227,9 @@ export default function Corp() {
className="bg-blue-950/20 border-blue-400/30 hover:border-blue-400/60 transition-all" className="bg-blue-950/20 border-blue-400/30 hover:border-blue-400/60 transition-all"
> >
<CardHeader> <CardHeader>
<div className={`w-12 h-12 rounded-lg bg-gradient-to-r ${service.color} flex items-center justify-center text-white mb-4`}> <div
className={`w-12 h-12 rounded-lg bg-gradient-to-r ${service.color} flex items-center justify-center text-white mb-4`}
>
<Briefcase className="h-6 w-6" /> <Briefcase className="h-6 w-6" />
</div> </div>
<CardTitle className="text-blue-300"> <CardTitle className="text-blue-300">
@ -234,7 +242,10 @@ export default function Corp() {
</p> </p>
<ul className="space-y-2"> <ul className="space-y-2">
{service.examples.map((example, i) => ( {service.examples.map((example, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-blue-300"> <li
key={i}
className="flex items-center gap-2 text-sm text-blue-300"
>
<CheckCircle className="h-4 w-4 text-blue-400" /> <CheckCircle className="h-4 w-4 text-blue-400" />
{example} {example}
</li> </li>
@ -255,10 +266,7 @@ export default function Corp() {
</h2> </h2>
<div className="space-y-6"> <div className="space-y-6">
{recentWins.map((win, idx) => ( {recentWins.map((win, idx) => (
<Card <Card key={idx} className="bg-blue-950/20 border-blue-400/30">
key={idx}
className="bg-blue-950/20 border-blue-400/30"
>
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
<div> <div>
@ -326,10 +334,7 @@ export default function Corp() {
description: "Specialized developers for your team", description: "Specialized developers for your team",
}, },
].map((model, idx) => ( ].map((model, idx) => (
<Card <Card key={idx} className="bg-blue-950/30 border-blue-400/40">
key={idx}
className="bg-blue-950/30 border-blue-400/40"
>
<CardContent className="pt-6 text-center"> <CardContent className="pt-6 text-center">
<h3 className="font-bold text-blue-300 mb-2"> <h3 className="font-bold text-blue-300 mb-2">
{model.model} {model.model}
@ -351,7 +356,8 @@ export default function Corp() {
Ready to Partner? Ready to Partner?
</h2> </h2>
<p className="text-lg text-blue-100/80 mb-8"> <p className="text-lg text-blue-100/80 mb-8">
Let's discuss your business challenges and build a solution that drives results. Let's discuss your business challenges and build a solution that
drives results.
</p> </p>
<Button <Button
className="bg-blue-400 text-black shadow-[0_0_30px_rgba(59,130,246,0.35)] hover:bg-blue-300" className="bg-blue-400 text-black shadow-[0_0_30px_rgba(59,130,246,0.35)] hover:bg-blue-300"

View file

@ -137,7 +137,9 @@ export default function DevLink() {
{/* Creator Network CTAs */} {/* Creator Network CTAs */}
<div className="mt-8 pt-8 border-t border-cyan-400/20"> <div className="mt-8 pt-8 border-t border-cyan-400/20">
<p className="text-sm text-cyan-200/70 mb-4">Explore our creator community:</p> <p className="text-sm text-cyan-200/70 mb-4">
Explore our creator community:
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center"> <div className="flex flex-col sm:flex-row gap-3 justify-center">
<Button <Button
size="sm" size="sm"

View file

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from "react";
import { useDiscord } from '@/contexts/DiscordContext'; import { useDiscord } from "@/contexts/DiscordContext";
import LoadingScreen from '@/components/LoadingScreen'; import LoadingScreen from "@/components/LoadingScreen";
interface DiscordSDK { interface DiscordSDK {
ready: () => Promise<void>; ready: () => Promise<void>;
@ -22,7 +22,7 @@ export default function DiscordActivity() {
try { try {
// Discord SDK should be loaded by the script in index.html // Discord SDK should be loaded by the script in index.html
if (!(window as any).DiscordSDK) { if (!(window as any).DiscordSDK) {
throw new Error('Discord SDK not loaded'); throw new Error("Discord SDK not loaded");
} }
const discord = (window as any).DiscordSDK as DiscordSDK; const discord = (window as any).DiscordSDK as DiscordSDK;
@ -33,13 +33,16 @@ export default function DiscordActivity() {
// Subscribe to close events // Subscribe to close events
if (discord.subscribe) { if (discord.subscribe) {
discord.subscribe('ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE', (data: any) => { discord.subscribe(
console.log('Discord participants updated:', data); "ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE",
}); (data: any) => {
console.log("Discord participants updated:", data);
},
);
} }
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err); const errorMessage = err instanceof Error ? err.message : String(err);
console.error('Failed to initialize Discord SDK:', err); console.error("Failed to initialize Discord SDK:", err);
setError(errorMessage); setError(errorMessage);
} }
}; };
@ -50,14 +53,17 @@ export default function DiscordActivity() {
}, []); }, []);
if (error) { if (error) {
const isCloudflareError = error.includes('Direct IP access') || error.includes('Error 1003'); const isCloudflareError =
const isSDKError = error.includes('Discord SDK'); error.includes("Direct IP access") || error.includes("Error 1003");
const isSDKError = error.includes("Discord SDK");
return ( return (
<div className="min-h-screen flex items-center justify-center bg-background p-4"> <div className="min-h-screen flex items-center justify-center bg-background p-4">
<div className="max-w-md w-full text-center space-y-6"> <div className="max-w-md w-full text-center space-y-6">
<div className="space-y-2"> <div className="space-y-2">
<h1 className="text-2xl font-bold text-red-400">Connection Error</h1> <h1 className="text-2xl font-bold text-red-400">
Connection Error
</h1>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Unable to initialize Discord Activity Unable to initialize Discord Activity
</p> </p>
@ -68,9 +74,12 @@ export default function DiscordActivity() {
{isCloudflareError && ( {isCloudflareError && (
<div className="space-y-2 pt-2 border-t border-red-400/30"> <div className="space-y-2 pt-2 border-t border-red-400/30">
<p className="text-xs text-red-200 font-semibold">🌐 Cloudflare Blocking Access</p> <p className="text-xs text-red-200 font-semibold">
🌐 Cloudflare Blocking Access
</p>
<p className="text-xs text-red-200/80"> <p className="text-xs text-red-200/80">
This error occurs when accessing AeThex via an IP address. Please access through the proper domain: This error occurs when accessing AeThex via an IP address.
Please access through the proper domain:
</p> </p>
<code className="block text-xs bg-black/50 p-2 rounded text-yellow-300 break-all"> <code className="block text-xs bg-black/50 p-2 rounded text-yellow-300 break-all">
https://aethex.dev/discord https://aethex.dev/discord
@ -80,18 +89,26 @@ export default function DiscordActivity() {
{isSDKError && ( {isSDKError && (
<div className="space-y-2 pt-2 border-t border-red-400/30"> <div className="space-y-2 pt-2 border-t border-red-400/30">
<p className="text-xs text-red-200 font-semibold">🎮 Discord SDK Issue</p> <p className="text-xs text-red-200 font-semibold">
🎮 Discord SDK Issue
</p>
<p className="text-xs text-red-200/80"> <p className="text-xs text-red-200/80">
Make sure you're opening this as a Discord Activity within a Discord server, not as a standalone website. Make sure you're opening this as a Discord Activity within a
Discord server, not as a standalone website.
</p> </p>
</div> </div>
)} )}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs text-muted-foreground">Troubleshooting steps:</p> <p className="text-xs text-muted-foreground">
Troubleshooting steps:
</p>
<ul className="text-xs text-muted-foreground space-y-1 text-left list-disc list-inside"> <ul className="text-xs text-muted-foreground space-y-1 text-left list-disc list-inside">
<li>Access via domain: <span className="text-aethex-300">aethex.dev/discord</span></li> <li>
Access via domain:{" "}
<span className="text-aethex-300">aethex.dev/discord</span>
</li>
<li>Open in Discord Activity, not as regular website</li> <li>Open in Discord Activity, not as regular website</li>
<li>Ensure Discord server has AeThex Activity installed</li> <li>Ensure Discord server has AeThex Activity installed</li>
<li>Try refreshing the Discord window</li> <li>Try refreshing the Discord window</li>
@ -123,8 +140,12 @@ export default function DiscordActivity() {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-background"> <div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center space-y-4"> <div className="text-center space-y-4">
<h1 className="text-2xl font-bold text-foreground">Welcome to AeThex</h1> <h1 className="text-2xl font-bold text-foreground">
<p className="text-muted-foreground">Discord user information unavailable</p> Welcome to AeThex
</h1>
<p className="text-muted-foreground">
Discord user information unavailable
</p>
</div> </div>
</div> </div>
); );
@ -134,40 +155,55 @@ export default function DiscordActivity() {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<div className="p-4 text-sm text-muted-foreground border-b border-border/50"> <div className="p-4 text-sm text-muted-foreground border-b border-border/50">
<p>👋 Welcome <strong>{discordUser.username}</strong> to AeThex Discord Activity</p> <p>
👋 Welcome <strong>{discordUser.username}</strong> to AeThex Discord
Activity
</p>
</div> </div>
<div className="container mx-auto py-8"> <div className="container mx-auto py-8">
<div className="text-center space-y-4"> <div className="text-center space-y-4">
<h1 className="text-3xl font-bold text-foreground">AeThex Community</h1> <h1 className="text-3xl font-bold text-foreground">
<p className="text-muted-foreground">Full platform access from Discord</p> AeThex Community
</h1>
<p className="text-muted-foreground">
Full platform access from Discord
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-2xl mx-auto mt-8"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-2xl mx-auto mt-8">
<a <a
href="/feed" href="/feed"
className="p-4 rounded-lg border border-border/50 hover:border-aethex-400/50 transition text-left" className="p-4 rounded-lg border border-border/50 hover:border-aethex-400/50 transition text-left"
> >
<div className="font-semibold">Community Feed</div> <div className="font-semibold">Community Feed</div>
<p className="text-sm text-muted-foreground mt-1">View posts and engage with creators</p> <p className="text-sm text-muted-foreground mt-1">
View posts and engage with creators
</p>
</a> </a>
<a <a
href="/community" href="/community"
className="p-4 rounded-lg border border-border/50 hover:border-aethex-400/50 transition text-left" className="p-4 rounded-lg border border-border/50 hover:border-aethex-400/50 transition text-left"
> >
<div className="font-semibold">Community Hub</div> <div className="font-semibold">Community Hub</div>
<p className="text-sm text-muted-foreground mt-1">Connect with mentors and developers</p> <p className="text-sm text-muted-foreground mt-1">
Connect with mentors and developers
</p>
</a> </a>
<a <a
href="/dashboard" href="/dashboard"
className="p-4 rounded-lg border border-border/50 hover:border-aethex-400/50 transition text-left" className="p-4 rounded-lg border border-border/50 hover:border-aethex-400/50 transition text-left"
> >
<div className="font-semibold">Dashboard</div> <div className="font-semibold">Dashboard</div>
<p className="text-sm text-muted-foreground mt-1">Manage your projects and profile</p> <p className="text-sm text-muted-foreground mt-1">
Manage your projects and profile
</p>
</a> </a>
<a <a
href="/roadmap" href="/roadmap"
className="p-4 rounded-lg border border-border/50 hover:border-aethex-400/50 transition text-left" className="p-4 rounded-lg border border-border/50 hover:border-aethex-400/50 transition text-left"
> >
<div className="font-semibold">Roadmap</div> <div className="font-semibold">Roadmap</div>
<p className="text-sm text-muted-foreground mt-1">Vote on upcoming features</p> <p className="text-sm text-muted-foreground mt-1">
Vote on upcoming features
</p>
</a> </a>
</div> </div>
</div> </div>

View file

@ -35,15 +35,15 @@ export default function Foundation() {
}, },
{ {
name: "Developer CLI", name: "Developer CLI",
description: "Command-line tools for streamlined game development workflow", description:
"Command-line tools for streamlined game development workflow",
stars: "1.2K", stars: "1.2K",
language: "Go", language: "Go",
link: "github.com/aethex/dev-cli", link: "github.com/aethex/dev-cli",
}, },
{ {
name: "Multiplayer Framework", name: "Multiplayer Framework",
description: description: "Drop-in networking layer for real-time multiplayer games",
"Drop-in networking layer for real-time multiplayer games",
stars: "980", stars: "980",
language: "TypeScript", language: "TypeScript",
link: "github.com/aethex/multiplayer", link: "github.com/aethex/multiplayer",
@ -147,7 +147,11 @@ export default function Foundation() {
Community Impact & Talent Pipeline Community Impact & Talent Pipeline
</h1> </h1>
<p className="text-xl text-red-100/70 max-w-3xl"> <p className="text-xl text-red-100/70 max-w-3xl">
AeThex Foundation builds goodwill through open-source code (permissive licensing for maximum adoption), educational curriculum, and community workshops. We create a specialized talent pipeline feeding our ecosystem while advancing the broader developer community. AeThex Foundation builds goodwill through open-source code
(permissive licensing for maximum adoption), educational
curriculum, and community workshops. We create a specialized
talent pipeline feeding our ecosystem while advancing the
broader developer community.
</p> </p>
</div> </div>
@ -170,7 +174,9 @@ export default function Foundation() {
{/* Creator Network CTAs */} {/* Creator Network CTAs */}
<div className="mt-8 pt-8 border-t border-red-400/20"> <div className="mt-8 pt-8 border-t border-red-400/20">
<p className="text-sm text-red-200/70 mb-4">Explore our creator community:</p> <p className="text-sm text-red-200/70 mb-4">
Explore our creator community:
</p>
<div className="flex flex-col sm:flex-row gap-3"> <div className="flex flex-col sm:flex-row gap-3">
<Button <Button
size="sm" size="sm"
@ -300,10 +306,7 @@ export default function Foundation() {
</h2> </h2>
<div className="grid md:grid-cols-2 gap-6"> <div className="grid md:grid-cols-2 gap-6">
{resources.map((resource, idx) => ( {resources.map((resource, idx) => (
<Card <Card key={idx} className="bg-red-950/20 border-red-400/30">
key={idx}
className="bg-red-950/20 border-red-400/30"
>
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
@ -405,7 +408,9 @@ export default function Foundation() {
Ways to Contribute Ways to Contribute
</h2> </h2>
<p className="text-lg text-red-100/80 mb-12 max-w-3xl"> <p className="text-lg text-red-100/80 mb-12 max-w-3xl">
Join our community and help us build the future of game development. Whether you're a developer, designer, educator, or enthusiast, there's a place for you. Join our community and help us build the future of game
development. Whether you're a developer, designer, educator, or
enthusiast, there's a place for you.
</p> </p>
<div className="grid md:grid-cols-3 gap-6"> <div className="grid md:grid-cols-3 gap-6">
{[ {[
@ -425,10 +430,7 @@ export default function Foundation() {
"Help us improve by finding and reporting bugs", "Help us improve by finding and reporting bugs",
}, },
].map((item, idx) => ( ].map((item, idx) => (
<Card <Card key={idx} className="bg-red-950/20 border-red-400/30">
key={idx}
className="bg-red-950/20 border-red-400/30"
>
<CardContent className="pt-6 text-center"> <CardContent className="pt-6 text-center">
<h3 className="font-bold text-red-300 mb-2"> <h3 className="font-bold text-red-300 mb-2">
{item.title} {item.title}

View file

@ -2,7 +2,14 @@ import Layout from "@/components/Layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Gamepad2, Calendar, Users, TrendingUp, Rocket, ArrowRight } from "lucide-react"; import {
Gamepad2,
Calendar,
Users,
TrendingUp,
Rocket,
ArrowRight,
} from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
export default function GameForge() { export default function GameForge() {
@ -97,7 +104,11 @@ export default function GameForge() {
Shipping Games Monthly Shipping Games Monthly
</h1> </h1>
<p className="text-xl text-green-100/70 max-w-3xl"> <p className="text-xl text-green-100/70 max-w-3xl">
AeThex GameForge is our internal production studio that demonstrates disciplined, efficient development. We ship a new game every month using proprietary development pipelines and tools from Labs, proving our technology's real-world impact while maintaining controlled burn rates. AeThex GameForge is our internal production studio that
demonstrates disciplined, efficient development. We ship a new
game every month using proprietary development pipelines and
tools from Labs, proving our technology's real-world impact
while maintaining controlled burn rates.
</p> </p>
</div> </div>
@ -257,7 +268,8 @@ export default function GameForge() {
{ {
phase: "Prototyping", phase: "Prototyping",
duration: "1 week", duration: "1 week",
description: "Build playable prototype to test core mechanics", description:
"Build playable prototype to test core mechanics",
}, },
{ {
phase: "Development", phase: "Development",
@ -312,7 +324,8 @@ export default function GameForge() {
Part of Our Shipping Culture Part of Our Shipping Culture
</h2> </h2>
<p className="text-lg text-green-100/80 mb-8"> <p className="text-lg text-green-100/80 mb-8">
Our team represents the best of game development talent. Meet the people who make monthly shipping possible. Our team represents the best of game development talent. Meet
the people who make monthly shipping possible.
</p> </p>
<Button <Button
className="bg-green-400 text-black shadow-[0_0_30px_rgba(34,197,94,0.35)] hover:bg-green-300" className="bg-green-400 text-black shadow-[0_0_30px_rgba(34,197,94,0.35)] hover:bg-green-300"

View file

@ -2,7 +2,14 @@ import Layout from "@/components/Layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Microscope, Zap, Users, ArrowRight, Lightbulb, Target } from "lucide-react"; import {
Microscope,
Zap,
Users,
ArrowRight,
Lightbulb,
Target,
} from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
export default function Labs() { export default function Labs() {
@ -29,8 +36,7 @@ export default function Labs() {
}, },
{ {
title: "Procedural Content Generation", title: "Procedural Content Generation",
description: description: "Algorithms for infinite, dynamic game world generation",
"Algorithms for infinite, dynamic game world generation",
status: "Published Research", status: "Published Research",
team: 4, team: 4,
impact: "Game development tools", impact: "Game development tools",
@ -57,7 +63,8 @@ export default function Labs() {
{ {
title: "Open Source: AeThex Game Engine", title: "Open Source: AeThex Game Engine",
date: "November 2024", date: "November 2024",
description: "Lightweight, high-performance game engine for web and native", description:
"Lightweight, high-performance game engine for web and native",
stars: "2.5K GitHub stars", stars: "2.5K GitHub stars",
}, },
{ {
@ -99,10 +106,16 @@ export default function Labs() {
The Innovation Engine The Innovation Engine
</h1> </h1>
<p className="text-xl text-yellow-100/70 max-w-3xl"> <p className="text-xl text-yellow-100/70 max-w-3xl">
AeThex Labs is our dedicated R&D pillar, focused on breakthrough technologies that create lasting competitive advantage. We invest in bleeding-edge researchfrom advanced AI to next-generation web architectureswhile cultivating thought leadership that shapes industry direction. AeThex Labs is our dedicated R&D pillar, focused on
breakthrough technologies that create lasting competitive
advantage. We invest in bleeding-edge researchfrom advanced
AI to next-generation web architectureswhile cultivating
thought leadership that shapes industry direction.
</p> </p>
<p className="text-xl text-yellow-100/80 max-w-3xl"> <p className="text-xl text-yellow-100/80 max-w-3xl">
Applied R&D pushing the boundaries of what's possible in software, games, and digital experiences. Our research today shapes tomorrow's products. Applied R&D pushing the boundaries of what's possible in
software, games, and digital experiences. Our research today
shapes tomorrow's products.
</p> </p>
</div> </div>
@ -125,7 +138,9 @@ export default function Labs() {
{/* Creator Network CTAs */} {/* Creator Network CTAs */}
<div className="mt-8 pt-8 border-t border-yellow-400/20"> <div className="mt-8 pt-8 border-t border-yellow-400/20">
<p className="text-sm text-yellow-200/70 mb-4">Explore our creator community:</p> <p className="text-sm text-yellow-200/70 mb-4">
Explore our creator community:
</p>
<div className="flex flex-col sm:flex-row gap-3"> <div className="flex flex-col sm:flex-row gap-3">
<Button <Button
size="sm" size="sm"
@ -161,7 +176,9 @@ export default function Labs() {
className="bg-yellow-950/20 border-yellow-400/30 hover:border-yellow-400/60 transition-all" className="bg-yellow-950/20 border-yellow-400/30 hover:border-yellow-400/60 transition-all"
> >
<CardHeader> <CardHeader>
<div className={`w-12 h-12 rounded-lg bg-gradient-to-r ${project.color} flex items-center justify-center text-white mb-4`}> <div
className={`w-12 h-12 rounded-lg bg-gradient-to-r ${project.color} flex items-center justify-center text-white mb-4`}
>
<Lightbulb className="h-6 w-6" /> <Lightbulb className="h-6 w-6" />
</div> </div>
<CardTitle className="text-yellow-300"> <CardTitle className="text-yellow-300">
@ -267,7 +284,9 @@ export default function Labs() {
Meet the Lab Meet the Lab
</h2> </h2>
<p className="text-lg text-yellow-100/80 max-w-3xl mb-12"> <p className="text-lg text-yellow-100/80 max-w-3xl mb-12">
Our research team consists of PhD-level researchers, innovative engineers, and pioneering thinkers. We collaborate across disciplines to tackle the hardest problems in technology. Our research team consists of PhD-level researchers, innovative
engineers, and pioneering thinkers. We collaborate across
disciplines to tackle the hardest problems in technology.
</p> </p>
<Button <Button
className="bg-yellow-400 text-black hover:bg-yellow-300" className="bg-yellow-400 text-black hover:bg-yellow-300"
@ -286,7 +305,8 @@ export default function Labs() {
Be Part of the Innovation Be Part of the Innovation
</h2> </h2>
<p className="text-lg text-yellow-100/80 mb-8"> <p className="text-lg text-yellow-100/80 mb-8">
We're hiring researchers and engineers to push the boundaries of what's possible. We're hiring researchers and engineers to push the boundaries of
what's possible.
</p> </p>
<Button <Button
className="bg-yellow-400 text-black shadow-[0_0_30px_rgba(251,191,36,0.35)] hover:bg-yellow-300" className="bg-yellow-400 text-black shadow-[0_0_30px_rgba(251,191,36,0.35)] hover:bg-yellow-300"

View file

@ -2,7 +2,15 @@ import Layout from "@/components/Layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Users, Briefcase, Zap, Target, Network, Sparkles, ArrowRight } from "lucide-react"; import {
Users,
Briefcase,
Zap,
Target,
Network,
Sparkles,
ArrowRight,
} from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
export default function Nexus() { export default function Nexus() {
@ -12,32 +20,38 @@ export default function Nexus() {
{ {
icon: Users, icon: Users,
title: "Discover Talent", title: "Discover Talent",
description: "Browse creators across all AeThex arms with powerful filters and search." description:
"Browse creators across all AeThex arms with powerful filters and search.",
}, },
{ {
icon: Briefcase, icon: Briefcase,
title: "Post Opportunities", title: "Post Opportunities",
description: "Create job postings and collaboration requests for your team or studio." description:
"Create job postings and collaboration requests for your team or studio.",
}, },
{ {
icon: Network, icon: Network,
title: "Cross-Arm Integration", title: "Cross-Arm Integration",
description: "Find talent from Labs, GameForge, Corp, Foundation, and DevConnect." description:
"Find talent from Labs, GameForge, Corp, Foundation, and DevConnect.",
}, },
{ {
icon: Sparkles, icon: Sparkles,
title: "Hybrid Marketplace", title: "Hybrid Marketplace",
description: "Access both AeThex creators and DevConnect developers in one place." description:
"Access both AeThex creators and DevConnect developers in one place.",
}, },
{ {
icon: Target, icon: Target,
title: "Smart Matching", title: "Smart Matching",
description: "Match opportunities with creators based on skills, experience, and interests." description:
"Match opportunities with creators based on skills, experience, and interests.",
}, },
{ {
icon: Zap, icon: Zap,
title: "Instant Apply", title: "Instant Apply",
description: "Apply for opportunities directly or track your applications in real-time." description:
"Apply for opportunities directly or track your applications in real-time.",
}, },
]; ];
@ -135,7 +149,8 @@ export default function Nexus() {
Everything You Need Everything You Need
</h2> </h2>
<p className="text-purple-200/70"> <p className="text-purple-200/70">
Connect creators with opportunities in a single, unified marketplace Connect creators with opportunities in a single, unified
marketplace
</p> </p>
</div> </div>
@ -173,7 +188,8 @@ export default function Nexus() {
Multi-Arm Marketplace Multi-Arm Marketplace
</h2> </h2>
<p className="text-purple-200/70"> <p className="text-purple-200/70">
Access talent and opportunities from all AeThex arms in one place Access talent and opportunities from all AeThex arms in one
place
</p> </p>
</div> </div>
@ -205,7 +221,8 @@ export default function Nexus() {
Ready to Connect? Ready to Connect?
</h2> </h2>
<p className="text-purple-200/80 mb-8"> <p className="text-purple-200/80 mb-8">
Join the Nexus today and find your next opportunity or team member Join the Nexus today and find your next opportunity or team
member
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center">

View file

@ -53,7 +53,7 @@ export default function CorpContactUs() {
const handleInputChange = ( const handleInputChange = (
e: React.ChangeEvent< e: React.ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
> >,
) => { ) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value })); setFormData((prev) => ({ ...prev, [name]: value }));
@ -91,7 +91,9 @@ export default function CorpContactUs() {
Get In Touch Get In Touch
</h1> </h1>
<p className="text-lg text-blue-100/80 max-w-3xl"> <p className="text-lg text-blue-100/80 max-w-3xl">
Have questions about our services? Want to schedule a consultation? Contact our enterprise team and we'll get back to you quickly. Have questions about our services? Want to schedule a
consultation? Contact our enterprise team and we'll get back to
you quickly.
</p> </p>
</div> </div>
</section> </section>
@ -272,9 +274,9 @@ export default function CorpContactUs() {
Expected Response Time Expected Response Time
</h3> </h3>
<p className="text-blue-200/80"> <p className="text-blue-200/80">
We typically respond to inquiries within 24 business hours. We typically respond to inquiries within 24 business
For urgent matters, please call our direct line during hours. For urgent matters, please call our direct line
business hours (9AM-6PM EST, Monday-Friday). during business hours (9AM-6PM EST, Monday-Friday).
</p> </p>
</div> </div>
</div> </div>

View file

@ -15,7 +15,8 @@ export default function CorpScheduleConsultation() {
id: "custom-dev", id: "custom-dev",
name: "Custom Software Development", name: "Custom Software Development",
duration: "60 min", duration: "60 min",
description: "Discuss your project requirements and our development approach", description:
"Discuss your project requirements and our development approach",
details: [ details: [
"Project scope assessment", "Project scope assessment",
"Technology stack recommendations", "Technology stack recommendations",
@ -116,7 +117,9 @@ export default function CorpScheduleConsultation() {
Schedule Your Consultation Schedule Your Consultation
</h1> </h1>
<p className="text-lg text-blue-100/80 max-w-3xl"> <p className="text-lg text-blue-100/80 max-w-3xl">
Get expert guidance from our enterprise solutions team. Choose a service, pick your time, and let's discuss how we can help transform your business. Get expert guidance from our enterprise solutions team. Choose a
service, pick your time, and let's discuss how we can help
transform your business.
</p> </p>
</div> </div>
</section> </section>
@ -247,7 +250,9 @@ export default function CorpScheduleConsultation() {
What to Expect What to Expect
</h2> </h2>
<p className="text-lg text-blue-100/80 mb-8"> <p className="text-lg text-blue-100/80 mb-8">
Our consultants will assess your needs, provide expert recommendations, and create a customized solution proposal tailored to your business goals. Our consultants will assess your needs, provide expert
recommendations, and create a customized solution proposal
tailored to your business goals.
</p> </p>
<Button <Button
className="bg-blue-400 text-black shadow-[0_0_30px_rgba(59,130,246,0.35)] hover:bg-blue-300" className="bg-blue-400 text-black shadow-[0_0_30px_rgba(59,130,246,0.35)] hover:bg-blue-300"

View file

@ -133,7 +133,8 @@ export default function CorpViewCaseStudies() {
Case Studies Case Studies
</h1> </h1>
<p className="text-lg text-blue-100/80 max-w-3xl"> <p className="text-lg text-blue-100/80 max-w-3xl">
Real-world success stories from our enterprise clients. See how we've helped transform businesses across industries. Real-world success stories from our enterprise clients. See how
we've helped transform businesses across industries.
</p> </p>
</div> </div>
</section> </section>
@ -272,7 +273,8 @@ export default function CorpViewCaseStudies() {
Ready for Your Success Story? Ready for Your Success Story?
</h2> </h2>
<p className="text-lg text-blue-100/80 mb-8"> <p className="text-lg text-blue-100/80 mb-8">
Let's discuss how we can help transform your business and achieve similar results. Let's discuss how we can help transform your business and
achieve similar results.
</p> </p>
<Button <Button
className="bg-blue-400 text-black shadow-[0_0_30px_rgba(59,130,246,0.35)] hover:bg-blue-300" className="bg-blue-400 text-black shadow-[0_0_30px_rgba(59,130,246,0.35)] hover:bg-blue-300"

View file

@ -15,7 +15,7 @@ export default function CreatorDirectory() {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [search, setSearch] = useState(searchParams.get("search") || ""); const [search, setSearch] = useState(searchParams.get("search") || "");
const [selectedArm, setSelectedArm] = useState<string | undefined>( const [selectedArm, setSelectedArm] = useState<string | undefined>(
searchParams.get("arm") || undefined searchParams.get("arm") || undefined,
); );
const [page, setPage] = useState(parseInt(searchParams.get("page") || "1")); const [page, setPage] = useState(parseInt(searchParams.get("page") || "1"));
const [totalPages, setTotalPages] = useState(0); const [totalPages, setTotalPages] = useState(0);
@ -82,7 +82,8 @@ export default function CreatorDirectory() {
</h1> </h1>
</div> </div>
<p className="text-lg text-gray-300 max-w-2xl mx-auto"> <p className="text-lg text-gray-300 max-w-2xl mx-auto">
Discover talented creators across all AeThex arms. Filter by specialty, skills, and arm affiliation. Discover talented creators across all AeThex arms. Filter by
specialty, skills, and arm affiliation.
</p> </p>
</div> </div>
@ -166,8 +167,10 @@ export default function CreatorDirectory() {
Previous Previous
</Button> </Button>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{Array.from({ length: totalPages }, (_, i) => i + 1).map( {Array.from(
(p) => ( { length: totalPages },
(_, i) => i + 1,
).map((p) => (
<Button <Button
key={p} key={p}
onClick={() => setPage(p)} onClick={() => setPage(p)}
@ -176,11 +179,12 @@ export default function CreatorDirectory() {
> >
{p} {p}
</Button> </Button>
) ))}
)}
</div> </div>
<Button <Button
onClick={() => setPage(Math.min(totalPages, page + 1))} onClick={() =>
setPage(Math.min(totalPages, page + 1))
}
disabled={page === totalPages} disabled={page === totalPages}
variant="outline" variant="outline"
> >

View file

@ -50,7 +50,9 @@ export default function CreatorProfile() {
<div className="min-h-screen bg-black text-white flex items-center justify-center"> <div className="min-h-screen bg-black text-white flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<h1 className="text-3xl font-bold mb-4">Creator Not Found</h1> <h1 className="text-3xl font-bold mb-4">Creator Not Found</h1>
<p className="text-gray-400 mb-6">The creator you're looking for doesn't exist.</p> <p className="text-gray-400 mb-6">
The creator you're looking for doesn't exist.
</p>
<Button onClick={() => navigate("/creators")}> <Button onClick={() => navigate("/creators")}>
<ArrowLeft className="h-4 w-4 mr-2" /> <ArrowLeft className="h-4 w-4 mr-2" />
Back to Creators Back to Creators
@ -87,7 +89,10 @@ export default function CreatorProfile() {
<CardContent className="p-8"> <CardContent className="p-8">
<div className="flex flex-col md:flex-row items-start md:items-center gap-6 mb-6"> <div className="flex flex-col md:flex-row items-start md:items-center gap-6 mb-6">
<Avatar className="h-24 w-24"> <Avatar className="h-24 w-24">
<AvatarImage src={creator.avatar_url} alt={creator.username} /> <AvatarImage
src={creator.avatar_url}
alt={creator.username}
/>
<AvatarFallback> <AvatarFallback>
{creator.username.charAt(0).toUpperCase()} {creator.username.charAt(0).toUpperCase()}
</AvatarFallback> </AvatarFallback>
@ -95,7 +100,9 @@ export default function CreatorProfile() {
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<h1 className="text-3xl font-bold">@{creator.username}</h1> <h1 className="text-3xl font-bold">
@{creator.username}
</h1>
{creator.devconnect_linked && ( {creator.devconnect_linked && (
<Badge className="bg-cyan-500/10 text-cyan-300 border-cyan-500/20"> <Badge className="bg-cyan-500/10 text-cyan-300 border-cyan-500/20">
<ExternalLink className="h-3 w-3 mr-1" /> <ExternalLink className="h-3 w-3 mr-1" />
@ -112,16 +119,16 @@ export default function CreatorProfile() {
{creator.arm_affiliations && {creator.arm_affiliations &&
creator.arm_affiliations creator.arm_affiliations
.filter((arm) => arm !== creator.primary_arm) .filter((arm) => arm !== creator.primary_arm)
.map((arm) => ( .map((arm) => <ArmBadge key={arm} arm={arm} />)}
<ArmBadge key={arm} arm={arm} />
))}
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
{creator.devconnect_link && ( {creator.devconnect_link && (
<Button asChild> <Button asChild>
<a <a
href={creator.devconnect_link.devconnect_profile_url} href={
creator.devconnect_link.devconnect_profile_url
}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >

View file

@ -62,7 +62,9 @@ export default function DevLinkAbout() {
About Dev-Link About Dev-Link
</h1> </h1>
<p className="text-lg text-cyan-100/80 max-w-3xl"> <p className="text-lg text-cyan-100/80 max-w-3xl">
The professional networking platform built for Roblox developers. Connect, collaborate, and grow your career in game development. The professional networking platform built for Roblox
developers. Connect, collaborate, and grow your career in game
development.
</p> </p>
</div> </div>
</section> </section>
@ -75,7 +77,13 @@ export default function DevLinkAbout() {
<Card className="bg-cyan-950/20 border-cyan-400/30"> <Card className="bg-cyan-950/20 border-cyan-400/30">
<CardContent className="pt-6"> <CardContent className="pt-6">
<p className="text-lg text-cyan-200/80 leading-relaxed"> <p className="text-lg text-cyan-200/80 leading-relaxed">
Dev-Link is on a mission to empower Roblox developers worldwide. We believe that the Roblox platform has created an incredible community of creators who deserve a professional space to connect, showcase their work, and find amazing opportunities. Just like LinkedIn transformed professional networking, Dev-Link is transforming how Roblox developers collaborate and build their careers. Dev-Link is on a mission to empower Roblox developers
worldwide. We believe that the Roblox platform has created
an incredible community of creators who deserve a
professional space to connect, showcase their work, and find
amazing opportunities. Just like LinkedIn transformed
professional networking, Dev-Link is transforming how Roblox
developers collaborate and build their careers.
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@ -91,7 +99,10 @@ export default function DevLinkAbout() {
{values.map((value, idx) => { {values.map((value, idx) => {
const Icon = value.icon; const Icon = value.icon;
return ( return (
<Card key={idx} className="bg-cyan-950/20 border-cyan-400/30"> <Card
key={idx}
className="bg-cyan-950/20 border-cyan-400/30"
>
<CardContent className="pt-6"> <CardContent className="pt-6">
<Icon className="h-8 w-8 text-cyan-400 mb-3" /> <Icon className="h-8 w-8 text-cyan-400 mb-3" />
<h3 className="text-lg font-bold text-cyan-300 mb-2"> <h3 className="text-lg font-bold text-cyan-300 mb-2">

View file

@ -89,7 +89,8 @@ export default function DevLinkJobs() {
Job Board Job Board
</h1> </h1>
<p className="text-lg text-cyan-100/80 max-w-3xl"> <p className="text-lg text-cyan-100/80 max-w-3xl">
Find your next opportunity in the Roblox ecosystem. Full-time, part-time, and contract roles available. Find your next opportunity in the Roblox ecosystem. Full-time,
part-time, and contract roles available.
</p> </p>
</div> </div>
</section> </section>
@ -118,11 +119,13 @@ export default function DevLinkJobs() {
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Badge className={`${ <Badge
className={`${
job.type === "Full-time" job.type === "Full-time"
? "bg-green-500/20 text-green-300 border border-green-400/40" ? "bg-green-500/20 text-green-300 border border-green-400/40"
: "bg-cyan-500/20 text-cyan-300 border border-cyan-400/40" : "bg-cyan-500/20 text-cyan-300 border border-cyan-400/40"
}`}> }`}
>
{job.type} {job.type}
</Badge> </Badge>
<span className="flex items-center gap-1 text-sm text-cyan-200/70"> <span className="flex items-center gap-1 text-sm text-cyan-200/70">
@ -170,9 +173,7 @@ export default function DevLinkJobs() {
<p className="text-lg text-cyan-100/80 mb-8"> <p className="text-lg text-cyan-100/80 mb-8">
Reach 50K+ talented Roblox developers. Reach 50K+ talented Roblox developers.
</p> </p>
<Button <Button className="bg-cyan-400 text-black hover:bg-cyan-300">
className="bg-cyan-400 text-black hover:bg-cyan-300"
>
Post a Job Opening Post a Job Opening
<ArrowRight className="ml-2 h-4 w-4" /> <ArrowRight className="ml-2 h-4 w-4" />
</Button> </Button>

View file

@ -84,7 +84,8 @@ export default function DevLinkProfiles() {
Developer Directory Developer Directory
</h1> </h1>
<p className="text-lg text-cyan-100/80 max-w-3xl"> <p className="text-lg text-cyan-100/80 max-w-3xl">
Find and connect with talented Roblox developers, browse portfolios, and discover collaboration opportunities. Find and connect with talented Roblox developers, browse
portfolios, and discover collaboration opportunities.
</p> </p>
</div> </div>
</section> </section>
@ -115,7 +116,10 @@ export default function DevLinkProfiles() {
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 rounded-full bg-gradient-to-r from-cyan-500 to-blue-500 flex items-center justify-center text-white font-bold"> <div className="w-12 h-12 rounded-full bg-gradient-to-r from-cyan-500 to-blue-500 flex items-center justify-center text-white font-bold">
{dev.name.split(" ").map(n => n[0]).join("")} {dev.name
.split(" ")
.map((n) => n[0])
.join("")}
</div> </div>
{dev.available && ( {dev.available && (
<Badge className="bg-green-500/20 text-green-300 border border-green-400/40 text-xs"> <Badge className="bg-green-500/20 text-green-300 border border-green-400/40 text-xs">
@ -144,7 +148,9 @@ export default function DevLinkProfiles() {
<div className="pt-4 border-t border-cyan-400/10 space-y-3"> <div className="pt-4 border-t border-cyan-400/10 space-y-3">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-cyan-200/70">{dev.portfolio}</span> <span className="text-cyan-200/70">
{dev.portfolio}
</span>
<span className="flex items-center gap-1 text-yellow-400"> <span className="flex items-center gap-1 text-yellow-400">
<Star className="h-4 w-4 fill-yellow-400" /> <Star className="h-4 w-4 fill-yellow-400" />
{dev.rating} {dev.rating}
@ -172,9 +178,7 @@ export default function DevLinkProfiles() {
<p className="text-lg text-cyan-100/80 mb-8"> <p className="text-lg text-cyan-100/80 mb-8">
Showcase your work and connect with other developers. Showcase your work and connect with other developers.
</p> </p>
<Button <Button className="bg-cyan-400 text-black hover:bg-cyan-300">
className="bg-cyan-400 text-black hover:bg-cyan-300"
>
Get Started Get Started
<ArrowRight className="ml-2 h-4 w-4" /> <ArrowRight className="ml-2 h-4 w-4" />
</Button> </Button>

View file

@ -84,7 +84,8 @@ export default function FoundationContribute() {
}, },
{ {
name: "Developer CLI", name: "Developer CLI",
description: "Command-line tools for streamlined game development workflow", description:
"Command-line tools for streamlined game development workflow",
stars: "1.2K", stars: "1.2K",
language: "Go", language: "Go",
issues: "15 open", issues: "15 open",
@ -92,8 +93,7 @@ export default function FoundationContribute() {
}, },
{ {
name: "Multiplayer Framework", name: "Multiplayer Framework",
description: description: "Drop-in networking layer for real-time multiplayer games",
"Drop-in networking layer for real-time multiplayer games",
stars: "980", stars: "980",
language: "TypeScript", language: "TypeScript",
issues: "10 open", issues: "10 open",
@ -155,7 +155,9 @@ export default function FoundationContribute() {
Ways to Contribute Ways to Contribute
</h1> </h1>
<p className="text-lg text-red-100/80 max-w-3xl"> <p className="text-lg text-red-100/80 max-w-3xl">
Join our community and help us build amazing open-source projects. There are many ways to contribute, regardless of your skill level. Join our community and help us build amazing open-source
projects. There are many ways to contribute, regardless of your
skill level.
</p> </p>
</div> </div>
</section> </section>

View file

@ -173,7 +173,8 @@ export default function FoundationGetInvolved() {
Get Involved Get Involved
</h1> </h1>
<p className="text-lg text-red-100/80 max-w-3xl"> <p className="text-lg text-red-100/80 max-w-3xl">
Join our thriving community of developers. Whether you're just starting or a seasoned pro, there's a place for you here. Join our thriving community of developers. Whether you're just
starting or a seasoned pro, there's a place for you here.
</p> </p>
</div> </div>
</section> </section>

View file

@ -25,12 +25,7 @@ export default function FoundationLearnMore() {
"Complete introduction to game development concepts and best practices", "Complete introduction to game development concepts and best practices",
lessons: "50", lessons: "50",
duration: "20 hours", duration: "20 hours",
topics: [ topics: ["Game loops", "Physics", "Input handling", "Asset management"],
"Game loops",
"Physics",
"Input handling",
"Asset management",
],
free: true, free: true,
}, },
{ {
@ -59,12 +54,7 @@ export default function FoundationLearnMore() {
"Learn scalable architectural patterns used in professional game development", "Learn scalable architectural patterns used in professional game development",
modules: "8", modules: "8",
projects: "4", projects: "4",
topics: [ topics: ["MVC pattern", "ECS systems", "Networking", "State management"],
"MVC pattern",
"ECS systems",
"Networking",
"State management",
],
free: true, free: true,
}, },
{ {
@ -174,7 +164,9 @@ export default function FoundationLearnMore() {
Free Learning Resources Free Learning Resources
</h1> </h1>
<p className="text-lg text-red-100/80 max-w-3xl"> <p className="text-lg text-red-100/80 max-w-3xl">
Learn game development from the ground up with our free, comprehensive educational resources. Everything you need to become an expert developer. Learn game development from the ground up with our free,
comprehensive educational resources. Everything you need to
become an expert developer.
</p> </p>
</div> </div>
</section> </section>
@ -426,8 +418,8 @@ export default function FoundationLearnMore() {
Start Your Learning Journey Start Your Learning Journey
</h2> </h2>
<p className="text-lg text-red-100/80 mb-8"> <p className="text-lg text-red-100/80 mb-8">
Choose a learning path and begin mastering game development today. Choose a learning path and begin mastering game development
Everything is completely free and open to the community. today. Everything is completely free and open to the community.
</p> </p>
<Button <Button
className="bg-red-400 text-black shadow-[0_0_30px_rgba(239,68,68,0.35)] hover:bg-red-300" className="bg-red-400 text-black shadow-[0_0_30px_rgba(239,68,68,0.35)] hover:bg-red-300"

View file

@ -65,7 +65,8 @@ export default function GameForgeJoinGameForge() {
The Team The Team
</h1> </h1>
<p className="text-lg text-green-100/80 max-w-3xl"> <p className="text-lg text-green-100/80 max-w-3xl">
The talented developers, designers, and producers who ship a game every month. The talented developers, designers, and producers who ship a
game every month.
</p> </p>
</div> </div>
</section> </section>
@ -80,7 +81,10 @@ export default function GameForgeJoinGameForge() {
> >
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="w-12 h-12 rounded-full bg-gradient-to-r from-green-500 to-emerald-500 flex items-center justify-center text-white font-bold mb-3"> <div className="w-12 h-12 rounded-full bg-gradient-to-r from-green-500 to-emerald-500 flex items-center justify-center text-white font-bold mb-3">
{member.name.split(" ").map(n => n[0]).join("")} {member.name
.split(" ")
.map((n) => n[0])
.join("")}
</div> </div>
<h3 className="text-lg font-bold text-green-300 mb-1"> <h3 className="text-lg font-bold text-green-300 mb-1">
{member.name} {member.name}
@ -115,7 +119,8 @@ export default function GameForgeJoinGameForge() {
{ {
icon: <Zap className="h-6 w-6" />, icon: <Zap className="h-6 w-6" />,
title: "Ship Every Month", title: "Ship Every Month",
description: "We deliver a complete, polished game every month", description:
"We deliver a complete, polished game every month",
}, },
{ {
icon: <Heart className="h-6 w-6" />, icon: <Heart className="h-6 w-6" />,
@ -125,7 +130,8 @@ export default function GameForgeJoinGameForge() {
{ {
icon: <Users className="h-6 w-6" />, icon: <Users className="h-6 w-6" />,
title: "Together", title: "Together",
description: "Cross-functional teams working toward one goal", description:
"Cross-functional teams working toward one goal",
}, },
].map((item, idx) => ( ].map((item, idx) => (
<Card <Card

View file

@ -122,7 +122,9 @@ export default function GameForgeStartBuilding() {
Production Pipeline Production Pipeline
</h1> </h1>
<p className="text-lg text-green-100/80 max-w-3xl"> <p className="text-lg text-green-100/80 max-w-3xl">
How we ship a game every month. Our proven process, team coordination, and development tools that make monthly shipping possible. How we ship a game every month. Our proven process, team
coordination, and development tools that make monthly shipping
possible.
</p> </p>
</div> </div>
</section> </section>
@ -234,11 +236,13 @@ export default function GameForgeStartBuilding() {
</p> </p>
</div> </div>
</div> </div>
<Badge className={`${ <Badge
className={`${
item.status === "On Track" item.status === "On Track"
? "bg-green-500/20 text-green-300 border border-green-400/40" ? "bg-green-500/20 text-green-300 border border-green-400/40"
: "bg-purple-500/20 text-purple-300 border border-purple-400/40" : "bg-purple-500/20 text-purple-300 border border-purple-400/40"
}`}> }`}
>
{item.status} {item.status}
</Badge> </Badge>
</div> </div>
@ -257,7 +261,10 @@ export default function GameForgeStartBuilding() {
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
{productionPhases.map((item, idx) => ( {productionPhases.map((item, idx) => (
<Card key={idx} className="bg-green-950/20 border-green-400/30"> <Card
key={idx}
className="bg-green-950/20 border-green-400/30"
>
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-start gap-6"> <div className="flex items-start gap-6">
<div className="h-12 w-12 rounded-lg bg-gradient-to-r from-green-500 to-emerald-500 flex items-center justify-center text-white font-bold flex-shrink-0"> <div className="h-12 w-12 rounded-lg bg-gradient-to-r from-green-500 to-emerald-500 flex items-center justify-center text-white font-bold flex-shrink-0">

View file

@ -75,7 +75,8 @@ export default function GameForgeViewPortfolio() {
Released Games Released Games
</h1> </h1>
<p className="text-lg text-green-100/80 max-w-3xl"> <p className="text-lg text-green-100/80 max-w-3xl">
Games shipped by GameForge. See player stats, revenue, and team sizes from our monthly releases. Games shipped by GameForge. See player stats, revenue, and team
sizes from our monthly releases.
</p> </p>
</div> </div>
</section> </section>
@ -109,9 +110,7 @@ export default function GameForgeViewPortfolio() {
<p className="text-lg font-bold text-green-300"> <p className="text-lg font-bold text-green-300">
{game.players} {game.players}
</p> </p>
<p className="text-xs text-green-200/60"> <p className="text-xs text-green-200/60">active</p>
active
</p>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@ -119,7 +118,8 @@ export default function GameForgeViewPortfolio() {
RATING RATING
</p> </p>
<p className="text-lg font-bold text-green-300 flex items-center gap-1"> <p className="text-lg font-bold text-green-300 flex items-center gap-1">
{game.rating} <Star className="h-4 w-4 fill-yellow-400 text-yellow-400" /> {game.rating}{" "}
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
</p> </p>
<p className="text-xs text-green-200/60"> <p className="text-xs text-green-200/60">
{game.downloads} downloads {game.downloads} downloads

View file

@ -2,7 +2,14 @@ import Layout from "@/components/Layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ArrowRight, Users, Clock, Zap, Download, ExternalLink } from "lucide-react"; import {
ArrowRight,
Users,
Clock,
Zap,
Download,
ExternalLink,
} from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
export default function LabsExploreResearch() { export default function LabsExploreResearch() {
@ -105,7 +112,9 @@ export default function LabsExploreResearch() {
Research Projects Research Projects
</h1> </h1>
<p className="text-lg text-yellow-100/80 max-w-3xl"> <p className="text-lg text-yellow-100/80 max-w-3xl">
Explore the cutting-edge research being conducted in AeThex Labs. Each project represents our commitment to pushing the boundaries of technology. Explore the cutting-edge research being conducted in AeThex
Labs. Each project represents our commitment to pushing the
boundaries of technology.
</p> </p>
</div> </div>
</section> </section>
@ -125,13 +134,15 @@ export default function LabsExploreResearch() {
<CardTitle className="text-2xl text-yellow-300 mb-2"> <CardTitle className="text-2xl text-yellow-300 mb-2">
{project.title} {project.title}
</CardTitle> </CardTitle>
<Badge className={`${ <Badge
className={`${
project.status === "Published" project.status === "Published"
? "bg-green-500/20 border border-green-400/40 text-green-300" ? "bg-green-500/20 border border-green-400/40 text-green-300"
: project.status === "Active Research" : project.status === "Active Research"
? "bg-yellow-500/20 border border-yellow-400/40 text-yellow-300" ? "bg-yellow-500/20 border border-yellow-400/40 text-yellow-300"
: "bg-purple-500/20 border border-purple-400/40 text-purple-300" : "bg-purple-500/20 border border-purple-400/40 text-purple-300"
}`}> }`}
>
{project.status} {project.status}
</Badge> </Badge>
</div> </div>
@ -148,7 +159,9 @@ export default function LabsExploreResearch() {
<CardContent className="space-y-6"> <CardContent className="space-y-6">
{/* Description */} {/* Description */}
<p className="text-yellow-200/80">{project.description}</p> <p className="text-yellow-200/80">
{project.description}
</p>
{/* Key Achievements */} {/* Key Achievements */}
<div> <div>
@ -188,7 +201,9 @@ export default function LabsExploreResearch() {
{/* Impact & Links */} {/* Impact & Links */}
<div className="pt-4 border-t border-yellow-400/10 flex flex-wrap items-center justify-between gap-4"> <div className="pt-4 border-t border-yellow-400/10 flex flex-wrap items-center justify-between gap-4">
<p className="text-sm text-yellow-200/80"> <p className="text-sm text-yellow-200/80">
<span className="font-semibold text-yellow-400">Impact:</span>{" "} <span className="font-semibold text-yellow-400">
Impact:
</span>{" "}
{project.impact} {project.impact}
</p> </p>
<div className="flex gap-3"> <div className="flex gap-3">
@ -228,7 +243,8 @@ export default function LabsExploreResearch() {
Interested in Research? Interested in Research?
</h2> </h2>
<p className="text-lg text-yellow-100/80 mb-8"> <p className="text-lg text-yellow-100/80 mb-8">
Join our research team and contribute to the future of technology. Join our research team and contribute to the future of
technology.
</p> </p>
<Button <Button
className="bg-yellow-400 text-black hover:bg-yellow-300" className="bg-yellow-400 text-black hover:bg-yellow-300"

View file

@ -2,7 +2,14 @@ import Layout from "@/components/Layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CheckCircle, Users, BookOpen, Code, Zap, ArrowRight } from "lucide-react"; import {
CheckCircle,
Users,
BookOpen,
Code,
Zap,
ArrowRight,
} from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
export default function LabsGetInvolved() { export default function LabsGetInvolved() {
@ -109,7 +116,9 @@ export default function LabsGetInvolved() {
Get Involved Get Involved
</h1> </h1>
<p className="text-lg text-yellow-100/80 max-w-3xl"> <p className="text-lg text-yellow-100/80 max-w-3xl">
There are many ways to collaborate with AeThex Labs. Whether you're a researcher, developer, or thought leader, we'd love to work together. There are many ways to collaborate with AeThex Labs. Whether
you're a researcher, developer, or thought leader, we'd love
to work together.
</p> </p>
</div> </div>
</div> </div>
@ -125,7 +134,9 @@ export default function LabsGetInvolved() {
className="bg-yellow-950/20 border-yellow-400/30 hover:border-yellow-400/60 transition-all" className="bg-yellow-950/20 border-yellow-400/30 hover:border-yellow-400/60 transition-all"
> >
<CardHeader> <CardHeader>
<div className={`w-12 h-12 rounded-lg bg-gradient-to-r ${opp.color} flex items-center justify-center text-white mb-4`}> <div
className={`w-12 h-12 rounded-lg bg-gradient-to-r ${opp.color} flex items-center justify-center text-white mb-4`}
>
{opp.icon} {opp.icon}
</div> </div>
<CardTitle className="text-yellow-300"> <CardTitle className="text-yellow-300">

View file

@ -104,7 +104,9 @@ export default function LabsJoinTeam() {
Meet the Lab Meet the Lab
</h1> </h1>
<p className="text-lg text-yellow-100/80 max-w-3xl"> <p className="text-lg text-yellow-100/80 max-w-3xl">
World-class researchers and engineers dedicated to advancing technology. Our team includes PhD researchers, published authors, and visionary thinkers. World-class researchers and engineers dedicated to advancing
technology. Our team includes PhD researchers, published
authors, and visionary thinkers.
</p> </p>
</div> </div>
</div> </div>
@ -235,7 +237,8 @@ export default function LabsJoinTeam() {
Join the Lab Join the Lab
</h2> </h2>
<p className="text-lg text-yellow-100/80 mb-8"> <p className="text-lg text-yellow-100/80 mb-8">
We're looking for brilliant minds to join our team. Researchers, engineers, and visionaries welcome. We're looking for brilliant minds to join our team. Researchers,
engineers, and visionaries welcome.
</p> </p>
<Button <Button
className="bg-yellow-400 text-black hover:bg-yellow-300" className="bg-yellow-400 text-black hover:bg-yellow-300"

View file

@ -15,7 +15,7 @@ export default function OpportunitiesHub() {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [search, setSearch] = useState(searchParams.get("search") || ""); const [search, setSearch] = useState(searchParams.get("search") || "");
const [selectedArm, setSelectedArm] = useState<string | undefined>( const [selectedArm, setSelectedArm] = useState<string | undefined>(
searchParams.get("arm") || undefined searchParams.get("arm") || undefined,
); );
const [page, setPage] = useState(parseInt(searchParams.get("page") || "1")); const [page, setPage] = useState(parseInt(searchParams.get("page") || "1"));
const [totalPages, setTotalPages] = useState(0); const [totalPages, setTotalPages] = useState(0);
@ -83,7 +83,8 @@ export default function OpportunitiesHub() {
</h1> </h1>
</div> </div>
<p className="text-lg text-gray-300 max-w-2xl mx-auto"> <p className="text-lg text-gray-300 max-w-2xl mx-auto">
Find jobs, collaborations, and research opportunities across all AeThex arms. Find jobs, collaborations, and research opportunities across
all AeThex arms.
</p> </p>
</div> </div>
@ -167,8 +168,10 @@ export default function OpportunitiesHub() {
Previous Previous
</Button> </Button>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{Array.from({ length: totalPages }, (_, i) => i + 1).map( {Array.from(
(p) => ( { length: totalPages },
(_, i) => i + 1,
).map((p) => (
<Button <Button
key={p} key={p}
onClick={() => setPage(p)} onClick={() => setPage(p)}
@ -177,11 +180,12 @@ export default function OpportunitiesHub() {
> >
{p} {p}
</Button> </Button>
) ))}
)}
</div> </div>
<Button <Button
onClick={() => setPage(Math.min(totalPages, page + 1))} onClick={() =>
setPage(Math.min(totalPages, page + 1))
}
disabled={page === totalPages} disabled={page === totalPages}
variant="outline" variant="outline"
> >

View file

@ -79,7 +79,7 @@ export default function OpportunityDetail() {
} catch (error) { } catch (error) {
toast( toast(
error instanceof Error ? error.message : "Failed to submit application", error instanceof Error ? error.message : "Failed to submit application",
"error" "error",
); );
} finally { } finally {
setIsApplying(false); setIsApplying(false);
@ -88,7 +88,8 @@ export default function OpportunityDetail() {
const formatSalary = (min?: number, max?: number) => { const formatSalary = (min?: number, max?: number) => {
if (!min && !max) return "Not specified"; if (!min && !max) return "Not specified";
if (min && max) return `$${min.toLocaleString()} - $${max.toLocaleString()}`; if (min && max)
return `$${min.toLocaleString()} - $${max.toLocaleString()}`;
if (min) return `$${min.toLocaleString()}+`; if (min) return `$${min.toLocaleString()}+`;
if (max) return `Up to $${max.toLocaleString()}`; if (max) return `Up to $${max.toLocaleString()}`;
}; };
@ -152,9 +153,7 @@ export default function OpportunityDetail() {
<ArmBadge arm={opportunity.arm_affiliation} /> <ArmBadge arm={opportunity.arm_affiliation} />
</div> </div>
<h1 className="text-4xl font-bold mb-4"> <h1 className="text-4xl font-bold mb-4">{opportunity.title}</h1>
{opportunity.title}
</h1>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8 py-6 border-t border-b border-slate-700"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8 py-6 border-t border-b border-slate-700">
<div> <div>
@ -171,7 +170,7 @@ export default function OpportunityDetail() {
<p className="font-semibold text-sm"> <p className="font-semibold text-sm">
{formatSalary( {formatSalary(
opportunity.salary_min, opportunity.salary_min,
opportunity.salary_max opportunity.salary_max,
)} )}
</p> </p>
</div> </div>
@ -196,7 +195,10 @@ export default function OpportunityDetail() {
{/* Posted By */} {/* Posted By */}
<div className="flex items-center gap-4 mb-8 p-4 bg-slate-700/30 rounded-lg"> <div className="flex items-center gap-4 mb-8 p-4 bg-slate-700/30 rounded-lg">
<Avatar className="h-12 w-12"> <Avatar className="h-12 w-12">
<AvatarImage src={poster.avatar_url} alt={poster.username} /> <AvatarImage
src={poster.avatar_url}
alt={poster.username}
/>
<AvatarFallback> <AvatarFallback>
{poster.username.charAt(0).toUpperCase()} {poster.username.charAt(0).toUpperCase()}
</AvatarFallback> </AvatarFallback>
@ -206,9 +208,7 @@ export default function OpportunityDetail() {
<p className="font-semibold">@{poster.username}</p> <p className="font-semibold">@{poster.username}</p>
</div> </div>
<Button <Button
onClick={() => onClick={() => navigate(`/creators/${poster.username}`)}
navigate(`/creators/${poster.username}`)
}
variant="outline" variant="outline"
> >
View Profile View Profile
@ -253,8 +253,8 @@ export default function OpportunityDetail() {
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Apply for {opportunity.title}</AlertDialogTitle> <AlertDialogTitle>Apply for {opportunity.title}</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Submit your application with a cover letter explaining why you're Submit your application with a cover letter explaining why
interested in this opportunity. you're interested in this opportunity.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
@ -280,9 +280,7 @@ export default function OpportunityDetail() {
disabled={isApplying || !coverLetter.trim()} disabled={isApplying || !coverLetter.trim()}
className="gap-2" className="gap-2"
> >
{isApplying && ( {isApplying && <Loader2 className="h-4 w-4 animate-spin" />}
<Loader2 className="h-4 w-4 animate-spin" />
)}
Submit Application Submit Application
</AlertDialogAction> </AlertDialogAction>
</div> </div>

View file

@ -51,15 +51,13 @@ export default function MyApplications() {
try { try {
await withdrawApplication(applicationId); await withdrawApplication(applicationId);
toast("Application withdrawn", "success"); toast("Application withdrawn", "success");
setApplications( setApplications(applications.filter((app) => app.id !== applicationId));
applications.filter((app) => app.id !== applicationId)
);
} catch (error) { } catch (error) {
toast( toast(
error instanceof Error error instanceof Error
? error.message ? error.message
: "Failed to withdraw application", : "Failed to withdraw application",
"error" "error",
); );
} }
}; };
@ -215,7 +213,7 @@ export default function MyApplications() {
/> />
))} ))}
</TabsContent> </TabsContent>
) ),
)} )}
</Tabs> </Tabs>
</> </>
@ -265,10 +263,7 @@ function ApplicationCard({
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<Avatar className="h-10 w-10"> <Avatar className="h-10 w-10">
<AvatarImage <AvatarImage src={poster.avatar_url} alt={poster.username} />
src={poster.avatar_url}
alt={poster.username}
/>
<AvatarFallback> <AvatarFallback>
{poster.username.charAt(0).toUpperCase()} {poster.username.charAt(0).toUpperCase()}
</AvatarFallback> </AvatarFallback>

View file

@ -7,6 +7,7 @@ AeThex can be embedded as a Discord Activity, allowing users to access the platf
## What is a Discord Activity? ## What is a Discord Activity?
A Discord Activity is an embedded application that runs within Discord. It allows users to: A Discord Activity is an embedded application that runs within Discord. It allows users to:
- Access AeThex features directly in Discord - Access AeThex features directly in Discord
- Share their experience with server members - Share their experience with server members
- Collaborate in real-time without leaving Discord - Collaborate in real-time without leaving Discord
@ -51,6 +52,7 @@ This file tells Discord how to handle the AeThex Activity:
``` ```
**Key Configuration Points:** **Key Configuration Points:**
- `instance_url`: Where Discord Activity iframe loads (MUST be domain, not IP) - `instance_url`: Where Discord Activity iframe loads (MUST be domain, not IP)
- `redirect_uris`: OAuth callback endpoint (MUST match Discord app settings) - `redirect_uris`: OAuth callback endpoint (MUST match Discord app settings)
- `scopes`: What Discord permissions the Activity requests - `scopes`: What Discord permissions the Activity requests
@ -58,15 +60,18 @@ This file tells Discord how to handle the AeThex Activity:
### 2. Code Configuration ### 2. Code Configuration
**Frontend Pages** (`code/client/pages/`): **Frontend Pages** (`code/client/pages/`):
- `DiscordActivity.tsx` - Main Activity page mounted at `/discord` - `DiscordActivity.tsx` - Main Activity page mounted at `/discord`
- Discord OAuth callback handler at `/discord/callback` - Discord OAuth callback handler at `/discord/callback`
**Context** (`code/client/contexts/DiscordContext.tsx`): **Context** (`code/client/contexts/DiscordContext.tsx`):
- Manages Discord user session - Manages Discord user session
- Handles OAuth flow - Handles OAuth flow
- Exposes Discord user data to components - Exposes Discord user data to components
**Routes** (`code/client/App.tsx`): **Routes** (`code/client/App.tsx`):
```typescript ```typescript
<Route path="/discord" element={<DiscordActivity />} /> <Route path="/discord" element={<DiscordActivity />} />
<Route path="/discord/callback" element={<DiscordOAuthCallback />} /> <Route path="/discord/callback" element={<DiscordOAuthCallback />} />
@ -85,6 +90,7 @@ This must be present for Discord Activity to initialize.
## Local Testing ## Local Testing
### Prerequisites ### Prerequisites
- Node.js 18+ - Node.js 18+
- npm or yarn - npm or yarn
- Running AeThex dev server - Running AeThex dev server
@ -92,6 +98,7 @@ This must be present for Discord Activity to initialize.
### Steps ### Steps
1. **Start the dev server:** 1. **Start the dev server:**
```bash ```bash
npm run dev npm run dev
# or # or
@ -99,6 +106,7 @@ This must be present for Discord Activity to initialize.
``` ```
2. **Access locally via tunnel** (if testing Discord Activity): 2. **Access locally via tunnel** (if testing Discord Activity):
- Use a tool like `ngrok` to expose localhost to Discord - Use a tool like `ngrok` to expose localhost to Discord
- Or use Discord's local testing tools - Or use Discord's local testing tools
@ -111,6 +119,7 @@ This must be present for Discord Activity to initialize.
⚠️ **Note**: Discord Activities require HTTPS and a proper domain. Local testing with IP addresses will fail with **Cloudflare Error 1003**. ⚠️ **Note**: Discord Activities require HTTPS and a proper domain. Local testing with IP addresses will fail with **Cloudflare Error 1003**.
Local testing workarounds: Local testing workarounds:
- Use `ngrok` with a tunnel URL - Use `ngrok` with a tunnel URL
- Use Discord's local testing documentation - Use Discord's local testing documentation
- Test OAuth flow after deploying to staging - Test OAuth flow after deploying to staging
@ -145,6 +154,7 @@ Local testing workarounds:
### Environment Variables ### Environment Variables
No special environment variables needed for Discord Activity. The configuration is done via: No special environment variables needed for Discord Activity. The configuration is done via:
- `code/public/discord-manifest.json` - `code/public/discord-manifest.json`
- Discord Application settings - Discord Application settings
- `code/client/contexts/DiscordContext.tsx` - `code/client/contexts/DiscordContext.tsx`
@ -169,16 +179,19 @@ No special environment variables needed for Discord Activity. The configuration
### Implementation Details ### Implementation Details
**DiscordActivity.tsx** handles: **DiscordActivity.tsx** handles:
- Discord SDK initialization - Discord SDK initialization
- OAuth trigger and callback handling - OAuth trigger and callback handling
- Activity UI rendering - Activity UI rendering
**DiscordContext.tsx** manages: **DiscordContext.tsx** manages:
- Discord user state - Discord user state
- Token storage - Token storage
- Session lifecycle - Session lifecycle
**API calls** use Discord access token for: **API calls** use Discord access token for:
- User identification - User identification
- Guild information - Guild information
- Activity-specific operations - Activity-specific operations
@ -190,6 +203,7 @@ No special environment variables needed for Discord Activity. The configuration
**Cause**: Accessing Activity via IP address instead of domain **Cause**: Accessing Activity via IP address instead of domain
**Solution**: **Solution**:
``` ```
❌ http://192.168.1.100:5173/discord ❌ http://192.168.1.100:5173/discord
✅ https://aethex.dev/discord ✅ https://aethex.dev/discord
@ -197,6 +211,7 @@ No special environment variables needed for Discord Activity. The configuration
**Error Message in UI**: **Error Message in UI**:
Users will see a helpful error message explaining: Users will see a helpful error message explaining:
- The issue (Cloudflare blocking IP access) - The issue (Cloudflare blocking IP access)
- How to fix it (use domain instead) - How to fix it (use domain instead)
- Troubleshooting steps - Troubleshooting steps
@ -206,6 +221,7 @@ Users will see a helpful error message explaining:
**Cause**: Discord SDK not loaded or manifest invalid **Cause**: Discord SDK not loaded or manifest invalid
**Solution**: **Solution**:
- Verify Discord SDK script in `code/index.html` - Verify Discord SDK script in `code/index.html`
- Check manifest is accessible at `/public/discord-manifest.json` - Check manifest is accessible at `/public/discord-manifest.json`
- Verify Discord Application ID: `578971245454950421` - Verify Discord Application ID: `578971245454950421`
@ -244,6 +260,7 @@ Users will see a helpful error message explaining:
**Error**: Blank white screen in Discord Activity **Error**: Blank white screen in Discord Activity
**Debug Steps**: **Debug Steps**:
1. Open browser DevTools in Discord 1. Open browser DevTools in Discord
2. Check Console for errors 2. Check Console for errors
3. Check Network tab for failed requests 3. Check Network tab for failed requests
@ -251,6 +268,7 @@ Users will see a helpful error message explaining:
5. Verify Cloudflare isn't blocking traffic 5. Verify Cloudflare isn't blocking traffic
**Common Causes**: **Common Causes**:
- IP address used instead of domain (Cloudflare Error 1003) - IP address used instead of domain (Cloudflare Error 1003)
- Discord SDK script failed to load - Discord SDK script failed to load
- Manifest file not accessible - Manifest file not accessible
@ -261,12 +279,14 @@ Users will see a helpful error message explaining:
**Error**: OAuth flow doesn't complete or redirect fails **Error**: OAuth flow doesn't complete or redirect fails
**Debug Steps**: **Debug Steps**:
1. Check Discord Application settings - redirect URIs match exactly 1. Check Discord Application settings - redirect URIs match exactly
2. Verify OAuth callback route exists: `/discord/callback` 2. Verify OAuth callback route exists: `/discord/callback`
3. Check browser console for authorization error codes 3. Check browser console for authorization error codes
4. Verify Discord Application permissions (identify, email, guilds) 4. Verify Discord Application permissions (identify, email, guilds)
**Common Causes**: **Common Causes**:
- Redirect URI mismatch between manifest and Discord app - Redirect URI mismatch between manifest and Discord app
- Discord Application doesn't have "Identify" scope enabled - Discord Application doesn't have "Identify" scope enabled
- Activity not installed in Discord server - Activity not installed in Discord server
@ -276,6 +296,7 @@ Users will see a helpful error message explaining:
**Error**: Guild list is empty or shows no servers **Error**: Guild list is empty or shows no servers
**Debug Steps**: **Debug Steps**:
1. Verify "guilds" scope is in OAuth config 1. Verify "guilds" scope is in OAuth config
2. Check user actually has permission in those guilds 2. Check user actually has permission in those guilds
3. Verify Discord OAuth token has guilds scope 3. Verify Discord OAuth token has guilds scope
@ -316,6 +337,7 @@ Using your deployment provider (Netlify, Vercel, custom):
### Step 5: Add to Discord ### Step 5: Add to Discord
In Discord Developer Portal: In Discord Developer Portal:
1. Go to Application Settings 1. Go to Application Settings
2. Add Activity URL: `https://aethex.dev/discord` 2. Add Activity URL: `https://aethex.dev/discord`
3. Set OAuth2 Redirect URIs to: `https://aethex.dev/discord/callback` 3. Set OAuth2 Redirect URIs to: `https://aethex.dev/discord/callback`

View file

@ -7,12 +7,14 @@ The AeThex organizational structure implements a centralized IP holding company
## 1. Labs as IP Holding Company (IPCo) ## 1. Labs as IP Holding Company (IPCo)
All core intellectual property developed by Labs is owned by Labs, including: All core intellectual property developed by Labs is owned by Labs, including:
- Patent portfolios (AI, algorithms, architectures) - Patent portfolios (AI, algorithms, architectures)
- Software copyrights and source code - Software copyrights and source code
- Trade secrets and proprietary methodologies - Trade secrets and proprietary methodologies
- Trademarks and brand assets - Trademarks and brand assets
**Benefits:** **Benefits:**
- **Clean IP Title**: Consolidated, encumbrance-free IP ownership improves enterprise valuation - **Clean IP Title**: Consolidated, encumbrance-free IP ownership improves enterprise valuation
- **Protection**: IP separated from operational liabilities (Corp consulting disputes, Dev-Link platform issues) - **Protection**: IP separated from operational liabilities (Corp consulting disputes, Dev-Link platform issues)
- **Management**: Centralized IP portfolio administration across subsidiaries - **Management**: Centralized IP portfolio administration across subsidiaries
@ -23,6 +25,7 @@ All core intellectual property developed by Labs is owned by Labs, including:
### Commercial Technology License ### Commercial Technology License
Corp receives a Commercial License to "make and use" proprietary Lab technologies in commercial service delivery, specifically: Corp receives a Commercial License to "make and use" proprietary Lab technologies in commercial service delivery, specifically:
- Advanced AI models for game development optimization - Advanced AI models for game development optimization
- Custom algorithms for multiplayer architecture - Custom algorithms for multiplayer architecture
- Tools and frameworks created through R&D - Tools and frameworks created through R&D
@ -43,6 +46,7 @@ Licensing fees from Corp to Labs must comply with the **Arm's Length Principle**
For unique intangibles (advanced AI), cost-plus pricing is insufficient. Instead: For unique intangibles (advanced AI), cost-plus pricing is insufficient. Instead:
1. **Economic Value Uplift**: Measure how Lab IP enables Corp to charge premium rates 1. **Economic Value Uplift**: Measure how Lab IP enables Corp to charge premium rates
- Example: If Lab AI enables Corp to charge 30% premium for specialized dev services, royalty should reflect that uplift - Example: If Lab AI enables Corp to charge 30% premium for specialized dev services, royalty should reflect that uplift
- Comparable: Industry benchmarks for AI licensing typically 15-25% of incremental revenue - Comparable: Industry benchmarks for AI licensing typically 15-25% of incremental revenue
@ -67,6 +71,7 @@ Labs AI licensing to Corp:
## 3. Labs → Dev-Link: Licensing for Platform Features ## 3. Labs → Dev-Link: Licensing for Platform Features
Dev-Link platform may license Lab IP for: Dev-Link platform may license Lab IP for:
- AI-assisted candidate assessment and matching - AI-assisted candidate assessment and matching
- Specialized skill evaluation algorithms - Specialized skill evaluation algorithms
- Predictive analytics for placement success - Predictive analytics for placement success
@ -92,12 +97,14 @@ Charge = Actual Costs + Markup (typically 5-15%)
``` ```
**Eligible Services:** **Eligible Services:**
- HR administration, payroll processing - HR administration, payroll processing
- IT infrastructure and support - IT infrastructure and support
- General accounting and bookkeeping - General accounting and bookkeeping
- Legal documentation review - Legal documentation review
**Requirements:** **Requirements:**
- Clearly define which activities qualify - Clearly define which activities qualify
- Meticulously track direct costs - Meticulously track direct costs
- Document markup selection (compare to similar service providers) - Document markup selection (compare to similar service providers)
@ -106,6 +113,7 @@ Charge = Actual Costs + Markup (typically 5-15%)
### Documentation ### Documentation
Maintain **Intercompany Service Agreement** including: Maintain **Intercompany Service Agreement** including:
- Services provided - Services provided
- Cost allocation methodology - Cost allocation methodology
- Markup rationale - Markup rationale
@ -118,11 +126,13 @@ The Foundation may receive services or assets from Corp/Dev-Link. **All must be
### Examples of Related-Party Transactions ### Examples of Related-Party Transactions
1. **Corp provides office space to Foundation** 1. **Corp provides office space to Foundation**
- Must charge Fair Market Rent (comparable leases in area) - Must charge Fair Market Rent (comparable leases in area)
- Document: Annual appraisal or comparable rent analysis - Document: Annual appraisal or comparable rent analysis
- Forbidden: Below-market lease = private inurement - Forbidden: Below-market lease = private inurement
2. **Corp donates curriculum materials to Foundation** 2. **Corp donates curriculum materials to Foundation**
- Document as charitable contribution - Document as charitable contribution
- Fair value: Cost of development + reasonable markup - Fair value: Cost of development + reasonable markup
- Record in Foundation's fund accounting system - Record in Foundation's fund accounting system
@ -135,6 +145,7 @@ The Foundation may receive services or assets from Corp/Dev-Link. **All must be
### Conflict of Interest Policy ### Conflict of Interest Policy
Foundation must adopt and enforce a Conflict of Interest Policy including: Foundation must adopt and enforce a Conflict of Interest Policy including:
- Board members declare conflicts with related for-profit entities - Board members declare conflicts with related for-profit entities
- Disclosure of family/business relationships with Corp/Dev-Link executives - Disclosure of family/business relationships with Corp/Dev-Link executives
- Voting restrictions: Conflicted directors abstain on related-party votes - Voting restrictions: Conflicted directors abstain on related-party votes
@ -147,11 +158,13 @@ Foundation must adopt and enforce a Conflict of Interest Policy including:
### Methodology ### Methodology
**For Technology Licensing (Labs IP):** **For Technology Licensing (Labs IP):**
- Comparable License Analysis (market rates for similar IP) - Comparable License Analysis (market rates for similar IP)
- Relief-from-Royalty (value of IP to users) - Relief-from-Royalty (value of IP to users)
- Residual Profit Split (allocate profit between Lab innovation and operational execution) - Residual Profit Split (allocate profit between Lab innovation and operational execution)
**For Services:** **For Services:**
- Comparable Uncontrolled Price (market rates for same services) - Comparable Uncontrolled Price (market rates for same services)
- Cost-Plus Analysis (cost + reasonable markup) - Cost-Plus Analysis (cost + reasonable markup)
- Resale Price Method (if service is resold externally) - Resale Price Method (if service is resold externally)
@ -204,6 +217,7 @@ All transfers from Corp must be documented as grants/contributions with restrict
### Related-Party Transaction Board Approval ### Related-Party Transaction Board Approval
Before any Corp transfer to Foundation: Before any Corp transfer to Foundation:
1. Independent majority votes 1. Independent majority votes
2. Minority includes conflicted party vote counts documented 2. Minority includes conflicted party vote counts documented
3. FMV analysis presented 3. FMV analysis presented
@ -213,6 +227,7 @@ Before any Corp transfer to Foundation:
### Annual Form 990 Reporting ### Annual Form 990 Reporting
Foundation must file IRS Form 990 disclosing: Foundation must file IRS Form 990 disclosing:
- Compensation of officers/directors - Compensation of officers/directors
- Related-party transactions - Related-party transactions
- All grants and contributions received - All grants and contributions received
@ -223,6 +238,7 @@ Foundation must file IRS Form 990 disclosing:
## 9. Benefit Corporation Governance (Parent Level) ## 9. Benefit Corporation Governance (Parent Level)
Parent company incorporated as Benefit Corporation (not C-Corp) specifically to: Parent company incorporated as Benefit Corporation (not C-Corp) specifically to:
- Balance shareholder profit with stakeholder interests - Balance shareholder profit with stakeholder interests
- Legally protect Foundation funding decisions - Legally protect Foundation funding decisions
- Enable long-term R&D investment (Labs) even during fiscal pressure - Enable long-term R&D investment (Labs) even during fiscal pressure
@ -231,6 +247,7 @@ Parent company incorporated as Benefit Corporation (not C-Corp) specifically to:
### Board Duties ### Board Duties
Benefit Corporation directors have legal duty to: Benefit Corporation directors have legal duty to:
1. Consider impact on stakeholders (workers, customers, community) 1. Consider impact on stakeholders (workers, customers, community)
2. Balance shareholder returns with general public benefit 2. Balance shareholder returns with general public benefit
3. Document consideration of non-shareholder interests in board minutes 3. Document consideration of non-shareholder interests in board minutes
@ -240,6 +257,7 @@ This provides legal cover for capital allocation to Foundation or high-burn Labs
## 10. Private Inurement Prevention ## 10. Private Inurement Prevention
### Definition ### Definition
Private inurement = net earnings of Foundation inure to benefit of any shareholder/individual = immediate loss of 501(c)(3) status. Private inurement = net earnings of Foundation inure to benefit of any shareholder/individual = immediate loss of 501(c)(3) status.
### High-Risk Transactions ### High-Risk Transactions
@ -259,15 +277,18 @@ Private inurement = net earnings of Foundation inure to benefit of any sharehold
## 11. Transfer Pricing Documentation Requirements ## 11. Transfer Pricing Documentation Requirements
### When Required ### When Required
Any payment between related entities (Labs→Corp, Corp→Foundation, etc.) requires documentation. Any payment between related entities (Labs→Corp, Corp→Foundation, etc.) requires documentation.
### Documentation Package ### Documentation Package
1. **Intercompany Agreement** 1. **Intercompany Agreement**
- Parties, services/IP, payment terms, effective date - Parties, services/IP, payment terms, effective date
- Signed and dated - Signed and dated
2. **Transfer Pricing Study** 2. **Transfer Pricing Study**
- Executive summary - Executive summary
- Functional analysis (functions, assets, risks of each party) - Functional analysis (functions, assets, risks of each party)
- Economic analysis (industry data, market conditions) - Economic analysis (industry data, market conditions)
@ -316,6 +337,7 @@ Maintain and update:
## Conclusion ## Conclusion
This framework ensures: This framework ensures:
1. **IP Protection**: Centralized control and defensive valuation 1. **IP Protection**: Centralized control and defensive valuation
2. **Tax Compliance**: Arm's Length transfer pricing reduces audit risk 2. **Tax Compliance**: Arm's Length transfer pricing reduces audit risk
3. **Liability Insulation**: Separate entities prevent cross-contamination 3. **Liability Insulation**: Separate entities prevent cross-contamination

View file

@ -14,9 +14,7 @@ const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID || "578971245454950421";
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN; const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN;
if (!DISCORD_BOT_TOKEN) { if (!DISCORD_BOT_TOKEN) {
console.error( console.error("❌ DISCORD_BOT_TOKEN environment variable is required");
"❌ DISCORD_BOT_TOKEN environment variable is required",
);
process.exit(1); process.exit(1);
} }
@ -103,14 +101,14 @@ async function registerCommands() {
}); });
console.log("\n📍 Next steps:"); console.log("\n📍 Next steps:");
console.log( console.log("1. Go to Discord Developer Portal > Application Settings");
"1. Go to Discord Developer Portal > Application Settings",
);
console.log( console.log(
"2. Set Interactions Endpoint URL to: https://aethex.dev/api/discord/interactions", "2. Set Interactions Endpoint URL to: https://aethex.dev/api/discord/interactions",
); );
console.log("3. Save changes"); console.log("3. Save changes");
console.log("4. Test commands in Discord with /creators, /opportunities, etc."); console.log(
"4. Test commands in Discord with /creators, /opportunities, etc.",
);
} catch (error) { } catch (error) {
console.error("❌ Error registering commands:", error); console.error("❌ Error registering commands:", error);
process.exit(1); process.exit(1);

View file

@ -115,7 +115,10 @@ const handleDiscordInteractions = (
// For MESSAGE_COMPONENT interactions (buttons, etc.) // For MESSAGE_COMPONENT interactions (buttons, etc.)
if (interaction.type === 3) { if (interaction.type === 3) {
console.log("[Discord] Message component interaction:", interaction.data.custom_id); console.log(
"[Discord] Message component interaction:",
interaction.data.custom_id,
);
return res.json({ return res.json({
type: 4, type: 4,
@ -2418,7 +2421,7 @@ export function createServer() {
primary_arm, primary_arm,
created_at created_at
`, `,
{ count: "exact" } { count: "exact" },
) )
.eq("is_discoverable", true) .eq("is_discoverable", true)
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });
@ -2475,7 +2478,7 @@ export function createServer() {
primary_arm, primary_arm,
created_at, created_at,
updated_at updated_at
` `,
) )
.eq("username", username) .eq("username", username)
.eq("is_discoverable", true) .eq("is_discoverable", true)
@ -2507,10 +2510,21 @@ export function createServer() {
// Create creator profile // Create creator profile
app.post("/api/creators", async (req, res) => { app.post("/api/creators", async (req, res) => {
try { try {
const { user_id, username, bio, skills, avatar_url, experience_level, primary_arm, arm_affiliations } = req.body; const {
user_id,
username,
bio,
skills,
avatar_url,
experience_level,
primary_arm,
arm_affiliations,
} = req.body;
if (!user_id || !username) { if (!user_id || !username) {
return res.status(400).json({ error: "user_id and username required" }); return res
.status(400)
.json({ error: "user_id and username required" });
} }
const { data, error } = await adminSupabase const { data, error } = await adminSupabase
@ -2523,7 +2537,9 @@ export function createServer() {
avatar_url: avatar_url || null, avatar_url: avatar_url || null,
experience_level: experience_level || null, experience_level: experience_level || null,
primary_arm: primary_arm || null, primary_arm: primary_arm || null,
arm_affiliations: Array.isArray(arm_affiliations) ? arm_affiliations : [], arm_affiliations: Array.isArray(arm_affiliations)
? arm_affiliations
: [],
}) })
.select() .select()
.single(); .single();
@ -2538,7 +2554,9 @@ export function createServer() {
return res.status(201).json(data); return res.status(201).json(data);
} catch (e: any) { } catch (e: any) {
console.error("[Creator API] Error creating creator:", e?.message); console.error("[Creator API] Error creating creator:", e?.message);
return res.status(500).json({ error: "Failed to create creator profile" }); return res
.status(500)
.json({ error: "Failed to create creator profile" });
} }
}); });
@ -2550,7 +2568,16 @@ export function createServer() {
return res.status(400).json({ error: "creator id required" }); return res.status(400).json({ error: "creator id required" });
} }
const { bio, skills, avatar_url, experience_level, primary_arm, arm_affiliations, is_discoverable, allow_recommendations } = req.body; const {
bio,
skills,
avatar_url,
experience_level,
primary_arm,
arm_affiliations,
is_discoverable,
allow_recommendations,
} = req.body;
const { data, error } = await adminSupabase const { data, error } = await adminSupabase
.from("aethex_creators") .from("aethex_creators")
@ -2560,7 +2587,9 @@ export function createServer() {
avatar_url, avatar_url,
experience_level, experience_level,
primary_arm, primary_arm,
arm_affiliations: Array.isArray(arm_affiliations) ? arm_affiliations : undefined, arm_affiliations: Array.isArray(arm_affiliations)
? arm_affiliations
: undefined,
is_discoverable, is_discoverable,
allow_recommendations, allow_recommendations,
updated_at: new Date().toISOString(), updated_at: new Date().toISOString(),
@ -2574,7 +2603,9 @@ export function createServer() {
return res.json(data); return res.json(data);
} catch (e: any) { } catch (e: any) {
console.error("[Creator API] Error updating creator:", e?.message); console.error("[Creator API] Error updating creator:", e?.message);
return res.status(500).json({ error: "Failed to update creator profile" }); return res
.status(500)
.json({ error: "Failed to update creator profile" });
} }
}); });
@ -2606,7 +2637,7 @@ export function createServer() {
status, status,
created_at created_at
`, `,
{ count: "exact" } { count: "exact" },
) )
.eq("status", "open"); .eq("status", "open");
@ -2615,7 +2646,9 @@ export function createServer() {
} }
if (search) { if (search) {
query = query.or(`title.ilike.%${search}%,description.ilike.%${search}%`); query = query.or(
`title.ilike.%${search}%,description.ilike.%${search}%`,
);
} }
if (jobType) { if (jobType) {
@ -2646,7 +2679,10 @@ export function createServer() {
}, },
}); });
} catch (e: any) { } catch (e: any) {
console.error("[Opportunities API] Error fetching opportunities:", e?.message); console.error(
"[Opportunities API] Error fetching opportunities:",
e?.message,
);
return res.status(500).json({ error: "Failed to fetch opportunities" }); return res.status(500).json({ error: "Failed to fetch opportunities" });
} }
}); });
@ -2676,7 +2712,7 @@ export function createServer() {
status, status,
created_at, created_at,
updated_at updated_at
` `,
) )
.eq("id", opportunityId) .eq("id", opportunityId)
.eq("status", "open") .eq("status", "open")
@ -2691,7 +2727,10 @@ export function createServer() {
return res.json(data); return res.json(data);
} catch (e: any) { } catch (e: any) {
console.error("[Opportunities API] Error fetching opportunity:", e?.message); console.error(
"[Opportunities API] Error fetching opportunity:",
e?.message,
);
return res.status(500).json({ error: "Failed to fetch opportunity" }); return res.status(500).json({ error: "Failed to fetch opportunity" });
} }
}); });
@ -2699,7 +2738,16 @@ export function createServer() {
// Create opportunity // Create opportunity
app.post("/api/opportunities", async (req, res) => { app.post("/api/opportunities", async (req, res) => {
try { try {
const { user_id, title, description, job_type, salary_min, salary_max, experience_level, arm_affiliation } = req.body; const {
user_id,
title,
description,
job_type,
salary_min,
salary_max,
experience_level,
arm_affiliation,
} = req.body;
if (!user_id || !title) { if (!user_id || !title) {
return res.status(400).json({ error: "user_id and title required" }); return res.status(400).json({ error: "user_id and title required" });
@ -2712,7 +2760,11 @@ export function createServer() {
.single(); .single();
if (!creator) { if (!creator) {
return res.status(404).json({ error: "Creator profile not found. Create profile first." }); return res
.status(404)
.json({
error: "Creator profile not found. Create profile first.",
});
} }
const { data, error } = await adminSupabase const { data, error } = await adminSupabase
@ -2735,7 +2787,10 @@ export function createServer() {
return res.status(201).json(data); return res.status(201).json(data);
} catch (e: any) { } catch (e: any) {
console.error("[Opportunities API] Error creating opportunity:", e?.message); console.error(
"[Opportunities API] Error creating opportunity:",
e?.message,
);
return res.status(500).json({ error: "Failed to create opportunity" }); return res.status(500).json({ error: "Failed to create opportunity" });
} }
}); });
@ -2744,10 +2799,21 @@ export function createServer() {
app.put("/api/opportunities/:id", async (req, res) => { app.put("/api/opportunities/:id", async (req, res) => {
try { try {
const opportunityId = String(req.params.id || "").trim(); const opportunityId = String(req.params.id || "").trim();
const { user_id, title, description, job_type, salary_min, salary_max, experience_level, status } = req.body; const {
user_id,
title,
description,
job_type,
salary_min,
salary_max,
experience_level,
status,
} = req.body;
if (!opportunityId || !user_id) { if (!opportunityId || !user_id) {
return res.status(400).json({ error: "opportunity id and user_id required" }); return res
.status(400)
.json({ error: "opportunity id and user_id required" });
} }
const { data: opportunity } = await adminSupabase const { data: opportunity } = await adminSupabase
@ -2790,7 +2856,10 @@ export function createServer() {
return res.json(data); return res.json(data);
} catch (e: any) { } catch (e: any) {
console.error("[Opportunities API] Error updating opportunity:", e?.message); console.error(
"[Opportunities API] Error updating opportunity:",
e?.message,
);
return res.status(500).json({ error: "Failed to update opportunity" }); return res.status(500).json({ error: "Failed to update opportunity" });
} }
}); });
@ -2831,7 +2900,7 @@ export function createServer() {
updated_at, updated_at,
aethex_opportunities(id, title, arm_affiliation, job_type, posted_by_id, aethex_creators!aethex_opportunities_posted_by_id_fkey(username, avatar_url)) aethex_opportunities(id, title, arm_affiliation, job_type, posted_by_id, aethex_creators!aethex_opportunities_posted_by_id_fkey(username, avatar_url))
`, `,
{ count: "exact" } { count: "exact" },
) )
.eq("creator_id", creator.id); .eq("creator_id", creator.id);
@ -2858,7 +2927,10 @@ export function createServer() {
}, },
}); });
} catch (e: any) { } catch (e: any) {
console.error("[Applications API] Error fetching applications:", e?.message); console.error(
"[Applications API] Error fetching applications:",
e?.message,
);
return res.status(500).json({ error: "Failed to fetch applications" }); return res.status(500).json({ error: "Failed to fetch applications" });
} }
}); });
@ -2869,7 +2941,9 @@ export function createServer() {
const { user_id, opportunity_id, cover_letter } = req.body; const { user_id, opportunity_id, cover_letter } = req.body;
if (!user_id || !opportunity_id) { if (!user_id || !opportunity_id) {
return res.status(400).json({ error: "user_id and opportunity_id required" }); return res
.status(400)
.json({ error: "user_id and opportunity_id required" });
} }
const { data: creator } = await adminSupabase const { data: creator } = await adminSupabase
@ -2890,7 +2964,9 @@ export function createServer() {
.single(); .single();
if (!opportunity) { if (!opportunity) {
return res.status(404).json({ error: "Opportunity not found or closed" }); return res
.status(404)
.json({ error: "Opportunity not found or closed" });
} }
const { data: existing } = await adminSupabase const { data: existing } = await adminSupabase
@ -2901,7 +2977,9 @@ export function createServer() {
.maybeSingle(); .maybeSingle();
if (existing) { if (existing) {
return res.status(400).json({ error: "You have already applied to this opportunity" }); return res
.status(400)
.json({ error: "You have already applied to this opportunity" });
} }
const { data, error } = await adminSupabase const { data, error } = await adminSupabase
@ -2919,7 +2997,10 @@ export function createServer() {
return res.status(201).json(data); return res.status(201).json(data);
} catch (e: any) { } catch (e: any) {
console.error("[Applications API] Error submitting application:", e?.message); console.error(
"[Applications API] Error submitting application:",
e?.message,
);
return res.status(500).json({ error: "Failed to submit application" }); return res.status(500).json({ error: "Failed to submit application" });
} }
}); });
@ -2931,7 +3012,9 @@ export function createServer() {
const { user_id, status, response_message } = req.body; const { user_id, status, response_message } = req.body;
if (!applicationId || !user_id) { if (!applicationId || !user_id) {
return res.status(400).json({ error: "application id and user_id required" }); return res
.status(400)
.json({ error: "application id and user_id required" });
} }
const { data: application } = await adminSupabase const { data: application } = await adminSupabase
@ -2941,7 +3024,7 @@ export function createServer() {
id, id,
opportunity_id, opportunity_id,
aethex_opportunities(posted_by_id) aethex_opportunities(posted_by_id)
` `,
) )
.eq("id", applicationId) .eq("id", applicationId)
.single(); .single();
@ -2975,7 +3058,10 @@ export function createServer() {
return res.json(data); return res.json(data);
} catch (e: any) { } catch (e: any) {
console.error("[Applications API] Error updating application:", e?.message); console.error(
"[Applications API] Error updating application:",
e?.message,
);
return res.status(500).json({ error: "Failed to update application" }); return res.status(500).json({ error: "Failed to update application" });
} }
}); });
@ -2987,7 +3073,9 @@ export function createServer() {
const { user_id } = req.body; const { user_id } = req.body;
if (!applicationId || !user_id) { if (!applicationId || !user_id) {
return res.status(400).json({ error: "application id and user_id required" }); return res
.status(400)
.json({ error: "application id and user_id required" });
} }
const { data: application } = await adminSupabase const { data: application } = await adminSupabase
@ -3019,18 +3107,26 @@ export function createServer() {
return res.json({ ok: true }); return res.json({ ok: true });
} catch (e: any) { } catch (e: any) {
console.error("[Applications API] Error withdrawing application:", e?.message); console.error(
return res.status(500).json({ error: "Failed to withdraw application" }); "[Applications API] Error withdrawing application:",
e?.message,
);
return res
.status(500)
.json({ error: "Failed to withdraw application" });
} }
}); });
// Link DevConnect account // Link DevConnect account
app.post("/api/devconnect/link", async (req, res) => { app.post("/api/devconnect/link", async (req, res) => {
try { try {
const { user_id, devconnect_username, devconnect_profile_url } = req.body; const { user_id, devconnect_username, devconnect_profile_url } =
req.body;
if (!user_id || !devconnect_username) { if (!user_id || !devconnect_username) {
return res.status(400).json({ error: "user_id and devconnect_username required" }); return res
.status(400)
.json({ error: "user_id and devconnect_username required" });
} }
const { data: creator } = await adminSupabase const { data: creator } = await adminSupabase
@ -3040,7 +3136,11 @@ export function createServer() {
.single(); .single();
if (!creator) { if (!creator) {
return res.status(404).json({ error: "Creator profile not found. Create profile first." }); return res
.status(404)
.json({
error: "Creator profile not found. Create profile first.",
});
} }
const { data: existing } = await adminSupabase const { data: existing } = await adminSupabase
@ -3057,7 +3157,9 @@ export function createServer() {
.from("aethex_devconnect_links") .from("aethex_devconnect_links")
.update({ .update({
devconnect_username, devconnect_username,
devconnect_profile_url: devconnect_profile_url || `https://dev-link.me/${devconnect_username}`, devconnect_profile_url:
devconnect_profile_url ||
`https://dev-link.me/${devconnect_username}`,
}) })
.eq("aethex_creator_id", creator.id) .eq("aethex_creator_id", creator.id)
.select() .select()
@ -3072,7 +3174,9 @@ export function createServer() {
.insert({ .insert({
aethex_creator_id: creator.id, aethex_creator_id: creator.id,
devconnect_username, devconnect_username,
devconnect_profile_url: devconnect_profile_url || `https://dev-link.me/${devconnect_username}`, devconnect_profile_url:
devconnect_profile_url ||
`https://dev-link.me/${devconnect_username}`,
}) })
.select() .select()
.single(); .single();
@ -3089,7 +3193,9 @@ export function createServer() {
return res.status(status).json(result); return res.status(status).json(result);
} catch (e: any) { } catch (e: any) {
console.error("[DevConnect API] Error linking account:", e?.message); console.error("[DevConnect API] Error linking account:", e?.message);
return res.status(500).json({ error: "Failed to link DevConnect account" }); return res
.status(500)
.json({ error: "Failed to link DevConnect account" });
} }
}); });
@ -3125,7 +3231,9 @@ export function createServer() {
return res.json({ data: data || null }); return res.json({ data: data || null });
} catch (e: any) { } catch (e: any) {
console.error("[DevConnect API] Error fetching link:", e?.message); console.error("[DevConnect API] Error fetching link:", e?.message);
return res.status(500).json({ error: "Failed to fetch DevConnect link" }); return res
.status(500)
.json({ error: "Failed to fetch DevConnect link" });
} }
}); });
@ -3163,10 +3271,11 @@ export function createServer() {
return res.json({ ok: true }); return res.json({ ok: true });
} catch (e: any) { } catch (e: any) {
console.error("[DevConnect API] Error unlinking account:", e?.message); console.error("[DevConnect API] Error unlinking account:", e?.message);
return res.status(500).json({ error: "Failed to unlink DevConnect account" }); return res
.status(500)
.json({ error: "Failed to unlink DevConnect account" });
} }
}); });
} catch (e) { } catch (e) {
console.warn("Admin API not initialized:", e); console.warn("Admin API not initialized:", e);
} }

View file

@ -1,82 +1,76 @@
# Phase 3: Testing & Validation - COMPLETE ✅ # Phase 3: Testing & Validation - COMPLETE ✅
## Overview ## Overview
Phase 3 successfully delivered comprehensive testing infrastructure for the AeThex Creator Network, covering end-to-end flows, error handling, performance measurement, and security audit protocols. Phase 3 successfully delivered comprehensive testing infrastructure for the AeThex Creator Network, covering end-to-end flows, error handling, performance measurement, and security audit protocols.
## 📦 Deliverables ## 📦 Deliverables
### 1. End-to-End Test Suite (`code/tests/e2e-creator-network.test.ts`) ### 1. End-to-End Test Suite (`code/tests/e2e-creator-network.test.ts`)
**Status:** ✅ Complete (490 lines) **Status:** ✅ Complete (490 lines)
**Test Flows Covered:** **Test Flows Covered:**
- FLOW 1: Creator Registration & Profile Setup - FLOW 1: Creator Registration & Profile Setup
- Create 2 creator profiles with different arms - Create 2 creator profiles with different arms
- Verify profile data accuracy - Verify profile data accuracy
- FLOW 2: Opportunity Creation & Discovery - FLOW 2: Opportunity Creation & Discovery
- Create opportunities - Create opportunities
- Browse with filters - Browse with filters
- Pagination verification - Pagination verification
- FLOW 3: Creator Discovery & Profiles - FLOW 3: Creator Discovery & Profiles
- Browse creators with arm filters - Browse creators with arm filters
- Individual profile retrieval - Individual profile retrieval
- Profile data validation - Profile data validation
- FLOW 4: Application Submission & Tracking - FLOW 4: Application Submission & Tracking
- Submit applications - Submit applications
- Prevent duplicate applications - Prevent duplicate applications
- Get applications list - Get applications list
- Update application status - Update application status
- FLOW 5: DevConnect Linking - FLOW 5: DevConnect Linking
- Link DevConnect accounts - Link DevConnect accounts
- Get DevConnect links - Get DevConnect links
- Unlink accounts - Unlink accounts
- FLOW 6: Advanced Filtering & Search - FLOW 6: Advanced Filtering & Search
- Search creators - Search creators
- Filter opportunities - Filter opportunities
- Pagination testing - Pagination testing
**Features:** **Features:**
- Performance timing for each operation - Performance timing for each operation
- Detailed error messages - Detailed error messages
- Comprehensive test summary with pass/fail counts - Comprehensive test summary with pass/fail counts
### 2. Error Handling Test Suite (`code/tests/error-handling.test.ts`) ### 2. Error Handling Test Suite (`code/tests/error-handling.test.ts`)
**Status:** ✅ Complete (447 lines) **Status:** ✅ Complete (447 lines)
**Test Categories:** **Test Categories:**
1. **Input Validation Errors** (4 tests) 1. **Input Validation Errors** (4 tests)
- Missing required fields (user_id, username, title, opportunity_id) - Missing required fields (user_id, username, title, opportunity_id)
- Validation of mandatory parameters - Validation of mandatory parameters
2. **Not Found Errors** (3 tests) 2. **Not Found Errors** (3 tests)
- Non-existent creators, opportunities, applications - Non-existent creators, opportunities, applications
- 404 responses for missing resources - 404 responses for missing resources
3. **Authorization & Ownership Errors** (2 tests) 3. **Authorization & Ownership Errors** (2 tests)
- Invalid creator IDs - Invalid creator IDs
- Unauthorized access attempts - Unauthorized access attempts
4. **Duplicate & Conflict Errors** (2 tests) 4. **Duplicate & Conflict Errors** (2 tests)
- Duplicate username prevention - Duplicate username prevention
- Duplicate application prevention - Duplicate application prevention
5. **Missing Required Relationships** (2 tests) 5. **Missing Required Relationships** (2 tests)
- Creating opportunities without creator profile - Creating opportunities without creator profile
- Applying without creator profile - Applying without creator profile
6. **Invalid Query Parameters** (3 tests) 6. **Invalid Query Parameters** (3 tests)
- Invalid pagination parameters - Invalid pagination parameters
- Oversized limits - Oversized limits
- Invalid arm filters - Invalid arm filters
7. **Empty & Null Values** (2 tests) 7. **Empty & Null Values** (2 tests)
- Empty user_id and username - Empty user_id and username
- Empty search strings - Empty search strings
8. **DevConnect Linking Errors** (3 tests) 8. **DevConnect Linking Errors** (3 tests)
- Missing required fields - Missing required fields
- Non-existent creator - Non-existent creator
@ -85,10 +79,13 @@ Phase 3 successfully delivered comprehensive testing infrastructure for the AeTh
**Total:** 22 error handling test cases **Total:** 22 error handling test cases
### 3. Performance Test Suite (`code/tests/performance.test.ts`) ### 3. Performance Test Suite (`code/tests/performance.test.ts`)
**Status:** ✅ Complete (282 lines) **Status:** ✅ Complete (282 lines)
**Benchmarked Categories:** **Benchmarked Categories:**
1. **GET Endpoints** (Browse, Filter, Individual Retrieval) 1. **GET Endpoints** (Browse, Filter, Individual Retrieval)
- /api/creators (pagination) - /api/creators (pagination)
- /api/opportunities (pagination) - /api/opportunities (pagination)
- /api/applications - /api/applications
@ -99,11 +96,13 @@ Phase 3 successfully delivered comprehensive testing infrastructure for the AeTh
- /api/devconnect/link - /api/devconnect/link
2. **POST Endpoints** (Create Operations) 2. **POST Endpoints** (Create Operations)
- POST /api/creators - POST /api/creators
- POST /api/opportunities - POST /api/opportunities
- POST /api/applications - POST /api/applications
3. **PUT Endpoints** (Update Operations) 3. **PUT Endpoints** (Update Operations)
- PUT /api/creators/:id - PUT /api/creators/:id
- PUT /api/opportunities/:id - PUT /api/opportunities/:id
@ -112,6 +111,7 @@ Phase 3 successfully delivered comprehensive testing infrastructure for the AeTh
- Deep pagination - Deep pagination
**Metrics Collected:** **Metrics Collected:**
- Average response time (ms) - Average response time (ms)
- Min/Max response times - Min/Max response times
- P95/P99 percentiles - P95/P99 percentiles
@ -119,54 +119,66 @@ Phase 3 successfully delivered comprehensive testing infrastructure for the AeTh
- Performance target compliance - Performance target compliance
**Performance Targets:** **Performance Targets:**
- GET endpoints: < 100ms - GET endpoints: < 100ms
- POST endpoints: < 200ms - POST endpoints: < 200ms
- PUT endpoints: < 150ms - PUT endpoints: < 150ms
- Complex queries: < 250ms - Complex queries: < 250ms
### 4. Security Audit Checklist (`code/tests/SECURITY_AUDIT.md`) ### 4. Security Audit Checklist (`code/tests/SECURITY_AUDIT.md`)
**Status:** ✅ Complete (276 lines) **Status:** ✅ Complete (276 lines)
**Sections:** **Sections:**
1. **Authentication & Authorization** 1. **Authentication & Authorization**
- JWT validation - JWT validation
- User context extraction - User context extraction
- Authorization checks - Authorization checks
2. **Row Level Security (RLS) Policies** 2. **Row Level Security (RLS) Policies**
- Per-table RLS policies - Per-table RLS policies
- Visibility controls - Visibility controls
- Ownership enforcement - Ownership enforcement
3. **Data Protection** 3. **Data Protection**
- Sensitive data handling - Sensitive data handling
- Private field protection - Private field protection
- Rate limiting - Rate limiting
4. **Input Validation & Sanitization** 4. **Input Validation & Sanitization**
- Text field validation - Text field validation
- File upload security - File upload security
- Array field validation - Array field validation
- Numeric field validation - Numeric field validation
5. **API Endpoint Security** 5. **API Endpoint Security**
- Per-endpoint security checklist - Per-endpoint security checklist
- GET/POST/PUT/DELETE security - GET/POST/PUT/DELETE security
- Parameter validation - Parameter validation
6. **SQL Injection Prevention** 6. **SQL Injection Prevention**
- Parameterized queries - Parameterized queries
- Search/filter safety - Search/filter safety
7. **CORS & External Access** 7. **CORS & External Access**
- CORS headers - CORS headers
- URL validation - URL validation
8. **Audit Logging** 8. **Audit Logging**
- Critical action logging - Critical action logging
- Log retention - Log retention
9. **API Response Security** 9. **API Response Security**
- Error message safety - Error message safety
- Response headers - Response headers
@ -180,6 +192,7 @@ Phase 3 successfully delivered comprehensive testing infrastructure for the AeTh
## 📊 Testing Coverage ## 📊 Testing Coverage
### APIs Tested ### APIs Tested
- ✅ GET /api/creators (browse, filters, search, pagination) - ✅ GET /api/creators (browse, filters, search, pagination)
- ✅ GET /api/creators/:username (individual profile) - ✅ GET /api/creators/:username (individual profile)
- ✅ POST /api/creators (create profile) - ✅ POST /api/creators (create profile)
@ -197,6 +210,7 @@ Phase 3 successfully delivered comprehensive testing infrastructure for the AeTh
- ✅ DELETE /api/devconnect/link (unlink account) - ✅ DELETE /api/devconnect/link (unlink account)
### Test Scenarios Covered ### Test Scenarios Covered
- ✅ Complete user journeys (signup → profile → post → apply → track) - ✅ Complete user journeys (signup → profile → post → apply → track)
- ✅ Filtering and search functionality - ✅ Filtering and search functionality
- ✅ Pagination and sorting - ✅ Pagination and sorting
@ -213,6 +227,7 @@ Phase 3 successfully delivered comprehensive testing infrastructure for the AeTh
## 🎯 Key Findings ## 🎯 Key Findings
### Strengths ### Strengths
1. **Comprehensive API**: All creator network endpoints fully functional 1. **Comprehensive API**: All creator network endpoints fully functional
2. **Error Handling**: Proper HTTP status codes and error messages 2. **Error Handling**: Proper HTTP status codes and error messages
3. **Data Validation**: Required fields validated on all endpoints 3. **Data Validation**: Required fields validated on all endpoints
@ -220,6 +235,7 @@ Phase 3 successfully delivered comprehensive testing infrastructure for the AeTh
5. **Performance**: Response times within acceptable ranges 5. **Performance**: Response times within acceptable ranges
### Recommendations ### Recommendations
1. **Security**: Implement full RLS policies (see SECURITY_AUDIT.md) 1. **Security**: Implement full RLS policies (see SECURITY_AUDIT.md)
2. **Rate Limiting**: Add rate limiting to prevent abuse 2. **Rate Limiting**: Add rate limiting to prevent abuse
3. **Logging**: Implement audit logging for critical operations 3. **Logging**: Implement audit logging for critical operations
@ -229,6 +245,7 @@ Phase 3 successfully delivered comprehensive testing infrastructure for the AeTh
## 🚀 What's Next ## 🚀 What's Next
Phase 4: Onboarding Integration Phase 4: Onboarding Integration
- Integrate creator profile setup into signup flow - Integrate creator profile setup into signup flow
- Auto-create creator profiles on account creation - Auto-create creator profiles on account creation
- Collect creator preferences during onboarding - Collect creator preferences during onboarding

View file

@ -1,15 +1,18 @@
# Creator Network Security Audit Checklist # Creator Network Security Audit Checklist
## Phase 3: Testing & Validation ## Phase 3: Testing & Validation
### 🔐 Authentication & Authorization ### 🔐 Authentication & Authorization
- [ ] **JWT Validation** - [ ] **JWT Validation**
- [ ] All protected endpoints require valid JWT token - [ ] All protected endpoints require valid JWT token
- [ ] Expired tokens are rejected - [ ] Expired tokens are rejected
- [ ] Invalid/malformed tokens return 401 - [ ] Invalid/malformed tokens return 401
- [ ] Token claims are validated before processing - [ ] Token claims are validated before processing
- [ ] **User Context Extraction** - [ ] **User Context Extraction**
- [ ] user_id is extracted from Supabase auth context (not request body) - [ ] user_id is extracted from Supabase auth context (not request body)
- [ ] User cannot access/modify other users' data - [ ] User cannot access/modify other users' data
- [ ] Session invalidation works properly on logout - [ ] Session invalidation works properly on logout
@ -24,23 +27,27 @@
### 🛡️ Row Level Security (RLS) Policies ### 🛡️ Row Level Security (RLS) Policies
- [ ] **aethex_creators table** - [ ] **aethex_creators table**
- [ ] Users can read own profile - [ ] Users can read own profile
- [ ] Users can update own profile - [ ] Users can update own profile
- [ ] Public profiles are discoverable (is_discoverable=true) - [ ] Public profiles are discoverable (is_discoverable=true)
- [ ] Private profiles (is_discoverable=false) are hidden from directory - [ ] Private profiles (is_discoverable=false) are hidden from directory
- [ ] **aethex_opportunities table** - [ ] **aethex_opportunities table**
- [ ] Anyone can read open opportunities - [ ] Anyone can read open opportunities
- [ ] Only creator can update/delete own opportunities - [ ] Only creator can update/delete own opportunities
- [ ] Closed opportunities not visible to applicants - [ ] Closed opportunities not visible to applicants
- [ ] **aethex_applications table** - [ ] **aethex_applications table**
- [ ] Users can read their own applications - [ ] Users can read their own applications
- [ ] Applicant can only see their own applications - [ ] Applicant can only see their own applications
- [ ] Opportunity creator can see applications for their opportunities - [ ] Opportunity creator can see applications for their opportunities
- [ ] Users cannot access others' applications - [ ] Users cannot access others' applications
- [ ] **aethex_devconnect_links table** - [ ] **aethex_devconnect_links table**
- [ ] Users can only access their own DevConnect links - [ ] Users can only access their own DevConnect links
- [ ] Links cannot be modified by non-owners - [ ] Links cannot be modified by non-owners
@ -51,11 +58,13 @@
### 🔒 Data Protection ### 🔒 Data Protection
- [ ] **Sensitive Data** - [ ] **Sensitive Data**
- [ ] Passwords are never returned in API responses - [ ] Passwords are never returned in API responses
- [ ] Email addresses are not exposed in public profiles - [ ] Email addresses are not exposed in public profiles
- [ ] Private notes/applications are not leaked - [ ] Private notes/applications are not leaked
- [ ] **Cover Letters** - [ ] **Cover Letters**
- [ ] Only applicant and opportunity creator can see cover letters - [ ] Only applicant and opportunity creator can see cover letters
- [ ] Cover letters are not visible in search results - [ ] Cover letters are not visible in search results
@ -67,16 +76,19 @@
### 🚫 Input Validation & Sanitization ### 🚫 Input Validation & Sanitization
- [ ] **Text Fields** - [ ] **Text Fields**
- [ ] Bio/description max length enforced (e.g., 500 chars) - [ ] Bio/description max length enforced (e.g., 500 chars)
- [ ] Username format validated (alphanumeric, dashes, underscores) - [ ] Username format validated (alphanumeric, dashes, underscores)
- [ ] HTML/script tags are escaped in output - [ ] HTML/script tags are escaped in output
- [ ] **File Uploads** - [ ] **File Uploads**
- [ ] Avatar URLs are validated/whitelisted - [ ] Avatar URLs are validated/whitelisted
- [ ] No malicious file types accepted - [ ] No malicious file types accepted
- [ ] File size limits enforced - [ ] File size limits enforced
- [ ] **Array Fields** - [ ] **Array Fields**
- [ ] Skills array has max length - [ ] Skills array has max length
- [ ] Arm affiliations are from valid set - [ ] Arm affiliations are from valid set
- [ ] Invalid values are rejected - [ ] Invalid values are rejected
@ -89,16 +101,20 @@
### 🔗 API Endpoint Security ### 🔗 API Endpoint Security
**Creators Endpoints:** **Creators Endpoints:**
- [ ] GET /api/creators - [ ] GET /api/creators
- [ ] Pagination parameters validated - [ ] Pagination parameters validated
- [ ] Search doesn't expose private fields - [ ] Search doesn't expose private fields
- [ ] Arm filter works correctly - [ ] Arm filter works correctly
- [ ] GET /api/creators/:username - [ ] GET /api/creators/:username
- [ ] Returns 404 if profile is not discoverable - [ ] Returns 404 if profile is not discoverable
- [ ] No sensitive data leaked - [ ] No sensitive data leaked
- [ ] POST /api/creators - [ ] POST /api/creators
- [ ] Requires auth - [ ] Requires auth
- [ ] user_id extracted from auth context - [ ] user_id extracted from auth context
- [ ] Duplicate username prevention works - [ ] Duplicate username prevention works
@ -109,16 +125,20 @@
- [ ] No privilege escalation possible - [ ] No privilege escalation possible
**Opportunities Endpoints:** **Opportunities Endpoints:**
- [ ] GET /api/opportunities - [ ] GET /api/opportunities
- [ ] Only open opportunities shown - [ ] Only open opportunities shown
- [ ] Closed/draft opportunities hidden - [ ] Closed/draft opportunities hidden
- [ ] Pagination and filters work - [ ] Pagination and filters work
- [ ] GET /api/opportunities/:id - [ ] GET /api/opportunities/:id
- [ ] Only returns open opportunities - [ ] Only returns open opportunities
- [ ] Creator info is sanitized - [ ] Creator info is sanitized
- [ ] POST /api/opportunities - [ ] POST /api/opportunities
- [ ] Requires auth + creator profile - [ ] Requires auth + creator profile
- [ ] user_id extracted from auth - [ ] user_id extracted from auth
- [ ] Only opportunity creator can post - [ ] Only opportunity creator can post
@ -129,18 +149,22 @@
- [ ] Can't change posted_by_id - [ ] Can't change posted_by_id
**Applications Endpoints:** **Applications Endpoints:**
- [ ] GET /api/applications - [ ] GET /api/applications
- [ ] Requires user_id + auth - [ ] Requires user_id + auth
- [ ] Users only see their own applications - [ ] Users only see their own applications
- [ ] Opportunity creators can view applications - [ ] Opportunity creators can view applications
- [ ] POST /api/applications - [ ] POST /api/applications
- [ ] Requires auth + creator profile - [ ] Requires auth + creator profile
- [ ] Validates opportunity exists - [ ] Validates opportunity exists
- [ ] Prevents duplicate applications - [ ] Prevents duplicate applications
- [ ] Validates cover letter length - [ ] Validates cover letter length
- [ ] PUT /api/applications/:id - [ ] PUT /api/applications/:id
- [ ] Requires auth - [ ] Requires auth
- [ ] Only opportunity creator can update - [ ] Only opportunity creator can update
- [ ] Can only change status/response_message - [ ] Can only change status/response_message
@ -152,12 +176,15 @@
- [ ] Application is properly deleted - [ ] Application is properly deleted
**DevConnect Endpoints:** **DevConnect Endpoints:**
- [ ] POST /api/devconnect/link - [ ] POST /api/devconnect/link
- [ ] Requires auth + creator profile - [ ] Requires auth + creator profile
- [ ] user_id from auth context - [ ] user_id from auth context
- [ ] Validates DevConnect username format - [ ] Validates DevConnect username format
- [ ] GET /api/devconnect/link - [ ] GET /api/devconnect/link
- [ ] Requires user_id + auth - [ ] Requires user_id + auth
- [ ] Users only see their own link - [ ] Users only see their own link
- [ ] Returns null if not linked - [ ] Returns null if not linked
@ -170,6 +197,7 @@
### 🔍 SQL Injection Prevention ### 🔍 SQL Injection Prevention
- [ ] **Parameterized Queries** - [ ] **Parameterized Queries**
- [ ] All Supabase queries use parameterized queries (not string concatenation) - [ ] All Supabase queries use parameterized queries (not string concatenation)
- [ ] User input never directly in SQL strings - [ ] User input never directly in SQL strings
- [ ] Search queries are sanitized - [ ] Search queries are sanitized
@ -182,6 +210,7 @@
### 🌐 CORS & External Access ### 🌐 CORS & External Access
- [ ] **CORS Headers** - [ ] **CORS Headers**
- [ ] Only allowed origins can call API - [ ] Only allowed origins can call API
- [ ] Credentials are properly scoped - [ ] Credentials are properly scoped
- [ ] Preflight requests handled correctly - [ ] Preflight requests handled correctly
@ -194,6 +223,7 @@
### 📋 Audit Logging ### 📋 Audit Logging
- [ ] **Critical Actions Logged** - [ ] **Critical Actions Logged**
- [ ] User account creation - [ ] User account creation
- [ ] Opportunity creation/deletion - [ ] Opportunity creation/deletion
- [ ] Application status changes - [ ] Application status changes
@ -208,6 +238,7 @@
### 🔄 API Response Security ### 🔄 API Response Security
- [ ] **Error Messages** - [ ] **Error Messages**
- [ ] Don't leak system details - [ ] Don't leak system details
- [ ] Don't expose database structure - [ ] Don't expose database structure
- [ ] Generic error messages for auth failures - [ ] Generic error messages for auth failures
@ -222,11 +253,13 @@
### 📱 Frontend Security ### 📱 Frontend Security
- [ ] **Token Management** - [ ] **Token Management**
- [ ] Tokens stored securely (not localStorage if possible) - [ ] Tokens stored securely (not localStorage if possible)
- [ ] Tokens cleared on logout - [ ] Tokens cleared on logout
- [ ] Token refresh handled properly - [ ] Token refresh handled properly
- [ ] **XSS Prevention** - [ ] **XSS Prevention**
- [ ] User input escaped in templates - [ ] User input escaped in templates
- [ ] No dangerouslySetInnerHTML without sanitization - [ ] No dangerouslySetInnerHTML without sanitization
- [ ] No eval() or similar dangerous functions - [ ] No eval() or similar dangerous functions
@ -238,17 +271,20 @@
### ✅ Testing Recommendations ### ✅ Testing Recommendations
1. **Penetration Testing** 1. **Penetration Testing**
- Test SQL injection attempts - Test SQL injection attempts
- Test XSS payloads in input fields - Test XSS payloads in input fields
- Test CSRF attacks - Test CSRF attacks
- Test broken access control - Test broken access control
2. **Authorization Testing** 2. **Authorization Testing**
- Try accessing other users' resources - Try accessing other users' resources
- Test privilege escalation attempts - Test privilege escalation attempts
- Verify RLS policies are enforced - Verify RLS policies are enforced
3. **Data Validation Testing** 3. **Data Validation Testing**
- Send oversized inputs - Send oversized inputs
- Send malformed data - Send malformed data
- Test boundary values - Test boundary values
@ -269,7 +305,6 @@
--- ---
**Audit Date:** _________________ **Audit Date:** ********\_********
**Auditor:** _________________ **Auditor:** ********\_********
**Status:** PENDING ⏳ **Status:** PENDING ⏳

View file

@ -19,14 +19,14 @@ const log = (result: TestResult) => {
results.push(result); results.push(result);
const symbol = result.status === "✓" ? "✓" : "✗"; const symbol = result.status === "✓" ? "✓" : "✗";
console.log( console.log(
`${symbol} ${result.method.padEnd(6)} ${result.endpoint.padEnd(40)} - ${result.message}` `${symbol} ${result.method.padEnd(6)} ${result.endpoint.padEnd(40)} - ${result.message}`,
); );
}; };
const testEndpoint = async ( const testEndpoint = async (
method: string, method: string,
endpoint: string, endpoint: string,
body?: any body?: any,
): Promise<any> => { ): Promise<any> => {
try { try {
const options: RequestInit = { const options: RequestInit = {
@ -56,7 +56,7 @@ async function runTests() {
try { try {
const { response, data } = await testEndpoint( const { response, data } = await testEndpoint(
"GET", "GET",
"/api/creators?page=1&limit=10" "/api/creators?page=1&limit=10",
); );
if (response.ok) { if (response.ok) {
log({ log({
@ -89,7 +89,7 @@ async function runTests() {
try { try {
const { response, data } = await testEndpoint( const { response, data } = await testEndpoint(
"GET", "GET",
"/api/creators/testuser123" "/api/creators/testuser123",
); );
if (response.status === 404) { if (response.status === 404) {
log({ log({
@ -178,7 +178,7 @@ async function runTests() {
try { try {
const { response, data } = await testEndpoint( const { response, data } = await testEndpoint(
"GET", "GET",
"/api/opportunities?page=1&limit=10" "/api/opportunities?page=1&limit=10",
); );
if (response.ok) { if (response.ok) {
log({ log({
@ -211,7 +211,7 @@ async function runTests() {
try { try {
const { response, data } = await testEndpoint( const { response, data } = await testEndpoint(
"GET", "GET",
"/api/opportunities/fake-id-123" "/api/opportunities/fake-id-123",
); );
if (response.status === 404) { if (response.status === 404) {
log({ log({
@ -251,7 +251,7 @@ async function runTests() {
description: "A test job opportunity", description: "A test job opportunity",
job_type: "contract", job_type: "contract",
arm_affiliation: "gameforge", arm_affiliation: "gameforge",
} },
); );
if (response.status === 404) { if (response.status === 404) {
@ -320,7 +320,7 @@ async function runTests() {
{ {
user_id: "test-user-123", user_id: "test-user-123",
devconnect_username: "testuser", devconnect_username: "testuser",
} },
); );
if (response.status === 404) { if (response.status === 404) {
@ -363,12 +363,14 @@ async function runTests() {
const passed = results.filter((r) => r.status === "✓").length; const passed = results.filter((r) => r.status === "✓").length;
const failed = results.filter((r) => r.status === "✗").length; const failed = results.filter((r) => r.status === "✗").length;
console.log( console.log(
`\nTest Summary: ${passed} passed, ${failed} failed out of ${results.length} tests` `\nTest Summary: ${passed} passed, ${failed} failed out of ${results.length} tests`,
); );
if (failed > 0) { if (failed > 0) {
console.log("\n❌ Failed tests:"); console.log("\n❌ Failed tests:");
results.filter((r) => r.status === "✗").forEach((r) => { results
.filter((r) => r.status === "✗")
.forEach((r) => {
console.log(` - ${r.method} ${r.endpoint}: ${r.message}`); console.log(` - ${r.method} ${r.endpoint}: ${r.message}`);
}); });
} else { } else {

View file

@ -21,9 +21,14 @@ const results: TestCase[] = [];
const BASE_URL = "http://localhost:5173"; const BASE_URL = "http://localhost:5173";
// Test utilities // Test utilities
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const test = (name: string, passed: boolean, message: string, duration: number = 0) => { const test = (
name: string,
passed: boolean,
message: string,
duration: number = 0,
) => {
results.push({ name, passed, message, duration }); results.push({ name, passed, message, duration });
const symbol = passed ? "✓" : "✗"; const symbol = passed ? "✓" : "✗";
console.log(`${symbol} ${name}`); console.log(`${symbol} ${name}`);
@ -43,7 +48,12 @@ const assertExists = (value: any, msg: string) => {
} }
}; };
const assertInRange = (actual: number, min: number, max: number, msg: string) => { const assertInRange = (
actual: number,
min: number,
max: number,
msg: string,
) => {
if (actual < min || actual > max) { if (actual < min || actual > max) {
throw new Error(`${msg}: value ${actual} not in range [${min}, ${max}]`); throw new Error(`${msg}: value ${actual} not in range [${min}, ${max}]`);
} }
@ -69,7 +79,7 @@ async function runE2ETests() {
// FLOW 1: Creator Registration and Profile Setup // FLOW 1: Creator Registration and Profile Setup
console.log("\n📝 FLOW 1: Creator Registration & Profile Setup"); console.log("\n📝 FLOW 1: Creator Registration & Profile Setup");
console.log("=" .repeat(50)); console.log("=".repeat(50));
try { try {
// Create first creator profile // Create first creator profile
@ -95,13 +105,21 @@ async function runE2ETests() {
"Create creator profile 1", "Create creator profile 1",
createRes1.status === 201, createRes1.status === 201,
`Status: ${createRes1.status}`, `Status: ${createRes1.status}`,
createCreator1Duration createCreator1Duration,
); );
if (createRes1.ok) { if (createRes1.ok) {
assertExists(creator1Data.id, "Creator ID should exist"); assertExists(creator1Data.id, "Creator ID should exist");
assertEquals(creator1Data.username, testUsers.creator1.username, "Username mismatch"); assertEquals(
assertEquals(creator1Data.primary_arm, "gameforge", "Primary arm mismatch"); creator1Data.username,
testUsers.creator1.username,
"Username mismatch",
);
assertEquals(
creator1Data.primary_arm,
"gameforge",
"Primary arm mismatch",
);
} }
// Create second creator profile // Create second creator profile
@ -127,7 +145,7 @@ async function runE2ETests() {
"Create creator profile 2", "Create creator profile 2",
createRes2.status === 201, createRes2.status === 201,
`Status: ${createRes2.status}`, `Status: ${createRes2.status}`,
createCreator2Duration createCreator2Duration,
); );
} catch (error: any) { } catch (error: any) {
test("Create creator profiles", false, error.message); test("Create creator profiles", false, error.message);
@ -135,7 +153,7 @@ async function runE2ETests() {
// FLOW 2: Opportunity Creation // FLOW 2: Opportunity Creation
console.log("\n📋 FLOW 2: Opportunity Creation & Discovery"); console.log("\n📋 FLOW 2: Opportunity Creation & Discovery");
console.log("=" .repeat(50)); console.log("=".repeat(50));
let opportunityId: string | null = null; let opportunityId: string | null = null;
@ -148,7 +166,8 @@ async function runE2ETests() {
body: JSON.stringify({ body: JSON.stringify({
user_id: testUsers.creator1.id, user_id: testUsers.creator1.id,
title: "Senior Game Dev - Unity Project", title: "Senior Game Dev - Unity Project",
description: "Looking for experienced Unity developer for 6-month contract", description:
"Looking for experienced Unity developer for 6-month contract",
job_type: "contract", job_type: "contract",
salary_min: 80000, salary_min: 80000,
salary_max: 120000, salary_max: 120000,
@ -163,20 +182,24 @@ async function runE2ETests() {
"Create opportunity", "Create opportunity",
oppRes.status === 201, oppRes.status === 201,
`Status: ${oppRes.status}`, `Status: ${oppRes.status}`,
createOppDuration createOppDuration,
); );
if (oppRes.ok) { if (oppRes.ok) {
opportunityId = oppData.id; opportunityId = oppData.id;
assertExists(oppData.id, "Opportunity ID should exist"); assertExists(oppData.id, "Opportunity ID should exist");
assertEquals(oppData.title, "Senior Game Dev - Unity Project", "Title mismatch"); assertEquals(
oppData.title,
"Senior Game Dev - Unity Project",
"Title mismatch",
);
assertEquals(oppData.status, "open", "Status should be open"); assertEquals(oppData.status, "open", "Status should be open");
} }
// Browse opportunities with filters // Browse opportunities with filters
const browseOppStart = Date.now(); const browseOppStart = Date.now();
const browseRes = await fetch( const browseRes = await fetch(
`${BASE_URL}/api/opportunities?arm=gameforge&page=1&limit=10` `${BASE_URL}/api/opportunities?arm=gameforge&page=1&limit=10`,
); );
const browseData = await browseRes.json(); const browseData = await browseRes.json();
const browseOppDuration = Date.now() - browseOppStart; const browseOppDuration = Date.now() - browseOppStart;
@ -185,7 +208,7 @@ async function runE2ETests() {
"Browse opportunities with filters", "Browse opportunities with filters",
browseRes.ok && Array.isArray(browseData.data), browseRes.ok && Array.isArray(browseData.data),
`Status: ${browseRes.status}, Found: ${browseData.data?.length || 0}`, `Status: ${browseRes.status}, Found: ${browseData.data?.length || 0}`,
browseOppDuration browseOppDuration,
); );
if (browseRes.ok) { if (browseRes.ok) {
@ -198,13 +221,13 @@ async function runE2ETests() {
// FLOW 3: Creator Discovery // FLOW 3: Creator Discovery
console.log("\n👥 FLOW 3: Creator Discovery & Profiles"); console.log("\n👥 FLOW 3: Creator Discovery & Profiles");
console.log("=" .repeat(50)); console.log("=".repeat(50));
try { try {
// Browse creators // Browse creators
const browseCreatorsStart = Date.now(); const browseCreatorsStart = Date.now();
const creatorsRes = await fetch( const creatorsRes = await fetch(
`${BASE_URL}/api/creators?arm=gameforge&page=1&limit=20` `${BASE_URL}/api/creators?arm=gameforge&page=1&limit=20`,
); );
const creatorsData = await creatorsRes.json(); const creatorsData = await creatorsRes.json();
const browseCreatorsDuration = Date.now() - browseCreatorsStart; const browseCreatorsDuration = Date.now() - browseCreatorsStart;
@ -213,13 +236,13 @@ async function runE2ETests() {
"Browse creators with arm filter", "Browse creators with arm filter",
creatorsRes.ok && Array.isArray(creatorsData.data), creatorsRes.ok && Array.isArray(creatorsData.data),
`Status: ${creatorsRes.status}, Found: ${creatorsData.data?.length || 0}`, `Status: ${creatorsRes.status}, Found: ${creatorsData.data?.length || 0}`,
browseCreatorsDuration browseCreatorsDuration,
); );
// Get individual creator profile // Get individual creator profile
const getCreatorStart = Date.now(); const getCreatorStart = Date.now();
const creatorRes = await fetch( const creatorRes = await fetch(
`${BASE_URL}/api/creators/${testUsers.creator1.username}` `${BASE_URL}/api/creators/${testUsers.creator1.username}`,
); );
const creatorData = await creatorRes.json(); const creatorData = await creatorRes.json();
const getCreatorDuration = Date.now() - getCreatorStart; const getCreatorDuration = Date.now() - getCreatorStart;
@ -228,13 +251,17 @@ async function runE2ETests() {
"Get creator profile by username", "Get creator profile by username",
creatorRes.ok && creatorData.username === testUsers.creator1.username, creatorRes.ok && creatorData.username === testUsers.creator1.username,
`Status: ${creatorRes.status}, Username: ${creatorData.username}`, `Status: ${creatorRes.status}, Username: ${creatorData.username}`,
getCreatorDuration getCreatorDuration,
); );
if (creatorRes.ok) { if (creatorRes.ok) {
assertExists(creatorData.bio, "Bio should exist"); assertExists(creatorData.bio, "Bio should exist");
assertExists(creatorData.skills, "Skills should exist"); assertExists(creatorData.skills, "Skills should exist");
assertEquals(Array.isArray(creatorData.arm_affiliations), true, "Arm affiliations should be array"); assertEquals(
Array.isArray(creatorData.arm_affiliations),
true,
"Arm affiliations should be array",
);
} }
} catch (error: any) { } catch (error: any) {
test("Creator discovery and profiles", false, error.message); test("Creator discovery and profiles", false, error.message);
@ -242,7 +269,7 @@ async function runE2ETests() {
// FLOW 4: Application Submission & Tracking // FLOW 4: Application Submission & Tracking
console.log("\n✉ FLOW 4: Apply for Opportunity & Track Status"); console.log("\n✉ FLOW 4: Apply for Opportunity & Track Status");
console.log("=" .repeat(50)); console.log("=".repeat(50));
let applicationId: string | null = null; let applicationId: string | null = null;
@ -270,7 +297,7 @@ async function runE2ETests() {
"Submit application", "Submit application",
applyRes.status === 201, applyRes.status === 201,
`Status: ${applyRes.status}`, `Status: ${applyRes.status}`,
applyDuration applyDuration,
); );
if (applyRes.ok) { if (applyRes.ok) {
@ -295,13 +322,13 @@ async function runE2ETests() {
"Prevent duplicate applications", "Prevent duplicate applications",
dupRes.status === 400, dupRes.status === 400,
`Status: ${dupRes.status} (should be 400)`, `Status: ${dupRes.status} (should be 400)`,
0 0,
); );
// Get applications for creator // Get applications for creator
const getAppsStart = Date.now(); const getAppsStart = Date.now();
const appsRes = await fetch( const appsRes = await fetch(
`${BASE_URL}/api/applications?user_id=${testUsers.creator2.id}` `${BASE_URL}/api/applications?user_id=${testUsers.creator2.id}`,
); );
const appsData = await appsRes.json(); const appsData = await appsRes.json();
const getAppsDuration = Date.now() - getAppsStart; const getAppsDuration = Date.now() - getAppsStart;
@ -310,13 +337,15 @@ async function runE2ETests() {
"Get creator's applications", "Get creator's applications",
appsRes.ok && Array.isArray(appsData.data), appsRes.ok && Array.isArray(appsData.data),
`Status: ${appsRes.status}, Found: ${appsData.data?.length || 0}`, `Status: ${appsRes.status}, Found: ${appsData.data?.length || 0}`,
getAppsDuration getAppsDuration,
); );
// Update application status (as opportunity creator) // Update application status (as opportunity creator)
if (applicationId) { if (applicationId) {
const updateStart = Date.now(); const updateStart = Date.now();
const updateRes = await fetch(`${BASE_URL}/api/applications/${applicationId}`, { const updateRes = await fetch(
`${BASE_URL}/api/applications/${applicationId}`,
{
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
@ -324,14 +353,15 @@ async function runE2ETests() {
status: "accepted", status: "accepted",
response_message: "Great! We'd love to have you on board.", response_message: "Great! We'd love to have you on board.",
}), }),
}); },
);
const updateDuration = Date.now() - updateStart; const updateDuration = Date.now() - updateStart;
test( test(
"Update application status", "Update application status",
updateRes.ok, updateRes.ok,
`Status: ${updateRes.status}`, `Status: ${updateRes.status}`,
updateDuration updateDuration,
); );
} }
} catch (error: any) { } catch (error: any) {
@ -340,7 +370,7 @@ async function runE2ETests() {
// FLOW 5: DevConnect Linking // FLOW 5: DevConnect Linking
console.log("\n<><6E> FLOW 5: DevConnect Account Linking"); console.log("\n<><6E> FLOW 5: DevConnect Account Linking");
console.log("=" .repeat(50)); console.log("=".repeat(50));
try { try {
// Link DevConnect account // Link DevConnect account
@ -361,13 +391,13 @@ async function runE2ETests() {
"Link DevConnect account", "Link DevConnect account",
linkRes.status === 201 || linkRes.status === 200, linkRes.status === 201 || linkRes.status === 200,
`Status: ${linkRes.status}`, `Status: ${linkRes.status}`,
linkDuration linkDuration,
); );
// Get DevConnect link // Get DevConnect link
const getLinkStart = Date.now(); const getLinkStart = Date.now();
const getLinkRes = await fetch( const getLinkRes = await fetch(
`${BASE_URL}/api/devconnect/link?user_id=${testUsers.creator1.id}` `${BASE_URL}/api/devconnect/link?user_id=${testUsers.creator1.id}`,
); );
const getLinkData = await getLinkRes.json(); const getLinkData = await getLinkRes.json();
const getLinkDuration = Date.now() - getLinkStart; const getLinkDuration = Date.now() - getLinkStart;
@ -376,11 +406,15 @@ async function runE2ETests() {
"Get DevConnect link", "Get DevConnect link",
getLinkRes.ok && getLinkData.data, getLinkRes.ok && getLinkData.data,
`Status: ${getLinkRes.status}`, `Status: ${getLinkRes.status}`,
getLinkDuration getLinkDuration,
); );
if (getLinkRes.ok && getLinkData.data) { if (getLinkRes.ok && getLinkData.data) {
assertEquals(getLinkData.data.devconnect_username, "devconnect_user_1", "Username mismatch"); assertEquals(
getLinkData.data.devconnect_username,
"devconnect_user_1",
"Username mismatch",
);
} }
// Unlink DevConnect account // Unlink DevConnect account
@ -394,7 +428,7 @@ async function runE2ETests() {
"Unlink DevConnect account", "Unlink DevConnect account",
unlinkRes.ok, unlinkRes.ok,
`Status: ${unlinkRes.status}`, `Status: ${unlinkRes.status}`,
0 0,
); );
} catch (error: any) { } catch (error: any) {
test("DevConnect linking", false, error.message); test("DevConnect linking", false, error.message);
@ -402,13 +436,13 @@ async function runE2ETests() {
// FLOW 6: Filtering and Search // FLOW 6: Filtering and Search
console.log("\n🔍 FLOW 6: Advanced Filtering & Search"); console.log("\n🔍 FLOW 6: Advanced Filtering & Search");
console.log("=" .repeat(50)); console.log("=".repeat(50));
try { try {
// Test creator search // Test creator search
const searchStart = Date.now(); const searchStart = Date.now();
const searchRes = await fetch( const searchRes = await fetch(
`${BASE_URL}/api/creators?search=${testUsers.creator1.username.substring(0, 5)}` `${BASE_URL}/api/creators?search=${testUsers.creator1.username.substring(0, 5)}`,
); );
const searchData = await searchRes.json(); const searchData = await searchRes.json();
const searchDuration = Date.now() - searchStart; const searchDuration = Date.now() - searchStart;
@ -417,13 +451,13 @@ async function runE2ETests() {
"Search creators by name", "Search creators by name",
searchRes.ok && Array.isArray(searchData.data), searchRes.ok && Array.isArray(searchData.data),
`Status: ${searchRes.status}, Found: ${searchData.data?.length || 0}`, `Status: ${searchRes.status}, Found: ${searchData.data?.length || 0}`,
searchDuration searchDuration,
); );
// Test opportunity filtering by experience level // Test opportunity filtering by experience level
const expFilterStart = Date.now(); const expFilterStart = Date.now();
const expRes = await fetch( const expRes = await fetch(
`${BASE_URL}/api/opportunities?experienceLevel=senior` `${BASE_URL}/api/opportunities?experienceLevel=senior`,
); );
const expData = await expRes.json(); const expData = await expRes.json();
const expFilterDuration = Date.now() - expFilterStart; const expFilterDuration = Date.now() - expFilterStart;
@ -432,7 +466,7 @@ async function runE2ETests() {
"Filter opportunities by experience level", "Filter opportunities by experience level",
expRes.ok && Array.isArray(expData.data), expRes.ok && Array.isArray(expData.data),
`Status: ${expRes.status}, Found: ${expData.data?.length || 0}`, `Status: ${expRes.status}, Found: ${expData.data?.length || 0}`,
expFilterDuration expFilterDuration,
); );
// Test pagination // Test pagination
@ -445,10 +479,13 @@ async function runE2ETests() {
"Pagination - page 1", "Pagination - page 1",
page1Res.ok && page1Data.pagination?.page === 1, page1Res.ok && page1Data.pagination?.page === 1,
`Page: ${page1Data.pagination?.page}, Limit: ${page1Data.pagination?.limit}`, `Page: ${page1Data.pagination?.page}, Limit: ${page1Data.pagination?.limit}`,
page1Duration page1Duration,
); );
assertExists(page1Data.pagination?.pages, "Total pages should be calculated"); assertExists(
page1Data.pagination?.pages,
"Total pages should be calculated",
);
} catch (error: any) { } catch (error: any) {
test("Filtering and search", false, error.message); test("Filtering and search", false, error.message);
} }
@ -463,12 +500,16 @@ async function runE2ETests() {
console.log(` ✓ Passed: ${passed}`); console.log(` ✓ Passed: ${passed}`);
console.log(` ✗ Failed: ${failed}`); console.log(` ✗ Failed: ${failed}`);
console.log(` Total: ${results.length}`); console.log(` Total: ${results.length}`);
console.log(` Duration: ${totalDuration}ms (avg ${(totalDuration / results.length).toFixed(0)}ms per test)`); console.log(
` Duration: ${totalDuration}ms (avg ${(totalDuration / results.length).toFixed(0)}ms per test)`,
);
console.log("\n" + "=".repeat(50)); console.log("\n" + "=".repeat(50));
if (failed > 0) { if (failed > 0) {
console.log("\n❌ Failed Tests:"); console.log("\n❌ Failed Tests:");
results.filter((r) => !r.passed).forEach((r) => { results
.filter((r) => !r.passed)
.forEach((r) => {
console.log(` - ${r.name}: ${r.message}`); console.log(` - ${r.name}: ${r.message}`);
}); });
} else { } else {

View file

@ -19,12 +19,12 @@ const test = (
passed: boolean, passed: boolean,
expectedStatus: number, expectedStatus: number,
actualStatus: number, actualStatus: number,
message: string message: string,
) => { ) => {
results.push({ name, passed, expectedStatus, actualStatus, message }); results.push({ name, passed, expectedStatus, actualStatus, message });
const symbol = passed ? "✓" : "✗"; const symbol = passed ? "✓" : "✗";
console.log( console.log(
`${symbol} ${name.padEnd(50)} | Expected: ${expectedStatus}, Got: ${actualStatus}` `${symbol} ${name.padEnd(50)} | Expected: ${expectedStatus}, Got: ${actualStatus}`,
); );
}; };
@ -46,7 +46,7 @@ async function runErrorTests() {
res.status === 400, res.status === 400,
400, 400,
res.status, res.status,
"Should require user_id" "Should require user_id",
); );
res = await fetch(`${BASE_URL}/api/creators`, { res = await fetch(`${BASE_URL}/api/creators`, {
@ -59,7 +59,7 @@ async function runErrorTests() {
res.status === 400, res.status === 400,
400, 400,
res.status, res.status,
"Should require username" "Should require username",
); );
res = await fetch(`${BASE_URL}/api/opportunities`, { res = await fetch(`${BASE_URL}/api/opportunities`, {
@ -72,7 +72,7 @@ async function runErrorTests() {
res.status === 400, res.status === 400,
400, 400,
res.status, res.status,
"Should require title" "Should require title",
); );
res = await fetch(`${BASE_URL}/api/applications`, { res = await fetch(`${BASE_URL}/api/applications`, {
@ -85,7 +85,7 @@ async function runErrorTests() {
res.status === 400, res.status === 400,
400, 400,
res.status, res.status,
"Should require opportunity_id" "Should require opportunity_id",
); );
// ERROR CATEGORY 2: Not Found Errors // ERROR CATEGORY 2: Not Found Errors
@ -98,7 +98,7 @@ async function runErrorTests() {
res.status === 404, res.status === 404,
404, 404,
res.status, res.status,
"Should return 404 for non-existent creator" "Should return 404 for non-existent creator",
); );
res = await fetch(`${BASE_URL}/api/opportunities/fake-opp-id-99999`); res = await fetch(`${BASE_URL}/api/opportunities/fake-opp-id-99999`);
@ -107,7 +107,7 @@ async function runErrorTests() {
res.status === 404, res.status === 404,
404, 404,
res.status, res.status,
"Should return 404 for non-existent opportunity" "Should return 404 for non-existent opportunity",
); );
res = await fetch(`${BASE_URL}/api/applications?user_id=nonexistent-user`); res = await fetch(`${BASE_URL}/api/applications?user_id=nonexistent-user`);
@ -116,7 +116,7 @@ async function runErrorTests() {
res.status === 404, res.status === 404,
404, 404,
res.status, res.status,
"Should return 404 when creator doesn't exist" "Should return 404 when creator doesn't exist",
); );
// ERROR CATEGORY 3: Authorization/Ownership Errors // ERROR CATEGORY 3: Authorization/Ownership Errors
@ -154,7 +154,7 @@ async function runErrorTests() {
res.status === 500 || res.status === 404, res.status === 500 || res.status === 404,
404, 404,
res.status, res.status,
"Should reject invalid creator ID" "Should reject invalid creator ID",
); );
} }
@ -188,7 +188,7 @@ async function runErrorTests() {
res.status === 400, res.status === 400,
400, 400,
res.status, res.status,
"Should prevent duplicate usernames" "Should prevent duplicate usernames",
); );
// Test duplicate application (create opportunity first) // Test duplicate application (create opportunity first)
@ -260,7 +260,7 @@ async function runErrorTests() {
res.status === 400, res.status === 400,
400, 400,
res.status, res.status,
"Should prevent duplicate applications" "Should prevent duplicate applications",
); );
} }
} }
@ -283,7 +283,7 @@ async function runErrorTests() {
res.status === 404, res.status === 404,
404, 404,
res.status, res.status,
"Should require existing creator profile" "Should require existing creator profile",
); );
res = await fetch(`${BASE_URL}/api/applications`, { res = await fetch(`${BASE_URL}/api/applications`, {
@ -299,7 +299,7 @@ async function runErrorTests() {
res.status === 404, res.status === 404,
404, 404,
res.status, res.status,
"Should require existing creator profile" "Should require existing creator profile",
); );
// ERROR CATEGORY 6: Invalid Query Parameters // ERROR CATEGORY 6: Invalid Query Parameters
@ -313,7 +313,7 @@ async function runErrorTests() {
res.ok, // Should still work with default pagination res.ok, // Should still work with default pagination
200, 200,
res.status, res.status,
"Should handle invalid page gracefully" "Should handle invalid page gracefully",
); );
res = await fetch(`${BASE_URL}/api/opportunities?limit=999999`); res = await fetch(`${BASE_URL}/api/opportunities?limit=999999`);
@ -322,7 +322,7 @@ async function runErrorTests() {
res.ok, // Should cap the limit res.ok, // Should cap the limit
200, 200,
res.status, res.status,
"Should cap maximum limit" "Should cap maximum limit",
); );
res = await fetch(`${BASE_URL}/api/creators?arm=invalid_arm`); res = await fetch(`${BASE_URL}/api/creators?arm=invalid_arm`);
@ -332,7 +332,7 @@ async function runErrorTests() {
res.ok && Array.isArray(armData.data), res.ok && Array.isArray(armData.data),
200, 200,
res.status, res.status,
"Should return empty results or handle gracefully" "Should return empty results or handle gracefully",
); );
// ERROR CATEGORY 7: Empty/Null Values // ERROR CATEGORY 7: Empty/Null Values
@ -352,7 +352,7 @@ async function runErrorTests() {
res.status === 400, res.status === 400,
400, 400,
res.status, res.status,
"Should reject empty user_id" "Should reject empty user_id",
); );
res = await fetch(`${BASE_URL}/api/creators?search=`); res = await fetch(`${BASE_URL}/api/creators?search=`);
@ -361,7 +361,7 @@ async function runErrorTests() {
res.ok, res.ok,
200, 200,
res.status, res.status,
"Should handle empty search gracefully" "Should handle empty search gracefully",
); );
// ERROR CATEGORY 8: Missing DevConnect Parameters // ERROR CATEGORY 8: Missing DevConnect Parameters
@ -378,7 +378,7 @@ async function runErrorTests() {
res.status === 400, res.status === 400,
400, 400,
res.status, res.status,
"Should require user_id" "Should require user_id",
); );
res = await fetch(`${BASE_URL}/api/devconnect/link`, { res = await fetch(`${BASE_URL}/api/devconnect/link`, {
@ -391,7 +391,7 @@ async function runErrorTests() {
res.status === 400, res.status === 400,
400, 400,
res.status, res.status,
"Should require devconnect_username" "Should require devconnect_username",
); );
res = await fetch(`${BASE_URL}/api/devconnect/link`, { res = await fetch(`${BASE_URL}/api/devconnect/link`, {
@ -407,7 +407,7 @@ async function runErrorTests() {
res.status === 404, res.status === 404,
404, 404,
res.status, res.status,
"Should require existing creator profile" "Should require existing creator profile",
); );
// Summary // Summary
@ -426,7 +426,9 @@ async function runErrorTests() {
.filter((r) => !r.passed) .filter((r) => !r.passed)
.forEach((r) => { .forEach((r) => {
console.log(` - ${r.name}`); console.log(` - ${r.name}`);
console.log(` Expected ${r.expectedStatus}, got ${r.actualStatus}: ${r.message}`); console.log(
` Expected ${r.expectedStatus}, got ${r.actualStatus}: ${r.message}`,
);
}); });
} else { } else {
console.log("\n✅ All error handling tests passed!"); console.log("\n✅ All error handling tests passed!");

View file

@ -23,7 +23,7 @@ const metrics: PerformanceMetric[] = [];
async function measureRequest( async function measureRequest(
method: string, method: string,
endpoint: string, endpoint: string,
body?: any body?: any,
): Promise<number> { ): Promise<number> {
const start = performance.now(); const start = performance.now();
try { try {
@ -45,10 +45,10 @@ async function benchmarkEndpoint(
method: string, method: string,
endpoint: string, endpoint: string,
numRequests: number = 50, numRequests: number = 50,
body?: any body?: any,
): Promise<PerformanceMetric> { ): Promise<PerformanceMetric> {
console.log( console.log(
` Benchmarking ${method.padEnd(6)} ${endpoint.padEnd(35)} (${numRequests} requests)...` ` Benchmarking ${method.padEnd(6)} ${endpoint.padEnd(35)} (${numRequests} requests)...`,
); );
const times: number[] = []; const times: number[] = [];
@ -91,7 +91,11 @@ async function runPerformanceTests() {
console.log("\nFilter endpoints (filtered queries):"); console.log("\nFilter endpoints (filtered queries):");
await benchmarkEndpoint("GET", "/api/creators?arm=gameforge", 50); await benchmarkEndpoint("GET", "/api/creators?arm=gameforge", 50);
await benchmarkEndpoint("GET", "/api/opportunities?arm=gameforge&sort=recent", 50); await benchmarkEndpoint(
"GET",
"/api/opportunities?arm=gameforge&sort=recent",
50,
);
await benchmarkEndpoint("GET", "/api/creators?search=test", 40); await benchmarkEndpoint("GET", "/api/creators?search=test", 40);
console.log("\nIndividual resource retrieval:"); console.log("\nIndividual resource retrieval:");
@ -155,12 +159,12 @@ async function runPerformanceTests() {
await benchmarkEndpoint( await benchmarkEndpoint(
"GET", "GET",
"/api/creators?arm=gameforge&search=test&page=1&limit=50", "/api/creators?arm=gameforge&search=test&page=1&limit=50",
30 30,
); );
await benchmarkEndpoint( await benchmarkEndpoint(
"GET", "GET",
"/api/opportunities?arm=labs&experienceLevel=senior&jobType=full-time&page=1&limit=50", "/api/opportunities?arm=labs&experienceLevel=senior&jobType=full-time&page=1&limit=50",
25 25,
); );
console.log("\nMulti-page traversal:"); console.log("\nMulti-page traversal:");
@ -182,10 +186,15 @@ async function runPerformanceTests() {
// Print detailed metrics // Print detailed metrics
grouped.forEach((metricList, endpoint) => { grouped.forEach((metricList, endpoint) => {
const avg = metricList.reduce((sum, m) => sum + m.avgTime, 0) / metricList.length; const avg =
const p95 = metricList.reduce((sum, m) => sum + m.p95Time, 0) / metricList.length; metricList.reduce((sum, m) => sum + m.avgTime, 0) / metricList.length;
const p99 = metricList.reduce((sum, m) => sum + m.p99Time, 0) / metricList.length; const p95 =
const rps = metricList.reduce((sum, m) => sum + m.requestsPerSecond, 0) / metricList.length; metricList.reduce((sum, m) => sum + m.p95Time, 0) / metricList.length;
const p99 =
metricList.reduce((sum, m) => sum + m.p99Time, 0) / metricList.length;
const rps =
metricList.reduce((sum, m) => sum + m.requestsPerSecond, 0) /
metricList.length;
console.log(`📍 ${endpoint}`); console.log(`📍 ${endpoint}`);
console.log(` Avg: ${avg.toFixed(2)}ms`); console.log(` Avg: ${avg.toFixed(2)}ms`);
@ -210,21 +219,30 @@ async function runPerformanceTests() {
metrics.forEach((m) => { metrics.forEach((m) => {
if (m.method === "GET") { if (m.method === "GET") {
targets["GET endpoints"].actual = Math.max(targets["GET endpoints"].actual, m.avgTime); targets["GET endpoints"].actual = Math.max(
targets["GET endpoints"].actual,
m.avgTime,
);
} else if (m.method === "POST") { } else if (m.method === "POST") {
targets["POST endpoints"].actual = Math.max(targets["POST endpoints"].actual, m.avgTime); targets["POST endpoints"].actual = Math.max(
targets["POST endpoints"].actual,
m.avgTime,
);
} else if (m.method === "PUT") { } else if (m.method === "PUT") {
targets["PUT endpoints"].actual = Math.max(targets["PUT endpoints"].actual, m.avgTime); targets["PUT endpoints"].actual = Math.max(
targets["PUT endpoints"].actual,
m.avgTime,
);
} }
}); });
// Check complex queries // Check complex queries
const complexQueries = metrics.filter( const complexQueries = metrics.filter(
(m) => m.endpoint.includes("?") && m.endpoint.split("&").length > 2 (m) => m.endpoint.includes("?") && m.endpoint.split("&").length > 2,
); );
if (complexQueries.length > 0) { if (complexQueries.length > 0) {
targets["Complex queries"].actual = Math.max( targets["Complex queries"].actual = Math.max(
...complexQueries.map((m) => m.avgTime) ...complexQueries.map((m) => m.avgTime),
); );
} }
@ -234,7 +252,7 @@ async function runPerformanceTests() {
const passed = data.actual <= data.target; const passed = data.actual <= data.target;
const symbol = passed ? "✓" : "✗"; const symbol = passed ? "✓" : "✗";
console.log( console.log(
`${symbol} ${category.padEnd(20)} ${data.actual.toFixed(2)}ms ${data.threshold}` `${symbol} ${category.padEnd(20)} ${data.actual.toFixed(2)}ms ${data.threshold}`,
); );
if (passed) targetsPassed++; if (passed) targetsPassed++;
@ -247,24 +265,32 @@ async function runPerformanceTests() {
const allTimes = metrics.map((m) => m.avgTime); const allTimes = metrics.map((m) => m.avgTime);
const slowestEndpoint = metrics.reduce((a, b) => const slowestEndpoint = metrics.reduce((a, b) =>
a.avgTime > b.avgTime ? a : b a.avgTime > b.avgTime ? a : b,
); );
const fastestEndpoint = metrics.reduce((a, b) => const fastestEndpoint = metrics.reduce((a, b) =>
a.avgTime < b.avgTime ? a : b a.avgTime < b.avgTime ? a : b,
); );
console.log(`Total Endpoints Tested: ${metrics.length}`); console.log(`Total Endpoints Tested: ${metrics.length}`);
console.log(`Average Response Time: ${(allTimes.reduce((a, b) => a + b) / allTimes.length).toFixed(2)}ms`);
console.log(`Fastest: ${fastestEndpoint.method} ${fastestEndpoint.endpoint.split("?")[0]} (${fastestEndpoint.avgTime.toFixed(2)}ms)`);
console.log(`Slowest: ${slowestEndpoint.method} ${slowestEndpoint.endpoint.split("?")[0]} (${slowestEndpoint.avgTime.toFixed(2)}ms)`);
console.log( console.log(
`\nPerformance Targets: ${targetsPassed} passed, ${targetsFailed} failed` `Average Response Time: ${(allTimes.reduce((a, b) => a + b) / allTimes.length).toFixed(2)}ms`,
);
console.log(
`Fastest: ${fastestEndpoint.method} ${fastestEndpoint.endpoint.split("?")[0]} (${fastestEndpoint.avgTime.toFixed(2)}ms)`,
);
console.log(
`Slowest: ${slowestEndpoint.method} ${slowestEndpoint.endpoint.split("?")[0]} (${slowestEndpoint.avgTime.toFixed(2)}ms)`,
);
console.log(
`\nPerformance Targets: ${targetsPassed} passed, ${targetsFailed} failed`,
); );
if (targetsFailed === 0) { if (targetsFailed === 0) {
console.log("\n✅ All performance targets met!"); console.log("\n✅ All performance targets met!");
} else { } else {
console.log(`\n⚠ ${targetsFailed} performance targets not met. Optimization needed.`); console.log(
`\n⚠ ${targetsFailed} performance targets not met. Optimization needed.`,
);
} }
console.log("\n" + "=".repeat(70)); console.log("\n" + "=".repeat(70));