Skip to main content
The leaderboard feature is designed to add competitive elements to Quest Hunter, allowing you to compare your progress with other users. While the current implementation is minimal, this page outlines the planned competitive features.

Current Implementation Status

The leaderboard screen is currently a placeholder (app/(tabs)/(leaderboard)/index.tsx). Future versions will implement comprehensive ranking and competitive features.
The current screen returns an empty component:
const LeaderboardScreen = () => {
  return <Screen />;
};

Planned Leaderboard Features

Based on the available data structure, several leaderboard and ranking features are possible:

XP-Based Rankings

Rank users by total experience points earned:

Global Leaderboard

All users ranked by total XP from completed quests. Shows top performers across the entire platform.

Weekly Rankings

Users ranked by XP earned in the past 7 days. Encourages consistent engagement and competition.

Monthly Champions

Top performers for the current month. Resets each month for fresh competition.

Category Leaders

Separate leaderboards for each quest category (abenteuer, kultur, natur, essen, trinken, geschichte).

Completion-Based Rankings

Rank users by quest and location completion:
Leaderboard showing users with the most completed quests. Simple count of quests where completedAt is defined.
const userCompletions = await ctx.db
  .query("userQuests")
  .filter((q) => q.neq(q.field("completedAt"), undefined))
  .collect();

// Group by userId and count
const rankings = Object.entries(
  userCompletions.reduce((acc, uq) => {
    acc[uq.userId] = (acc[uq.userId] || 0) + 1;
    return acc;
  }, {})
)
.sort((a, b) => b[1] - a[1])
.slice(0, 100); // Top 100
Rank by number of individual location checkpoints completed. Shows users who are most thorough in exploration.
const locationCounts = await ctx.db
  .query("userLocations")
  .collect();

// Group by userId and count
const rankings = Object.entries(
  locationCounts.reduce((acc, ul) => {
    acc[ul.userId] = (acc[ul.userId] || 0) + 1;
    return acc;
  }, {})
)
.sort((a, b) => b[1] - a[1]);
Rank by percentage of started quests that were completed. Rewards users who finish what they start.Formula: (Completed Quests / Started Quests) × 100
Rank by average time to complete quests. Shows which users are most efficient.Calculate: (completedAt - startedAt) for each quest, then average.

Difficulty-Based Rankings

Recognize users who tackle challenging quests:
  • Hard Quest Champions: Users who completed the most “schwer” (hard) difficulty quests
  • All-Rounder Award: Users with completed quests across all three difficulty levels
  • Difficulty Points: Weighted system (easy: 1pt, medium: 2pts, hard: 3pts)

Potential Implementation Approach

Backend Query Structure

A comprehensive leaderboard query might look like:
export const getGlobalLeaderboard = query({
  args: {
    limit: v.optional(v.number()),
    timeRange: v.optional(v.union(
      v.literal("all-time"),
      v.literal("weekly"),
      v.literal("monthly")
    )),
  },
  handler: async (ctx, { limit = 100, timeRange = "all-time" }) => {
    // Calculate time cutoff
    const cutoff = timeRange === "weekly" 
      ? Date.now() - (7 * 24 * 60 * 60 * 1000)
      : timeRange === "monthly"
      ? Date.now() - (30 * 24 * 60 * 60 * 1000)
      : 0;

    // Fetch completed quests within time range
    const completedQuests = await ctx.db
      .query("userQuests")
      .filter((q) => 
        q.neq(q.field("completedAt"), undefined) &&
        q.gte(q.field("completedAt"), cutoff)
      )
      .collect();

    // Fetch quest details to get XP values
    const questsWithXP = await Promise.all(
      completedQuests.map(async (uq) => ({
        ...uq,
        quest: await ctx.db.get(uq.questId),
      }))
    );

    // Group by user and sum XP
    const userXP = questsWithXP.reduce((acc, { userId, quest }) => {
      if (!quest) return acc;
      acc[userId] = (acc[userId] || 0) + quest.xp;
      return acc;
    }, {} as Record<string, number>);

    // Fetch user details and create rankings
    const rankings = await Promise.all(
      Object.entries(userXP)
        .sort((a, b) => b[1] - a[1])
        .slice(0, limit)
        .map(async ([userId, xp], index) => ({
          rank: index + 1,
          user: await ctx.db.get(userId as Id<"users">),
          xp,
          questsCompleted: completedQuests.filter(
            (q) => q.userId === userId
          ).length,
        }))
    );

    return rankings.filter((r) => r.user !== null);
  },
});

Frontend Display Component

A leaderboard UI could include:
<Screen>
  <Tabs value={timeRange} onValueChange={setTimeRange}>
    <TabsList>
      <TabsTrigger value="all-time">All Time</TabsTrigger>
      <TabsTrigger value="weekly">This Week</TabsTrigger>
      <TabsTrigger value="monthly">This Month</TabsTrigger>
    </TabsList>
  </Tabs>

  <FlatList
    data={rankings}
    renderItem={({ item }) => (
      <View className="flex-row items-center p-4 gap-3">
        <Text className="text-2xl font-bold w-12">
          #{item.rank}
        </Text>
        <Image
          source={item.user.imageUrl}
          style={{ width: 40, height: 40, borderRadius: 20 }}
        />
        <View className="flex-1">
          <Text className="font-semibold">
            {item.user.firstName} {item.user.lastName}
          </Text>
          <Text className="text-sm text-muted-foreground">
            {item.questsCompleted} quests completed
          </Text>
        </View>
        <Text className="text-primary font-bold">
          {item.xp} XP
        </Text>
      </View>
    )}
  />
</Screen>

Data Privacy Considerations

When implementing leaderboards, consider user privacy preferences. Some users may want to opt out of public rankings.
Potential privacy features:
  • Private Mode: Option to hide profile from leaderboards
  • Anonymous Display: Show rankings without revealing full names
  • Friend Leaderboards: Only compete with users you’ve connected with
  • Local Rankings: City or region-based leaderboards instead of global

Gamification Elements

Leaderboards can be enhanced with additional gamification:

Badges & Achievements

Award badges for milestones: “First Quest”, “10 Quests Completed”, “Top 10 This Week”, “All Categories Mastered”

Streak Tracking

Count consecutive days with at least one quest completed. Show longest streak on profile.

Level System

Convert total XP into user levels (e.g., every 1000 XP = 1 level). Display level on leaderboard.

Seasonal Events

Special limited-time leaderboards with unique rewards or recognition.

Social Features

Competitive features work best with social elements:
  • Friends System: Add other users as friends
  • Friend Leaderboards: See rankings among your friends only
  • Challenge System: Send quest challenges to friends
  • Share Achievements: Post completions to social media
  • Team Quests: Collaborative quests with shared leaderboards

Performance Optimization

Leaderboard queries can be expensive with many users:

Caching Strategy

// Cache leaderboard results for 5 minutes
const CACHE_DURATION = 5 * 60 * 1000;

// Store cached results in a separate table
cachedLeaderboards: {
  timeRange: string,
  rankings: string, // JSON stringified
  updatedAt: number,
}

// Query cache first, regenerate if stale
const cached = await ctx.db
  .query("cachedLeaderboards")
  .filter((q) => 
    q.eq(q.field("timeRange"), timeRange) &&
    q.gt(q.field("updatedAt"), Date.now() - CACHE_DURATION)
  )
  .first();

if (cached) {
  return JSON.parse(cached.rankings);
}

// Regenerate and cache
const newRankings = await generateLeaderboard();
await ctx.db.insert("cachedLeaderboards", {
  timeRange,
  rankings: JSON.stringify(newRankings),
  updatedAt: Date.now(),
});

Pagination

Show top 100 by default, allow loading more:
export const getLeaderboardPage = query({
  args: {
    offset: v.number(),
    limit: v.number(),
  },
  handler: async (ctx, { offset, limit }) => {
    // Fetch and rank, then slice
    return allRankings.slice(offset, offset + limit);
  },
});

Future Enhancements

Use Convex’s reactive queries to update leaderboard positions in real-time as users complete quests.
Store weekly/monthly snapshots to show ranking trends over time. Graph showing rank position across weeks.
“Users near you” section showing +/- 5 ranks around your current position.
Combined score using multiple factors: XP, completion rate, difficulty, speed, etc.
  • User Progress - Track your personal XP and completion stats
  • Quests - Learn about XP values and quest difficulty
  • Locations - Understand location completion tracking

Build docs developers (and LLMs) love