From 3c499df528c6d4ebcebc504573ec131c872aa3e0 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 14 Oct 2025 02:57:20 +0000 Subject: [PATCH] Add Supabase demo feed seeding endpoint cgen-83da51189f5f451aadffb55afb7bcfc2 --- api/community/seed-demo.ts | 298 +++++++++++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 api/community/seed-demo.ts diff --git a/api/community/seed-demo.ts b/api/community/seed-demo.ts new file mode 100644 index 00000000..9cce3727 --- /dev/null +++ b/api/community/seed-demo.ts @@ -0,0 +1,298 @@ +import type { VercelRequest, VercelResponse } from "@vercel/node"; +import { getAdminClient } from "../_supabase"; + +interface DemoUser { + email: string; + fullName: string; + username: string; + avatarUrl: string; + bio: string; + location: string; + experienceLevel: "beginner" | "intermediate" | "advanced" | "expert"; +} + +interface DemoPost { + id: string; + authorEmail: string; + title: string; + content: { + text: string; + mediaUrl: string | null; + mediaType: "video" | "image" | "none"; + }; + category: string; + tags: string[]; + likes: number; + comments: number; + hoursAgo: number; +} + +const DEMO_USERS: DemoUser[] = [ + { + email: "updates@aethex.dev", + fullName: "AeThex Updates", + username: "aethex", + avatarUrl: + "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3979ec9a8a28471d900a80e94e2c45fe?format=png&width=256", + bio: "Official AeThex OS updates, roadmap signals, and community spotlights.", + location: "AeThex HQ", + experienceLevel: "expert", + }, + { + email: "labs@aethex.dev", + fullName: "AeThex Labs", + username: "aethexlabs", + avatarUrl: "https://i.pravatar.cc/150?img=8", + bio: "Experimental builds, prototypes, and R&D drops from the Labs team.", + location: "Global", + experienceLevel: "advanced", + }, + { + email: "mrpiglr+demo@aethex.dev", + fullName: "Mr Piglr", + username: "mrpiglr", + avatarUrl: "https://i.pravatar.cc/150?img=11", + bio: "Testing the admin pipeline and validating AeThex OS features.", + location: "AeThex Command Center", + experienceLevel: "expert", + }, +]; + +const DEMO_POSTS: DemoPost[] = [ + { + id: "f4dd3f65-462c-4b54-8d75-1830d5c6a001", + authorEmail: "labs@aethex.dev", + title: "Lab Drop: Procedural City Showcase", + content: { + text: "Fresh from the render farm — a procedural city loop rendered directly in AeThex Forge. Toggle sound for spatial audio cues!", + mediaUrl: "https://storage.googleapis.com/coverr-main/mp4/Mt_Baker.mp4", + mediaType: "video", + }, + category: "video", + tags: ["labs", "procedural", "video"], + likes: 128, + comments: 26, + hoursAgo: 3, + }, + { + id: "f4dd3f65-462c-4b54-8d75-1830d5c6a002", + authorEmail: "updates@aethex.dev", + title: "AeThex OS 0.9.4 release thread", + content: { + text: "Release 0.9.4 is live with the refreshed dashboard, passport syncing, and the new admin panel. Patch notes compiled in the docs portal!", + mediaUrl: + "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F74274d2890c845a2ade125b075444ef2?format=webp&width=1600", + mediaType: "image", + }, + category: "image", + tags: ["release", "aethex", "dashboard"], + likes: 94, + comments: 14, + hoursAgo: 6, + }, + { + id: "f4dd3f65-462c-4b54-8d75-1830d5c6a003", + authorEmail: "mrpiglr+demo@aethex.dev", + title: "Admin panel QA checklist", + content: { + text: "Running through the QA list for the admin suite. Permissions, member modals, and achievement tooling all check out. Logs look clean!", + mediaUrl: null, + mediaType: "none", + }, + category: "text", + tags: ["qa", "admin", "update"], + likes: 37, + comments: 5, + hoursAgo: 9, + }, + { + id: "f4dd3f65-462c-4b54-8d75-1830d5c6a004", + authorEmail: "updates@aethex.dev", + title: "Community shout-out", + content: { + text: "Huge shout-out to the AeThex community members posting their prototypes today. The energy in the feed is 🔥", + mediaUrl: + "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Ffef86bb69cf147a1a8614048d2d70502?format=webp&width=1600", + mediaType: "image", + }, + category: "image", + tags: ["community", "highlight"], + likes: 76, + comments: 9, + hoursAgo: 12, + }, +]; + +const FOLLOW_PAIRS: Array<{ followerEmail: string; followingEmail: string }> = [ + { followerEmail: "mrpiglr+demo@aethex.dev", followingEmail: "updates@aethex.dev" }, + { followerEmail: "mrpiglr+demo@aethex.dev", followingEmail: "labs@aethex.dev" }, + { followerEmail: "labs@aethex.dev", followingEmail: "updates@aethex.dev" }, +]; + +export default async function handler(req: VercelRequest, res: VercelResponse) { + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + try { + const admin = getAdminClient(); + const userMap = new Map(); + const seededUsers: any[] = []; + + for (const demoUser of DEMO_USERS) { + const { data: searchResult, error: searchError } = await admin.auth.admin + .listUsers({ email: demoUser.email }); + if (searchError) throw searchError; + + let authUser = searchResult.users?.[0]; + if (!authUser) { + const tempPassword = `Demo${Math.random().toString(36).slice(2, 10)}!9`; + const { data: createdUser, error: createError } = + await admin.auth.admin.createUser({ + email: demoUser.email, + password: tempPassword, + email_confirm: true, + user_metadata: { full_name: demoUser.fullName }, + }); + if (createError) throw createError; + authUser = createdUser.user ?? undefined; + } + + if (!authUser) continue; + + userMap.set(demoUser.email, authUser.id); + + const { data: existingProfile, error: profileLookupError } = await admin + .from("user_profiles") + .select("*") + .eq("id", authUser.id) + .maybeSingle(); + if (profileLookupError && profileLookupError.code !== "PGRST116") { + throw profileLookupError; + } + + if (!existingProfile) { + const profilePayload = { + id: authUser.id, + full_name: demoUser.fullName, + username: demoUser.username, + avatar_url: demoUser.avatarUrl, + bio: demoUser.bio, + location: demoUser.location, + user_type: "community_member" as const, + experience_level: demoUser.experienceLevel, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }; + const { data: insertedProfile, error: insertProfileError } = await admin + .from("user_profiles") + .insert(profilePayload) + .select() + .single(); + if (insertProfileError) throw insertProfileError; + seededUsers.push(insertProfileError ? null : insertedProfile); + } else { + seededUsers.push(existingProfile); + } + } + + const seededPosts: any[] = []; + const now = Date.now(); + + for (const post of DEMO_POSTS) { + const authorId = userMap.get(post.authorEmail); + if (!authorId) continue; + + const { data: existingPost, error: postLookupError } = await admin + .from("community_posts") + .select( + ` + *, + user_profiles ( + username, + full_name, + avatar_url + ) + `, + ) + .eq("id", post.id) + .maybeSingle(); + if (postLookupError && postLookupError.code !== "PGRST116") { + throw postLookupError; + } + + if (existingPost) { + seededPosts.push(existingPost); + continue; + } + + const createdAt = new Date(now - post.hoursAgo * 3600 * 1000).toISOString(); + const { data: insertedPost, error: insertPostError } = await admin + .from("community_posts") + .insert({ + id: post.id, + author_id: authorId, + title: post.title, + content: JSON.stringify(post.content), + category: post.category, + tags: post.tags, + likes_count: post.likes, + comments_count: post.comments, + is_published: true, + created_at: createdAt, + updated_at: createdAt, + }) + .select( + ` + *, + user_profiles ( + username, + full_name, + avatar_url + ) + `, + ) + .single(); + if (insertPostError) throw insertPostError; + seededPosts.push(insertPostError ? null : insertedPost); + } + + const followRows = FOLLOW_PAIRS.flatMap(({ followerEmail, followingEmail }) => { + const followerId = userMap.get(followerEmail); + const followingId = userMap.get(followingEmail); + if (!followerId || !followingId) return []; + return [ + { + follower_id: followerId, + following_id: followingId, + created_at: new Date().toISOString(), + }, + ]; + }); + + if (followRows.length) { + const { error: followError } = await admin + .from("user_follows") + .upsert(followRows, { + onConflict: "follower_id,following_id" as any, + ignoreDuplicates: true as any, + }); + if (followError) throw followError; + } + + const sanitizedUsers = seededUsers.filter(Boolean); + const sanitizedPosts = seededPosts.filter(Boolean); + + return res.status(200).json({ + ok: true, + usersSeeded: sanitizedUsers.length, + postsSeeded: sanitizedPosts.length, + posts: sanitizedPosts, + }); + } catch (error: any) { + console.error("Demo feed seeding failed", error); + return res.status(500).json({ + error: error?.message || "Unable to seed demo feed", + }); + } +}