import { supabase } from "@/lib/supabase"; import type { Database, UserProfile, Project, Achievement, CommunityPost, } from "./database.types"; // User Profile Services export const userProfileService = { async getProfile(userId: string): Promise { const { data, error } = await supabase .from("user_profiles") .select("*") .eq("id", userId) .single(); if (error && error.code !== "PGRST116") { throw error; } return data; }, async updateProfile( userId: string, updates: Partial, ): Promise { const { data, error } = await supabase .from("user_profiles") .update(updates) .eq("id", userId) .select() .single(); if (error) throw error; return data; }, async createProfile( profile: Omit, ): Promise { const { data, error } = await supabase .from("user_profiles") .insert(profile) .select() .single(); if (error) throw error; return data; }, async addInterests(userId: string, interests: string[]): Promise { const interestRows = interests.map((interest) => ({ user_id: userId, interest, })); const { error } = await supabase .from("user_interests") .insert(interestRows); if (error) throw error; }, async getUserInterests(userId: string): Promise { const { data, error } = await supabase .from("user_interests") .select("interest") .eq("user_id", userId); if (error) throw error; return data.map((item) => item.interest); }, }; // Project Services export const projectService = { async getUserProjects(userId: string): Promise { const { data, error } = await supabase .from("projects") .select("*") .eq("user_id", userId) .order("created_at", { ascending: false }); if (error) throw error; return data; }, async createProject( project: Omit, ): Promise { const { data, error } = await supabase .from("projects") .insert(project) .select() .single(); if (error) throw error; return data; }, async updateProject( projectId: string, updates: Partial, ): Promise { const { data, error } = await supabase .from("projects") .update(updates) .eq("id", projectId) .select() .single(); if (error) throw error; return data; }, async deleteProject(projectId: string): Promise { const { error } = await supabase .from("projects") .delete() .eq("id", projectId); if (error) throw error; }, async getAllProjects(limit = 10): Promise { const { data, error } = await supabase .from("projects") .select( ` *, user_profiles ( username, full_name, avatar_url ) `, ) .eq("status", "completed") .order("created_at", { ascending: false }) .limit(limit); if (error) throw error; return data; }, }; // Achievement Services export const achievementService = { async getAllAchievements(): Promise { const { data, error } = await supabase .from("achievements") .select("*") .order("xp_reward", { ascending: false }); if (error) throw error; return data; }, async getUserAchievements(userId: string): Promise { const { data, error } = await supabase .from("user_achievements") .select( ` earned_at, achievements (*) `, ) .eq("user_id", userId) .order("earned_at", { ascending: false }); if (error) throw error; return data .map((item) => item.achievements) .filter(Boolean) as Achievement[]; }, async awardAchievement(userId: string, achievementId: string): Promise { const { error } = await supabase.from("user_achievements").insert({ user_id: userId, achievement_id: achievementId, }); if (error && error.code !== "23505") { // Ignore duplicate key error throw error; } }, async checkAndAwardAchievements(userId: string): Promise { // Check for various achievement conditions const profile = await userProfileService.getProfile(userId); const projects = await projectService.getUserProjects(userId); if (!profile) return; const achievements = await this.getAllAchievements(); // Welcome achievement if (profile.full_name && profile.user_type) { const welcomeAchievement = achievements.find( (a) => a.name === "Welcome to AeThex", ); if (welcomeAchievement) { await this.awardAchievement(userId, welcomeAchievement.id); } } // First project achievement if (projects.length >= 1) { const firstProjectAchievement = achievements.find( (a) => a.name === "First Project", ); if (firstProjectAchievement) { await this.awardAchievement(userId, firstProjectAchievement.id); } } // Experienced developer achievement const completedProjects = projects.filter((p) => p.status === "completed"); if (completedProjects.length >= 5) { const experiencedAchievement = achievements.find( (a) => a.name === "Experienced Developer", ); if (experiencedAchievement) { await this.awardAchievement(userId, experiencedAchievement.id); } } }, }; // Helper function for timeouts function withTimeout( promise: Promise, ms: number, label: string, ): Promise { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timeout after ${ms}ms`)), ms), ), ]); } // Community Services export const communityService = { async getPosts(limit = 10): Promise { const DEFAULT_TIMEOUT = 8000; // 8 seconds per attempt // 1) Try relational select with embedded author profile (with timeout) try { const { data, error } = await withTimeout( supabase .from("community_posts") .select( ` *, user_profiles ( username, full_name, avatar_url ) `, ) .eq("is_published", true) .order("created_at", { ascending: false }) .limit(limit), DEFAULT_TIMEOUT, "Relational select", ); if (!error) { return (Array.isArray(data) ? data : []) as CommunityPost[]; } console.warn( "Supabase getPosts relational select failed:", (error as any)?.message || error, ); } catch (e) { console.warn( "Supabase getPosts relational select threw:", (e as any)?.message || e, ); } // 2) Fallback to simple posts select, then hydrate author profiles manually (with timeout) try { const { data: posts, error: postsErr } = await withTimeout( supabase .from("community_posts") .select("*") .eq("is_published", true) .order("created_at", { ascending: false }) .limit(limit), DEFAULT_TIMEOUT, "Simple posts select", ); if (!postsErr && Array.isArray(posts) && posts.length) { const authorIds = Array.from( new Set(posts.map((p: any) => p.author_id).filter(Boolean)), ); let profilesById: Record = {}; if (authorIds.length) { try { const { data: profiles, error: profErr } = await withTimeout( supabase .from("user_profiles") .select("id, username, full_name, avatar_url") .in("id", authorIds), DEFAULT_TIMEOUT, "Profile hydration", ); if (!profErr && Array.isArray(profiles)) { profilesById = Object.fromEntries( profiles.map((u: any) => [ u.id, { username: u.username, full_name: u.full_name, avatar_url: u.avatar_url, }, ]), ); } } catch (profError) { console.warn( "Profile hydration timeout/error:", (profError as any)?.message || profError, ); } } return posts.map((p: any) => ({ ...p, user_profiles: profilesById[p.author_id] || null, })) as CommunityPost[]; } if (postsErr) console.warn( "Supabase getPosts simple select failed:", (postsErr as any)?.message || postsErr, ); } catch (e2) { console.warn( "Supabase getPosts simple select threw:", (e2 as any)?.message || e2, ); } // 3) Final fallback to API if available (with timeout) try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT); const resp = await fetch( `/api/posts?limit=${encodeURIComponent(String(limit))}`, { signal: controller.signal }, ); clearTimeout(timeoutId); if (resp.ok) { const ct = resp.headers.get("content-type") || ""; if (ct.includes("application/json") || ct.includes("json")) { const payload = await resp.json(); return (Array.isArray(payload) ? payload : []) as CommunityPost[]; } else { const text = await resp.text(); console.warn( "API fallback returned non-JSON content-type:", ct, text.slice(0, 120), ); } } else { console.warn( "API fallback /api/posts not ok:", resp.status, resp.statusText, ); } } catch (apiErr) { console.error( "API fallback for getPosts failed:", (apiErr as any)?.message || apiErr, ); } // Return actual empty array (no demo/mocks) return [] as CommunityPost[]; }, async createPost( post: Omit< CommunityPost, "id" | "created_at" | "updated_at" | "likes_count" | "comments_count" >, ): Promise { try { const resp = await fetch(`/api/posts`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(post), }); if (resp.ok) { return (await resp.json()) as CommunityPost; } if (resp.status >= 400) { const payload = await resp.json().catch(() => ({})); throw new Error(payload?.error || `API responded with ${resp.status}`); } } catch (error) { console.warn("Falling back to Supabase insert for post:", error); } const { data, error } = await supabase .from("community_posts") .insert(post) .select() .single(); if (error || !data) { throw new Error(error?.message || "Unable to publish post"); } return data as CommunityPost; }, async getUserPosts(userId: string): Promise { const { data, error } = await supabase .from("community_posts") .select("*") .eq("author_id", userId) .order("created_at", { ascending: false }); if (error) { throw error; } return (Array.isArray(data) ? data : []) as CommunityPost[]; }, async likePost(postId: string, userId: string): Promise { try { const resp = await fetch( `/api/community/posts/${encodeURIComponent(postId)}/like`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: userId }), }, ); if (resp.ok) { const json = await resp.json(); return typeof json?.likes === "number" ? json.likes : null; } } catch {} return null; }, async unlikePost(postId: string, userId: string): Promise { try { const resp = await fetch( `/api/community/posts/${encodeURIComponent(postId)}/unlike`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: userId }), }, ); if (resp.ok) { const json = await resp.json(); return typeof json?.likes === "number" ? json.likes : null; } } catch {} return null; }, async listComments(postId: string): Promise { try { const resp = await fetch( `/api/community/posts/${encodeURIComponent(postId)}/comments`, ); if (!resp.ok) return []; return await resp.json(); } catch { return []; } }, async addComment( postId: string, userId: string, content: string, ): Promise { const resp = await fetch( `/api/community/posts/${encodeURIComponent(postId)}/comments`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: userId, content }), }, ); if (!resp.ok) return null; return await resp.json(); }, }; // Notification Services export const notificationService = { async getUserNotifications(userId: string): Promise { const { data, error } = await supabase .from("notifications") .select("*") .eq("user_id", userId) .order("created_at", { ascending: false }) .limit(10); if (error) throw error; return data; }, async markAsRead(notificationId: string): Promise { const { error } = await supabase .from("notifications") .update({ read: true }) .eq("id", notificationId); if (error) throw error; }, async createNotification( userId: string, title: string, message?: string, type = "info", ): Promise { const { error } = await supabase.from("notifications").insert({ user_id: userId, title, message, type, }); if (error) throw error; }, }; // Real-time subscriptions export const realtimeService = { subscribeToUserNotifications( userId: string, callback: (notification: any) => void, ) { const client: any = supabase as any; if (!client || typeof client.channel !== "function") { return { unsubscribe: () => {} } as any; } return client .channel(`notifications:${userId}`) .on( "postgres_changes", { event: "INSERT", schema: "public", table: "notifications", filter: `user_id=eq.${userId}`, }, callback, ) .subscribe(); }, subscribeToCommunityPosts(callback: (post: any) => void) { const client: any = supabase as any; if (!client || typeof client.channel !== "function") { return { unsubscribe: () => {} } as any; } return client .channel("community_posts") .on( "postgres_changes", { event: "INSERT", schema: "public", table: "community_posts", filter: "is_published=eq.true", }, callback, ) .subscribe(); }, };