Remove local achievement fallbacks and rely on Supabase
cgen-1e5a5a6f6f014fa59abcb5e35ad3a4ce
This commit is contained in:
parent
a6b7c214c3
commit
b193c280a1
1 changed files with 92 additions and 234 deletions
|
|
@ -438,226 +438,88 @@ export const aethexProjectService = {
|
|||
},
|
||||
};
|
||||
|
||||
// Achievement Services (maps to existing achievements table) with robust local fallbacks
|
||||
// Achievement Services (Supabase only)
|
||||
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[]> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from("achievements")
|
||||
.select("*")
|
||||
.order("xp_reward", { ascending: false });
|
||||
if (!error && Array.isArray(data) && data.length) {
|
||||
return data as AethexAchievement[];
|
||||
}
|
||||
} catch {}
|
||||
return this.loadLocalAchievements();
|
||||
const { data, error } = await supabase
|
||||
.from("achievements")
|
||||
.select("*")
|
||||
.order("xp_reward", { ascending: false });
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (Array.isArray(data) ? data : []) as AethexAchievement[];
|
||||
},
|
||||
|
||||
async getUserAchievements(userId: string): Promise<AethexAchievement[]> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from("user_achievements")
|
||||
.select(
|
||||
`
|
||||
const { data, error } = await supabase
|
||||
.from("user_achievements")
|
||||
.select(
|
||||
`
|
||||
achievement_id,
|
||||
achievements (*)
|
||||
`,
|
||||
)
|
||||
.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 {}
|
||||
)
|
||||
.eq("user_id", userId);
|
||||
|
||||
// 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[];
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return ((Array.isArray(data) ? data : []) as any[])
|
||||
.map((item) => (item as any).achievements)
|
||||
.filter(Boolean) as AethexAchievement[];
|
||||
},
|
||||
|
||||
async awardAchievement(userId: string, achievementId: string): Promise<void> {
|
||||
let usedLocal = false;
|
||||
try {
|
||||
const { error } = await supabase.from("user_achievements").insert({
|
||||
user_id: userId,
|
||||
achievement_id: achievementId,
|
||||
});
|
||||
if (error && error.code !== "23505") {
|
||||
if (!isTableMissing(error)) throw error;
|
||||
usedLocal = true;
|
||||
}
|
||||
} catch {
|
||||
usedLocal = true;
|
||||
}
|
||||
const { error } = await supabase.from("user_achievements").insert({
|
||||
user_id: userId,
|
||||
achievement_id: achievementId,
|
||||
});
|
||||
|
||||
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);
|
||||
if (error && error.code !== "23505") {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async updateUserXPAndLevel(userId: string, xpGained: number): Promise<void> {
|
||||
ensureSupabase();
|
||||
async updateUserXPAndLevel(userId: string, xpGained: number | null = null): Promise<void> {
|
||||
const { data: profile, error } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("total_xp, level, loyalty_points")
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
|
||||
try {
|
||||
const { data: profile, error } = await supabase
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentProfile: any = profile;
|
||||
const xpDelta = xpGained ?? 0;
|
||||
const newTotalXP = (currentProfile.total_xp || 0) + xpDelta;
|
||||
const newLevel = Math.floor(newTotalXP / 1000) + 1;
|
||||
const newLoyaltyPoints = (currentProfile.loyalty_points || 0) + xpDelta;
|
||||
|
||||
const updates: Record<string, number> = {};
|
||||
if ("total_xp" in currentProfile) updates.total_xp = newTotalXP;
|
||||
if ("level" in currentProfile) updates.level = newLevel;
|
||||
if ("loyalty_points" in currentProfile) updates.loyalty_points = newLoyaltyPoints;
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
const { error: updateError } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("total_xp, level, loyalty_points")
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
if (isTableMissing(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) {
|
||||
console.warn("No profile found while updating XP for", userId);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentProfile: any = profile;
|
||||
const newTotalXP = (currentProfile.total_xp || 0) + xpGained;
|
||||
const newLevel = Math.floor(newTotalXP / 1000) + 1;
|
||||
const newLoyaltyPoints = (currentProfile.loyalty_points || 0) + xpGained;
|
||||
|
||||
const updates: Record<string, number> = {};
|
||||
if ("total_xp" in currentProfile) updates.total_xp = newTotalXP;
|
||||
if ("level" in currentProfile) updates.level = newLevel;
|
||||
if ("loyalty_points" in currentProfile)
|
||||
updates.loyalty_points = newLoyaltyPoints;
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await supabase.from("user_profiles").update(updates).eq("id", userId);
|
||||
}
|
||||
|
||||
if (newLevel > (currentProfile.level || 1) && newLevel >= 5) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to update XP and level:", error);
|
||||
.update(updates)
|
||||
.eq("id", userId);
|
||||
if (updateError) throw updateError;
|
||||
}
|
||||
},
|
||||
|
||||
async checkAndAwardOnboardingAchievement(userId: string): Promise<void> {
|
||||
let awarded = false;
|
||||
try {
|
||||
const resp = await fetch(`/api/achievements/award`, {
|
||||
method: "POST",
|
||||
|
|
@ -667,56 +529,52 @@ export const aethexAchievementService = {
|
|||
achievement_names: ["Welcome to AeThex", "AeThex Explorer"],
|
||||
}),
|
||||
});
|
||||
awarded = resp.ok;
|
||||
} catch {}
|
||||
|
||||
if (!awarded) {
|
||||
const all = await this.getAllAchievements();
|
||||
const byName = new Map(all.map((a) => [a.name, a.id] as const));
|
||||
const ids = [
|
||||
byName.get("Welcome to AeThex") || "ach_welcome",
|
||||
byName.get("AeThex Explorer") || "ach_explorer",
|
||||
];
|
||||
for (const id of ids) await this.awardAchievement(userId, id);
|
||||
if (resp.ok) {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Edge function award failed, attempting direct Supabase insert", error);
|
||||
}
|
||||
|
||||
aethexToast.aethex({
|
||||
title: "Achievement Unlocked! 🎉",
|
||||
description: "Welcome to AeThex - Profile setup complete",
|
||||
duration: 8000,
|
||||
});
|
||||
const achievements = await this.getAllAchievements();
|
||||
const byName = new Map(achievements.map((item) => [item.name, item.id] as const));
|
||||
const names = ["Welcome to AeThex", "AeThex Explorer"];
|
||||
|
||||
for (const name of names) {
|
||||
const id = byName.get(name);
|
||||
if (!id) continue;
|
||||
await this.awardAchievement(userId, id);
|
||||
}
|
||||
},
|
||||
|
||||
async checkAndAwardProjectAchievements(userId: string): Promise<void> {
|
||||
const projects = await aethexProjectService.getUserProjects(userId);
|
||||
|
||||
if (projects.length >= 1) {
|
||||
// Portfolio Creator
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from("achievements")
|
||||
.select("id")
|
||||
.eq("name", "Portfolio Creator")
|
||||
.single();
|
||||
const id = (data as any)?.id || "ach_portfolio";
|
||||
await this.awardAchievement(userId, id);
|
||||
} catch {
|
||||
await this.awardAchievement(userId, "ach_portfolio");
|
||||
const { data, error } = await supabase
|
||||
.from("achievements")
|
||||
.select("id")
|
||||
.eq("name", "Portfolio Creator")
|
||||
.maybeSingle();
|
||||
|
||||
if (error) throw error;
|
||||
if (data?.id) {
|
||||
await this.awardAchievement(userId, data.id);
|
||||
}
|
||||
}
|
||||
|
||||
const completed = projects.filter((p) => p.status === "completed");
|
||||
if (completed.length >= 10) {
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from("achievements")
|
||||
.select("id")
|
||||
.eq("name", "Project Master")
|
||||
.single();
|
||||
const id = (data as any)?.id || "ach_project_master";
|
||||
await this.awardAchievement(userId, id);
|
||||
} catch {
|
||||
await this.awardAchievement(userId, "ach_project_master");
|
||||
const { data, error } = await supabase
|
||||
.from("achievements")
|
||||
.select("id")
|
||||
.eq("name", "Project Master")
|
||||
.maybeSingle();
|
||||
|
||||
if (error) throw error;
|
||||
if (data?.id) {
|
||||
await this.awardAchievement(userId, data.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue