Overview
The Ranking system (app/ranking.tsx) displays the top 20 ParkIn users based on their XP (experience points). It creates healthy competition and motivates users to engage more with the platform.
Leaderboard Display
Top 20 Users
The leaderboard queries Firebase to show the highest XP earners:
const fetchRanking = async () => {
const q = query (
collection ( db , "users" ),
where ( "role" , "==" , "client" ),
orderBy ( "xp" , "desc" ),
limit ( 20 )
);
const snap = await getDocs ( q );
setLeaders ( snap . docs . map (( d , index ) => ({
id: d . id ,
rank: index + 1 ,
... d . data ()
})));
};
This query requires a Firebase composite index on role and xp (descending). The console will provide the index creation link if missing.
Ranking Card Layout
Each leaderboard entry shows:
Rank Number Position #1-20 (top 3 highlighted)
Avatar User’s profile picture via SmartAvatar
User Info Display name and total XP
Trophy Icon Gold/Silver/Bronze for top 3
Trophy System
Top 3 Recognition
The top three positions receive special treatment:
const renderItem = ({ item } : any ) => (
< View style = {styles. card } >
< Text style = { [styles.rank, item.rank <= 3 && styles.topRank]}>
#{item.rank}
</Text>
<SmartAvatar uri={item.photoURL} userId={item.id} size={45} />
<View style={styles.info}>
<Text style={styles.name}>{item.nombres}</Text>
<Text style={styles.xp}>{item.xp || 0} XP</Text>
</View>
{item.rank === 1 && <Ionicons name="trophy" size={24} color="#FFD700" />}
{item.rank === 2 && <Ionicons name="trophy" size={24} color="#C0C0C0" />}
{item.rank === 3 && <Ionicons name="trophy" size={24} color="#CD7F32" />}
</View>
);
1st Place Gold trophy (#FFD700)
2nd Place Silver trophy (#C0C0C0)
3rd Place Bronze trophy (#CD7F32)
XP Calculation System
How XP is Earned
Users accumulate XP through various activities. The calculation is synchronized across the profile system:
// From app/PerfilPublicoScreen.tsx:48
const calculateTotalXP = ( currentStats : UserStats ) => {
const now = new Date ();
const diffTime = Math . abs ( now . getTime () - currentStats . memberSinceDate . getTime ());
const monthsActive = Math . floor ( diffTime / ( 1000 * 60 * 60 * 24 * 30 ));
let totalXP = 0 ;
totalXP += currentStats . reservationsClean * 100 ; // Clean reservations
totalXP += currentStats . dailyLogins * 30 ; // Daily login streak
totalXP += currentStats . weeksCompleted * 100 ; // Weekly challenges
totalXP += monthsActive * 150 ; // Membership duration
totalXP += currentStats . friendsCount * 50 ; // Friends
return totalXP ;
};
XP Breakdown
Clean Reservations
100 XP per reservation completed without penalties
Daily Logins
30 XP per day of consecutive logins
Weekly Challenges
100 XP per week of consistent activity
Membership Duration
150 XP per month as a member
Social Connections
50 XP per friend added
The most consistent way to climb the leaderboard is maintaining clean reservations (no penalties) and building your friend network
Level System
XP directly determines user levels and titles:
Level Tiers
const getLevelData = ( xp : number ) => {
if ( xp <= 100 ) return {
level: 1 ,
title: "Visitante" ,
minXP: 0 ,
maxXP: 100
};
if ( xp <= 220 ) return {
level: 2 ,
title: "Residente" ,
minXP: 101 ,
maxXP: 220
};
if ( xp <= 350 ) return {
level: 3 ,
title: "Dueño de la Plaza" ,
minXP: 221 ,
maxXP: 350
};
if ( xp <= 500 ) return {
level: 4 ,
title: "Experto en Parking" ,
minXP: 351 ,
maxXP: 500
};
return {
level: 5 ,
title: "Leyenda del Estacionamiento" ,
minXP: 501 ,
maxXP: 10000
};
};
Level 1: Visitante 0-100 XP - New users
Level 2: Residente 101-220 XP - Regular users
Level 3: Dueño de la Plaza 221-350 XP - Experienced users
Level 4: Experto en Parking 351-500 XP - Power users
Level 5: Leyenda 501+ XP - Elite users
Progress Tracking
Profiles display a progress bar showing advancement within current level:
const levelInfo = getLevelData ( currentXP );
const xpRange = levelInfo . maxXP - levelInfo . minXP ;
const xpProgress = currentXP - levelInfo . minXP ;
const progressPercent = Math . min ( Math . max ( xpProgress / xpRange , 0 ), 1 );
< View style = { styles . levelContainer } >
< View style = { styles . levelInfo } >
< Text style = { styles . levelText } > Nivel { levelInfo . level } </ Text >
< Text style = { styles . levelText } > { currentXP } XP </ Text >
</ View >
< View style = { styles . progressBarBackground } >
< View style = { [
styles . progressBarFill ,
{ width: ` ${ progressPercent * 100 } %` }
] } />
</ View >
</ View >
Achievement System
Available Achievements
Users can unlock 7 different achievements visible on their public profile:
Primera Reserva Complete your first reservation
Magnate Add 4+ payment cards
Cliente Fiel Complete 20+ total reservations
Veterano Be a member for 5+ months
Conductor Perfecto All reservations clean (zero penalties)
Primer Amigo Add your first friend
Alma de la Fiesta Have 10+ friends
Achievement Logic
const achievementsList = [
{
id: 1 ,
title: "Primera Reserva" ,
icon: "key" ,
color: "#FF5733" ,
unlocked: stats . reservationsTotal >= 1
},
{
id: 2 ,
title: "Magnate" ,
icon: "card" ,
color: "#FFC300" ,
unlocked: stats . cardsCount >= 4
},
{
id: 3 ,
title: "Cliente Fiel" ,
icon: "heart" ,
color: "#E91E63" ,
unlocked: stats . reservationsTotal > 20
},
{
id: 4 ,
title: "Veterano" ,
icon: "medal" ,
color: "#9B59B6" ,
unlocked : (
( new Date (). getTime () - stats . memberSinceDate . getTime ()) /
( 1000 * 60 * 60 * 24 * 30 )
) >= 5
},
{
id: 5 ,
title: "Conductor Perfecto" ,
icon: "shield-checkmark" ,
color: "#2ECC71" ,
unlocked: stats . reservationsTotal > 0 &&
stats . reservationsClean === stats . reservationsTotal
},
{
id: 6 ,
title: "Primer Amigo" ,
icon: "people" ,
color: "#2196F3" ,
unlocked: stats . friendsCount >= 1
},
{
id: 7 ,
title: "Alma de la Fiesta" ,
icon: "globe" ,
color: "#9C27B0" ,
unlocked: stats . friendsCount >= 10
}
];
Badge Display
Achievements appear as badges on public profiles:
const Badge = ({ icon , color , title , unlocked } : any ) => (
< View style = { [styles.badgeContainer, ! unlocked && { opacity: 0.5 }]}>
<View style={[
styles.badgeCircle,
{
backgroundColor: unlocked ? '#FFF' : '#DDD' ,
borderColor: unlocked ? color : '#999'
}
]}>
<Ionicons name={icon} size={28} color={unlocked ? color : '#777' } />
</ View >
< Text style = {styles. badgeText } > { title } </ Text >
{! unlocked && (
< View style = {{ position : 'absolute' , top : 0 , right : 5 }} >
< Ionicons name = "lock-closed" size = { 12 } color = "#555" />
</ View >
)}
</ View >
);
Locked achievements show at 50% opacity with a lock icon, giving users clear goals to work toward
Statistics Display
Public profiles show key stats that contribute to rankings:
Stat Cards
const StatCard = ({ value , label , icon } : any ) => (
< View style = {styles. statCard } >
< View style = {styles. statHeader } >
< Text style = {styles. statValue } > { value } </ Text >
< Ionicons name = { icon } size = { 20 } color = "#FFE100" />
</ View >
< Text style = {styles. statLabel } > { label } </ Text >
</ View >
);
Displayed Metrics
Reservas Total reservations completed
Limpias Reservations without penalties
Tarjetas Payment methods registered
Amigos Total friends count
Data Sources
User Statistics Collection
The profile screen aggregates data from multiple Firebase collections:
// 1. User document
const userRef = doc ( db , "users" , userId );
const userSnap = await getDoc ( userRef );
// 2. Payment cards
const qCards = query ( collection ( db , "cards" ), where ( "userId" , "==" , userId ));
const cardsSnap = await getDocs ( qCards );
// 3. Reservations
const qRes = query ( collection ( db , "reservations" ), where ( "clientId" , "==" , userId ));
const resSnap = await getDocs ( qRes );
// 4. Friends (both directions)
const qFriends1 = query (
collection ( db , "friend_requests" ),
where ( "fromId" , "==" , userId ),
where ( "status" , "==" , "accepted" )
);
const qFriends2 = query (
collection ( db , "friend_requests" ),
where ( "toId" , "==" , userId ),
where ( "status" , "==" , "accepted" )
);
Clean Reservation Logic
A reservation counts as “clean” if it’s completed without penalties:
let totalRes = 0 ;
let cleanRes = 0 ;
resSnap . forEach (( doc ) => {
const data = doc . data ();
totalRes ++ ;
const penalty = data . penaltyApplied || 0 ;
if ( data . status === 'inactive' && penalty === 0 ) {
cleanRes ++ ;
}
});
UI Design
The ranking screen features a prominent header:
< SafeAreaView style = {styles. header } >
< TouchableOpacity onPress = {() => router.back()} >
< Ionicons name = "arrow-back" size = { 24 } />
</ TouchableOpacity >
< Text style = {styles. title } > Tabla de Líderes 🏆 </ Text >
</ SafeAreaView >
The trophy emoji reinforces the competitive nature of the leaderboard
Card Styling
Ranking cards use elevated white cards against a light background:
const styles = StyleSheet . create ({
card: {
flexDirection: 'row' ,
alignItems: 'center' ,
backgroundColor: '#FFF' ,
padding: 15 ,
borderRadius: 15 ,
marginBottom: 10 ,
elevation: 3
},
rank: {
fontSize: 18 ,
fontWeight: 'bold' ,
width: 40 ,
color: '#555'
},
topRank: {
color: '#D4AF37' , // Gold color for top 3
fontSize: 22
}
});
Competitive Features
Motivational Elements
Users can see exactly where they stand among the top 20
Top 3 positions get special trophy icons
Every user can see the XP totals needed to reach higher ranks
Locked badges show users what actions to take next
Perfect driving record provides significant XP advantages
Social Competition
The integration with the friend system creates friendly rivalry:
View friends’ profiles to compare XP
See friends’ achievement progress
Motivates clean driving to maintain ranking
Encourages social features (50 XP per friend)
Users strategically aiming for top positions should focus on:
Maintaining clean reservations (100 XP each)
Building friend network (50 XP per friend)
Long-term membership (150 XP per month)
Future Enhancements
Potential competitive features to consider:
Weekly/monthly leaderboards
Category-specific rankings (most reservations, cleanest record, etc.)
Achievement rarity statistics
Push notifications when friends pass your rank
Seasonal competitions with prizes
Team/group challenges
The leaderboard only shows client users (where("role", "==", "client")) to exclude staff accounts from rankings