react-native-ranking-leaderboard
Version:
Customizable leaderboard for react native mobile apps
483 lines (466 loc) • 14.9 kB
JavaScript
"use strict";
import { FlatList, StyleSheet, Text, View, Image, TouchableOpacity, TextInput } from 'react-native';
import { useState } from 'react';
import { LeaderboardProfile } from "./Profile.js";
import { SortingTypes } from "./SortingTypes.js";
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
export function Leaderboard({
entries,
style,
showPodium = true,
showSearchBar = true,
showSortingTypes = false,
showRankDifference = false,
enableProfiles = true,
customProfile
}) {
const [selectedUser, setSelectedUser] = useState(null);
const [modalVisible, setModalVisible] = useState(false);
// Search Bar
const [searchQuery, setSearchQuery] = useState('');
// Get the start and end of the week for a given date (Monday to Sunday)
function getWeekRange(date) {
const d = new Date(date);
const day = d.getDay();
const diff = day === 0 ? -6 : 1 - day; // lundi = jour 1
const start = new Date(d);
start.setDate(d.getDate() + diff);
start.setHours(0, 0, 0, 0);
const end = new Date(start);
end.setDate(start.getDate() + 6);
end.setHours(23, 59, 59, 999);
return {
start,
end
};
}
// Get the start and end of the month for a given date
function getMonthRange(date) {
const d = new Date(date);
const start = new Date(d.getFullYear(), d.getMonth(), 1);
start.setHours(0, 0, 0, 0);
const end = new Date(d.getFullYear(), d.getMonth() + 1, 0);
end.setHours(23, 59, 59, 999);
return {
start,
end
};
}
// Current and past dates for filtering
const now = new Date();
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const oneMonthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
// Past Ranking comparison
const {
start: startWeek,
end: endWeek
} = getWeekRange(oneWeekAgo);
const {
start: startMonth,
end: endMonth
} = getMonthRange(oneMonthAgo);
const {
start,
end
} = getWeekRange(now);
const currentWeekRange = filterByDateRange(entries, start, end);
const currentMonthRange = filterByDateRange(entries, start, end);
const lastWeekRange = filterByDateRange(entries, startWeek, endWeek);
const lastMonthRange = filterByDateRange(entries, startMonth, endMonth);
// Map: username → their previous rank (weekly)
const lastWeekRanks = {};
lastWeekRange.forEach((user, index) => {
lastWeekRanks[user.name] = index + 1;
});
// Map: username → their previous rank (monthly)
const lastMonthRanks = {};
lastMonthRange.forEach((user, index) => {
lastMonthRanks[user.name] = index + 1;
});
// Calculates rank change between current and past ranking
function getRankChange(user, period) {
const lastRanks = period === 'weekly' ? lastWeekRanks : lastMonthRanks;
const currentRange = period === 'weekly' ? currentWeekRange : currentMonthRange;
const lastRank = lastRanks[user.name] ?? currentRange.length + 1;
const currentRank = currentRange.findIndex(u => u.name === user.name) + 1 || currentRange.length + 1;
return currentRank - lastRank; // Positive = moved down, negative = moved up
}
// Sorting Feature
// Filters points by a date range and returns sorted leaderboard
function filterByDateRange(data, startDate, endDate) {
const filtered = data.map(user => {
const filteredScores = user.sorting?.filter(sorting => sorting.date >= startDate && sorting.date <= endDate);
const totalPoints = filteredScores?.reduce((sum, score) => sum + score.points, 0) ?? 0;
return {
...user,
points: totalPoints
};
}).filter(user => (user.points ?? 0) > 0).sort((a, b) => (b.points ?? 0) - (a.points ?? 0)); // Sort descending
// Recalculate ranks
return filtered.map((user, index) => ({
...user,
rank: index + 1,
rankDifference: 0
}));
}
// Handle sorting selection
const sortingPosition = style?.sortingPosition ?? 'bottom';
let displayedEntries = [];
const [sorting, setSorting] = useState('general');
const sortingTypes = ['weekly', 'monthly', 'general'];
// Depending on selected sorting type, update displayed entries
if (sorting === 'weekly') {
const {
start,
end
} = getWeekRange(now);
displayedEntries = filterByDateRange(entries, start, end);
} else if (sorting === 'monthly') {
const {
start,
end
} = getMonthRange(now);
displayedEntries = filterByDateRange(entries, start, end);
} else {
displayedEntries = entries.map((user, index) => ({
...user,
rank: index + 1,
rankDifference: 0
}));
}
// Search bar filter
const filteredEntries = displayedEntries.filter(entry => entry.name.toLowerCase().includes(searchQuery.toLowerCase()));
const podiumEntries = filteredEntries.slice(0, 3);
// Handle display of rest of the list (excluding podium if enabled)
const baseRestEntries = searchQuery.trim() === '' && showPodium ? filteredEntries.slice(3) : filteredEntries;
// Attach rank changes if sorting is weekly or monthly
const restEntries = sorting === 'weekly' || sorting === 'monthly' ? baseRestEntries.map(user => ({
...user,
rankDifference: getRankChange(user, sorting)
})) : baseRestEntries.map(user => ({
...user,
rankDifference: 0
}));
const isSearchActive = searchQuery.trim() !== '';
const rankOffset = showPodium && !isSearchActive ? 3 : 0;
// Handle empty state
if (!entries || entries.length === 0) {
return /*#__PURE__*/_jsx(View, {
style: [styles.container, style?.containerStyle, {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}],
children: /*#__PURE__*/_jsx(Text, {
style: styles.emptyText,
children: "No users yet \uD83D\uDE15"
})
});
}
return /*#__PURE__*/_jsxs(View, {
style: [styles.container, style?.containerStyle],
children: [showSearchBar && /*#__PURE__*/_jsx(TextInput, {
style: [styles.searchBar, style?.searchBarStyle],
placeholder: "Search users...",
placeholderTextColor: "#999",
value: searchQuery,
onChangeText: setSearchQuery
}), showSortingTypes && sortingPosition === 'top' && /*#__PURE__*/_jsx(SortingTypes, {
sortingTypes: sortingTypes,
sorting: sorting,
setSorting: setSorting,
showSortingTypes: showSortingTypes,
style: style,
styles: styles,
sortingPosition: "top"
}), showPodium && searchQuery.trim() === '' && podiumEntries.length >= 1 && /*#__PURE__*/_jsxs(View, {
style: styles.podiumContainer,
children: [podiumEntries[1] && /*#__PURE__*/_jsxs(TouchableOpacity, {
style: styles.podiumColumn,
onPress: () => {
if (enableProfiles) {
setSelectedUser(podiumEntries[1] ?? null);
setModalVisible(true);
}
},
children: [podiumEntries[1]?.picture && /*#__PURE__*/_jsx(Image, {
source: {
uri: podiumEntries[1]?.picture || ''
},
style: styles.avatar
}), /*#__PURE__*/_jsx(View, {
style: [styles.podiumBlock, styles.second, style?.podiumStyle?.second],
children: /*#__PURE__*/_jsx(Text, {
style: styles.podiumRank,
children: style?.podiumRankRenderer ? style?.podiumRankRenderer?.(2) : '2'
})
}), /*#__PURE__*/_jsx(Text, {
style: styles.playerName,
children: style?.podiumNameRenderer ? style?.podiumNameRenderer(podiumEntries[1]) : podiumEntries[1]?.name
})]
}), podiumEntries[0] && /*#__PURE__*/_jsxs(TouchableOpacity, {
style: styles.podiumColumn,
onPress: () => {
if (enableProfiles) {
setSelectedUser(podiumEntries[0] ?? null);
setModalVisible(true);
}
},
children: [podiumEntries[0]?.picture && /*#__PURE__*/_jsx(Image, {
source: {
uri: podiumEntries[0]?.picture || ''
},
style: [styles.avatar, styles.firstAvatar]
}), /*#__PURE__*/_jsx(View, {
style: [styles.podiumBlock, styles.first, style?.podiumStyle?.first],
children: /*#__PURE__*/_jsx(Text, {
style: styles.podiumRank,
children: style?.podiumRankRenderer ? style?.podiumRankRenderer?.(1) : '1'
})
}), /*#__PURE__*/_jsx(Text, {
style: styles.playerName,
children: style?.podiumNameRenderer ? style?.podiumNameRenderer(podiumEntries[0]) : podiumEntries[0]?.name
})]
}), podiumEntries[2] && /*#__PURE__*/_jsxs(TouchableOpacity, {
style: styles.podiumColumn,
onPress: () => {
if (enableProfiles) {
setSelectedUser(podiumEntries[2] ?? null);
setModalVisible(true);
}
},
children: [podiumEntries[2]?.picture && /*#__PURE__*/_jsx(Image, {
source: {
uri: podiumEntries[2]?.picture || ''
},
style: styles.avatar
}), /*#__PURE__*/_jsx(View, {
style: [styles.podiumBlock, styles.third, style?.podiumStyle?.third],
children: /*#__PURE__*/_jsx(Text, {
style: styles.podiumRank,
children: style?.podiumRankRenderer ? style?.podiumRankRenderer?.(3) : '3'
})
}), /*#__PURE__*/_jsx(Text, {
style: styles.playerName,
children: style?.podiumNameRenderer ? style?.podiumNameRenderer(podiumEntries[2]) : podiumEntries[2]?.name
})]
})]
}), /*#__PURE__*/_jsx(FlatList, {
data: restEntries,
keyExtractor: (_, index) => (index + 1 + rankOffset).toString(),
contentContainerStyle: styles.leaderboardList,
renderItem: ({
item
}) => /*#__PURE__*/_jsxs(TouchableOpacity, {
style: [styles.leaderboardItem, style?.itemStyle],
activeOpacity: 0.8,
onPress: () => {
if (enableProfiles) {
setSelectedUser(item);
setModalVisible(true);
}
},
children: [/*#__PURE__*/_jsx(Text, {
style: [styles.rank, style?.rankStyle],
children: item.rank
}), item.picture ? /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx(Image, {
source: {
uri: item.picture
},
style: [styles.itemAvatar, style?.avatarStyle]
}), /*#__PURE__*/_jsx(Text, {
style: [styles.itemName, style?.usernameStyle],
children: item.name
})]
}) : /*#__PURE__*/_jsx(Text, {
style: [styles.itemName, style?.usernameStyle],
children: item.name
}), /*#__PURE__*/_jsxs(View, {
style: styles.pointsAndRankDiffContainer,
children: [/*#__PURE__*/_jsx(Text, {
style: [styles.itemPoints, style?.pointStyle],
children: item.points
}), showRankDifference && (sorting === 'monthly' || sorting === 'weekly') && /*#__PURE__*/_jsx(Text, {
style: styles.rankDifference,
children: style?.rankDifferenceIcon ? style?.rankDifferenceIcon(item.rankDifference) : item.rankDifference > 0 ? `↑ ${item.rankDifference}` : item.rankDifference < 0 ? `↓ ${Math.abs(item.rankDifference)}` : '➖'
})]
})]
})
}), customProfile ? customProfile(selectedUser, () => {
setModalVisible(false);
setSelectedUser(null);
}) : /*#__PURE__*/_jsx(LeaderboardProfile, {
visible: modalVisible,
user: selectedUser,
onClose: () => {
setModalVisible(false);
setSelectedUser(null);
},
style: style?.profileStyle
}), showSortingTypes && sortingPosition === 'bottom' && /*#__PURE__*/_jsx(SortingTypes, {
sortingTypes: sortingTypes,
sorting: sorting,
setSorting: setSorting,
showSortingTypes: showSortingTypes,
style: style,
styles: styles,
sortingPosition: "bottom"
})]
});
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f4f6fc',
paddingVertical: 32
},
podiumContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'flex-end',
marginBottom: 24
},
podiumColumn: {
alignItems: 'center',
marginHorizontal: 12
},
avatar: {
width: 60,
height: 60,
borderRadius: 30,
marginBottom: 6,
borderWidth: 2,
borderColor: '#fff'
},
firstAvatar: {
width: 70,
height: 70,
borderRadius: 35
},
podiumBlock: {
width: 60,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 8,
shadowColor: '#000',
shadowOpacity: 0.15,
shadowOffset: {
width: 0,
height: 2
},
shadowRadius: 4,
elevation: 5,
marginBottom: 6
},
first: {
height: 100,
backgroundColor: 'gold'
},
second: {
height: 80,
backgroundColor: 'silver'
},
third: {
height: 60,
backgroundColor: '#cd7f32'
},
podiumRank: {
color: '#fff',
fontWeight: 'bold',
fontSize: 20
},
playerName: {
fontSize: 14,
fontWeight: '600',
color: '#333',
textAlign: 'center',
maxWidth: 80
},
leaderboardList: {
paddingHorizontal: 16
},
leaderboardItem: {
backgroundColor: '#fff',
flexDirection: 'row',
alignItems: 'center',
padding: 12,
marginBottom: 10,
borderRadius: 10,
elevation: 2
},
rank: {
width: 28,
textAlign: 'center',
fontWeight: 'bold',
color: '#2c3e50'
},
itemAvatar: {
width: 36,
height: 36,
borderRadius: 18,
marginHorizontal: 10
},
itemName: {
flex: 1,
fontSize: 16,
color: '#2c3e50'
},
itemPoints: {
fontSize: 16,
marginHorizontal: 10,
textAlign: 'right',
color: '#2c3e50'
},
emptyText: {
fontSize: 16,
color: 'black',
textAlign: 'center',
marginTop: 40
},
searchBar: {
backgroundColor: '#fff',
paddingHorizontal: 12,
paddingVertical: 8,
marginHorizontal: 16,
marginBottom: 16,
borderRadius: 8,
fontSize: 16,
elevation: 2
},
sortingTypesContainer: {
flexDirection: 'row',
justifyContent: 'space-around'
},
sortingText: {
fontSize: 14,
fontWeight: 600,
textTransform: 'capitalize'
},
sortingButton: {
backgroundColor: '#dfe6f3',
paddingVertical: 8,
paddingHorizontal: 12,
borderRadius: 20,
marginHorizontal: 4,
elevation: 1
},
sortingTextActive: {
color: '#fff'
},
sortingButtonActive: {
backgroundColor: '#2c3e50'
},
pointsAndRankDiffContainer: {
flexDirection: 'row'
},
rankDifference: {
width: 40,
paddingLeft: 18,
textAlign: 'left',
fontSize: 14,
color: '#888',
fontWeight: '500'
}
});
//# sourceMappingURL=index.js.map