5180 fichiers 38.5 Mo 0 téléchargements 267 vues mis à jour il y a 5 heures ui
nitro-v36-with-renderer-v36 / src / components / badge-leaderboard / BadgeLeaderboardView.tsx
BadgeLeaderboardView.tsx 16.6 Ko Télécharger le fichier
import { AddLinkEventTracker, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker } from '@octane/renderer';
import { CSSProperties, FC, useEffect, useMemo, useState } from 'react';
import { BadgeLeaderboardBoard, BadgeLeaderboardEntry, BadgeRarityKey, fetchBadgeLeaderboard, getCachedBadgeLeaderboard, LocalizeText } from '../../api';
import {
    badgeEmblemAchievement,
    badgeEmblemCommon,
    badgeEmblemDefault,
    badgeEmblemEpic,
    badgeEmblemLegendary,
    badgeEmblemMythical,
    badgeEmblemRare,
    badgeEmblemUnique,
    frameLeaderboardAchievement,
    frameLeaderboardRarityCommon,
    frameLeaderboardRarityEpic,
    frameLeaderboardRarityLegendary,
    frameLeaderboardRarityMythical,
    frameLeaderboardRarityRare,
    frameLeaderboardRarityUnique,
    frameLeaderboardTotal,
    leaderboardButtonCloseSwf,
    leaderboardDivider,
    leaderboardDropdownOpener
} from '../../assets/images/leaderboard_badge';
import { Column, DraggableWindow, DraggableWindowPosition, Flex, LayoutAvatarImageView, ListSelect, Text } from '../../common';
import { BaseButton } from '../../common/BaseButton';
import { NativeTextTree } from '../../common/native-text/NativeTextTree';

type LeaderboardPage =
    | { key: 'totalBadges'; board: BadgeLeaderboardBoard; frame: string; emblem: string; title: () => string; info: () => string; option: () => string }
    | { key: 'achievementLevel'; board: BadgeLeaderboardBoard; frame: string; emblem: string; title: () => string; info: () => string; option: () => string }
    | {
          key: `rarity-${BadgeRarityKey}`;
          rarity: BadgeRarityKey;
          board: BadgeLeaderboardBoard;
          frame: string;
          emblem: string;
          title: () => string;
          info: () => string;
          option: () => string;
      };

const RARITY_ASSETS: Record<BadgeRarityKey, { frame: string; emblem: string }> = {
    common: { frame: frameLeaderboardRarityCommon, emblem: badgeEmblemCommon },
    rare: { frame: frameLeaderboardRarityRare, emblem: badgeEmblemRare },
    epic: { frame: frameLeaderboardRarityEpic, emblem: badgeEmblemEpic },
    legendary: { frame: frameLeaderboardRarityLegendary, emblem: badgeEmblemLegendary },
    mythical: { frame: frameLeaderboardRarityMythical, emblem: badgeEmblemMythical },
    unique: { frame: frameLeaderboardRarityUnique, emblem: badgeEmblemUnique }
};

const RARITY_ORDER: BadgeRarityKey[] = ['common', 'rare', 'epic', 'legendary', 'mythical', 'unique'];
const PAGE_SIZE = 10;

export const BadgeLeaderboardView: FC<{}> = (props) => {
    const [isVisible, setIsVisible] = useState(false);
    const [isLoading, setIsLoading] = useState(false);
    const [loadError, setLoadError] = useState<string>(null);
    const [version, setVersion] = useState(0);
    const [categoryIndex, setCategoryIndex] = useState(0);
    const [entryPageIndex, setEntryPageIndex] = useState(0);

    useEffect(() => {
        const linkTracker: ILinkEventTracker = {
            linkReceived: (url: string) => {
                const parts = url.split('/');

                if (parts.length < 2) return;

                switch (parts[1]) {
                    case 'show':
                        setIsVisible(true);
                        return;
                    case 'hide':
                        setIsVisible(false);
                        return;
                    case 'toggle':
                        setIsVisible((value) => !value);
                        return;
                    case 'refresh':
                        setVersion((value) => value + 1);
                        return;
                }
            },
            eventUrlPrefix: 'badge-leaderboard/'
        };

        AddLinkEventTracker(linkTracker);

        return () => RemoveLinkEventTracker(linkTracker);
    }, []);

    useEffect(() => {
        if (!isVisible) return;

        let cancelled = false;

        setIsLoading(true);
        setLoadError(null);

        fetchBadgeLeaderboard(version > 0)
            .then(() => {
                if (cancelled) return;

                setIsLoading(false);
            })
            .catch((error) => {
                if (cancelled) return;

                setLoadError(String((error as Error)?.message || error));
                setIsLoading(false);
            });

        return () => {
            cancelled = true;
        };
    }, [isVisible, version]);

    const leaderboard = getCachedBadgeLeaderboard();

    const pages = useMemo<LeaderboardPage[]>(() => {
        if (!leaderboard) return [];

        const built: LeaderboardPage[] = [
            {
                key: 'totalBadges',
                board: leaderboard.leaderboards.totalBadges,
                frame: frameLeaderboardTotal,
                emblem: badgeEmblemDefault,
                title: () => LocalizeText('badge_leaderboard.title.total_badges'),
                info: () => LocalizeText('badge_leaderboard.info.total_badges'),
                option: () => LocalizeText('badge_leaderboard.option.total_badges')
            },
            {
                key: 'achievementLevel',
                board: leaderboard.leaderboards.achievementLevel,
                frame: frameLeaderboardAchievement,
                emblem: badgeEmblemAchievement,
                title: () => LocalizeText('badge_leaderboard.title.achievement_level'),
                info: () => LocalizeText('badge_leaderboard.info.achievement_level'),
                option: () => LocalizeText('badge_leaderboard.option.achievement_level')
            }
        ];

        for (const rarity of RARITY_ORDER) {
            const board = leaderboard.leaderboards.rarity?.[rarity];

            if (!board?.totalPlayers) continue;

            const assets = RARITY_ASSETS[rarity];
            const rarityText = LocalizeText(`badge.rarity.${rarity}`);

            built.push({
                key: `rarity-${rarity}`,
                rarity,
                board,
                frame: assets.frame,
                emblem: assets.emblem,
                title: () => LocalizeText('badge_leaderboard.title.rarity', ['rarity'], [rarityText]),
                info: () => LocalizeText(`badge_leaderboard.info.rarity.${rarity}`),
                option: () => LocalizeText('badge_leaderboard.option.rarity', ['rarity'], [rarityText])
            });
        }

        return built;
    }, [leaderboard]);

    useEffect(() => {
        if (!pages.length) return;
        if (categoryIndex < pages.length) return;

        setCategoryIndex(0);
    }, [categoryIndex, pages.length]);

    useEffect(() => setEntryPageIndex(0), [categoryIndex]);

    const currentPage = pages[categoryIndex] || null;
    const allEntries = currentPage?.board?.entries || [];
    const viewerEntry = useMemo(() => {
        const fromBoard = currentPage?.board?.viewerEntry as BadgeLeaderboardEntry;

        if (fromBoard?.userId) return fromBoard;
        if (!leaderboard?.viewerUserId) return null;

        const session = GetSessionDataManager();

        if (!session || session.userId !== leaderboard.viewerUserId) return null;

        return {
            userId: session.userId,
            username: session.userName || '',
            figure: session.figure || '',
            score: 0,
            rank: 0
        } as BadgeLeaderboardEntry;
    }, [currentPage?.board?.viewerEntry, leaderboard?.viewerUserId]);
    // The viewer keeps their place in the ranking and is highlighted there; the extra row at the bottom shows them only when
    // they are not on the page in view.
    const rankedTotalPlayers = Math.max(currentPage?.board?.totalPlayers || 0, allEntries.length);
    const totalEntryPages = Math.max(1, Math.ceil(rankedTotalPlayers / PAGE_SIZE));
    const clampedEntryPageIndex = Math.min(entryPageIndex, totalEntryPages - 1);
    const pageStart = clampedEntryPageIndex * PAGE_SIZE;
    const pageEntries = allEntries.slice(pageStart, pageStart + PAGE_SIZE);
    const isViewer = (entry: BadgeLeaderboardEntry) => !!viewerEntry?.userId && entry.userId === viewerEntry.userId;
    const showViewerEntry = !!viewerEntry?.userId && !pageEntries.some(isViewer);

    if (!isVisible) return null;

    return (
        <NativeTextTree>
            <div className="octane-badge-leaderboard fixed inset-0 z-[100] flex items-center justify-center pointer-events-none">
                <DraggableWindow
                    uniqueKey="badge-leaderboard"
                    handleSelector=".octane-badge-leaderboard__drag-handle"
                    windowPosition={DraggableWindowPosition.CENTER}
                >
                    <div
                        className="octane-badge-leaderboard__window pointer-events-auto"
                        style={{ '--badge-leaderboard-frame': `url(${currentPage?.frame || frameLeaderboardTotal})` } as CSSProperties}
                    >
                        <div className="octane-badge-leaderboard__frame" aria-hidden="true" />
                        <div className="octane-badge-leaderboard__drag-handle" />
                        <BaseButton
                            className="octane-badge-leaderboard__close"
                            type="button"
                            onPointerDown={(event) => event.stopPropagation()}
                            onClick={() => setIsVisible(false)}
                            aria-label="Close"
                        >
                            <span className="octane-badge-leaderboard__close-icon" style={{ backgroundImage: `url(${leaderboardButtonCloseSwf})` }} />
                        </BaseButton>
                        <div className="octane-badge-leaderboard__header">
                            <div onPointerDown={(event) => event.stopPropagation()}>
                                <ListSelect
                                    aria-label={LocalizeText('badge_leaderboard.title.total_badges')}
                                    itemClassName="octane-badge-leaderboard__category-option"
                                    options={pages.map((page) => ({ value: page.key, label: page.option() }))}
                                    popupClassName="octane-badge-leaderboard__category-menu"
                                    positionerClassName="octane-badge-leaderboard__category-positioner"
                                    renderTrigger={() => (
                                        <>
                                            <Text className="octane-badge-leaderboard__header-title">
                                                {currentPage?.title() || LocalizeText('badge_leaderboard.title.total_badges')}
                                            </Text>
                                            <img className="octane-badge-leaderboard__header-arrow" src={leaderboardDropdownOpener} alt="" />
                                        </>
                                    )}
                                    side="bottom"
                                    sideOffset={-2}
                                    triggerClassName="octane-badge-leaderboard__category-button"
                                    value={currentPage?.key ?? ''}
                                    align="center"
                                    onValueChange={(key) => {
                                        const index = pages.findIndex((page) => page.key === key);

                                        if (index >= 0) setCategoryIndex(index);
                                    }}
                                />
                            </div>
                        </div>
                        <div className="octane-badge-leaderboard__content">
                            {isLoading && !leaderboard && <div className="octane-badge-leaderboard__state">{LocalizeText('generic.loading')}</div>}
                            {loadError && !leaderboard && <div className="octane-badge-leaderboard__state octane-badge-leaderboard__state--error">{loadError}</div>}
                            {currentPage && (
                                <>
                                    <div className="octane-badge-leaderboard__info-card">
                                        <img className="octane-badge-leaderboard__info-icon" src={currentPage.emblem} alt="" />
                                        <Text className="octane-badge-leaderboard__info-text" small wrap>
                                            {currentPage.info()}
                                        </Text>
                                    </div>
                                    <div className="octane-badge-leaderboard__list">
                                        {pageEntries.map((entry, index) => (
                                            <LeaderboardRow
                                                key={`${currentPage.key}-${entry.userId}`}
                                                entry={entry}
                                                emblem={currentPage.emblem}
                                                rowIndex={pageStart + index}
                                                isCurrentUser={isViewer(entry)}
                                            />
                                        ))}
                                        {showViewerEntry && (
                                            <LeaderboardRow entry={viewerEntry} emblem={currentPage.emblem} rowIndex={pageStart + pageEntries.length} isCurrentUser={true} />
                                        )}
                                    </div>
                                    <img className="octane-badge-leaderboard__divider" src={leaderboardDivider} alt="" />
                                    <Flex className="octane-badge-leaderboard__footer" justifyContent="between" alignItems="center">
                                        <BaseButton
                                            className="octane-badge-leaderboard__nav-button is-previous"
                                            disabled={clampedEntryPageIndex <= 0}
                                            onClick={() => setEntryPageIndex((value) => Math.max(0, value - 1))}
                                        >
                                            {LocalizeText('badge_leaderboard.previous')}
                                        </BaseButton>
                                        <Column gap={0} alignItems="center">
                                            <Text small bold>
                                                {currentPage.option()}
                                            </Text>
                                            <Text className="opacity-70" small>{`${clampedEntryPageIndex + 1} / ${totalEntryPages}`}</Text>
                                        </Column>
                                        <BaseButton
                                            className="octane-badge-leaderboard__nav-button is-next"
                                            disabled={clampedEntryPageIndex >= totalEntryPages - 1}
                                            onClick={() => setEntryPageIndex((value) => Math.min(totalEntryPages - 1, value + 1))}
                                        >
                                            {LocalizeText('badge_leaderboard.next')}
                                        </BaseButton>
                                    </Flex>
                                </>
                            )}
                        </div>
                    </div>
                </DraggableWindow>
            </div>
        </NativeTextTree>
    );
};

interface LeaderboardRowProps {
    entry: BadgeLeaderboardEntry;
    emblem: string;
    rowIndex?: number;
    isCurrentUser?: boolean;
}

const LeaderboardRow: FC<LeaderboardRowProps> = (props) => {
    const { entry = null, emblem = null, rowIndex = 0, isCurrentUser = false } = props;

    if (!entry) return null;

    const rankClassName = entry.rank === 1 ? 'is-rank-1' : entry.rank === 2 ? 'is-rank-2' : entry.rank === 3 ? 'is-rank-3' : '';

    return (
        <NativeTextTree>
            <div className={`octane-badge-leaderboard__row ${isCurrentUser ? 'is-current-user' : ''} ${rowIndex % 2 === 0 ? 'is-even' : 'is-odd'}`}>
                <div className={`octane-badge-leaderboard__rank ${rankClassName}`}>{entry.rank}</div>
                <div className="octane-badge-leaderboard__avatar">
                    <LayoutAvatarImageView figure={entry.figure} headOnly direction={2} />
                </div>
                <Text className="octane-badge-leaderboard__username" bold>
                    {entry.username}
                </Text>
                <Text className="octane-badge-leaderboard__score" bold>
                    {entry.score}
                </Text>
                <img className="octane-badge-leaderboard__row-emblem" src={emblem} alt="" />
            </div>
        </NativeTextTree>
    );
};
15Sujets
1Dépôts
20Membres
2En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser