diff --git a/client/lib/aethex-database-adapter.ts b/client/lib/aethex-database-adapter.ts index b97fbac5..dddafd62 100644 --- a/client/lib/aethex-database-adapter.ts +++ b/client/lib/aethex-database-adapter.ts @@ -1,9 +1,561 @@ +// Maps existing schema to our application needs + import { supabase } from "./supabase"; import type { Database } from "./database.types"; import { aethexToast } from "./aethex-toast"; import { mockAuth } from "./mock-auth"; -// ... existing content above ... +// Use the existing database user profile type directly +import type { UserProfile } from "./database.types"; + +// Extended type that matches the existing shared database +export interface AethexUserProfile extends UserProfile { + email?: string; + username: string | null; + onboarded?: boolean; + role?: string; + loyalty_points?: number; + banner_url?: string; + social_links?: any; + skills?: string[]; +} + +export interface AethexProject { + id: string; + user_id: string; + title: string; + description?: string; + status: "planning" | "in_progress" | "completed" | "on_hold"; + technologies?: string[]; + github_url?: string; + live_url?: string; + image_url?: string; + start_date?: string; + end_date?: string; + created_at: string; + updated_at: string; +} + +export interface AethexAchievement { + id: string; + name: string; + description: string; + icon?: string; + xp_reward: number; + badge_color?: string; + created_at: string; +} + +export interface AethexUserAchievement { + id: string; + user_id: string; + achievement_id: string; + unlocked_at: string; +} + +function isTableMissing(err: any): boolean { + const msg = String(err?.message || err?.hint || err?.details || ""); + return ( + err?.code === "42P01" || // undefined_table + msg.includes("relation \"") || + msg.includes("does not exist") || + msg.includes("table") + ); +} + +// User Profile Services +export const aethexUserService = { + async getCurrentUser(): Promise { + const { + data: { user }, + } = await supabase.auth.getUser(); + if (!user) return null; + + const { data, error } = await supabase + .from("user_profiles") + .select("*") + .eq("id", user.id) + .single(); + + if (error) { + console.warn("Error fetching user profile, falling back to mock:", error); + const mock = await mockAuth.getUserProfile(user.id as any); + if (mock) { + return { + ...(mock as any), + email: user.email, + } as AethexUserProfile; + } + const created = await mockAuth.updateProfile(user.id as any, { + username: user.email?.split("@")[0] || "user", + email: user.email || "", + role: "member", + onboarded: true, + } as any); + return { + ...(created as any), + email: user.email, + } as AethexUserProfile; + } + + // Map the existing database fields to our interface + return { + ...data, + email: user.email, + username: (data as any).username || user.email?.split("@")[0] || "user", + onboarded: true, + role: "member", + loyalty_points: 0, + } as AethexUserProfile; + }, + + async updateProfile( + userId: string, + updates: Partial, + ): Promise { + const { data, error } = await supabase + .from("user_profiles") + .update(updates) + .eq("id", userId) + .select() + .single(); + + if (error) { + console.warn("Error updating profile, attempting mock fallback:", error); + if (isTableMissing(error)) { + const mock = await mockAuth.updateProfile(userId as any, updates as any); + return mock as unknown as AethexUserProfile; + } + throw error; + } + + return data as AethexUserProfile; + }, + + async createInitialProfile( + userId: string, + profileData: Partial, + ): Promise { + // Only insert fields that exist in the actual database schema + const { data, error } = await supabase + .from("user_profiles") + .insert({ + id: userId, + username: profileData.username || `user_${Date.now()}`, + user_type: (profileData as any).user_type || "community_member", + experience_level: (profileData as any).experience_level || "beginner", + full_name: profileData.full_name, + bio: profileData.bio, + location: profileData.location, + website_url: (profileData as any).website_url, + github_url: profileData.github_url, + twitter_url: profileData.twitter_url, + linkedin_url: profileData.linkedin_url, + level: 1, + total_xp: 0, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }) + .select() + .single(); + + if (error) { + console.warn("Error creating profile, attempting mock fallback:", error); + if (isTableMissing(error)) { + const mock = await mockAuth.updateProfile(userId as any, { + username: profileData.username || `user_${Date.now()}`, + full_name: profileData.full_name, + bio: profileData.bio, + location: profileData.location, + linkedin_url: profileData.linkedin_url as any, + github_url: profileData.github_url as any, + twitter_url: profileData.twitter_url as any, + level: 1, + total_xp: 0, + } as any); + + return { + ...(mock as any), + onboarded: true, + role: "member", + loyalty_points: 0, + } as any; + } + throw error; + } + + return { + ...data, + onboarded: true, + role: "member", + loyalty_points: 0, + } as AethexUserProfile; + }, + + async addUserInterests(userId: string, interests: string[]): Promise { + // First, delete existing interests (ignore failures when table missing) + await supabase + .from("user_interests") + .delete() + .eq("user_id", userId) + .catch(() => undefined); + + // Insert new interests + const interestRows = interests.map((interest) => ({ + user_id: userId, + interest, + })); + + const { error } = await supabase.from("user_interests").insert(interestRows); + + if (error) { + if (isTableMissing(error)) return; + throw error; + } + }, + + async getUserInterests(userId: string): Promise { + const { data, error } = await supabase + .from("user_interests") + .select("interest") + .eq("user_id", userId); + + if (error) { + console.warn("Error fetching interests:", error); + return []; + } + + return (data as any[]).map((item: any) => item.interest); + }, +}; + +// Project Services +export const aethexProjectService = { + async getUserProjects(userId: string): Promise { + const { data, error } = await supabase + .from("projects") + .select("*") + .eq("user_id", userId) + .order("created_at", { ascending: false }); + + if (error) { + console.warn("Error fetching projects:", error); + return []; + } + + return data as AethexProject[]; + }, + + async createProject( + project: Omit, + ): Promise { + const { data, error } = await supabase + .from("projects") + .insert(project) + .select() + .single(); + + if (error) { + console.warn("Error creating project:", error); + throw error; + } + + return data as AethexProject; + }, + + async updateProject( + projectId: string, + updates: Partial, + ): Promise { + const { data, error } = await supabase + .from("projects") + .update(updates) + .eq("id", projectId) + .select() + .single(); + + if (error) { + console.warn("Error updating project:", error); + throw error; + } + + return data as AethexProject; + }, + + async deleteProject(projectId: string): Promise { + const { error } = await supabase.from("projects").delete().eq("id", projectId); + + if (error) { + console.warn("Error deleting project:", error); + return false; + } + + return true; + }, + + async getAllProjects(limit = 10): Promise { + const { data, error } = await supabase + .from("projects") + .select( + ` + *, + profiles!projects_user_id_fkey ( + username, + full_name, + avatar_url + ) + `, + ) + .eq("status", "completed") + .order("created_at", { ascending: false }) + .limit(limit); + + if (error) { + console.warn("Error fetching all projects:", error); + return []; + } + + return data as AethexProject[]; + }, +}; + +// Achievement Services (maps to existing achievements table) +export const aethexAchievementService = { + async getAllAchievements(): Promise { + const { data, error } = await supabase + .from("achievements") + .select("*") + .order("xp_reward", { ascending: false }); + + if (error) { + console.warn("Error fetching achievements:", error); + return []; + } + + return data as AethexAchievement[]; + }, + + async getUserAchievements(userId: string): Promise { + const { data, error } = await supabase + .from("user_achievements") + .select( + ` + achievement_id, + achievements (*) + `, + ) + .eq("user_id", userId); + + if (error) { + console.warn("Error fetching user achievements:", error); + return []; + } + + return (data as any[]) + .map((item) => (item as any).achievements) + .filter(Boolean) as AethexAchievement[]; + }, + + 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 + if (isTableMissing(error)) return; + console.warn("Error awarding achievement:", error); + throw error; + } + + // Get achievement details for toast + const { data: achievement } = await supabase + .from("achievements") + .select("*") + .eq("id", achievementId) + .single(); + + if (achievement) { + aethexToast.aethex({ + title: "Achievement Unlocked! 🎉", + description: `${(achievement as any).icon} ${(achievement as any).name} - ${(achievement as any).description}`, + duration: 8000, + }); + + // Update user's total XP and level + await this.updateUserXPAndLevel(userId, (achievement as any).xp_reward); + } + }, + + async updateUserXPAndLevel(userId: string, xpGained: number): Promise { + // Get current user data + const { data: profile, error } = await supabase + .from("user_profiles") + .select("total_xp, level, loyalty_points") + .eq("id", userId) + .single(); + + if (error || !profile) { + if (isTableMissing(error)) return; + console.log("Profile not found or missing XP fields, skipping XP update"); + return; + } + + const newTotalXP = ((profile as any).total_xp || 0) + xpGained; + const newLevel = Math.floor(newTotalXP / 1000) + 1; // 1000 XP per level + const newLoyaltyPoints = ((profile as any).loyalty_points || 0) + xpGained; + + // Update profile (only update existing fields) + const updates: any = {}; + if ("total_xp" in (profile as any)) updates.total_xp = newTotalXP; + if ("level" in (profile as any)) updates.level = newLevel; + if ("loyalty_points" in (profile as any)) updates.loyalty_points = newLoyaltyPoints; + + if (Object.keys(updates).length > 0) { + await supabase.from("user_profiles").update(updates).eq("id", userId); + } + + // Check for level-up achievements + if (newLevel > ((profile as any).level || 1)) { + if (newLevel >= 5) { + const levelUpAchievement = await supabase + .from("achievements") + .select("id") + .eq("name", "Level Master") + .single(); + + if (levelUpAchievement.data) { + await this.awardAchievement(userId, (levelUpAchievement.data as any).id); + } + } + } + }, + + async checkAndAwardOnboardingAchievement(userId: string): Promise { + const { data: achievement } = await supabase + .from("achievements") + .select("id") + .eq("name", "AeThex Explorer") + .single(); + + if (achievement) { + await this.awardAchievement(userId, (achievement as any).id); + } + }, + + async checkAndAwardProjectAchievements(userId: string): Promise { + const projects = await aethexProjectService.getUserProjects(userId); + + // First project achievement + if (projects.length >= 1) { + const { data: achievement } = await supabase + .from("achievements") + .select("id") + .eq("name", "Portfolio Creator") + .single(); + + if (achievement) { + await this.awardAchievement(userId, (achievement as any).id); + } + } + + // Project master achievement + const completedProjects = projects.filter((p) => p.status === "completed"); + if (completedProjects.length >= 10) { + const { data: achievement } = await supabase + .from("achievements") + .select("id") + .eq("name", "Project Master") + .single(); + + if (achievement) { + await this.awardAchievement(userId, (achievement as any).id); + } + } + }, +}; + +// Notification Service (uses existing notifications table) +export const aethexNotificationService = { + 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) { + console.warn("Error fetching notifications:", error); + return []; + } + + return data as any[]; + }, + + async markAsRead(notificationId: string): Promise { + try { + await supabase + .from("notifications") + .update({ read: true }) + .eq("id", notificationId); + } catch {} + }, + + async createNotification( + userId: string, + type: string, + title: string, + message: string, + ): Promise { + try { + await supabase.from("notifications").insert({ + user_id: userId, + type, + title, + message, + }); + } catch {} + }, +}; + +// Real-time subscriptions +export const aethexRealtimeService = { + subscribeToUserNotifications( + userId: string, + callback: (notification: any) => void, + ) { + return supabase + .channel(`notifications:${userId}`) + .on( + "postgres_changes", + { + event: "INSERT", + schema: "public", + table: "notifications", + filter: `user_id=eq.${userId}`, + }, + callback, + ) + .subscribe(); + }, + + subscribeToProjects(callback: (project: any) => void) { + return supabase + .channel("projects") + .on( + "postgres_changes", + { + event: "INSERT", + schema: "public", + table: "projects", + }, + callback, + ) + .subscribe(); + }, +}; // Role Services (with Supabase table fallback) export const aethexRoleService = {