AeThex-OS/server/community-routes.ts
MrPiglr b3c308b2c8 Add functional marketplace modules, bottom nav bar, root terminal, arcade games
- ModuleManager: Central tracking for installed marketplace modules
- DataAnalyzerWidget: Real-time CPU/RAM/Battery/Storage widget (unlocked by Data Analyzer module)
- BottomNavBar: Navigation bar for Projects/Chat/Marketplace/Settings
- RootShell: Real root command execution utility
- TerminalActivity: Full root shell with neofetch, sysinfo, real Linux commands
- Terminal Pro module: Adds aliases (ll, la, h), command history
- ArcadeActivity + SnakeGame: Pixel Arcade module unlocks retro games
- fade_in/fade_out animations for smooth transitions
2026-02-18 22:03:50 -07:00

474 lines
12 KiB
TypeScript

/**
* 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;