diff --git a/src/pages/FactionPage.tsx b/src/pages/FactionPage.tsx index 33c9158..865c10c 100644 --- a/src/pages/FactionPage.tsx +++ b/src/pages/FactionPage.tsx @@ -1,21 +1,29 @@ -import React, { useEffect, useState } from 'react'; -import { useWallet } from '../hooks/useWallet'; -import { useNavigate } from 'react-router-dom'; -import { getFactionOptions, FactionOptions, setFaction, purchaseAccess, TokenOption, getTotalOfferings, OfferingStats, getUserOfferings } from '../utils/aoHelpers'; -import { currentTheme } from '../constants/theme'; -import { Gateway, ACTIVITY_POINTS } from '../constants/Constants'; -import PurchaseModal from '../components/PurchaseModal'; -import CheckInButton from '../components/CheckInButton'; -import Header from '../components/Header'; -import Confetti from 'react-confetti'; -import LoadingAnimation from '../components/LoadingAnimation'; -import Footer from '../components/Footer'; +import React, { useEffect, useState, useMemo } from "react"; +import { useWallet } from "../hooks/useWallet"; +import { useNavigate } from "react-router-dom"; +import { + getFactionOptions, + FactionOptions, + setFaction, + purchaseAccess, + TokenOption, + getTotalOfferings, + OfferingStats, + getUserOfferings, +} from "../utils/aoHelpers"; +import { currentTheme } from "../constants/theme"; +import { Gateway, ACTIVITY_POINTS } from "../constants/Constants"; +import PurchaseModal from "../components/PurchaseModal"; +import Header from "../components/Header"; +import Confetti from "react-confetti"; +import LoadingAnimation from "../components/LoadingAnimation"; +import Footer from "../components/Footer"; const FACTION_TO_PATH = { - 'Sky Nomads': 'air', - 'Aqua Guardians': 'water', - 'Inferno Blades': 'fire', - 'Stone Titans': 'rock' + "Sky Nomads": "air", + "Aqua Guardians": "water", + "Inferno Blades": "fire", + "Stone Titans": "rock", }; interface OfferingData { @@ -26,68 +34,37 @@ interface OfferingData { // Type guard function to check if a value is an OfferingData object const isOfferingData = (value: unknown): value is OfferingData => { - return typeof value === 'object' && - value !== null && - 'LastOffering' in value && - 'IndividualOfferings' in value && - 'Streak' in value; + return ( + typeof value === "object" && + value !== null && + "LastOffering" in value && + "IndividualOfferings" in value && + "Streak" in value + ); }; export const FactionPage: React.FC = () => { const navigate = useNavigate(); - const { wallet, walletStatus, darkMode, connectWallet, setDarkMode, refreshTrigger, triggerRefresh } = useWallet(); + const { + wallet, + walletStatus, + darkMode, + connectWallet, + setDarkMode, + refreshTrigger, + triggerRefresh, + } = useWallet(); const [factions, setFactions] = useState([]); const [isPurchaseModalOpen, setIsPurchaseModalOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const [isInitialLoad, setIsInitialLoad] = useState(true); const [showConfetti, setShowConfetti] = useState(false); - const [offeringStats, setOfferingStats] = useState(null); + const [offeringStats, setOfferingStats] = useState( + null + ); const [userOfferings, setUserOfferings] = useState(null); - const [nextOfferingTime, setNextOfferingTime] = useState(''); const theme = currentTheme(darkMode); - - useEffect(() => { - const updateNextOfferingTime = () => { - if (!userOfferings?.LastOffering) { - const now = new Date(); - const midnight = new Date(); - midnight.setUTCHours(24, 0, 0, 0); - - if (midnight.getTime() <= now.getTime()) { - midnight.setUTCDate(midnight.getUTCDate() + 1); - } - - const hours = Math.floor((midnight.getTime() - now.getTime()) / (1000 * 60 * 60)); - const minutes = Math.floor(((midnight.getTime() - now.getTime()) % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor(((midnight.getTime() - now.getTime()) % (1000 * 60)) / 1000); - - setNextOfferingTime(`${hours}h ${minutes}m ${seconds}s`); - } else { - const lastOffering = new Date(userOfferings.LastOffering * 1000); - const nextOffering = new Date(lastOffering); - nextOffering.setUTCDate(nextOffering.getUTCDate() + 1); - nextOffering.setUTCHours(0, 0, 0, 0); - - const now = new Date(); - const diff = nextOffering.getTime() - now.getTime(); - - if (diff <= 0) { - setNextOfferingTime(''); - return; - } - - const hours = Math.floor(diff / (1000 * 60 * 60)); - const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((diff % (1000 * 60)) / 1000); - - setNextOfferingTime(`${hours}h ${minutes}m ${seconds}s`); - } - }; - - updateNextOfferingTime(); - const interval = setInterval(updateNextOfferingTime, 1000); - return () => clearInterval(interval); - }, [userOfferings?.LastOffering]); + const [isInfoModalOpen, setIsInfoModalOpen] = useState(false); // Function to load data const loadAllData = async () => { @@ -100,10 +77,11 @@ export const FactionPage: React.FC = () => { } try { + setIsLoading(true); const [factionData, totalStats, userStats] = await Promise.all([ getFactionOptions(wallet), getTotalOfferings(), - getUserOfferings(wallet.address) + getUserOfferings(wallet.address), ]); if (factionData) setFactions(factionData); @@ -114,9 +92,10 @@ export const FactionPage: React.FC = () => { setUserOfferings(null); } } catch (error) { - console.error('Error loading faction data:', error); + console.error("Error loading faction data:", error); } finally { setIsInitialLoad(false); + setIsLoading(false); } }; @@ -125,17 +104,24 @@ export const FactionPage: React.FC = () => { loadAllData(); }, [wallet?.address, refreshTrigger]); + // Handle faction info modal + useEffect(() => { + if (!walletStatus?.faction && walletStatus?.isUnlocked) { + setIsInfoModalOpen(true); + } + }, [walletStatus]); + const handleJoinFaction = async (factionName: string) => { if (!wallet) { - console.error('Wallet not connected'); + console.error("Wallet not connected"); return; } - + try { setIsLoading(true); await setFaction(wallet, factionName, walletStatus, triggerRefresh); } catch (error) { - console.error('Error joining faction:', error); + console.error("Error joining faction:", error); } finally { setIsLoading(false); } @@ -154,286 +140,683 @@ export const FactionPage: React.FC = () => { setShowConfetti(false); }, 5000); } catch (error) { - console.error('Purchase failed:', error); + console.error("Purchase failed:", error); throw error; } }; // Calculate total points for a faction const calculateFactionPoints = (faction: FactionOptions) => { - const offeringPoints = Number(offeringStats?.[faction.name as keyof OfferingStats] || 0) * ACTIVITY_POINTS.OFFERING; - const feedPoints = Number(faction.totalTimesFed || 0) * ACTIVITY_POINTS.FEED; - const playPoints = Number(faction.totalTimesPlay || 0) * ACTIVITY_POINTS.PLAY; - const missionPoints = Number(faction.totalTimesMission || 0) * ACTIVITY_POINTS.MISSION; + const offeringPoints = + Number(offeringStats?.[faction.name as keyof OfferingStats] || 0) * + ACTIVITY_POINTS.OFFERING; + const feedPoints = + Number(faction.totalTimesFed || 0) * ACTIVITY_POINTS.FEED; + const playPoints = + Number(faction.totalTimesPlay || 0) * ACTIVITY_POINTS.PLAY; + const missionPoints = + Number(faction.totalTimesMission || 0) * ACTIVITY_POINTS.MISSION; return offeringPoints + feedPoints + playPoints + missionPoints; }; + // Memoize the sorted factions list to ensure ranks are correct and improve performance + const sortedFactions = useMemo(() => { + if (!factions.length || !offeringStats) return []; + return [...factions].sort( + (a, b) => calculateFactionPoints(b) - calculateFactionPoints(a) + ); + }, [factions, offeringStats]); + // Calculate user's total points const calculateUserPoints = () => { - const offeringPoints = Number(userOfferings?.IndividualOfferings || 0) * ACTIVITY_POINTS.OFFERING; - const feedPoints = Number(walletStatus?.monster?.totalTimesFed || 0) * ACTIVITY_POINTS.FEED; - const playPoints = Number(walletStatus?.monster?.totalTimesPlay || 0) * ACTIVITY_POINTS.PLAY; - const missionPoints = Number(walletStatus?.monster?.totalTimesMission || 0) * ACTIVITY_POINTS.MISSION; + const offeringPoints = + Number(userOfferings?.IndividualOfferings || 0) * + ACTIVITY_POINTS.OFFERING; + const feedPoints = + Number(walletStatus?.monster?.totalTimesFed || 0) * ACTIVITY_POINTS.FEED; + const playPoints = + Number(walletStatus?.monster?.totalTimesPlay || 0) * ACTIVITY_POINTS.PLAY; + const missionPoints = + Number(walletStatus?.monster?.totalTimesMission || 0) * + ACTIVITY_POINTS.MISSION; return offeringPoints + feedPoints + playPoints + missionPoints; }; - const currentFaction = factions.find(f => f.name === walletStatus?.faction); + const currentFaction = sortedFactions.find( + (f) => f.name === walletStatus?.faction + ); return ( -
-
-
+ {/* Header */} +
+
+
+ + {showConfetti && ( + - - {showConfetti && ( - + )} + + {isInfoModalOpen && ( +
+
+

+ Picking Your Faction +

+
+

+ Important: Faction selection is final - Team players only, no + team quitting! +

+
+

+ Rewards Distribution:{" "} + Faction rewards are split among all faction members - being in + the biggest faction may not be the best strategy. +

+

+ Activity Matters: The + most active members will receive additional rewards, while + non-active members will receive no rewards. +

+

+ Reward Sources: Rewards + come from multiple sources: +

+
    +
  • Partnerships
  • +
  • Premium pass sales revenue
  • +
  • In-game revenue
  • +
  • Funds raised
  • +
  • Profits from staking
  • +
+
+
+ +
+
+
+
+ )} + + setIsPurchaseModalOpen(false)} + onPurchase={handlePurchase} + contractName="Eternal Pass" + /> + + {/* MAIN CONTENT */} +
+ {isLoading && ( +
+ +
)} - setIsPurchaseModalOpen(false)} - onPurchase={handlePurchase} - contractName="Eternal Pass" - /> + {walletStatus?.faction && currentFaction && ( +
+
+ {/* LEFT - USER FACTION INFO */} +
+

+ Your Faction +

+ + {/* Faction Hero Section */} +
+
+ {/* Image */} +
+ {currentFaction.mascot && ( + {`${currentFaction.name} + )} +
+ + {/* Teks */} +
+
+

+ {currentFaction.name} +

+ {currentFaction.perks && ( +

+ {currentFaction.perks[0]} +

+ )} +
-
- {/* Header Section */} -
-

Factions

- {!walletStatus?.faction && walletStatus?.isUnlocked && ( -
-

Picking Your Faction

-
-

- Important: Faction selection is final - Team players only, no team quitting! -

-
-

- Rewards Distribution: Faction rewards are split among all faction members - being in the biggest faction may not be the best strategy. -

-

- Activity Matters: The most active members will receive additional rewards, while non-active members will receive no rewards. -

-

- Reward Sources: Rewards come from multiple sources: -

-
    -
  • Partnerships
  • -
  • Premium pass sales revenue
  • -
  • In-game revenue
  • -
  • Funds raised
  • -
  • Profits from staking
  • -
+ {/* Mini Stats */} +
+
+

+ Members +

+

+ {Number(currentFaction.memberCount || 0)} +

+
+
+

+ Avg Level +

+

+ {currentFaction.averageLevel?.toFixed(1)} +

+
+
+

+ Points +

+

+ {calculateFactionPoints(currentFaction)} +

+
+
+

+ Rank +

+

+ # + {sortedFactions.findIndex( + (f) => f.name === currentFaction.name + ) + 1} +

+
+
+
-
- )} - {walletStatus?.faction && currentFaction && ( -
-
-
- {currentFaction.mascot && ( - {`${currentFaction.name} - )} -
-

Your Faction

-

{currentFaction.name}

- {currentFaction.perks && ( -
-

{currentFaction.perks[0]}

+ + {/* User Stats Section */} +
+

+ Your Contribution +

+
+
+
+
+ Offerings + + {userOfferings?.IndividualOfferings || 0} +
- )} +
+
+
+
+ +
+
+ Times Fed + + {walletStatus?.monster?.totalTimesFed || 0} + +
+
+
+
+
+
+ +
+
+
+ Times Played + + {walletStatus?.monster?.totalTimesPlay || 0} + +
+
+
+
+
+ +
+
+ Missions + + {walletStatus?.monster?.totalTimesMission || 0} + +
+
+
+
+
+
+ + {/* Total Points */} +
+
+ + Total Points + + + {calculateUserPoints()} + +
- -
-
-

Daily Offerings

-

- Offer praise to the altar of your team once daily. Build streaks to earn RUNE rewards - consistency is key! -

-
-
-
-
- Your Offerings: - {userOfferings?.IndividualOfferings || 0} -
-
- Times Fed: - {walletStatus?.monster?.totalTimesFed || 0} -
-
- Times Played: - {walletStatus?.monster?.totalTimesPlay || 0} -
-
- Missions: - {walletStatus?.monster?.totalTimesMission || 0} -
-
- Total Points: - {calculateUserPoints()} +
+
+ + {/* RIGHT - FACTION LIST */} +
+
+

+ Faction Rankings +

+ + {sortedFactions.length - 1} opponents + +
+ +
+ {sortedFactions + .filter((f) => f.name !== walletStatus?.faction) + .map((faction) => ( +
+ navigate( + `/factions/${ + FACTION_TO_PATH[ + faction.name as keyof typeof FACTION_TO_PATH + ] + }` + ) + } + className={`p-4 rounded-xl ${theme.container} border ${theme.border} backdrop-blur-md hover:scale-[1.01] hover:shadow-lg cursor-pointer transition-all duration-200 mb-4`} + > +
+ {/* Image */} + {faction.mascot && ( +
+ {`${faction.name}
-
-
-
- Avg Level: - {currentFaction.averageLevel ? Math.round(currentFaction.averageLevel * 10) / 10 : 0} + )} + +
+
+

+ {faction.name} +

+ + # + {sortedFactions.findIndex( + (f) => f.name === faction.name + ) + 1} +
-
-
Offering: {ACTIVITY_POINTS.OFFERING}pts
-
Feed: {ACTIVITY_POINTS.FEED}pt
-
Play: {ACTIVITY_POINTS.PLAY}pts
-
Mission: {ACTIVITY_POINTS.MISSION}pts
+ + {/* Stats Grid */} +
+
+
+ Members: + + {faction.memberCount} + +
+
+ +
+
+ Avg Level: + + {faction.averageLevel?.toFixed(1)} + +
+
+ +
+
+ Points: + + {calculateFactionPoints(faction)} + +
+
+ +
+
+ Offerings: + + {offeringStats?.[ + faction.name as keyof OfferingStats + ] || 0} + +
+
+ +
+
+ Fed: + + {faction.totalTimesFed || 0} + +
+
+ +
+
+ Played: + + {faction.totalTimesPlay || 0} + +
+
+ +
+
+ Missions: + + {faction.totalTimesMission || 0} + +
+
+ +
+
+ Activity: + + {(() => { + const members = Number( + faction.memberCount || 0 + ); + const activity = Number( + (faction as any).totalActivity || 0 + ); + return members > 0 + ? Math.round(activity / members) + : 0; + })()} + +
+
+ + {/* Perks */} + {faction.perks && faction.perks.length > 0 && ( +
+

+ Faction Perks: +

+

+ {faction.perks[0]} +

+
+ )}
-
- - {nextOfferingTime && ( -
- Next offering in: {nextOfferingTime} -
- )} -
-
-
+ ))}
- )} +
+ )} + + {isInitialLoad && !sortedFactions.length ? ( +
+ +
+ ) : ( + !walletStatus?.faction && + sortedFactions.length > 0 && ( +
+
+

+ Choose Your Faction +

+ +
+ {sortedFactions.map((faction) => ( +
+ navigate( + `/factions/${ + FACTION_TO_PATH[ + faction.name as keyof typeof FACTION_TO_PATH + ] + }` + ) + } + className={`flex flex-col h-full rounded-xl ${theme.container} border ${theme.border} backdrop-blur-md hover:scale-[1.02] hover:shadow-lg transition-all duration-300 cursor-pointer overflow-hidden`} + > + {/* Image */} +
+ {faction.mascot && ( + {`${faction.name} + )} +
+ +
+
+

+ {faction.name} +

+ + # + {sortedFactions.findIndex( + (f) => f.name === faction.name + ) + 1} + +
- {/* Loading State or Content */} - {isInitialLoad && !factions.length ? ( -
- -
- ) : factions.length > 0 && ( -
- {factions.map((faction) => ( -
navigate(`/factions/${FACTION_TO_PATH[faction.name as keyof typeof FACTION_TO_PATH]}`)} - className={`flex flex-col h-full p-2.5 rounded-xl ${theme.container} border ${theme.border} backdrop-blur-md transform transition-all duration-300 hover:scale-[1.02] hover:shadow-xl min-h-[480px] cursor-pointer`} - > -
- {faction.mascot && ( - {`${faction.name} - )} -
-
-

{faction.name}

-
-
{faction.perks && ( -
    +
      {faction.perks.map((perk, index) => ( -
    • - +
    • + + • + {perk}
    • ))}
    )} -
-
-
-
- Members: - {faction.memberCount} -
-
- Monsters: - {faction.monsterCount} -
-
- Avg Level: - {faction.averageLevel ? Math.round(faction.averageLevel * 10) / 10 : 0} -
-
- Offerings: - - {offeringStats?.[faction.name as keyof OfferingStats] || 0} + + {/* Stats Grid */} +
+
+
+ Members: + + {Number(faction.memberCount || 0)}
-
-
- Times Fed: - {faction.totalTimesFed || 0} -
-
- Times Played: - {faction.totalTimesPlay || 0} + +
+
+ Level: + + {faction.averageLevel?.toFixed(1)} +
-
- Missions: - {faction.totalTimesMission || 0} +
+ +
+
+ Points: + + {calculateFactionPoints(faction)} +
-
- Points: - {calculateFactionPoints(faction)} +
+ +
+
+ Offerings: + + {offeringStats?.[ + faction.name as keyof OfferingStats + ] || 0} +
+
+ +
+ {!walletStatus?.isUnlocked ? ( + + ) : ( + !walletStatus?.faction && ( + + ) + )} +
-
-
- {!walletStatus?.isUnlocked ? ( - - ) : !walletStatus?.faction && ( - - )} - {walletStatus?.faction === faction.name && ( -
- Current Faction -
- )} -
+ ))}
- ))} +
- )} -
+ ) + )} +
+ + {/* Footer */} +
+ +
); };