Add robust local fallback for achievements, XP/level, and onboarding without Supabase

cgen-4ec3c99880b14107a6a6cfe83dd8af97
This commit is contained in:
Builder.io 2025-09-30 00:24:18 +00:00
parent 1df7a691db
commit 8df7ddda9e

View file

@ -370,124 +370,225 @@ export const aethexProjectService = {
}, },
}; };
// Achievement Services (maps to existing achievements table) // Achievement Services (maps to existing achievements table) with robust local fallbacks
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[]> {
const { data, error } = await supabase try {
.from("achievements") const { data, error } = await supabase
.select("*") .from("achievements")
.order("xp_reward", { ascending: false }); .select("*")
.order("xp_reward", { ascending: false });
if (error) { if (!error && Array.isArray(data) && data.length) {
console.warn("Error fetching achievements:", error); return data as AethexAchievement[];
return []; }
} } catch {}
return this.loadLocalAchievements();
return data as AethexAchievement[];
}, },
async getUserAchievements(userId: string): Promise<AethexAchievement[]> { async getUserAchievements(userId: string): Promise<AethexAchievement[]> {
const { data, error } = await supabase try {
.from("user_achievements") const { data, error } = await supabase
.select( .from("user_achievements")
` .select(
`
achievement_id, achievement_id,
achievements (*) achievements (*)
`, `,
) )
.eq("user_id", userId); .eq("user_id", userId);
if (!error && Array.isArray(data)) {
const list = (data as any[])
.map((item) => (item as any).achievements)
.filter(Boolean) as AethexAchievement[];
if (list.length) return list;
}
} catch {}
if (error) { // Local fallback
console.warn("Error fetching user achievements:", error); const key = `demo_user_achievements_${userId}`;
return []; let ids: string[] = [];
} try {
ids = JSON.parse(localStorage.getItem(key) || "[]");
return (data as any[]) } catch {}
.map((item) => (item as any).achievements) const all = await this.getAllAchievements();
.filter(Boolean) as AethexAchievement[]; 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> {
const { error } = await supabase.from("user_achievements").insert({ let usedLocal = false;
user_id: userId, try {
achievement_id: achievementId, const { error } = await supabase.from("user_achievements").insert({
}); user_id: userId,
achievement_id: achievementId,
if (error && error.code !== "23505") { });
// Ignore duplicate key error if (error && error.code !== "23505") {
if (isTableMissing(error)) return; if (!isTableMissing(error)) throw error;
console.warn("Error awarding achievement:", error); usedLocal = true;
throw error; }
} catch {
usedLocal = true;
} }
// Get achievement details for toast let achievement: AethexAchievement | null = null;
const { data: achievement } = await supabase if (!usedLocal) {
.from("achievements") try {
.select("*") const { data } = await supabase
.eq("id", achievementId) .from("achievements")
.single(); .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) { if (achievement) {
aethexToast.aethex({ aethexToast.aethex({
title: "Achievement Unlocked! 🎉", title: "Achievement Unlocked! 🎉",
description: `${(achievement as any).icon} ${(achievement as any).name} - ${(achievement as any).description}`, description: `${achievement.icon || "🏅"} ${achievement.name} - ${achievement.description}`,
duration: 8000, duration: 8000,
}); });
await this.updateUserXPAndLevel(userId, achievement.xp_reward);
// Update user's total XP and level
await this.updateUserXPAndLevel(userId, (achievement as any).xp_reward);
} }
}, },
async updateUserXPAndLevel(userId: string, xpGained: number): Promise<void> { async updateUserXPAndLevel(userId: string, xpGained: number): Promise<void> {
// Get current user data 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")
.eq("id", userId) .eq("id", userId)
.single(); .single();
if (error || !profile) { if (!error && profile) {
if (isTableMissing(error)) return; const newTotalXP = ((profile as any).total_xp || 0) + xpGained;
console.log("Profile not found or missing XP fields, skipping XP update"); const newLevel = Math.floor(newTotalXP / 1000) + 1;
return; const newLoyaltyPoints = ((profile as any).loyalty_points || 0) + xpGained;
}
const newTotalXP = ((profile as any).total_xp || 0) + xpGained; const updates: any = {};
const newLevel = Math.floor(newTotalXP / 1000) + 1; // 1000 XP per level if ("total_xp" in (profile as any)) updates.total_xp = newTotalXP;
const newLoyaltyPoints = ((profile as any).loyalty_points || 0) + xpGained; if ("level" in (profile as any)) updates.level = newLevel;
if ("loyalty_points" in (profile as any)) updates.loyalty_points = newLoyaltyPoints;
// Update profile (only update existing fields) if (Object.keys(updates).length > 0) {
const updates: any = {}; await supabase.from("user_profiles").update(updates).eq("id", userId);
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,
);
} }
if (newLevel > ((profile as any).level || 1) && newLevel >= 5) {
// Try to find Level Master by name, fall back to default id
try {
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");
}
}
return;
} }
} } catch {}
// Local fallback using mock profile persistence
try {
const current = await mockAuth.getUserProfile(userId as any);
const total_xp = ((current as any)?.total_xp || 0) + xpGained;
const level = Math.floor(total_xp / 1000) + 1;
const loyalty_points = ((current as any)?.loyalty_points || 0) + xpGained;
await mockAuth.updateProfile(userId as any, {
total_xp,
level,
loyalty_points,
} as any);
if (level > ((current as any)?.level || 1) && level >= 5) {
await this.awardAchievement(userId, "ach_level_master");
}
} catch {}
}, },
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",
@ -497,49 +598,56 @@ export const aethexAchievementService = {
achievement_names: ["Welcome to AeThex", "AeThex Explorer"], achievement_names: ["Welcome to AeThex", "AeThex Explorer"],
}), }),
}); });
if (!resp.ok) { awarded = resp.ok;
const text = await resp.text().catch(() => ""); } catch {}
console.warn("Award onboarding achievement failed:", text);
return; if (!awarded) {
} const all = await this.getAllAchievements();
// Show celebratory toast const byName = new Map(all.map((a) => [a.name, a.id] as const));
aethexToast.aethex({ const ids = [
title: "Achievement Unlocked! 🎉", byName.get("Welcome to AeThex") || "ach_welcome",
description: "Welcome to AeThex - Profile setup complete", byName.get("AeThex Explorer") || "ach_explorer",
duration: 8000, ];
}); for (const id of ids) await this.awardAchievement(userId, id);
} catch (e) {
console.warn("Award onboarding achievement exception:", e);
} }
aethexToast.aethex({
title: "Achievement Unlocked! 🎉",
description: "Welcome to AeThex - Profile setup complete",
duration: 8000,
});
}, },
async checkAndAwardProjectAchievements(userId: string): Promise<void> { async checkAndAwardProjectAchievements(userId: string): Promise<void> {
const projects = await aethexProjectService.getUserProjects(userId); const projects = await aethexProjectService.getUserProjects(userId);
// First project achievement
if (projects.length >= 1) { if (projects.length >= 1) {
const { data: achievement } = await supabase // Portfolio Creator
.from("achievements") try {
.select("id") const { data } = await supabase
.eq("name", "Portfolio Creator") .from("achievements")
.single(); .select("id")
.eq("name", "Portfolio Creator")
if (achievement) { .single();
await this.awardAchievement(userId, (achievement as any).id); const id = (data as any)?.id || "ach_portfolio";
await this.awardAchievement(userId, id);
} catch {
await this.awardAchievement(userId, "ach_portfolio");
} }
} }
// Project master achievement const completed = projects.filter((p) => p.status === "completed");
const completedProjects = projects.filter((p) => p.status === "completed"); if (completed.length >= 10) {
if (completedProjects.length >= 10) { try {
const { data: achievement } = await supabase const { data } = await supabase
.from("achievements") .from("achievements")
.select("id") .select("id")
.eq("name", "Project Master") .eq("name", "Project Master")
.single(); .single();
const id = (data as any)?.id || "ach_project_master";
if (achievement) { await this.awardAchievement(userId, id);
await this.awardAchievement(userId, (achievement as any).id); } catch {
await this.awardAchievement(userId, "ach_project_master");
} }
} }
}, },