diff --git a/api/opportunities.ts b/api/opportunities.ts new file mode 100644 index 00000000..82361804 --- /dev/null +++ b/api/opportunities.ts @@ -0,0 +1,320 @@ +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = process.env.VITE_SUPABASE_URL || ""; +const supabaseServiceRole = process.env.SUPABASE_SERVICE_ROLE || ""; + +const supabase = createClient(supabaseUrl, supabaseServiceRole); + +export async function getOpportunities(req: Request) { + const url = new URL(req.url); + const arm = url.searchParams.get("arm"); + const page = parseInt(url.searchParams.get("page") || "1"); + const limit = parseInt(url.searchParams.get("limit") || "20"); + const sort = url.searchParams.get("sort") || "recent"; + const search = url.searchParams.get("search"); + const jobType = url.searchParams.get("jobType"); + const experienceLevel = url.searchParams.get("experienceLevel"); + + try { + let query = supabase + .from("aethex_opportunities") + .select( + ` + id, + title, + description, + job_type, + salary_min, + salary_max, + experience_level, + arm_affiliation, + posted_by_id, + aethex_creators!aethex_opportunities_posted_by_id_fkey(username, avatar_url), + status, + created_at, + aethex_applications(count) + `, + { count: "exact" } + ) + .eq("status", "open"); + + if (arm) { + query = query.eq("arm_affiliation", arm); + } + + if (search) { + query = query.or( + `title.ilike.%${search}%,description.ilike.%${search}%` + ); + } + + if (jobType) { + query = query.eq("job_type", jobType); + } + + if (experienceLevel) { + query = query.eq("experience_level", experienceLevel); + } + + // Sort by parameter + const ascending = sort === "oldest"; + query = query.order("created_at", { ascending }); + + const start = (page - 1) * limit; + query = query.range(start, start + limit - 1); + + const { data, error, count } = await query; + + if (error) throw error; + + return new Response( + JSON.stringify({ + data, + pagination: { + page, + limit, + total: count || 0, + pages: Math.ceil((count || 0) / limit), + }, + }), + { + 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" } } + ); + } +} + +export async function getOpportunityById(opportunityId: string) { + try { + const { data, error } = await supabase + .from("aethex_opportunities") + .select( + ` + id, + title, + description, + job_type, + salary_min, + salary_max, + experience_level, + arm_affiliation, + posted_by_id, + aethex_creators!aethex_opportunities_posted_by_id_fkey(id, username, bio, avatar_url), + status, + created_at, + updated_at, + aethex_applications(count) + ` + ) + .eq("id", opportunityId) + .eq("status", "open") + .single(); + + if (error) throw error; + + return data; + } catch (error) { + console.error("Error fetching opportunity:", error); + return null; + } +} + +export async function createOpportunity( + req: Request, + userId: string +) { + try { + const body = await req.json(); + const { + title, + description, + job_type, + salary_min, + salary_max, + experience_level, + arm_affiliation, + } = body; + + // Get the creator ID for this user + const { data: creator } = await supabase + .from("aethex_creators") + .select("id") + .eq("user_id", userId) + .single(); + + if (!creator) { + return new Response( + JSON.stringify({ error: "Creator profile not found. Create profile first." }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + const { data, error } = await supabase + .from("aethex_opportunities") + .insert({ + title, + description, + job_type, + salary_min, + salary_max, + experience_level, + arm_affiliation, + posted_by_id: creator.id, + status: "open", + }) + .select() + .single(); + + if (error) throw error; + + return new Response(JSON.stringify(data), { + status: 201, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error creating opportunity:", error); + return new Response( + JSON.stringify({ error: "Failed to create opportunity" }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +} + +export async function updateOpportunity( + req: Request, + opportunityId: string, + userId: string +) { + try { + const body = await req.json(); + const { + title, + description, + job_type, + salary_min, + salary_max, + experience_level, + status, + } = body; + + // Verify user owns this opportunity + const { data: opportunity } = await supabase + .from("aethex_opportunities") + .select("posted_by_id") + .eq("id", opportunityId) + .single(); + + if (!opportunity) { + return new Response( + JSON.stringify({ error: "Opportunity not found" }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + const { data: creator } = await supabase + .from("aethex_creators") + .select("id") + .eq("user_id", userId) + .single(); + + if (creator?.id !== opportunity.posted_by_id) { + return new Response( + JSON.stringify({ error: "Unauthorized" }), + { status: 403, headers: { "Content-Type": "application/json" } } + ); + } + + const { data, error } = await supabase + .from("aethex_opportunities") + .update({ + title, + description, + job_type, + salary_min, + salary_max, + experience_level, + status, + updated_at: new Date().toISOString(), + }) + .eq("id", opportunityId) + .select() + .single(); + + if (error) throw error; + + return new Response(JSON.stringify(data), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error updating opportunity:", error); + return new Response( + JSON.stringify({ error: "Failed to update opportunity" }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +} + +export async function closeOpportunity( + opportunityId: string, + userId: string +) { + try { + // Verify user owns this opportunity + const { data: opportunity } = await supabase + .from("aethex_opportunities") + .select("posted_by_id") + .eq("id", opportunityId) + .single(); + + if (!opportunity) { + return new Response( + JSON.stringify({ error: "Opportunity not found" }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + const { data: creator } = await supabase + .from("aethex_creators") + .select("id") + .eq("user_id", userId) + .single(); + + if (creator?.id !== opportunity.posted_by_id) { + return new Response( + JSON.stringify({ error: "Unauthorized" }), + { status: 403, headers: { "Content-Type": "application/json" } } + ); + } + + const { data, error } = await supabase + .from("aethex_opportunities") + .update({ + status: "closed", + updated_at: new Date().toISOString(), + }) + .eq("id", opportunityId) + .select() + .single(); + + if (error) throw error; + + return new Response(JSON.stringify(data), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error closing opportunity:", error); + return new Response( + JSON.stringify({ error: "Failed to close opportunity" }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +}