Update achievement activation to also award achievements to users
Modify the `/api/achievements/activate` endpoint to seed and award achievements, resolving foreign key constraint errors by ensuring `user_achievements` is populated correctly. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9203795e-937a-4306-b81d-b4d5c78c240e Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: ec6ee112-a299-40f7-9649-dc69aa3eaf2f Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7c94b7a0-29c7-4f2e-94ef-44b2153872b7/9203795e-937a-4306-b81d-b4d5c78c240e/lX9tyiI Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
e87525aa1c
commit
8d2508dbf9
3 changed files with 652 additions and 48 deletions
4
.replit
4
.replit
|
|
@ -52,6 +52,10 @@ externalPort = 80
|
|||
localPort = 8044
|
||||
externalPort = 3003
|
||||
|
||||
[[ports]]
|
||||
localPort = 33301
|
||||
externalPort = 3002
|
||||
|
||||
[[ports]]
|
||||
localPort = 38557
|
||||
externalPort = 3000
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
[Rewards] Activating at: https://aethex.dev/api/achievements/activate
|
||||
index-SltPHFdZ.js:1029 POST https://aethex.dev/api/achievements/activate 500 (Internal Server Error)
|
||||
activateCommunityRewards @ index-SltPHFdZ.js:1029
|
||||
(anonymous) @ index-SltPHFdZ.js:1029
|
||||
cf @ index-SltPHFdZ.js:40
|
||||
Kc @ index-SltPHFdZ.js:40
|
||||
(anonymous) @ index-SltPHFdZ.js:40
|
||||
_ @ index-SltPHFdZ.js:25
|
||||
F @ index-SltPHFdZ.js:25Understand this error
|
||||
index-SltPHFdZ.js:1029 [Rewards] Activation failed: 500 {"error":"insert or update on table \"user_achievements\" violates foreign key constraint \"user_achievements_achievement_id_fkey\""}
|
||||
activateCommunityRewards @ index-SltPHFdZ.js:1029
|
||||
await in activateCommunityRewards
|
||||
(anonymous) @ index-SltPHFdZ.js:1029
|
||||
cf @ index-SltPHFdZ.js:40
|
||||
Kc @ index-SltPHFdZ.js:40
|
||||
(anonymous) @ index-SltPHFdZ.js:40
|
||||
_ @ index-SltPHFdZ.js:25
|
||||
F @ index-SltPHFdZ.js:25Understand this warning
|
||||
index-SltPHFdZ.js:1762 GET https://aethex.dev/api/nexus/creator/contracts?limit=10 500 (Internal Server Error)
|
||||
G @ index-SltPHFdZ.js:1762
|
||||
await in G
|
||||
(anonymous) @ index-SltPHFdZ.js:1762
|
||||
cf @ index-SltPHFdZ.js:40
|
||||
Kc @ index-SltPHFdZ.js:40
|
||||
(anonymous) @ index-SltPHFdZ.js:40
|
||||
_ @ index-SltPHFdZ.js:25
|
||||
F @ index-SltPHFdZ.js:25Understand this error
|
||||
index-SltPHFdZ.js:1762 GET https://aethex.dev/api/nexus/creator/contracts?limit=10 500 (Internal Server Error)
|
||||
G @ index-SltPHFdZ.js:1762
|
||||
await in G
|
||||
(anonymous) @ index-SltPHFdZ.js:1762
|
||||
cf @ index-SltPHFdZ.js:40
|
||||
Kc @ index-SltPHFdZ.js:40
|
||||
(anonymous) @ index-SltPHFdZ.js:40
|
||||
_ @ index-SltPHFdZ.js:25
|
||||
F @ index-SltPHFdZ.js:25Understand this error
|
||||
index-SltPHFdZ.js:1762 GET https://aethex.dev/api/nexus/creator/contracts?limit=10 500 (Internal Server Error)
|
||||
G @ index-SltPHFdZ.js:1762
|
||||
await in G
|
||||
(anonymous) @ index-SltPHFdZ.js:1762
|
||||
cf @ index-SltPHFdZ.js:40
|
||||
Kc @ index-SltPHFdZ.js:40
|
||||
(anonymous) @ index-SltPHFdZ.js:40
|
||||
_ @ index-SltPHFdZ.js:25
|
||||
F @ index-SltPHFdZ.js:25Understand this error
|
||||
index-SltPHFdZ.js:1762 GET https://aethex.dev/api/nexus/client/applicants?limit=50 400 (Bad Request)
|
||||
G @ index-SltPHFdZ.js:1762
|
||||
await in G
|
||||
(anonymous) @ index-SltPHFdZ.js:1762
|
||||
cf @ index-SltPHFdZ.js:40
|
||||
Kc @ index-SltPHFdZ.js:40
|
||||
(anonymous) @ index-SltPHFdZ.js:40
|
||||
_ @ index-SltPHFdZ.js:25
|
||||
F @ index-SltPHFdZ.js:25Understand this error
|
||||
index-SltPHFdZ.js:1762 Uncaught (in promise) TypeError: ge is not a function
|
||||
at G (index-SltPHFdZ.js:1762:176575)
|
||||
G @ index-SltPHFdZ.js:1762
|
||||
await in G
|
||||
(anonymous) @ index-SltPHFdZ.js:1762
|
||||
cf @ index-SltPHFdZ.js:40
|
||||
Kc @ index-SltPHFdZ.js:40
|
||||
(anonymous) @ index-SltPHFdZ.js:40
|
||||
_ @ index-SltPHFdZ.js:25
|
||||
F @ index-SltPHFdZ.js:25Understand this error
|
||||
index-SltPHFdZ.js:1762 GET https://aethex.dev/api/nexus/client/applicants?limit=50 400 (Bad Request)
|
||||
G @ index-SltPHFdZ.js:1762
|
||||
await in G
|
||||
(anonymous) @ index-SltPHFdZ.js:1762
|
||||
cf @ index-SltPHFdZ.js:40
|
||||
Kc @ index-SltPHFdZ.js:40
|
||||
(anonymous) @ index-SltPHFdZ.js:40
|
||||
_ @ index-SltPHFdZ.js:25
|
||||
F @ index-SltPHFdZ.js:25Understand this error
|
||||
index-SltPHFdZ.js:1762 GET https://aethex.dev/api/nexus/client/applicants?limit=50 400 (Bad Request)
|
||||
G @ index-SltPHFdZ.js:1762
|
||||
await in G
|
||||
(anonymous) @ index-SltPHFdZ.js:1762
|
||||
cf @ index-SltPHFdZ.js:40
|
||||
Kc @ index-SltPHFdZ.js:40
|
||||
(anonymous) @ index-SltPHFdZ.js:40
|
||||
_ @ index-SltPHFdZ.js:25
|
||||
F @ index-SltPHFdZ.js:25Understand this error
|
||||
2index-SltPHFdZ.js:1762 Uncaught (in promise) TypeError: ge is not a function
|
||||
at G (index-SltPHFdZ.js:1762:176575)
|
||||
562
server/index.ts
562
server/index.ts
|
|
@ -2989,6 +2989,8 @@ export function createServer() {
|
|||
|
||||
app.post("/api/achievements/activate", async (req, res) => {
|
||||
try {
|
||||
const { targetEmail, targetUsername } = req.body || {};
|
||||
|
||||
const CORE_ACHIEVEMENTS = [
|
||||
{
|
||||
id: "welcome-to-aethex",
|
||||
|
|
@ -3001,8 +3003,7 @@ export function createServer() {
|
|||
{
|
||||
id: "aethex-explorer",
|
||||
name: "AeThex Explorer",
|
||||
description:
|
||||
"Engaged with community initiatives and posted first update.",
|
||||
description: "Engaged with community initiatives and posted first update.",
|
||||
icon: "🧭",
|
||||
badge_color: "#0EA5E9",
|
||||
xp_reward: 400,
|
||||
|
|
@ -3010,8 +3011,7 @@ export function createServer() {
|
|||
{
|
||||
id: "community-champion",
|
||||
name: "Community Champion",
|
||||
description:
|
||||
"Contributed feedback, resolved bugs, and mentored squads.",
|
||||
description: "Contributed feedback, resolved bugs, and mentored squads.",
|
||||
icon: "🏆",
|
||||
badge_color: "#22C55E",
|
||||
xp_reward: 750,
|
||||
|
|
@ -3019,28 +3019,21 @@ export function createServer() {
|
|||
{
|
||||
id: "workshop-architect",
|
||||
name: "Workshop Architect",
|
||||
description:
|
||||
"Published a high-impact mod or toolkit adopted by teams.",
|
||||
icon: "<22><>️",
|
||||
description: "Published a high-impact mod or toolkit adopted by teams.",
|
||||
icon: "🛠️",
|
||||
badge_color: "#F97316",
|
||||
xp_reward: 1200,
|
||||
},
|
||||
{
|
||||
id: "god-mode",
|
||||
name: "GOD Mode",
|
||||
description:
|
||||
"Legendary status awarded by AeThex studio leadership.",
|
||||
description: "Legendary status awarded by AeThex studio leadership.",
|
||||
icon: "⚡",
|
||||
badge_color: "#FACC15",
|
||||
xp_reward: 5000,
|
||||
},
|
||||
];
|
||||
|
||||
const nowIso = new Date().toISOString();
|
||||
|
||||
const achievementResults = await Promise.all(
|
||||
CORE_ACHIEVEMENTS.map(async (achievement) => {
|
||||
const { createHash } = await import("crypto");
|
||||
const generateDeterministicUUID = (str: string): string => {
|
||||
const hash = createHash("sha256").update(str).digest("hex");
|
||||
return [
|
||||
|
|
@ -3054,6 +3047,9 @@ export function createServer() {
|
|||
].join("-");
|
||||
};
|
||||
|
||||
// Step 1: Seed all core achievements
|
||||
const seededAchievements: { [key: string]: string } = {};
|
||||
for (const achievement of CORE_ACHIEVEMENTS) {
|
||||
const uuidId = generateDeterministicUUID(achievement.id);
|
||||
const { error } = await adminSupabase.from("achievements").upsert(
|
||||
{
|
||||
|
|
@ -3068,20 +3064,74 @@ export function createServer() {
|
|||
);
|
||||
|
||||
if (error && error.code !== "23505") {
|
||||
console.error(
|
||||
`Failed to upsert achievement ${achievement.id}:`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
console.error(`Failed to upsert achievement ${achievement.id}:`, error);
|
||||
} else {
|
||||
seededAchievements[achievement.name] = uuidId;
|
||||
}
|
||||
return achievement.id;
|
||||
}),
|
||||
}
|
||||
|
||||
console.log("[Achievements] Seeded", Object.keys(seededAchievements).length, "achievements");
|
||||
|
||||
// Step 2: Try to find target user and award achievements
|
||||
let targetUserId: string | null = null;
|
||||
let godModeAwarded = false;
|
||||
const awardedAchievementIds: string[] = [];
|
||||
|
||||
if (targetEmail || targetUsername) {
|
||||
let query = adminSupabase.from("user_profiles").select("id, email, username");
|
||||
if (targetEmail) {
|
||||
query = query.eq("email", targetEmail);
|
||||
} else if (targetUsername) {
|
||||
query = query.eq("username", targetUsername);
|
||||
}
|
||||
|
||||
const { data: userProfile } = await query.single();
|
||||
|
||||
if (userProfile?.id) {
|
||||
targetUserId = userProfile.id;
|
||||
|
||||
// Check if admin user (mrpiglr)
|
||||
const isAdmin = targetEmail === "mrpiglr@gmail.com" || targetUsername === "mrpiglr";
|
||||
|
||||
// Award Welcome achievement to everyone
|
||||
const welcomeId = seededAchievements["Welcome to AeThex"];
|
||||
if (welcomeId) {
|
||||
const { error: welcomeError } = await adminSupabase
|
||||
.from("user_achievements")
|
||||
.upsert(
|
||||
{ user_id: targetUserId, achievement_id: welcomeId },
|
||||
{ onConflict: "user_id,achievement_id" as any }
|
||||
);
|
||||
if (!welcomeError || welcomeError.code === "23505") {
|
||||
awardedAchievementIds.push(welcomeId);
|
||||
}
|
||||
}
|
||||
|
||||
// Award GOD Mode to admins
|
||||
if (isAdmin) {
|
||||
const godModeId = seededAchievements["GOD Mode"];
|
||||
if (godModeId) {
|
||||
const { error: godError } = await adminSupabase
|
||||
.from("user_achievements")
|
||||
.upsert(
|
||||
{ user_id: targetUserId, achievement_id: godModeId },
|
||||
{ onConflict: "user_id,achievement_id" as any }
|
||||
);
|
||||
if (!godError || godError.code === "23505") {
|
||||
godModeAwarded = true;
|
||||
awardedAchievementIds.push(godModeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
ok: true,
|
||||
achievementsSeeded: achievementResults.length,
|
||||
achievements: achievementResults,
|
||||
achievementsSeeded: Object.keys(seededAchievements).length,
|
||||
godModeAwarded,
|
||||
awardedAchievementIds,
|
||||
targetUserId,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("activate achievements error", error);
|
||||
|
|
@ -6522,6 +6572,472 @@ export function createServer() {
|
|||
console.warn("Admin API not initialized:", e);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NEXUS MARKETPLACE API ENDPOINTS
|
||||
// ========================================
|
||||
|
||||
// Helper function to get user from token
|
||||
const getUserFromToken = async (req: express.Request) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) return null;
|
||||
const token = authHeader.replace("Bearer ", "");
|
||||
const { data: { user }, error } = await adminSupabase.auth.getUser(token);
|
||||
if (error || !user) return null;
|
||||
return user;
|
||||
};
|
||||
|
||||
// GET/POST /api/nexus/creator/profile
|
||||
app.get("/api/nexus/creator/profile", async (req, res) => {
|
||||
const user = await getUserFromToken(req);
|
||||
if (!user) return res.status(401).json({ error: "Unauthorized" });
|
||||
|
||||
try {
|
||||
const { data: profile, error: profileError } = await adminSupabase
|
||||
.from("nexus_creator_profiles")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.single();
|
||||
|
||||
if (profileError && profileError.code !== "PGRST116") {
|
||||
return res.status(500).json({ error: profileError.message });
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
return res.status(200).json({
|
||||
user_id: user.id,
|
||||
headline: "",
|
||||
bio: "",
|
||||
profile_image_url: null,
|
||||
skills: [],
|
||||
experience_level: "intermediate",
|
||||
hourly_rate: null,
|
||||
portfolio_url: null,
|
||||
availability_status: "available",
|
||||
availability_hours_per_week: null,
|
||||
verified: false,
|
||||
total_earnings: 0,
|
||||
rating: null,
|
||||
review_count: 0,
|
||||
created_at: null,
|
||||
updated_at: null,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json(profile);
|
||||
} catch (error: any) {
|
||||
return res.status(500).json({ error: error?.message || "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/nexus/creator/profile", async (req, res) => {
|
||||
const user = await getUserFromToken(req);
|
||||
if (!user) return res.status(401).json({ error: "Unauthorized" });
|
||||
|
||||
try {
|
||||
const {
|
||||
headline, bio, profile_image_url, skills, experience_level,
|
||||
hourly_rate, portfolio_url, availability_status, availability_hours_per_week,
|
||||
} = req.body;
|
||||
|
||||
const { data: profile, error: upsertError } = await adminSupabase
|
||||
.from("nexus_creator_profiles")
|
||||
.upsert({
|
||||
user_id: user.id,
|
||||
headline: headline || null,
|
||||
bio: bio || null,
|
||||
profile_image_url: profile_image_url || null,
|
||||
skills: Array.isArray(skills) ? skills : [],
|
||||
experience_level: experience_level || "intermediate",
|
||||
hourly_rate: hourly_rate || null,
|
||||
portfolio_url: portfolio_url || null,
|
||||
availability_status: availability_status || "available",
|
||||
availability_hours_per_week: availability_hours_per_week || null,
|
||||
}, { onConflict: "user_id" })
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (upsertError) {
|
||||
return res.status(500).json({ error: upsertError.message });
|
||||
}
|
||||
|
||||
return res.status(200).json(profile);
|
||||
} catch (error: any) {
|
||||
return res.status(500).json({ error: error?.message || "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/nexus/creator/applications
|
||||
app.get("/api/nexus/creator/applications", async (req, res) => {
|
||||
const user = await getUserFromToken(req);
|
||||
if (!user) return res.status(401).json({ error: "Unauthorized" });
|
||||
|
||||
try {
|
||||
const status = req.query.status as string | undefined;
|
||||
const limit = parseInt((req.query.limit as string) || "50", 10);
|
||||
const offset = parseInt((req.query.offset as string) || "0", 10);
|
||||
|
||||
let query = adminSupabase
|
||||
.from("nexus_applications")
|
||||
.select(`
|
||||
*,
|
||||
opportunity:nexus_opportunities(
|
||||
id, title, description, category, budget_type, budget_min, budget_max,
|
||||
timeline_type, status, posted_by, created_at
|
||||
)
|
||||
`)
|
||||
.eq("creator_id", user.id)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (status) {
|
||||
query = query.eq("status", status);
|
||||
}
|
||||
|
||||
const { data: applications, error: appError } = await query.range(offset, offset + limit - 1);
|
||||
|
||||
if (appError) {
|
||||
return res.status(500).json({ error: appError.message });
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
applications: applications || [],
|
||||
total: applications?.length || 0,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return res.status(500).json({ error: error?.message || "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/nexus/creator/contracts
|
||||
app.get("/api/nexus/creator/contracts", async (req, res) => {
|
||||
const user = await getUserFromToken(req);
|
||||
if (!user) return res.status(401).json({ error: "Unauthorized" });
|
||||
|
||||
try {
|
||||
const status = req.query.status as string | undefined;
|
||||
const limit = parseInt((req.query.limit as string) || "50", 10);
|
||||
const offset = parseInt((req.query.offset as string) || "0", 10);
|
||||
|
||||
let query = adminSupabase
|
||||
.from("nexus_contracts")
|
||||
.select(`
|
||||
*,
|
||||
client:user_profiles(id, full_name, avatar_url),
|
||||
milestones:nexus_milestones(*),
|
||||
payments:nexus_payments(*)
|
||||
`)
|
||||
.eq("creator_id", user.id)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (status) {
|
||||
query = query.eq("status", status);
|
||||
}
|
||||
|
||||
const { data: contracts, error: contractsError } = await query.range(offset, offset + limit - 1);
|
||||
|
||||
if (contractsError) {
|
||||
return res.status(500).json({ error: contractsError.message });
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
contracts: contracts || [],
|
||||
total: contracts?.length || 0,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return res.status(500).json({ error: error?.message || "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/nexus/creator/payouts
|
||||
app.get("/api/nexus/creator/payouts", async (req, res) => {
|
||||
const user = await getUserFromToken(req);
|
||||
if (!user) return res.status(401).json({ error: "Unauthorized" });
|
||||
|
||||
try {
|
||||
const status = req.query.status as string | undefined;
|
||||
const limit = parseInt((req.query.limit as string) || "50", 10);
|
||||
const offset = parseInt((req.query.offset as string) || "0", 10);
|
||||
|
||||
let query = adminSupabase
|
||||
.from("nexus_payments")
|
||||
.select(`
|
||||
*,
|
||||
contract:nexus_contracts(id, title, total_amount, status, client_id, created_at),
|
||||
milestone:nexus_milestones(id, description, amount, status)
|
||||
`)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (status) {
|
||||
query = query.eq("payment_status", status);
|
||||
}
|
||||
|
||||
const { data: payments, error: paymentsError } = await query.range(offset, offset + limit - 1);
|
||||
|
||||
if (paymentsError) {
|
||||
return res.status(500).json({ error: paymentsError.message });
|
||||
}
|
||||
|
||||
// Calculate summary stats
|
||||
const { data: contracts } = await adminSupabase
|
||||
.from("nexus_contracts")
|
||||
.select("total_amount, creator_payout_amount, status")
|
||||
.eq("creator_id", user.id);
|
||||
|
||||
const totalEarnings = (contracts || []).reduce((sum: number, c: any) => sum + (c.creator_payout_amount || 0), 0);
|
||||
const completedContracts = (contracts || []).filter((c: any) => c.status === "completed").length;
|
||||
const pendingPayouts = (payments || [])
|
||||
.filter((p: any) => p.payment_status === "pending")
|
||||
.reduce((sum: number, p: any) => sum + (p.creator_payout || 0), 0);
|
||||
|
||||
return res.status(200).json({
|
||||
payments: payments || [],
|
||||
summary: {
|
||||
total_earnings: totalEarnings,
|
||||
pending_payouts: pendingPayouts,
|
||||
completed_contracts: completedContracts,
|
||||
},
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return res.status(500).json({ error: error?.message || "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET/POST /api/nexus/client/opportunities
|
||||
app.get("/api/nexus/client/opportunities", async (req, res) => {
|
||||
const user = await getUserFromToken(req);
|
||||
if (!user) return res.status(401).json({ error: "Unauthorized" });
|
||||
|
||||
try {
|
||||
const status = req.query.status as string | undefined;
|
||||
const limit = parseInt((req.query.limit as string) || "50", 10);
|
||||
const offset = parseInt((req.query.offset as string) || "0", 10);
|
||||
|
||||
let query = adminSupabase
|
||||
.from("nexus_opportunities")
|
||||
.select(`
|
||||
*,
|
||||
applications:nexus_applications(id, status, creator_id)
|
||||
`)
|
||||
.eq("posted_by", user.id)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (status) {
|
||||
query = query.eq("status", status);
|
||||
}
|
||||
|
||||
const { data: opportunities, error: oppError } = await query.range(offset, offset + limit - 1);
|
||||
|
||||
if (oppError) {
|
||||
return res.status(500).json({ error: oppError.message });
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
opportunities: opportunities || [],
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return res.status(500).json({ error: error?.message || "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/nexus/client/opportunities", async (req, res) => {
|
||||
const user = await getUserFromToken(req);
|
||||
if (!user) return res.status(401).json({ error: "Unauthorized" });
|
||||
|
||||
try {
|
||||
const {
|
||||
title, description, category, required_skills, budget_type,
|
||||
budget_min, budget_max, timeline_type, duration_weeks,
|
||||
location_requirement, required_experience, company_name,
|
||||
} = req.body;
|
||||
|
||||
if (!title || !description || !category || !budget_type) {
|
||||
return res.status(400).json({
|
||||
error: "Missing required fields: title, description, category, budget_type",
|
||||
});
|
||||
}
|
||||
|
||||
const { data: opportunity, error: createError } = await adminSupabase
|
||||
.from("nexus_opportunities")
|
||||
.insert({
|
||||
posted_by: user.id,
|
||||
title,
|
||||
description,
|
||||
category,
|
||||
required_skills: Array.isArray(required_skills) ? required_skills : [],
|
||||
budget_type,
|
||||
budget_min: budget_min || null,
|
||||
budget_max: budget_max || null,
|
||||
timeline_type: timeline_type || "flexible",
|
||||
duration_weeks: duration_weeks || null,
|
||||
location_requirement: location_requirement || "remote",
|
||||
required_experience: required_experience || "any",
|
||||
company_name: company_name || null,
|
||||
status: "open",
|
||||
published_at: new Date().toISOString(),
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (createError) {
|
||||
return res.status(500).json({ error: createError.message });
|
||||
}
|
||||
|
||||
return res.status(201).json(opportunity);
|
||||
} catch (error: any) {
|
||||
return res.status(500).json({ error: error?.message || "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/nexus/client/applicants
|
||||
app.get("/api/nexus/client/applicants", async (req, res) => {
|
||||
const user = await getUserFromToken(req);
|
||||
if (!user) return res.status(401).json({ error: "Unauthorized" });
|
||||
|
||||
try {
|
||||
const opportunityId = req.query.opportunity_id as string | undefined;
|
||||
const status = req.query.status as string | undefined;
|
||||
const limit = parseInt((req.query.limit as string) || "50", 10);
|
||||
const offset = parseInt((req.query.offset as string) || "0", 10);
|
||||
|
||||
// If no opportunity_id, return all applicants across all user's opportunities
|
||||
if (!opportunityId) {
|
||||
// Get all opportunities for this user
|
||||
const { data: userOpps } = await adminSupabase
|
||||
.from("nexus_opportunities")
|
||||
.select("id")
|
||||
.eq("posted_by", user.id);
|
||||
|
||||
if (!userOpps || userOpps.length === 0) {
|
||||
return res.status(200).json({
|
||||
applicants: [],
|
||||
limit,
|
||||
offset,
|
||||
total: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const oppIds = userOpps.map((o: any) => o.id);
|
||||
|
||||
let query = adminSupabase
|
||||
.from("nexus_applications")
|
||||
.select(`
|
||||
*,
|
||||
creator:user_profiles(id, full_name, avatar_url, email),
|
||||
creator_profile:nexus_creator_profiles(skills, experience_level, hourly_rate, rating, review_count),
|
||||
opportunity:nexus_opportunities(id, title)
|
||||
`)
|
||||
.in("opportunity_id", oppIds)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (status) {
|
||||
query = query.eq("status", status);
|
||||
}
|
||||
|
||||
const { data: applications, error: appError } = await query.range(offset, offset + limit - 1);
|
||||
|
||||
if (appError) {
|
||||
return res.status(500).json({ error: appError.message });
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
applicants: applications || [],
|
||||
limit,
|
||||
offset,
|
||||
total: applications?.length || 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Verify client owns this opportunity
|
||||
const { data: opportunity, error: oppError } = await adminSupabase
|
||||
.from("nexus_opportunities")
|
||||
.select("id, posted_by")
|
||||
.eq("id", opportunityId)
|
||||
.single();
|
||||
|
||||
if (oppError || !opportunity) {
|
||||
return res.status(404).json({ error: "Opportunity not found" });
|
||||
}
|
||||
|
||||
if (opportunity.posted_by !== user.id) {
|
||||
return res.status(403).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
let query = adminSupabase
|
||||
.from("nexus_applications")
|
||||
.select(`
|
||||
*,
|
||||
creator:user_profiles(id, full_name, avatar_url, email),
|
||||
creator_profile:nexus_creator_profiles(skills, experience_level, hourly_rate, rating, review_count)
|
||||
`)
|
||||
.eq("opportunity_id", opportunityId)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (status) {
|
||||
query = query.eq("status", status);
|
||||
}
|
||||
|
||||
const { data: applications, error: appError } = await query.range(offset, offset + limit - 1);
|
||||
|
||||
if (appError) {
|
||||
return res.status(500).json({ error: appError.message });
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
applicants: applications || [],
|
||||
limit,
|
||||
offset,
|
||||
total: applications?.length || 0,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return res.status(500).json({ error: error?.message || "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/nexus/client/payment-history
|
||||
app.get("/api/nexus/client/payment-history", async (req, res) => {
|
||||
const user = await getUserFromToken(req);
|
||||
if (!user) return res.status(401).json({ error: "Unauthorized" });
|
||||
|
||||
try {
|
||||
const limit = parseInt((req.query.limit as string) || "50", 10);
|
||||
const offset = parseInt((req.query.offset as string) || "0", 10);
|
||||
|
||||
const { data: payments, error: paymentsError } = await adminSupabase
|
||||
.from("nexus_payments")
|
||||
.select(`
|
||||
*,
|
||||
contract:nexus_contracts(id, title, creator_id, total_amount, status),
|
||||
milestone:nexus_milestones(id, description, amount)
|
||||
`)
|
||||
.order("created_at", { ascending: false })
|
||||
.range(offset, offset + limit - 1);
|
||||
|
||||
if (paymentsError) {
|
||||
return res.status(500).json({ error: paymentsError.message });
|
||||
}
|
||||
|
||||
// Filter to only payments for contracts where user is the client
|
||||
const userPayments = (payments || []).filter((p: any) => {
|
||||
return p.contract?.creator_id !== user.id;
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
payments: userPayments,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return res.status(500).json({ error: error?.message || "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Blog API routes
|
||||
app.get("/api/blog", blogIndexHandler);
|
||||
app.get("/api/blog/:slug", (req: express.Request, res: express.Response) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue