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 = {
|
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) {
|
if (error) {
|
||||||
return data as AethexAchievement[];
|
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(
|
`
|
||||||
`
|
|
||||||
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 {}
|
|
||||||
|
|
||||||
// Local fallback
|
if (error) {
|
||||||
const key = `demo_user_achievements_${userId}`;
|
throw error;
|
||||||
let ids: string[] = [];
|
}
|
||||||
try {
|
|
||||||
ids = JSON.parse(localStorage.getItem(key) || "[]");
|
return ((Array.isArray(data) ? 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> {
|
||||||
let usedLocal = false;
|
const { error } = await supabase.from("user_achievements").insert({
|
||||||
try {
|
user_id: userId,
|
||||||
const { error } = await supabase.from("user_achievements").insert({
|
achievement_id: achievementId,
|
||||||
user_id: userId,
|
});
|
||||||
achievement_id: achievementId,
|
|
||||||
});
|
|
||||||
if (error && error.code !== "23505") {
|
|
||||||
if (!isTableMissing(error)) throw error;
|
|
||||||
usedLocal = true;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
usedLocal = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let achievement: AethexAchievement | null = null;
|
if (error && error.code !== "23505") {
|
||||||
if (!usedLocal) {
|
throw error;
|
||||||
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();
|
const { data: profile, error } = await supabase
|
||||||
|
.from("user_profiles")
|
||||||
|
.select("total_xp, level, loyalty_points")
|
||||||
|
.eq("id", userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
try {
|
if (error) {
|
||||||
const { data: profile, error } = await supabase
|
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")
|
.from("user_profiles")
|
||||||
.select("total_xp, level, loyalty_points")
|
.update(updates)
|
||||||
.eq("id", userId)
|
.eq("id", userId);
|
||||||
.single();
|
if (updateError) throw updateError;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
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 {
|
.from("achievements")
|
||||||
const { data } = await supabase
|
.select("id")
|
||||||
.from("achievements")
|
.eq("name", "Portfolio Creator")
|
||||||
.select("id")
|
.maybeSingle();
|
||||||
.eq("name", "Portfolio Creator")
|
|
||||||
.single();
|
if (error) throw error;
|
||||||
const id = (data as any)?.id || "ach_portfolio";
|
if (data?.id) {
|
||||||
await this.awardAchievement(userId, id);
|
await this.awardAchievement(userId, data.id);
|
||||||
} catch {
|
|
||||||
await this.awardAchievement(userId, "ach_portfolio");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
.maybeSingle();
|
||||||
.single();
|
|
||||||
const id = (data as any)?.id || "ach_project_master";
|
if (error) throw error;
|
||||||
await this.awardAchievement(userId, id);
|
if (data?.id) {
|
||||||
} catch {
|
await this.awardAchievement(userId, data.id);
|
||||||
await this.awardAchievement(userId, "ach_project_master");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue