Prettier format pending files
This commit is contained in:
parent
b3f92fe96d
commit
0afd536c9b
53 changed files with 1467 additions and 720 deletions
|
|
@ -5,10 +5,7 @@ const supabaseServiceRole = process.env.SUPABASE_SERVICE_ROLE || "";
|
|||
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceRole);
|
||||
|
||||
export async function getMyApplications(
|
||||
req: Request,
|
||||
userId: string
|
||||
) {
|
||||
export async function getMyApplications(req: Request, userId: string) {
|
||||
const url = new URL(req.url);
|
||||
const page = parseInt(url.searchParams.get("page") || "1");
|
||||
const limit = parseInt(url.searchParams.get("limit") || "10");
|
||||
|
|
@ -25,7 +22,7 @@ export async function getMyApplications(
|
|||
if (!creator) {
|
||||
return new Response(
|
||||
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,
|
||||
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);
|
||||
|
||||
|
|
@ -73,20 +70,20 @@ export async function getMyApplications(
|
|||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error fetching applications:", error);
|
||||
return new Response(
|
||||
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(
|
||||
opportunityId: string,
|
||||
userId: string
|
||||
userId: string,
|
||||
) {
|
||||
try {
|
||||
// Verify user owns this opportunity
|
||||
|
|
@ -97,10 +94,10 @@ export async function getApplicationsForOpportunity(
|
|||
.single();
|
||||
|
||||
if (!opportunity) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Opportunity not found" }),
|
||||
{ status: 404, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
return new Response(JSON.stringify({ error: "Opportunity not found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const { data: creator } = await supabase
|
||||
|
|
@ -110,10 +107,10 @@ export async function getApplicationsForOpportunity(
|
|||
.single();
|
||||
|
||||
if (creator?.id !== opportunity.posted_by_id) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Unauthorized" }),
|
||||
{ status: 403, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
|
|
@ -126,7 +123,7 @@ export async function getApplicationsForOpportunity(
|
|||
cover_letter,
|
||||
applied_at,
|
||||
aethex_creators(username, avatar_url, bio, skills)
|
||||
`
|
||||
`,
|
||||
)
|
||||
.eq("opportunity_id", opportunityId)
|
||||
.order("applied_at", { ascending: false });
|
||||
|
|
@ -141,15 +138,12 @@ export async function getApplicationsForOpportunity(
|
|||
console.error("Error fetching opportunity applications:", error);
|
||||
return new Response(
|
||||
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(
|
||||
req: Request,
|
||||
userId: string
|
||||
) {
|
||||
export async function submitApplication(req: Request, userId: string) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { opportunity_id, cover_letter } = body;
|
||||
|
|
@ -164,7 +158,7 @@ export async function submitApplication(
|
|||
if (!creator) {
|
||||
return new Response(
|
||||
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) {
|
||||
return new Response(
|
||||
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) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "You have already applied to this opportunity" }),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
JSON.stringify({
|
||||
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);
|
||||
return new Response(
|
||||
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(
|
||||
req: Request,
|
||||
applicationId: string,
|
||||
userId: string
|
||||
userId: string,
|
||||
) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
|
|
@ -241,16 +237,16 @@ export async function updateApplicationStatus(
|
|||
id,
|
||||
opportunity_id,
|
||||
aethex_opportunities(posted_by_id)
|
||||
`
|
||||
`,
|
||||
)
|
||||
.eq("id", applicationId)
|
||||
.single();
|
||||
|
||||
if (!application) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Application not found" }),
|
||||
{ status: 404, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
return new Response(JSON.stringify({ error: "Application not found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const { data: creator } = await supabase
|
||||
|
|
@ -260,10 +256,10 @@ export async function updateApplicationStatus(
|
|||
.single();
|
||||
|
||||
if (creator?.id !== application.aethex_opportunities.posted_by_id) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Unauthorized" }),
|
||||
{ status: 403, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
|
|
@ -287,14 +283,14 @@ export async function updateApplicationStatus(
|
|||
console.error("Error updating application:", error);
|
||||
return new Response(
|
||||
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(
|
||||
applicationId: string,
|
||||
userId: string
|
||||
userId: string,
|
||||
) {
|
||||
try {
|
||||
// Verify user owns this application
|
||||
|
|
@ -305,10 +301,10 @@ export async function withdrawApplication(
|
|||
.single();
|
||||
|
||||
if (!application) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Application not found" }),
|
||||
{ status: 404, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
return new Response(JSON.stringify({ error: "Application not found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const { data: creator } = await supabase
|
||||
|
|
@ -318,10 +314,10 @@ export async function withdrawApplication(
|
|||
.single();
|
||||
|
||||
if (creator?.id !== application.creator_id) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Unauthorized" }),
|
||||
{ status: 403, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
|
|
@ -339,7 +335,7 @@ export async function withdrawApplication(
|
|||
console.error("Error withdrawing application:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Failed to withdraw application" }),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export async function getCreators(req: Request) {
|
|||
created_at,
|
||||
aethex_projects(count)
|
||||
`,
|
||||
{ count: "exact" }
|
||||
{ count: "exact" },
|
||||
)
|
||||
.eq("is_discoverable", true)
|
||||
.order("created_at", { ascending: false });
|
||||
|
|
@ -38,9 +38,7 @@ export async function getCreators(req: Request) {
|
|||
}
|
||||
|
||||
if (search) {
|
||||
query = query.or(
|
||||
`username.ilike.%${search}%,bio.ilike.%${search}%`
|
||||
);
|
||||
query = query.or(`username.ilike.%${search}%,bio.ilike.%${search}%`);
|
||||
}
|
||||
|
||||
const start = (page - 1) * limit;
|
||||
|
|
@ -63,14 +61,14 @@ export async function getCreators(req: Request) {
|
|||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error fetching creators:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Failed to fetch creators" }),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
return new Response(JSON.stringify({ error: "Failed to fetch creators" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +90,7 @@ export async function getCreatorByUsername(username: string) {
|
|||
updated_at,
|
||||
aethex_projects(id, title, description, url, image_url, tags, is_featured),
|
||||
aethex_skill_endorsements(skill, count)
|
||||
`
|
||||
`,
|
||||
)
|
||||
.eq("username", username)
|
||||
.eq("is_discoverable", true)
|
||||
|
|
@ -117,10 +115,7 @@ export async function getCreatorByUsername(username: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function createCreatorProfile(
|
||||
req: Request,
|
||||
userId: string
|
||||
) {
|
||||
export async function createCreatorProfile(req: Request, userId: string) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const {
|
||||
|
|
@ -158,15 +153,12 @@ export async function createCreatorProfile(
|
|||
console.error("Error creating creator profile:", error);
|
||||
return new Response(
|
||||
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(
|
||||
req: Request,
|
||||
userId: string
|
||||
) {
|
||||
export async function updateCreatorProfile(req: Request, userId: string) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const {
|
||||
|
|
@ -209,15 +201,12 @@ export async function updateCreatorProfile(
|
|||
console.error("Error updating creator profile:", error);
|
||||
return new Response(
|
||||
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(
|
||||
req: Request,
|
||||
creatorId: string
|
||||
) {
|
||||
export async function addProjectToCreator(req: Request, creatorId: string) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { title, description, url, image_url, tags, is_featured } = body;
|
||||
|
|
@ -244,17 +233,17 @@ export async function addProjectToCreator(
|
|||
});
|
||||
} catch (error) {
|
||||
console.error("Error adding project:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Failed to add project" }),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
return new Response(JSON.stringify({ error: "Failed to add project" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function endorseSkill(
|
||||
req: Request,
|
||||
creatorId: string,
|
||||
endorsedByUserId: string
|
||||
endorsedByUserId: string,
|
||||
) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
|
|
@ -270,7 +259,7 @@ export async function endorseSkill(
|
|||
if (!endorsingCreator) {
|
||||
return new Response(
|
||||
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) {
|
||||
console.error("Error endorsing skill:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Failed to endorse skill" }),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
return new Response(JSON.stringify({ error: "Failed to endorse skill" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,7 @@ const supabaseServiceRole = process.env.SUPABASE_SERVICE_ROLE || "";
|
|||
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceRole);
|
||||
|
||||
export async function linkDevConnectAccount(
|
||||
req: Request,
|
||||
userId: string
|
||||
) {
|
||||
export async function linkDevConnectAccount(req: Request, userId: string) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { devconnect_username, devconnect_profile_url } = body;
|
||||
|
|
@ -16,7 +13,7 @@ export async function linkDevConnectAccount(
|
|||
if (!devconnect_username) {
|
||||
return new Response(
|
||||
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) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Creator profile not found. Create profile first." }),
|
||||
{ status: 404, headers: { "Content-Type": "application/json" } }
|
||||
JSON.stringify({
|
||||
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")
|
||||
.update({
|
||||
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)
|
||||
.select()
|
||||
|
|
@ -66,7 +67,9 @@ export async function linkDevConnectAccount(
|
|||
.insert({
|
||||
aethex_creator_id: creator.id,
|
||||
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()
|
||||
.single();
|
||||
|
|
@ -89,7 +92,7 @@ export async function linkDevConnectAccount(
|
|||
console.error("Error linking DevConnect account:", error);
|
||||
return new Response(
|
||||
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) {
|
||||
return new Response(
|
||||
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);
|
||||
return new Response(
|
||||
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) {
|
||||
return new Response(
|
||||
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);
|
||||
return new Response(
|
||||
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(
|
||||
req: Request,
|
||||
userId: string
|
||||
) {
|
||||
export async function verifyDevConnectLink(req: Request, userId: string) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { verification_code } = body;
|
||||
|
|
@ -193,7 +193,7 @@ export async function verifyDevConnectLink(
|
|||
if (!creator) {
|
||||
return new Response(
|
||||
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);
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Failed to verify DevConnect link" }),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export async function getOpportunities(req: Request) {
|
|||
created_at,
|
||||
aethex_applications(count)
|
||||
`,
|
||||
{ count: "exact" }
|
||||
{ count: "exact" },
|
||||
)
|
||||
.eq("status", "open");
|
||||
|
||||
|
|
@ -43,9 +43,7 @@ export async function getOpportunities(req: Request) {
|
|||
}
|
||||
|
||||
if (search) {
|
||||
query = query.or(
|
||||
`title.ilike.%${search}%,description.ilike.%${search}%`
|
||||
);
|
||||
query = query.or(`title.ilike.%${search}%,description.ilike.%${search}%`);
|
||||
}
|
||||
|
||||
if (jobType) {
|
||||
|
|
@ -80,13 +78,13 @@ export async function getOpportunities(req: Request) {
|
|||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error fetching opportunities:", error);
|
||||
return new Response(
|
||||
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,
|
||||
updated_at,
|
||||
aethex_applications(count)
|
||||
`
|
||||
`,
|
||||
)
|
||||
.eq("id", opportunityId)
|
||||
.eq("status", "open")
|
||||
|
|
@ -126,10 +124,7 @@ export async function getOpportunityById(opportunityId: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function createOpportunity(
|
||||
req: Request,
|
||||
userId: string
|
||||
) {
|
||||
export async function createOpportunity(req: Request, userId: string) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const {
|
||||
|
|
@ -151,8 +146,10 @@ export async function createOpportunity(
|
|||
|
||||
if (!creator) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Creator profile not found. Create profile first." }),
|
||||
{ status: 404, headers: { "Content-Type": "application/json" } }
|
||||
JSON.stringify({
|
||||
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);
|
||||
return new Response(
|
||||
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(
|
||||
req: Request,
|
||||
opportunityId: string,
|
||||
userId: string
|
||||
userId: string,
|
||||
) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
|
|
@ -212,10 +209,10 @@ export async function updateOpportunity(
|
|||
.single();
|
||||
|
||||
if (!opportunity) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Opportunity not found" }),
|
||||
{ status: 404, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
return new Response(JSON.stringify({ error: "Opportunity not found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const { data: creator } = await supabase
|
||||
|
|
@ -225,10 +222,10 @@ export async function updateOpportunity(
|
|||
.single();
|
||||
|
||||
if (creator?.id !== opportunity.posted_by_id) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Unauthorized" }),
|
||||
{ status: 403, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
|
|
@ -257,15 +254,12 @@ export async function updateOpportunity(
|
|||
console.error("Error updating opportunity:", error);
|
||||
return new Response(
|
||||
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(
|
||||
opportunityId: string,
|
||||
userId: string
|
||||
) {
|
||||
export async function closeOpportunity(opportunityId: string, userId: string) {
|
||||
try {
|
||||
// Verify user owns this opportunity
|
||||
const { data: opportunity } = await supabase
|
||||
|
|
@ -275,10 +269,10 @@ export async function closeOpportunity(
|
|||
.single();
|
||||
|
||||
if (!opportunity) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Opportunity not found" }),
|
||||
{ status: 404, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
return new Response(JSON.stringify({ error: "Opportunity not found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const { data: creator } = await supabase
|
||||
|
|
@ -288,10 +282,10 @@ export async function closeOpportunity(
|
|||
.single();
|
||||
|
||||
if (creator?.id !== opportunity.posted_by_id) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Unauthorized" }),
|
||||
{ status: 403, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
|
|
@ -314,7 +308,7 @@ export async function closeOpportunity(
|
|||
console.error("Error closing opportunity:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Failed to close opportunity" }),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,10 @@ const App = () => (
|
|||
/>
|
||||
<Route path="/profile" 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
|
||||
|
|
@ -183,9 +186,15 @@ const App = () => (
|
|||
|
||||
{/* Creator Network routes */}
|
||||
<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/:id" element={<OpportunityDetail />} />
|
||||
<Route
|
||||
path="/opportunities/:id"
|
||||
element={<OpportunityDetail />}
|
||||
/>
|
||||
|
||||
{/* Service routes */}
|
||||
<Route path="/game-development" element={<GameDevelopment />} />
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export async function getMyApplications(filters?: {
|
|||
|
||||
export async function getApplicationsForOpportunity(opportunityId: string) {
|
||||
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");
|
||||
const data = await response.json();
|
||||
|
|
@ -88,20 +88,28 @@ export async function updateApplicationStatus(
|
|||
data: {
|
||||
status: "reviewing" | "accepted" | "rejected";
|
||||
response_message?: string;
|
||||
}
|
||||
},
|
||||
): Promise<Application> {
|
||||
const response = await fetch(`${API_BASE}/api/applications/${applicationId}/status`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/applications/${applicationId}/status`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
if (!response.ok) throw new Error("Failed to update application");
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function withdrawApplication(applicationId: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/api/applications/${applicationId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
export async function withdrawApplication(
|
||||
applicationId: string,
|
||||
): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/applications/${applicationId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
if (!response.ok) throw new Error("Failed to withdraw application");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,35 +130,50 @@ export async function addProject(data: {
|
|||
return response.json();
|
||||
}
|
||||
|
||||
export async function updateProject(projectId: string, data: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
url?: string;
|
||||
image_url?: string;
|
||||
tags?: string[];
|
||||
is_featured?: boolean;
|
||||
}): Promise<Project> {
|
||||
const response = await fetch(`${API_BASE}/api/creators/me/projects/${projectId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
export async function updateProject(
|
||||
projectId: string,
|
||||
data: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
url?: string;
|
||||
image_url?: string;
|
||||
tags?: string[];
|
||||
is_featured?: boolean;
|
||||
},
|
||||
): Promise<Project> {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/creators/me/projects/${projectId}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
if (!response.ok) throw new Error("Failed to update project");
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function deleteProject(projectId: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/api/creators/me/projects/${projectId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/creators/me/projects/${projectId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
if (!response.ok) throw new Error("Failed to delete project");
|
||||
}
|
||||
|
||||
export async function endorseSkill(creatorId: string, skill: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/api/creators/${creatorId}/endorse`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ skill }),
|
||||
});
|
||||
export async function endorseSkill(
|
||||
creatorId: string,
|
||||
skill: string,
|
||||
): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/creators/${creatorId}/endorse`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ skill }),
|
||||
},
|
||||
);
|
||||
if (!response.ok) throw new Error("Failed to endorse skill");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ export async function unlinkDevConnectAccount(): Promise<void> {
|
|||
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`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@ export async function getOpportunities(filters?: {
|
|||
if (filters?.arm) params.append("arm", filters.arm);
|
||||
if (filters?.search) params.append("search", filters.search);
|
||||
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?.page) params.append("page", String(filters.page));
|
||||
if (filters?.limit) params.append("limit", String(filters.limit));
|
||||
|
|
@ -73,7 +74,9 @@ export async function getOpportunityById(id: string): Promise<Opportunity> {
|
|||
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`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -85,7 +88,7 @@ export async function createOpportunity(data: CreateOpportunityData): Promise<Op
|
|||
|
||||
export async function updateOpportunity(
|
||||
id: string,
|
||||
data: Partial<CreateOpportunityData>
|
||||
data: Partial<CreateOpportunityData>,
|
||||
): Promise<Opportunity> {
|
||||
const response = await fetch(`${API_BASE}/api/opportunities/${id}`, {
|
||||
method: "PUT",
|
||||
|
|
@ -104,7 +107,9 @@ export async function closeOpportunity(id: string): Promise<void> {
|
|||
}
|
||||
|
||||
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");
|
||||
return response.json();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ export interface ArmBadgeProps {
|
|||
|
||||
export function ArmBadge({ arm, size = "md" }: ArmBadgeProps) {
|
||||
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 (
|
||||
<Badge className={`${colors.bg} border-0 ${colors.text} ${sizeClass}`}>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ export function CreatorCard({ creator }: CreatorCardProps) {
|
|||
<div className="flex items-start justify-between mb-4">
|
||||
<Avatar className="h-16 w-16">
|
||||
<AvatarImage src={creator.avatar_url} alt={creator.username} />
|
||||
<AvatarFallback>{creator.username.charAt(0).toUpperCase()}</AvatarFallback>
|
||||
<AvatarFallback>
|
||||
{creator.username.charAt(0).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{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">
|
||||
|
|
|
|||
|
|
@ -47,8 +47,10 @@ export function DevConnectLinkModal({
|
|||
onSuccess?.();
|
||||
} catch (error) {
|
||||
toast(
|
||||
error instanceof Error ? error.message : "Failed to link DevConnect account",
|
||||
"error"
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to link DevConnect account",
|
||||
"error",
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
|
@ -61,7 +63,8 @@ export function DevConnectLinkModal({
|
|||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Link Your DevConnect Account</AlertDialogTitle>
|
||||
<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>
|
||||
</AlertDialogHeader>
|
||||
|
||||
|
|
@ -79,7 +82,8 @@ export function DevConnectLinkModal({
|
|||
/>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ export function OpportunityCard({ opportunity }: OpportunityCardProps) {
|
|||
|
||||
const formatSalary = (min?: number, max?: number) => {
|
||||
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 (max) return `Up to $${max.toLocaleString()}`;
|
||||
};
|
||||
|
|
@ -41,11 +42,15 @@ export function OpportunityCard({ opportunity }: OpportunityCardProps) {
|
|||
<div className="flex items-start gap-3 flex-1">
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarImage src={poster.avatar_url} alt={poster.username} />
|
||||
<AvatarFallback>{poster.username.charAt(0).toUpperCase()}</AvatarFallback>
|
||||
<AvatarFallback>
|
||||
{poster.username.charAt(0).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<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 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">
|
||||
<Clock className="h-4 w-4 mr-2 text-blue-400" />
|
||||
{opportunity.aethex_applications.count}{" "}
|
||||
{opportunity.aethex_applications.count === 1 ? "applicant" : "applicants"}
|
||||
{opportunity.aethex_applications.count === 1
|
||||
? "applicant"
|
||||
: "applicants"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,10 +15,34 @@ interface CreatorProfileProps {
|
|||
}
|
||||
|
||||
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: "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 },
|
||||
{
|
||||
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: "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 = [
|
||||
|
|
@ -48,7 +72,11 @@ export default function CreatorProfile({
|
|||
totalSteps,
|
||||
}: CreatorProfileProps) {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const creatorData = data.creatorProfile || { bio: "", skills: [], primaryArm: "" };
|
||||
const creatorData = data.creatorProfile || {
|
||||
bio: "",
|
||||
skills: [],
|
||||
primaryArm: "",
|
||||
};
|
||||
|
||||
const canProceed = useMemo(() => {
|
||||
return creatorData.primaryArm && creatorData.skills.length > 0;
|
||||
|
|
@ -138,7 +166,9 @@ export default function CreatorProfile({
|
|||
</div>
|
||||
<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>
|
||||
</button>
|
||||
|
|
@ -150,7 +180,10 @@ export default function CreatorProfile({
|
|||
{/* Skills Selection */}
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
|
||||
{/* Skills Input */}
|
||||
|
|
@ -221,11 +254,7 @@ export default function CreatorProfile({
|
|||
|
||||
{/* Navigation */}
|
||||
<div className="flex gap-3 pt-8 border-t border-gray-800">
|
||||
<Button
|
||||
onClick={prevStep}
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
>
|
||||
<Button onClick={prevStep} variant="outline" className="flex-1">
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
import Layout from "@/components/Layout";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
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() {
|
||||
const pillars = [
|
||||
|
|
@ -11,9 +20,16 @@ export default function About() {
|
|||
color: "from-yellow-500 to-orange-500",
|
||||
icon: Beaker,
|
||||
tagline: "The Innovation Engine",
|
||||
description: "Bleeding-edge R&D focused on proprietary technology and thought leadership",
|
||||
focus: ["Advanced AI & Machine Learning", "Next-gen Web Architectures", "Procedural Content Generation", "Graphics & Performance Optimization"],
|
||||
function: "High-burn speculative research creating lasting competitive advantage",
|
||||
description:
|
||||
"Bleeding-edge R&D focused on proprietary technology and thought leadership",
|
||||
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",
|
||||
|
|
@ -21,9 +37,16 @@ export default function About() {
|
|||
color: "from-blue-500 to-cyan-500",
|
||||
icon: Briefcase,
|
||||
tagline: "The Profit Engine",
|
||||
description: "High-margin consulting and enterprise solutions subsidizing R&D investment",
|
||||
focus: ["Custom Software Development", "Enterprise Consulting", "Game Development Services", "Digital Transformation"],
|
||||
function: "Stable revenue generation and financial discipline for investor confidence",
|
||||
description:
|
||||
"High-margin consulting and enterprise solutions subsidizing R&D investment",
|
||||
focus: [
|
||||
"Custom Software Development",
|
||||
"Enterprise Consulting",
|
||||
"Game Development Services",
|
||||
"Digital Transformation",
|
||||
],
|
||||
function:
|
||||
"Stable revenue generation and financial discipline for investor confidence",
|
||||
},
|
||||
{
|
||||
id: "foundation",
|
||||
|
|
@ -31,9 +54,16 @@ export default function About() {
|
|||
color: "from-red-500 to-pink-500",
|
||||
icon: Heart,
|
||||
tagline: "Community & Education",
|
||||
description: "Open-source and educational mission building goodwill and talent pipeline",
|
||||
focus: ["Open-Source Code Repositories", "Educational Curriculum", "Community Workshops", "Talent Development"],
|
||||
function: "Goodwill moat, specialized talent pipeline, and ecosystem growth",
|
||||
description:
|
||||
"Open-source and educational mission building goodwill and talent pipeline",
|
||||
focus: [
|
||||
"Open-Source Code Repositories",
|
||||
"Educational Curriculum",
|
||||
"Community Workshops",
|
||||
"Talent Development",
|
||||
],
|
||||
function:
|
||||
"Goodwill moat, specialized talent pipeline, and ecosystem growth",
|
||||
},
|
||||
{
|
||||
id: "devlink",
|
||||
|
|
@ -41,28 +71,38 @@ export default function About() {
|
|||
color: "from-cyan-500 to-blue-500",
|
||||
icon: Network,
|
||||
tagline: "Talent Network",
|
||||
description: "B2B SaaS platform for specialized professional networking and recruitment",
|
||||
focus: ["Recruitment Platform", "Talent Matching", "Professional Network", "Career Development"],
|
||||
function: "Access-based moat and lock-in effect for specialized human capital",
|
||||
description:
|
||||
"B2B SaaS platform for specialized professional networking and recruitment",
|
||||
focus: [
|
||||
"Recruitment Platform",
|
||||
"Talent Matching",
|
||||
"Professional Network",
|
||||
"Career Development",
|
||||
],
|
||||
function:
|
||||
"Access-based moat and lock-in effect for specialized human capital",
|
||||
},
|
||||
];
|
||||
|
||||
const moats = [
|
||||
{
|
||||
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,
|
||||
color: "bg-yellow-500/20 border-yellow-500/40 text-yellow-300",
|
||||
},
|
||||
{
|
||||
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,
|
||||
color: "bg-cyan-500/20 border-cyan-500/40 text-cyan-300",
|
||||
},
|
||||
{
|
||||
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,
|
||||
color: "bg-red-500/20 border-red-500/40 text-red-300",
|
||||
},
|
||||
|
|
@ -71,22 +111,26 @@ export default function About() {
|
|||
const benefits = [
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
|
@ -98,10 +142,17 @@ export default function About() {
|
|||
<section className="py-16 lg:py-24 border-b border-gray-800">
|
||||
<div className="container mx-auto max-w-6xl px-4">
|
||||
<h1 className="text-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>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -114,29 +165,44 @@ export default function About() {
|
|||
{pillars.map((pillar) => {
|
||||
const Icon = pillar.icon;
|
||||
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>
|
||||
<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" />
|
||||
</div>
|
||||
<CardTitle>
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-gray-300">{pillar.description}</p>
|
||||
<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">
|
||||
{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>
|
||||
</div>
|
||||
<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-sm text-gray-300">{pillar.function}</p>
|
||||
<p className="text-xs font-semibold text-gray-400 mb-2">
|
||||
STRATEGIC FUNCTION
|
||||
</p>
|
||||
<p className="text-sm text-gray-300">
|
||||
{pillar.function}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -149,21 +215,30 @@ export default function About() {
|
|||
{/* Competitive Moats */}
|
||||
<section className="py-16 border-b border-gray-800">
|
||||
<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">
|
||||
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>
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{moats.map((moat, idx) => {
|
||||
const Icon = moat.icon;
|
||||
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>
|
||||
<Icon className="h-8 w-8 mb-4" />
|
||||
<CardTitle className="text-lg">{moat.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-300">{moat.description}</p>
|
||||
<p className="text-sm text-gray-300">
|
||||
{moat.description}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
|
@ -185,8 +260,12 @@ export default function About() {
|
|||
<div className="flex gap-4">
|
||||
<Icon className="h-6 w-6 text-blue-400 flex-shrink-0" />
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">{benefit.title}</h3>
|
||||
<p className="text-sm text-gray-400">{benefit.description}</p>
|
||||
<h3 className="font-semibold mb-2">
|
||||
{benefit.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
{benefit.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -208,9 +287,14 @@ export default function About() {
|
|||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<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>
|
||||
<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>
|
||||
</Card>
|
||||
<Card className="bg-gray-900/50 border-gray-800">
|
||||
|
|
@ -219,9 +303,14 @@ export default function About() {
|
|||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<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>
|
||||
<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>
|
||||
</Card>
|
||||
<Card className="bg-gray-900/50 border-gray-800">
|
||||
|
|
@ -230,9 +319,14 @@ export default function About() {
|
|||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<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>
|
||||
<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>
|
||||
</Card>
|
||||
<Card className="bg-gray-900/50 border-gray-800">
|
||||
|
|
@ -241,9 +335,13 @@ export default function About() {
|
|||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<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>
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1154,29 +1154,56 @@ export default function Admin() {
|
|||
<Lightbulb className="h-5 w-5 text-yellow-400" />
|
||||
<CardTitle>Labs - Research & Innovation</CardTitle>
|
||||
</div>
|
||||
<CardDescription>Research initiatives, publications, and innovation pipeline</CardDescription>
|
||||
<CardDescription>
|
||||
Research initiatives, publications, and innovation
|
||||
pipeline
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<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">
|
||||
<p className="text-sm text-muted-foreground mb-2">Active Research Projects</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>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Active Research Projects
|
||||
</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 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-2xl font-bold text-yellow-400">8</p>
|
||||
<p className="text-xs text-gray-400 mt-2">Peer-reviewed papers</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Publications
|
||||
</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 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-2xl font-bold text-yellow-400">24</p>
|
||||
<p className="text-xs text-gray-400 mt-2">PhD & researchers</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Research Team Size
|
||||
</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 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-2xl font-bold text-yellow-400">342</p>
|
||||
<p className="text-xs text-gray-400 mt-2">Academic references</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Impact Citations
|
||||
</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>
|
||||
</CardContent>
|
||||
|
|
@ -1190,29 +1217,55 @@ export default function Admin() {
|
|||
<Zap className="h-5 w-5 text-green-400" />
|
||||
<CardTitle>GameForge - Game Development</CardTitle>
|
||||
</div>
|
||||
<CardDescription>Monthly shipping cycles and game production metrics</CardDescription>
|
||||
<CardDescription>
|
||||
Monthly shipping cycles and game production metrics
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<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">
|
||||
<p className="text-sm text-muted-foreground mb-2">Games in Production</p>
|
||||
<p className="text-2xl font-bold text-green-400">18</p>
|
||||
<p className="text-xs text-gray-400 mt-2">Active development</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Games in Production
|
||||
</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 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-2xl font-bold text-green-400">45</p>
|
||||
<p className="text-xs text-gray-400 mt-2">This year</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Games Shipped
|
||||
</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 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-2xl font-bold text-green-400">2.8M</p>
|
||||
<p className="text-xs text-gray-400 mt-2">Monthly active</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Player Base
|
||||
</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 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-2xl font-bold text-green-400">3.2x</p>
|
||||
<p className="text-xs text-gray-400 mt-2">vs industry standard</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Avg Shipping Velocity
|
||||
</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>
|
||||
</CardContent>
|
||||
|
|
@ -1226,29 +1279,55 @@ export default function Admin() {
|
|||
<Briefcase className="h-5 w-5 text-blue-400" />
|
||||
<CardTitle>Corp - Enterprise Services</CardTitle>
|
||||
</div>
|
||||
<CardDescription>Enterprise consulting and business solutions</CardDescription>
|
||||
<CardDescription>
|
||||
Enterprise consulting and business solutions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<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">
|
||||
<p className="text-sm text-muted-foreground mb-2">Active Clients</p>
|
||||
<p className="text-2xl font-bold text-blue-400">34</p>
|
||||
<p className="text-xs text-gray-400 mt-2">Enterprise accounts</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Active Clients
|
||||
</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 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-2xl font-bold text-blue-400">$4.2M</p>
|
||||
<p className="text-xs text-gray-400 mt-2">Annual recurring</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
ARR
|
||||
</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 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-2xl font-bold text-blue-400">94%</p>
|
||||
<p className="text-xs text-gray-400 mt-2">12-month</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Retention Rate
|
||||
</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 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-2xl font-bold text-blue-400">16</p>
|
||||
<p className="text-xs text-gray-400 mt-2">Senior consultants</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Consulting Team
|
||||
</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>
|
||||
</CardContent>
|
||||
|
|
@ -1260,51 +1339,93 @@ export default function Admin() {
|
|||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Heart className="h-5 w-5 text-red-400" />
|
||||
<CardTitle>Foundation - Education & Community</CardTitle>
|
||||
<CardTitle>
|
||||
Foundation - Education & Community
|
||||
</CardTitle>
|
||||
</div>
|
||||
<CardDescription>Educational impact and talent pipeline effectiveness</CardDescription>
|
||||
<CardDescription>
|
||||
Educational impact and talent pipeline effectiveness
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<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="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-2xl font-bold text-green-400">87.5%</p>
|
||||
<p className="text-xs text-gray-400 mt-2">↑ +12% YoY</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Course Completion
|
||||
</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 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-2xl font-bold text-blue-400">342</p>
|
||||
<p className="text-xs text-gray-400 mt-2">↑ +28 new</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Active Learners
|
||||
</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 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-2xl font-bold text-yellow-400">42%</p>
|
||||
<p className="text-xs text-gray-400 mt-2">54 hired</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Placement Rate
|
||||
</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>
|
||||
<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="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-xl font-bold text-purple-400">2.3K</p>
|
||||
<p className="text-xs text-gray-400 mb-1">
|
||||
GitHub Forks
|
||||
</p>
|
||||
<p className="text-xl font-bold text-purple-400">
|
||||
2.3K
|
||||
</p>
|
||||
</div>
|
||||
<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-xl font-bold text-green-400">184</p>
|
||||
<p className="text-xs text-gray-400 mb-1">
|
||||
PRs
|
||||
</p>
|
||||
<p className="text-xl font-bold text-green-400">
|
||||
184
|
||||
</p>
|
||||
</div>
|
||||
<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-xl font-bold text-blue-400">452</p>
|
||||
<p className="text-xs text-gray-400 mb-1">
|
||||
External Usage
|
||||
</p>
|
||||
<p className="text-xl font-bold text-blue-400">
|
||||
452
|
||||
</p>
|
||||
</div>
|
||||
<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-xl font-bold text-cyan-400">67</p>
|
||||
<p className="text-xs text-gray-400 mb-1">
|
||||
Contributors
|
||||
</p>
|
||||
<p className="text-xl font-bold text-cyan-400">
|
||||
67
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1320,29 +1441,55 @@ export default function Admin() {
|
|||
<Users className="h-5 w-5 text-purple-400" />
|
||||
<CardTitle>Nexus - Talent Marketplace</CardTitle>
|
||||
</div>
|
||||
<CardDescription>Creator network and opportunity matching metrics</CardDescription>
|
||||
<CardDescription>
|
||||
Creator network and opportunity matching metrics
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<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">
|
||||
<p className="text-sm text-muted-foreground mb-2">Active Creators</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>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Active Creators
|
||||
</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 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-2xl font-bold text-purple-400">342</p>
|
||||
<p className="text-xs text-gray-400 mt-2">Across all arms</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Open Opportunities
|
||||
</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 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-2xl font-bold text-purple-400">87</p>
|
||||
<p className="text-xs text-gray-400 mt-2">This quarter</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Successful Matches
|
||||
</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 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-2xl font-bold text-purple-400">68%</p>
|
||||
<p className="text-xs text-gray-400 mt-2">Application to hire</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Match Success Rate
|
||||
</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>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -137,7 +137,11 @@ export default function Corp() {
|
|||
The Profit Engine
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
@ -160,7 +164,9 @@ export default function Corp() {
|
|||
|
||||
{/* Creator Network CTAs */}
|
||||
<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">
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
<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" />
|
||||
</div>
|
||||
<CardTitle className="text-blue-300">
|
||||
|
|
@ -234,7 +242,10 @@ export default function Corp() {
|
|||
</p>
|
||||
<ul className="space-y-2">
|
||||
{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" />
|
||||
{example}
|
||||
</li>
|
||||
|
|
@ -255,10 +266,7 @@ export default function Corp() {
|
|||
</h2>
|
||||
<div className="space-y-6">
|
||||
{recentWins.map((win, idx) => (
|
||||
<Card
|
||||
key={idx}
|
||||
className="bg-blue-950/20 border-blue-400/30"
|
||||
>
|
||||
<Card key={idx} className="bg-blue-950/20 border-blue-400/30">
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div>
|
||||
|
|
@ -326,10 +334,7 @@ export default function Corp() {
|
|||
description: "Specialized developers for your team",
|
||||
},
|
||||
].map((model, idx) => (
|
||||
<Card
|
||||
key={idx}
|
||||
className="bg-blue-950/30 border-blue-400/40"
|
||||
>
|
||||
<Card key={idx} className="bg-blue-950/30 border-blue-400/40">
|
||||
<CardContent className="pt-6 text-center">
|
||||
<h3 className="font-bold text-blue-300 mb-2">
|
||||
{model.model}
|
||||
|
|
@ -351,7 +356,8 @@ export default function Corp() {
|
|||
Ready to Partner?
|
||||
</h2>
|
||||
<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>
|
||||
<Button
|
||||
className="bg-blue-400 text-black shadow-[0_0_30px_rgba(59,130,246,0.35)] hover:bg-blue-300"
|
||||
|
|
|
|||
|
|
@ -137,7 +137,9 @@ export default function DevLink() {
|
|||
|
||||
{/* Creator Network CTAs */}
|
||||
<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">
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useDiscord } from '@/contexts/DiscordContext';
|
||||
import LoadingScreen from '@/components/LoadingScreen';
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDiscord } from "@/contexts/DiscordContext";
|
||||
import LoadingScreen from "@/components/LoadingScreen";
|
||||
|
||||
interface DiscordSDK {
|
||||
ready: () => Promise<void>;
|
||||
|
|
@ -22,7 +22,7 @@ export default function DiscordActivity() {
|
|||
try {
|
||||
// Discord SDK should be loaded by the script in index.html
|
||||
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;
|
||||
|
|
@ -33,13 +33,16 @@ export default function DiscordActivity() {
|
|||
|
||||
// Subscribe to close events
|
||||
if (discord.subscribe) {
|
||||
discord.subscribe('ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE', (data: any) => {
|
||||
console.log('Discord participants updated:', data);
|
||||
});
|
||||
discord.subscribe(
|
||||
"ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE",
|
||||
(data: any) => {
|
||||
console.log("Discord participants updated:", data);
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (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);
|
||||
}
|
||||
};
|
||||
|
|
@ -50,14 +53,17 @@ export default function DiscordActivity() {
|
|||
}, []);
|
||||
|
||||
if (error) {
|
||||
const isCloudflareError = error.includes('Direct IP access') || error.includes('Error 1003');
|
||||
const isSDKError = error.includes('Discord SDK');
|
||||
const isCloudflareError =
|
||||
error.includes("Direct IP access") || error.includes("Error 1003");
|
||||
const isSDKError = error.includes("Discord SDK");
|
||||
|
||||
return (
|
||||
<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="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">
|
||||
Unable to initialize Discord Activity
|
||||
</p>
|
||||
|
|
@ -68,9 +74,12 @@ export default function DiscordActivity() {
|
|||
|
||||
{isCloudflareError && (
|
||||
<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">
|
||||
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>
|
||||
<code className="block text-xs bg-black/50 p-2 rounded text-yellow-300 break-all">
|
||||
https://aethex.dev/discord
|
||||
|
|
@ -80,18 +89,26 @@ export default function DiscordActivity() {
|
|||
|
||||
{isSDKError && (
|
||||
<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">
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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>Ensure Discord server has AeThex Activity installed</li>
|
||||
<li>Try refreshing the Discord window</li>
|
||||
|
|
@ -123,8 +140,12 @@ export default function DiscordActivity() {
|
|||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-center space-y-4">
|
||||
<h1 className="text-2xl font-bold text-foreground">Welcome to AeThex</h1>
|
||||
<p className="text-muted-foreground">Discord user information unavailable</p>
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Welcome to AeThex
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Discord user information unavailable
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -134,40 +155,55 @@ export default function DiscordActivity() {
|
|||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<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 className="container mx-auto py-8">
|
||||
<div className="text-center space-y-4">
|
||||
<h1 className="text-3xl font-bold text-foreground">AeThex Community</h1>
|
||||
<p className="text-muted-foreground">Full platform access from Discord</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">
|
||||
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">
|
||||
<a
|
||||
href="/feed"
|
||||
className="p-4 rounded-lg border border-border/50 hover:border-aethex-400/50 transition text-left"
|
||||
>
|
||||
<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
|
||||
href="/community"
|
||||
className="p-4 rounded-lg border border-border/50 hover:border-aethex-400/50 transition text-left"
|
||||
>
|
||||
<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
|
||||
href="/dashboard"
|
||||
className="p-4 rounded-lg border border-border/50 hover:border-aethex-400/50 transition text-left"
|
||||
>
|
||||
<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
|
||||
href="/roadmap"
|
||||
className="p-4 rounded-lg border border-border/50 hover:border-aethex-400/50 transition text-left"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -35,15 +35,15 @@ export default function Foundation() {
|
|||
},
|
||||
{
|
||||
name: "Developer CLI",
|
||||
description: "Command-line tools for streamlined game development workflow",
|
||||
description:
|
||||
"Command-line tools for streamlined game development workflow",
|
||||
stars: "1.2K",
|
||||
language: "Go",
|
||||
link: "github.com/aethex/dev-cli",
|
||||
},
|
||||
{
|
||||
name: "Multiplayer Framework",
|
||||
description:
|
||||
"Drop-in networking layer for real-time multiplayer games",
|
||||
description: "Drop-in networking layer for real-time multiplayer games",
|
||||
stars: "980",
|
||||
language: "TypeScript",
|
||||
link: "github.com/aethex/multiplayer",
|
||||
|
|
@ -147,7 +147,11 @@ export default function Foundation() {
|
|||
Community Impact & Talent Pipeline
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
@ -170,7 +174,9 @@ export default function Foundation() {
|
|||
|
||||
{/* Creator Network CTAs */}
|
||||
<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">
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
@ -300,10 +306,7 @@ export default function Foundation() {
|
|||
</h2>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{resources.map((resource, idx) => (
|
||||
<Card
|
||||
key={idx}
|
||||
className="bg-red-950/20 border-red-400/30"
|
||||
>
|
||||
<Card key={idx} className="bg-red-950/20 border-red-400/30">
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
|
|
@ -405,7 +408,9 @@ export default function Foundation() {
|
|||
Ways to Contribute
|
||||
</h2>
|
||||
<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>
|
||||
<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",
|
||||
},
|
||||
].map((item, idx) => (
|
||||
<Card
|
||||
key={idx}
|
||||
className="bg-red-950/20 border-red-400/30"
|
||||
>
|
||||
<Card key={idx} className="bg-red-950/20 border-red-400/30">
|
||||
<CardContent className="pt-6 text-center">
|
||||
<h3 className="font-bold text-red-300 mb-2">
|
||||
{item.title}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,14 @@ import Layout from "@/components/Layout";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { 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";
|
||||
|
||||
export default function GameForge() {
|
||||
|
|
@ -97,7 +104,11 @@ export default function GameForge() {
|
|||
Shipping Games Monthly
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
@ -257,7 +268,8 @@ export default function GameForge() {
|
|||
{
|
||||
phase: "Prototyping",
|
||||
duration: "1 week",
|
||||
description: "Build playable prototype to test core mechanics",
|
||||
description:
|
||||
"Build playable prototype to test core mechanics",
|
||||
},
|
||||
{
|
||||
phase: "Development",
|
||||
|
|
@ -312,7 +324,8 @@ export default function GameForge() {
|
|||
Part of Our Shipping Culture
|
||||
</h2>
|
||||
<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>
|
||||
<Button
|
||||
className="bg-green-400 text-black shadow-[0_0_30px_rgba(34,197,94,0.35)] hover:bg-green-300"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,14 @@ import Layout from "@/components/Layout";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { 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";
|
||||
|
||||
export default function Labs() {
|
||||
|
|
@ -29,8 +36,7 @@ export default function Labs() {
|
|||
},
|
||||
{
|
||||
title: "Procedural Content Generation",
|
||||
description:
|
||||
"Algorithms for infinite, dynamic game world generation",
|
||||
description: "Algorithms for infinite, dynamic game world generation",
|
||||
status: "Published Research",
|
||||
team: 4,
|
||||
impact: "Game development tools",
|
||||
|
|
@ -57,7 +63,8 @@ export default function Labs() {
|
|||
{
|
||||
title: "Open Source: AeThex Game Engine",
|
||||
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",
|
||||
},
|
||||
{
|
||||
|
|
@ -99,10 +106,16 @@ export default function Labs() {
|
|||
The Innovation Engine
|
||||
</h1>
|
||||
<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 research—from advanced AI to next-generation web architectures—while 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 research—from advanced
|
||||
AI to next-generation web architectures—while cultivating
|
||||
thought leadership that shapes industry direction.
|
||||
</p>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
@ -125,7 +138,9 @@ export default function Labs() {
|
|||
|
||||
{/* Creator Network CTAs */}
|
||||
<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">
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
<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" />
|
||||
</div>
|
||||
<CardTitle className="text-yellow-300">
|
||||
|
|
@ -267,7 +284,9 @@ export default function Labs() {
|
|||
Meet the Lab
|
||||
</h2>
|
||||
<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>
|
||||
<Button
|
||||
className="bg-yellow-400 text-black hover:bg-yellow-300"
|
||||
|
|
@ -286,7 +305,8 @@ export default function Labs() {
|
|||
Be Part of the Innovation
|
||||
</h2>
|
||||
<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>
|
||||
<Button
|
||||
className="bg-yellow-400 text-black shadow-[0_0_30px_rgba(251,191,36,0.35)] hover:bg-yellow-300"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,15 @@ import Layout from "@/components/Layout";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { 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";
|
||||
|
||||
export default function Nexus() {
|
||||
|
|
@ -12,32 +20,38 @@ export default function Nexus() {
|
|||
{
|
||||
icon: Users,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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
|
||||
</h2>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
@ -173,7 +188,8 @@ export default function Nexus() {
|
|||
Multi-Arm Marketplace
|
||||
</h2>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
@ -205,7 +221,8 @@ export default function Nexus() {
|
|||
Ready to Connect?
|
||||
</h2>
|
||||
<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>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export default function CorpContactUs() {
|
|||
const handleInputChange = (
|
||||
e: React.ChangeEvent<
|
||||
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||
>
|
||||
>,
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
|
|
@ -91,7 +91,9 @@ export default function CorpContactUs() {
|
|||
Get In Touch
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -272,9 +274,9 @@ export default function CorpContactUs() {
|
|||
Expected Response Time
|
||||
</h3>
|
||||
<p className="text-blue-200/80">
|
||||
We typically respond to inquiries within 24 business hours.
|
||||
For urgent matters, please call our direct line during
|
||||
business hours (9AM-6PM EST, Monday-Friday).
|
||||
We typically respond to inquiries within 24 business
|
||||
hours. For urgent matters, please call our direct line
|
||||
during business hours (9AM-6PM EST, Monday-Friday).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ export default function CorpScheduleConsultation() {
|
|||
id: "custom-dev",
|
||||
name: "Custom Software Development",
|
||||
duration: "60 min",
|
||||
description: "Discuss your project requirements and our development approach",
|
||||
description:
|
||||
"Discuss your project requirements and our development approach",
|
||||
details: [
|
||||
"Project scope assessment",
|
||||
"Technology stack recommendations",
|
||||
|
|
@ -116,7 +117,9 @@ export default function CorpScheduleConsultation() {
|
|||
Schedule Your Consultation
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -247,7 +250,9 @@ export default function CorpScheduleConsultation() {
|
|||
What to Expect
|
||||
</h2>
|
||||
<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>
|
||||
<Button
|
||||
className="bg-blue-400 text-black shadow-[0_0_30px_rgba(59,130,246,0.35)] hover:bg-blue-300"
|
||||
|
|
|
|||
|
|
@ -133,7 +133,8 @@ export default function CorpViewCaseStudies() {
|
|||
Case Studies
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -272,7 +273,8 @@ export default function CorpViewCaseStudies() {
|
|||
Ready for Your Success Story?
|
||||
</h2>
|
||||
<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>
|
||||
<Button
|
||||
className="bg-blue-400 text-black shadow-[0_0_30px_rgba(59,130,246,0.35)] hover:bg-blue-300"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export default function CreatorDirectory() {
|
|||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [search, setSearch] = useState(searchParams.get("search") || "");
|
||||
const [selectedArm, setSelectedArm] = useState<string | undefined>(
|
||||
searchParams.get("arm") || undefined
|
||||
searchParams.get("arm") || undefined,
|
||||
);
|
||||
const [page, setPage] = useState(parseInt(searchParams.get("page") || "1"));
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
|
|
@ -82,7 +82,8 @@ export default function CreatorDirectory() {
|
|||
</h1>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
@ -166,21 +167,24 @@ export default function CreatorDirectory() {
|
|||
Previous
|
||||
</Button>
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
|
||||
(p) => (
|
||||
<Button
|
||||
key={p}
|
||||
onClick={() => setPage(p)}
|
||||
variant={page === p ? "default" : "outline"}
|
||||
size="sm"
|
||||
>
|
||||
{p}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
{Array.from(
|
||||
{ length: totalPages },
|
||||
(_, i) => i + 1,
|
||||
).map((p) => (
|
||||
<Button
|
||||
key={p}
|
||||
onClick={() => setPage(p)}
|
||||
variant={page === p ? "default" : "outline"}
|
||||
size="sm"
|
||||
>
|
||||
{p}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setPage(Math.min(totalPages, page + 1))}
|
||||
onClick={() =>
|
||||
setPage(Math.min(totalPages, page + 1))
|
||||
}
|
||||
disabled={page === totalPages}
|
||||
variant="outline"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,9 @@ export default function CreatorProfile() {
|
|||
<div className="min-h-screen bg-black text-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<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")}>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Creators
|
||||
|
|
@ -87,7 +89,10 @@ export default function CreatorProfile() {
|
|||
<CardContent className="p-8">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-6 mb-6">
|
||||
<Avatar className="h-24 w-24">
|
||||
<AvatarImage src={creator.avatar_url} alt={creator.username} />
|
||||
<AvatarImage
|
||||
src={creator.avatar_url}
|
||||
alt={creator.username}
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{creator.username.charAt(0).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
|
|
@ -95,7 +100,9 @@ export default function CreatorProfile() {
|
|||
|
||||
<div className="flex-1">
|
||||
<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 && (
|
||||
<Badge className="bg-cyan-500/10 text-cyan-300 border-cyan-500/20">
|
||||
<ExternalLink className="h-3 w-3 mr-1" />
|
||||
|
|
@ -112,16 +119,16 @@ export default function CreatorProfile() {
|
|||
{creator.arm_affiliations &&
|
||||
creator.arm_affiliations
|
||||
.filter((arm) => arm !== creator.primary_arm)
|
||||
.map((arm) => (
|
||||
<ArmBadge key={arm} arm={arm} />
|
||||
))}
|
||||
.map((arm) => <ArmBadge key={arm} arm={arm} />)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
{creator.devconnect_link && (
|
||||
<Button asChild>
|
||||
<a
|
||||
href={creator.devconnect_link.devconnect_profile_url}
|
||||
href={
|
||||
creator.devconnect_link.devconnect_profile_url
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -62,7 +62,9 @@ export default function DevLinkAbout() {
|
|||
About Dev-Link
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -75,7 +77,13 @@ export default function DevLinkAbout() {
|
|||
<Card className="bg-cyan-950/20 border-cyan-400/30">
|
||||
<CardContent className="pt-6">
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -91,7 +99,10 @@ export default function DevLinkAbout() {
|
|||
{values.map((value, idx) => {
|
||||
const Icon = value.icon;
|
||||
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">
|
||||
<Icon className="h-8 w-8 text-cyan-400 mb-3" />
|
||||
<h3 className="text-lg font-bold text-cyan-300 mb-2">
|
||||
|
|
|
|||
|
|
@ -89,7 +89,8 @@ export default function DevLinkJobs() {
|
|||
Job Board
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -118,11 +119,13 @@ export default function DevLinkJobs() {
|
|||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge className={`${
|
||||
job.type === "Full-time"
|
||||
? "bg-green-500/20 text-green-300 border border-green-400/40"
|
||||
: "bg-cyan-500/20 text-cyan-300 border border-cyan-400/40"
|
||||
}`}>
|
||||
<Badge
|
||||
className={`${
|
||||
job.type === "Full-time"
|
||||
? "bg-green-500/20 text-green-300 border border-green-400/40"
|
||||
: "bg-cyan-500/20 text-cyan-300 border border-cyan-400/40"
|
||||
}`}
|
||||
>
|
||||
{job.type}
|
||||
</Badge>
|
||||
<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">
|
||||
Reach 50K+ talented Roblox developers.
|
||||
</p>
|
||||
<Button
|
||||
className="bg-cyan-400 text-black hover:bg-cyan-300"
|
||||
>
|
||||
<Button className="bg-cyan-400 text-black hover:bg-cyan-300">
|
||||
Post a Job Opening
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,8 @@ export default function DevLinkProfiles() {
|
|||
Developer Directory
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -115,7 +116,10 @@ export default function DevLinkProfiles() {
|
|||
<CardContent className="pt-6">
|
||||
<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">
|
||||
{dev.name.split(" ").map(n => n[0]).join("")}
|
||||
{dev.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</div>
|
||||
{dev.available && (
|
||||
<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="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">
|
||||
<Star className="h-4 w-4 fill-yellow-400" />
|
||||
{dev.rating}
|
||||
|
|
@ -172,9 +178,7 @@ export default function DevLinkProfiles() {
|
|||
<p className="text-lg text-cyan-100/80 mb-8">
|
||||
Showcase your work and connect with other developers.
|
||||
</p>
|
||||
<Button
|
||||
className="bg-cyan-400 text-black hover:bg-cyan-300"
|
||||
>
|
||||
<Button className="bg-cyan-400 text-black hover:bg-cyan-300">
|
||||
Get Started
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,8 @@ export default function FoundationContribute() {
|
|||
},
|
||||
{
|
||||
name: "Developer CLI",
|
||||
description: "Command-line tools for streamlined game development workflow",
|
||||
description:
|
||||
"Command-line tools for streamlined game development workflow",
|
||||
stars: "1.2K",
|
||||
language: "Go",
|
||||
issues: "15 open",
|
||||
|
|
@ -92,8 +93,7 @@ export default function FoundationContribute() {
|
|||
},
|
||||
{
|
||||
name: "Multiplayer Framework",
|
||||
description:
|
||||
"Drop-in networking layer for real-time multiplayer games",
|
||||
description: "Drop-in networking layer for real-time multiplayer games",
|
||||
stars: "980",
|
||||
language: "TypeScript",
|
||||
issues: "10 open",
|
||||
|
|
@ -155,7 +155,9 @@ export default function FoundationContribute() {
|
|||
Ways to Contribute
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -173,7 +173,8 @@ export default function FoundationGetInvolved() {
|
|||
Get Involved
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -25,12 +25,7 @@ export default function FoundationLearnMore() {
|
|||
"Complete introduction to game development concepts and best practices",
|
||||
lessons: "50",
|
||||
duration: "20 hours",
|
||||
topics: [
|
||||
"Game loops",
|
||||
"Physics",
|
||||
"Input handling",
|
||||
"Asset management",
|
||||
],
|
||||
topics: ["Game loops", "Physics", "Input handling", "Asset management"],
|
||||
free: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -59,12 +54,7 @@ export default function FoundationLearnMore() {
|
|||
"Learn scalable architectural patterns used in professional game development",
|
||||
modules: "8",
|
||||
projects: "4",
|
||||
topics: [
|
||||
"MVC pattern",
|
||||
"ECS systems",
|
||||
"Networking",
|
||||
"State management",
|
||||
],
|
||||
topics: ["MVC pattern", "ECS systems", "Networking", "State management"],
|
||||
free: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -174,7 +164,9 @@ export default function FoundationLearnMore() {
|
|||
Free Learning Resources
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -426,8 +418,8 @@ export default function FoundationLearnMore() {
|
|||
Start Your Learning Journey
|
||||
</h2>
|
||||
<p className="text-lg text-red-100/80 mb-8">
|
||||
Choose a learning path and begin mastering game development today.
|
||||
Everything is completely free and open to the community.
|
||||
Choose a learning path and begin mastering game development
|
||||
today. Everything is completely free and open to the community.
|
||||
</p>
|
||||
<Button
|
||||
className="bg-red-400 text-black shadow-[0_0_30px_rgba(239,68,68,0.35)] hover:bg-red-300"
|
||||
|
|
|
|||
|
|
@ -65,7 +65,8 @@ export default function GameForgeJoinGameForge() {
|
|||
The Team
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -80,7 +81,10 @@ export default function GameForgeJoinGameForge() {
|
|||
>
|
||||
<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">
|
||||
{member.name.split(" ").map(n => n[0]).join("")}
|
||||
{member.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-green-300 mb-1">
|
||||
{member.name}
|
||||
|
|
@ -115,7 +119,8 @@ export default function GameForgeJoinGameForge() {
|
|||
{
|
||||
icon: <Zap className="h-6 w-6" />,
|
||||
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" />,
|
||||
|
|
@ -125,7 +130,8 @@ export default function GameForgeJoinGameForge() {
|
|||
{
|
||||
icon: <Users className="h-6 w-6" />,
|
||||
title: "Together",
|
||||
description: "Cross-functional teams working toward one goal",
|
||||
description:
|
||||
"Cross-functional teams working toward one goal",
|
||||
},
|
||||
].map((item, idx) => (
|
||||
<Card
|
||||
|
|
|
|||
|
|
@ -122,7 +122,9 @@ export default function GameForgeStartBuilding() {
|
|||
Production Pipeline
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -234,11 +236,13 @@ export default function GameForgeStartBuilding() {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className={`${
|
||||
item.status === "On Track"
|
||||
? "bg-green-500/20 text-green-300 border border-green-400/40"
|
||||
: "bg-purple-500/20 text-purple-300 border border-purple-400/40"
|
||||
}`}>
|
||||
<Badge
|
||||
className={`${
|
||||
item.status === "On Track"
|
||||
? "bg-green-500/20 text-green-300 border border-green-400/40"
|
||||
: "bg-purple-500/20 text-purple-300 border border-purple-400/40"
|
||||
}`}
|
||||
>
|
||||
{item.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
|
@ -257,7 +261,10 @@ export default function GameForgeStartBuilding() {
|
|||
</h2>
|
||||
<div className="space-y-4">
|
||||
{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">
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@ export default function GameForgeViewPortfolio() {
|
|||
Released Games
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -109,9 +110,7 @@ export default function GameForgeViewPortfolio() {
|
|||
<p className="text-lg font-bold text-green-300">
|
||||
{game.players}
|
||||
</p>
|
||||
<p className="text-xs text-green-200/60">
|
||||
active
|
||||
</p>
|
||||
<p className="text-xs text-green-200/60">active</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
|
|
@ -119,7 +118,8 @@ export default function GameForgeViewPortfolio() {
|
|||
RATING
|
||||
</p>
|
||||
<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 className="text-xs text-green-200/60">
|
||||
{game.downloads} downloads
|
||||
|
|
|
|||
|
|
@ -2,7 +2,14 @@ import Layout from "@/components/Layout";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { 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";
|
||||
|
||||
export default function LabsExploreResearch() {
|
||||
|
|
@ -105,7 +112,9 @@ export default function LabsExploreResearch() {
|
|||
Research Projects
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -125,13 +134,15 @@ export default function LabsExploreResearch() {
|
|||
<CardTitle className="text-2xl text-yellow-300 mb-2">
|
||||
{project.title}
|
||||
</CardTitle>
|
||||
<Badge className={`${
|
||||
project.status === "Published"
|
||||
? "bg-green-500/20 border border-green-400/40 text-green-300"
|
||||
: project.status === "Active Research"
|
||||
? "bg-yellow-500/20 border border-yellow-400/40 text-yellow-300"
|
||||
: "bg-purple-500/20 border border-purple-400/40 text-purple-300"
|
||||
}`}>
|
||||
<Badge
|
||||
className={`${
|
||||
project.status === "Published"
|
||||
? "bg-green-500/20 border border-green-400/40 text-green-300"
|
||||
: project.status === "Active Research"
|
||||
? "bg-yellow-500/20 border border-yellow-400/40 text-yellow-300"
|
||||
: "bg-purple-500/20 border border-purple-400/40 text-purple-300"
|
||||
}`}
|
||||
>
|
||||
{project.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
|
@ -148,7 +159,9 @@ export default function LabsExploreResearch() {
|
|||
|
||||
<CardContent className="space-y-6">
|
||||
{/* Description */}
|
||||
<p className="text-yellow-200/80">{project.description}</p>
|
||||
<p className="text-yellow-200/80">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
{/* Key Achievements */}
|
||||
<div>
|
||||
|
|
@ -188,7 +201,9 @@ export default function LabsExploreResearch() {
|
|||
{/* Impact & Links */}
|
||||
<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">
|
||||
<span className="font-semibold text-yellow-400">Impact:</span>{" "}
|
||||
<span className="font-semibold text-yellow-400">
|
||||
Impact:
|
||||
</span>{" "}
|
||||
{project.impact}
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
|
|
@ -228,7 +243,8 @@ export default function LabsExploreResearch() {
|
|||
Interested in Research?
|
||||
</h2>
|
||||
<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>
|
||||
<Button
|
||||
className="bg-yellow-400 text-black hover:bg-yellow-300"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,14 @@ import Layout from "@/components/Layout";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { 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";
|
||||
|
||||
export default function LabsGetInvolved() {
|
||||
|
|
@ -109,7 +116,9 @@ export default function LabsGetInvolved() {
|
|||
Get Involved
|
||||
</h1>
|
||||
<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>
|
||||
</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"
|
||||
>
|
||||
<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}
|
||||
</div>
|
||||
<CardTitle className="text-yellow-300">
|
||||
|
|
|
|||
|
|
@ -104,7 +104,9 @@ export default function LabsJoinTeam() {
|
|||
Meet the Lab
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -235,7 +237,8 @@ export default function LabsJoinTeam() {
|
|||
Join the Lab
|
||||
</h2>
|
||||
<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>
|
||||
<Button
|
||||
className="bg-yellow-400 text-black hover:bg-yellow-300"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export default function OpportunitiesHub() {
|
|||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [search, setSearch] = useState(searchParams.get("search") || "");
|
||||
const [selectedArm, setSelectedArm] = useState<string | undefined>(
|
||||
searchParams.get("arm") || undefined
|
||||
searchParams.get("arm") || undefined,
|
||||
);
|
||||
const [page, setPage] = useState(parseInt(searchParams.get("page") || "1"));
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
|
|
@ -83,7 +83,8 @@ export default function OpportunitiesHub() {
|
|||
</h1>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
@ -167,21 +168,24 @@ export default function OpportunitiesHub() {
|
|||
Previous
|
||||
</Button>
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
|
||||
(p) => (
|
||||
<Button
|
||||
key={p}
|
||||
onClick={() => setPage(p)}
|
||||
variant={page === p ? "default" : "outline"}
|
||||
size="sm"
|
||||
>
|
||||
{p}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
{Array.from(
|
||||
{ length: totalPages },
|
||||
(_, i) => i + 1,
|
||||
).map((p) => (
|
||||
<Button
|
||||
key={p}
|
||||
onClick={() => setPage(p)}
|
||||
variant={page === p ? "default" : "outline"}
|
||||
size="sm"
|
||||
>
|
||||
{p}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setPage(Math.min(totalPages, page + 1))}
|
||||
onClick={() =>
|
||||
setPage(Math.min(totalPages, page + 1))
|
||||
}
|
||||
disabled={page === totalPages}
|
||||
variant="outline"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export default function OpportunityDetail() {
|
|||
} catch (error) {
|
||||
toast(
|
||||
error instanceof Error ? error.message : "Failed to submit application",
|
||||
"error"
|
||||
"error",
|
||||
);
|
||||
} finally {
|
||||
setIsApplying(false);
|
||||
|
|
@ -88,7 +88,8 @@ export default function OpportunityDetail() {
|
|||
|
||||
const formatSalary = (min?: number, max?: number) => {
|
||||
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 (max) return `Up to $${max.toLocaleString()}`;
|
||||
};
|
||||
|
|
@ -152,9 +153,7 @@ export default function OpportunityDetail() {
|
|||
<ArmBadge arm={opportunity.arm_affiliation} />
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl font-bold mb-4">
|
||||
{opportunity.title}
|
||||
</h1>
|
||||
<h1 className="text-4xl font-bold mb-4">{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>
|
||||
|
|
@ -171,7 +170,7 @@ export default function OpportunityDetail() {
|
|||
<p className="font-semibold text-sm">
|
||||
{formatSalary(
|
||||
opportunity.salary_min,
|
||||
opportunity.salary_max
|
||||
opportunity.salary_max,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -196,7 +195,10 @@ export default function OpportunityDetail() {
|
|||
{/* Posted By */}
|
||||
<div className="flex items-center gap-4 mb-8 p-4 bg-slate-700/30 rounded-lg">
|
||||
<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>
|
||||
|
|
@ -206,9 +208,7 @@ export default function OpportunityDetail() {
|
|||
<p className="font-semibold">@{poster.username}</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
navigate(`/creators/${poster.username}`)
|
||||
}
|
||||
onClick={() => navigate(`/creators/${poster.username}`)}
|
||||
variant="outline"
|
||||
>
|
||||
View Profile
|
||||
|
|
@ -253,8 +253,8 @@ export default function OpportunityDetail() {
|
|||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Apply for {opportunity.title}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Submit your application with a cover letter explaining why you're
|
||||
interested in this opportunity.
|
||||
Submit your application with a cover letter explaining why
|
||||
you're interested in this opportunity.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
|
|
@ -280,9 +280,7 @@ export default function OpportunityDetail() {
|
|||
disabled={isApplying || !coverLetter.trim()}
|
||||
className="gap-2"
|
||||
>
|
||||
{isApplying && (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{isApplying && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
Submit Application
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -51,15 +51,13 @@ export default function MyApplications() {
|
|||
try {
|
||||
await withdrawApplication(applicationId);
|
||||
toast("Application withdrawn", "success");
|
||||
setApplications(
|
||||
applications.filter((app) => app.id !== applicationId)
|
||||
);
|
||||
setApplications(applications.filter((app) => app.id !== applicationId));
|
||||
} catch (error) {
|
||||
toast(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to withdraw application",
|
||||
"error"
|
||||
"error",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -215,7 +213,7 @@ export default function MyApplications() {
|
|||
/>
|
||||
))}
|
||||
</TabsContent>
|
||||
)
|
||||
),
|
||||
)}
|
||||
</Tabs>
|
||||
</>
|
||||
|
|
@ -265,10 +263,7 @@ function ApplicationCard({
|
|||
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarImage
|
||||
src={poster.avatar_url}
|
||||
alt={poster.username}
|
||||
/>
|
||||
<AvatarImage src={poster.avatar_url} alt={poster.username} />
|
||||
<AvatarFallback>
|
||||
{poster.username.charAt(0).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ AeThex can be embedded as a Discord Activity, allowing users to access the platf
|
|||
## What is a Discord Activity?
|
||||
|
||||
A Discord Activity is an embedded application that runs within Discord. It allows users to:
|
||||
|
||||
- Access AeThex features directly in Discord
|
||||
- Share their experience with server members
|
||||
- Collaborate in real-time without leaving Discord
|
||||
|
|
@ -51,6 +52,7 @@ This file tells Discord how to handle the AeThex Activity:
|
|||
```
|
||||
|
||||
**Key Configuration Points:**
|
||||
|
||||
- `instance_url`: Where Discord Activity iframe loads (MUST be domain, not IP)
|
||||
- `redirect_uris`: OAuth callback endpoint (MUST match Discord app settings)
|
||||
- `scopes`: What Discord permissions the Activity requests
|
||||
|
|
@ -58,15 +60,18 @@ This file tells Discord how to handle the AeThex Activity:
|
|||
### 2. Code Configuration
|
||||
|
||||
**Frontend Pages** (`code/client/pages/`):
|
||||
|
||||
- `DiscordActivity.tsx` - Main Activity page mounted at `/discord`
|
||||
- Discord OAuth callback handler at `/discord/callback`
|
||||
|
||||
**Context** (`code/client/contexts/DiscordContext.tsx`):
|
||||
|
||||
- Manages Discord user session
|
||||
- Handles OAuth flow
|
||||
- Exposes Discord user data to components
|
||||
|
||||
**Routes** (`code/client/App.tsx`):
|
||||
|
||||
```typescript
|
||||
<Route path="/discord" element={<DiscordActivity />} />
|
||||
<Route path="/discord/callback" element={<DiscordOAuthCallback />} />
|
||||
|
|
@ -85,6 +90,7 @@ This must be present for Discord Activity to initialize.
|
|||
## Local Testing
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- npm or yarn
|
||||
- Running AeThex dev server
|
||||
|
|
@ -92,6 +98,7 @@ This must be present for Discord Activity to initialize.
|
|||
### Steps
|
||||
|
||||
1. **Start the dev server:**
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
|
|
@ -99,6 +106,7 @@ This must be present for Discord Activity to initialize.
|
|||
```
|
||||
|
||||
2. **Access locally via tunnel** (if testing Discord Activity):
|
||||
|
||||
- Use a tool like `ngrok` to expose localhost to Discord
|
||||
- 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**.
|
||||
|
||||
Local testing workarounds:
|
||||
|
||||
- Use `ngrok` with a tunnel URL
|
||||
- Use Discord's local testing documentation
|
||||
- Test OAuth flow after deploying to staging
|
||||
|
|
@ -145,6 +154,7 @@ Local testing workarounds:
|
|||
### Environment Variables
|
||||
|
||||
No special environment variables needed for Discord Activity. The configuration is done via:
|
||||
|
||||
- `code/public/discord-manifest.json`
|
||||
- Discord Application settings
|
||||
- `code/client/contexts/DiscordContext.tsx`
|
||||
|
|
@ -169,16 +179,19 @@ No special environment variables needed for Discord Activity. The configuration
|
|||
### Implementation Details
|
||||
|
||||
**DiscordActivity.tsx** handles:
|
||||
|
||||
- Discord SDK initialization
|
||||
- OAuth trigger and callback handling
|
||||
- Activity UI rendering
|
||||
|
||||
**DiscordContext.tsx** manages:
|
||||
|
||||
- Discord user state
|
||||
- Token storage
|
||||
- Session lifecycle
|
||||
|
||||
**API calls** use Discord access token for:
|
||||
|
||||
- User identification
|
||||
- Guild information
|
||||
- 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
|
||||
|
||||
**Solution**:
|
||||
|
||||
```
|
||||
❌ http://192.168.1.100:5173/discord
|
||||
✅ https://aethex.dev/discord
|
||||
|
|
@ -197,6 +211,7 @@ No special environment variables needed for Discord Activity. The configuration
|
|||
|
||||
**Error Message in UI**:
|
||||
Users will see a helpful error message explaining:
|
||||
|
||||
- The issue (Cloudflare blocking IP access)
|
||||
- How to fix it (use domain instead)
|
||||
- Troubleshooting steps
|
||||
|
|
@ -206,6 +221,7 @@ Users will see a helpful error message explaining:
|
|||
**Cause**: Discord SDK not loaded or manifest invalid
|
||||
|
||||
**Solution**:
|
||||
|
||||
- Verify Discord SDK script in `code/index.html`
|
||||
- Check manifest is accessible at `/public/discord-manifest.json`
|
||||
- Verify Discord Application ID: `578971245454950421`
|
||||
|
|
@ -244,6 +260,7 @@ Users will see a helpful error message explaining:
|
|||
**Error**: Blank white screen in Discord Activity
|
||||
|
||||
**Debug Steps**:
|
||||
|
||||
1. Open browser DevTools in Discord
|
||||
2. Check Console for errors
|
||||
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
|
||||
|
||||
**Common Causes**:
|
||||
|
||||
- IP address used instead of domain (Cloudflare Error 1003)
|
||||
- Discord SDK script failed to load
|
||||
- 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
|
||||
|
||||
**Debug Steps**:
|
||||
|
||||
1. Check Discord Application settings - redirect URIs match exactly
|
||||
2. Verify OAuth callback route exists: `/discord/callback`
|
||||
3. Check browser console for authorization error codes
|
||||
4. Verify Discord Application permissions (identify, email, guilds)
|
||||
|
||||
**Common Causes**:
|
||||
|
||||
- Redirect URI mismatch between manifest and Discord app
|
||||
- Discord Application doesn't have "Identify" scope enabled
|
||||
- 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
|
||||
|
||||
**Debug Steps**:
|
||||
|
||||
1. Verify "guilds" scope is in OAuth config
|
||||
2. Check user actually has permission in those guilds
|
||||
3. Verify Discord OAuth token has guilds scope
|
||||
|
|
@ -316,6 +337,7 @@ Using your deployment provider (Netlify, Vercel, custom):
|
|||
### Step 5: Add to Discord
|
||||
|
||||
In Discord Developer Portal:
|
||||
|
||||
1. Go to Application Settings
|
||||
2. Add Activity URL: `https://aethex.dev/discord`
|
||||
3. Set OAuth2 Redirect URIs to: `https://aethex.dev/discord/callback`
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@ The AeThex organizational structure implements a centralized IP holding company
|
|||
## 1. Labs as IP Holding Company (IPCo)
|
||||
|
||||
All core intellectual property developed by Labs is owned by Labs, including:
|
||||
|
||||
- Patent portfolios (AI, algorithms, architectures)
|
||||
- Software copyrights and source code
|
||||
- Trade secrets and proprietary methodologies
|
||||
- Trademarks and brand assets
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- **Clean IP Title**: Consolidated, encumbrance-free IP ownership improves enterprise valuation
|
||||
- **Protection**: IP separated from operational liabilities (Corp consulting disputes, Dev-Link platform issues)
|
||||
- **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
|
||||
|
||||
Corp receives a Commercial License to "make and use" proprietary Lab technologies in commercial service delivery, specifically:
|
||||
|
||||
- Advanced AI models for game development optimization
|
||||
- Custom algorithms for multiplayer architecture
|
||||
- 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:
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
Dev-Link platform may license Lab IP for:
|
||||
|
||||
- AI-assisted candidate assessment and matching
|
||||
- Specialized skill evaluation algorithms
|
||||
- Predictive analytics for placement success
|
||||
|
|
@ -92,12 +97,14 @@ Charge = Actual Costs + Markup (typically 5-15%)
|
|||
```
|
||||
|
||||
**Eligible Services:**
|
||||
|
||||
- HR administration, payroll processing
|
||||
- IT infrastructure and support
|
||||
- General accounting and bookkeeping
|
||||
- Legal documentation review
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- Clearly define which activities qualify
|
||||
- Meticulously track direct costs
|
||||
- Document markup selection (compare to similar service providers)
|
||||
|
|
@ -106,6 +113,7 @@ Charge = Actual Costs + Markup (typically 5-15%)
|
|||
### Documentation
|
||||
|
||||
Maintain **Intercompany Service Agreement** including:
|
||||
|
||||
- Services provided
|
||||
- Cost allocation methodology
|
||||
- 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
|
||||
|
||||
1. **Corp provides office space to Foundation**
|
||||
|
||||
- Must charge Fair Market Rent (comparable leases in area)
|
||||
- Document: Annual appraisal or comparable rent analysis
|
||||
- Forbidden: Below-market lease = private inurement
|
||||
|
||||
2. **Corp donates curriculum materials to Foundation**
|
||||
|
||||
- Document as charitable contribution
|
||||
- Fair value: Cost of development + reasonable markup
|
||||
- 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
|
||||
|
||||
Foundation must adopt and enforce a Conflict of Interest Policy including:
|
||||
|
||||
- Board members declare conflicts with related for-profit entities
|
||||
- Disclosure of family/business relationships with Corp/Dev-Link executives
|
||||
- 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
|
||||
|
||||
**For Technology Licensing (Labs IP):**
|
||||
|
||||
- Comparable License Analysis (market rates for similar IP)
|
||||
- Relief-from-Royalty (value of IP to users)
|
||||
- Residual Profit Split (allocate profit between Lab innovation and operational execution)
|
||||
|
||||
**For Services:**
|
||||
|
||||
- Comparable Uncontrolled Price (market rates for same services)
|
||||
- Cost-Plus Analysis (cost + reasonable markup)
|
||||
- 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
|
||||
|
||||
Before any Corp transfer to Foundation:
|
||||
|
||||
1. Independent majority votes
|
||||
2. Minority includes conflicted party vote counts documented
|
||||
3. FMV analysis presented
|
||||
|
|
@ -213,6 +227,7 @@ Before any Corp transfer to Foundation:
|
|||
### Annual Form 990 Reporting
|
||||
|
||||
Foundation must file IRS Form 990 disclosing:
|
||||
|
||||
- Compensation of officers/directors
|
||||
- Related-party transactions
|
||||
- All grants and contributions received
|
||||
|
|
@ -223,6 +238,7 @@ Foundation must file IRS Form 990 disclosing:
|
|||
## 9. Benefit Corporation Governance (Parent Level)
|
||||
|
||||
Parent company incorporated as Benefit Corporation (not C-Corp) specifically to:
|
||||
|
||||
- Balance shareholder profit with stakeholder interests
|
||||
- Legally protect Foundation funding decisions
|
||||
- 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
|
||||
|
||||
Benefit Corporation directors have legal duty to:
|
||||
|
||||
1. Consider impact on stakeholders (workers, customers, community)
|
||||
2. Balance shareholder returns with general public benefit
|
||||
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
|
||||
|
||||
### Definition
|
||||
|
||||
Private inurement = net earnings of Foundation inure to benefit of any shareholder/individual = immediate loss of 501(c)(3) status.
|
||||
|
||||
### High-Risk Transactions
|
||||
|
|
@ -259,15 +277,18 @@ Private inurement = net earnings of Foundation inure to benefit of any sharehold
|
|||
## 11. Transfer Pricing Documentation Requirements
|
||||
|
||||
### When Required
|
||||
|
||||
Any payment between related entities (Labs→Corp, Corp→Foundation, etc.) requires documentation.
|
||||
|
||||
### Documentation Package
|
||||
|
||||
1. **Intercompany Agreement**
|
||||
|
||||
- Parties, services/IP, payment terms, effective date
|
||||
- Signed and dated
|
||||
|
||||
2. **Transfer Pricing Study**
|
||||
|
||||
- Executive summary
|
||||
- Functional analysis (functions, assets, risks of each party)
|
||||
- Economic analysis (industry data, market conditions)
|
||||
|
|
@ -316,6 +337,7 @@ Maintain and update:
|
|||
## Conclusion
|
||||
|
||||
This framework ensures:
|
||||
|
||||
1. **IP Protection**: Centralized control and defensive valuation
|
||||
2. **Tax Compliance**: Arm's Length transfer pricing reduces audit risk
|
||||
3. **Liability Insulation**: Separate entities prevent cross-contamination
|
||||
|
|
|
|||
|
|
@ -14,9 +14,7 @@ const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID || "578971245454950421";
|
|||
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN;
|
||||
|
||||
if (!DISCORD_BOT_TOKEN) {
|
||||
console.error(
|
||||
"❌ DISCORD_BOT_TOKEN environment variable is required",
|
||||
);
|
||||
console.error("❌ DISCORD_BOT_TOKEN environment variable is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
|
@ -103,14 +101,14 @@ async function registerCommands() {
|
|||
});
|
||||
|
||||
console.log("\n📍 Next steps:");
|
||||
console.log(
|
||||
"1. Go to Discord Developer Portal > Application Settings",
|
||||
);
|
||||
console.log("1. Go to Discord Developer Portal > Application Settings");
|
||||
console.log(
|
||||
"2. Set Interactions Endpoint URL to: https://aethex.dev/api/discord/interactions",
|
||||
);
|
||||
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) {
|
||||
console.error("❌ Error registering commands:", error);
|
||||
process.exit(1);
|
||||
|
|
|
|||
193
server/index.ts
193
server/index.ts
|
|
@ -115,7 +115,10 @@ const handleDiscordInteractions = (
|
|||
|
||||
// For MESSAGE_COMPONENT interactions (buttons, etc.)
|
||||
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({
|
||||
type: 4,
|
||||
|
|
@ -2418,7 +2421,7 @@ export function createServer() {
|
|||
primary_arm,
|
||||
created_at
|
||||
`,
|
||||
{ count: "exact" }
|
||||
{ count: "exact" },
|
||||
)
|
||||
.eq("is_discoverable", true)
|
||||
.order("created_at", { ascending: false });
|
||||
|
|
@ -2475,7 +2478,7 @@ export function createServer() {
|
|||
primary_arm,
|
||||
created_at,
|
||||
updated_at
|
||||
`
|
||||
`,
|
||||
)
|
||||
.eq("username", username)
|
||||
.eq("is_discoverable", true)
|
||||
|
|
@ -2507,10 +2510,21 @@ export function createServer() {
|
|||
// Create creator profile
|
||||
app.post("/api/creators", async (req, res) => {
|
||||
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) {
|
||||
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
|
||||
|
|
@ -2523,7 +2537,9 @@ export function createServer() {
|
|||
avatar_url: avatar_url || null,
|
||||
experience_level: experience_level || null,
|
||||
primary_arm: primary_arm || null,
|
||||
arm_affiliations: Array.isArray(arm_affiliations) ? arm_affiliations : [],
|
||||
arm_affiliations: Array.isArray(arm_affiliations)
|
||||
? arm_affiliations
|
||||
: [],
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
|
@ -2538,7 +2554,9 @@ export function createServer() {
|
|||
return res.status(201).json(data);
|
||||
} catch (e: any) {
|
||||
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" });
|
||||
}
|
||||
|
||||
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
|
||||
.from("aethex_creators")
|
||||
|
|
@ -2560,7 +2587,9 @@ export function createServer() {
|
|||
avatar_url,
|
||||
experience_level,
|
||||
primary_arm,
|
||||
arm_affiliations: Array.isArray(arm_affiliations) ? arm_affiliations : undefined,
|
||||
arm_affiliations: Array.isArray(arm_affiliations)
|
||||
? arm_affiliations
|
||||
: undefined,
|
||||
is_discoverable,
|
||||
allow_recommendations,
|
||||
updated_at: new Date().toISOString(),
|
||||
|
|
@ -2574,7 +2603,9 @@ export function createServer() {
|
|||
return res.json(data);
|
||||
} catch (e: any) {
|
||||
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,
|
||||
created_at
|
||||
`,
|
||||
{ count: "exact" }
|
||||
{ count: "exact" },
|
||||
)
|
||||
.eq("status", "open");
|
||||
|
||||
|
|
@ -2615,7 +2646,9 @@ export function createServer() {
|
|||
}
|
||||
|
||||
if (search) {
|
||||
query = query.or(`title.ilike.%${search}%,description.ilike.%${search}%`);
|
||||
query = query.or(
|
||||
`title.ilike.%${search}%,description.ilike.%${search}%`,
|
||||
);
|
||||
}
|
||||
|
||||
if (jobType) {
|
||||
|
|
@ -2646,7 +2679,10 @@ export function createServer() {
|
|||
},
|
||||
});
|
||||
} 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" });
|
||||
}
|
||||
});
|
||||
|
|
@ -2676,7 +2712,7 @@ export function createServer() {
|
|||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
`
|
||||
`,
|
||||
)
|
||||
.eq("id", opportunityId)
|
||||
.eq("status", "open")
|
||||
|
|
@ -2691,7 +2727,10 @@ export function createServer() {
|
|||
|
||||
return res.json(data);
|
||||
} 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" });
|
||||
}
|
||||
});
|
||||
|
|
@ -2699,7 +2738,16 @@ export function createServer() {
|
|||
// Create opportunity
|
||||
app.post("/api/opportunities", async (req, res) => {
|
||||
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) {
|
||||
return res.status(400).json({ error: "user_id and title required" });
|
||||
|
|
@ -2712,7 +2760,11 @@ export function createServer() {
|
|||
.single();
|
||||
|
||||
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
|
||||
|
|
@ -2735,7 +2787,10 @@ export function createServer() {
|
|||
|
||||
return res.status(201).json(data);
|
||||
} 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" });
|
||||
}
|
||||
});
|
||||
|
|
@ -2744,10 +2799,21 @@ export function createServer() {
|
|||
app.put("/api/opportunities/:id", async (req, res) => {
|
||||
try {
|
||||
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) {
|
||||
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
|
||||
|
|
@ -2790,7 +2856,10 @@ export function createServer() {
|
|||
|
||||
return res.json(data);
|
||||
} 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" });
|
||||
}
|
||||
});
|
||||
|
|
@ -2831,7 +2900,7 @@ export function createServer() {
|
|||
updated_at,
|
||||
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);
|
||||
|
||||
|
|
@ -2858,7 +2927,10 @@ export function createServer() {
|
|||
},
|
||||
});
|
||||
} 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" });
|
||||
}
|
||||
});
|
||||
|
|
@ -2869,7 +2941,9 @@ export function createServer() {
|
|||
const { user_id, opportunity_id, cover_letter } = req.body;
|
||||
|
||||
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
|
||||
|
|
@ -2890,7 +2964,9 @@ export function createServer() {
|
|||
.single();
|
||||
|
||||
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
|
||||
|
|
@ -2901,7 +2977,9 @@ export function createServer() {
|
|||
.maybeSingle();
|
||||
|
||||
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
|
||||
|
|
@ -2919,7 +2997,10 @@ export function createServer() {
|
|||
|
||||
return res.status(201).json(data);
|
||||
} 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" });
|
||||
}
|
||||
});
|
||||
|
|
@ -2931,7 +3012,9 @@ export function createServer() {
|
|||
const { user_id, status, response_message } = req.body;
|
||||
|
||||
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
|
||||
|
|
@ -2941,7 +3024,7 @@ export function createServer() {
|
|||
id,
|
||||
opportunity_id,
|
||||
aethex_opportunities(posted_by_id)
|
||||
`
|
||||
`,
|
||||
)
|
||||
.eq("id", applicationId)
|
||||
.single();
|
||||
|
|
@ -2975,7 +3058,10 @@ export function createServer() {
|
|||
|
||||
return res.json(data);
|
||||
} 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" });
|
||||
}
|
||||
});
|
||||
|
|
@ -2987,7 +3073,9 @@ export function createServer() {
|
|||
const { user_id } = req.body;
|
||||
|
||||
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
|
||||
|
|
@ -3019,18 +3107,26 @@ export function createServer() {
|
|||
|
||||
return res.json({ ok: true });
|
||||
} catch (e: any) {
|
||||
console.error("[Applications API] Error withdrawing application:", e?.message);
|
||||
return res.status(500).json({ error: "Failed to withdraw application" });
|
||||
console.error(
|
||||
"[Applications API] Error withdrawing application:",
|
||||
e?.message,
|
||||
);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Failed to withdraw application" });
|
||||
}
|
||||
});
|
||||
|
||||
// Link DevConnect account
|
||||
app.post("/api/devconnect/link", async (req, res) => {
|
||||
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) {
|
||||
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
|
||||
|
|
@ -3040,7 +3136,11 @@ export function createServer() {
|
|||
.single();
|
||||
|
||||
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
|
||||
|
|
@ -3057,7 +3157,9 @@ export function createServer() {
|
|||
.from("aethex_devconnect_links")
|
||||
.update({
|
||||
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)
|
||||
.select()
|
||||
|
|
@ -3072,7 +3174,9 @@ export function createServer() {
|
|||
.insert({
|
||||
aethex_creator_id: creator.id,
|
||||
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()
|
||||
.single();
|
||||
|
|
@ -3089,7 +3193,9 @@ export function createServer() {
|
|||
return res.status(status).json(result);
|
||||
} catch (e: any) {
|
||||
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 });
|
||||
} catch (e: any) {
|
||||
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 });
|
||||
} catch (e: any) {
|
||||
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) {
|
||||
console.warn("Admin API not initialized:", e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,82 +1,76 @@
|
|||
# Phase 3: Testing & Validation - COMPLETE ✅
|
||||
|
||||
## 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.
|
||||
|
||||
## 📦 Deliverables
|
||||
|
||||
### 1. End-to-End Test Suite (`code/tests/e2e-creator-network.test.ts`)
|
||||
|
||||
**Status:** ✅ Complete (490 lines)
|
||||
|
||||
**Test Flows Covered:**
|
||||
|
||||
- FLOW 1: Creator Registration & Profile Setup
|
||||
- Create 2 creator profiles with different arms
|
||||
- Verify profile data accuracy
|
||||
|
||||
- FLOW 2: Opportunity Creation & Discovery
|
||||
- Create opportunities
|
||||
- Browse with filters
|
||||
- Pagination verification
|
||||
|
||||
- FLOW 3: Creator Discovery & Profiles
|
||||
- Browse creators with arm filters
|
||||
- Individual profile retrieval
|
||||
- Profile data validation
|
||||
|
||||
- FLOW 4: Application Submission & Tracking
|
||||
- Submit applications
|
||||
- Prevent duplicate applications
|
||||
- Get applications list
|
||||
- Update application status
|
||||
|
||||
- FLOW 5: DevConnect Linking
|
||||
- Link DevConnect accounts
|
||||
- Get DevConnect links
|
||||
- Unlink accounts
|
||||
|
||||
- FLOW 6: Advanced Filtering & Search
|
||||
- Search creators
|
||||
- Filter opportunities
|
||||
- Pagination testing
|
||||
|
||||
**Features:**
|
||||
|
||||
- Performance timing for each operation
|
||||
- Detailed error messages
|
||||
- Comprehensive test summary with pass/fail counts
|
||||
|
||||
### 2. Error Handling Test Suite (`code/tests/error-handling.test.ts`)
|
||||
|
||||
**Status:** ✅ Complete (447 lines)
|
||||
|
||||
**Test Categories:**
|
||||
|
||||
1. **Input Validation Errors** (4 tests)
|
||||
- Missing required fields (user_id, username, title, opportunity_id)
|
||||
- Validation of mandatory parameters
|
||||
|
||||
2. **Not Found Errors** (3 tests)
|
||||
- Non-existent creators, opportunities, applications
|
||||
- 404 responses for missing resources
|
||||
|
||||
3. **Authorization & Ownership Errors** (2 tests)
|
||||
- Invalid creator IDs
|
||||
- Unauthorized access attempts
|
||||
|
||||
4. **Duplicate & Conflict Errors** (2 tests)
|
||||
- Duplicate username prevention
|
||||
- Duplicate application prevention
|
||||
|
||||
5. **Missing Required Relationships** (2 tests)
|
||||
- Creating opportunities without creator profile
|
||||
- Applying without creator profile
|
||||
|
||||
6. **Invalid Query Parameters** (3 tests)
|
||||
- Invalid pagination parameters
|
||||
- Oversized limits
|
||||
- Invalid arm filters
|
||||
|
||||
7. **Empty & Null Values** (2 tests)
|
||||
- Empty user_id and username
|
||||
- Empty search strings
|
||||
|
||||
8. **DevConnect Linking Errors** (3 tests)
|
||||
- Missing required fields
|
||||
- Non-existent creator
|
||||
|
|
@ -85,10 +79,13 @@ Phase 3 successfully delivered comprehensive testing infrastructure for the AeTh
|
|||
**Total:** 22 error handling test cases
|
||||
|
||||
### 3. Performance Test Suite (`code/tests/performance.test.ts`)
|
||||
|
||||
**Status:** ✅ Complete (282 lines)
|
||||
|
||||
**Benchmarked Categories:**
|
||||
|
||||
1. **GET Endpoints** (Browse, Filter, Individual Retrieval)
|
||||
|
||||
- /api/creators (pagination)
|
||||
- /api/opportunities (pagination)
|
||||
- /api/applications
|
||||
|
|
@ -99,11 +96,13 @@ Phase 3 successfully delivered comprehensive testing infrastructure for the AeTh
|
|||
- /api/devconnect/link
|
||||
|
||||
2. **POST Endpoints** (Create Operations)
|
||||
|
||||
- POST /api/creators
|
||||
- POST /api/opportunities
|
||||
- POST /api/applications
|
||||
|
||||
3. **PUT Endpoints** (Update Operations)
|
||||
|
||||
- PUT /api/creators/:id
|
||||
- PUT /api/opportunities/:id
|
||||
|
||||
|
|
@ -112,6 +111,7 @@ Phase 3 successfully delivered comprehensive testing infrastructure for the AeTh
|
|||
- Deep pagination
|
||||
|
||||
**Metrics Collected:**
|
||||
|
||||
- Average response time (ms)
|
||||
- Min/Max response times
|
||||
- P95/P99 percentiles
|
||||
|
|
@ -119,54 +119,66 @@ Phase 3 successfully delivered comprehensive testing infrastructure for the AeTh
|
|||
- Performance target compliance
|
||||
|
||||
**Performance Targets:**
|
||||
|
||||
- GET endpoints: < 100ms
|
||||
- POST endpoints: < 200ms
|
||||
- PUT endpoints: < 150ms
|
||||
- Complex queries: < 250ms
|
||||
|
||||
### 4. Security Audit Checklist (`code/tests/SECURITY_AUDIT.md`)
|
||||
|
||||
**Status:** ✅ Complete (276 lines)
|
||||
|
||||
**Sections:**
|
||||
|
||||
1. **Authentication & Authorization**
|
||||
|
||||
- JWT validation
|
||||
- User context extraction
|
||||
- Authorization checks
|
||||
|
||||
2. **Row Level Security (RLS) Policies**
|
||||
|
||||
- Per-table RLS policies
|
||||
- Visibility controls
|
||||
- Ownership enforcement
|
||||
|
||||
3. **Data Protection**
|
||||
|
||||
- Sensitive data handling
|
||||
- Private field protection
|
||||
- Rate limiting
|
||||
|
||||
4. **Input Validation & Sanitization**
|
||||
|
||||
- Text field validation
|
||||
- File upload security
|
||||
- Array field validation
|
||||
- Numeric field validation
|
||||
|
||||
5. **API Endpoint Security**
|
||||
|
||||
- Per-endpoint security checklist
|
||||
- GET/POST/PUT/DELETE security
|
||||
- Parameter validation
|
||||
|
||||
6. **SQL Injection Prevention**
|
||||
|
||||
- Parameterized queries
|
||||
- Search/filter safety
|
||||
|
||||
7. **CORS & External Access**
|
||||
|
||||
- CORS headers
|
||||
- URL validation
|
||||
|
||||
8. **Audit Logging**
|
||||
|
||||
- Critical action logging
|
||||
- Log retention
|
||||
|
||||
9. **API Response Security**
|
||||
|
||||
- Error message safety
|
||||
- Response headers
|
||||
|
||||
|
|
@ -180,6 +192,7 @@ Phase 3 successfully delivered comprehensive testing infrastructure for the AeTh
|
|||
## 📊 Testing Coverage
|
||||
|
||||
### APIs Tested
|
||||
|
||||
- ✅ GET /api/creators (browse, filters, search, pagination)
|
||||
- ✅ GET /api/creators/:username (individual 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)
|
||||
|
||||
### Test Scenarios Covered
|
||||
|
||||
- ✅ Complete user journeys (signup → profile → post → apply → track)
|
||||
- ✅ Filtering and search functionality
|
||||
- ✅ Pagination and sorting
|
||||
|
|
@ -213,6 +227,7 @@ Phase 3 successfully delivered comprehensive testing infrastructure for the AeTh
|
|||
## 🎯 Key Findings
|
||||
|
||||
### Strengths
|
||||
|
||||
1. **Comprehensive API**: All creator network endpoints fully functional
|
||||
2. **Error Handling**: Proper HTTP status codes and error messages
|
||||
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
|
||||
|
||||
### Recommendations
|
||||
|
||||
1. **Security**: Implement full RLS policies (see SECURITY_AUDIT.md)
|
||||
2. **Rate Limiting**: Add rate limiting to prevent abuse
|
||||
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
|
||||
|
||||
Phase 4: Onboarding Integration
|
||||
|
||||
- Integrate creator profile setup into signup flow
|
||||
- Auto-create creator profiles on account creation
|
||||
- Collect creator preferences during onboarding
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
# Creator Network Security Audit Checklist
|
||||
|
||||
## Phase 3: Testing & Validation
|
||||
|
||||
### 🔐 Authentication & Authorization
|
||||
|
||||
- [ ] **JWT Validation**
|
||||
|
||||
- [ ] All protected endpoints require valid JWT token
|
||||
- [ ] Expired tokens are rejected
|
||||
- [ ] Invalid/malformed tokens return 401
|
||||
- [ ] Token claims are validated before processing
|
||||
|
||||
- [ ] **User Context Extraction**
|
||||
|
||||
- [ ] user_id is extracted from Supabase auth context (not request body)
|
||||
- [ ] User cannot access/modify other users' data
|
||||
- [ ] Session invalidation works properly on logout
|
||||
|
|
@ -24,23 +27,27 @@
|
|||
### 🛡️ Row Level Security (RLS) Policies
|
||||
|
||||
- [ ] **aethex_creators table**
|
||||
|
||||
- [ ] Users can read own profile
|
||||
- [ ] Users can update own profile
|
||||
- [ ] Public profiles are discoverable (is_discoverable=true)
|
||||
- [ ] Private profiles (is_discoverable=false) are hidden from directory
|
||||
|
||||
- [ ] **aethex_opportunities table**
|
||||
|
||||
- [ ] Anyone can read open opportunities
|
||||
- [ ] Only creator can update/delete own opportunities
|
||||
- [ ] Closed opportunities not visible to applicants
|
||||
|
||||
- [ ] **aethex_applications table**
|
||||
|
||||
- [ ] Users can read their own applications
|
||||
- [ ] Applicant can only see their own applications
|
||||
- [ ] Opportunity creator can see applications for their opportunities
|
||||
- [ ] Users cannot access others' applications
|
||||
|
||||
- [ ] **aethex_devconnect_links table**
|
||||
|
||||
- [ ] Users can only access their own DevConnect links
|
||||
- [ ] Links cannot be modified by non-owners
|
||||
|
||||
|
|
@ -51,11 +58,13 @@
|
|||
### 🔒 Data Protection
|
||||
|
||||
- [ ] **Sensitive Data**
|
||||
|
||||
- [ ] Passwords are never returned in API responses
|
||||
- [ ] Email addresses are not exposed in public profiles
|
||||
- [ ] Private notes/applications are not leaked
|
||||
|
||||
- [ ] **Cover Letters**
|
||||
|
||||
- [ ] Only applicant and opportunity creator can see cover letters
|
||||
- [ ] Cover letters are not visible in search results
|
||||
|
||||
|
|
@ -67,16 +76,19 @@
|
|||
### 🚫 Input Validation & Sanitization
|
||||
|
||||
- [ ] **Text Fields**
|
||||
|
||||
- [ ] Bio/description max length enforced (e.g., 500 chars)
|
||||
- [ ] Username format validated (alphanumeric, dashes, underscores)
|
||||
- [ ] HTML/script tags are escaped in output
|
||||
|
||||
- [ ] **File Uploads**
|
||||
|
||||
- [ ] Avatar URLs are validated/whitelisted
|
||||
- [ ] No malicious file types accepted
|
||||
- [ ] File size limits enforced
|
||||
|
||||
- [ ] **Array Fields**
|
||||
|
||||
- [ ] Skills array has max length
|
||||
- [ ] Arm affiliations are from valid set
|
||||
- [ ] Invalid values are rejected
|
||||
|
|
@ -89,16 +101,20 @@
|
|||
### 🔗 API Endpoint Security
|
||||
|
||||
**Creators Endpoints:**
|
||||
|
||||
- [ ] GET /api/creators
|
||||
|
||||
- [ ] Pagination parameters validated
|
||||
- [ ] Search doesn't expose private fields
|
||||
- [ ] Arm filter works correctly
|
||||
|
||||
- [ ] GET /api/creators/:username
|
||||
|
||||
- [ ] Returns 404 if profile is not discoverable
|
||||
- [ ] No sensitive data leaked
|
||||
|
||||
- [ ] POST /api/creators
|
||||
|
||||
- [ ] Requires auth
|
||||
- [ ] user_id extracted from auth context
|
||||
- [ ] Duplicate username prevention works
|
||||
|
|
@ -109,16 +125,20 @@
|
|||
- [ ] No privilege escalation possible
|
||||
|
||||
**Opportunities Endpoints:**
|
||||
|
||||
- [ ] GET /api/opportunities
|
||||
|
||||
- [ ] Only open opportunities shown
|
||||
- [ ] Closed/draft opportunities hidden
|
||||
- [ ] Pagination and filters work
|
||||
|
||||
- [ ] GET /api/opportunities/:id
|
||||
|
||||
- [ ] Only returns open opportunities
|
||||
- [ ] Creator info is sanitized
|
||||
|
||||
- [ ] POST /api/opportunities
|
||||
|
||||
- [ ] Requires auth + creator profile
|
||||
- [ ] user_id extracted from auth
|
||||
- [ ] Only opportunity creator can post
|
||||
|
|
@ -129,18 +149,22 @@
|
|||
- [ ] Can't change posted_by_id
|
||||
|
||||
**Applications Endpoints:**
|
||||
|
||||
- [ ] GET /api/applications
|
||||
|
||||
- [ ] Requires user_id + auth
|
||||
- [ ] Users only see their own applications
|
||||
- [ ] Opportunity creators can view applications
|
||||
|
||||
- [ ] POST /api/applications
|
||||
|
||||
- [ ] Requires auth + creator profile
|
||||
- [ ] Validates opportunity exists
|
||||
- [ ] Prevents duplicate applications
|
||||
- [ ] Validates cover letter length
|
||||
|
||||
- [ ] PUT /api/applications/:id
|
||||
|
||||
- [ ] Requires auth
|
||||
- [ ] Only opportunity creator can update
|
||||
- [ ] Can only change status/response_message
|
||||
|
|
@ -152,12 +176,15 @@
|
|||
- [ ] Application is properly deleted
|
||||
|
||||
**DevConnect Endpoints:**
|
||||
|
||||
- [ ] POST /api/devconnect/link
|
||||
|
||||
- [ ] Requires auth + creator profile
|
||||
- [ ] user_id from auth context
|
||||
- [ ] Validates DevConnect username format
|
||||
|
||||
- [ ] GET /api/devconnect/link
|
||||
|
||||
- [ ] Requires user_id + auth
|
||||
- [ ] Users only see their own link
|
||||
- [ ] Returns null if not linked
|
||||
|
|
@ -170,6 +197,7 @@
|
|||
### 🔍 SQL Injection Prevention
|
||||
|
||||
- [ ] **Parameterized Queries**
|
||||
|
||||
- [ ] All Supabase queries use parameterized queries (not string concatenation)
|
||||
- [ ] User input never directly in SQL strings
|
||||
- [ ] Search queries are sanitized
|
||||
|
|
@ -182,6 +210,7 @@
|
|||
### 🌐 CORS & External Access
|
||||
|
||||
- [ ] **CORS Headers**
|
||||
|
||||
- [ ] Only allowed origins can call API
|
||||
- [ ] Credentials are properly scoped
|
||||
- [ ] Preflight requests handled correctly
|
||||
|
|
@ -194,6 +223,7 @@
|
|||
### 📋 Audit Logging
|
||||
|
||||
- [ ] **Critical Actions Logged**
|
||||
|
||||
- [ ] User account creation
|
||||
- [ ] Opportunity creation/deletion
|
||||
- [ ] Application status changes
|
||||
|
|
@ -208,6 +238,7 @@
|
|||
### 🔄 API Response Security
|
||||
|
||||
- [ ] **Error Messages**
|
||||
|
||||
- [ ] Don't leak system details
|
||||
- [ ] Don't expose database structure
|
||||
- [ ] Generic error messages for auth failures
|
||||
|
|
@ -222,11 +253,13 @@
|
|||
### 📱 Frontend Security
|
||||
|
||||
- [ ] **Token Management**
|
||||
|
||||
- [ ] Tokens stored securely (not localStorage if possible)
|
||||
- [ ] Tokens cleared on logout
|
||||
- [ ] Token refresh handled properly
|
||||
|
||||
- [ ] **XSS Prevention**
|
||||
|
||||
- [ ] User input escaped in templates
|
||||
- [ ] No dangerouslySetInnerHTML without sanitization
|
||||
- [ ] No eval() or similar dangerous functions
|
||||
|
|
@ -238,17 +271,20 @@
|
|||
### ✅ Testing Recommendations
|
||||
|
||||
1. **Penetration Testing**
|
||||
|
||||
- Test SQL injection attempts
|
||||
- Test XSS payloads in input fields
|
||||
- Test CSRF attacks
|
||||
- Test broken access control
|
||||
|
||||
2. **Authorization Testing**
|
||||
|
||||
- Try accessing other users' resources
|
||||
- Test privilege escalation attempts
|
||||
- Verify RLS policies are enforced
|
||||
|
||||
3. **Data Validation Testing**
|
||||
|
||||
- Send oversized inputs
|
||||
- Send malformed data
|
||||
- Test boundary values
|
||||
|
|
@ -269,7 +305,6 @@
|
|||
|
||||
---
|
||||
|
||||
**Audit Date:** _________________
|
||||
**Auditor:** _________________
|
||||
**Audit Date:** ********\_********
|
||||
**Auditor:** ********\_********
|
||||
**Status:** PENDING ⏳
|
||||
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@ const log = (result: TestResult) => {
|
|||
results.push(result);
|
||||
const symbol = result.status === "✓" ? "✓" : "✗";
|
||||
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 (
|
||||
method: string,
|
||||
endpoint: string,
|
||||
body?: any
|
||||
body?: any,
|
||||
): Promise<any> => {
|
||||
try {
|
||||
const options: RequestInit = {
|
||||
|
|
@ -56,7 +56,7 @@ async function runTests() {
|
|||
try {
|
||||
const { response, data } = await testEndpoint(
|
||||
"GET",
|
||||
"/api/creators?page=1&limit=10"
|
||||
"/api/creators?page=1&limit=10",
|
||||
);
|
||||
if (response.ok) {
|
||||
log({
|
||||
|
|
@ -89,7 +89,7 @@ async function runTests() {
|
|||
try {
|
||||
const { response, data } = await testEndpoint(
|
||||
"GET",
|
||||
"/api/creators/testuser123"
|
||||
"/api/creators/testuser123",
|
||||
);
|
||||
if (response.status === 404) {
|
||||
log({
|
||||
|
|
@ -178,7 +178,7 @@ async function runTests() {
|
|||
try {
|
||||
const { response, data } = await testEndpoint(
|
||||
"GET",
|
||||
"/api/opportunities?page=1&limit=10"
|
||||
"/api/opportunities?page=1&limit=10",
|
||||
);
|
||||
if (response.ok) {
|
||||
log({
|
||||
|
|
@ -211,7 +211,7 @@ async function runTests() {
|
|||
try {
|
||||
const { response, data } = await testEndpoint(
|
||||
"GET",
|
||||
"/api/opportunities/fake-id-123"
|
||||
"/api/opportunities/fake-id-123",
|
||||
);
|
||||
if (response.status === 404) {
|
||||
log({
|
||||
|
|
@ -251,7 +251,7 @@ async function runTests() {
|
|||
description: "A test job opportunity",
|
||||
job_type: "contract",
|
||||
arm_affiliation: "gameforge",
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status === 404) {
|
||||
|
|
@ -320,7 +320,7 @@ async function runTests() {
|
|||
{
|
||||
user_id: "test-user-123",
|
||||
devconnect_username: "testuser",
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status === 404) {
|
||||
|
|
@ -363,14 +363,16 @@ async function runTests() {
|
|||
const passed = results.filter((r) => r.status === "✓").length;
|
||||
const failed = results.filter((r) => r.status === "✗").length;
|
||||
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) {
|
||||
console.log("\n❌ Failed tests:");
|
||||
results.filter((r) => r.status === "✗").forEach((r) => {
|
||||
console.log(` - ${r.method} ${r.endpoint}: ${r.message}`);
|
||||
});
|
||||
results
|
||||
.filter((r) => r.status === "✗")
|
||||
.forEach((r) => {
|
||||
console.log(` - ${r.method} ${r.endpoint}: ${r.message}`);
|
||||
});
|
||||
} else {
|
||||
console.log("\n✅ All tests passed!");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,14 @@ const results: TestCase[] = [];
|
|||
const BASE_URL = "http://localhost:5173";
|
||||
|
||||
// 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 });
|
||||
const symbol = passed ? "✓" : "✗";
|
||||
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) {
|
||||
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
|
||||
console.log("\n📝 FLOW 1: Creator Registration & Profile Setup");
|
||||
console.log("=" .repeat(50));
|
||||
console.log("=".repeat(50));
|
||||
|
||||
try {
|
||||
// Create first creator profile
|
||||
|
|
@ -95,13 +105,21 @@ async function runE2ETests() {
|
|||
"Create creator profile 1",
|
||||
createRes1.status === 201,
|
||||
`Status: ${createRes1.status}`,
|
||||
createCreator1Duration
|
||||
createCreator1Duration,
|
||||
);
|
||||
|
||||
if (createRes1.ok) {
|
||||
assertExists(creator1Data.id, "Creator ID should exist");
|
||||
assertEquals(creator1Data.username, testUsers.creator1.username, "Username mismatch");
|
||||
assertEquals(creator1Data.primary_arm, "gameforge", "Primary arm mismatch");
|
||||
assertEquals(
|
||||
creator1Data.username,
|
||||
testUsers.creator1.username,
|
||||
"Username mismatch",
|
||||
);
|
||||
assertEquals(
|
||||
creator1Data.primary_arm,
|
||||
"gameforge",
|
||||
"Primary arm mismatch",
|
||||
);
|
||||
}
|
||||
|
||||
// Create second creator profile
|
||||
|
|
@ -127,7 +145,7 @@ async function runE2ETests() {
|
|||
"Create creator profile 2",
|
||||
createRes2.status === 201,
|
||||
`Status: ${createRes2.status}`,
|
||||
createCreator2Duration
|
||||
createCreator2Duration,
|
||||
);
|
||||
} catch (error: any) {
|
||||
test("Create creator profiles", false, error.message);
|
||||
|
|
@ -135,7 +153,7 @@ async function runE2ETests() {
|
|||
|
||||
// FLOW 2: Opportunity Creation
|
||||
console.log("\n📋 FLOW 2: Opportunity Creation & Discovery");
|
||||
console.log("=" .repeat(50));
|
||||
console.log("=".repeat(50));
|
||||
|
||||
let opportunityId: string | null = null;
|
||||
|
||||
|
|
@ -148,7 +166,8 @@ async function runE2ETests() {
|
|||
body: JSON.stringify({
|
||||
user_id: testUsers.creator1.id,
|
||||
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",
|
||||
salary_min: 80000,
|
||||
salary_max: 120000,
|
||||
|
|
@ -163,20 +182,24 @@ async function runE2ETests() {
|
|||
"Create opportunity",
|
||||
oppRes.status === 201,
|
||||
`Status: ${oppRes.status}`,
|
||||
createOppDuration
|
||||
createOppDuration,
|
||||
);
|
||||
|
||||
if (oppRes.ok) {
|
||||
opportunityId = oppData.id;
|
||||
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");
|
||||
}
|
||||
|
||||
// Browse opportunities with filters
|
||||
const browseOppStart = Date.now();
|
||||
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 browseOppDuration = Date.now() - browseOppStart;
|
||||
|
|
@ -185,7 +208,7 @@ async function runE2ETests() {
|
|||
"Browse opportunities with filters",
|
||||
browseRes.ok && Array.isArray(browseData.data),
|
||||
`Status: ${browseRes.status}, Found: ${browseData.data?.length || 0}`,
|
||||
browseOppDuration
|
||||
browseOppDuration,
|
||||
);
|
||||
|
||||
if (browseRes.ok) {
|
||||
|
|
@ -198,13 +221,13 @@ async function runE2ETests() {
|
|||
|
||||
// FLOW 3: Creator Discovery
|
||||
console.log("\n👥 FLOW 3: Creator Discovery & Profiles");
|
||||
console.log("=" .repeat(50));
|
||||
console.log("=".repeat(50));
|
||||
|
||||
try {
|
||||
// Browse creators
|
||||
const browseCreatorsStart = Date.now();
|
||||
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 browseCreatorsDuration = Date.now() - browseCreatorsStart;
|
||||
|
|
@ -213,13 +236,13 @@ async function runE2ETests() {
|
|||
"Browse creators with arm filter",
|
||||
creatorsRes.ok && Array.isArray(creatorsData.data),
|
||||
`Status: ${creatorsRes.status}, Found: ${creatorsData.data?.length || 0}`,
|
||||
browseCreatorsDuration
|
||||
browseCreatorsDuration,
|
||||
);
|
||||
|
||||
// Get individual creator profile
|
||||
const getCreatorStart = Date.now();
|
||||
const creatorRes = await fetch(
|
||||
`${BASE_URL}/api/creators/${testUsers.creator1.username}`
|
||||
`${BASE_URL}/api/creators/${testUsers.creator1.username}`,
|
||||
);
|
||||
const creatorData = await creatorRes.json();
|
||||
const getCreatorDuration = Date.now() - getCreatorStart;
|
||||
|
|
@ -228,13 +251,17 @@ async function runE2ETests() {
|
|||
"Get creator profile by username",
|
||||
creatorRes.ok && creatorData.username === testUsers.creator1.username,
|
||||
`Status: ${creatorRes.status}, Username: ${creatorData.username}`,
|
||||
getCreatorDuration
|
||||
getCreatorDuration,
|
||||
);
|
||||
|
||||
if (creatorRes.ok) {
|
||||
assertExists(creatorData.bio, "Bio 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) {
|
||||
test("Creator discovery and profiles", false, error.message);
|
||||
|
|
@ -242,7 +269,7 @@ async function runE2ETests() {
|
|||
|
||||
// FLOW 4: Application Submission & Tracking
|
||||
console.log("\n✉️ FLOW 4: Apply for Opportunity & Track Status");
|
||||
console.log("=" .repeat(50));
|
||||
console.log("=".repeat(50));
|
||||
|
||||
let applicationId: string | null = null;
|
||||
|
||||
|
|
@ -270,7 +297,7 @@ async function runE2ETests() {
|
|||
"Submit application",
|
||||
applyRes.status === 201,
|
||||
`Status: ${applyRes.status}`,
|
||||
applyDuration
|
||||
applyDuration,
|
||||
);
|
||||
|
||||
if (applyRes.ok) {
|
||||
|
|
@ -295,13 +322,13 @@ async function runE2ETests() {
|
|||
"Prevent duplicate applications",
|
||||
dupRes.status === 400,
|
||||
`Status: ${dupRes.status} (should be 400)`,
|
||||
0
|
||||
0,
|
||||
);
|
||||
|
||||
// Get applications for creator
|
||||
const getAppsStart = Date.now();
|
||||
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 getAppsDuration = Date.now() - getAppsStart;
|
||||
|
|
@ -310,28 +337,31 @@ async function runE2ETests() {
|
|||
"Get creator's applications",
|
||||
appsRes.ok && Array.isArray(appsData.data),
|
||||
`Status: ${appsRes.status}, Found: ${appsData.data?.length || 0}`,
|
||||
getAppsDuration
|
||||
getAppsDuration,
|
||||
);
|
||||
|
||||
// Update application status (as opportunity creator)
|
||||
if (applicationId) {
|
||||
const updateStart = Date.now();
|
||||
const updateRes = await fetch(`${BASE_URL}/api/applications/${applicationId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
user_id: testUsers.creator1.id,
|
||||
status: "accepted",
|
||||
response_message: "Great! We'd love to have you on board.",
|
||||
}),
|
||||
});
|
||||
const updateRes = await fetch(
|
||||
`${BASE_URL}/api/applications/${applicationId}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
user_id: testUsers.creator1.id,
|
||||
status: "accepted",
|
||||
response_message: "Great! We'd love to have you on board.",
|
||||
}),
|
||||
},
|
||||
);
|
||||
const updateDuration = Date.now() - updateStart;
|
||||
|
||||
test(
|
||||
"Update application status",
|
||||
updateRes.ok,
|
||||
`Status: ${updateRes.status}`,
|
||||
updateDuration
|
||||
updateDuration,
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
|
@ -340,7 +370,7 @@ async function runE2ETests() {
|
|||
|
||||
// FLOW 5: DevConnect Linking
|
||||
console.log("\n<><6E> FLOW 5: DevConnect Account Linking");
|
||||
console.log("=" .repeat(50));
|
||||
console.log("=".repeat(50));
|
||||
|
||||
try {
|
||||
// Link DevConnect account
|
||||
|
|
@ -361,13 +391,13 @@ async function runE2ETests() {
|
|||
"Link DevConnect account",
|
||||
linkRes.status === 201 || linkRes.status === 200,
|
||||
`Status: ${linkRes.status}`,
|
||||
linkDuration
|
||||
linkDuration,
|
||||
);
|
||||
|
||||
// Get DevConnect link
|
||||
const getLinkStart = Date.now();
|
||||
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 getLinkDuration = Date.now() - getLinkStart;
|
||||
|
|
@ -376,11 +406,15 @@ async function runE2ETests() {
|
|||
"Get DevConnect link",
|
||||
getLinkRes.ok && getLinkData.data,
|
||||
`Status: ${getLinkRes.status}`,
|
||||
getLinkDuration
|
||||
getLinkDuration,
|
||||
);
|
||||
|
||||
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
|
||||
|
|
@ -394,7 +428,7 @@ async function runE2ETests() {
|
|||
"Unlink DevConnect account",
|
||||
unlinkRes.ok,
|
||||
`Status: ${unlinkRes.status}`,
|
||||
0
|
||||
0,
|
||||
);
|
||||
} catch (error: any) {
|
||||
test("DevConnect linking", false, error.message);
|
||||
|
|
@ -402,13 +436,13 @@ async function runE2ETests() {
|
|||
|
||||
// FLOW 6: Filtering and Search
|
||||
console.log("\n🔍 FLOW 6: Advanced Filtering & Search");
|
||||
console.log("=" .repeat(50));
|
||||
console.log("=".repeat(50));
|
||||
|
||||
try {
|
||||
// Test creator search
|
||||
const searchStart = Date.now();
|
||||
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 searchDuration = Date.now() - searchStart;
|
||||
|
|
@ -417,13 +451,13 @@ async function runE2ETests() {
|
|||
"Search creators by name",
|
||||
searchRes.ok && Array.isArray(searchData.data),
|
||||
`Status: ${searchRes.status}, Found: ${searchData.data?.length || 0}`,
|
||||
searchDuration
|
||||
searchDuration,
|
||||
);
|
||||
|
||||
// Test opportunity filtering by experience level
|
||||
const expFilterStart = Date.now();
|
||||
const expRes = await fetch(
|
||||
`${BASE_URL}/api/opportunities?experienceLevel=senior`
|
||||
`${BASE_URL}/api/opportunities?experienceLevel=senior`,
|
||||
);
|
||||
const expData = await expRes.json();
|
||||
const expFilterDuration = Date.now() - expFilterStart;
|
||||
|
|
@ -432,7 +466,7 @@ async function runE2ETests() {
|
|||
"Filter opportunities by experience level",
|
||||
expRes.ok && Array.isArray(expData.data),
|
||||
`Status: ${expRes.status}, Found: ${expData.data?.length || 0}`,
|
||||
expFilterDuration
|
||||
expFilterDuration,
|
||||
);
|
||||
|
||||
// Test pagination
|
||||
|
|
@ -445,10 +479,13 @@ async function runE2ETests() {
|
|||
"Pagination - page 1",
|
||||
page1Res.ok && page1Data.pagination?.page === 1,
|
||||
`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) {
|
||||
test("Filtering and search", false, error.message);
|
||||
}
|
||||
|
|
@ -463,14 +500,18 @@ async function runE2ETests() {
|
|||
console.log(` ✓ Passed: ${passed}`);
|
||||
console.log(` ✗ Failed: ${failed}`);
|
||||
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));
|
||||
|
||||
if (failed > 0) {
|
||||
console.log("\n❌ Failed Tests:");
|
||||
results.filter((r) => !r.passed).forEach((r) => {
|
||||
console.log(` - ${r.name}: ${r.message}`);
|
||||
});
|
||||
results
|
||||
.filter((r) => !r.passed)
|
||||
.forEach((r) => {
|
||||
console.log(` - ${r.name}: ${r.message}`);
|
||||
});
|
||||
} else {
|
||||
console.log("\n✅ All tests passed!");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ const test = (
|
|||
passed: boolean,
|
||||
expectedStatus: number,
|
||||
actualStatus: number,
|
||||
message: string
|
||||
message: string,
|
||||
) => {
|
||||
results.push({ name, passed, expectedStatus, actualStatus, message });
|
||||
const symbol = passed ? "✓" : "✗";
|
||||
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,
|
||||
400,
|
||||
res.status,
|
||||
"Should require user_id"
|
||||
"Should require user_id",
|
||||
);
|
||||
|
||||
res = await fetch(`${BASE_URL}/api/creators`, {
|
||||
|
|
@ -59,7 +59,7 @@ async function runErrorTests() {
|
|||
res.status === 400,
|
||||
400,
|
||||
res.status,
|
||||
"Should require username"
|
||||
"Should require username",
|
||||
);
|
||||
|
||||
res = await fetch(`${BASE_URL}/api/opportunities`, {
|
||||
|
|
@ -72,7 +72,7 @@ async function runErrorTests() {
|
|||
res.status === 400,
|
||||
400,
|
||||
res.status,
|
||||
"Should require title"
|
||||
"Should require title",
|
||||
);
|
||||
|
||||
res = await fetch(`${BASE_URL}/api/applications`, {
|
||||
|
|
@ -85,7 +85,7 @@ async function runErrorTests() {
|
|||
res.status === 400,
|
||||
400,
|
||||
res.status,
|
||||
"Should require opportunity_id"
|
||||
"Should require opportunity_id",
|
||||
);
|
||||
|
||||
// ERROR CATEGORY 2: Not Found Errors
|
||||
|
|
@ -98,7 +98,7 @@ async function runErrorTests() {
|
|||
res.status === 404,
|
||||
404,
|
||||
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`);
|
||||
|
|
@ -107,7 +107,7 @@ async function runErrorTests() {
|
|||
res.status === 404,
|
||||
404,
|
||||
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`);
|
||||
|
|
@ -116,7 +116,7 @@ async function runErrorTests() {
|
|||
res.status === 404,
|
||||
404,
|
||||
res.status,
|
||||
"Should return 404 when creator doesn't exist"
|
||||
"Should return 404 when creator doesn't exist",
|
||||
);
|
||||
|
||||
// ERROR CATEGORY 3: Authorization/Ownership Errors
|
||||
|
|
@ -154,7 +154,7 @@ async function runErrorTests() {
|
|||
res.status === 500 || res.status === 404,
|
||||
404,
|
||||
res.status,
|
||||
"Should reject invalid creator ID"
|
||||
"Should reject invalid creator ID",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +188,7 @@ async function runErrorTests() {
|
|||
res.status === 400,
|
||||
400,
|
||||
res.status,
|
||||
"Should prevent duplicate usernames"
|
||||
"Should prevent duplicate usernames",
|
||||
);
|
||||
|
||||
// Test duplicate application (create opportunity first)
|
||||
|
|
@ -260,7 +260,7 @@ async function runErrorTests() {
|
|||
res.status === 400,
|
||||
400,
|
||||
res.status,
|
||||
"Should prevent duplicate applications"
|
||||
"Should prevent duplicate applications",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -283,7 +283,7 @@ async function runErrorTests() {
|
|||
res.status === 404,
|
||||
404,
|
||||
res.status,
|
||||
"Should require existing creator profile"
|
||||
"Should require existing creator profile",
|
||||
);
|
||||
|
||||
res = await fetch(`${BASE_URL}/api/applications`, {
|
||||
|
|
@ -299,7 +299,7 @@ async function runErrorTests() {
|
|||
res.status === 404,
|
||||
404,
|
||||
res.status,
|
||||
"Should require existing creator profile"
|
||||
"Should require existing creator profile",
|
||||
);
|
||||
|
||||
// ERROR CATEGORY 6: Invalid Query Parameters
|
||||
|
|
@ -313,7 +313,7 @@ async function runErrorTests() {
|
|||
res.ok, // Should still work with default pagination
|
||||
200,
|
||||
res.status,
|
||||
"Should handle invalid page gracefully"
|
||||
"Should handle invalid page gracefully",
|
||||
);
|
||||
|
||||
res = await fetch(`${BASE_URL}/api/opportunities?limit=999999`);
|
||||
|
|
@ -322,7 +322,7 @@ async function runErrorTests() {
|
|||
res.ok, // Should cap the limit
|
||||
200,
|
||||
res.status,
|
||||
"Should cap maximum limit"
|
||||
"Should cap maximum limit",
|
||||
);
|
||||
|
||||
res = await fetch(`${BASE_URL}/api/creators?arm=invalid_arm`);
|
||||
|
|
@ -332,7 +332,7 @@ async function runErrorTests() {
|
|||
res.ok && Array.isArray(armData.data),
|
||||
200,
|
||||
res.status,
|
||||
"Should return empty results or handle gracefully"
|
||||
"Should return empty results or handle gracefully",
|
||||
);
|
||||
|
||||
// ERROR CATEGORY 7: Empty/Null Values
|
||||
|
|
@ -352,7 +352,7 @@ async function runErrorTests() {
|
|||
res.status === 400,
|
||||
400,
|
||||
res.status,
|
||||
"Should reject empty user_id"
|
||||
"Should reject empty user_id",
|
||||
);
|
||||
|
||||
res = await fetch(`${BASE_URL}/api/creators?search=`);
|
||||
|
|
@ -361,7 +361,7 @@ async function runErrorTests() {
|
|||
res.ok,
|
||||
200,
|
||||
res.status,
|
||||
"Should handle empty search gracefully"
|
||||
"Should handle empty search gracefully",
|
||||
);
|
||||
|
||||
// ERROR CATEGORY 8: Missing DevConnect Parameters
|
||||
|
|
@ -378,7 +378,7 @@ async function runErrorTests() {
|
|||
res.status === 400,
|
||||
400,
|
||||
res.status,
|
||||
"Should require user_id"
|
||||
"Should require user_id",
|
||||
);
|
||||
|
||||
res = await fetch(`${BASE_URL}/api/devconnect/link`, {
|
||||
|
|
@ -391,7 +391,7 @@ async function runErrorTests() {
|
|||
res.status === 400,
|
||||
400,
|
||||
res.status,
|
||||
"Should require devconnect_username"
|
||||
"Should require devconnect_username",
|
||||
);
|
||||
|
||||
res = await fetch(`${BASE_URL}/api/devconnect/link`, {
|
||||
|
|
@ -407,7 +407,7 @@ async function runErrorTests() {
|
|||
res.status === 404,
|
||||
404,
|
||||
res.status,
|
||||
"Should require existing creator profile"
|
||||
"Should require existing creator profile",
|
||||
);
|
||||
|
||||
// Summary
|
||||
|
|
@ -426,7 +426,9 @@ async function runErrorTests() {
|
|||
.filter((r) => !r.passed)
|
||||
.forEach((r) => {
|
||||
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 {
|
||||
console.log("\n✅ All error handling tests passed!");
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const metrics: PerformanceMetric[] = [];
|
|||
async function measureRequest(
|
||||
method: string,
|
||||
endpoint: string,
|
||||
body?: any
|
||||
body?: any,
|
||||
): Promise<number> {
|
||||
const start = performance.now();
|
||||
try {
|
||||
|
|
@ -45,10 +45,10 @@ async function benchmarkEndpoint(
|
|||
method: string,
|
||||
endpoint: string,
|
||||
numRequests: number = 50,
|
||||
body?: any
|
||||
body?: any,
|
||||
): Promise<PerformanceMetric> {
|
||||
console.log(
|
||||
` Benchmarking ${method.padEnd(6)} ${endpoint.padEnd(35)} (${numRequests} requests)...`
|
||||
` Benchmarking ${method.padEnd(6)} ${endpoint.padEnd(35)} (${numRequests} requests)...`,
|
||||
);
|
||||
|
||||
const times: number[] = [];
|
||||
|
|
@ -91,7 +91,11 @@ async function runPerformanceTests() {
|
|||
|
||||
console.log("\nFilter endpoints (filtered queries):");
|
||||
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);
|
||||
|
||||
console.log("\nIndividual resource retrieval:");
|
||||
|
|
@ -155,12 +159,12 @@ async function runPerformanceTests() {
|
|||
await benchmarkEndpoint(
|
||||
"GET",
|
||||
"/api/creators?arm=gameforge&search=test&page=1&limit=50",
|
||||
30
|
||||
30,
|
||||
);
|
||||
await benchmarkEndpoint(
|
||||
"GET",
|
||||
"/api/opportunities?arm=labs&experienceLevel=senior&jobType=full-time&page=1&limit=50",
|
||||
25
|
||||
25,
|
||||
);
|
||||
|
||||
console.log("\nMulti-page traversal:");
|
||||
|
|
@ -182,10 +186,15 @@ async function runPerformanceTests() {
|
|||
|
||||
// Print detailed metrics
|
||||
grouped.forEach((metricList, endpoint) => {
|
||||
const avg = metricList.reduce((sum, m) => sum + m.avgTime, 0) / metricList.length;
|
||||
const p95 = 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;
|
||||
const avg =
|
||||
metricList.reduce((sum, m) => sum + m.avgTime, 0) / metricList.length;
|
||||
const p95 =
|
||||
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(` Avg: ${avg.toFixed(2)}ms`);
|
||||
|
|
@ -210,21 +219,30 @@ async function runPerformanceTests() {
|
|||
|
||||
metrics.forEach((m) => {
|
||||
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") {
|
||||
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") {
|
||||
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
|
||||
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) {
|
||||
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 symbol = passed ? "✓" : "✗";
|
||||
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++;
|
||||
|
|
@ -247,24 +265,32 @@ async function runPerformanceTests() {
|
|||
|
||||
const allTimes = metrics.map((m) => m.avgTime);
|
||||
const slowestEndpoint = metrics.reduce((a, b) =>
|
||||
a.avgTime > b.avgTime ? a : b
|
||||
a.avgTime > b.avgTime ? 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(`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`
|
||||
`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) {
|
||||
console.log("\n✅ All performance targets met!");
|
||||
} 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));
|
||||
|
|
|
|||
Loading…
Reference in a new issue