Remove local achievement fallbacks and rely on Supabase

cgen-1e5a5a6f6f014fa59abcb5e35ad3a4ce
This commit is contained in:
Builder.io 2025-10-01 00:55:25 +00:00
parent a6b7c214c3
commit b193c280a1

View file

@ -438,95 +438,22 @@ export const aethexProjectService = {
}, },
}; };
// Achievement Services (maps to existing achievements table) with robust local fallbacks // Achievement Services (Supabase only)
export const aethexAchievementService = { export const aethexAchievementService = {
defaultAchievements(): AethexAchievement[] {
return [
{
id: "ach_welcome",
name: "Welcome to AeThex",
description: "Completed onboarding and set up your profile",
icon: "👋",
xp_reward: 100,
badge_color: "#10b981",
created_at: new Date().toISOString(),
},
{
id: "ach_explorer",
name: "AeThex Explorer",
description: "Visited key sections and explored the app",
icon: "🧭",
xp_reward: 150,
badge_color: "#3b82f6",
created_at: new Date().toISOString(),
},
{
id: "ach_level_master",
name: "Level Master",
description: "Reached level 5",
icon: "🏆",
xp_reward: 250,
badge_color: "#f59e0b",
created_at: new Date().toISOString(),
},
{
id: "ach_portfolio",
name: "Portfolio Creator",
description: "Created your first project",
icon: "📁",
xp_reward: 200,
badge_color: "#8b5cf6",
created_at: new Date().toISOString(),
},
{
id: "ach_project_master",
name: "Project Master",
description: "Completed 10 projects",
icon: "🛠️",
xp_reward: 500,
badge_color: "#ef4444",
created_at: new Date().toISOString(),
},
];
},
loadLocalAchievements(): AethexAchievement[] {
try {
const raw = localStorage.getItem("demo_achievements");
if (raw) return JSON.parse(raw);
} catch {}
const defaults = this.defaultAchievements();
try {
localStorage.setItem("demo_achievements", JSON.stringify(defaults));
} catch {}
return defaults;
},
saveUserAchievement(userId: string, achievementId: string) {
try {
const key = `demo_user_achievements_${userId}`;
const raw = localStorage.getItem(key);
const ids: string[] = raw ? JSON.parse(raw) : [];
if (!ids.includes(achievementId)) ids.push(achievementId);
localStorage.setItem(key, JSON.stringify(ids));
} catch {}
},
async getAllAchievements(): Promise<AethexAchievement[]> { async getAllAchievements(): Promise<AethexAchievement[]> {
try {
const { data, error } = await supabase const { data, error } = await supabase
.from("achievements") .from("achievements")
.select("*") .select("*")
.order("xp_reward", { ascending: false }); .order("xp_reward", { ascending: false });
if (!error && Array.isArray(data) && data.length) {
return data as AethexAchievement[]; if (error) {
throw error;
} }
} catch {}
return this.loadLocalAchievements(); return (Array.isArray(data) ? data : []) as AethexAchievement[];
}, },
async getUserAchievements(userId: string): Promise<AethexAchievement[]> { async getUserAchievements(userId: string): Promise<AethexAchievement[]> {
try {
const { data, error } = await supabase const { data, error } = await supabase
.from("user_achievements") .from("user_achievements")
.select( .select(
@ -536,72 +463,28 @@ export const aethexAchievementService = {
`, `,
) )
.eq("user_id", userId); .eq("user_id", userId);
if (!error && Array.isArray(data)) {
const list = (data as any[]) if (error) {
throw error;
}
return ((Array.isArray(data) ? data : []) as any[])
.map((item) => (item as any).achievements) .map((item) => (item as any).achievements)
.filter(Boolean) as AethexAchievement[]; .filter(Boolean) as AethexAchievement[];
if (list.length) return list;
}
} catch {}
// Local fallback
const key = `demo_user_achievements_${userId}`;
let ids: string[] = [];
try {
ids = JSON.parse(localStorage.getItem(key) || "[]");
} catch {}
const all = await this.getAllAchievements();
const byId = new Map(all.map((a) => [a.id, a] as const));
return ids.map((id) => byId.get(id)).filter(Boolean) as AethexAchievement[];
}, },
async awardAchievement(userId: string, achievementId: string): Promise<void> { async awardAchievement(userId: string, achievementId: string): Promise<void> {
let usedLocal = false;
try {
const { error } = await supabase.from("user_achievements").insert({ const { error } = await supabase.from("user_achievements").insert({
user_id: userId, user_id: userId,
achievement_id: achievementId, achievement_id: achievementId,
}); });
if (error && error.code !== "23505") { if (error && error.code !== "23505") {
if (!isTableMissing(error)) throw error; throw error;
usedLocal = true;
}
} catch {
usedLocal = true;
}
let achievement: AethexAchievement | null = null;
if (!usedLocal) {
try {
const { data } = await supabase
.from("achievements")
.select("*")
.eq("id", achievementId)
.single();
achievement = (data as any) || null;
} catch {}
}
if (usedLocal || !achievement) {
this.saveUserAchievement(userId, achievementId);
const all = await this.getAllAchievements();
achievement = all.find((a) => a.id === achievementId) || null;
}
if (achievement) {
aethexToast.aethex({
title: "Achievement Unlocked! 🎉",
description: `${achievement.icon || "🏅"} ${achievement.name} - ${achievement.description}`,
duration: 8000,
});
await this.updateUserXPAndLevel(userId, achievement.xp_reward);
} }
}, },
async updateUserXPAndLevel(userId: string, xpGained: number): Promise<void> { async updateUserXPAndLevel(userId: string, xpGained: number | null = null): Promise<void> {
ensureSupabase();
try {
const { data: profile, error } = await supabase const { data: profile, error } = await supabase
.from("user_profiles") .from("user_profiles")
.select("total_xp, level, loyalty_points") .select("total_xp, level, loyalty_points")
@ -609,55 +492,34 @@ export const aethexAchievementService = {
.single(); .single();
if (error) { if (error) {
if (isTableMissing(error)) { throw error;
throw new Error(
'Supabase table "user_profiles" is missing. Please run the required migrations.',
);
}
console.warn("Unable to load profile for XP update:", error);
return;
} }
if (!profile) { if (!profile) {
console.warn("No profile found while updating XP for", userId);
return; return;
} }
const currentProfile: any = profile; const currentProfile: any = profile;
const newTotalXP = (currentProfile.total_xp || 0) + xpGained; const xpDelta = xpGained ?? 0;
const newTotalXP = (currentProfile.total_xp || 0) + xpDelta;
const newLevel = Math.floor(newTotalXP / 1000) + 1; const newLevel = Math.floor(newTotalXP / 1000) + 1;
const newLoyaltyPoints = (currentProfile.loyalty_points || 0) + xpGained; const newLoyaltyPoints = (currentProfile.loyalty_points || 0) + xpDelta;
const updates: Record<string, number> = {}; const updates: Record<string, number> = {};
if ("total_xp" in currentProfile) updates.total_xp = newTotalXP; if ("total_xp" in currentProfile) updates.total_xp = newTotalXP;
if ("level" in currentProfile) updates.level = newLevel; if ("level" in currentProfile) updates.level = newLevel;
if ("loyalty_points" in currentProfile) if ("loyalty_points" in currentProfile) updates.loyalty_points = newLoyaltyPoints;
updates.loyalty_points = newLoyaltyPoints;
if (Object.keys(updates).length > 0) { if (Object.keys(updates).length > 0) {
await supabase.from("user_profiles").update(updates).eq("id", userId); const { error: updateError } = await supabase
} .from("user_profiles")
.update(updates)
if (newLevel > (currentProfile.level || 1) && newLevel >= 5) { .eq("id", userId);
try { if (updateError) throw updateError;
const { data } = await supabase
.from("achievements")
.select("id")
.eq("name", "Level Master")
.single();
const id = (data as any)?.id || "ach_level_master";
await this.awardAchievement(userId, id);
} catch {
await this.awardAchievement(userId, "ach_level_master");
}
}
} catch (error) {
console.warn("Failed to update XP and level:", error);
} }
}, },
async checkAndAwardOnboardingAchievement(userId: string): Promise<void> { async checkAndAwardOnboardingAchievement(userId: string): Promise<void> {
let awarded = false;
try { try {
const resp = await fetch(`/api/achievements/award`, { const resp = await fetch(`/api/achievements/award`, {
method: "POST", method: "POST",
@ -667,56 +529,52 @@ export const aethexAchievementService = {
achievement_names: ["Welcome to AeThex", "AeThex Explorer"], achievement_names: ["Welcome to AeThex", "AeThex Explorer"],
}), }),
}); });
awarded = resp.ok;
} catch {}
if (!awarded) { if (resp.ok) {
const all = await this.getAllAchievements(); return;
const byName = new Map(all.map((a) => [a.name, a.id] as const)); }
const ids = [ } catch (error) {
byName.get("Welcome to AeThex") || "ach_welcome", console.warn("Edge function award failed, attempting direct Supabase insert", error);
byName.get("AeThex Explorer") || "ach_explorer",
];
for (const id of ids) await this.awardAchievement(userId, id);
} }
aethexToast.aethex({ const achievements = await this.getAllAchievements();
title: "Achievement Unlocked! 🎉", const byName = new Map(achievements.map((item) => [item.name, item.id] as const));
description: "Welcome to AeThex - Profile setup complete", const names = ["Welcome to AeThex", "AeThex Explorer"];
duration: 8000,
}); for (const name of names) {
const id = byName.get(name);
if (!id) continue;
await this.awardAchievement(userId, id);
}
}, },
async checkAndAwardProjectAchievements(userId: string): Promise<void> { async checkAndAwardProjectAchievements(userId: string): Promise<void> {
const projects = await aethexProjectService.getUserProjects(userId); const projects = await aethexProjectService.getUserProjects(userId);
if (projects.length >= 1) { if (projects.length >= 1) {
// Portfolio Creator const { data, error } = await supabase
try {
const { data } = await supabase
.from("achievements") .from("achievements")
.select("id") .select("id")
.eq("name", "Portfolio Creator") .eq("name", "Portfolio Creator")
.single(); .maybeSingle();
const id = (data as any)?.id || "ach_portfolio";
await this.awardAchievement(userId, id); if (error) throw error;
} catch { if (data?.id) {
await this.awardAchievement(userId, "ach_portfolio"); await this.awardAchievement(userId, data.id);
} }
} }
const completed = projects.filter((p) => p.status === "completed"); const completed = projects.filter((p) => p.status === "completed");
if (completed.length >= 10) { if (completed.length >= 10) {
try { const { data, error } = await supabase
const { data } = await supabase
.from("achievements") .from("achievements")
.select("id") .select("id")
.eq("name", "Project Master") .eq("name", "Project Master")
.single(); .maybeSingle();
const id = (data as any)?.id || "ach_project_master";
await this.awardAchievement(userId, id); if (error) throw error;
} catch { if (data?.id) {
await this.awardAchievement(userId, "ach_project_master"); await this.awardAchievement(userId, data.id);
} }
} }
}, },