Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/dev-dist/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ define(['./workbox-86c9b217'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "index.html",
"revision": "0.vs5n5ncup7"
"revision": "0.9tvgq2asjt8"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
Expand Down
115 changes: 59 additions & 56 deletions client/src/components/screens/Home/DailyMissionsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { MissionDisplayData } from "../../types/missionTypes";
import { useMissionQuery } from "../../../dojo/hooks/useMissionQuery";
import { useMissionSpawner } from "../../../dojo/hooks/useMissionSpawner";
import { useMissionData } from "../../../dojo/hooks/useMissionData";
import { useMissionRewardClaimer } from "../../../dojo/hooks/useMissionRewardClaimer";
import { usePlayer } from "../../../dojo/hooks/usePlayer";

interface DailyMissionsModalProps {
/** Callback to close the modal */
onClose: () => void
}

Expand Down Expand Up @@ -48,25 +49,33 @@ const getEnumVariant = (enumObj: any, defaultValue: string): string => {
};

/**
* NEW FUNCTION: Determines if a mission is completed based on its status
* Determines if a mission is completed based on its status
*/
const isMissionCompleted = (mission: Mission): boolean => {
const statusVariant = getEnumVariant(mission.status, 'Pending');
return statusVariant === 'Completed';
};

/**
* Determines if a mission has been claimed based on its status
*/
const isMissionClaimed = (mission: Mission): boolean => {
const statusVariant = getEnumVariant(mission.status, 'Pending');
return statusVariant === 'Claimed';
};

/**
* Converts Mission bindings to display data for UI
*/
const missionToDisplayData = (mission: Mission, claimedMissionIds: Set<string>): MissionDisplayData => {
const missionToDisplayData = (mission: Mission): MissionDisplayData => {
let difficulty: 'Easy' | 'Mid' | 'Hard' = 'Easy';
if (mission.target_coins >= 1000) difficulty = 'Hard';
else if (mission.target_coins >= 500) difficulty = 'Mid';

const worldVariant = getEnumVariant(mission.required_world, 'Forest');
const golemVariant = getEnumVariant(mission.required_golem, 'Fire');
const completed = isMissionCompleted(mission);
const claimed = claimedMissionIds.has(mission.id.toString());
const claimed = isMissionClaimed(mission);

const requiredWorld = worldVariant.charAt(0).toUpperCase() + worldVariant.slice(1);
const requiredGolem = golemVariant.charAt(0).toUpperCase() + golemVariant.slice(1);
Expand Down Expand Up @@ -95,20 +104,18 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) {
[account]
);

// Hooks modulares
// Hooks
const { fetchTodayMissions, isLoading: isQuerying, error: queryError } = useMissionQuery();
const { spawnMissions, isSpawning, error: spawnError } = useMissionSpawner();
const { claimMissionReward, isClaiming, error: claimError } = useMissionRewardClaimer();
const { refetch: refetchPlayer } = usePlayer();

// Estado local
const [missions, setMissions] = useState<Mission[]>([]);
const [isInitialized, setIsInitialized] = useState(false);
const [showCelebration, setShowCelebration] = useState(false);
const [claimedMission, setClaimedMission] = useState<MissionDisplayData | null>(null);
const [claimedMissionIds, setClaimedMissionIds] = useState<Set<string>>(new Set());

// NEW STATE: For claim reward process
const [claimingMissionId, setClaimingMissionId] = useState<string | null>(null);
const [claimError, setClaimError] = useState<string | null>(null);

// Procesar data
const { todayMissions, hasData } = useMissionData(missions);
Expand All @@ -117,70 +124,57 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) {
const isLoading = isQuerying || isSpawning;
const error = queryError || spawnError || claimError;

// NEW FUNCTION: Refresh missions after claim
// Refresh missions after claim
const refreshMissionsAfterClaim = useCallback(async () => {
if (!playerAddress) return;

try {
console.log("🔄 Refreshing missions after claim...");
const refreshedMissions = await fetchTodayMissions(playerAddress);
setMissions(refreshedMissions);
console.log("✅ Missions refreshed successfully");
} catch (error) {
console.error("❌ Error refreshing missions:", error);
// Silently handle refresh error
}
}, [playerAddress, fetchTodayMissions]);

// NEW FUNCTION: Handle claim reward (placeholder for future implementation)
// Handle real claim reward with blockchain transaction
const handleClaimReward = useCallback(async (mission: MissionDisplayData) => {
console.log(`🎯 Claiming reward for mission ${mission.id}:`, mission);

setClaimingMissionId(mission.id);
setClaimError(null);

try {
// TODO: Implement actual claim reward transaction
// This would call a hook like useMissionRewardClaimer
// For now, we'll simulate the process

console.log("🔄 Processing claim reward transaction...");

// Simulate API call delay
await new Promise(resolve => setTimeout(resolve, 2000));
const result = await claimMissionReward(
parseInt(mission.id),
mission.difficulty // Use the mission's existing difficulty field
);

// For now, just mark as claimed locally
// In the real implementation, this would be handled by the blockchain transaction
setClaimedMission(mission);
setShowCelebration(true);
setClaimedMissionIds(prev => new Set(prev).add(mission.id));

// Refresh missions from blockchain after successful claim
await refreshMissionsAfterClaim();

console.log("✅ Mission reward claimed successfully");
if (result.success) {
// Optimistic update
setClaimedMission(mission);
setShowCelebration(true);

// Refresh data from blockchain
await Promise.all([
refreshMissionsAfterClaim(),
refetchPlayer() // Update player coins
]);
}

} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Failed to claim reward";
setClaimError(errorMessage);
console.error("❌ Error claiming mission reward:", error);
// Error is handled by the hook
} finally {
setClaimingMissionId(null);
}
}, [refreshMissionsAfterClaim]);
}, [claimMissionReward, refreshMissionsAfterClaim, refetchPlayer]);

// 🎯 ORQUESTACIÓN PRINCIPAL
// Initialize missions
const initializeMissions = useCallback(async () => {
if (!playerAddress || isInitialized) return;

try {
console.log("📡 Checking for existing missions...");
const existingMissions = await fetchTodayMissions(playerAddress);

if (existingMissions.length > 0) {
console.log(`✅ Found ${existingMissions.length} existing missions`);
setMissions(existingMissions);
} else {
console.log("🎲 No missions found, creating new ones...");
const spawnSuccess = await spawnMissions(playerAddress);

if (spawnSuccess) {
Expand All @@ -191,23 +185,21 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) {

setIsInitialized(true);
} catch (error) {
console.error("❌ Error initializing missions:", error);
// Silently handle error
}
}, [playerAddress, isInitialized, fetchTodayMissions, spawnMissions]);

// Ejecutar al abrir modal
// Load missions when modal opens
useEffect(() => {
if (playerAddress) {
initializeMissions();
}
}, [playerAddress, initializeMissions]);

// Reset cuando cambia de usuario
// Reset state when user changes
useEffect(() => {
setMissions([]);
setIsInitialized(false);
setClaimedMissionIds(new Set()); // NEW: Reset claimed missions
setClaimError(null); // NEW: Reset claim errors
}, [playerAddress]);

// Early return if no account
Expand Down Expand Up @@ -244,12 +236,12 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) {
);
}

// Convertir misiones - UPDATED to pass claimedMissionIds
// Convert missions to display data
const displayMissions: MissionDisplayData[] = todayMissions.map(mission => {
return missionToDisplayData(mission, claimedMissionIds);
return missionToDisplayData(mission);
});

// NEW FUNCTION: Get mission card styling based on status
// Get mission card styling based on status
const getMissionCardStyling = (mission: MissionDisplayData) => {
if (mission.completed) {
if (mission.claimed) {
Expand Down Expand Up @@ -286,6 +278,16 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) {
}
};

// Get fixed reward amount based on difficulty
const getFixedReward = (difficulty: MissionDisplayData['difficulty']): number => {
switch (difficulty) {
case 'Easy': return 100;
case 'Mid': return 250;
case 'Hard': return 500;
default: return 100;
}
};

const handleCloseCelebration = () => {
setShowCelebration(false);
setClaimedMission(null);
Expand Down Expand Up @@ -375,6 +377,7 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) {
{displayMissions.map((mission: MissionDisplayData, index: number) => {
const styling = getMissionCardStyling(mission);
const isClaimingThis = claimingMissionId === mission.id;
const fixedReward = getFixedReward(mission.difficulty);

return (
<motion.div
Expand All @@ -401,7 +404,7 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) {
: 'text-green-600'
: 'text-yellow-600'
}`}>
{mission.reward}
{fixedReward}
</span>
<img
src={coinIcon}
Expand Down Expand Up @@ -450,15 +453,15 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) {
</div>
</div>

{/* NEW SECTION: Action Buttons */}
{/* Action Buttons */}
{mission.completed && !mission.claimed && (
<div className="flex justify-end">
<motion.button
className="bg-green-500 hover:bg-green-600 text-white px-3 py-2 sm:px-4 sm:py-2 rounded-[5px] font-luckiest text-xs sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
whileHover={!isClaimingThis ? { scale: 1.05 } : {}}
whileTap={!isClaimingThis ? { scale: 0.95 } : {}}
onClick={() => handleClaimReward(mission)}
disabled={isClaimingThis || claimingMissionId !== null}
disabled={isClaimingThis || isClaiming}
>
{isClaimingThis ? (
<>
Expand All @@ -470,7 +473,7 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) {
<span>Claiming...</span>
</>
) : (
'Claim Reward'
`Claim ${fixedReward} Coins`
)}
</motion.button>
</div>
Expand Down Expand Up @@ -518,7 +521,7 @@ export function DailyMissionsModal({ onClose }: DailyMissionsModalProps) {
</motion.div>
</motion.div>

{/* Animación de celebración */}
{/* Celebration animation */}
<AnimatePresence>
{showCelebration && claimedMission && (
<ClaimMissionAnimation
Expand Down
7 changes: 5 additions & 2 deletions client/src/dojo/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export type GolemTypeEnum = CairoCustomEnum;
export type MissionStatus = {
Pending: string;
Completed: string;
Claimed: string;
}
export type MissionStatusEnum = CairoCustomEnum;

Expand Down Expand Up @@ -258,7 +259,8 @@ export const schema: SchemaType = {
description: "",
status: new CairoCustomEnum({
Pending: "Pending",
Completed: "Completed", }),
Completed: "Completed",
Claimed: "Claimed",}),
created_at: 0,
},
MissionValue: {
Expand All @@ -274,7 +276,8 @@ export const schema: SchemaType = {
description: "",
status: new CairoCustomEnum({
Pending: "Pending",
Completed: "Completed", }),
Completed: "Completed",
Claimed: "Claimed",}),
created_at: 0,
},
Player: {
Expand Down
4 changes: 3 additions & 1 deletion client/src/dojo/hooks/useMissionQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const createCairoEnum = (rawValue: any, enumMap: Record<string, string>, default
const toriiNodeToMission = (rawNode: RawMissionNode): Mission => {
const worldEnumMap = { Volcano: "Volcano", Glacier: "Glacier", Forest: "Forest" };
const golemEnumMap = { Ice: "Ice", Stone: "Stone", Fire: "Fire" };
const statusEnumMap = { Completed: "Completed", Pending: "Pending" };
const statusEnumMap = { Completed: "Completed", Pending: "Pending", Claimed: "Claimed" };

const required_world = createCairoEnum(rawNode.required_world, worldEnumMap, "Forest");
const required_golem = createCairoEnum(rawNode.required_golem, golemEnumMap, "Fire");
Expand Down Expand Up @@ -98,9 +98,11 @@ const toriiNodeToMission = (rawNode: RawMissionNode): Mission => {
if (statusObj.variant) {
if (statusObj.variant.Pending !== undefined) statusKey = "Pending";
else if (statusObj.variant.Completed !== undefined) statusKey = "Completed";
else if (statusObj.variant.Claimed !== undefined) statusKey = "Claimed";
}
else if (statusObj.Pending !== undefined) statusKey = "Pending";
else if (statusObj.Completed !== undefined) statusKey = "Completed";
else if (statusObj.Claimed !== undefined) statusKey = "Claimed";
}

mission.status = new CairoCustomEnum({ [statusKey]: statusKey });
Expand Down
Loading