diff --git a/sql/init.sql b/sql/init.sql index e350fd3..b80c45c 100644 --- a/sql/init.sql +++ b/sql/init.sql @@ -23,6 +23,9 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; \i contests/user_contest_feedback.sql \i contests/contest_functions.sql +-- Leaderboard functions +\i leaderboard/leaderboard_functions.sql + -- Grant necessary permissions GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role; GRANT ALL ON ALL TABLES IN SCHEMA public TO anon, authenticated, service_role; diff --git a/sql/leaderboard/leaderboard_functions.sql b/sql/leaderboard/leaderboard_functions.sql new file mode 100644 index 0000000..118607b --- /dev/null +++ b/sql/leaderboard/leaderboard_functions.sql @@ -0,0 +1,50 @@ +-- Create a function to get leaderboard data +CREATE OR REPLACE FUNCTION get_leaderboard() RETURNS TABLE ( + user_id UUID, + username TEXT, + avatar_url TEXT, + github_url TEXT, + problems_solved BIGINT, + earliest_solves_sum BIGINT, + rank BIGINT + ) AS $$ WITH user_stats AS ( + SELECT u.id AS user_id, + u.raw_user_meta_data->>'user_name' AS username, + u.raw_user_meta_data->>'avatar_url' AS avatar_url, + CASE + WHEN u.raw_user_meta_data->>'html_url' IS NOT NULL THEN u.raw_user_meta_data->>'html_url' + ELSE 'https://github.com/' || (u.raw_user_meta_data->>'user_name') + END AS github_url, + COUNT(usp.problem_id)::BIGINT AS problems_solved, + COALESCE( + SUM( + EXTRACT( + EPOCH + FROM usp.solved_at + )::BIGINT + ), + 0 + ) AS earliest_solves_sum + FROM auth.users u + LEFT JOIN user_solved_problems usp ON u.id = usp.user_id + GROUP BY u.id, + u.raw_user_meta_data + HAVING COUNT(usp.problem_id) > 0 + ) +SELECT user_stats.user_id, + user_stats.username, + user_stats.avatar_url, + user_stats.github_url, + user_stats.problems_solved, + user_stats.earliest_solves_sum, + ROW_NUMBER() OVER ( + ORDER BY user_stats.problems_solved DESC, + user_stats.earliest_solves_sum ASC + )::BIGINT AS rank +FROM user_stats +ORDER BY user_stats.problems_solved DESC, + user_stats.earliest_solves_sum ASC; +$$ LANGUAGE SQL SECURITY DEFINER; +-- Grant permissions +GRANT EXECUTE ON FUNCTION get_leaderboard() TO authenticated, + anon; \ No newline at end of file diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index b1b7fb6..7cd0e18 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -186,6 +186,17 @@ $: if ($page) { >Contests +
  • + Leaderboard +
  • Contests
  • +
  • + Leaderboard +
  • +import type { LeaderboardEntry } from '$lib/services/leaderboard'; + +// Props +export let leaderboardEntries: LeaderboardEntry[] = []; + +// Format large numbers with commas +function formatNumber(num: number): string { + return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); +} + + +
    +
    + + + + + + + + + + {#each leaderboardEntries as entry} + + + + + + {/each} + + {#if leaderboardEntries.length === 0} + + + + {/if} + +
    + Rank + + User + + Solves +
    + + {#if entry.rank === 1} + + {entry.rank} + + {:else if entry.rank === 2} + + {entry.rank} + + {:else if entry.rank === 3} + + {entry.rank} + + {:else} + {entry.rank} + {/if} + +
    + {#if entry.avatarUrl} + {entry.username} + {:else} +
    + {entry.username.substring(0, 2).toUpperCase()} +
    + {/if} + + @{entry.username} + +
    +
    + {formatNumber(entry.problemsSolved)} +
    + No data available. Start solving problems to appear on the leaderboard! +
    +
    +
    + + diff --git a/src/lib/services/leaderboard.ts b/src/lib/services/leaderboard.ts new file mode 100644 index 0000000..bbbaeb7 --- /dev/null +++ b/src/lib/services/leaderboard.ts @@ -0,0 +1,60 @@ +/** + * Service for leaderboard operations + */ +import { supabase } from './database'; + +/** + * Leaderboard entry interface + */ +export type LeaderboardEntry = { + userId: string; + username: string; + avatarUrl: string; + githubUrl: string; + problemsSolved: number; + earliestSolvesSum: number; + rank: number; // Now comes directly from the database +}; + +/** + * Database record type from Supabase + */ +export type LeaderboardRecord = { + user_id: string; + username: string; + avatar_url: string; + github_url: string; + problems_solved: number; + earliest_solves_sum: number; + rank: number; +}; + +/** + * Fetches leaderboard data from the database + * @returns Array of leaderboard entries + */ +export async function fetchLeaderboard(): Promise { + try { + // Fetch leaderboard data using the get_leaderboard function + const { data, error } = await supabase.rpc('get_leaderboard'); + + if (error) { + console.error('Error fetching leaderboard:', error); + return []; + } + + // Transform database records to LeaderboardEntry type + return (data as LeaderboardRecord[]).map((record) => ({ + userId: record.user_id, + username: record.username, + avatarUrl: record.avatar_url, + githubUrl: record.github_url, + problemsSolved: record.problems_solved, + earliestSolvesSum: record.earliest_solves_sum, + rank: record.rank + })); + } catch (err) { + console.error('Failed to fetch leaderboard:', err); + return []; + } +} diff --git a/src/routes/leaderboard/+page.svelte b/src/routes/leaderboard/+page.svelte new file mode 100644 index 0000000..aabb6c3 --- /dev/null +++ b/src/routes/leaderboard/+page.svelte @@ -0,0 +1,95 @@ + + + + AlgoHub | Leaderboard + + + +
    + {#if loading} +
    +
    + + + + +

    Loading leaderboard...

    +
    +
    + {:else if error} +
    +

    {error}

    + +
    + {:else} +
    + +
    +
    +
    + +
    +
    +
    +
    + {/if} +
    + +