diff --git a/api/community/collaboration-posts.ts b/api/community/collaboration-posts.ts new file mode 100644 index 00000000..9b56b381 --- /dev/null +++ b/api/community/collaboration-posts.ts @@ -0,0 +1,395 @@ +export const config = { + runtime: "nodejs", +}; + +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = process.env.VITE_SUPABASE_URL; +const supabaseServiceRole = process.env.SUPABASE_SERVICE_ROLE; + +if (!supabaseUrl || !supabaseServiceRole) { + throw new Error("Missing Supabase configuration"); +} + +const supabase = createClient(supabaseUrl, supabaseServiceRole); + +const VALID_ARMS = ["labs", "gameforge", "corp", "foundation", "devlink", "nexus", "staff"]; + +export default async function handler(req: any, res: any) { + if (req.method === "POST") { + try { + const { title, content, arm_affiliation, creator_id, collaborator_ids, tags, category } = + req.body; + + // Validate required fields + if (!title || !content || !arm_affiliation || !creator_id) { + return res.status(400).json({ + error: "Missing required fields: title, content, arm_affiliation, creator_id", + }); + } + + // Validate arm_affiliation + if (!VALID_ARMS.includes(arm_affiliation)) { + return res.status(400).json({ + error: `Invalid arm_affiliation. Must be one of: ${VALID_ARMS.join(", ")}`, + }); + } + + // Validate content length + if (title.trim().length === 0 || title.length > 500) { + return res + .status(400) + .json({ error: "Title must be between 1 and 500 characters" }); + } + + if (content.trim().length === 0 || content.length > 5000) { + return res + .status(400) + .json({ error: "Content must be between 1 and 5000 characters" }); + } + + // Validate creator exists + const { data: creator, error: creatorError } = await supabase + .from("user_profiles") + .select("id") + .eq("id", creator_id) + .single(); + + if (creatorError || !creator) { + return res.status(401).json({ error: "Creator not found" }); + } + + // Insert the collaboration post + const { data: postData, error: postError } = await supabase + .from("collaboration_posts") + .insert({ + title: title.trim(), + content: content.trim(), + arm_affiliation, + created_by: creator_id, + tags: tags || [], + category: category || null, + is_published: true, + likes_count: 0, + comments_count: 0, + }) + .select("id"); + + if (postError) { + console.error("[Collaboration Posts API] Insert error:", postError); + return res.status(500).json({ error: postError.message }); + } + + const postId = postData?.[0]?.id; + if (!postId) { + return res.status(500).json({ error: "Failed to create post" }); + } + + // Add creator as author + await supabase.from("collaboration_posts_authors").insert({ + collaboration_post_id: postId, + user_id: creator_id, + role: "creator", + }); + + // Add collaborators if provided + if (collaborator_ids && Array.isArray(collaborator_ids) && collaborator_ids.length > 0) { + const collaboratorInserts = collaborator_ids + .filter((id: string) => id !== creator_id) // Exclude creator from collaborators + .map((id: string) => ({ + collaboration_post_id: postId, + user_id: id, + role: "contributor", + })); + + if (collaboratorInserts.length > 0) { + await supabase.from("collaboration_posts_authors").insert(collaboratorInserts); + } + } + + // Fetch the complete post with authors + const { data, error: fetchError } = await supabase + .from("collaboration_posts") + .select( + ` + id, + title, + content, + arm_affiliation, + created_by, + created_at, + updated_at, + is_published, + likes_count, + comments_count, + tags, + category, + user_profiles!collaboration_posts_created_by_fkey ( + id, + username, + full_name, + avatar_url + ), + collaboration_posts_authors ( + user_id, + role, + user_profiles!collaboration_posts_authors_user_id_fkey ( + id, + username, + full_name, + avatar_url + ) + ) + ` + ) + .eq("id", postId) + .single(); + + if (fetchError) { + console.error("[Collaboration Posts API] Fetch error:", fetchError); + return res.status(500).json({ error: fetchError.message }); + } + + return res.status(201).json({ + post: data, + }); + } catch (error: any) { + console.error("[Collaboration Posts API POST] Unexpected error:", error); + return res + .status(500) + .json({ error: error.message || "Internal server error" }); + } + } + + if (req.method === "GET") { + try { + const { arm_filter, limit = 20, offset = 0 } = req.query; + + // Build query + let query = supabase + .from("collaboration_posts") + .select( + ` + id, + title, + content, + arm_affiliation, + created_by, + created_at, + updated_at, + is_published, + likes_count, + comments_count, + tags, + category, + user_profiles!collaboration_posts_created_by_fkey ( + id, + username, + full_name, + avatar_url + ), + collaboration_posts_authors ( + user_id, + role, + user_profiles!collaboration_posts_authors_user_id_fkey ( + id, + username, + full_name, + avatar_url + ) + ) + `, + { count: "exact" } + ) + .eq("is_published", true) + .order("created_at", { ascending: false }); + + // Apply arm filter if specified + if (arm_filter) { + const armIds = Array.isArray(arm_filter) ? arm_filter : [arm_filter]; + query = query.in("arm_affiliation", armIds); + } + + // Apply pagination + query = query.range(parseInt(offset), parseInt(offset) + parseInt(limit) - 1); + + const { data, error, count } = await query; + + if (error) { + console.error("[Collaboration Posts API] Query error:", error); + return res.status(500).json({ error: error.message }); + } + + return res.status(200).json({ + posts: data || [], + total: count || 0, + limit: parseInt(limit), + offset: parseInt(offset), + }); + } catch (error: any) { + console.error("[Collaboration Posts API GET] Unexpected error:", error); + return res + .status(500) + .json({ error: error.message || "Internal server error" }); + } + } + + if (req.method === "PUT") { + try { + const { id, title, content, arm_affiliation, category, tags, user_id } = req.body; + + if (!id || !user_id) { + return res.status(400).json({ error: "Missing id or user_id" }); + } + + // Get the post to verify ownership + const { data: post, error: fetchError } = await supabase + .from("collaboration_posts") + .select("created_by") + .eq("id", id) + .single(); + + if (fetchError) { + console.error("[Collaboration Posts API] Fetch error:", fetchError); + return res.status(404).json({ error: "Post not found" }); + } + + if (post.created_by !== user_id) { + return res + .status(403) + .json({ error: "You can only edit posts you created" }); + } + + // Validate updates + if (title && (title.trim().length === 0 || title.length > 500)) { + return res + .status(400) + .json({ error: "Title must be between 1 and 500 characters" }); + } + + if (content && (content.trim().length === 0 || content.length > 5000)) { + return res + .status(400) + .json({ error: "Content must be between 1 and 5000 characters" }); + } + + if (arm_affiliation && !VALID_ARMS.includes(arm_affiliation)) { + return res + .status(400) + .json({ + error: `Invalid arm_affiliation. Must be one of: ${VALID_ARMS.join(", ")}`, + }); + } + + // Build update object + const updateData: any = {}; + if (title) updateData.title = title.trim(); + if (content) updateData.content = content.trim(); + if (arm_affiliation) updateData.arm_affiliation = arm_affiliation; + if (category !== undefined) updateData.category = category; + if (tags) updateData.tags = tags; + + const { data, error } = await supabase + .from("collaboration_posts") + .update(updateData) + .eq("id", id) + .select( + ` + id, + title, + content, + arm_affiliation, + created_by, + created_at, + updated_at, + is_published, + likes_count, + comments_count, + tags, + category, + user_profiles!collaboration_posts_created_by_fkey ( + id, + username, + full_name, + avatar_url + ), + collaboration_posts_authors ( + user_id, + role, + user_profiles!collaboration_posts_authors_user_id_fkey ( + id, + username, + full_name, + avatar_url + ) + ) + ` + ); + + if (error) { + console.error("[Collaboration Posts API] Update error:", error); + return res.status(500).json({ error: error.message }); + } + + return res.status(200).json({ + post: data?.[0], + }); + } catch (error: any) { + console.error("[Collaboration Posts API PUT] Unexpected error:", error); + return res + .status(500) + .json({ error: error.message || "Internal server error" }); + } + } + + if (req.method === "DELETE") { + try { + const { id, user_id } = req.body; + + if (!id || !user_id) { + return res.status(400).json({ error: "Missing id or user_id" }); + } + + // Get the post to verify ownership + const { data: post, error: fetchError } = await supabase + .from("collaboration_posts") + .select("created_by") + .eq("id", id) + .single(); + + if (fetchError) { + console.error("[Collaboration Posts API] Fetch error:", fetchError); + return res.status(404).json({ error: "Post not found" }); + } + + if (post.created_by !== user_id) { + return res + .status(403) + .json({ error: "You can only delete posts you created" }); + } + + // Delete the post (cascade will delete likes, comments, and authors) + const { error } = await supabase + .from("collaboration_posts") + .delete() + .eq("id", id); + + if (error) { + console.error("[Collaboration Posts API] Delete error:", error); + return res.status(500).json({ error: error.message }); + } + + return res.status(200).json({ + success: true, + message: "Post deleted successfully", + }); + } catch (error: any) { + console.error("[Collaboration Posts API DELETE] Unexpected error:", error); + return res + .status(500) + .json({ error: error.message || "Internal server error" }); + } + } + + return res.status(405).json({ error: "Method not allowed" }); +}