API handler for opportunities endpoints
cgen-cb369258880a4f2396783edf046c066e
This commit is contained in:
parent
717f315156
commit
936fe8a3f8
1 changed files with 320 additions and 0 deletions
320
api/opportunities.ts
Normal file
320
api/opportunities.ts
Normal file
|
|
@ -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" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue