/** * Community Routes * API endpoints for events, opportunities, and community posts * Uses Supabase for data storage */ import express, { Request, Response } from 'express'; import { supabase } from './supabase.js'; const router = express.Router(); // Auth middleware helper function getUserId(req: Request): string | null { return (req.session as any)?.userId || null; } // ==================== EVENTS ROUTES ==================== // GET /api/events - List all events router.get('/events', async (req: Request, res: Response) => { try { const { category, featured, upcoming, limit = 20, offset = 0 } = req.query; let query = supabase .from('community_events') .select('*', { count: 'exact' }); if (category && category !== 'all') { query = query.eq('category', category as string); } if (featured === 'true') { query = query.eq('featured', true); } if (upcoming === 'true') { query = query.gte('date', new Date().toISOString()); } const { data, error, count } = await query .order('date', { ascending: true }) .range(Number(offset), Number(offset) + Number(limit) - 1); if (error) throw error; res.json({ events: data || [], total: count || 0 }); } catch (error: any) { console.error('[Events] List error:', error); res.status(500).json({ error: error.message }); } }); // GET /api/events/:id - Get single event router.get('/events/:id', async (req: Request, res: Response) => { try { const { id } = req.params; const { data, error } = await supabase .from('community_events') .select('*') .eq('id', id) .single(); if (error) throw error; if (!data) { return res.status(404).json({ error: 'Event not found' }); } res.json(data); } catch (error: any) { console.error('[Events] Get error:', error); res.status(500).json({ error: error.message }); } }); // POST /api/events/:id/register - Register for event router.post('/events/:id/register', async (req: Request, res: Response) => { try { const { id } = req.params; const userId = getUserId(req); if (!userId) { return res.status(401).json({ error: 'Unauthorized' }); } // Check event exists and not full const { data: event, error: eventError } = await supabase .from('community_events') .select('id, capacity, attendees') .eq('id', id) .single(); if (eventError || !event) { return res.status(404).json({ error: 'Event not found' }); } if (event.capacity && event.attendees >= event.capacity) { return res.status(400).json({ error: 'Event is full' }); } // Register user (trigger will update attendees count) const { error: regError } = await supabase .from('community_event_registrations') .insert({ event_id: id, user_id: userId }); if (regError) { if (regError.code === '23505') { return res.status(400).json({ error: 'Already registered for this event' }); } throw regError; } // Get updated event const { data: updatedEvent } = await supabase .from('community_events') .select('*') .eq('id', id) .single(); res.json({ success: true, message: 'Successfully registered for event', event: updatedEvent }); } catch (error: any) { console.error('[Events] Register error:', error); res.status(500).json({ error: error.message }); } }); // POST /api/events - Create new event router.post('/events', async (req: Request, res: Response) => { try { const userId = getUserId(req); if (!userId) { return res.status(401).json({ error: 'Unauthorized' }); } const { title, description, category, date, end_date, location, is_virtual, price, capacity } = req.body; if (!title || !category || !date) { return res.status(400).json({ error: 'title, category, and date are required' }); } const { data, error } = await supabase .from('community_events') .insert({ title, description, category, date, end_date, location, is_virtual: is_virtual || false, price: price || 0, capacity, organizer_id: userId }) .select() .single(); if (error) throw error; res.status(201).json(data); } catch (error: any) { console.error('[Events] Create error:', error); res.status(500).json({ error: error.message }); } }); // ==================== OPPORTUNITIES ROUTES ==================== // GET /api/opportunities - List all job opportunities router.get('/opportunities', async (req: Request, res: Response) => { try { const { arm, type, remote, limit = 20, offset = 0 } = req.query; let query = supabase .from('community_opportunities') .select('*', { count: 'exact' }) .eq('is_active', true); if (arm && arm !== 'all') { query = query.eq('arm', arm as string); } if (type && type !== 'all') { query = query.eq('type', type as string); } if (remote === 'true') { query = query.eq('is_remote', true); } const { data, error, count } = await query .order('created_at', { ascending: false }) .range(Number(offset), Number(offset) + Number(limit) - 1); if (error) throw error; res.json({ opportunities: data || [], total: count || 0 }); } catch (error: any) { console.error('[Opportunities] List error:', error); res.status(500).json({ error: error.message }); } }); // GET /api/opportunities/:id - Get single opportunity router.get('/opportunities/:id', async (req: Request, res: Response) => { try { const { id } = req.params; const { data, error } = await supabase .from('community_opportunities') .select('*') .eq('id', id) .single(); if (error) throw error; if (!data) { return res.status(404).json({ error: 'Opportunity not found' }); } res.json(data); } catch (error: any) { console.error('[Opportunities] Get error:', error); res.status(500).json({ error: error.message }); } }); // POST /api/opportunities/:id/apply - Apply to job router.post('/opportunities/:id/apply', async (req: Request, res: Response) => { try { const { id } = req.params; const userId = getUserId(req); if (!userId) { return res.status(401).json({ error: 'Unauthorized' }); } const { resume_url, cover_letter } = req.body; // Check opportunity exists const { data: opportunity, error: oppError } = await supabase .from('community_opportunities') .select('id, is_active') .eq('id', id) .single(); if (oppError || !opportunity) { return res.status(404).json({ error: 'Opportunity not found' }); } if (!opportunity.is_active) { return res.status(400).json({ error: 'This opportunity is no longer accepting applications' }); } // Submit application const { error: appError } = await supabase .from('community_applications') .insert({ opportunity_id: id, user_id: userId, resume_url, cover_letter }); if (appError) { if (appError.code === '23505') { return res.status(400).json({ error: 'Already applied to this opportunity' }); } throw appError; } // Get updated opportunity const { data: updatedOpp } = await supabase .from('community_opportunities') .select('*') .eq('id', id) .single(); res.json({ success: true, message: 'Application submitted successfully', opportunity: updatedOpp }); } catch (error: any) { console.error('[Opportunities] Apply error:', error); res.status(500).json({ error: error.message }); } }); // POST /api/opportunities - Create new opportunity router.post('/opportunities', async (req: Request, res: Response) => { try { const userId = getUserId(req); if (!userId) { return res.status(401).json({ error: 'Unauthorized' }); } const { title, company, arm, type, location, is_remote, description, requirements, salary_min, salary_max } = req.body; if (!title || !company || !arm || !type) { return res.status(400).json({ error: 'title, company, arm, and type are required' }); } const { data, error } = await supabase .from('community_opportunities') .insert({ title, company, arm, type, location, is_remote: is_remote || false, description, requirements: requirements || [], salary_min, salary_max, posted_by: userId }) .select() .single(); if (error) throw error; res.status(201).json(data); } catch (error: any) { console.error('[Opportunities] Create error:', error); res.status(500).json({ error: error.message }); } }); // ==================== COMMUNITY POSTS ROUTES ==================== // GET /api/posts - List community posts router.get('/posts', async (req: Request, res: Response) => { try { const { arm, limit = 20, offset = 0 } = req.query; let query = supabase .from('community_posts') .select('*', { count: 'exact' }) .eq('is_published', true); if (arm && arm !== 'all') { query = query.eq('arm_affiliation', arm as string); } const { data, error, count } = await query .order('created_at', { ascending: false }) .range(Number(offset), Number(offset) + Number(limit) - 1); if (error) throw error; res.json({ posts: data || [], total: count || 0 }); } catch (error: any) { console.error('[Posts] List error:', error); res.status(500).json({ error: error.message }); } }); // POST /api/posts - Create new post router.post('/posts', async (req: Request, res: Response) => { try { const userId = getUserId(req); if (!userId) { return res.status(401).json({ error: 'Unauthorized' }); } const { title, content, arm_affiliation, tags, category } = req.body; if (!title || !content) { return res.status(400).json({ error: 'title and content are required' }); } const { data, error } = await supabase .from('community_posts') .insert({ title, content, arm_affiliation: arm_affiliation || 'general', tags: tags || [], category, author_id: userId }) .select() .single(); if (error) throw error; res.status(201).json(data); } catch (error: any) { console.error('[Posts] Create error:', error); res.status(500).json({ error: error.message }); } }); // POST /api/posts/:id/like - Like a post router.post('/posts/:id/like', async (req: Request, res: Response) => { try { const { id } = req.params; const userId = getUserId(req); if (!userId) { return res.status(401).json({ error: 'Unauthorized' }); } // Toggle like const { data: existingLike } = await supabase .from('community_post_likes') .select('id') .eq('post_id', id) .eq('user_id', userId) .single(); if (existingLike) { // Unlike await supabase .from('community_post_likes') .delete() .eq('id', existingLike.id); // Decrement likes count await supabase .from('community_posts') .update({ likes_count: supabase.rpc('decrement', { x: 1 }) }) .eq('id', id); res.json({ success: true, liked: false }); } else { // Like await supabase .from('community_post_likes') .insert({ post_id: id, user_id: userId }); // Increment likes count await supabase .from('community_posts') .update({ likes_count: supabase.rpc('increment', { x: 1 }) }) .eq('id', id); res.json({ success: true, liked: true }); } } catch (error: any) { console.error('[Posts] Like error:', error); res.status(500).json({ error: error.message }); } }); // ==================== LEGACY MESSAGES ROUTES ==================== // Note: Real messaging is handled by messaging-routes.ts // These routes provide backward compatibility router.get('/messages', async (req: Request, res: Response) => { res.json({ messages: [], note: 'Use /api/conversations for messaging' }); }); export default router;