From 3096290214d84a38c7aecfc0f4db8528ce04ed30 Mon Sep 17 00:00:00 2001 From: Rasheed <37883750+ws-rush@users.noreply.github.com> Date: Fri, 26 Jun 2026 06:08:25 +0300 Subject: [PATCH 01/10] feat: add country-based impact leaderboard and paginate GitHub API searches (#155) --- .env.example | 2 +- app/api/score/route.ts | 56 ++ app/globals.css | 2 + app/leaderboard/[country]/page.tsx | 221 +++++++ app/leaderboard/country-grid-client.tsx | 112 ++++ app/leaderboard/page.tsx | 69 ++ app/sitemap.ts | 12 +- components/leaderboard-table.tsx | 304 +++++++++ .../scoring-methodology-page-client.tsx | 6 +- data/countries.json | 606 ++++++++++++++++++ lib/country-flags.ts | 162 +++++ lib/github.ts | 271 +++++--- locales/ar.json | 30 +- locales/en.json | 30 +- next-env.d.ts | 2 +- package.json | 3 + pnpm-lock.yaml | 100 +-- test/github/github-cache.test.ts | 20 +- test/seo/seo.test.ts | 4 +- types/leaderboard.ts | 4 + 20 files changed, 1871 insertions(+), 145 deletions(-) create mode 100644 app/api/score/route.ts create mode 100644 app/leaderboard/[country]/page.tsx create mode 100644 app/leaderboard/country-grid-client.tsx create mode 100644 app/leaderboard/page.tsx create mode 100644 components/leaderboard-table.tsx create mode 100644 data/countries.json create mode 100644 lib/country-flags.ts create mode 100644 types/leaderboard.ts diff --git a/.env.example b/.env.example index d976952..4f33176 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ GITHUB_TOKEN=your_github_token_here # If omitted, the app falls back to https://github.com/O2sa/DevImpact NEXT_PUBLIC_GITHUB_REPO_URL=your_github_repo_url_here -# Redis caching (optional) +# Redis caching (optional — strongly recommended for leaderboard performance) # Use either redis://localhost:6379 or include password if enabled: redis://:password@localhost:6379 REDIS_URL= REDIS_ENABLED=false diff --git a/app/api/score/route.ts b/app/api/score/route.ts new file mode 100644 index 0000000..e1fd639 --- /dev/null +++ b/app/api/score/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from "next/server"; +import { fetchGitHubUserData } from "@/lib/github"; +import { calculateUserScore } from "@/lib/score"; + +export const runtime = "nodejs"; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const usernames = searchParams.getAll("username").map((u) => u.trim()).filter(Boolean); + + if (usernames.length === 0) { + return NextResponse.json( + { success: false, error: "Provide at least one username" }, + { status: 400 }, + ); + } + + if (usernames.length > 256) { + return NextResponse.json( + { success: false, error: "Maximum 256 usernames per request" }, + { status: 400 }, + ); + } + + const scored: Array<{ + username: string; + name: string | null; + avatarUrl: string; + repoScore: number; + prScore: number; + contributionScore: number; + finalScore: number; + }> = []; + + const errors: string[] = []; + + for (const username of usernames) { + try { + const data = await fetchGitHubUserData(username); + const score = calculateUserScore(data, username); + scored.push({ + username, + name: data.name, + avatarUrl: data.avatarUrl, + repoScore: Math.round(score.repoScore), + prScore: Math.round(score.prScore), + contributionScore: Math.round(score.contributionScore), + finalScore: Math.round(score.finalScore), + }); + } catch { + errors.push(username); + } + } + + return NextResponse.json({ success: true, scored, errors }); +} diff --git a/app/globals.css b/app/globals.css index b503fd8..65ff396 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,3 +1,5 @@ +@import "flag-icons/css/flag-icons.min.css"; + @tailwind base; @tailwind components; @tailwind utilities; diff --git a/app/leaderboard/[country]/page.tsx b/app/leaderboard/[country]/page.tsx new file mode 100644 index 0000000..09c8067 --- /dev/null +++ b/app/leaderboard/[country]/page.tsx @@ -0,0 +1,221 @@ +"use client"; + +import { useEffect, useState, use } from "react"; +import Link from "next/link"; +import { ArrowLeft, Loader2 } from "lucide-react"; +import yaml from "js-yaml"; +import { LeaderboardTable } from "@/components/leaderboard-table"; +import { AppHeader } from "@/components/app-header"; +import { AppFooter } from "@/components/app-footer"; +import { Button } from "@/components/ui/button"; +import { useTranslation } from "@/components/language-provider"; + +// ─── Types ───────────────────────────────────────────────────────────── + +type CommitterEntry = { + rank: number; + name: string; + login: string; + avatarUrl: string; + contributions: number; +}; + +type CommitterYaml = { + title?: string; + total_user_count?: number; + users?: CommitterEntry[]; +}; + +type ScoredEntry = { + username: string; + name: string | null; + avatarUrl: string; + repoScore: number; + prScore: number; + contributionScore: number; + finalScore: number; + originalRank: number; + originalContributions: number; + impactRank: number; +}; + +type Props = { + params: Promise<{ country: string }>; +}; + +// ─── Fetch helpers ───────────────────────────────────────────────────── + +async function fetchCommiters(country: string) { + const url = `https://raw.githubusercontent.com/ashkulz/committers.top/gh-pages/_data/locations/${country}.yml`; + const res = await fetch(url, { + headers: { "User-Agent": "DevImpact-Bot" }, + }); + if (!res.ok) throw new Error(`Failed to fetch ${country}`); + const text = await res.text(); + const data = yaml.load(text) as CommitterYaml; + if (!data?.users) throw new Error("Invalid data"); + return { + title: data.title || country, + totalFromSource: data.total_user_count ?? data.users.length, + users: data.users, + }; +} + +async function fetchScores(logins: string[]) { + const params = logins + .map((u) => `username=${encodeURIComponent(u)}`) + .join("&"); + const res = await fetch(`/api/score?${params}`); + if (!res.ok) throw new Error("Failed to score users"); + const json = await res.json(); + if (!json.success) throw new Error(json.error || "Scoring failed"); + return { scored: json.scored, errors: json.errors }; +} + +// ─── Component ───────────────────────────────────────────────────────── + +export default function CountryLeaderboardPage({ params }: Props) { + const { country } = use(params); + const { t } = useTranslation(); + + const [title, setTitle] = useState(""); + const [totalFromSource, setTotalFromSource] = useState(0); + const [scored, setScored] = useState([]); + const [errors, setErrors] = useState([]); + const [loading, setLoading] = useState(true); + const [failed, setFailed] = useState(null); + + useEffect(() => { + let cancelled = false; + + (async () => { + try { + const data = await fetchCommiters(country); + if (cancelled) return; + setTitle(data.title); + setTotalFromSource(data.totalFromSource); + + const logins = data.users.map((u) => u.login); + const { scored: apiScored, errors: apiErrors } = + await fetchScores(logins); + if (cancelled) return; + + // Build a rank lookup from the original committers data + const rankMap = new Map( + data.users.map((u) => [u.login, { rank: u.rank, contributions: u.contributions }]) + ); + + const results: ScoredEntry[] = apiScored.map((s: Record) => { + const original = rankMap.get(s.username as string) ?? { rank: 0, contributions: 0 }; + return { + username: s.username as string, + name: s.name as string | null, + avatarUrl: s.avatarUrl as string, + repoScore: s.repoScore as number, + prScore: s.prScore as number, + contributionScore: s.contributionScore as number, + finalScore: s.finalScore as number, + originalRank: original.rank, + originalContributions: original.contributions, + impactRank: 0, + }; + }); + + results.sort((a, b) => b.finalScore - a.finalScore); + results.forEach((u, idx) => (u.impactRank = idx + 1)); + + setScored(results); + setErrors(apiErrors); + setLoading(false); + } catch (err) { + if (!cancelled) + setFailed( + err instanceof Error ? err.message : "Failed to load leaderboard" + ); + } + })(); + + return () => { + cancelled = true; + }; + }, [country]); + + if (failed) { + return ( +
+ +
+
+

+ {t("leaderboard.error.title")} +

+

+ {failed} +

+ + + +
+
+ +
+ ); + } + + return ( +
+ +
+ {/* Back navigation */} +
+ + + +
+ + {/* Leaderboard table */} + {scored.length > 0 ? ( +
+ +
+ ) : loading ? ( +
+ + + {t("leaderboard.loading")} + +
+ ) : ( +
+

+ {t("leaderboard.noDevelopersFor", { title })} +

+ + + +
+ )} + + {/* Scoring progress indicator */} + {loading && scored.length > 0 && ( +
+ + + {t("leaderboard.loading")} + +
+ )} +
+ +
+ ); +} diff --git a/app/leaderboard/country-grid-client.tsx b/app/leaderboard/country-grid-client.tsx new file mode 100644 index 0000000..9087274 --- /dev/null +++ b/app/leaderboard/country-grid-client.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { Globe, Search, Users } from "lucide-react"; +import Link from "next/link"; +import Image from "next/image"; +import { Input } from "@/components/ui/input"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { useTranslation } from "@/components/language-provider"; +import { getCountryCode } from "@/lib/country-flags"; +import type { CountryInfo } from "@/types/leaderboard"; +import type { Route } from "next"; + +type Props = { + countries: CountryInfo[]; +}; + +export function CountryGridClient({ countries }: Props) { + const { t } = useTranslation(); + const [search, setSearch] = useState(""); + + const filtered = useMemo(() => { + if (!search.trim()) return countries; + const q = search.trim().toLowerCase(); + return countries.filter( + (c) => + c.slug.toLowerCase().includes(q) || c.title.toLowerCase().includes(q), + ); + }, [countries, search]); + + return ( + + +

+ {t("leaderboard.header.eyebrow")} +

+ {t("leaderboard.header.title")} + + {t("leaderboard.header.description")} + +
+ + {countries.length > 10 && ( +
+ + setSearch(e.target.value)} + /> +
+ )} + + {filtered.length === 0 && ( +
+ +

+ {search + ? t("leaderboard.noCountriesSearch") + : t("leaderboard.empty.title")} +

+
+ )} + + {filtered.length > 0 && ( +
+ {filtered.map((c) => { + const code = getCountryCode(c.slug); + return ( + + + {code ? ( + + ) : ( + + )} + {c.title} + + + + {t("leaderboard.viewCountry")} + + + ); + })} +
+ )} +
+
+ ); +} diff --git a/app/leaderboard/page.tsx b/app/leaderboard/page.tsx new file mode 100644 index 0000000..abd6e23 --- /dev/null +++ b/app/leaderboard/page.tsx @@ -0,0 +1,69 @@ +import type { Metadata } from "next"; +import { AppHeader } from "@/components/app-header"; +import { AppFooter } from "@/components/app-footer"; +import { JsonLd } from "@/components/seo/json-ld"; +import { toAbsoluteUrl } from "@/lib/seo"; +import countriesData from "@/data/countries.json"; +import { CountryGridClient } from "./country-grid-client"; +import type { CountryInfo } from "@/types/leaderboard"; + +// ─── Metadata ────────────────────────────────────────────────────────── + +export const metadata: Metadata = { + title: "Leaderboard — Country Impact Rankings", + description: + "Browse committers.top countries and view developers ranked by DevImpact's transparent repository, PR, and community contribution scoring.", + alternates: { + canonical: "/leaderboard", + }, + openGraph: { + title: "Country Leaderboards by Impact | DevImpact", + description: + "Browse committers.top country leaderboards reordered by real open-source impact: repository quality, merged PRs, and community contributions.", + url: "/leaderboard", + images: [ + { + url: toAbsoluteUrl("/og-image.svg"), + width: 1200, + height: 630, + alt: "DevImpact Country Leaderboard", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "Country Leaderboards by Impact | DevImpact", + description: + "Browse committers.top country leaderboards reordered by real open-source impact.", + images: [toAbsoluteUrl("/og-image.svg")], + }, +}; + +const webPageSchema = { + "@context": "https://schema.org", + "@type": "WebPage", + name: "Country Leaderboards by Impact", + description: + "Browse committers.top country leaderboards reordered by DevImpact impact scoring.", + url: toAbsoluteUrl("/leaderboard"), +}; + +// ─── Static country list (sourced from committers.top YAML, converted to JSON) ─ +const countries: CountryInfo[] = countriesData as CountryInfo[]; + +// ─── Page ────────────────────────────────────────────────────────────── + +export default function LeaderboardPage() { + return ( + <> + +
+ +
+ +
+ +
+ + ); +} diff --git a/app/sitemap.ts b/app/sitemap.ts index e758893..8fad05f 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,17 +1,23 @@ import type { MetadataRoute } from "next"; import { getSiteUrl } from "@/lib/seo"; -export default function sitemap(): MetadataRoute.Sitemap { +export default async function sitemap(): Promise { const baseUrl = getSiteUrl(); const now = new Date(); - return [ + const entries: MetadataRoute.Sitemap = [ { url: `${baseUrl}/`, lastModified: now, changeFrequency: "weekly", priority: 1, }, + { + url: `${baseUrl}/leaderboard`, + lastModified: now, + changeFrequency: "weekly", + priority: 0.8, + }, { url: `${baseUrl}/scoring-methodology`, lastModified: now, @@ -19,4 +25,6 @@ export default function sitemap(): MetadataRoute.Sitemap { priority: 0.7, }, ]; + + return entries; } diff --git a/components/leaderboard-table.tsx b/components/leaderboard-table.tsx new file mode 100644 index 0000000..aa43525 --- /dev/null +++ b/components/leaderboard-table.tsx @@ -0,0 +1,304 @@ +"use client"; + +import { useState, useMemo } from "react"; +import Image from "next/image"; +import { + TrendingUp, + TrendingDown, + Minus, + ChevronLeft, + ChevronRight, + Search, + AlertTriangle, +} from "lucide-react"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "./ui/tooltip"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "./ui/card"; +import { Input } from "./ui/input"; +import { Button } from "./ui/button"; +import { useTranslation } from "./language-provider"; +import { cn } from "@/lib/utils"; + +type LeaderboardEntry = { + username: string; + name: string | null; + avatarUrl: string; + repoScore: number; + prScore: number; + contributionScore: number; + finalScore: number; + originalRank: number; + originalContributions: number; + impactRank: number; +}; + +type Props = { + users: LeaderboardEntry[]; + failedUsers: string[]; + title: string; + totalFromSource: number; + usersProcessed: number; +}; + +const PAGE_SIZE = 25; + +function RankChangeIndicator({ + impactRank, + originalRank, +}: { + impactRank: number; + originalRank: number; +}) { + const change = originalRank - impactRank; + if (change > 0) { + return ( + + +{change} + + ); + } + if (change < 0) { + return ( + + + {change} + + ); + } + return ( + + + + ); +} + +function getGithubProfileUrl(username: string): string { + return `https://github.com/${username}`; +} + +export function LeaderboardTable({ + users, + failedUsers, + title, + totalFromSource, + usersProcessed, +}: Props) { + const { t } = useTranslation(); + const [page, setPage] = useState(0); + const [search, setSearch] = useState(""); + + const filtered = useMemo(() => { + if (!search.trim()) return users; + const q = search.trim().toLowerCase(); + return users.filter( + (u) => + u.username.toLowerCase().includes(q) || + (u.name && u.name.toLowerCase().includes(q)), + ); + }, [users, search]); + + const totalPages = Math.ceil(filtered.length / PAGE_SIZE); + const paged = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE); + + if (users.length === 0) return null; + + return ( +
+ + + + {t("leaderboard.title")} — {title} + +
+ + {t("leaderboard.description", { + processed: usersProcessed, + total: totalFromSource, + })} + + {failedUsers.length > 0 && ( + + + + + {t("leaderboard.partialErrors", { count: failedUsers.length })} + + + +

{failedUsers.join(", ")}

+
+
+ )} +
+ {filtered.length > PAGE_SIZE && ( +
+ + { + setSearch(e.target.value); + setPage(0); + }} + /> +
+ )} +
+ +
+ + + + + + + + + + + + + + + {paged.map((user) => ( + + + + + + + + + + + ))} + +
+ {t("leaderboard.impactRank")} + + {t("leaderboard.developer")} + + {t("comparsion.final.score")} + + {t("comparsion.repo.score")} + + {t("comparsion.pr.score")} + + {t("comparsion.contribution.score")} + + {t("leaderboard.committersRank")} + + {t("leaderboard.change")} +
+ + {user.impactRank} + + +
+ {user.name +
+ + {user.name || user.username} + +

+ {user.username} +

+
+
+
+ + {user.finalScore} + + + {user.repoScore} + + {user.prScore} + + {user.contributionScore} + + #{user.originalRank} + + +
+
+ + {totalPages > 1 && ( +
+

+ {t("leaderboard.pagination", { + from: page * PAGE_SIZE + 1, + to: Math.min((page + 1) * PAGE_SIZE, filtered.length), + total: filtered.length, + })} +

+
+ + + {page + 1} / {totalPages} + + +
+
+ )} +
+
+
+ ); +} diff --git a/components/scoring-methodology-page-client.tsx b/components/scoring-methodology-page-client.tsx index 76f9ed1..16565f3 100644 --- a/components/scoring-methodology-page-client.tsx +++ b/components/scoring-methodology-page-client.tsx @@ -9,11 +9,9 @@ import { ScoringMethodologyFlow } from "@/components/scoring/scoring-methodology import { ScoringMethodologySection } from "@/components/scoring/scoring-methodology-section"; export function ScoringMethodologyPageClient() { - const { t, dir } = useTranslation(); + const { t } = useTranslation(); const router = useRouter(); const searchParams = useSearchParams(); - const backIconClass = dir === "rtl" ? "rotate-180" : ""; - const handleBack = () => { const query = searchParams.toString(); if (query) { @@ -39,7 +37,7 @@ export function ScoringMethodologyPageClient() { className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-3 py-2 text-sm font-semibold text-foreground transition-colors hover:bg-muted" aria-label={t("methodology.back")} > - + {t("methodology.back")} diff --git a/data/countries.json b/data/countries.json new file mode 100644 index 0000000..403d9f9 --- /dev/null +++ b/data/countries.json @@ -0,0 +1,606 @@ +[ + { + "slug": "afghanistan", + "title": "Afghanistan" + }, + { + "slug": "albania", + "title": "Albania" + }, + { + "slug": "algeria", + "title": "Algeria" + }, + { + "slug": "angola", + "title": "Angola" + }, + { + "slug": "argentina", + "title": "Argentina" + }, + { + "slug": "armenia", + "title": "Armenia" + }, + { + "slug": "australia", + "title": "Australia" + }, + { + "slug": "austria", + "title": "Austria" + }, + { + "slug": "azerbaijan", + "title": "Azerbaijan" + }, + { + "slug": "bahrain", + "title": "Bahrain" + }, + { + "slug": "bangladesh", + "title": "Bangladesh" + }, + { + "slug": "belarus", + "title": "Belarus" + }, + { + "slug": "belgium", + "title": "Belgium" + }, + { + "slug": "benin", + "title": "Benin" + }, + { + "slug": "bolivia", + "title": "Bolivia" + }, + { + "slug": "bosnia_and_herzegovina", + "title": "Bosnia and Herzegovina" + }, + { + "slug": "botswana", + "title": "Botswana" + }, + { + "slug": "brazil", + "title": "Brazil" + }, + { + "slug": "bulgaria", + "title": "Bulgaria" + }, + { + "slug": "burkina_faso", + "title": "Burkina Faso" + }, + { + "slug": "burundi", + "title": "Burundi" + }, + { + "slug": "cambodia", + "title": "Cambodia" + }, + { + "slug": "cameroon", + "title": "Cameroon" + }, + { + "slug": "canada", + "title": "Canada" + }, + { + "slug": "chad", + "title": "Chad" + }, + { + "slug": "chile", + "title": "Chile" + }, + { + "slug": "china", + "title": "China" + }, + { + "slug": "colombia", + "title": "Colombia" + }, + { + "slug": "costa_rica", + "title": "Costa Rica" + }, + { + "slug": "croatia", + "title": "Croatia" + }, + { + "slug": "cuba", + "title": "Cuba" + }, + { + "slug": "cyprus", + "title": "Cyprus" + }, + { + "slug": "czech_republic", + "title": "Czech Republic" + }, + { + "slug": "congo_kinshasa", + "title": "Democratic Republic of the Congo" + }, + { + "slug": "denmark", + "title": "Denmark" + }, + { + "slug": "dominican_republic", + "title": "Dominican Republic" + }, + { + "slug": "ecuador", + "title": "Ecuador" + }, + { + "slug": "egypt", + "title": "Egypt" + }, + { + "slug": "el_salvador", + "title": "El Salvador" + }, + { + "slug": "estonia", + "title": "Estonia" + }, + { + "slug": "ethiopia", + "title": "Ethiopia" + }, + { + "slug": "finland", + "title": "Finland" + }, + { + "slug": "france", + "title": "France" + }, + { + "slug": "gabon", + "title": "Gabon" + }, + { + "slug": "georgia", + "title": "Georgia" + }, + { + "slug": "germany", + "title": "Germany" + }, + { + "slug": "ghana", + "title": "Ghana" + }, + { + "slug": "greece", + "title": "Greece" + }, + { + "slug": "guatemala", + "title": "Guatemala" + }, + { + "slug": "guinea", + "title": "Guinea" + }, + { + "slug": "haiti", + "title": "Haiti" + }, + { + "slug": "honduras", + "title": "Honduras" + }, + { + "slug": "hong_kong", + "title": "Hong Kong" + }, + { + "slug": "hungary", + "title": "Hungary" + }, + { + "slug": "india", + "title": "India" + }, + { + "slug": "indonesia", + "title": "Indonesia" + }, + { + "slug": "iran", + "title": "Iran" + }, + { + "slug": "iraq", + "title": "Iraq" + }, + { + "slug": "ireland", + "title": "Ireland" + }, + { + "slug": "israel", + "title": "Israel" + }, + { + "slug": "italy", + "title": "Italy" + }, + { + "slug": "ivory_coast", + "title": "Ivory Coast" + }, + { + "slug": "japan", + "title": "Japan" + }, + { + "slug": "jordan", + "title": "Jordan" + }, + { + "slug": "kazakhstan", + "title": "Kazakhstan" + }, + { + "slug": "kenya", + "title": "Kenya" + }, + { + "slug": "kosovo", + "title": "Kosovo" + }, + { + "slug": "kurdistan", + "title": "Kurdistan" + }, + { + "slug": "kyrgyzstan", + "title": "Kyrgyzstan" + }, + { + "slug": "laos", + "title": "Laos" + }, + { + "slug": "latvia", + "title": "Latvia" + }, + { + "slug": "lebanon", + "title": "Lebanon" + }, + { + "slug": "libya", + "title": "Libya" + }, + { + "slug": "lithuania", + "title": "Lithuania" + }, + { + "slug": "luxembourg", + "title": "Luxembourg" + }, + { + "slug": "macau", + "title": "Macau" + }, + { + "slug": "madagascar", + "title": "Madagascar" + }, + { + "slug": "malawi", + "title": "Malawi" + }, + { + "slug": "malaysia", + "title": "Malaysia" + }, + { + "slug": "mali", + "title": "Mali" + }, + { + "slug": "malta", + "title": "Malta" + }, + { + "slug": "mauritania", + "title": "Mauritania" + }, + { + "slug": "mauritius", + "title": "Mauritius" + }, + { + "slug": "mexico", + "title": "Mexico" + }, + { + "slug": "moldova", + "title": "Moldova" + }, + { + "slug": "morocco", + "title": "Morocco" + }, + { + "slug": "mozambique", + "title": "Mozambique" + }, + { + "slug": "myanmar", + "title": "Myanmar" + }, + { + "slug": "nepal", + "title": "Nepal" + }, + { + "slug": "netherlands", + "title": "Netherlands" + }, + { + "slug": "new_zealand", + "title": "New Zealand" + }, + { + "slug": "nicaragua", + "title": "Nicaragua" + }, + { + "slug": "niger", + "title": "Niger" + }, + { + "slug": "nigeria", + "title": "Nigeria" + }, + { + "slug": "macedonia", + "title": "North Macedonia" + }, + { + "slug": "norway", + "title": "Norway" + }, + { + "slug": "oman", + "title": "Oman" + }, + { + "slug": "pakistan", + "title": "Pakistan" + }, + { + "slug": "palestine", + "title": "Palestine" + }, + { + "slug": "panama", + "title": "Panama" + }, + { + "slug": "papua_new_guinea", + "title": "Papua New Guinea" + }, + { + "slug": "paraguay", + "title": "Paraguay" + }, + { + "slug": "peru", + "title": "Peru" + }, + { + "slug": "philippines", + "title": "Philippines" + }, + { + "slug": "poland", + "title": "Poland" + }, + { + "slug": "portugal", + "title": "Portugal" + }, + { + "slug": "qatar", + "title": "Qatar" + }, + { + "slug": "south_korea", + "title": "Republic of Korea" + }, + { + "slug": "congo_brazzaville", + "title": "Republic of the Congo" + }, + { + "slug": "romania", + "title": "Romania" + }, + { + "slug": "russia", + "title": "Russia" + }, + { + "slug": "rwanda", + "title": "Rwanda" + }, + { + "slug": "saudi_arabia", + "title": "Saudi Arabia" + }, + { + "slug": "senegal", + "title": "Senegal" + }, + { + "slug": "serbia", + "title": "Serbia" + }, + { + "slug": "sierra_leone", + "title": "Sierra Leone" + }, + { + "slug": "singapore", + "title": "Singapore" + }, + { + "slug": "slovakia", + "title": "Slovakia" + }, + { + "slug": "slovenia", + "title": "Slovenia" + }, + { + "slug": "somalia", + "title": "Somalia" + }, + { + "slug": "south_africa", + "title": "South Africa" + }, + { + "slug": "south_sudan", + "title": "South Sudan" + }, + { + "slug": "spain", + "title": "Spain" + }, + { + "slug": "sri_lanka", + "title": "Sri Lanka" + }, + { + "slug": "sudan", + "title": "Sudan" + }, + { + "slug": "suriname", + "title": "Suriname" + }, + { + "slug": "sweden", + "title": "Sweden" + }, + { + "slug": "switzerland", + "title": "Switzerland" + }, + { + "slug": "syria", + "title": "Syria" + }, + { + "slug": "taiwan", + "title": "Taiwan" + }, + { + "slug": "tajikistan", + "title": "Tajikistan" + }, + { + "slug": "tanzania", + "title": "Tanzania" + }, + { + "slug": "thailand", + "title": "Thailand" + }, + { + "slug": "the_bahamas", + "title": "The Bahamas" + }, + { + "slug": "togo", + "title": "Togo" + }, + { + "slug": "tunisia", + "title": "Tunisia" + }, + { + "slug": "turkey", + "title": "Turkey" + }, + { + "slug": "turkmenistan", + "title": "Turkmenistan" + }, + { + "slug": "uganda", + "title": "Uganda" + }, + { + "slug": "ukraine", + "title": "Ukraine" + }, + { + "slug": "uae", + "title": "United Arab Emirates" + }, + { + "slug": "uk", + "title": "United Kingdom" + }, + { + "slug": "united_states", + "title": "United States" + }, + { + "slug": "uruguay", + "title": "Uruguay" + }, + { + "slug": "uzbekistan", + "title": "Uzbekistan" + }, + { + "slug": "venezuela", + "title": "Venezuela" + }, + { + "slug": "vietnam", + "title": "Vietnam" + }, + { + "slug": "worldwide", + "title": "Worldwide" + }, + { + "slug": "yemen", + "title": "Yemen" + }, + { + "slug": "zambia", + "title": "Zambia" + }, + { + "slug": "zimbabwe", + "title": "Zimbabwe" + } +] diff --git a/lib/country-flags.ts b/lib/country-flags.ts new file mode 100644 index 0000000..7228197 --- /dev/null +++ b/lib/country-flags.ts @@ -0,0 +1,162 @@ +// ─── Slug → ISO 3166-1 alpha-2 mapping ───────────────────────────────── +// Maps committers.top location slugs to ISO country codes for flag icons. +// Keep in sync with data/countries.json. + +const SLUG_TO_ISO: Record = { + afghanistan: "af", + albania: "al", + algeria: "dz", + angola: "ao", + argentina: "ar", + armenia: "am", + australia: "au", + austria: "at", + azerbaijan: "az", + bahrain: "bh", + bangladesh: "bd", + belarus: "by", + belgium: "be", + benin: "bj", + bolivia: "bo", + bosnia_and_herzegovina: "ba", + botswana: "bw", + brazil: "br", + bulgaria: "bg", + burkina_faso: "bf", + burundi: "bi", + cambodia: "kh", + cameroon: "cm", + canada: "ca", + chad: "td", + chile: "cl", + china: "cn", + colombia: "co", + congo_brazzaville: "cg", + congo_kinshasa: "cd", + costa_rica: "cr", + croatia: "hr", + cuba: "cu", + cyprus: "cy", + czech_republic: "cz", + denmark: "dk", + dominican_republic: "do", + ecuador: "ec", + egypt: "eg", + el_salvador: "sv", + estonia: "ee", + ethiopia: "et", + finland: "fi", + france: "fr", + gabon: "ga", + georgia: "ge", + germany: "de", + ghana: "gh", + greece: "gr", + guatemala: "gt", + guinea: "gn", + haiti: "ht", + honduras: "hn", + hong_kong: "hk", + hungary: "hu", + india: "in", + indonesia: "id", + iran: "ir", + iraq: "iq", + ireland: "ie", + israel: "il", + italy: "it", + ivory_coast: "ci", + japan: "jp", + jordan: "jo", + kazakhstan: "kz", + kenya: "ke", + kosovo: "xk", + kyrgyzstan: "kg", + laos: "la", + latvia: "lv", + lebanon: "lb", + libya: "ly", + lithuania: "lt", + luxembourg: "lu", + macau: "mo", + madagascar: "mg", + malawi: "mw", + malaysia: "my", + mali: "ml", + malta: "mt", + mauritania: "mr", + mauritius: "mu", + mexico: "mx", + moldova: "md", + morocco: "ma", + mozambique: "mz", + myanmar: "mm", + macedonia: "mk", + nepal: "np", + netherlands: "nl", + new_zealand: "nz", + nicaragua: "ni", + niger: "ne", + nigeria: "ng", + norway: "no", + oman: "om", + pakistan: "pk", + palestine: "ps", + panama: "pa", + papua_new_guinea: "pg", + paraguay: "py", + peru: "pe", + philippines: "ph", + poland: "pl", + portugal: "pt", + qatar: "qa", + south_korea: "kr", + romania: "ro", + russia: "ru", + rwanda: "rw", + saudi_arabia: "sa", + senegal: "sn", + serbia: "rs", + sierra_leone: "sl", + singapore: "sg", + slovakia: "sk", + slovenia: "si", + somalia: "so", + south_africa: "za", + south_sudan: "ss", + spain: "es", + sri_lanka: "lk", + sudan: "sd", + suriname: "sr", + sweden: "se", + switzerland: "ch", + syria: "sy", + taiwan: "tw", + tajikistan: "tj", + tanzania: "tz", + thailand: "th", + the_bahamas: "bs", + togo: "tg", + tunisia: "tn", + turkey: "tr", + turkmenistan: "tm", + uganda: "ug", + ukraine: "ua", + uae: "ae", + uk: "gb", + united_states: "us", + uruguay: "uy", + uzbekistan: "uz", + venezuela: "ve", + vietnam: "vn", + yemen: "ye", + zambia: "zm", + zimbabwe: "zw", +}; + +/** + * Get the ISO 3166-1 alpha-2 code for a committers.top country slug. + */ +export function getCountryCode(slug: string): string | null { + return SLUG_TO_ISO[slug] ?? null; +} diff --git a/lib/github.ts b/lib/github.ts index 4c2a369..d7aa5c7 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -49,17 +49,38 @@ type RawDiscussionNode = { }; }; +type PageInfo = { + hasNextPage: boolean; + endCursor: string | null; +}; + type FetchUserAndPullRequestsResponse = { user: GitHubRawUser | null; - pullRequests: { nodes: Array }; + pullRequests: { + nodes: Array; + pageInfo: PageInfo; + }; }; -type FetchIssuesResponse = { - issues: { nodes: Array }; +type FetchPullRequestsPageResponse = { + pullRequests: { + nodes: Array; + pageInfo: PageInfo; + }; }; -type FetchDiscussionsResponse = { - discussions: { nodes: Array }; +type FetchIssuesPageResponse = { + issues: { + nodes: Array; + pageInfo: PageInfo; + }; +}; + +type FetchDiscussionsPageResponse = { + discussions: { + nodes: Array; + pageInfo: PageInfo; + }; }; type GitHubQueryExecutor = Pick; @@ -71,13 +92,8 @@ export type GitHubFetcherDependencies = { logger?: Logger; }; -const USER_AND_PULL_REQUESTS_QUERY = /* GraphQL */ ` - query FetchUserAndPullRequests( - $login: String! - $repoCount: Int = 100 - $prCount: Int = 100 - $externalPrQuery: String! - ) { +const USER_QUERY = /* GraphQL */ ` + query FetchUser($login: String!, $repoCount: Int = 100) { user(login: $login) { name avatarUrl(size: 80) @@ -114,30 +130,31 @@ const USER_AND_PULL_REQUESTS_QUERY = /* GraphQL */ ` } } } + } +`; - pullRequests: search(query: $externalPrQuery, type: ISSUE, first: $prCount) { - nodes { - ... on PullRequest { - merged - additions - deletions - title - url - repository { - nameWithOwner - url - stargazerCount - pushedAt - owner { - login - } - languages(first: 5, orderBy: { field: SIZE, direction: DESC }) { - edges { - size - node { - name - } - } +const PULL_REQUESTS_SEARCH_FRAGMENT = ` + pageInfo { hasNextPage endCursor } + nodes { + ... on PullRequest { + merged + additions + deletions + title + url + repository { + nameWithOwner + url + stargazerCount + pushedAt + owner { + login + } + languages(first: 5, orderBy: { field: SIZE, direction: DESC }) { + edges { + size + node { + name } } } @@ -146,9 +163,18 @@ const USER_AND_PULL_REQUESTS_QUERY = /* GraphQL */ ` } `; +const PULL_REQUESTS_QUERY = /* GraphQL */ ` + query FetchUserPullRequests($prCount: Int = 100, $externalPrQuery: String!, $prCursor: String) { + pullRequests: search(query: $externalPrQuery, type: ISSUE, first: $prCount, after: $prCursor) { + ${PULL_REQUESTS_SEARCH_FRAGMENT} + } + } +`; + const ISSUES_QUERY = /* GraphQL */ ` - query FetchUserIssues($issueCount: Int = 20, $externalIssueQuery: String!) { - issues: search(query: $externalIssueQuery, type: ISSUE, first: $issueCount) { + query FetchUserIssues($issueCount: Int = 100, $externalIssueQuery: String!, $issueCursor: String) { + issues: search(query: $externalIssueQuery, type: ISSUE, first: $issueCount, after: $issueCursor) { + pageInfo { hasNextPage endCursor } nodes { ... on Issue { title @@ -171,14 +197,17 @@ const ISSUES_QUERY = /* GraphQL */ ` const DISCUSSIONS_QUERY = /* GraphQL */ ` query FetchUserDiscussions( - $discussionCount: Int = 10 + $discussionCount: Int = 100 $externalDiscussionQuery: String! + $discussionCursor: String ) { discussions: search( query: $externalDiscussionQuery type: DISCUSSION first: $discussionCount + after: $discussionCursor ) { + pageInfo { hasNextPage endCursor } nodes { ... on Discussion { title @@ -199,6 +228,65 @@ const DISCUSSIONS_QUERY = /* GraphQL */ ` } `; +// --------------------------------------------------------------------------- +// Search pagination helper +// --------------------------------------------------------------------------- + +type SearchPaginateParams = { + operationName: string; + query: string; + buildVariables: (cursor: string | null) => Record; + extractField: (data: Record) => { + nodes: Array; + pageInfo: PageInfo; + } | undefined; + maxPages?: number; +}; + +const SEARCH_PAGE_SIZE = 100; +const DEFAULT_MAX_SEARCH_PAGES = 10; + +async function paginateSearch( + executor: GitHubQueryExecutor, + params: SearchPaginateParams, +): Promise> { + const maxPages = params.maxPages ?? DEFAULT_MAX_SEARCH_PAGES; + const allNodes: Array = []; + let cursor: string | null = null; + + for (let page = 0; page < maxPages; page += 1) { + const variables = params.buildVariables(cursor); + const data = await executor.execute< + Record, + Record + >({ + operationName: params.operationName, + query: params.query, + variables, + }); + + const field = params.extractField(data); + if (!field) { + break; + } + + const pageNodes = field.nodes.filter(isDefined); + allNodes.push(...pageNodes); + + if (!field.pageInfo.hasNextPage || !field.pageInfo.endCursor) { + break; + } + + cursor = field.pageInfo.endCursor; + } + + return allNodes; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + function isDefined(value: T | null | undefined): value is T { return value !== null && value !== undefined; } @@ -275,6 +363,10 @@ export function buildGitHubUserCacheKey( return `${namespace}:github-user:${normalizeGitHubUsername(username)}`; } +// --------------------------------------------------------------------------- +// Core data fetch +// --------------------------------------------------------------------------- + async function fetchUserDataFromGitHub( executor: GitHubQueryExecutor, username: string, @@ -283,75 +375,76 @@ async function fetchUserDataFromGitHub( const externalIssueQuery = `type:issue author:${username} -user:${username}`; const externalDiscussionQuery = `author:${username} -user:${username}`; - const userAndPrResponse = - await executor.execute< - FetchUserAndPullRequestsResponse, - { - login: string; - repoCount: number; - prCount: number; - externalPrQuery: string; - } - >({ - operationName: "FetchUserAndPullRequests", - query: USER_AND_PULL_REQUESTS_QUERY, - variables: { - login: username, - repoCount: 30, - prCount: 80, - externalPrQuery, - }, - }); - - const user = userAndPrResponse.user; - if (!user) { - throw new Error("User not found"); - } - - const [issuesResponse, discussionsResponse] = await Promise.all([ + const [userResponse, pullRequests, issues, discussions] = await Promise.all([ executor.execute< - FetchIssuesResponse, - { - issueCount: number; - externalIssueQuery: string; - } + { user: GitHubRawUser | null }, + { login: string; repoCount: number } >({ + operationName: "FetchUser", + query: USER_QUERY, + variables: { login: username, repoCount: 30 }, + }), + paginateSearch(executor, { + operationName: "FetchUserPullRequests", + query: PULL_REQUESTS_QUERY, + buildVariables: (cursor) => ({ + prCount: SEARCH_PAGE_SIZE, + externalPrQuery, + prCursor: cursor, + }), + extractField: (data) => + data.pullRequests as + | { nodes: Array; pageInfo: PageInfo } + | undefined, + }), + paginateSearch(executor, { operationName: "FetchUserIssues", query: ISSUES_QUERY, - variables: { - issueCount: 20, + buildVariables: (cursor) => ({ + issueCount: SEARCH_PAGE_SIZE, externalIssueQuery, - }, + issueCursor: cursor, + }), + extractField: (data) => + data.issues as + | { nodes: Array; pageInfo: PageInfo } + | undefined, }), - executor.execute< - FetchDiscussionsResponse, - { - discussionCount: number; - externalDiscussionQuery: string; - } - >({ + paginateSearch(executor, { operationName: "FetchUserDiscussions", query: DISCUSSIONS_QUERY, - variables: { - discussionCount: 10, + buildVariables: (cursor) => ({ + discussionCount: SEARCH_PAGE_SIZE, externalDiscussionQuery, - }, + discussionCursor: cursor, + }), + extractField: (data) => + data.discussions as + | { nodes: Array; pageInfo: PageInfo } + | undefined, }), ]); + const user = userResponse.user; + if (!user) { + throw new Error("User not found"); + } + return { name: user.name, avatarUrl: user.avatarUrl, repos: user.repositories.nodes.filter(isDefined), - pullRequests: userAndPrResponse.pullRequests.nodes.filter(isDefined), + pullRequests, contributions: user.contributionsCollection, - issues: issuesResponse.issues.nodes.filter(isDefined).map(toIssueNode), - discussions: discussionsResponse.discussions.nodes - .filter(isDefined) - .map(toDiscussionNode), + issues: issues.map(toIssueNode), + discussions: discussions.map(toDiscussionNode), }; } +// --------------------------------------------------------------------------- +// Fetcher factory (caching + single-flight) +// --------------------------------------------------------------------------- + export function createGitHubUserDataFetcher( dependencies: GitHubFetcherDependencies, ): (username: string) => Promise { @@ -427,6 +520,10 @@ export function createGitHubUserDataFetcher( }; } +// --------------------------------------------------------------------------- +// Default singleton +// --------------------------------------------------------------------------- + function createDefaultGitHubExecutor(): GitHubQueryExecutor { const token = process.env.GITHUB_TOKEN?.trim(); if (!token) { diff --git a/locales/ar.json b/locales/ar.json index 1d205a4..f70b7f6 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -215,5 +215,33 @@ "a11y.openCommunityContribution": "افتح مساهمة المجتمع {title} على GitHub", "a11y.openProfile": "افتح ملف GitHub للمستخدم {name}", "a11y.openPullRequest": "افتح طلب السحب {title} على GitHub", - "a11y.openRepo": "افتح المستودع {name} على GitHub" + "a11y.openRepo": "افتح المستودع {name} على GitHub", + "leaderboard.header.eyebrow": "لوحة المتصدرين", + "leaderboard.header.title": "إعادة ترتيب المساهمين حسب الأثر", + "leaderboard.header.description": "اختر دولة لعرض المطورين مرتبين حسب أثر المصدر المفتوح الفعلي", + "leaderboard.title": "لوحة المتصدرين حسب الأثر", + "leaderboard.description": "تم تحليل {processed} مطور من أصل {total} مسجل على committers.top", + "leaderboard.impactRank": "ترتيب الأثر", + "leaderboard.developer": "المطور", + "leaderboard.committersRank": "ترتيب المساهمين", + "leaderboard.change": "التغير", + "leaderboard.topImpact": "المطور الأعلى أثرًا", + "leaderboard.impactScore": "درجة الأثر", + "leaderboard.error.title": "فشل تحميل لوحة المتصدرين", + "leaderboard.error.generic": "حدث خطأ أثناء تحميل لوحة المتصدرين. يرجى المحاولة لاحقًا.", + "leaderboard.error.upstreamError": "تعذر جلب البيانات. يرجى المحاولة بعد قليل.", + "leaderboard.partialErrors": "لم يمكن حساب درجة {count} مطورين", + "leaderboard.search": "البحث عن مطور...", + "leaderboard.pagination": "عرض {from}–{to} من {total}", + "leaderboard.back": "جميع الدول", + "leaderboard.loading": "جاري حساب درجات المطورين...", + "leaderboard.scoringHint": "قد يستغرق هذا لحظة عند التحميل الأول. يتم تخزين النتائج مؤقتًا للزيارات اللاحقة.", + "leaderboard.searchCountry": "البحث عن دول...", + "leaderboard.loadingCountries": "جاري تحميل الدول المتاحة...", + "leaderboard.empty.title": "لم يتم العثور على دول", + "leaderboard.empty.description": "ستظهر الدول من committers.top هنا.", + "leaderboard.noCountriesSearch": "لا توجد دول تطابق بحثك.", + "leaderboard.noDevelopersFor": "لم يتم العثور على مطورين في {title}", + "leaderboard.error.retry": "حاول مرة أخرى", + "leaderboard.viewCountry": "عرض لوحة المتصدرين" } diff --git a/locales/en.json b/locales/en.json index 1d617a0..067be4b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -215,5 +215,33 @@ "a11y.openCommunityContribution": "Open community contribution {title} on GitHub", "a11y.openProfile": "Open GitHub profile for {name}", "a11y.openPullRequest": "Open pull request {title} on GitHub", - "a11y.openRepo": "Open repository {name} on GitHub" + "a11y.openRepo": "Open repository {name} on GitHub", + "leaderboard.header.eyebrow": "Country Leaderboard", + "leaderboard.header.title": "Reorder Committers by Impact", + "leaderboard.header.description": "Select a country to view developers ranked by real open-source impact", + "leaderboard.title": "Impact Leaderboard", + "leaderboard.description": "{processed} developers analyzed out of {total} total on committers.top", + "leaderboard.impactRank": "Impact Rank", + "leaderboard.developer": "Developer", + "leaderboard.committersRank": "Committers Rank", + "leaderboard.change": "Change", + "leaderboard.topImpact": "Highest Impact Developer", + "leaderboard.impactScore": "Impact Score", + "leaderboard.error.title": "Leaderboard load failed", + "leaderboard.error.generic": "Something went wrong while loading the leaderboard. Please try again later.", + "leaderboard.error.upstreamError": "Failed to fetch data. Please try again shortly.", + "leaderboard.partialErrors": "{count} developers could not be scored", + "leaderboard.search": "Search developer...", + "leaderboard.pagination": "Showing {from}–{to} of {total}", + "leaderboard.back": "All countries", + "leaderboard.loading": "Scoring developers…", + "leaderboard.scoringHint": "This may take a moment on first load. Results are cached for subsequent visits.", + "leaderboard.searchCountry": "Search countries...", + "leaderboard.loadingCountries": "Loading available countries...", + "leaderboard.empty.title": "No countries found", + "leaderboard.empty.description": "Countries from committers.top will appear here.", + "leaderboard.noCountriesSearch": "No countries match your search.", + "leaderboard.noDevelopersFor": "No developers found for {title}", + "leaderboard.error.retry": "Try again", + "leaderboard.viewCountry": "View leaderboard" } diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package.json b/package.json index ccf9f3b..fa8edcf 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "@octokit/graphql": "^9.0.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "flag-icons": "^7.5.0", + "js-yaml": "^4.1.1", "lucide-react": "^1.7.0", "next": "^16.2.6", "next-themes": "^0.3.0", @@ -29,6 +31,7 @@ "tailwind-merge": "^2.5.3" }, "devDependencies": { + "@types/js-yaml": "^4.0.9", "@types/node": "^25.5.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e083670..bf6a652 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,12 +17,18 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + flag-icons: + specifier: ^7.5.0 + version: 7.5.0 + js-yaml: + specifier: ^4.1.1 + version: 4.1.1 lucide-react: specifier: ^1.7.0 version: 1.8.0(react@19.2.5) next: specifier: ^16.2.6 - version: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -48,6 +54,9 @@ importers: specifier: ^2.5.3 version: 2.6.1 devDependencies: + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 '@types/node': specifier: ^25.5.2 version: 25.6.0 @@ -270,105 +279,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -441,28 +434,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@16.2.6': resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@16.2.6': resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@16.2.6': resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@16.2.6': resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} @@ -517,6 +506,11 @@ packages: '@oxc-project/types@0.128.0': resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==} + '@playwright/test@1.60.0': + resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} + engines: {node: '>=18'} + hasBin: true + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -1278,42 +1272,36 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': resolution: {integrity: sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': resolution: {integrity: sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': resolution: {integrity: sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': resolution: {integrity: sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': resolution: {integrity: sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': resolution: {integrity: sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==} @@ -1389,6 +1377,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1504,49 +1495,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -2148,6 +2131,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + flag-icons@7.5.0: + resolution: {integrity: sha512-kd+MNXviFIg5hijH766tt+3x76ele1AXlo4zDdCxIvqWZhKt4T83bOtxUOOMlTx/EcFdUMH5yvQgYlFh1EqqFg==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -2162,6 +2148,11 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2492,28 +2483,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -2734,6 +2721,16 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -3757,6 +3754,11 @@ snapshots: '@oxc-project/types@0.128.0': {} + '@playwright/test@1.60.0': + dependencies: + playwright: 1.60.0 + optional: true + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -4621,6 +4623,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/js-yaml@4.0.9': {} + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -5531,6 +5535,8 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + flag-icons@7.5.0: {} + flat-cache@4.0.1: dependencies: flatted: 3.4.2 @@ -5544,6 +5550,9 @@ snapshots: fraction.js@5.3.4: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -5950,7 +5959,7 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + next@16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@next/env': 16.2.6 '@swc/helpers': 0.5.15 @@ -5969,6 +5978,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.2.6 '@next/swc-win32-arm64-msvc': 16.2.6 '@next/swc-win32-x64-msvc': 16.2.6 + '@playwright/test': 1.60.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -6076,6 +6086,16 @@ snapshots: pirates@4.0.7: {} + playwright-core@1.60.0: + optional: true + + playwright@1.60.0: + dependencies: + playwright-core: 1.60.0 + optionalDependencies: + fsevents: 2.3.2 + optional: true + possible-typed-array-names@1.1.0: {} postcss-import@15.1.0(postcss@8.5.14): diff --git a/test/github/github-cache.test.ts b/test/github/github-cache.test.ts index 7c744b1..29d5a87 100644 --- a/test/github/github-cache.test.ts +++ b/test/github/github-cache.test.ts @@ -35,7 +35,7 @@ function makeExecutor( await new Promise((resolve) => setTimeout(resolve, delayMs)); } - if (params.operationName === "FetchUserAndPullRequests") { + if (params.operationName === "FetchUser") { return { user: { name: "User Name", @@ -63,6 +63,11 @@ function makeExecutor( totalIssueContributions: 0, }, }, + } as unknown as TData; + } + + if (params.operationName === "FetchUserPullRequests") { + return { pullRequests: { nodes: [ { @@ -83,6 +88,7 @@ function makeExecutor( }, }, ], + pageInfo: { hasNextPage: false, endCursor: null }, }, } as unknown as TData; } @@ -102,6 +108,7 @@ function makeExecutor( }, }, ], + pageInfo: { hasNextPage: false, endCursor: null }, }, } as unknown as TData; } @@ -121,6 +128,7 @@ function makeExecutor( }, }, ], + pageInfo: { hasNextPage: false, endCursor: null }, }, } as unknown as TData; } @@ -224,7 +232,7 @@ describe("GitHub user data caching", () => { const result = await fetcher("TeStUser"); expect(isGitHubUserData(result)).toBe(true); - expect(calls).toHaveLength(3); + expect(calls).toHaveLength(4); expect(setCalls).toHaveLength(1); expect(setCalls[0]?.ttl).toBe(604_800); expect(setCalls[0]?.key).toBe("devimpact:v1:github-user:testuser"); @@ -247,7 +255,7 @@ describe("GitHub user data caching", () => { const result = await fetcher("testuser"); expect(result.name).toBe("User Name"); - expect(calls).toHaveLength(3); + expect(calls).toHaveLength(4); }); test("cache write failure does not fail request", async () => { @@ -268,7 +276,7 @@ describe("GitHub user data caching", () => { const result = await fetcher("testuser"); expect(result.pullRequests).toHaveLength(1); - expect(calls).toHaveLength(3); + expect(calls).toHaveLength(4); }); test("corrupted cache payload is treated as miss", async () => { @@ -290,7 +298,7 @@ describe("GitHub user data caching", () => { const result = await fetcher("testuser"); expect(result.name).toBe("User Name"); - expect(calls).toHaveLength(3); + expect(calls).toHaveLength(4); expect(deleted).toEqual(["devimpact:v1:github-user:testuser"]); }); @@ -313,7 +321,7 @@ describe("GitHub user data caching", () => { ]); expect(first.avatarUrl).toBe(second.avatarUrl); - expect(calls).toHaveLength(3); + expect(calls).toHaveLength(4); }); test("default cache TTL is seven days", () => { diff --git a/test/seo/seo.test.ts b/test/seo/seo.test.ts index b80a6f3..7bec353 100644 --- a/test/seo/seo.test.ts +++ b/test/seo/seo.test.ts @@ -42,8 +42,8 @@ describe("seo routes", () => { expect(result.sitemap).toContain("/sitemap.xml"); }); - test("sitemap includes key public pages", () => { - const result = sitemap(); + test("sitemap includes key public pages", async () => { + const result = await sitemap(); const urls = result.map((entry) => entry.url); expect(urls).toContain("http://localhost:3000/"); diff --git a/types/leaderboard.ts b/types/leaderboard.ts new file mode 100644 index 0000000..e75c2f1 --- /dev/null +++ b/types/leaderboard.ts @@ -0,0 +1,4 @@ +export type CountryInfo = { + slug: string; + title: string; +}; From 487f40e79a7183d4664cb6044525e6cc2a4380e1 Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Sun, 12 Jul 2026 02:57:55 +0300 Subject: [PATCH 02/10] feat: separate the leaderboard logic to two endpoints one for calculate and another for the result --- app/api/calculate-leaderboard/route.ts | 172 +++++++++++++++++++++++++ app/api/leaderboard/route.ts | 76 +++++++++++ app/api/score/route.ts | 56 -------- app/leaderboard/[country]/page.tsx | 93 +++---------- components/app-header.tsx | 10 +- data/countries.json | 4 - lib/country-flags.ts | 1 - 7 files changed, 275 insertions(+), 137 deletions(-) create mode 100644 app/api/calculate-leaderboard/route.ts create mode 100644 app/api/leaderboard/route.ts delete mode 100644 app/api/score/route.ts diff --git a/app/api/calculate-leaderboard/route.ts b/app/api/calculate-leaderboard/route.ts new file mode 100644 index 0000000..2291651 --- /dev/null +++ b/app/api/calculate-leaderboard/route.ts @@ -0,0 +1,172 @@ +import { NextResponse } from "next/server"; +import yaml from "js-yaml"; +import { fetchGitHubUserData } from "@/lib/github"; +import { calculateUserScore } from "@/lib/score"; +import { createCacheStore, getCacheConfigFromEnv } from "@/lib/cache-store"; + +export const runtime = "nodejs"; + +// ─── Types ───────────────────────────────────────────────────────────── + +type CommitterEntry = { + rank: number; + name: string; + login: string; + avatarUrl: string; + contributions: number; +}; + +type CommitterYaml = { + title?: string; + total_user_count?: number; + users?: CommitterEntry[]; +}; + +type ScoredEntry = { + username: string; + name: string | null; + avatarUrl: string; + repoScore: number; + prScore: number; + contributionScore: number; + finalScore: number; + originalRank: number; + originalContributions: number; + impactRank: number; +}; + +type LeaderboardResult = { + title: string; + totalFromSource: number; + scored: ScoredEntry[]; + errors: string[]; +}; + +// ─── Helpers ──────────────────────────────────────────────────────────── + +function buildLeaderboardCacheKey(country: string, namespace: string): string { + return `${namespace}:leaderboard:${country.trim().toLowerCase()}`; +} + +// ─── POST handler ────────────────────────────────────────────────────── + +export async function POST(request: Request) { + const { searchParams } = new URL(request.url); + const country = searchParams.get("country")?.trim(); + + if (!country) { + return NextResponse.json( + { success: false, error: "Provide a country parameter" }, + { status: 400 }, + ); + } + + // ── 1. Fetch committers from committers.top ────────────────────────── + let committersData: { + title: string; + totalFromSource: number; + users: CommitterEntry[]; + }; + try { + const url = `https://raw.githubusercontent.com/ashkulz/committers.top/gh-pages/_data/locations/${country}.yml`; + const res = await fetch(url, { + headers: { "User-Agent": "DevImpact-Bot" }, + }); + if (!res.ok) { + return NextResponse.json( + { + success: false, + error: `Failed to fetch committers data for "${country}"`, + }, + { status: 502 }, + ); + } + const text = await res.text(); + const data = yaml.load(text) as CommitterYaml; + if (!data?.users) { + return NextResponse.json( + { + success: false, + error: `Invalid or empty committers data for "${country}"`, + }, + { status: 502 }, + ); + } + committersData = { + title: data.title || country, + totalFromSource: data.total_user_count ?? data.users.length, + users: data.users, + }; + } catch (err) { + return NextResponse.json( + { + success: false, + error: + err instanceof Error + ? err.message + : "Failed to fetch committers data", + }, + { status: 502 }, + ); + } + + // ── 2. Calculate scores for all users ──────────────────────────────── + const scored: ScoredEntry[] = []; + const errors: string[] = []; + + // Build rank lookup from original committers data + const rankMap = new Map( + committersData.users.map((u) => [ + u.login, + { rank: u.rank, contributions: u.contributions }, + ]), + ); + + for (const user of committersData.users) { + try { + const data = await fetchGitHubUserData(user.login); + const score = calculateUserScore(data, user.login); + const original = rankMap.get(user.login) ?? { rank: 0, contributions: 0 }; + scored.push({ + username: user.login, + name: data.name, + avatarUrl: data.avatarUrl, + repoScore: Math.round(score.repoScore), + prScore: Math.round(score.prScore), + contributionScore: Math.round(score.contributionScore), + finalScore: Math.round(score.finalScore), + originalRank: original.rank, + originalContributions: original.contributions, + impactRank: 0, + }); + } catch { + errors.push(user.login); + } + } + + // Sort by finalScore descending and assign impactRank + scored.sort((a, b) => b.finalScore - a.finalScore); + scored.forEach((u, idx) => (u.impactRank = idx + 1)); + + const result: LeaderboardResult = { + title: committersData.title, + totalFromSource: committersData.totalFromSource, + scored, + errors, + }; + + // ── 3. Store in Redis cache ────────────────────────────────────────── + try { + const cacheConfig = getCacheConfigFromEnv(); + const cacheStore = createCacheStore(cacheConfig); + if (cacheStore.enabled) { + const cacheKey = buildLeaderboardCacheKey(country, cacheConfig.namespace); + await cacheStore.set(cacheKey, result, cacheConfig.ttlSeconds); + } + } catch { + // Non-fatal: cache write failure should not block the response + console.warn("Failed to cache leaderboard result"); + } + + return NextResponse.json({ success: true, ...result }); +} diff --git a/app/api/leaderboard/route.ts b/app/api/leaderboard/route.ts new file mode 100644 index 0000000..988ee43 --- /dev/null +++ b/app/api/leaderboard/route.ts @@ -0,0 +1,76 @@ +import { NextResponse } from "next/server"; +import { + createCacheStore, + getCacheConfigFromEnv, +} from "@/lib/cache-store"; + +export const runtime = "nodejs"; + +// ─── Types ───────────────────────────────────────────────────────────── + +type ScoredEntry = { + username: string; + name: string | null; + avatarUrl: string; + repoScore: number; + prScore: number; + contributionScore: number; + finalScore: number; + originalRank: number; + originalContributions: number; + impactRank: number; +}; + +type LeaderboardResult = { + title: string; + totalFromSource: number; + scored: ScoredEntry[]; + errors: string[]; +}; + +// ─── Helpers ──────────────────────────────────────────────────────────── + +function buildLeaderboardCacheKey(country: string, namespace: string): string { + return `${namespace}:leaderboard:${country.trim().toLowerCase()}`; +} + +// ─── GET handler ─────────────────────────────────────────────────────── + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const country = searchParams.get("country")?.trim(); + + if (!country) { + return NextResponse.json( + { success: false, error: "Provide a country parameter" }, + { status: 400 }, + ); + } + + // Try to fetch from cache + try { + const cacheConfig = getCacheConfigFromEnv(); + const cacheStore = createCacheStore(cacheConfig); + + if (cacheStore.enabled) { + const cacheKey = buildLeaderboardCacheKey(country, cacheConfig.namespace); + const cached = await cacheStore.get(cacheKey); + + if (cached) { + return NextResponse.json({ success: true, ...cached }); + } + } + } catch { + // Non-fatal: cache read failure should not block the response + console.warn("Failed to read leaderboard from cache"); + } + + // No cached data found — return empty result + return NextResponse.json({ + success: true, + title: country, + totalFromSource: 0, + scored: [], + errors: [], + }); +} \ No newline at end of file diff --git a/app/api/score/route.ts b/app/api/score/route.ts deleted file mode 100644 index e1fd639..0000000 --- a/app/api/score/route.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { NextResponse } from "next/server"; -import { fetchGitHubUserData } from "@/lib/github"; -import { calculateUserScore } from "@/lib/score"; - -export const runtime = "nodejs"; - -export async function GET(request: Request) { - const { searchParams } = new URL(request.url); - const usernames = searchParams.getAll("username").map((u) => u.trim()).filter(Boolean); - - if (usernames.length === 0) { - return NextResponse.json( - { success: false, error: "Provide at least one username" }, - { status: 400 }, - ); - } - - if (usernames.length > 256) { - return NextResponse.json( - { success: false, error: "Maximum 256 usernames per request" }, - { status: 400 }, - ); - } - - const scored: Array<{ - username: string; - name: string | null; - avatarUrl: string; - repoScore: number; - prScore: number; - contributionScore: number; - finalScore: number; - }> = []; - - const errors: string[] = []; - - for (const username of usernames) { - try { - const data = await fetchGitHubUserData(username); - const score = calculateUserScore(data, username); - scored.push({ - username, - name: data.name, - avatarUrl: data.avatarUrl, - repoScore: Math.round(score.repoScore), - prScore: Math.round(score.prScore), - contributionScore: Math.round(score.contributionScore), - finalScore: Math.round(score.finalScore), - }); - } catch { - errors.push(username); - } - } - - return NextResponse.json({ success: true, scored, errors }); -} diff --git a/app/leaderboard/[country]/page.tsx b/app/leaderboard/[country]/page.tsx index 09c8067..b469db4 100644 --- a/app/leaderboard/[country]/page.tsx +++ b/app/leaderboard/[country]/page.tsx @@ -3,7 +3,6 @@ import { useEffect, useState, use } from "react"; import Link from "next/link"; import { ArrowLeft, Loader2 } from "lucide-react"; -import yaml from "js-yaml"; import { LeaderboardTable } from "@/components/leaderboard-table"; import { AppHeader } from "@/components/app-header"; import { AppFooter } from "@/components/app-footer"; @@ -12,20 +11,6 @@ import { useTranslation } from "@/components/language-provider"; // ─── Types ───────────────────────────────────────────────────────────── -type CommitterEntry = { - rank: number; - name: string; - login: string; - avatarUrl: string; - contributions: number; -}; - -type CommitterYaml = { - title?: string; - total_user_count?: number; - users?: CommitterEntry[]; -}; - type ScoredEntry = { username: string; name: string | null; @@ -39,37 +24,26 @@ type ScoredEntry = { impactRank: number; }; +type LeaderboardApiResponse = { + success: boolean; + title: string; + totalFromSource: number; + scored: ScoredEntry[]; + errors: string[]; +}; + type Props = { params: Promise<{ country: string }>; }; // ─── Fetch helpers ───────────────────────────────────────────────────── -async function fetchCommiters(country: string) { - const url = `https://raw.githubusercontent.com/ashkulz/committers.top/gh-pages/_data/locations/${country}.yml`; - const res = await fetch(url, { - headers: { "User-Agent": "DevImpact-Bot" }, - }); - if (!res.ok) throw new Error(`Failed to fetch ${country}`); - const text = await res.text(); - const data = yaml.load(text) as CommitterYaml; - if (!data?.users) throw new Error("Invalid data"); - return { - title: data.title || country, - totalFromSource: data.total_user_count ?? data.users.length, - users: data.users, - }; -} - -async function fetchScores(logins: string[]) { - const params = logins - .map((u) => `username=${encodeURIComponent(u)}`) - .join("&"); - const res = await fetch(`/api/score?${params}`); - if (!res.ok) throw new Error("Failed to score users"); +async function fetchLeaderboard(country: string): Promise { + const res = await fetch(`/api/leaderboard?country=${encodeURIComponent(country)}`); + if (!res.ok) throw new Error(`Failed to fetch leaderboard for ${country}`); const json = await res.json(); - if (!json.success) throw new Error(json.error || "Scoring failed"); - return { scored: json.scored, errors: json.errors }; + if (!json.success) throw new Error(json.error || "Failed to load leaderboard"); + return json; } // ─── Component ───────────────────────────────────────────────────────── @@ -90,47 +64,18 @@ export default function CountryLeaderboardPage({ params }: Props) { (async () => { try { - const data = await fetchCommiters(country); + const data = await fetchLeaderboard(country); if (cancelled) return; + setTitle(data.title); setTotalFromSource(data.totalFromSource); - - const logins = data.users.map((u) => u.login); - const { scored: apiScored, errors: apiErrors } = - await fetchScores(logins); - if (cancelled) return; - - // Build a rank lookup from the original committers data - const rankMap = new Map( - data.users.map((u) => [u.login, { rank: u.rank, contributions: u.contributions }]) - ); - - const results: ScoredEntry[] = apiScored.map((s: Record) => { - const original = rankMap.get(s.username as string) ?? { rank: 0, contributions: 0 }; - return { - username: s.username as string, - name: s.name as string | null, - avatarUrl: s.avatarUrl as string, - repoScore: s.repoScore as number, - prScore: s.prScore as number, - contributionScore: s.contributionScore as number, - finalScore: s.finalScore as number, - originalRank: original.rank, - originalContributions: original.contributions, - impactRank: 0, - }; - }); - - results.sort((a, b) => b.finalScore - a.finalScore); - results.forEach((u, idx) => (u.impactRank = idx + 1)); - - setScored(results); - setErrors(apiErrors); + setScored(data.scored); + setErrors(data.errors); setLoading(false); } catch (err) { if (!cancelled) setFailed( - err instanceof Error ? err.message : "Failed to load leaderboard" + err instanceof Error ? err.message : "Failed to load leaderboard", ); } })(); @@ -218,4 +163,4 @@ export default function CountryLeaderboardPage({ params }: Props) { ); -} +} \ No newline at end of file diff --git a/components/app-header.tsx b/components/app-header.tsx index 437486c..488b01e 100644 --- a/components/app-header.tsx +++ b/components/app-header.tsx @@ -12,11 +12,17 @@ export function AppHeader() { -
+
+ ); diff --git a/data/countries.json b/data/countries.json index 403d9f9..f6ebd25 100644 --- a/data/countries.json +++ b/data/countries.json @@ -235,10 +235,6 @@ "slug": "ireland", "title": "Ireland" }, - { - "slug": "israel", - "title": "Israel" - }, { "slug": "italy", "title": "Italy" diff --git a/lib/country-flags.ts b/lib/country-flags.ts index 7228197..28d50d0 100644 --- a/lib/country-flags.ts +++ b/lib/country-flags.ts @@ -63,7 +63,6 @@ const SLUG_TO_ISO: Record = { iran: "ir", iraq: "iq", ireland: "ie", - israel: "il", italy: "it", ivory_coast: "ci", japan: "jp", From 8067e4fc310f153baa24e644dfa8e270bf95ac9e Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Thu, 16 Jul 2026 01:29:33 +0300 Subject: [PATCH 03/10] feat: add location detection for GitHub users and initialize PostgreSQL schema - Enhanced GitHub user data model to include `login` and `location` fields. - Implemented a location detector to normalize user-provided locations into country slugs. - Updated GraphQL queries to fetch the new `login` and `location` fields. - Increased default pull request count from 250 to 300. - Added a script to initialize the PostgreSQL database schema. - Updated package dependencies, including adding `pg` for PostgreSQL support. - Refactored error handling and improved code formatting for better readability. Co-authored-by: Copilot --- app/api/calculate-leaderboard/route.ts | 160 +++++++-- app/api/compare/route.ts | 45 ++- app/api/leaderboard/route.ts | 101 +++++- data/countries.json | 450 ++++++++++++++++--------- docker-compose.yml | 21 ++ lib/db-store.ts | 271 +++++++++++++++ lib/github-graphql-client.ts | 26 +- lib/github.ts | 8 +- lib/location-detector.ts | 102 ++++++ next-env.d.ts | 2 +- package.json | 2 + pnpm-lock.yaml | 160 +++++++++ scripts/init-db.ts | 40 +++ types/github.ts | 2 + 14 files changed, 1186 insertions(+), 204 deletions(-) create mode 100644 lib/db-store.ts create mode 100644 lib/location-detector.ts create mode 100644 scripts/init-db.ts diff --git a/app/api/calculate-leaderboard/route.ts b/app/api/calculate-leaderboard/route.ts index 2291651..80c8f2a 100644 --- a/app/api/calculate-leaderboard/route.ts +++ b/app/api/calculate-leaderboard/route.ts @@ -3,6 +3,8 @@ import yaml from "js-yaml"; import { fetchGitHubUserData } from "@/lib/github"; import { calculateUserScore } from "@/lib/score"; import { createCacheStore, getCacheConfigFromEnv } from "@/lib/cache-store"; +import { getDatabaseStore } from "@/lib/db-store"; +import { detectCountry } from "@/lib/location-detector"; export const runtime = "nodejs"; @@ -48,6 +50,25 @@ function buildLeaderboardCacheKey(country: string, namespace: string): string { return `${namespace}:leaderboard:${country.trim().toLowerCase()}`; } +function getEnvInt(key: string, fallback: number): number { + const raw = process.env[key]?.trim(); + if (!raw) return fallback; + const parsed = Number.parseInt(raw, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +} + +function getStaleDays(): number { + return getEnvInt("LEADERBOARD_USER_STALE_DAYS", 30); +} + +function getSeedLimit(): number { + return getEnvInt("LEADERBOARD_SEED_LIMIT", 256); +} + +function getRefreshLimit(): number { + return getEnvInt("LEADERBOARD_REFRESH_LIMIT", 500); +} + // ─── POST handler ────────────────────────────────────────────────────── export async function POST(request: Request) { @@ -61,6 +82,10 @@ export async function POST(request: Request) { ); } + // Ensure database schema exists + const db = getDatabaseStore(); + await db.initializeSchema(); + // ── 1. Fetch committers from committers.top ────────────────────────── let committersData: { title: string; @@ -110,63 +135,144 @@ export async function POST(request: Request) { ); } - // ── 2. Calculate scores for all users ──────────────────────────────── - const scored: ScoredEntry[] = []; + // ── 2a. Seed NEW users from committers.top ─────────────────────────── const errors: string[] = []; + const seedLimit = getSeedLimit(); + const usersToSeed = committersData.users.slice(0, seedLimit); + let newUsersCount = 0; + let skippedExistingCount = 0; - // Build rank lookup from original committers data - const rankMap = new Map( - committersData.users.map((u) => [ - u.login, - { rank: u.rank, contributions: u.contributions }, - ]), - ); - - for (const user of committersData.users) { + for (const user of usersToSeed) { try { + const exists = await db.userExists(user.login); + if (exists) { + skippedExistingCount += 1; + continue; + } + const data = await fetchGitHubUserData(user.login); const score = calculateUserScore(data, user.login); - const original = rankMap.get(user.login) ?? { rank: 0, contributions: 0 }; - scored.push({ - username: user.login, + const countryDetected = detectCountry(data.location); + + await db.upsertUser({ + username: data.login, name: data.name, avatarUrl: data.avatarUrl, + location: data.location, + country: countryDetected, + rawData: data, + scores: score, repoScore: Math.round(score.repoScore), prScore: Math.round(score.prScore), contributionScore: Math.round(score.contributionScore), finalScore: Math.round(score.finalScore), - originalRank: original.rank, - originalContributions: original.contributions, - impactRank: 0, + staleDays: getStaleDays(), }); + + newUsersCount += 1; } catch { errors.push(user.login); } } - // Sort by finalScore descending and assign impactRank + // ── 2b. Refresh STALE users from DB top N ─────────────────────────── + const refreshLimit = getRefreshLimit(); + const topUsers = await db.getTopUsers(country, refreshLimit); + let refreshedCount = 0; + + for (const row of topUsers) { + // Check if stale + if (row.stale_after >= new Date()) { + continue; // still fresh + } + + try { + const data = await fetchGitHubUserData(row.username); + const score = calculateUserScore(data, row.username); + const countryDetected = detectCountry(data.location); + + await db.upsertUser({ + username: data.login, + name: data.name, + avatarUrl: data.avatarUrl, + location: data.location, + country: countryDetected, + rawData: data, + scores: score, + repoScore: Math.round(score.repoScore), + prScore: Math.round(score.prScore), + contributionScore: Math.round(score.contributionScore), + finalScore: Math.round(score.finalScore), + staleDays: getStaleDays(), + }); + + refreshedCount += 1; + } catch { + errors.push(row.username); + } + } + + // ── 3. Build & cache the leaderboard result ────────────────────────── + const allUsers = await db.getLeaderboard(country, getEnvInt("LEADERBOARD_DISPLAY_LIMIT", 500)); + const totalCount = await db.getLeaderboardCount(country); + + // Build rank lookup from original committers data + const rankMap = new Map( + committersData.users.map((u) => [ + u.login, + { rank: u.rank, contributions: u.contributions }, + ]), + ); + + const scored: ScoredEntry[] = allUsers.map((row) => { + const original = rankMap.get(row.username) ?? { rank: 0, contributions: 0 }; + return { + username: row.username, + name: row.name, + avatarUrl: row.avatar_url, + repoScore: row.repo_score, + prScore: row.pr_score, + contributionScore: row.contribution_score, + finalScore: row.final_score, + originalRank: original.rank, + originalContributions: original.contributions, + impactRank: 0, + }; + }); + + // Sort and assign impactRank scored.sort((a, b) => b.finalScore - a.finalScore); scored.forEach((u, idx) => (u.impactRank = idx + 1)); const result: LeaderboardResult = { title: committersData.title, - totalFromSource: committersData.totalFromSource, + totalFromSource: totalCount, scored, - errors, + errors: [...new Set(errors)], // deduplicate }; - // ── 3. Store in Redis cache ────────────────────────────────────────── + // ── 4. Invalidate Redis cache (lazy rebuild on next GET) ───────────── try { const cacheConfig = getCacheConfigFromEnv(); const cacheStore = createCacheStore(cacheConfig); - if (cacheStore.enabled) { + if (cacheStore.enabled && cacheStore.del) { const cacheKey = buildLeaderboardCacheKey(country, cacheConfig.namespace); - await cacheStore.set(cacheKey, result, cacheConfig.ttlSeconds); + await cacheStore.del(cacheKey); } } catch { - // Non-fatal: cache write failure should not block the response - console.warn("Failed to cache leaderboard result"); + // Non-fatal: cache invalidation failure should not block the response + console.warn("Failed to invalidate leaderboard cache"); } - return NextResponse.json({ success: true, ...result }); -} + return NextResponse.json({ + success: true, + ...result, + _meta: { + newUsers: newUsersCount, + refreshedUsers: refreshedCount, + skippedExisting: skippedExistingCount, + errors: errors.length, + totalInDb: totalCount, + }, + }); +} \ No newline at end of file diff --git a/app/api/compare/route.ts b/app/api/compare/route.ts index afeb6f8..e4cb70b 100644 --- a/app/api/compare/route.ts +++ b/app/api/compare/route.ts @@ -3,6 +3,12 @@ import { fetchGitHubUserData } from "../../../lib/github"; import { calculateUserScore } from "../../../lib/score"; import { normalizeSelectedLanguages } from "@/lib/scoring/languageScoring"; import { toSafeApiError } from "@/lib/github-graphql-client"; +import { getDatabaseStore } from "@/lib/db-store"; +import { + createCacheStore, + getCacheConfigFromEnv, +} from "@/lib/cache-store"; +import { detectCountry } from "@/lib/location-detector"; import type { CompareInsights, SafeApiError } from "@/types/api-response"; import { DEFAULT_LOCALE, @@ -322,7 +328,7 @@ async function compareUsers( ); results.push({ - username, + username: data.login, name: data.name, avatarUrl: data.avatarUrl, repoScore: Math.round(score.repoScore), @@ -340,6 +346,43 @@ async function compareUsers( signals: score.signals, explanations: score.explanations, }); + + // ── Fire-and-forget: detect country & upsert into DB ────────────── + const country = detectCountry(data.location); + if (country) { + const staleDays = parseInt( + process.env.GITHUB_USER_STALE_DAYS ?? "14", + 10, + ); + + const db = getDatabaseStore(); + db.upsertUser({ + username: data.login, + name: data.name, + avatarUrl: data.avatarUrl, + location: data.location, + country, + rawData: data, + scores: score, + repoScore: Math.round(score.repoScore), + prScore: Math.round(score.prScore), + contributionScore: Math.round(score.contributionScore), + finalScore: Math.round(score.finalScore), + staleDays, + }) + .then(() => { + // Invalidate Redis cache for this country + const cacheConfig = getCacheConfigFromEnv(); + const cacheStore = createCacheStore(cacheConfig); + if (cacheStore.enabled && cacheStore.del) { + const key = `${cacheConfig.namespace}:leaderboard:${country}`; + cacheStore.del(key).catch(() => {}); + } + }) + .catch((err: unknown) => { + console.warn("Failed to upsert user from compare:", err); + }); + } } return results; diff --git a/app/api/leaderboard/route.ts b/app/api/leaderboard/route.ts index 988ee43..dade668 100644 --- a/app/api/leaderboard/route.ts +++ b/app/api/leaderboard/route.ts @@ -3,6 +3,7 @@ import { createCacheStore, getCacheConfigFromEnv, } from "@/lib/cache-store"; +import { getDatabaseStore } from "@/lib/db-store"; export const runtime = "nodejs"; @@ -34,6 +35,13 @@ function buildLeaderboardCacheKey(country: string, namespace: string): string { return `${namespace}:leaderboard:${country.trim().toLowerCase()}`; } +function getDisplayLimit(): number { + const raw = process.env.LEADERBOARD_DISPLAY_LIMIT?.trim(); + if (!raw) return 500; + const parsed = Number.parseInt(raw, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : 500; +} + // ─── GET handler ─────────────────────────────────────────────────────── export async function GET(request: Request) { @@ -47,30 +55,89 @@ export async function GET(request: Request) { ); } - // Try to fetch from cache - try { - const cacheConfig = getCacheConfigFromEnv(); - const cacheStore = createCacheStore(cacheConfig); + // ── 1. Try Redis cache first ───────────────────────────────────────── + const cacheConfig = getCacheConfigFromEnv(); + const cacheStore = createCacheStore(cacheConfig); - if (cacheStore.enabled) { + if (cacheStore.enabled) { + try { const cacheKey = buildLeaderboardCacheKey(country, cacheConfig.namespace); const cached = await cacheStore.get(cacheKey); - if (cached) { return NextResponse.json({ success: true, ...cached }); } + } catch { + console.warn("Failed to read leaderboard from cache"); } - } catch { - // Non-fatal: cache read failure should not block the response - console.warn("Failed to read leaderboard from cache"); } - // No cached data found — return empty result - return NextResponse.json({ - success: true, - title: country, - totalFromSource: 0, - scored: [], - errors: [], - }); + // ── 2. Cache miss — query PostgreSQL ───────────────────────────────── + try { + const db = getDatabaseStore(); + const displayLimit = getDisplayLimit(); + + // Ensure schema exists (idempotent) + await db.initializeSchema(); + + const rows = await db.getLeaderboard(country, displayLimit); + const totalCount = await db.getLeaderboardCount(country); + + if (rows.length === 0) { + // No data for this country at all — return empty + return NextResponse.json({ + success: true, + title: country, + totalFromSource: 0, + scored: [], + errors: [], + }); + } + + const scored: ScoredEntry[] = rows.map((row) => ({ + username: row.username, + name: row.name, + avatarUrl: row.avatar_url, + repoScore: row.repo_score, + prScore: row.pr_score, + contributionScore: row.contribution_score, + finalScore: row.final_score, + originalRank: 0, + originalContributions: 0, + impactRank: 0, + })); + + // Sort and assign impactRank + scored.sort((a, b) => b.finalScore - a.finalScore); + scored.forEach((u, idx) => (u.impactRank = idx + 1)); + + const result: LeaderboardResult = { + title: country, + totalFromSource: totalCount, + scored, + errors: [], + }; + + // ── 3. Warm the Redis cache ────────────────────────────────────────── + if (cacheStore.enabled) { + try { + const cacheKey = buildLeaderboardCacheKey(country, cacheConfig.namespace); + await cacheStore.set(cacheKey, result, cacheConfig.ttlSeconds); + } catch { + // Non-fatal: cache write failure should not block the response + } + } + + return NextResponse.json({ success: true, ...result }); + } catch (err) { + console.error("Leaderboard DB query failed:", err); + + // Fallback: return empty data on error + return NextResponse.json({ + success: true, + title: country, + totalFromSource: 0, + scored: [], + errors: [], + }); + } } \ No newline at end of file diff --git a/data/countries.json b/data/countries.json index f6ebd25..b8478ed 100644 --- a/data/countries.json +++ b/data/countries.json @@ -1,602 +1,752 @@ [ { "slug": "afghanistan", - "title": "Afghanistan" + "title": "Afghanistan", + "keywords": ["afghanistan", "kabul", "kandahar", "herat", "mazar-e-sharif", "jalalabad", "ghazni", "nangarhar", "khost", "zabul", "helmand", "parwan", "farah", "kunar", "wardak", "baghlan", "kunduz", "takhar", "paktia", "paktika"] }, { "slug": "albania", - "title": "Albania" + "title": "Albania", + "keywords": ["albania", "tirana", "durres", "vlore", "elbasan", "shkoder"] }, { "slug": "algeria", - "title": "Algeria" + "title": "Algeria", + "keywords": ["algeria", "algiers", "oran", "constantine", "annaba", "blida", "batna", "djelfa", "setif", "sidi bel abbes", "biskra", "tiaret", "relizane", "mostaganem", "tlemcen", "chlef", "jijel"] }, { "slug": "angola", - "title": "Angola" + "title": "Angola", + "keywords": ["angola", "luanda", "huambo", "lobito", "benguela"] }, { "slug": "argentina", - "title": "Argentina" + "title": "Argentina", + "keywords": ["argentina", "buenos aires", "cordoba", "rosario", "mendoza", "la plata", "tucuman", "mar del plata", "salta", "resistencia"] }, { "slug": "armenia", - "title": "Armenia" + "title": "Armenia", + "keywords": ["armenia", "yerevan", "gyumri", "vanadzor", "vagharshapat", "abovyan", "kapan", "hrazdan", "armavir", "artashat", "ijevan", "gavar", "goris", "dilijan", "stepanakert", "martuni", "sisian", "alaverdi", "stepanavan", "berd"] }, { "slug": "australia", - "title": "Australia" + "title": "Australia", + "keywords": ["australia", "sydney", "melbourne", "brisbane", "perth", "adelaide", "canberra", "hobart"] }, { "slug": "austria", - "title": "Austria" + "title": "Austria", + "keywords": ["austria", "österreich", "vienna", "wien", "linz", "salzburg", "graz", "innsbruck", "klagenfurt", "wels", "dornbirn"] }, { "slug": "azerbaijan", - "title": "Azerbaijan" + "title": "Azerbaijan", + "keywords": ["azerbaijan", "baku", "sumqayit", "ganja", "lankaran"] }, { "slug": "bahrain", - "title": "Bahrain" + "title": "Bahrain", + "keywords": ["bahrain", "manama", "muharraq", "riffa", "hamad town", "isa town"] }, { "slug": "bangladesh", - "title": "Bangladesh" + "title": "Bangladesh", + "keywords": ["bangladesh", "dhaka", "chittagong", "khulna", "rajshahi", "barisal", "sylhet", "rangpur", "comilla", "gazipur"] }, { "slug": "belarus", - "title": "Belarus" + "title": "Belarus", + "keywords": ["belarus", "minsk", "brest", "grodno", "gomel", "vitebsk", "mogilev", "slutsk", "borisov", "pinsk", "baranovichi", "bobruisk", "soligorsk"] }, { "slug": "belgium", - "title": "Belgium" + "title": "Belgium", + "keywords": ["belgium", "antwerp", "ghent", "charleroi", "liege", "brussels", "belgique"] }, { "slug": "benin", - "title": "Benin" + "title": "Benin", + "keywords": ["benin", "cotonou", "porto-novo", "abomey"] }, { "slug": "bolivia", - "title": "Bolivia" + "title": "Bolivia", + "keywords": ["bolivia", "santa cruz de la sierra", "el alto", "la paz", "cochabamba", "oruro", "sucre"] }, { "slug": "bosnia_and_herzegovina", - "title": "Bosnia and Herzegovina" + "title": "Bosnia and Herzegovina", + "keywords": ["sarajevo", "banja luka", "tuzla", "zenica", "bijeljina", "mostar", "prijedor", "brcko", "doboj", "cazin"] }, { "slug": "botswana", - "title": "Botswana" + "title": "Botswana", + "keywords": ["botswana", "gaborone", "francistown"] }, { "slug": "brazil", - "title": "Brazil" + "title": "Brazil", + "keywords": ["brazil", "brasil", "são paulo", "brasília", "salvador", "fortaleza", "belém", "belo horizonte", "manaus", "curitiba", "recife", "rio de janeiro", "maceió", "aracaju", "porto alegre", "florianópolis", "acre", "alagoas", "amapá", "amazonas", "bahia", "ceará", "distrito federal", "espírito santo", "goiás", "maranhão", "mato grosso", "mato grosso do sul", "minas gerais", "pará", "paraíba", "paraná", "pernambuco", "piauí", "rio grande do norte", "rio grande do sul", "rondônia", "roraima", "santa catarina", "sergipe", "tocantins"] }, { "slug": "bulgaria", - "title": "Bulgaria" + "title": "Bulgaria", + "keywords": ["bulgaria", "sofia", "plovdiv", "varna", "burgas", "ruse", "stara zagora", "pleven"] }, { "slug": "burkina_faso", - "title": "Burkina Faso" + "title": "Burkina Faso", + "keywords": ["burkina faso", "ouagadougou", "bobo-dioulasso", "koudougou", "banfora", "ouahigouya", "pouytenga", "kaya", "tenkodogo", "fada n'gourma", "houndé"] }, { "slug": "burundi", - "title": "Burundi" + "title": "Burundi", + "keywords": ["burundi", "bujumbura", "gitega"] }, { "slug": "cambodia", - "title": "Cambodia" + "title": "Cambodia", + "keywords": ["cambodia", "phnom", "battambang", "siem reap", "kampong"] }, { "slug": "cameroon", - "title": "Cameroon" + "title": "Cameroon", + "keywords": ["cameroon", "douala", "yaoundé", "bafoussam", "bamenda", "garoua", "maroua", "ngaoundéré", "kumba", "nkongsamba", "buea"] }, { "slug": "canada", - "title": "Canada" + "title": "Canada", + "keywords": ["canada", "ottawa", "edmonton", "winnipeg", "vancouver", "toronto", "quebec", "montreal", "mississauga", "calgary"] }, { "slug": "chad", - "title": "Chad" + "title": "Chad", + "keywords": ["chad", "tchad", "n'djamena", "moundou"] }, { "slug": "chile", - "title": "Chile" + "title": "Chile", + "keywords": ["chile", "santiago", "valparaíso", "concepción", "la serena", "antofagasta", "temuco", "rancagua", "talca", "arica", "chillán"] }, { "slug": "china", - "title": "China" + "title": "China", + "keywords": ["china", "中国", "guangzhou", "shanghai", "beijing", "hangzhou"] }, { "slug": "colombia", - "title": "Colombia" + "title": "Colombia", + "keywords": ["colombia", "bogota", "medellin", "cali", "barranquilla", "cartagena", "cucuta", "bucaramanga", "ibague", "soledad", "pereira", "santa marta"] }, { "slug": "costa_rica", - "title": "Costa Rica" + "title": "Costa Rica", + "keywords": ["costa rica", "san josé", "alajuela", "cartago", "heredia", "guanacaste", "puntarenas", "limón"] }, { "slug": "croatia", - "title": "Croatia" + "title": "Croatia", + "keywords": ["croatia", "hrvatska", "zagreb", "split", "rijeka", "osijek", "zadar", "pula"] }, { "slug": "cuba", - "title": "Cuba" + "title": "Cuba", + "keywords": ["cuba", "havana", "santiago de cuba", "camaguey", "holguin", "guantanamo", "bayamo"] }, { "slug": "cyprus", - "title": "Cyprus" + "title": "Cyprus", + "keywords": ["cyprus", "nicosia", "lefkosia", "limassol", "lemessos", "larnaka", "paphos"] }, { "slug": "czech_republic", - "title": "Czech Republic" + "title": "Czech Republic", + "keywords": ["czech", "czechia", "ceska", "prague", "budejovice", "plzen", "karlovy", "ostrava", "brno"] }, { "slug": "congo_kinshasa", - "title": "Democratic Republic of the Congo" + "title": "Democratic Republic of the Congo", + "keywords": ["congo kinshasa", "drc", "cod", "kinshasa", "lubumbashi", "bukavu", "kananga", "goma", "mbuji mayi", "likasi", "kolwezi", "kalemie", "uvira", "matadi", "moba", "kamina", "kabalo", "fungurume"] }, { "slug": "denmark", - "title": "Denmark" + "title": "Denmark", + "keywords": ["denmark", "danmark", "copenhagen", "aarhus", "odense", "aalborg"] }, { "slug": "dominican_republic", - "title": "Dominican Republic" + "title": "Dominican Republic", + "keywords": ["dominican republic", "republica dominicana", "santo domingo", "la vega", "macoris"] }, { "slug": "ecuador", - "title": "Ecuador" + "title": "Ecuador", + "keywords": ["ecuador", "guayaquil", "quito", "cuenca", "machala"] }, { "slug": "egypt", - "title": "Egypt" + "title": "Egypt", + "keywords": ["egypt", "cairo", "alexandria", "giza", "port said", "suez", "luxor", "el mahalla", "asyut", "asiut", "al mansurah", "mansoura", "tanta", "ismailia", "hurghada", "sharm el-sheikh", "nuweiba", "dahab", "ain shams", "ain el sokhna", "ain elsokhna", "gouna", "el gouna", "zagazig", "fayoum", "faiyum", "aswan", "minya", "sohag", "beni suef", "damietta", "kafr el-sheikh", "banha", "damanhur", "shibin el kom", "qena", "arish", "marsa matrouh", "kharga", "monufia", "sharkia", "sharqia", "dakahlia", "gharbia", "qalyubia", "beheira", "matrouh", "north sinai", "south sinai", "red sea", "new valley", "10th of ramadan", "6th of october", "obour city", "new cairo", "sadat city", "borg el arab", "om el donia", "masr", "heliopolis", "nasr city"] }, { "slug": "el_salvador", - "title": "El Salvador" + "title": "El Salvador", + "keywords": ["el salvador"] }, { "slug": "estonia", - "title": "Estonia" + "title": "Estonia", + "keywords": ["estonia", "eesti", "tallinn", "tartu", "narva", "pärnu", "rakvere", "kohtla-järve", "viljandi", "maardu", "sillamäe"] }, { "slug": "ethiopia", - "title": "Ethiopia" + "title": "Ethiopia", + "keywords": ["ethiopia", "addis ababa", "gondar", "adama", "hawassa", "bahir dar"] }, { "slug": "finland", - "title": "Finland" + "title": "Finland", + "keywords": ["finland", "suomi", "helsinki", "tampere", "oulu", "espoo", "vantaa", "turku", "rovaniemi", "jyväskylä", "lahti", "kuopio", "pori", "lappeenranta", "vaasa"] }, { "slug": "france", - "title": "France" + "title": "France", + "keywords": ["france", "paris", "marseille", "lyon", "toulouse", "nice", "nantes", "strasbourg", "montpellier", "bordeaux", "lille", "rennes", "reims", "rouen", "toulon", "le havre", "grenoble", "dijon", "le mans", "brest", "tours"] }, { "slug": "gabon", - "title": "Gabon" + "title": "Gabon", + "keywords": ["gabon", "libreville", "port-gentil", "franceville", "oyem", "moanda"] }, { "slug": "georgia", - "title": "Georgia" + "title": "Georgia", + "keywords": ["tbilisi", "batumi", "kutaisi", "rustavi", "zugdidi", "gori", "poti", "telavi", "akhaltsikhe", "mtskheta", "ozurgeti", "sukhumi", "samtredia", "marneuli"] }, { "slug": "germany", - "title": "Germany" + "title": "Germany", + "keywords": ["germany", "deutschland", "berlin", "frankfurt", "munich", "münchen", "hamburg", "cologne", "köln"] }, { "slug": "ghana", - "title": "Ghana" + "title": "Ghana", + "keywords": ["ghana", "accra", "kumasi", "sekondi", "ashaiman", "sunyani", "tamale", "tema"] }, { "slug": "greece", - "title": "Greece" + "title": "Greece", + "keywords": ["greece", "ελλάδα", "athens", "thessaloniki", "patras", "heraklion", "larissa", "volos", "rhodes", "ioannina", "chania", "crete"] }, { "slug": "guatemala", - "title": "Guatemala" + "title": "Guatemala", + "keywords": ["guatemala", "mixco", "villa nueva", "petapa", "quetzaltenango"] }, { "slug": "guinea", - "title": "Guinea" + "title": "Guinea", + "keywords": ["conakry"] }, { "slug": "haiti", - "title": "Haiti" + "title": "Haiti", + "keywords": ["haiti", "port-au-prince", "cap-haitien", "carrefour", "delmas", "petion-ville"] }, { "slug": "honduras", - "title": "Honduras" + "title": "Honduras", + "keywords": ["honduras", "tegucigalpa", "san pedro sula", "choloma", "la ceiba", "el progreso", "choluteca", "comayagua"] }, { "slug": "hong_kong", - "title": "Hong Kong" + "title": "Hong Kong", + "keywords": ["hong kong", "香港", "kowloon", "九龍"] }, { "slug": "hungary", - "title": "Hungary" + "title": "Hungary", + "keywords": ["hungary", "magyarország", "budapest", "szeged", "miskolc"] }, { "slug": "india", - "title": "India" + "title": "India", + "keywords": ["india", "mumbai", "delhi", "bangalore", "hyderabad", "ahmedabad", "chennai", "kolkata", "jaipur", "pune", "gurgaon", "noida"] }, { "slug": "indonesia", - "title": "Indonesia" + "title": "Indonesia", + "keywords": ["indonesia", "jakarta", "surabaya", "bandung", "medan", "bekasi", "semarang", "tangerang", "depok", "makassar", "palembang"] }, { "slug": "iran", - "title": "Iran" + "title": "Iran", + "keywords": ["iran", "tehran", "mashhad", "isfahan", "esfahan", "karaj", "shiraz", "tabriz", "qom", "ahvaz", "ahwaz", "kermanshah", "urmia", "rasht", "kerman"] }, { "slug": "iraq", - "title": "Iraq" + "title": "Iraq", + "keywords": ["baghdad", "mosul", "basra", "najaf", "karbala", "al-nasiriya", "al-amarah"] }, { "slug": "ireland", - "title": "Ireland" + "title": "Ireland", + "keywords": ["ireland", "dublin", "cork", "limerick", "galway", "waterford", "drogheda", "dundalk"] }, { "slug": "italy", - "title": "Italy" + "title": "Italy", + "keywords": ["italy", "italia", "rome", "roma", "milan", "naples", "napoli", "turin", "torino", "palermo", "genoa", "genova", "bologna", "florence", "firenze", "bari", "catania", "venice", "verona"] }, { "slug": "ivory_coast", - "title": "Ivory Coast" + "title": "Ivory Coast", + "keywords": ["ivory", "abidjan", "bouaké", "daloa", "yamoussoukro"] }, { "slug": "japan", - "title": "Japan" + "title": "Japan", + "keywords": ["japan", "tokyo", "yokohama", "osaka", "nagoya", "sapporo", "kobe", "kyoto", "fukuoka", "kawasaki", "saitama", "hiroshima", "sendai"] }, { "slug": "jordan", - "title": "Jordan" + "title": "Jordan", + "keywords": ["jordan", "amman", "zarqa", "irbid"] }, { "slug": "kazakhstan", - "title": "Kazakhstan" + "title": "Kazakhstan", + "keywords": ["kazakhstan", "almaty", "shymkent", "karagandy", "taraz", "nur-sultan", "pavlodar", "oskemen", "semey"] }, { "slug": "kenya", - "title": "Kenya" + "title": "Kenya", + "keywords": ["kenya", "nairobi", "mombasa", "kisumu", "nakuru", "eldoret", "kisii", "nyeri", "machakos", "embu"] }, { "slug": "kosovo", - "title": "Kosovo" + "title": "Kosovo", + "keywords": ["kosovo", "kosove", "prishtine", "prizren", "peja", "gjakova", "ferizaj", "gjilan", "mitrovica", "podujev", "vushtrri", "suhareke", "rahovec", "lipjan", "skenderaj", "kamenice", "malisheve", "decan", "istog", "kline", "fushe kosove"] }, { "slug": "kurdistan", - "title": "Kurdistan" + "title": "Kurdistan", + "keywords": ["kurdistan", "erbil", "hawler", "sulaymaniyah", "slemani", "duhok", "halabja", "kirkuk"] }, { "slug": "kyrgyzstan", - "title": "Kyrgyzstan" + "title": "Kyrgyzstan", + "keywords": ["kyrgyzstan", "bishkek", "osh", "jalal-abad", "karakol", "tokmok"] }, { "slug": "laos", - "title": "Laos" + "title": "Laos", + "keywords": ["laos", "vientiane", "pakse"] }, { "slug": "latvia", - "title": "Latvia" + "title": "Latvia", + "keywords": ["latvia", "latvija", "riga", "rīga", "kuldiga", "kuldīga", "ventspils", "liepaja", "liepāja", "daugavpils", "jelgava", "jurmala", "jūrmala"] }, { "slug": "lebanon", - "title": "Lebanon" + "title": "Lebanon", + "keywords": ["lebanon", "beirut", "sidon", "tyre", "tripoli", "byblos", "bekaa", "jounieh", "zahle", "baalbek", "nabatieh", "jbeil", "batroun", "achrafieh", "hamra"] }, { "slug": "libya", - "title": "Libya" + "title": "Libya", + "keywords": ["libya", "tripoli", "benghazi", "misrata", "zliten", "bayda"] }, { "slug": "lithuania", - "title": "Lithuania" + "title": "Lithuania", + "keywords": ["lithuania", "vilnius", "kaunas", "klaipeda", "siauliai", "panevezys", "alytus"] }, { "slug": "luxembourg", - "title": "Luxembourg" + "title": "Luxembourg", + "keywords": ["luxembourg", "esch-sur-alzette", "differdange", "dudelange", "ettelbruck", "diekirch", "wiltz", "echternach", "rumelange", "grevenmacher", "bertrange", "mamer", "capellen", "strassen"] }, { "slug": "macau", - "title": "Macau" + "title": "Macau", + "keywords": ["macau", "macao"] }, { "slug": "madagascar", - "title": "Madagascar" + "title": "Madagascar", + "keywords": ["madagascar", "antananarivo", "toamasina", "antsiranana", "mahajanga", "fianarantsoa", "toliara", "antsirabe", "ambositra", "ambatondrazaka", "manakara", "sambava", "morondava", "ambanja", "farafangana", "maintirano", "antsalova", "isoa", "mampikony", "ambatolampy", "ambatofinandrahana", "mandritsara", "marovoay", "moramanga", "vangaindrano", "soaindrana", "ikongo", "tamatave", "diego suarez", "mananjary", "vohemar", "amparafaravola"] }, { "slug": "malawi", - "title": "Malawi" + "title": "Malawi", + "keywords": ["malawi", "lilongwe", "blantyre", "mzuzu", "zomba", "karonga", "kasungu", "mangochi", "salima", "liwonde", "balaka"] }, { "slug": "malaysia", - "title": "Malaysia" + "title": "Malaysia", + "keywords": ["malaysia", "kuala lumpur", "kajang", "klang", "subang", "penang", "ipoh", "selangor", "melaka", "johor", "sabah", "johor bahru", "shah alam", "iskandar puteri"] }, { "slug": "mali", - "title": "Mali" + "title": "Mali", + "keywords": ["mali", "bamako", "sikasso", "kalabancoro", "koutiala", "ségou", "kayes", "kati", "mopti", "niono"] }, { "slug": "malta", - "title": "Malta" + "title": "Malta", + "keywords": ["malta", "birgu", "bormla", "mdina", "qormi", "senglea", "siġġiewi", "valletta", "zabbar", "zebbuġ", "zejtun"] }, { "slug": "mauritania", - "title": "Mauritania" + "title": "Mauritania", + "keywords": ["mauritania", "mauritanie", "nouakchott", "nouadhibou"] }, { "slug": "mauritius", - "title": "Mauritius" + "title": "Mauritius", + "keywords": ["mauritius", "port louis", "curepipe", "quatre bornes", "vacoas-phoenix", "vacoas", "beau-bassin-rose-hill", "beau bassin", "rose hill", "mahebourg", "goodlands", "triolet", "bel air", "flacq", "souillac", "pamplemousses", "grand baie", "ebene"] }, { "slug": "mexico", - "title": "Mexico" + "title": "Mexico", + "keywords": ["mexico", "mexico city", "guadalajara", "puebla", "tijuana", "mexicali", "monterrey", "hermosillo", "zapopan", "ciudad juarez", "chihuahua", "aguascalientes", "mx"] }, { "slug": "moldova", - "title": "Moldova" + "title": "Moldova", + "keywords": ["moldova", "chisinau", "tiraspol", "balti", "bender", "ribnita", "cahul", "ungheni", "soroca", "orhei", "dubasari"] }, { "slug": "morocco", - "title": "Morocco" + "title": "Morocco", + "keywords": ["morocco", "casablanca", "fez", "tangier", "marrakesh", "salé", "meknes", "rabat", "oujda", "kenitra", "agadir", "tetouan", "temara", "safi", "mohammedia", "khouribga", "el jadida"] }, { "slug": "mozambique", - "title": "Mozambique" + "title": "Mozambique", + "keywords": ["mozambique", "maputo", "matola", "nampula", "beira", "sofala", "chimoio", "tete", "quelimane"] }, { "slug": "myanmar", - "title": "Myanmar" + "title": "Myanmar", + "keywords": ["myanmar", "burma", "yangon", "rangoon", "mandalay", "nay pyi taw", "taunggyi", "bago", "mawlamyine"] }, { "slug": "nepal", - "title": "Nepal" + "title": "Nepal", + "keywords": ["nepal", "kathmandu", "pokhara", "lalitpur", "bharatpur", "birgunj", "biratnagar", "janakpur", "ghorahi"] }, { "slug": "netherlands", - "title": "Netherlands" + "title": "Netherlands", + "keywords": ["netherlands", "nederland", "amsterdam", "rotterdam", "hague", "utrecht", "holland", "delft"] }, { "slug": "new_zealand", - "title": "New Zealand" + "title": "New Zealand", + "keywords": ["new zealand", "auckland", "wellington", "christchurch", "hamilton", "tauranga", "napier-hastings", "dunedin", "palmerston north", "nelson", "rotorua", "whangarei", "new plymouth", "invercargill", "whanganui", "gisborne"] }, { "slug": "nicaragua", - "title": "Nicaragua" + "title": "Nicaragua", + "keywords": ["nicaragua", "managua", "matagalpa", "chinandega"] }, { "slug": "niger", - "title": "Niger" + "title": "Niger", + "keywords": ["niger", "niamey", "maradi", "zinder", "tahoua", "agadez", "arlit", "birni-n'konni", "dosso", "gaya", "tessaoua"] }, { "slug": "nigeria", - "title": "Nigeria" + "title": "Nigeria", + "keywords": ["nigeria", "lagos", "kano", "ibadan", "benin city", "port harcourt", "jos", "ilorin", "kaduna"] }, { "slug": "macedonia", - "title": "North Macedonia" + "title": "North Macedonia", + "keywords": ["macedonia", "fyrom", "north macedonia", "mk", "mkd", "ohd", "skp", "skopje", "bitola", "kumanovo", "prilep", "tetovo", "veles", "shtip", "ohrid", "gostivar", "strumica", "kavadarci", "negotino", "berovo", "kratovo", "struga", "valandovo", "demir kapija", "demir hisar", "krusheve", "gevgelija"] }, { "slug": "norway", - "title": "Norway" + "title": "Norway", + "keywords": ["norway", "norge", "oslo", "bergen", "trondheim", "stavanger", "drammen", "fredrikstad", "kristiansand", "tromsø", "sandnes", "ålesund", "bodø", "skien", "haugesund", "tønsberg", "arendal", "porsgrunn", "hamar", "larvik", "moss", "sandefjord", "halden", "harstad", "lillehammer", "molde", "gjøvik", "mo i rana", "steinkjer", "alta", "lommedalen"] }, { "slug": "oman", - "title": "Oman" + "title": "Oman", + "keywords": ["oman", "ad+dakhiliyah", "ad+dhahirah", "batinah+north", "batinah+south", "al+buraymi", "al+wusta", "ash+sharqiyah+north", "ash+sharqiyah+south", "dhofar", "muscat", "musandam"] }, { "slug": "pakistan", - "title": "Pakistan" + "title": "Pakistan", + "keywords": ["pakistan", "karachi", "lahore", "faisalabad", "rawalpindi", "peshawar", "islamabad"] }, { "slug": "palestine", - "title": "Palestine" + "title": "Palestine", + "keywords": ["palestine", "jerusalem", "gaza", "hebron", "jenin", "nablus", "ramallah", "rafah"] }, { "slug": "panama", - "title": "Panama" + "title": "Panama", + "keywords": ["panama", "panamá", "tocumen"] }, { "slug": "papua_new_guinea", - "title": "Papua New Guinea" + "title": "Papua New Guinea", + "keywords": ["papua new guinea", "port moresby", "lae"] }, { "slug": "paraguay", - "title": "Paraguay" + "title": "Paraguay", + "keywords": ["paraguay", "asunción", "asuncion", "ciudad del este", "san lorenzo", "luque", "capiata"] }, { "slug": "peru", - "title": "Peru" + "title": "Peru", + "keywords": ["peru", "lima", "cusco", "cuzco", "ica", "arequipa", "trujillo", "chiclayo", "huancayo", "piura", "chimbote", "iquitos", "juliaca", "cajamarca"] }, { "slug": "philippines", - "title": "Philippines" + "title": "Philippines", + "keywords": ["philippines", "pilipinas", "quezon", "manila", "davao", "caloocan", "cebu", "zamboanga", "bohol", "pasig", "bacolod", "makati", "baguio", "cavite"] }, { "slug": "poland", - "title": "Poland" + "title": "Poland", + "keywords": ["poland", "polska", "warsaw", "krakow", "lodz", "wroclaw", "poznan", "gdansk", "szczecin", "bydgoszcz", "lublin", "katowice", "bialystok"] }, { "slug": "portugal", - "title": "Portugal" + "title": "Portugal", + "keywords": ["portugal", "lisbon", "lisboa", "braga", "porto", "aveiro", "coimbra", "funchal", "madeira"] }, { "slug": "qatar", - "title": "Qatar" + "title": "Qatar", + "keywords": ["qatar", "doha"] }, { "slug": "south_korea", - "title": "Republic of Korea" + "title": "Republic of Korea", + "keywords": ["south korea", "rok", "korea", "seoul", "busan", "incheon", "daegu", "daejeon", "gwangju", "대한민국", "서울", "서울시"] }, { "slug": "congo_brazzaville", - "title": "Republic of the Congo" + "title": "Republic of the Congo", + "keywords": ["congo brazza", "cog", "brazzaville", "djambala", "pointe noire", "sibiti", "owando", "madingou", "loango", "kinkala", "impfondo", "dolisie"] }, { "slug": "romania", - "title": "Romania" + "title": "Romania", + "keywords": ["romania", "bucharest", "cluj", "iasi", "timisoara", "craiova", "brasov", "sibiu", "constanta", "oradea", "galati", "ploesti", "pitesti", "arad", "bacau"] }, { "slug": "russia", - "title": "Russia" + "title": "Russia", + "keywords": ["russia", "moscow", "saint petersburg", "novosibirsk", "yekaterinburg", "nizhny novgorod", "samara", "omsk", "kazan", "chelyabinsk", "rostov-on-don", "ufa", "volgograd"] }, { "slug": "rwanda", - "title": "Rwanda" + "title": "Rwanda", + "keywords": ["rwanda", "kigali", "butare", "muhanga", "ruhengeri", "gisenyi", "nyarugenge", "huye", "musanze", "rubavu", "rwamagana", "kirehe", "kibungo", "ngoma", "nyagatare", "gicumbi", "nyabihu", "kibuye", "karongi", "rusizi", "nyamasheke", "ruhango", "nyanza", "kamonyi", "kicukiro", "gasabo"] }, { "slug": "saudi_arabia", - "title": "Saudi Arabia" + "title": "Saudi Arabia", + "keywords": ["saudi", "ksa", "riyadh", "mecca", "jeddah", "dammam"] }, { "slug": "senegal", - "title": "Senegal" + "title": "Senegal", + "keywords": ["senegal", "dakar", "touba", "thies", "rufisque", "kaolack", "ziguinchor", "tambacounda", "kaffrine", "diourbel"] }, { "slug": "serbia", - "title": "Serbia" + "title": "Serbia", + "keywords": ["serbia", "belgrade", "novi sad", "nis", "kragujevac", "subotica", "zrenjanin", "pancevo", "cacak", "novi pazar", "kraljevo", "smederevo"] }, { "slug": "sierra_leone", - "title": "Sierra Leone" + "title": "Sierra Leone", + "keywords": ["sierra leone", "freetown", "makeni", "koidu"] }, { "slug": "singapore", - "title": "Singapore" + "title": "Singapore", + "keywords": ["singapore"] }, { "slug": "slovakia", - "title": "Slovakia" + "title": "Slovakia", + "keywords": ["slovakia", "bratislava", "kosice", "presov", "zilina"] }, { "slug": "slovenia", - "title": "Slovenia" + "title": "Slovenia", + "keywords": ["slovenia", "slovenija", "ljubljana", "maribor", "celje", "kranj", "koper", "velenje", "novo mesto", "nova gorica", "krsko", "krško", "murska sobota", "postojna", "slovenj gradec"] }, { "slug": "somalia", - "title": "Somalia" + "title": "Somalia", + "keywords": ["somalia", "mogadishu", "hargeisa", "bosaso", "borama", "garowe", "kismayo"] }, { "slug": "south_africa", - "title": "South Africa" + "title": "South Africa", + "keywords": ["south africa", "johannesburg", "cape town", "rsa", "durban", "port elizabeth", "pretoria", "nelspruit", "knysna"] }, { "slug": "south_sudan", - "title": "South Sudan" + "title": "South Sudan", + "keywords": ["south sudan", "juba", "yei", "wau", "aweil", "jonglei", "maridi"] }, { "slug": "spain", - "title": "Spain" + "title": "Spain", + "keywords": ["spain", "españa", "madrid", "barcelona", "valencia", "seville", "sevilla", "zaragoza", "malaga", "murcia", "palma", "bilbao", "alicante", "cordoba"] }, { "slug": "sri_lanka", - "title": "Sri Lanka" + "title": "Sri Lanka", + "keywords": ["sri lanka", "balangoda", "ratnapura", "colombo", "moratuwa", "negombo", "galle", "jaffna"] }, { "slug": "sudan", - "title": "Sudan" + "title": "Sudan", + "keywords": ["sudan", "khartoum", "omdurman"] }, { "slug": "suriname", - "title": "Suriname" + "title": "Suriname", + "keywords": ["suriname", "paramaribo"] }, { "slug": "sweden", - "title": "Sweden" + "title": "Sweden", + "keywords": ["sweden", "sverige", "stockholm", "malmö", "uppsala", "göteborg", "gothenburg"] }, { "slug": "switzerland", - "title": "Switzerland" + "title": "Switzerland", + "keywords": ["switzerland", "zurich", "zürich", "geneva", "basel", "lausanne", "bern", "winterthur", "lucerne", "gallen", "lugano", "biel", "thun"] }, { "slug": "syria", - "title": "Syria" + "title": "Syria", + "keywords": ["syria", "سوريا", "damascus", "hama", "aleppo", "homs", "rif dimashq", "tartus", "latakia", "idlib", "raqqa", "daraa", "alhasakah", "dierezzor", "quneitra", "alsuwayda"] }, { "slug": "taiwan", - "title": "Taiwan" + "title": "Taiwan", + "keywords": ["taiwan", "taichung", "kaohsiung", "taipei", "taoyuan", "tainan", "hsinchu", "keelung", "chiayi", "changhua"] }, { "slug": "tajikistan", - "title": "Tajikistan" + "title": "Tajikistan", + "keywords": ["tajikistan", "dushanbe", "khujand", "kulob", "bokhtar", "khorugh", "panjakent", "isfara", "hisor", "ghissar", "rasht"] }, { "slug": "tanzania", - "title": "Tanzania" + "title": "Tanzania", + "keywords": ["tanzania", "dar es salaam", "mwanza", "arusha", "dodoma", "mbeya", "morogoro", "tanga", "kilimanjaro"] }, { "slug": "thailand", - "title": "Thailand" + "title": "Thailand", + "keywords": ["thailand", "bangkok", "nonthaburi", "nakhon", "phuket", "pattaya", "chiang mai"] }, { "slug": "the_bahamas", - "title": "The Bahamas" + "title": "The Bahamas", + "keywords": ["bahamas"] }, { "slug": "togo", - "title": "Togo" + "title": "Togo", + "keywords": ["togo", "lome"] }, { "slug": "tunisia", - "title": "Tunisia" + "title": "Tunisia", + "keywords": ["tunisia", "tunis", "sfax", "sousse", "kairouan", "ariana", "gabes", "bizerte"] }, { "slug": "turkey", - "title": "Turkey" + "title": "Turkey", + "keywords": ["turkey", "turkiye", "istanbul", "ankara", "izmir", "bursa", "adana", "gaziantep", "konya", "antalya", "kayseri", "mersin", "eskisehir", "samsun", "denizli", "malatya"] }, { "slug": "turkmenistan", - "title": "Turkmenistan" + "title": "Turkmenistan", + "keywords": ["turkmenistan", "turkmenabat"] }, { "slug": "uganda", - "title": "Uganda" + "title": "Uganda", + "keywords": ["uganda", "kampala", "mbarara", "mukono", "jinja", "arua", "gulu", "masaka"] }, { "slug": "ukraine", - "title": "Ukraine" + "title": "Ukraine", + "keywords": ["ukraine", "kiev", "kyiv", "kharkiv", "dnipro", "odesa", "donetsk", "zaporizhia"] }, { "slug": "uae", - "title": "United Arab Emirates" + "title": "United Arab Emirates", + "keywords": ["uae", "emirates", "dubai", "abu dhabi", "sharjah", "al ain", "ajman"] }, { "slug": "uk", - "title": "United Kingdom" + "title": "United Kingdom", + "keywords": ["uk", "england", "scotland", "wales", "northern ireland", "london", "birmingham", "leeds", "glasgow", "sheffield", "bradford", "manchester", "edinburgh", "liverpool", "bristol", "cardiff", "belfast", "leicester", "wakefield", "coventry", "nottingham", "newcastle"] }, { "slug": "united_states", - "title": "United States" + "title": "United States", + "keywords": ["us", "usa", "united states", "alabama", "alaska", "ak", "arizona", "az", "arkansas", "ar", "california", "ca", "colorado", "co", "connecticut", "ct", "delaware", "de", "florida", "fl", "georgia", "ga", "hawaii", "hi", "idaho", "id", "illinois", "il", "indiana", "in", "iowa", "ia", "kansas", "ks", "kentucky", "ky", "louisiana", "la", "maine", "me", "maryland", "md", "massachusetts", "ma", "michigan", "mi", "minnesota", "mn", "mississippi", "ms", "missouri", "mo", "montana", "mt", "nebraska", "ne", "nevada", "nv", "new hampshire", "nh", "new jersey", "nj", "new mexico", "nm", "new york", "ny", "north carolina", "nc", "north dakota", "nd", "ohio", "oh", "oklahoma", "ok", "oregon", "or", "pennsylvania", "pa", "rhode island", "ri", "south carolina", "sc", "south dakota", "sd", "tennessee", "tn", "texas", "tx", "utah", "ut", "vermont", "vt", "virginia", "va", "washington", "wa", "west virginia", "wv", "wisconsin", "wi", "wyoming", "wy", "los angeles", "chicago", "houston", "phoenix", "philadelphia", "san antonio", "san diego", "dallas", "san jose", "austin", "jacksonville", "fort worth", "columbus", "charlotte", "san francisco", "indianapolis", "seattle", "denver", "boston", "el paso", "nashville", "detroit", "portland", "las vegas", "memphis", "louisville", "baltimore"] }, { "slug": "uruguay", - "title": "Uruguay" + "title": "Uruguay", + "keywords": ["uruguay", "montevideo"] }, { "slug": "uzbekistan", - "title": "Uzbekistan" + "title": "Uzbekistan", + "keywords": ["uzbekistan", "tashkent", "namangan", "samarkand", "andijan", "nukus", "bukhara", "qarshi", "fergana"] }, { "slug": "venezuela", - "title": "Venezuela" + "title": "Venezuela", + "keywords": ["venezuela", "caracas", "maracaibo", "barquisimeto", "guayana", "maturín", "zulia", "bolivar"] }, { "slug": "vietnam", - "title": "Vietnam" + "title": "Vietnam", + "keywords": ["vietnam", "viet nam", "ho chi minh", "hanoi", "ha noi", "hai phong", "da nang", "can tho", "bien hoa", "nha trang", "vinh"] }, { "slug": "worldwide", - "title": "Worldwide" + "title": "Worldwide", + "keywords": [] }, { "slug": "yemen", - "title": "Yemen" + "title": "Yemen", + "keywords": ["yemen", "sana'a", "taiz", "aden", "mukalla", "ibb"] }, { "slug": "zambia", - "title": "Zambia" + "title": "Zambia", + "keywords": ["zambia", "lusaka", "kitwe", "ndola"] }, { "slug": "zimbabwe", - "title": "Zimbabwe" + "title": "Zimbabwe", + "keywords": ["zimbabwe", "harare", "bulawayo", "mutare", "gweru", "kwekwe"] } ] diff --git a/docker-compose.yml b/docker-compose.yml index 24098f1..809ebdf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,24 @@ services: + postgres: + image: postgres:16-alpine + container_name: devimpact-postgres + restart: unless-stopped + environment: + POSTGRES_DB: devimpact + POSTGRES_USER: devimpact + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-devimpact} + # Required for local dev: trust auth allows non-SSL connections from Docker bridge + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U devimpact"] + interval: 10s + timeout: 5s + retries: 5 + redis: image: redis:7-alpine container_name: devimpact-redis @@ -23,3 +43,4 @@ services: volumes: redis-data: + pgdata: \ No newline at end of file diff --git a/lib/db-store.ts b/lib/db-store.ts new file mode 100644 index 0000000..290ef5a --- /dev/null +++ b/lib/db-store.ts @@ -0,0 +1,271 @@ +import { Pool, PoolConfig } from "pg"; + +// ─── Types ───────────────────────────────────────────────────────────── + +export type GitHubUserRow = { + username: string; + name: string | null; + avatar_url: string; + location: string | null; + country: string | null; + raw_data: unknown; + scores: unknown; + repo_score: number; + pr_score: number; + contribution_score: number; + final_score: number; + fetched_at: Date; + stale_after: Date; + created_at: Date; + updated_at: Date; +}; + +export type UpsertUserParams = { + username: string; + name: string | null; + avatarUrl: string; + location: string | null; + country: string | null; + rawData: unknown; + scores: unknown; + repoScore: number; + prScore: number; + contributionScore: number; + finalScore: number; + staleDays: number; +}; + +// ─── Connection pool (singleton) ─────────────────────────────────────── + +type DbGlobalState = typeof globalThis & { + __devimpactDbPool?: Pool; +}; + +function getPoolConfig(): PoolConfig { + const connectionString = process.env.DATABASE_URL?.trim(); + if (!connectionString) { + throw new Error( + "Missing DATABASE_URL environment variable. " + + "Set it to a PostgreSQL connection string, e.g. " + + "postgresql://user:password@localhost:5432/devimpact", + ); + } + + return { + connectionString, + max: 10, + idleTimeoutMillis: 30_000, + connectionTimeoutMillis: 5_000, + }; +} + +let pool: Pool | null = null; + +function getPool(): Pool { + if (pool) return pool; + + const globalState = globalThis as DbGlobalState; + if (!globalState.__devimpactDbPool) { + globalState.__devimpactDbPool = new Pool(getPoolConfig()); + + globalState.__devimpactDbPool.on("error", (err) => { + console.error("Unexpected database pool error:", err); + }); + } + + pool = globalState.__devimpactDbPool; + return pool; +} + +// ─── DatabaseStore class ─────────────────────────────────────────────── + +export class DatabaseStore { + // ── Schema ────────────────────────────────────────────────────────── + + async initializeSchema(): Promise { + const client = getPool(); + await client.query(` + CREATE TABLE IF NOT EXISTS github_users ( + username VARCHAR(255) PRIMARY KEY, + name TEXT, + avatar_url TEXT, + location TEXT, + country VARCHAR(100), + raw_data JSONB, + scores JSONB, + repo_score INTEGER DEFAULT 0, + pr_score INTEGER DEFAULT 0, + contribution_score INTEGER DEFAULT 0, + final_score INTEGER DEFAULT 0, + fetched_at TIMESTAMPTZ DEFAULT NOW(), + stale_after TIMESTAMPTZ DEFAULT (NOW() + INTERVAL '14 days'), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() + ); + + CREATE INDEX IF NOT EXISTS idx_github_users_country + ON github_users(country); + + CREATE INDEX IF NOT EXISTS idx_github_users_country_score + ON github_users(country, final_score DESC); + + CREATE INDEX IF NOT EXISTS idx_github_users_stale + ON github_users(stale_after) + WHERE country IS NOT NULL; + `); + } + + // ── User operations ───────────────────────────────────────────────── + + async upsertUser(params: UpsertUserParams): Promise { + const client = getPool(); + await client.query( + ` + INSERT INTO github_users ( + username, name, avatar_url, location, country, + raw_data, scores, + repo_score, pr_score, contribution_score, final_score, + fetched_at, stale_after, updated_at + ) VALUES ( + $1, $2, $3, $4, $5, + $6::jsonb, $7::jsonb, + $8, $9, $10, $11, + NOW(), NOW() + ($12 || ' days')::INTERVAL, NOW() + ) + ON CONFLICT (username) DO UPDATE SET + name = EXCLUDED.name, + avatar_url = EXCLUDED.avatar_url, + location = EXCLUDED.location, + country = EXCLUDED.country, + raw_data = EXCLUDED.raw_data, + scores = EXCLUDED.scores, + repo_score = EXCLUDED.repo_score, + pr_score = EXCLUDED.pr_score, + contribution_score = EXCLUDED.contribution_score, + final_score = EXCLUDED.final_score, + fetched_at = EXCLUDED.fetched_at, + stale_after = EXCLUDED.stale_after, + updated_at = EXCLUDED.updated_at + `, + [ + params.username, + params.name, + params.avatarUrl, + params.location, + params.country, + JSON.stringify(params.rawData), + JSON.stringify(params.scores), + params.repoScore, + params.prScore, + params.contributionScore, + params.finalScore, + params.staleDays, + ], + ); + } + + async getUser(username: string): Promise { + const client = getPool(); + const result = await client.query( + "SELECT * FROM github_users WHERE LOWER(username) = LOWER($1)", + [username], + ); + return result.rows[0] ?? null; + } + + async userExists(username: string): Promise { + const client = getPool(); + const result = await client.query( + "SELECT 1 FROM github_users WHERE LOWER(username) = LOWER($1)", + [username], + ); + return (result.rowCount ?? 0) > 0; + } + + // ── Leaderboard operations ────────────────────────────────────────── + + async getLeaderboard( + country: string, + limit: number = 500, + ): Promise { + const client = getPool(); + const result = await client.query( + `SELECT * FROM github_users + WHERE country = $1 + ORDER BY final_score DESC + LIMIT $2`, + [country, limit], + ); + return result.rows; + } + + async getLeaderboardCount(country: string): Promise { + const client = getPool(); + const result = await client.query( + "SELECT COUNT(*) FROM github_users WHERE country = $1", + [country], + ); + return Number(result.rows[0].count); + } + + /** + * Returns stale users in a country, ordered by score descending. + * These are users whose data needs to be refreshed from GitHub. + */ + async getTopStaleUsers( + country: string, + limit: number = 500, + ): Promise { + const client = getPool(); + const result = await client.query( + `SELECT * FROM github_users + WHERE country = $1 AND stale_after < NOW() + ORDER BY final_score DESC + LIMIT $2`, + [country, limit], + ); + return result.rows; + } + + /** + * Returns the top-scoring users in a country regardless of staleness. + * Used to determine which users to check for refresh. + */ + async getTopUsers( + country: string, + limit: number = 500, + ): Promise { + const client = getPool(); + const result = await client.query( + `SELECT * FROM github_users + WHERE country = $1 + ORDER BY final_score DESC + LIMIT $2`, + [country, limit], + ); + return result.rows; + } + + // ── Health check ──────────────────────────────────────────────────── + + async ping(): Promise { + try { + const client = getPool(); + await client.query("SELECT 1"); + return true; + } catch { + return false; + } + } +} + +// ─── Singleton export ────────────────────────────────────────────────── + +let defaultStore: DatabaseStore | undefined; + +export function getDatabaseStore(): DatabaseStore { + if (!defaultStore) { + defaultStore = new DatabaseStore(); + } + return defaultStore; +} \ No newline at end of file diff --git a/lib/github-graphql-client.ts b/lib/github-graphql-client.ts index 9ede06e..2ed05a1 100644 --- a/lib/github-graphql-client.ts +++ b/lib/github-graphql-client.ts @@ -290,7 +290,9 @@ export class GitHubGraphQLClient { async execute>( params: ExecuteQueryParams, ): Promise { - return this.scheduler.schedule(() => this.executeWithRetries(params)); + return this.scheduler.schedule(() => + this.executeWithRetries(params), + ); } private async executeWithRetries< @@ -365,7 +367,10 @@ export class GitHubGraphQLClient { }); throw new GitHubApiError({ message: - payload.errors?.map((error) => error.message).filter(Boolean).join(" | ") || + payload.errors + ?.map((error) => error.message) + .filter(Boolean) + .join(" | ") || `GitHub GraphQL request failed with status ${response.status}`, kind, status: response.status, @@ -398,7 +403,8 @@ export class GitHubGraphQLClient { return error; } - const message = error instanceof Error ? error.message : "Unknown network error"; + const message = + error instanceof Error ? error.message : "Unknown network error"; return new GitHubApiError({ message, kind: "NETWORK", @@ -418,7 +424,10 @@ export class GitHubGraphQLClient { typeof rateLimit.remaining === "number" && typeof rateLimit.resetAt === "number" ) { - const secondsToReset = Math.max(1, rateLimit.resetAt - Math.floor(now / 1000)); + const secondsToReset = Math.max( + 1, + rateLimit.resetAt - Math.floor(now / 1000), + ); const budgetPerSecond = rateLimit.remaining / secondsToReset; if (budgetPerSecond < 0.25) { @@ -468,21 +477,24 @@ export function toSafeApiError(error: unknown): SafeApiError { case "TIMEOUT": return { code: "GITHUB_TIMEOUT", - message: "GitHub API timed out while processing the request. Please try again shortly.", + message: + "GitHub API timed out while processing the request. Please try again shortly.", retryAfterSeconds, rateLimit, }; case "RESOURCE_LIMIT": return { code: "GITHUB_RESOURCE_LIMIT", - message: "GitHub API resource limits were reached for this query. Please retry shortly.", + message: + "GitHub API resource limits were reached for this query. Please retry shortly.", retryAfterSeconds, rateLimit, }; case "AUTH": return { code: "GITHUB_AUTH", - message: "GitHub authentication failed. Check GitHub token configuration.", + message: + "GitHub authentication failed. Check GitHub token configuration.", rateLimit, }; case "NOT_FOUND": diff --git a/lib/github.ts b/lib/github.ts index f935304..235999a 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -17,8 +17,10 @@ import type { type Logger = Pick; type GitHubRawUser = { + login: string; name: string | null; avatarUrl: string; + location: string | null; repositories: { nodes: Array }; contributionsCollection: { totalCommitContributions: number; @@ -93,7 +95,7 @@ export type GitHubFetcherDependencies = { }; const DEFAULT_GITHUB_REPO_COUNT = 50; -const DEFAULT_GITHUB_PR_COUNT = 250; +const DEFAULT_GITHUB_PR_COUNT = 300; const DEFAULT_GITHUB_ISSUE_COUNT = 100; const DEFAULT_GITHUB_DISCUSSION_COUNT = 50; @@ -120,8 +122,10 @@ export function parseCountEnv( const USER_QUERY = /* GraphQL */ ` query FetchUser($login: String!, $repoCount: Int = 100) { user(login: $login) { + login name avatarUrl(size: 80) + location contributionsCollection { totalCommitContributions totalPullRequestContributions @@ -503,8 +507,10 @@ const discussionCount = parseCountEnv( } return { + login: user.login, name: user.name, avatarUrl: user.avatarUrl, + location: user.location, repos: user.repositories.nodes.filter(isDefined), pullRequests, contributions: user.contributionsCollection, diff --git a/lib/location-detector.ts b/lib/location-detector.ts new file mode 100644 index 0000000..59691f7 --- /dev/null +++ b/lib/location-detector.ts @@ -0,0 +1,102 @@ +import countries from "@/data/countries.json"; + +// ─── Types ───────────────────────────────────────────────────────────── + +type CountryEntry = { + slug: string; + title: string; +}; + +// ─── Keyword mapping ──────────────────────────────────────────────────── +// +// GitHub's "location" field is free text. Users might write: +// - "Riyadh, Saudi Arabia" +// - "Saudi Arabia" +// - "Jeddah" +// - "KSA" +// - "🇸🇦" +// - "Earth" (unmatchable) +// - "" or null (absent) +// +// This mapping converts those variations into country slugs +// that match the slugs in data/countries.json. +// +// Keywords are pulled from committers.top presets via data/countries.json. + +type CountryEntry2 = { + slug: string; + title: string; + keywords?: string[]; +}; + +type CountryMapping = { + slug: string; + keywords: string[]; +}; + +// Build COUNTRY_MAPPINGS from countries.json with keywords +const COUNTRY_MAPPINGS: CountryMapping[] = ( + countries as CountryEntry2[] +) + .filter((country) => country.keywords && country.keywords.length > 0) + .map((country) => ({ + slug: country.slug, + keywords: country.keywords || [], + })); + +// ─── Validate all slugs exist in countries.json ─────────────────────── + +const VALID_SLUGS = new Set( + (countries as CountryEntry[]).map((c) => c.slug), +); + +// Validate on import in development +if (process.env.NODE_ENV === "development") { + for (const mapping of COUNTRY_MAPPINGS) { + if (!VALID_SLUGS.has(mapping.slug)) { + console.warn( + `[location-detector] Warning: slug "${mapping.slug}" not found in data/countries.json`, + ); + } + } +} + +// ─── Helpers ─────────────────────────────────────────────────────────── + +/** + * Checks if a keyword appears as a whole word (or phrase) within the text. + * This prevents false matches like "uk" matching inside "mukalla". + */ +function matchesKeyword(text: string, keyword: string): boolean { + // Escape regex special characters in the keyword + const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + // Match as a whole word — surrounded by word boundaries or non-alphanumeric chars + const regex = new RegExp(`(^|[^a-z])${escaped}([^a-z]|$)`, "i"); + return regex.test(text); +} + +// ─── Detection function ──────────────────────────────────────────────── + +/** + * Attempts to detect a country from a GitHub user's free-text location field. + * + * @param location - The raw `location` field from the GitHub API (can be null/empty). + * @returns A normalized country slug (e.g. "saudi-arabia") or null if unmatchable. + */ +export function detectCountry(location: string | null): string | null { + if (!location || !location.trim()) { + return null; + } + + const normalized = location.trim().toLowerCase(); + + for (const mapping of COUNTRY_MAPPINGS) { + for (const keyword of mapping.keywords) { + if (matchesKeyword(normalized, keyword)) { + return mapping.slug; + } + } + } + + return null; +} diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package.json b/package.json index fa8edcf..12f072b 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "lucide-react": "^1.7.0", "next": "^16.2.6", "next-themes": "^0.3.0", + "pg": "^8.22.0", "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", @@ -33,6 +34,7 @@ "devDependencies": { "@types/js-yaml": "^4.0.9", "@types/node": "^25.5.2", + "@types/pg": "^8.20.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "autoprefixer": "^10.4.20", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf6a652..5e3ebf9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + pg: + specifier: ^8.22.0 + version: 8.22.0 radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -60,6 +63,9 @@ importers: '@types/node': specifier: ^25.5.2 version: 25.6.0 + '@types/pg': + specifier: ^8.20.0 + version: 8.20.0 '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -279,89 +285,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -434,24 +456,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@16.2.6': resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@16.2.6': resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@16.2.6': resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@16.2.6': resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} @@ -1272,36 +1298,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': resolution: {integrity: sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': resolution: {integrity: sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': resolution: {integrity: sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': resolution: {integrity: sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': resolution: {integrity: sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': resolution: {integrity: sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==} @@ -1389,6 +1421,9 @@ packages: '@types/node@25.6.0': resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1495,41 +1530,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -2483,24 +2526,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -2702,6 +2749,40 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.15.0: + resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.22.0: + resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2786,6 +2867,22 @@ packages: resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3004,6 +3101,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -3325,6 +3426,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -4633,6 +4738,12 @@ snapshots: dependencies: undici-types: 7.19.2 + '@types/pg@8.20.0': + dependencies: + '@types/node': 25.6.0 + pg-protocol: 1.15.0 + pg-types: 2.2.0 + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -6076,6 +6187,41 @@ snapshots: pathe@2.0.3: {} + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + + pg-protocol@1.15.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.22.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.22.0) + pg-protocol: 1.15.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -6141,6 +6287,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prelude-ls@1.2.1: {} prop-types@15.8.1: @@ -6495,6 +6651,8 @@ snapshots: source-map-js@1.2.1: {} + split2@4.2.0: {} + stable-hash@0.0.5: {} stackback@0.0.2: {} @@ -6877,6 +7035,8 @@ snapshots: word-wrap@1.2.5: {} + xtend@4.0.2: {} + yallist@3.1.1: {} yocto-queue@0.1.0: {} diff --git a/scripts/init-db.ts b/scripts/init-db.ts new file mode 100644 index 0000000..214ab06 --- /dev/null +++ b/scripts/init-db.ts @@ -0,0 +1,40 @@ +/** + * One-time script to initialize the PostgreSQL schema. + * + * Usage: + * npx tsx scripts/init-db.ts + * + * Prerequisites: + * - PostgreSQL must be running (e.g. `docker compose up -d postgres`) + * - DATABASE_URL must be set in .env or environment + */ +import { getDatabaseStore } from "../lib/db-store"; + +async function main() { + console.log("Initializing database schema..."); + + const store = getDatabaseStore(); + + console.log(" • Connecting to:", process.env.DATABASE_URL ?? "(not set)"); + + // Test connection + const alive = await store.ping(); + if (!alive) { + console.error(" ✗ Could not connect to PostgreSQL. Is it running?"); + console.error(" Start it with: docker compose up -d postgres"); + process.exit(1); + } + console.log(" ✓ Connection successful"); + + // Create schema + await store.initializeSchema(); + console.log(" ✓ Schema created (github_users table + indexes)"); + + console.log("\nDone. Database is ready."); + process.exit(0); +} + +main().catch((err) => { + console.error("Schema initialization failed:", err); + process.exit(1); +}); \ No newline at end of file diff --git a/types/github.ts b/types/github.ts index 7303a5a..0547eff 100644 --- a/types/github.ts +++ b/types/github.ts @@ -65,8 +65,10 @@ export type ContributionTotals = { }; export type GitHubUserData = { + login: string; name: string | null; avatarUrl: string; + location: string | null; repos: RepoNode[]; pullRequests: PullRequestNode[]; contributions: ContributionTotals; From fd877c971d754bb6e6ccc023f61bb77442f3c83f Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Tue, 21 Jul 2026 05:28:29 +0300 Subject: [PATCH 04/10] feat: implement country leaderboard client and enhance leaderboard page with hero section --- .../[country]/country-leaderboard-client.tsx | 112 +++++++ app/leaderboard/[country]/page.tsx | 309 +++++++++--------- app/leaderboard/leaderboard-hero.tsx | 30 ++ app/leaderboard/page.tsx | 81 ++++- app/sitemap.ts | 12 + components/app-header.tsx | 26 +- components/language-switcher.tsx | 5 +- components/leaderboard-table.tsx | 140 ++------ 8 files changed, 424 insertions(+), 291 deletions(-) create mode 100644 app/leaderboard/[country]/country-leaderboard-client.tsx create mode 100644 app/leaderboard/leaderboard-hero.tsx diff --git a/app/leaderboard/[country]/country-leaderboard-client.tsx b/app/leaderboard/[country]/country-leaderboard-client.tsx new file mode 100644 index 0000000..f7d47a6 --- /dev/null +++ b/app/leaderboard/[country]/country-leaderboard-client.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { ArrowLeft, Loader2 } from "lucide-react"; +import { LeaderboardTable } from "@/components/leaderboard-table"; +import { AppHeader } from "@/components/app-header"; +import { AppFooter } from "@/components/app-footer"; +import { Button } from "@/components/ui/button"; +import { useTranslation } from "@/components/language-provider"; +import type { LeaderboardResult } from "@/lib/leaderboard"; + +type Props = { + countryTitle: string; + initialLeaderboard: LeaderboardResult; + initialError?: string | null; +}; + +export function CountryLeaderboardClient({ + countryTitle, + initialLeaderboard, + initialError = null, +}: Props) { + const { t } = useTranslation(); + const [title] = useState(initialLeaderboard.title || countryTitle); + const [totalFromSource] = useState(initialLeaderboard.totalFromSource); + const [scored] = useState(initialLeaderboard.scored); + const [errors] = useState(initialLeaderboard.errors); + const [failed] = useState(initialError); + const [loading] = useState(false); + + if (failed) { + return ( +
+ +
+
+

+ {t("leaderboard.error.title")} +

+

+ {failed} +

+ + + +
+
+ +
+ ); + } + + return ( +
+ +
+
+ + + +
+ +
+
+

+ {t("leaderboard.header.eyebrow")} +

+

+ {t("leaderboard.country.title", { title })} +

+

+ {t("leaderboard.country.description", { title })} +

+
+
+ + {scored.length > 0 ? ( +
+ +
+ ) : loading ? ( +
+ + + {t("leaderboard.loading")} + +
+ ) : ( +
+

+ {t("leaderboard.noDevelopersFor", { title })} +

+ + + +
+ )} +
+ +
+ ); +} diff --git a/app/leaderboard/[country]/page.tsx b/app/leaderboard/[country]/page.tsx index b469db4..521f248 100644 --- a/app/leaderboard/[country]/page.tsx +++ b/app/leaderboard/[country]/page.tsx @@ -1,166 +1,177 @@ -"use client"; - -import { useEffect, useState, use } from "react"; -import Link from "next/link"; -import { ArrowLeft, Loader2 } from "lucide-react"; -import { LeaderboardTable } from "@/components/leaderboard-table"; -import { AppHeader } from "@/components/app-header"; -import { AppFooter } from "@/components/app-footer"; -import { Button } from "@/components/ui/button"; -import { useTranslation } from "@/components/language-provider"; - -// ─── Types ───────────────────────────────────────────────────────────── - -type ScoredEntry = { - username: string; - name: string | null; - avatarUrl: string; - repoScore: number; - prScore: number; - contributionScore: number; - finalScore: number; - originalRank: number; - originalContributions: number; - impactRank: number; -}; - -type LeaderboardApiResponse = { - success: boolean; +import type { Metadata } from "next"; +import countriesData from "@/data/countries.json"; +import { JsonLd } from "@/components/seo/json-ld"; +import { getLeaderboardResult } from "@/lib/leaderboard"; +import { toAbsoluteUrl } from "@/lib/seo"; +import { CountryLeaderboardClient } from "./country-leaderboard-client"; + +type CountryInfo = { + slug: string; title: string; - totalFromSource: number; - scored: ScoredEntry[]; - errors: string[]; + keywords?: string[]; }; type Props = { params: Promise<{ country: string }>; }; -// ─── Fetch helpers ───────────────────────────────────────────────────── +const countries = countriesData as CountryInfo[]; -async function fetchLeaderboard(country: string): Promise { - const res = await fetch(`/api/leaderboard?country=${encodeURIComponent(country)}`); - if (!res.ok) throw new Error(`Failed to fetch leaderboard for ${country}`); - const json = await res.json(); - if (!json.success) throw new Error(json.error || "Failed to load leaderboard"); - return json; +function formatCountryTitle(country: string): string { + return country + .split("_") + .filter(Boolean) + .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) + .join(" "); } -// ─── Component ───────────────────────────────────────────────────────── - -export default function CountryLeaderboardPage({ params }: Props) { - const { country } = use(params); - const { t } = useTranslation(); - - const [title, setTitle] = useState(""); - const [totalFromSource, setTotalFromSource] = useState(0); - const [scored, setScored] = useState([]); - const [errors, setErrors] = useState([]); - const [loading, setLoading] = useState(true); - const [failed, setFailed] = useState(null); +function getCountryInfo(country: string): CountryInfo { + return ( + countries.find((entry) => entry.slug === country) ?? { + slug: country, + title: formatCountryTitle(country), + keywords: [], + } + ); +} - useEffect(() => { - let cancelled = false; +export async function generateMetadata({ params }: Props): Promise { + const { country } = await params; + const countryInfo = getCountryInfo(country); + const pageTitle = `${countryInfo.title} Developer Leaderboard`; + const description = `Explore the ${countryInfo.title} GitHub developer leaderboard on DevImpact. Compare repository impact, merged pull request strength, and community contribution signals in one country ranking.`; + const keywords = [ + `${countryInfo.title} developer leaderboard`, + `${countryInfo.title} GitHub developers`, + `${countryInfo.title} open source ranking`, + `${countryInfo.title} developer ranking`, + "GitHub leaderboard", + "developer impact score", + ...(countryInfo.keywords ?? []).slice(0, 6), + ]; + + return { + title: pageTitle, + description, + keywords, + alternates: { + canonical: `/leaderboard/${country}`, + }, + openGraph: { + type: "website", + title: `${pageTitle} | DevImpact`, + description, + url: `/leaderboard/${country}`, + images: [ + { + url: toAbsoluteUrl("/og-image.svg"), + width: 1200, + height: 630, + alt: `${countryInfo.title} developer leaderboard preview`, + }, + ], + }, + twitter: { + card: "summary_large_image", + title: `${pageTitle} | DevImpact`, + description, + images: [toAbsoluteUrl("/og-image.svg")], + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + "max-image-preview": "large", + "max-snippet": -1, + "max-video-preview": -1, + }, + }, + }; +} - (async () => { - try { - const data = await fetchLeaderboard(country); - if (cancelled) return; +export async function generateStaticParams() { + return countries.map((country) => ({ country: country.slug })); +} - setTitle(data.title); - setTotalFromSource(data.totalFromSource); - setScored(data.scored); - setErrors(data.errors); - setLoading(false); - } catch (err) { - if (!cancelled) - setFailed( - err instanceof Error ? err.message : "Failed to load leaderboard", - ); +export default async function CountryLeaderboardPage({ params }: Props) { + const { country } = await params; + const countryInfo = getCountryInfo(country); + const countryUrl = toAbsoluteUrl(`/leaderboard/${country}`); + const leaderboard = await getLeaderboardResult(country); + + const webPageSchema = { + "@context": "https://schema.org", + "@type": "CollectionPage", + name: `${countryInfo.title} Developer Leaderboard`, + description: `Country ranking for ${countryInfo.title} developers based on repository impact, merged pull requests, and community contribution signals.`, + url: countryUrl, + isPartOf: { + "@type": "WebSite", + name: "DevImpact", + url: toAbsoluteUrl("/"), + }, + about: { + "@type": "Thing", + name: `${countryInfo.title} open-source developers`, + }, + }; + + const breadcrumbSchema = { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + itemListElement: [ + { + "@type": "ListItem", + position: 1, + name: "Home", + item: toAbsoluteUrl("/"), + }, + { + "@type": "ListItem", + position: 2, + name: "Leaderboards", + item: toAbsoluteUrl("/leaderboard"), + }, + { + "@type": "ListItem", + position: 3, + name: countryInfo.title, + item: countryUrl, + }, + ], + }; + + const topUsersSchema = leaderboard.scored.length + ? { + "@context": "https://schema.org", + "@type": "ItemList", + name: `${countryInfo.title} top GitHub developers`, + itemListOrder: "https://schema.org/ItemListOrderDescending", + numberOfItems: leaderboard.scored.length, + itemListElement: leaderboard.scored.map((user) => ({ + "@type": "ListItem", + position: user.impactRank, + name: user.name || user.username, + url: `https://github.com/${user.username}`, + })), } - })(); - - return () => { - cancelled = true; - }; - }, [country]); - - if (failed) { - return ( -
- -
-
-

- {t("leaderboard.error.title")} -

-

- {failed} -

- - - -
-
- -
- ); - } + : null; return ( -
- -
- {/* Back navigation */} -
- - - -
- - {/* Leaderboard table */} - {scored.length > 0 ? ( -
- -
- ) : loading ? ( -
- - - {t("leaderboard.loading")} - -
- ) : ( -
-

- {t("leaderboard.noDevelopersFor", { title })} -

- - - -
- )} - - {/* Scoring progress indicator */} - {loading && scored.length > 0 && ( -
- - - {t("leaderboard.loading")} - -
- )} -
- -
+ <> + + + ); -} \ No newline at end of file +} diff --git a/app/leaderboard/leaderboard-hero.tsx b/app/leaderboard/leaderboard-hero.tsx new file mode 100644 index 0000000..2fd829d --- /dev/null +++ b/app/leaderboard/leaderboard-hero.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { useTranslation } from "@/components/language-provider"; + +type Props = { + countryCount: number; +}; + +export function LeaderboardHero({ countryCount }: Props) { + const { t } = useTranslation(); + + return ( +
+
+

+ {t("leaderboard.page.eyebrow")} +

+

+ {t("leaderboard.page.title")} +

+

+ {t("leaderboard.page.description")} +

+

+ {t("leaderboard.page.count", { count: countryCount })} +

+
+
+ ); +} diff --git a/app/leaderboard/page.tsx b/app/leaderboard/page.tsx index abd6e23..a048966 100644 --- a/app/leaderboard/page.tsx +++ b/app/leaderboard/page.tsx @@ -5,28 +5,36 @@ import { JsonLd } from "@/components/seo/json-ld"; import { toAbsoluteUrl } from "@/lib/seo"; import countriesData from "@/data/countries.json"; import { CountryGridClient } from "./country-grid-client"; +import { LeaderboardHero } from "./leaderboard-hero"; import type { CountryInfo } from "@/types/leaderboard"; -// ─── Metadata ────────────────────────────────────────────────────────── - export const metadata: Metadata = { - title: "Leaderboard — Country Impact Rankings", + title: "Leaderboard - Country Impact Rankings", description: - "Browse committers.top countries and view developers ranked by DevImpact's transparent repository, PR, and community contribution scoring.", + "Browse country leaderboards and view developers ranked by DevImpact's transparent repository, PR, and community contribution scoring.", + keywords: [ + "github leaderboard", + "country developer leaderboard", + "open source developer rankings", + "github developer rankings by country", + "developer impact leaderboard", + "devimpact leaderboard", + ], alternates: { canonical: "/leaderboard", }, openGraph: { + type: "website", title: "Country Leaderboards by Impact | DevImpact", description: - "Browse committers.top country leaderboards reordered by real open-source impact: repository quality, merged PRs, and community contributions.", + "Browse country leaderboards ranked by real open-source impact: repository quality, merged PRs, and community contributions.", url: "/leaderboard", images: [ { url: toAbsoluteUrl("/og-image.svg"), width: 1200, height: 630, - alt: "DevImpact Country Leaderboard", + alt: "DevImpact country leaderboard", }, ], }, @@ -34,32 +42,75 @@ export const metadata: Metadata = { card: "summary_large_image", title: "Country Leaderboards by Impact | DevImpact", description: - "Browse committers.top country leaderboards reordered by real open-source impact.", + "Browse country leaderboards ranked by real open-source impact.", images: [toAbsoluteUrl("/og-image.svg")], }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + "max-image-preview": "large", + "max-snippet": -1, + "max-video-preview": -1, + }, + }, }; -const webPageSchema = { +const countries: CountryInfo[] = countriesData as CountryInfo[]; + +const collectionPageSchema = { "@context": "https://schema.org", - "@type": "WebPage", + "@type": "CollectionPage", name: "Country Leaderboards by Impact", description: - "Browse committers.top country leaderboards reordered by DevImpact impact scoring.", + "Browse country leaderboards ranked by DevImpact impact scoring.", url: toAbsoluteUrl("/leaderboard"), }; -// ─── Static country list (sourced from committers.top YAML, converted to JSON) ─ -const countries: CountryInfo[] = countriesData as CountryInfo[]; +const breadcrumbSchema = { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + itemListElement: [ + { + "@type": "ListItem", + position: 1, + name: "Home", + item: toAbsoluteUrl("/"), + }, + { + "@type": "ListItem", + position: 2, + name: "Leaderboards", + item: toAbsoluteUrl("/leaderboard"), + }, + ], +}; -// ─── Page ────────────────────────────────────────────────────────────── +const itemListSchema = { + "@context": "https://schema.org", + "@type": "ItemList", + name: "Developer leaderboards by country", + itemListOrder: "https://schema.org/ItemListUnordered", + numberOfItems: countries.length, + itemListElement: countries.slice(0, 24).map((country, index) => ({ + "@type": "ListItem", + position: index + 1, + name: `${country.title} Developer Leaderboard`, + url: toAbsoluteUrl(`/leaderboard/${country.slug}`), + })), +}; export default function LeaderboardPage() { return ( <> - +
-
+
+ +
diff --git a/app/sitemap.ts b/app/sitemap.ts index 8fad05f..bd6f258 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,9 +1,15 @@ import type { MetadataRoute } from "next"; import { getSiteUrl } from "@/lib/seo"; +import countriesData from "@/data/countries.json"; + +type CountryInfo = { + slug: string; +}; export default async function sitemap(): Promise { const baseUrl = getSiteUrl(); const now = new Date(); + const countries = countriesData as CountryInfo[]; const entries: MetadataRoute.Sitemap = [ { @@ -12,6 +18,12 @@ export default async function sitemap(): Promise { changeFrequency: "weekly", priority: 1, }, + ...countries.map((country) => ({ + url: `${baseUrl}/leaderboard/${country.slug}`, + lastModified: now, + changeFrequency: "weekly" as const, + priority: 0.9, + })), { url: `${baseUrl}/leaderboard`, lastModified: now, diff --git a/components/app-header.tsx b/components/app-header.tsx index 488b01e..17d8c30 100644 --- a/components/app-header.tsx +++ b/components/app-header.tsx @@ -1,24 +1,42 @@ +"use client"; + import Link from "next/link"; +import { Trophy } from "lucide-react"; import { BrandLogo } from "@/components/brand-logo"; import { LanguageSwitcher } from "@/components/language-switcher"; import { ThemeToggle } from "@/components/theme-toggle"; import { GithubLink } from "@/components/github-link"; +import { useTranslation } from "@/components/language-provider"; +import { cn } from "@/lib/utils"; export function AppHeader() { + const { t } = useTranslation(); + return (
-
+
-
- {filtered.length > PAGE_SIZE && ( + {users.length > 0 && (
{ - setSearch(e.target.value); - setPage(0); - }} + onChange={(e) => setSearch(e.target.value)} />
)} @@ -159,34 +107,28 @@ export function LeaderboardTable({ - - - - - - - - - {paged.map((user) => ( + {filtered.map((user) => ( - - - - - - ))}
+ {t("leaderboard.impactRank")} + {t("leaderboard.developer")} + {t("comparsion.final.score")} + {t("comparsion.repo.score")} + {t("comparsion.pr.score")} + {t("comparsion.contribution.score")} - {t("leaderboard.committersRank")} - - {t("leaderboard.change")} -
+ {user.finalScore} + {user.repoScore} + {user.prScore} + {user.contributionScore} - #{user.originalRank} - - -
- - {totalPages > 1 && ( -
-

- {t("leaderboard.pagination", { - from: page * PAGE_SIZE + 1, - to: Math.min((page + 1) * PAGE_SIZE, filtered.length), - total: filtered.length, - })} -

-
- - - {page + 1} / {totalPages} - - -
-
- )}
From be540c2721d3628da369e23d224753fb18ded427 Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Tue, 21 Jul 2026 05:30:08 +0300 Subject: [PATCH 05/10] refactor: country flag mapping and leaderboard calculation - Updated country flag mapping to dynamically build from countries.json. - Introduced leaderboard_calculation table to track country calculations. - Added methods for seeding, starting, and finishing leaderboard calculations in DatabaseStore. - Enhanced location detection to utilize updated country data structure. - Improved localization for leaderboard features in Arabic and English. --- data/countries.json | 902 +++++++-------------------------------- lib/country-flags.ts | 172 +------- lib/db-store.ts | 61 +++ lib/location-detector.ts | 56 +-- locales/ar.json | 17 +- locales/en.json | 17 +- 6 files changed, 261 insertions(+), 964 deletions(-) diff --git a/data/countries.json b/data/countries.json index b8478ed..25f8139 100644 --- a/data/countries.json +++ b/data/countries.json @@ -1,752 +1,152 @@ [ - { - "slug": "afghanistan", - "title": "Afghanistan", - "keywords": ["afghanistan", "kabul", "kandahar", "herat", "mazar-e-sharif", "jalalabad", "ghazni", "nangarhar", "khost", "zabul", "helmand", "parwan", "farah", "kunar", "wardak", "baghlan", "kunduz", "takhar", "paktia", "paktika"] - }, - { - "slug": "albania", - "title": "Albania", - "keywords": ["albania", "tirana", "durres", "vlore", "elbasan", "shkoder"] - }, - { - "slug": "algeria", - "title": "Algeria", - "keywords": ["algeria", "algiers", "oran", "constantine", "annaba", "blida", "batna", "djelfa", "setif", "sidi bel abbes", "biskra", "tiaret", "relizane", "mostaganem", "tlemcen", "chlef", "jijel"] - }, - { - "slug": "angola", - "title": "Angola", - "keywords": ["angola", "luanda", "huambo", "lobito", "benguela"] - }, - { - "slug": "argentina", - "title": "Argentina", - "keywords": ["argentina", "buenos aires", "cordoba", "rosario", "mendoza", "la plata", "tucuman", "mar del plata", "salta", "resistencia"] - }, - { - "slug": "armenia", - "title": "Armenia", - "keywords": ["armenia", "yerevan", "gyumri", "vanadzor", "vagharshapat", "abovyan", "kapan", "hrazdan", "armavir", "artashat", "ijevan", "gavar", "goris", "dilijan", "stepanakert", "martuni", "sisian", "alaverdi", "stepanavan", "berd"] - }, - { - "slug": "australia", - "title": "Australia", - "keywords": ["australia", "sydney", "melbourne", "brisbane", "perth", "adelaide", "canberra", "hobart"] - }, - { - "slug": "austria", - "title": "Austria", - "keywords": ["austria", "österreich", "vienna", "wien", "linz", "salzburg", "graz", "innsbruck", "klagenfurt", "wels", "dornbirn"] - }, - { - "slug": "azerbaijan", - "title": "Azerbaijan", - "keywords": ["azerbaijan", "baku", "sumqayit", "ganja", "lankaran"] - }, - { - "slug": "bahrain", - "title": "Bahrain", - "keywords": ["bahrain", "manama", "muharraq", "riffa", "hamad town", "isa town"] - }, - { - "slug": "bangladesh", - "title": "Bangladesh", - "keywords": ["bangladesh", "dhaka", "chittagong", "khulna", "rajshahi", "barisal", "sylhet", "rangpur", "comilla", "gazipur"] - }, - { - "slug": "belarus", - "title": "Belarus", - "keywords": ["belarus", "minsk", "brest", "grodno", "gomel", "vitebsk", "mogilev", "slutsk", "borisov", "pinsk", "baranovichi", "bobruisk", "soligorsk"] - }, - { - "slug": "belgium", - "title": "Belgium", - "keywords": ["belgium", "antwerp", "ghent", "charleroi", "liege", "brussels", "belgique"] - }, - { - "slug": "benin", - "title": "Benin", - "keywords": ["benin", "cotonou", "porto-novo", "abomey"] - }, - { - "slug": "bolivia", - "title": "Bolivia", - "keywords": ["bolivia", "santa cruz de la sierra", "el alto", "la paz", "cochabamba", "oruro", "sucre"] - }, - { - "slug": "bosnia_and_herzegovina", - "title": "Bosnia and Herzegovina", - "keywords": ["sarajevo", "banja luka", "tuzla", "zenica", "bijeljina", "mostar", "prijedor", "brcko", "doboj", "cazin"] - }, - { - "slug": "botswana", - "title": "Botswana", - "keywords": ["botswana", "gaborone", "francistown"] - }, - { - "slug": "brazil", - "title": "Brazil", - "keywords": ["brazil", "brasil", "são paulo", "brasília", "salvador", "fortaleza", "belém", "belo horizonte", "manaus", "curitiba", "recife", "rio de janeiro", "maceió", "aracaju", "porto alegre", "florianópolis", "acre", "alagoas", "amapá", "amazonas", "bahia", "ceará", "distrito federal", "espírito santo", "goiás", "maranhão", "mato grosso", "mato grosso do sul", "minas gerais", "pará", "paraíba", "paraná", "pernambuco", "piauí", "rio grande do norte", "rio grande do sul", "rondônia", "roraima", "santa catarina", "sergipe", "tocantins"] - }, - { - "slug": "bulgaria", - "title": "Bulgaria", - "keywords": ["bulgaria", "sofia", "plovdiv", "varna", "burgas", "ruse", "stara zagora", "pleven"] - }, - { - "slug": "burkina_faso", - "title": "Burkina Faso", - "keywords": ["burkina faso", "ouagadougou", "bobo-dioulasso", "koudougou", "banfora", "ouahigouya", "pouytenga", "kaya", "tenkodogo", "fada n'gourma", "houndé"] - }, - { - "slug": "burundi", - "title": "Burundi", - "keywords": ["burundi", "bujumbura", "gitega"] - }, - { - "slug": "cambodia", - "title": "Cambodia", - "keywords": ["cambodia", "phnom", "battambang", "siem reap", "kampong"] - }, - { - "slug": "cameroon", - "title": "Cameroon", - "keywords": ["cameroon", "douala", "yaoundé", "bafoussam", "bamenda", "garoua", "maroua", "ngaoundéré", "kumba", "nkongsamba", "buea"] - }, - { - "slug": "canada", - "title": "Canada", - "keywords": ["canada", "ottawa", "edmonton", "winnipeg", "vancouver", "toronto", "quebec", "montreal", "mississauga", "calgary"] - }, - { - "slug": "chad", - "title": "Chad", - "keywords": ["chad", "tchad", "n'djamena", "moundou"] - }, - { - "slug": "chile", - "title": "Chile", - "keywords": ["chile", "santiago", "valparaíso", "concepción", "la serena", "antofagasta", "temuco", "rancagua", "talca", "arica", "chillán"] - }, - { - "slug": "china", - "title": "China", - "keywords": ["china", "中国", "guangzhou", "shanghai", "beijing", "hangzhou"] - }, - { - "slug": "colombia", - "title": "Colombia", - "keywords": ["colombia", "bogota", "medellin", "cali", "barranquilla", "cartagena", "cucuta", "bucaramanga", "ibague", "soledad", "pereira", "santa marta"] - }, - { - "slug": "costa_rica", - "title": "Costa Rica", - "keywords": ["costa rica", "san josé", "alajuela", "cartago", "heredia", "guanacaste", "puntarenas", "limón"] - }, - { - "slug": "croatia", - "title": "Croatia", - "keywords": ["croatia", "hrvatska", "zagreb", "split", "rijeka", "osijek", "zadar", "pula"] - }, - { - "slug": "cuba", - "title": "Cuba", - "keywords": ["cuba", "havana", "santiago de cuba", "camaguey", "holguin", "guantanamo", "bayamo"] - }, - { - "slug": "cyprus", - "title": "Cyprus", - "keywords": ["cyprus", "nicosia", "lefkosia", "limassol", "lemessos", "larnaka", "paphos"] - }, - { - "slug": "czech_republic", - "title": "Czech Republic", - "keywords": ["czech", "czechia", "ceska", "prague", "budejovice", "plzen", "karlovy", "ostrava", "brno"] - }, - { - "slug": "congo_kinshasa", - "title": "Democratic Republic of the Congo", - "keywords": ["congo kinshasa", "drc", "cod", "kinshasa", "lubumbashi", "bukavu", "kananga", "goma", "mbuji mayi", "likasi", "kolwezi", "kalemie", "uvira", "matadi", "moba", "kamina", "kabalo", "fungurume"] - }, - { - "slug": "denmark", - "title": "Denmark", - "keywords": ["denmark", "danmark", "copenhagen", "aarhus", "odense", "aalborg"] - }, - { - "slug": "dominican_republic", - "title": "Dominican Republic", - "keywords": ["dominican republic", "republica dominicana", "santo domingo", "la vega", "macoris"] - }, - { - "slug": "ecuador", - "title": "Ecuador", - "keywords": ["ecuador", "guayaquil", "quito", "cuenca", "machala"] - }, - { - "slug": "egypt", - "title": "Egypt", - "keywords": ["egypt", "cairo", "alexandria", "giza", "port said", "suez", "luxor", "el mahalla", "asyut", "asiut", "al mansurah", "mansoura", "tanta", "ismailia", "hurghada", "sharm el-sheikh", "nuweiba", "dahab", "ain shams", "ain el sokhna", "ain elsokhna", "gouna", "el gouna", "zagazig", "fayoum", "faiyum", "aswan", "minya", "sohag", "beni suef", "damietta", "kafr el-sheikh", "banha", "damanhur", "shibin el kom", "qena", "arish", "marsa matrouh", "kharga", "monufia", "sharkia", "sharqia", "dakahlia", "gharbia", "qalyubia", "beheira", "matrouh", "north sinai", "south sinai", "red sea", "new valley", "10th of ramadan", "6th of october", "obour city", "new cairo", "sadat city", "borg el arab", "om el donia", "masr", "heliopolis", "nasr city"] - }, - { - "slug": "el_salvador", - "title": "El Salvador", - "keywords": ["el salvador"] - }, - { - "slug": "estonia", - "title": "Estonia", - "keywords": ["estonia", "eesti", "tallinn", "tartu", "narva", "pärnu", "rakvere", "kohtla-järve", "viljandi", "maardu", "sillamäe"] - }, - { - "slug": "ethiopia", - "title": "Ethiopia", - "keywords": ["ethiopia", "addis ababa", "gondar", "adama", "hawassa", "bahir dar"] - }, - { - "slug": "finland", - "title": "Finland", - "keywords": ["finland", "suomi", "helsinki", "tampere", "oulu", "espoo", "vantaa", "turku", "rovaniemi", "jyväskylä", "lahti", "kuopio", "pori", "lappeenranta", "vaasa"] - }, - { - "slug": "france", - "title": "France", - "keywords": ["france", "paris", "marseille", "lyon", "toulouse", "nice", "nantes", "strasbourg", "montpellier", "bordeaux", "lille", "rennes", "reims", "rouen", "toulon", "le havre", "grenoble", "dijon", "le mans", "brest", "tours"] - }, - { - "slug": "gabon", - "title": "Gabon", - "keywords": ["gabon", "libreville", "port-gentil", "franceville", "oyem", "moanda"] - }, - { - "slug": "georgia", - "title": "Georgia", - "keywords": ["tbilisi", "batumi", "kutaisi", "rustavi", "zugdidi", "gori", "poti", "telavi", "akhaltsikhe", "mtskheta", "ozurgeti", "sukhumi", "samtredia", "marneuli"] - }, - { - "slug": "germany", - "title": "Germany", - "keywords": ["germany", "deutschland", "berlin", "frankfurt", "munich", "münchen", "hamburg", "cologne", "köln"] - }, - { - "slug": "ghana", - "title": "Ghana", - "keywords": ["ghana", "accra", "kumasi", "sekondi", "ashaiman", "sunyani", "tamale", "tema"] - }, - { - "slug": "greece", - "title": "Greece", - "keywords": ["greece", "ελλάδα", "athens", "thessaloniki", "patras", "heraklion", "larissa", "volos", "rhodes", "ioannina", "chania", "crete"] - }, - { - "slug": "guatemala", - "title": "Guatemala", - "keywords": ["guatemala", "mixco", "villa nueva", "petapa", "quetzaltenango"] - }, - { - "slug": "guinea", - "title": "Guinea", - "keywords": ["conakry"] - }, - { - "slug": "haiti", - "title": "Haiti", - "keywords": ["haiti", "port-au-prince", "cap-haitien", "carrefour", "delmas", "petion-ville"] - }, - { - "slug": "honduras", - "title": "Honduras", - "keywords": ["honduras", "tegucigalpa", "san pedro sula", "choloma", "la ceiba", "el progreso", "choluteca", "comayagua"] - }, - { - "slug": "hong_kong", - "title": "Hong Kong", - "keywords": ["hong kong", "香港", "kowloon", "九龍"] - }, - { - "slug": "hungary", - "title": "Hungary", - "keywords": ["hungary", "magyarország", "budapest", "szeged", "miskolc"] - }, - { - "slug": "india", - "title": "India", - "keywords": ["india", "mumbai", "delhi", "bangalore", "hyderabad", "ahmedabad", "chennai", "kolkata", "jaipur", "pune", "gurgaon", "noida"] - }, - { - "slug": "indonesia", - "title": "Indonesia", - "keywords": ["indonesia", "jakarta", "surabaya", "bandung", "medan", "bekasi", "semarang", "tangerang", "depok", "makassar", "palembang"] - }, - { - "slug": "iran", - "title": "Iran", - "keywords": ["iran", "tehran", "mashhad", "isfahan", "esfahan", "karaj", "shiraz", "tabriz", "qom", "ahvaz", "ahwaz", "kermanshah", "urmia", "rasht", "kerman"] - }, - { - "slug": "iraq", - "title": "Iraq", - "keywords": ["baghdad", "mosul", "basra", "najaf", "karbala", "al-nasiriya", "al-amarah"] - }, - { - "slug": "ireland", - "title": "Ireland", - "keywords": ["ireland", "dublin", "cork", "limerick", "galway", "waterford", "drogheda", "dundalk"] - }, - { - "slug": "italy", - "title": "Italy", - "keywords": ["italy", "italia", "rome", "roma", "milan", "naples", "napoli", "turin", "torino", "palermo", "genoa", "genova", "bologna", "florence", "firenze", "bari", "catania", "venice", "verona"] - }, - { - "slug": "ivory_coast", - "title": "Ivory Coast", - "keywords": ["ivory", "abidjan", "bouaké", "daloa", "yamoussoukro"] - }, - { - "slug": "japan", - "title": "Japan", - "keywords": ["japan", "tokyo", "yokohama", "osaka", "nagoya", "sapporo", "kobe", "kyoto", "fukuoka", "kawasaki", "saitama", "hiroshima", "sendai"] - }, - { - "slug": "jordan", - "title": "Jordan", - "keywords": ["jordan", "amman", "zarqa", "irbid"] - }, - { - "slug": "kazakhstan", - "title": "Kazakhstan", - "keywords": ["kazakhstan", "almaty", "shymkent", "karagandy", "taraz", "nur-sultan", "pavlodar", "oskemen", "semey"] - }, - { - "slug": "kenya", - "title": "Kenya", - "keywords": ["kenya", "nairobi", "mombasa", "kisumu", "nakuru", "eldoret", "kisii", "nyeri", "machakos", "embu"] - }, - { - "slug": "kosovo", - "title": "Kosovo", - "keywords": ["kosovo", "kosove", "prishtine", "prizren", "peja", "gjakova", "ferizaj", "gjilan", "mitrovica", "podujev", "vushtrri", "suhareke", "rahovec", "lipjan", "skenderaj", "kamenice", "malisheve", "decan", "istog", "kline", "fushe kosove"] - }, - { - "slug": "kurdistan", - "title": "Kurdistan", - "keywords": ["kurdistan", "erbil", "hawler", "sulaymaniyah", "slemani", "duhok", "halabja", "kirkuk"] - }, - { - "slug": "kyrgyzstan", - "title": "Kyrgyzstan", - "keywords": ["kyrgyzstan", "bishkek", "osh", "jalal-abad", "karakol", "tokmok"] - }, - { - "slug": "laos", - "title": "Laos", - "keywords": ["laos", "vientiane", "pakse"] - }, - { - "slug": "latvia", - "title": "Latvia", - "keywords": ["latvia", "latvija", "riga", "rīga", "kuldiga", "kuldīga", "ventspils", "liepaja", "liepāja", "daugavpils", "jelgava", "jurmala", "jūrmala"] - }, - { - "slug": "lebanon", - "title": "Lebanon", - "keywords": ["lebanon", "beirut", "sidon", "tyre", "tripoli", "byblos", "bekaa", "jounieh", "zahle", "baalbek", "nabatieh", "jbeil", "batroun", "achrafieh", "hamra"] - }, - { - "slug": "libya", - "title": "Libya", - "keywords": ["libya", "tripoli", "benghazi", "misrata", "zliten", "bayda"] - }, - { - "slug": "lithuania", - "title": "Lithuania", - "keywords": ["lithuania", "vilnius", "kaunas", "klaipeda", "siauliai", "panevezys", "alytus"] - }, - { - "slug": "luxembourg", - "title": "Luxembourg", - "keywords": ["luxembourg", "esch-sur-alzette", "differdange", "dudelange", "ettelbruck", "diekirch", "wiltz", "echternach", "rumelange", "grevenmacher", "bertrange", "mamer", "capellen", "strassen"] - }, - { - "slug": "macau", - "title": "Macau", - "keywords": ["macau", "macao"] - }, - { - "slug": "madagascar", - "title": "Madagascar", - "keywords": ["madagascar", "antananarivo", "toamasina", "antsiranana", "mahajanga", "fianarantsoa", "toliara", "antsirabe", "ambositra", "ambatondrazaka", "manakara", "sambava", "morondava", "ambanja", "farafangana", "maintirano", "antsalova", "isoa", "mampikony", "ambatolampy", "ambatofinandrahana", "mandritsara", "marovoay", "moramanga", "vangaindrano", "soaindrana", "ikongo", "tamatave", "diego suarez", "mananjary", "vohemar", "amparafaravola"] - }, - { - "slug": "malawi", - "title": "Malawi", - "keywords": ["malawi", "lilongwe", "blantyre", "mzuzu", "zomba", "karonga", "kasungu", "mangochi", "salima", "liwonde", "balaka"] - }, - { - "slug": "malaysia", - "title": "Malaysia", - "keywords": ["malaysia", "kuala lumpur", "kajang", "klang", "subang", "penang", "ipoh", "selangor", "melaka", "johor", "sabah", "johor bahru", "shah alam", "iskandar puteri"] - }, - { - "slug": "mali", - "title": "Mali", - "keywords": ["mali", "bamako", "sikasso", "kalabancoro", "koutiala", "ségou", "kayes", "kati", "mopti", "niono"] - }, - { - "slug": "malta", - "title": "Malta", - "keywords": ["malta", "birgu", "bormla", "mdina", "qormi", "senglea", "siġġiewi", "valletta", "zabbar", "zebbuġ", "zejtun"] - }, - { - "slug": "mauritania", - "title": "Mauritania", - "keywords": ["mauritania", "mauritanie", "nouakchott", "nouadhibou"] - }, - { - "slug": "mauritius", - "title": "Mauritius", - "keywords": ["mauritius", "port louis", "curepipe", "quatre bornes", "vacoas-phoenix", "vacoas", "beau-bassin-rose-hill", "beau bassin", "rose hill", "mahebourg", "goodlands", "triolet", "bel air", "flacq", "souillac", "pamplemousses", "grand baie", "ebene"] - }, - { - "slug": "mexico", - "title": "Mexico", - "keywords": ["mexico", "mexico city", "guadalajara", "puebla", "tijuana", "mexicali", "monterrey", "hermosillo", "zapopan", "ciudad juarez", "chihuahua", "aguascalientes", "mx"] - }, - { - "slug": "moldova", - "title": "Moldova", - "keywords": ["moldova", "chisinau", "tiraspol", "balti", "bender", "ribnita", "cahul", "ungheni", "soroca", "orhei", "dubasari"] - }, - { - "slug": "morocco", - "title": "Morocco", - "keywords": ["morocco", "casablanca", "fez", "tangier", "marrakesh", "salé", "meknes", "rabat", "oujda", "kenitra", "agadir", "tetouan", "temara", "safi", "mohammedia", "khouribga", "el jadida"] - }, - { - "slug": "mozambique", - "title": "Mozambique", - "keywords": ["mozambique", "maputo", "matola", "nampula", "beira", "sofala", "chimoio", "tete", "quelimane"] - }, - { - "slug": "myanmar", - "title": "Myanmar", - "keywords": ["myanmar", "burma", "yangon", "rangoon", "mandalay", "nay pyi taw", "taunggyi", "bago", "mawlamyine"] - }, - { - "slug": "nepal", - "title": "Nepal", - "keywords": ["nepal", "kathmandu", "pokhara", "lalitpur", "bharatpur", "birgunj", "biratnagar", "janakpur", "ghorahi"] - }, - { - "slug": "netherlands", - "title": "Netherlands", - "keywords": ["netherlands", "nederland", "amsterdam", "rotterdam", "hague", "utrecht", "holland", "delft"] - }, - { - "slug": "new_zealand", - "title": "New Zealand", - "keywords": ["new zealand", "auckland", "wellington", "christchurch", "hamilton", "tauranga", "napier-hastings", "dunedin", "palmerston north", "nelson", "rotorua", "whangarei", "new plymouth", "invercargill", "whanganui", "gisborne"] - }, - { - "slug": "nicaragua", - "title": "Nicaragua", - "keywords": ["nicaragua", "managua", "matagalpa", "chinandega"] - }, - { - "slug": "niger", - "title": "Niger", - "keywords": ["niger", "niamey", "maradi", "zinder", "tahoua", "agadez", "arlit", "birni-n'konni", "dosso", "gaya", "tessaoua"] - }, - { - "slug": "nigeria", - "title": "Nigeria", - "keywords": ["nigeria", "lagos", "kano", "ibadan", "benin city", "port harcourt", "jos", "ilorin", "kaduna"] - }, - { - "slug": "macedonia", - "title": "North Macedonia", - "keywords": ["macedonia", "fyrom", "north macedonia", "mk", "mkd", "ohd", "skp", "skopje", "bitola", "kumanovo", "prilep", "tetovo", "veles", "shtip", "ohrid", "gostivar", "strumica", "kavadarci", "negotino", "berovo", "kratovo", "struga", "valandovo", "demir kapija", "demir hisar", "krusheve", "gevgelija"] - }, - { - "slug": "norway", - "title": "Norway", - "keywords": ["norway", "norge", "oslo", "bergen", "trondheim", "stavanger", "drammen", "fredrikstad", "kristiansand", "tromsø", "sandnes", "ålesund", "bodø", "skien", "haugesund", "tønsberg", "arendal", "porsgrunn", "hamar", "larvik", "moss", "sandefjord", "halden", "harstad", "lillehammer", "molde", "gjøvik", "mo i rana", "steinkjer", "alta", "lommedalen"] - }, - { - "slug": "oman", - "title": "Oman", - "keywords": ["oman", "ad+dakhiliyah", "ad+dhahirah", "batinah+north", "batinah+south", "al+buraymi", "al+wusta", "ash+sharqiyah+north", "ash+sharqiyah+south", "dhofar", "muscat", "musandam"] - }, - { - "slug": "pakistan", - "title": "Pakistan", - "keywords": ["pakistan", "karachi", "lahore", "faisalabad", "rawalpindi", "peshawar", "islamabad"] - }, - { - "slug": "palestine", - "title": "Palestine", - "keywords": ["palestine", "jerusalem", "gaza", "hebron", "jenin", "nablus", "ramallah", "rafah"] - }, - { - "slug": "panama", - "title": "Panama", - "keywords": ["panama", "panamá", "tocumen"] - }, - { - "slug": "papua_new_guinea", - "title": "Papua New Guinea", - "keywords": ["papua new guinea", "port moresby", "lae"] - }, - { - "slug": "paraguay", - "title": "Paraguay", - "keywords": ["paraguay", "asunción", "asuncion", "ciudad del este", "san lorenzo", "luque", "capiata"] - }, - { - "slug": "peru", - "title": "Peru", - "keywords": ["peru", "lima", "cusco", "cuzco", "ica", "arequipa", "trujillo", "chiclayo", "huancayo", "piura", "chimbote", "iquitos", "juliaca", "cajamarca"] - }, - { - "slug": "philippines", - "title": "Philippines", - "keywords": ["philippines", "pilipinas", "quezon", "manila", "davao", "caloocan", "cebu", "zamboanga", "bohol", "pasig", "bacolod", "makati", "baguio", "cavite"] - }, - { - "slug": "poland", - "title": "Poland", - "keywords": ["poland", "polska", "warsaw", "krakow", "lodz", "wroclaw", "poznan", "gdansk", "szczecin", "bydgoszcz", "lublin", "katowice", "bialystok"] - }, - { - "slug": "portugal", - "title": "Portugal", - "keywords": ["portugal", "lisbon", "lisboa", "braga", "porto", "aveiro", "coimbra", "funchal", "madeira"] - }, - { - "slug": "qatar", - "title": "Qatar", - "keywords": ["qatar", "doha"] - }, - { - "slug": "south_korea", - "title": "Republic of Korea", - "keywords": ["south korea", "rok", "korea", "seoul", "busan", "incheon", "daegu", "daejeon", "gwangju", "대한민국", "서울", "서울시"] - }, - { - "slug": "congo_brazzaville", - "title": "Republic of the Congo", - "keywords": ["congo brazza", "cog", "brazzaville", "djambala", "pointe noire", "sibiti", "owando", "madingou", "loango", "kinkala", "impfondo", "dolisie"] - }, - { - "slug": "romania", - "title": "Romania", - "keywords": ["romania", "bucharest", "cluj", "iasi", "timisoara", "craiova", "brasov", "sibiu", "constanta", "oradea", "galati", "ploesti", "pitesti", "arad", "bacau"] - }, - { - "slug": "russia", - "title": "Russia", - "keywords": ["russia", "moscow", "saint petersburg", "novosibirsk", "yekaterinburg", "nizhny novgorod", "samara", "omsk", "kazan", "chelyabinsk", "rostov-on-don", "ufa", "volgograd"] - }, - { - "slug": "rwanda", - "title": "Rwanda", - "keywords": ["rwanda", "kigali", "butare", "muhanga", "ruhengeri", "gisenyi", "nyarugenge", "huye", "musanze", "rubavu", "rwamagana", "kirehe", "kibungo", "ngoma", "nyagatare", "gicumbi", "nyabihu", "kibuye", "karongi", "rusizi", "nyamasheke", "ruhango", "nyanza", "kamonyi", "kicukiro", "gasabo"] - }, - { - "slug": "saudi_arabia", - "title": "Saudi Arabia", - "keywords": ["saudi", "ksa", "riyadh", "mecca", "jeddah", "dammam"] - }, - { - "slug": "senegal", - "title": "Senegal", - "keywords": ["senegal", "dakar", "touba", "thies", "rufisque", "kaolack", "ziguinchor", "tambacounda", "kaffrine", "diourbel"] - }, - { - "slug": "serbia", - "title": "Serbia", - "keywords": ["serbia", "belgrade", "novi sad", "nis", "kragujevac", "subotica", "zrenjanin", "pancevo", "cacak", "novi pazar", "kraljevo", "smederevo"] - }, - { - "slug": "sierra_leone", - "title": "Sierra Leone", - "keywords": ["sierra leone", "freetown", "makeni", "koidu"] - }, - { - "slug": "singapore", - "title": "Singapore", - "keywords": ["singapore"] - }, - { - "slug": "slovakia", - "title": "Slovakia", - "keywords": ["slovakia", "bratislava", "kosice", "presov", "zilina"] - }, - { - "slug": "slovenia", - "title": "Slovenia", - "keywords": ["slovenia", "slovenija", "ljubljana", "maribor", "celje", "kranj", "koper", "velenje", "novo mesto", "nova gorica", "krsko", "krško", "murska sobota", "postojna", "slovenj gradec"] - }, - { - "slug": "somalia", - "title": "Somalia", - "keywords": ["somalia", "mogadishu", "hargeisa", "bosaso", "borama", "garowe", "kismayo"] - }, - { - "slug": "south_africa", - "title": "South Africa", - "keywords": ["south africa", "johannesburg", "cape town", "rsa", "durban", "port elizabeth", "pretoria", "nelspruit", "knysna"] - }, - { - "slug": "south_sudan", - "title": "South Sudan", - "keywords": ["south sudan", "juba", "yei", "wau", "aweil", "jonglei", "maridi"] - }, - { - "slug": "spain", - "title": "Spain", - "keywords": ["spain", "españa", "madrid", "barcelona", "valencia", "seville", "sevilla", "zaragoza", "malaga", "murcia", "palma", "bilbao", "alicante", "cordoba"] - }, - { - "slug": "sri_lanka", - "title": "Sri Lanka", - "keywords": ["sri lanka", "balangoda", "ratnapura", "colombo", "moratuwa", "negombo", "galle", "jaffna"] - }, - { - "slug": "sudan", - "title": "Sudan", - "keywords": ["sudan", "khartoum", "omdurman"] - }, - { - "slug": "suriname", - "title": "Suriname", - "keywords": ["suriname", "paramaribo"] - }, - { - "slug": "sweden", - "title": "Sweden", - "keywords": ["sweden", "sverige", "stockholm", "malmö", "uppsala", "göteborg", "gothenburg"] - }, - { - "slug": "switzerland", - "title": "Switzerland", - "keywords": ["switzerland", "zurich", "zürich", "geneva", "basel", "lausanne", "bern", "winterthur", "lucerne", "gallen", "lugano", "biel", "thun"] - }, - { - "slug": "syria", - "title": "Syria", - "keywords": ["syria", "سوريا", "damascus", "hama", "aleppo", "homs", "rif dimashq", "tartus", "latakia", "idlib", "raqqa", "daraa", "alhasakah", "dierezzor", "quneitra", "alsuwayda"] - }, - { - "slug": "taiwan", - "title": "Taiwan", - "keywords": ["taiwan", "taichung", "kaohsiung", "taipei", "taoyuan", "tainan", "hsinchu", "keelung", "chiayi", "changhua"] - }, - { - "slug": "tajikistan", - "title": "Tajikistan", - "keywords": ["tajikistan", "dushanbe", "khujand", "kulob", "bokhtar", "khorugh", "panjakent", "isfara", "hisor", "ghissar", "rasht"] - }, - { - "slug": "tanzania", - "title": "Tanzania", - "keywords": ["tanzania", "dar es salaam", "mwanza", "arusha", "dodoma", "mbeya", "morogoro", "tanga", "kilimanjaro"] - }, - { - "slug": "thailand", - "title": "Thailand", - "keywords": ["thailand", "bangkok", "nonthaburi", "nakhon", "phuket", "pattaya", "chiang mai"] - }, - { - "slug": "the_bahamas", - "title": "The Bahamas", - "keywords": ["bahamas"] - }, - { - "slug": "togo", - "title": "Togo", - "keywords": ["togo", "lome"] - }, - { - "slug": "tunisia", - "title": "Tunisia", - "keywords": ["tunisia", "tunis", "sfax", "sousse", "kairouan", "ariana", "gabes", "bizerte"] - }, - { - "slug": "turkey", - "title": "Turkey", - "keywords": ["turkey", "turkiye", "istanbul", "ankara", "izmir", "bursa", "adana", "gaziantep", "konya", "antalya", "kayseri", "mersin", "eskisehir", "samsun", "denizli", "malatya"] - }, - { - "slug": "turkmenistan", - "title": "Turkmenistan", - "keywords": ["turkmenistan", "turkmenabat"] - }, - { - "slug": "uganda", - "title": "Uganda", - "keywords": ["uganda", "kampala", "mbarara", "mukono", "jinja", "arua", "gulu", "masaka"] - }, - { - "slug": "ukraine", - "title": "Ukraine", - "keywords": ["ukraine", "kiev", "kyiv", "kharkiv", "dnipro", "odesa", "donetsk", "zaporizhia"] - }, - { - "slug": "uae", - "title": "United Arab Emirates", - "keywords": ["uae", "emirates", "dubai", "abu dhabi", "sharjah", "al ain", "ajman"] - }, - { - "slug": "uk", - "title": "United Kingdom", - "keywords": ["uk", "england", "scotland", "wales", "northern ireland", "london", "birmingham", "leeds", "glasgow", "sheffield", "bradford", "manchester", "edinburgh", "liverpool", "bristol", "cardiff", "belfast", "leicester", "wakefield", "coventry", "nottingham", "newcastle"] - }, - { - "slug": "united_states", - "title": "United States", - "keywords": ["us", "usa", "united states", "alabama", "alaska", "ak", "arizona", "az", "arkansas", "ar", "california", "ca", "colorado", "co", "connecticut", "ct", "delaware", "de", "florida", "fl", "georgia", "ga", "hawaii", "hi", "idaho", "id", "illinois", "il", "indiana", "in", "iowa", "ia", "kansas", "ks", "kentucky", "ky", "louisiana", "la", "maine", "me", "maryland", "md", "massachusetts", "ma", "michigan", "mi", "minnesota", "mn", "mississippi", "ms", "missouri", "mo", "montana", "mt", "nebraska", "ne", "nevada", "nv", "new hampshire", "nh", "new jersey", "nj", "new mexico", "nm", "new york", "ny", "north carolina", "nc", "north dakota", "nd", "ohio", "oh", "oklahoma", "ok", "oregon", "or", "pennsylvania", "pa", "rhode island", "ri", "south carolina", "sc", "south dakota", "sd", "tennessee", "tn", "texas", "tx", "utah", "ut", "vermont", "vt", "virginia", "va", "washington", "wa", "west virginia", "wv", "wisconsin", "wi", "wyoming", "wy", "los angeles", "chicago", "houston", "phoenix", "philadelphia", "san antonio", "san diego", "dallas", "san jose", "austin", "jacksonville", "fort worth", "columbus", "charlotte", "san francisco", "indianapolis", "seattle", "denver", "boston", "el paso", "nashville", "detroit", "portland", "las vegas", "memphis", "louisville", "baltimore"] - }, - { - "slug": "uruguay", - "title": "Uruguay", - "keywords": ["uruguay", "montevideo"] - }, - { - "slug": "uzbekistan", - "title": "Uzbekistan", - "keywords": ["uzbekistan", "tashkent", "namangan", "samarkand", "andijan", "nukus", "bukhara", "qarshi", "fergana"] - }, - { - "slug": "venezuela", - "title": "Venezuela", - "keywords": ["venezuela", "caracas", "maracaibo", "barquisimeto", "guayana", "maturín", "zulia", "bolivar"] - }, - { - "slug": "vietnam", - "title": "Vietnam", - "keywords": ["vietnam", "viet nam", "ho chi minh", "hanoi", "ha noi", "hai phong", "da nang", "can tho", "bien hoa", "nha trang", "vinh"] - }, - { - "slug": "worldwide", - "title": "Worldwide", - "keywords": [] - }, - { - "slug": "yemen", - "title": "Yemen", - "keywords": ["yemen", "sana'a", "taiz", "aden", "mukalla", "ibb"] - }, - { - "slug": "zambia", - "title": "Zambia", - "keywords": ["zambia", "lusaka", "kitwe", "ndola"] - }, - { - "slug": "zimbabwe", - "title": "Zimbabwe", - "keywords": ["zimbabwe", "harare", "bulawayo", "mutare", "gweru", "kwekwe"] - } -] + { "slug": "afghanistan", "title": "Afghanistan", "isoCode": "af", "keywords": ["afghanistan", "kabul", "kandahar", "herat", "mazar-e-sharif", "jalalabad", "ghazni", "nangarhar", "khost", "zabul", "helmand", "parwan", "farah", "kunar", "wardak", "baghlan", "kunduz", "takhar", "paktia", "paktika"] }, + { "slug": "albania", "title": "Albania", "isoCode": "al", "keywords": ["albania", "tirana", "durres", "vlore", "elbasan", "shkoder"] }, + { "slug": "algeria", "title": "Algeria", "isoCode": "dz", "keywords": ["algeria", "algiers", "oran", "constantine", "annaba", "blida", "batna", "djelfa", "setif", "sidi bel abbes", "biskra", "tiaret", "relizane", "mostaganem", "tlemcen", "chlef", "jijel"] }, + { "slug": "angola", "title": "Angola", "isoCode": "ao", "keywords": ["angola", "luanda", "huambo", "lobito", "benguela"] }, + { "slug": "argentina", "title": "Argentina", "isoCode": "ar", "keywords": ["argentina", "buenos aires", "cordoba", "rosario", "mendoza", "la plata", "tucuman", "mar del plata", "salta", "resistencia"] }, + { "slug": "armenia", "title": "Armenia", "isoCode": "am", "keywords": ["armenia", "yerevan", "gyumri", "vanadzor", "vagharshapat", "abovyan", "kapan", "hrazdan", "armavir", "artashat", "ijevan", "gavar", "goris", "dilijan", "stepanakert", "martuni", "sisian", "alaverdi", "stepanavan", "berd"] }, + { "slug": "australia", "title": "Australia", "isoCode": "au", "keywords": ["australia", "sydney", "melbourne", "brisbane", "perth", "adelaide", "canberra", "hobart"] }, + { "slug": "austria", "title": "Austria", "isoCode": "at", "keywords": ["austria", "österreich", "vienna", "wien", "linz", "salzburg", "graz", "innsbruck", "klagenfurt", "wels", "dornbirn"] }, + { "slug": "azerbaijan", "title": "Azerbaijan", "isoCode": "az", "keywords": ["azerbaijan", "baku", "sumqayit", "ganja", "lankaran"] }, + { "slug": "bahrain", "title": "Bahrain", "isoCode": "bh", "keywords": ["bahrain", "manama", "muharraq", "riffa", "hamad town", "isa town"] }, + { "slug": "bangladesh", "title": "Bangladesh", "isoCode": "bd", "keywords": ["bangladesh", "dhaka", "chittagong", "khulna", "rajshahi", "barisal", "sylhet", "rangpur", "comilla", "gazipur"] }, + { "slug": "belarus", "title": "Belarus", "isoCode": "by", "keywords": ["belarus", "minsk", "brest", "grodno", "gomel", "vitebsk", "mogilev", "slutsk", "borisov", "pinsk", "baranovichi", "bobruisk", "soligorsk"] }, + { "slug": "belgium", "title": "Belgium", "isoCode": "be", "keywords": ["belgium", "antwerp", "ghent", "charleroi", "liege", "brussels", "belgique"] }, + { "slug": "benin", "title": "Benin", "isoCode": "bj", "keywords": ["benin", "cotonou", "porto-novo", "abomey"] }, + { "slug": "bolivia", "title": "Bolivia", "isoCode": "bo", "keywords": ["bolivia", "santa cruz de la sierra", "el alto", "la paz", "cochabamba", "oruro", "sucre"] }, + { "slug": "bosnia_and_herzegovina", "title": "Bosnia and Herzegovina", "isoCode": "ba", "keywords": ["sarajevo", "banja luka", "tuzla", "zenica", "bijeljina", "mostar", "prijedor", "brcko", "doboj", "cazin"] }, + { "slug": "botswana", "title": "Botswana", "isoCode": "bw", "keywords": ["botswana", "gaborone", "francistown"] }, + { "slug": "brazil", "title": "Brazil", "isoCode": "br", "keywords": ["brazil", "brasil", "são paulo", "brasília", "salvador", "fortaleza", "belém", "belo horizonte", "manaus", "curitiba", "recife", "rio de janeiro", "maceió", "aracaju", "porto alegre", "florianópolis", "acre", "alagoas", "amapá", "amazonas", "bahia", "ceará", "distrito federal", "espírito santo", "goiás", "maranhão", "mato grosso", "mato grosso do sul", "minas gerais", "pará", "paraíba", "paraná", "pernambuco", "piauí", "rio grande do norte", "rio grande do sul", "rondônia", "roraima", "santa catarina", "sergipe", "tocantins"] }, + { "slug": "bulgaria", "title": "Bulgaria", "isoCode": "bg", "keywords": ["bulgaria", "sofia", "plovdiv", "varna", "burgas", "ruse", "stara zagora", "pleven"] }, + { "slug": "burkina_faso", "title": "Burkina Faso", "isoCode": "bf", "keywords": ["burkina faso", "ouagadougou", "bobo-dioulasso", "koudougou", "banfora", "ouahigouya", "pouytenga", "kaya", "tenkodogo", "fada n'gourma", "houndé"] }, + { "slug": "burundi", "title": "Burundi", "isoCode": "bi", "keywords": ["burundi", "bujumbura", "gitega"] }, + { "slug": "cambodia", "title": "Cambodia", "isoCode": "kh", "keywords": ["cambodia", "phnom", "battambang", "siem reap", "kampong"] }, + { "slug": "cameroon", "title": "Cameroon", "isoCode": "cm", "keywords": ["cameroon", "douala", "yaoundé", "bafoussam", "bamenda", "garoua", "maroua", "ngaoundéré", "kumba", "nkongsamba", "buea"] }, + { "slug": "canada", "title": "Canada", "isoCode": "ca", "keywords": ["canada", "ottawa", "edmonton", "winnipeg", "vancouver", "toronto", "quebec", "montreal", "mississauga", "calgary"] }, + { "slug": "chad", "title": "Chad", "isoCode": "td", "keywords": ["chad", "tchad", "n'djamena", "moundou"] }, + { "slug": "chile", "title": "Chile", "isoCode": "cl", "keywords": ["chile", "santiago", "valparaíso", "concepción", "la serena", "antofagasta", "temuco", "rancagua", "talca", "arica", "chillán"] }, + { "slug": "china", "title": "China", "isoCode": "cn", "keywords": ["china", "中国", "guangzhou", "shanghai", "beijing", "hangzhou"] }, + { "slug": "colombia", "title": "Colombia", "isoCode": "co", "keywords": ["colombia", "bogota", "medellin", "cali", "barranquilla", "cartagena", "cucuta", "bucaramanga", "ibague", "soledad", "pereira", "santa marta"] }, + { "slug": "costa_rica", "title": "Costa Rica", "isoCode": "cr", "keywords": ["costa rica", "san josé", "alajuela", "cartago", "heredia", "guanacaste", "puntarenas", "limón"] }, + { "slug": "croatia", "title": "Croatia", "isoCode": "hr", "keywords": ["croatia", "hrvatska", "zagreb", "split", "rijeka", "osijek", "zadar", "pula"] }, + { "slug": "cuba", "title": "Cuba", "isoCode": "cu", "keywords": ["cuba", "havana", "santiago de cuba", "camaguey", "holguin", "guantanamo", "bayamo"] }, + { "slug": "cyprus", "title": "Cyprus", "isoCode": "cy", "keywords": ["cyprus", "nicosia", "lefkosia", "limassol", "lemessos", "larnaka", "paphos"] }, + { "slug": "czech_republic", "title": "Czech Republic", "isoCode": "cz", "keywords": ["czech", "czechia", "ceska", "prague", "budejovice", "plzen", "karlovy", "ostrava", "brno"] }, + { "slug": "congo_kinshasa", "title": "Democratic Republic of the Congo", "isoCode": "cd", "keywords": ["congo kinshasa", "drc", "cod", "kinshasa", "lubumbashi", "bukavu", "kananga", "goma", "mbuji mayi", "likasi", "kolwezi", "kalemie", "uvira", "matadi", "moba", "kamina", "kabalo", "fungurume"] }, + { "slug": "denmark", "title": "Denmark", "isoCode": "dk", "keywords": ["denmark", "danmark", "copenhagen", "aarhus", "odense", "aalborg"] }, + { "slug": "dominican_republic", "title": "Dominican Republic", "isoCode": "do", "keywords": ["dominican republic", "republica dominicana", "santo domingo", "la vega", "macoris"] }, + { "slug": "ecuador", "title": "Ecuador", "isoCode": "ec", "keywords": ["ecuador", "guayaquil", "quito", "cuenca", "machala"] }, + { "slug": "egypt", "title": "Egypt", "isoCode": "eg", "keywords": ["egypt", "cairo", "alexandria", "giza", "port said", "suez", "luxor", "el mahalla", "asyut", "asiut", "al mansurah", "mansoura", "tanta", "ismailia", "hurghada", "sharm el-sheikh", "nuweiba", "dahab", "ain shams", "ain el sokhna", "ain elsokhna", "gouna", "el gouna", "zagazig", "fayoum", "faiyum", "aswan", "minya", "sohag", "beni suef", "damietta", "kafr el-sheikh", "banha", "damanhur", "shibin el kom", "qena", "arish", "marsa matrouh", "kharga", "monufia", "sharkia", "sharqia", "dakahlia", "gharbia", "qalyubia", "beheira", "matrouh", "north sinai", "south sinai", "red sea", "new valley", "10th of ramadan", "6th of october", "obour city", "new cairo", "sadat city", "borg el arab", "om el donia", "masr", "heliopolis", "nasr city"] }, + { "slug": "el_salvador", "title": "El Salvador", "isoCode": "sv", "keywords": ["el salvador"] }, + { "slug": "estonia", "title": "Estonia", "isoCode": "ee", "keywords": ["estonia", "eesti", "tallinn", "tartu", "narva", "pärnu", "rakvere", "kohtla-järve", "viljandi", "maardu", "sillamäe"] }, + { "slug": "ethiopia", "title": "Ethiopia", "isoCode": "et", "keywords": ["ethiopia", "addis ababa", "gondar", "adama", "hawassa", "bahir dar"] }, + { "slug": "finland", "title": "Finland", "isoCode": "fi", "keywords": ["finland", "suomi", "helsinki", "tampere", "oulu", "espoo", "vantaa", "turku", "rovaniemi", "jyväskylä", "lahti", "kuopio", "pori", "lappeenranta", "vaasa"] }, + { "slug": "france", "title": "France", "isoCode": "fr", "keywords": ["france", "paris", "marseille", "lyon", "toulouse", "nice", "nantes", "strasbourg", "montpellier", "bordeaux", "lille", "rennes", "reims", "rouen", "toulon", "le havre", "grenoble", "dijon", "le mans", "brest", "tours"] }, + { "slug": "gabon", "title": "Gabon", "isoCode": "ga", "keywords": ["gabon", "libreville", "port-gentil", "franceville", "oyem", "moanda"] }, + { "slug": "georgia", "title": "Georgia", "isoCode": "ge", "keywords": ["tbilisi", "batumi", "kutaisi", "rustavi", "zugdidi", "gori", "poti", "telavi", "akhaltsikhe", "mtskheta", "ozurgeti", "sukhumi", "samtredia", "marneuli"] }, + { "slug": "germany", "title": "Germany", "isoCode": "de", "keywords": ["germany", "deutschland", "berlin", "frankfurt", "munich", "münchen", "hamburg", "cologne", "köln"] }, + { "slug": "ghana", "title": "Ghana", "isoCode": "gh", "keywords": ["ghana", "accra", "kumasi", "sekondi", "ashaiman", "sunyani", "tamale", "tema"] }, + { "slug": "greece", "title": "Greece", "isoCode": "gr", "keywords": ["greece", "ελλάδα", "athens", "thessaloniki", "patras", "heraklion", "larissa", "volos", "rhodes", "ioannina", "chania", "crete"] }, + { "slug": "guatemala", "title": "Guatemala", "isoCode": "gt", "keywords": ["guatemala", "mixco", "villa nueva", "petapa", "quetzaltenango"] }, + { "slug": "guinea", "title": "Guinea", "isoCode": "gn", "keywords": ["conakry"] }, + { "slug": "haiti", "title": "Haiti", "isoCode": "ht", "keywords": ["haiti", "port-au-prince", "cap-haitien", "carrefour", "delmas", "petion-ville"] }, + { "slug": "honduras", "title": "Honduras", "isoCode": "hn", "keywords": ["honduras", "tegucigalpa", "san pedro sula", "choloma", "la ceiba", "el progreso", "choluteca", "comayagua"] }, + { "slug": "hong_kong", "title": "Hong Kong", "isoCode": "hk", "keywords": ["hong kong", "香港", "kowloon", "九龍"] }, + { "slug": "hungary", "title": "Hungary", "isoCode": "hu", "keywords": ["hungary", "magyarország", "budapest", "szeged", "miskolc"] }, + { "slug": "india", "title": "India", "isoCode": "in", "keywords": ["india", "mumbai", "delhi", "bangalore", "hyderabad", "ahmedabad", "chennai", "kolkata", "jaipur", "pune", "gurgaon", "noida"] }, + { "slug": "indonesia", "title": "Indonesia", "isoCode": "id", "keywords": ["indonesia", "jakarta", "surabaya", "bandung", "medan", "bekasi", "semarang", "tangerang", "depok", "makassar", "palembang"] }, + { "slug": "iran", "title": "Iran", "isoCode": "ir", "keywords": ["iran", "tehran", "mashhad", "isfahan", "esfahan", "karaj", "shiraz", "tabriz", "qom", "ahvaz", "ahwaz", "kermanshah", "urmia", "rasht", "kerman"] }, + { "slug": "iraq", "title": "Iraq", "isoCode": "iq", "keywords": ["baghdad", "mosul", "basra", "najaf", "karbala", "al-nasiriya", "al-amarah"] }, + { "slug": "ireland", "title": "Ireland", "isoCode": "ie", "keywords": ["ireland", "dublin", "cork", "limerick", "galway", "waterford", "drogheda", "dundalk"] }, + { "slug": "italy", "title": "Italy", "isoCode": "it", "keywords": ["italy", "italia", "rome", "roma", "milan", "naples", "napoli", "turin", "torino", "palermo", "genoa", "genova", "bologna", "florence", "firenze", "bari", "catania", "venice", "verona"] }, + { "slug": "ivory_coast", "title": "Ivory Coast", "isoCode": "ci", "keywords": ["ivory", "abidjan", "bouaké", "daloa", "yamoussoukro"] }, + { "slug": "japan", "title": "Japan", "isoCode": "jp", "keywords": ["japan", "tokyo", "yokohama", "osaka", "nagoya", "sapporo", "kobe", "kyoto", "fukuoka", "kawasaki", "saitama", "hiroshima", "sendai"] }, + { "slug": "jordan", "title": "Jordan", "isoCode": "jo", "keywords": ["jordan", "amman", "zarqa", "irbid"] }, + { "slug": "kazakhstan", "title": "Kazakhstan", "isoCode": "kz", "keywords": ["kazakhstan", "almaty", "shymkent", "karagandy", "taraz", "nur-sultan", "pavlodar", "oskemen", "semey"] }, + { "slug": "kenya", "title": "Kenya", "isoCode": "ke", "keywords": ["kenya", "nairobi", "mombasa", "kisumu", "nakuru", "eldoret", "kisii", "nyeri", "machakos", "embu"] }, + { "slug": "kosovo", "title": "Kosovo", "isoCode": "xk", "keywords": ["kosovo", "kosove", "prishtine", "prizren", "peja", "gjakova", "ferizaj", "gjilan", "mitrovica", "podujev", "vushtrri", "suhareke", "rahovec", "lipjan", "skenderaj", "kamenice", "malisheve", "decan", "istog", "kline", "fushe kosove"] }, + { "slug": "kurdistan", "title": "Kurdistan", "isoCode": "iq", "keywords": ["kurdistan", "erbil", "hawler", "sulaymaniyah", "slemani", "duhok", "halabja", "kirkuk"] }, + { "slug": "kyrgyzstan", "title": "Kyrgyzstan", "isoCode": "kg", "keywords": ["kyrgyzstan", "bishkek", "osh", "jalal-abad", "karakol", "tokmok"] }, + { "slug": "laos", "title": "Laos", "isoCode": "la", "keywords": ["laos", "vientiane", "pakse"] }, + { "slug": "latvia", "title": "Latvia", "isoCode": "lv", "keywords": ["latvia", "latvija", "riga", "rīga", "kuldiga", "kuldīga", "ventspils", "liepaja", "liepāja", "daugavpils", "jelgava", "jurmala", "jūrmala"] }, + { "slug": "lebanon", "title": "Lebanon", "isoCode": "lb", "keywords": ["lebanon", "beirut", "sidon", "tyre", "tripoli", "byblos", "bekaa", "jounieh", "zahle", "baalbek", "nabatieh", "jbeil", "batroun", "achrafieh", "hamra"] }, + { "slug": "libya", "title": "Libya", "isoCode": "ly", "keywords": ["libya", "tripoli", "benghazi", "misrata", "zliten", "bayda"] }, + { "slug": "lithuania", "title": "Lithuania", "isoCode": "lt", "keywords": ["lithuania", "vilnius", "kaunas", "klaipeda", "siauliai", "panevezys", "alytus"] }, + { "slug": "luxembourg", "title": "Luxembourg", "isoCode": "lu", "keywords": ["luxembourg", "esch-sur-alzette", "differdange", "dudelange", "ettelbruck", "diekirch", "wiltz", "echternach", "rumelange", "grevenmacher", "bertrange", "mamer", "capellen", "strassen"] }, + { "slug": "macau", "title": "Macau", "isoCode": "mo", "keywords": ["macau", "macao"] }, + { "slug": "madagascar", "title": "Madagascar", "isoCode": "mg", "keywords": ["madagascar", "antananarivo", "toamasina", "antsiranana", "mahajanga", "fianarantsoa", "toliara", "antsirabe", "ambositra", "ambatondrazaka", "manakara", "sambava", "morondava", "ambanja", "farafangana", "maintirano", "antsalova", "isoa", "mampikony", "ambatolampy", "ambatofinandrahana", "mandritsara", "marovoay", "moramanga", "vangaindrano", "soaindrana", "ikongo", "tamatave", "diego suarez", "mananjary", "vohemar", "amparafaravola"] }, + { "slug": "malawi", "title": "Malawi", "isoCode": "mw", "keywords": ["malawi", "lilongwe", "blantyre", "mzuzu", "zomba", "karonga", "kasungu", "mangochi", "salima", "liwonde", "balaka"] }, + { "slug": "malaysia", "title": "Malaysia", "isoCode": "my", "keywords": ["malaysia", "kuala lumpur", "kajang", "klang", "subang", "penang", "ipoh", "selangor", "melaka", "johor", "sabah", "johor bahru", "shah alam", "iskandar puteri"] }, + { "slug": "mali", "title": "Mali", "isoCode": "ml", "keywords": ["mali", "bamako", "sikasso", "kalabancoro", "koutiala", "ségou", "kayes", "kati", "mopti", "niono"] }, + { "slug": "malta", "title": "Malta", "isoCode": "mt", "keywords": ["malta", "birgu", "bormla", "mdina", "qormi", "senglea", "siġġiewi", "valletta", "zabbar", "zebbuġ", "zejtun"] }, + { "slug": "mauritania", "title": "Mauritania", "isoCode": "mr", "keywords": ["mauritania", "mauritanie", "nouakchott", "nouadhibou"] }, + { "slug": "mauritius", "title": "Mauritius", "isoCode": "mu", "keywords": ["mauritius", "port louis", "curepipe", "quatre bornes", "vacoas-phoenix", "vacoas", "beau-bassin-rose-hill", "beau bassin", "rose hill", "mahebourg", "goodlands", "triolet", "bel air", "flacq", "souillac", "pamplemousses", "grand baie", "ebene"] }, + { "slug": "mexico", "title": "Mexico", "isoCode": "mx", "keywords": ["mexico", "mexico city", "guadalajara", "puebla", "tijuana", "mexicali", "monterrey", "hermosillo", "zapopan", "ciudad juarez", "chihuahua", "aguascalientes", "mx"] }, + { "slug": "moldova", "title": "Moldova", "isoCode": "md", "keywords": ["moldova", "chisinau", "tiraspol", "balti", "bender", "ribnita", "cahul", "ungheni", "soroca", "orhei", "dubasari"] }, + { "slug": "morocco", "title": "Morocco", "isoCode": "ma", "keywords": ["morocco", "casablanca", "fez", "tangier", "marrakesh", "salé", "meknes", "rabat", "oujda", "kenitra", "agadir", "tetouan", "temara", "safi", "mohammedia", "khouribga", "el jadida"] }, + { "slug": "mozambique", "title": "Mozambique", "isoCode": "mz", "keywords": ["mozambique", "maputo", "matola", "nampula", "beira", "sofala", "chimoio", "tete", "quelimane"] }, + { "slug": "myanmar", "title": "Myanmar", "isoCode": "mm", "keywords": ["myanmar", "burma", "yangon", "rangoon", "mandalay", "nay pyi taw", "taunggyi", "bago", "mawlamyine"] }, + { "slug": "nepal", "title": "Nepal", "isoCode": "np", "keywords": ["nepal", "kathmandu", "pokhara", "lalitpur", "bharatpur", "birgunj", "biratnagar", "janakpur", "ghorahi"] }, + { "slug": "netherlands", "title": "Netherlands", "isoCode": "nl", "keywords": ["netherlands", "nederland", "amsterdam", "rotterdam", "hague", "utrecht", "holland", "delft"] }, + { "slug": "new_zealand", "title": "New Zealand", "isoCode": "nz", "keywords": ["new zealand", "auckland", "wellington", "christchurch", "hamilton", "tauranga", "napier-hastings", "dunedin", "palmerston north", "nelson", "rotorua", "whangarei", "new plymouth", "invercargill", "whanganui", "gisborne"] }, + { "slug": "nicaragua", "title": "Nicaragua", "isoCode": "ni", "keywords": ["nicaragua", "managua", "matagalpa", "chinandega"] }, + { "slug": "niger", "title": "Niger", "isoCode": "ne", "keywords": ["niger", "niamey", "maradi", "zinder", "tahoua", "agadez", "arlit", "birni-n'konni", "dosso", "gaya", "tessaoua"] }, + { "slug": "nigeria", "title": "Nigeria", "isoCode": "ng", "keywords": ["nigeria", "lagos", "kano", "ibadan", "benin city", "port harcourt", "jos", "ilorin", "kaduna"] }, + { "slug": "macedonia", "title": "North Macedonia", "isoCode": "mk", "keywords": ["macedonia", "fyrom", "north macedonia", "mk", "mkd", "ohd", "skp", "skopje", "bitola", "kumanovo", "prilep", "tetovo", "veles", "shtip", "ohrid", "gostivar", "strumica", "kavadarci", "negotino", "berovo", "kratovo", "struga", "valandovo", "demir kapija", "demir hisar", "krusheve", "gevgelija"] }, + { "slug": "norway", "title": "Norway", "isoCode": "no", "keywords": ["norway", "norge", "oslo", "bergen", "trondheim", "stavanger", "drammen", "fredrikstad", "kristiansand", "tromsø", "sandnes", "ålesund", "bodø", "skien", "haugesund", "tønsberg", "arendal", "porsgrunn", "hamar", "larvik", "moss", "sandefjord", "halden", "harstad", "lillehammer", "molde", "gjøvik", "mo i rana", "steinkjer", "alta", "lommedalen"] }, + { "slug": "oman", "title": "Oman", "isoCode": "om", "keywords": ["oman", "ad+dakhiliyah", "ad+dhahirah", "batinah+north", "batinah+south", "al+buraymi", "al+wusta", "ash+sharqiyah+north", "ash+sharqiyah+south", "dhofar", "muscat", "musandam"] }, + { "slug": "pakistan", "title": "Pakistan", "isoCode": "pk", "keywords": ["pakistan", "karachi", "lahore", "faisalabad", "rawalpindi", "peshawar", "islamabad"] }, + { "slug": "palestine", "title": "Palestine", "isoCode": "ps", "keywords": ["palestine", "jerusalem", "gaza", "hebron", "jenin", "nablus", "ramallah", "rafah"] }, + { "slug": "panama", "title": "Panama", "isoCode": "pa", "keywords": ["panama", "panamá", "tocumen"] }, + { "slug": "papua_new_guinea", "title": "Papua New Guinea", "isoCode": "pg", "keywords": ["papua new guinea", "port moresby", "lae"] }, + { "slug": "paraguay", "title": "Paraguay", "isoCode": "py", "keywords": ["paraguay", "asunción", "asuncion", "ciudad del este", "san lorenzo", "luque", "capiata"] }, + { "slug": "peru", "title": "Peru", "isoCode": "pe", "keywords": ["peru", "lima", "cusco", "cuzco", "ica", "arequipa", "trujillo", "chiclayo", "huancayo", "piura", "chimbote", "iquitos", "juliaca", "cajamarca"] }, + { "slug": "philippines", "title": "Philippines", "isoCode": "ph", "keywords": ["philippines", "pilipinas", "quezon", "manila", "davao", "caloocan", "cebu", "zamboanga", "bohol", "pasig", "bacolod", "makati", "baguio", "cavite"] }, + { "slug": "poland", "title": "Poland", "isoCode": "pl", "keywords": ["poland", "polska", "warsaw", "krakow", "lodz", "wroclaw", "poznan", "gdansk", "szczecin", "bydgoszcz", "lublin", "katowice", "bialystok"] }, + { "slug": "portugal", "title": "Portugal", "isoCode": "pt", "keywords": ["portugal", "lisbon", "lisboa", "braga", "porto", "aveiro", "coimbra", "funchal", "madeira"] }, + { "slug": "qatar", "title": "Qatar", "isoCode": "qa", "keywords": ["qatar", "doha"] }, + { "slug": "south_korea", "title": "Republic of Korea", "isoCode": "kr", "keywords": ["south korea", "rok", "korea", "seoul", "busan", "incheon", "daegu", "daejeon", "gwangju", "대한민국", "서울", "서울시"] }, + { "slug": "congo_brazzaville", "title": "Republic of the Congo", "isoCode": "cg", "keywords": ["congo brazza", "cog", "brazzaville", "djambala", "pointe noire", "sibiti", "owando", "madingou", "loango", "kinkala", "impfondo", "dolisie"] }, + { "slug": "romania", "title": "Romania", "isoCode": "ro", "keywords": ["romania", "bucharest", "cluj", "iasi", "timisoara", "craiova", "brasov", "sibiu", "constanta", "oradea", "galati", "ploesti", "pitesti", "arad", "bacau"] }, + { "slug": "russia", "title": "Russia", "isoCode": "ru", "keywords": ["russia", "moscow", "saint petersburg", "novosibirsk", "yekaterinburg", "nizhny novgorod", "samara", "omsk", "kazan", "chelyabinsk", "rostov-on-don", "ufa", "volgograd"] }, + { "slug": "rwanda", "title": "Rwanda", "isoCode": "rw", "keywords": ["rwanda", "kigali", "butare", "muhanga", "ruhengeri", "gisenyi", "nyarugenge", "huye", "musanze", "rubavu", "rwamagana", "kirehe", "kibungo", "ngoma", "nyagatare", "gicumbi", "nyabihu", "kibuye", "karongi", "rusizi", "nyamasheke", "ruhango", "nyanza", "kamonyi", "kicukiro", "gasabo"] }, + { "slug": "saudi_arabia", "title": "Saudi Arabia", "isoCode": "sa", "keywords": ["saudi", "ksa", "riyadh", "mecca", "jeddah", "dammam"] }, + { "slug": "senegal", "title": "Senegal", "isoCode": "sn", "keywords": ["senegal", "dakar", "touba", "thies", "rufisque", "kaolack", "ziguinchor", "tambacounda", "kaffrine", "diourbel"] }, + { "slug": "serbia", "title": "Serbia", "isoCode": "rs", "keywords": ["serbia", "belgrade", "novi sad", "nis", "kragujevac", "subotica", "zrenjanin", "pancevo", "cacak", "novi pazar", "kraljevo", "smederevo"] }, + { "slug": "sierra_leone", "title": "Sierra Leone", "isoCode": "sl", "keywords": ["sierra leone", "freetown", "makeni", "koidu"] }, + { "slug": "singapore", "title": "Singapore", "isoCode": "sg", "keywords": ["singapore"] }, + { "slug": "slovakia", "title": "Slovakia", "isoCode": "sk", "keywords": ["slovakia", "bratislava", "kosice", "presov", "zilina"] }, + { "slug": "slovenia", "title": "Slovenia", "isoCode": "si", "keywords": ["slovenia", "slovenija", "ljubljana", "maribor", "celje", "kranj", "koper", "velenje", "novo mesto", "nova gorica", "krsko", "krško", "murska sobota", "postojna", "slovenj gradec"] }, + { "slug": "somalia", "title": "Somalia", "isoCode": "so", "keywords": ["somalia", "mogadishu", "hargeisa", "bosaso", "borama", "garowe", "kismayo"] }, + { "slug": "south_africa", "title": "South Africa", "isoCode": "za", "keywords": ["south africa", "johannesburg", "cape town", "rsa", "durban", "port elizabeth", "pretoria", "nelspruit", "knysna"] }, + { "slug": "south_sudan", "title": "South Sudan", "isoCode": "ss", "keywords": ["south sudan", "juba", "yei", "wau", "aweil", "jonglei", "maridi"] }, + { "slug": "spain", "title": "Spain", "isoCode": "es", "keywords": ["spain", "españa", "madrid", "barcelona", "valencia", "seville", "sevilla", "zaragoza", "malaga", "murcia", "palma", "bilbao", "alicante", "cordoba"] }, + { "slug": "sri_lanka", "title": "Sri Lanka", "isoCode": "lk", "keywords": ["sri lanka", "balangoda", "ratnapura", "colombo", "moratuwa", "negombo", "galle", "jaffna"] }, + { "slug": "sudan", "title": "Sudan", "isoCode": "sd", "keywords": ["sudan", "khartoum", "omdurman"] }, + { "slug": "suriname", "title": "Suriname", "isoCode": "sr", "keywords": ["suriname", "paramaribo"] }, + { "slug": "sweden", "title": "Sweden", "isoCode": "se", "keywords": ["sweden", "sverige", "stockholm", "malmö", "uppsala", "göteborg", "gothenburg"] }, + { "slug": "switzerland", "title": "Switzerland", "isoCode": "ch", "keywords": ["switzerland", "zurich", "zürich", "geneva", "basel", "lausanne", "bern", "winterthur", "lucerne", "gallen", "lugano", "biel", "thun"] }, + { "slug": "syria", "title": "Syria", "isoCode": "sy", "keywords": ["syria", "سوريا", "damascus", "hama", "aleppo", "homs", "rif dimashq", "tartus", "latakia", "idlib", "raqqa", "daraa", "alhasakah", "dierezzor", "quneitra", "alsuwayda"] }, + { "slug": "taiwan", "title": "Taiwan", "isoCode": "tw", "keywords": ["taiwan", "taichung", "kaohsiung", "taipei", "taoyuan", "tainan", "hsinchu", "keelung", "chiayi", "changhua"] }, + { "slug": "tajikistan", "title": "Tajikistan", "isoCode": "tj", "keywords": ["tajikistan", "dushanbe", "khujand", "kulob", "bokhtar", "khorugh", "panjakent", "isfara", "hisor", "ghissar", "rasht"] }, + { "slug": "tanzania", "title": "Tanzania", "isoCode": "tz", "keywords": ["tanzania", "dar es salaam", "mwanza", "arusha", "dodoma", "mbeya", "morogoro", "tanga", "kilimanjaro"] }, + { "slug": "thailand", "title": "Thailand", "isoCode": "th", "keywords": ["thailand", "bangkok", "nonthaburi", "nakhon", "phuket", "pattaya", "chiang mai"] }, + { "slug": "the_bahamas", "title": "The Bahamas", "isoCode": "bs", "keywords": ["bahamas"] }, + { "slug": "togo", "title": "Togo", "isoCode": "tg", "keywords": ["togo", "lome"] }, + { "slug": "tunisia", "title": "Tunisia", "isoCode": "tn", "keywords": ["tunisia", "tunis", "sfax", "sousse", "kairouan", "ariana", "gabes", "bizerte"] }, + { "slug": "turkey", "title": "Turkey", "isoCode": "tr", "keywords": ["turkey", "turkiye", "istanbul", "ankara", "izmir", "bursa", "adana", "gaziantep", "konya", "antalya", "kayseri", "mersin", "eskisehir", "samsun", "denizli", "malatya"] }, + { "slug": "turkmenistan", "title": "Turkmenistan", "isoCode": "tm", "keywords": ["turkmenistan", "turkmenabat"] }, + { "slug": "uganda", "title": "Uganda", "isoCode": "ug", "keywords": ["uganda", "kampala", "mbarara", "mukono", "jinja", "arua", "gulu", "masaka"] }, + { "slug": "ukraine", "title": "Ukraine", "isoCode": "ua", "keywords": ["ukraine", "kiev", "kyiv", "kharkiv", "dnipro", "odesa", "donetsk", "zaporizhia"] }, + { "slug": "uae", "title": "United Arab Emirates", "isoCode": "ae", "keywords": ["uae", "emirates", "dubai", "abu dhabi", "sharjah", "al ain", "ajman"] }, + { "slug": "uk", "title": "United Kingdom", "isoCode": "gb", "keywords": ["uk", "england", "scotland", "wales", "northern ireland", "london", "birmingham", "leeds", "glasgow", "sheffield", "bradford", "manchester", "edinburgh", "liverpool", "bristol", "cardiff", "belfast", "leicester", "wakefield", "coventry", "nottingham", "newcastle"] }, + { "slug": "united_states", "title": "United States", "isoCode": "us", "keywords": ["us", "usa", "united states", "alabama", "alaska", "ak", "arizona", "az", "arkansas", "ar", "california", "ca", "colorado", "co", "connecticut", "ct", "delaware", "de", "florida", "fl", "georgia", "ga", "hawaii", "hi", "idaho", "id", "illinois", "il", "indiana", "in", "iowa", "ia", "kansas", "ks", "kentucky", "ky", "louisiana", "la", "maine", "me", "maryland", "md", "massachusetts", "ma", "michigan", "mi", "minnesota", "mn", "mississippi", "ms", "missouri", "mo", "montana", "mt", "nebraska", "ne", "nevada", "nv", "new hampshire", "nh", "new jersey", "nj", "new mexico", "nm", "new york", "ny", "north carolina", "nc", "north dakota", "nd", "ohio", "oh", "oklahoma", "ok", "oregon", "or", "pennsylvania", "pa", "rhode island", "ri", "south carolina", "sc", "south dakota", "sd", "tennessee", "tn", "texas", "tx", "utah", "ut", "vermont", "vt", "virginia", "va", "washington", "wa", "west virginia", "wv", "wisconsin", "wi", "wyoming", "wy", "los angeles", "chicago", "houston", "phoenix", "philadelphia", "san antonio", "san diego", "dallas", "san jose", "austin", "jacksonville", "fort worth", "columbus", "charlotte", "san francisco", "indianapolis", "seattle", "denver", "boston", "el paso", "nashville", "detroit", "portland", "las vegas", "memphis", "louisville", "baltimore"] }, + { "slug": "uruguay", "title": "Uruguay", "isoCode": "uy", "keywords": ["uruguay", "montevideo"] }, + { "slug": "uzbekistan", "title": "Uzbekistan", "isoCode": "uz", "keywords": ["uzbekistan", "tashkent", "namangan", "samarkand", "andijan", "nukus", "bukhara", "qarshi", "fergana"] }, + { "slug": "venezuela", "title": "Venezuela", "isoCode": "ve", "keywords": ["venezuela", "caracas", "maracaibo", "barquisimeto", "guayana", "maturín", "zulia", "bolivar"] }, + { "slug": "vietnam", "title": "Vietnam", "isoCode": "vn", "keywords": ["vietnam", "viet nam", "ho chi minh", "hanoi", "ha noi", "hai phong", "da nang", "can tho", "bien hoa", "nha trang", "vinh"] }, + { "slug": "worldwide", "title": "Worldwide", "isoCode": "", "keywords": [] }, + { "slug": "yemen", "title": "Yemen", "isoCode": "ye", "keywords": ["yemen", "sana'a", "taiz", "aden", "mukalla", "ibb"] }, + { "slug": "zambia", "title": "Zambia", "isoCode": "zm", "keywords": ["zambia", "lusaka", "kitwe", "ndola"] }, + { "slug": "zimbabwe", "title": "Zimbabwe", "isoCode": "zw", "keywords": ["zimbabwe", "harare", "bulawayo", "mutare", "gweru", "kwekwe"] } +] \ No newline at end of file diff --git a/lib/country-flags.ts b/lib/country-flags.ts index 28d50d0..5fc8c3b 100644 --- a/lib/country-flags.ts +++ b/lib/country-flags.ts @@ -1,161 +1,25 @@ -// ─── Slug → ISO 3166-1 alpha-2 mapping ───────────────────────────────── -// Maps committers.top location slugs to ISO country codes for flag icons. -// Keep in sync with data/countries.json. +import countries from "@/data/countries.json"; -const SLUG_TO_ISO: Record = { - afghanistan: "af", - albania: "al", - algeria: "dz", - angola: "ao", - argentina: "ar", - armenia: "am", - australia: "au", - austria: "at", - azerbaijan: "az", - bahrain: "bh", - bangladesh: "bd", - belarus: "by", - belgium: "be", - benin: "bj", - bolivia: "bo", - bosnia_and_herzegovina: "ba", - botswana: "bw", - brazil: "br", - bulgaria: "bg", - burkina_faso: "bf", - burundi: "bi", - cambodia: "kh", - cameroon: "cm", - canada: "ca", - chad: "td", - chile: "cl", - china: "cn", - colombia: "co", - congo_brazzaville: "cg", - congo_kinshasa: "cd", - costa_rica: "cr", - croatia: "hr", - cuba: "cu", - cyprus: "cy", - czech_republic: "cz", - denmark: "dk", - dominican_republic: "do", - ecuador: "ec", - egypt: "eg", - el_salvador: "sv", - estonia: "ee", - ethiopia: "et", - finland: "fi", - france: "fr", - gabon: "ga", - georgia: "ge", - germany: "de", - ghana: "gh", - greece: "gr", - guatemala: "gt", - guinea: "gn", - haiti: "ht", - honduras: "hn", - hong_kong: "hk", - hungary: "hu", - india: "in", - indonesia: "id", - iran: "ir", - iraq: "iq", - ireland: "ie", - italy: "it", - ivory_coast: "ci", - japan: "jp", - jordan: "jo", - kazakhstan: "kz", - kenya: "ke", - kosovo: "xk", - kyrgyzstan: "kg", - laos: "la", - latvia: "lv", - lebanon: "lb", - libya: "ly", - lithuania: "lt", - luxembourg: "lu", - macau: "mo", - madagascar: "mg", - malawi: "mw", - malaysia: "my", - mali: "ml", - malta: "mt", - mauritania: "mr", - mauritius: "mu", - mexico: "mx", - moldova: "md", - morocco: "ma", - mozambique: "mz", - myanmar: "mm", - macedonia: "mk", - nepal: "np", - netherlands: "nl", - new_zealand: "nz", - nicaragua: "ni", - niger: "ne", - nigeria: "ng", - norway: "no", - oman: "om", - pakistan: "pk", - palestine: "ps", - panama: "pa", - papua_new_guinea: "pg", - paraguay: "py", - peru: "pe", - philippines: "ph", - poland: "pl", - portugal: "pt", - qatar: "qa", - south_korea: "kr", - romania: "ro", - russia: "ru", - rwanda: "rw", - saudi_arabia: "sa", - senegal: "sn", - serbia: "rs", - sierra_leone: "sl", - singapore: "sg", - slovakia: "sk", - slovenia: "si", - somalia: "so", - south_africa: "za", - south_sudan: "ss", - spain: "es", - sri_lanka: "lk", - sudan: "sd", - suriname: "sr", - sweden: "se", - switzerland: "ch", - syria: "sy", - taiwan: "tw", - tajikistan: "tj", - tanzania: "tz", - thailand: "th", - the_bahamas: "bs", - togo: "tg", - tunisia: "tn", - turkey: "tr", - turkmenistan: "tm", - uganda: "ug", - ukraine: "ua", - uae: "ae", - uk: "gb", - united_states: "us", - uruguay: "uy", - uzbekistan: "uz", - venezuela: "ve", - vietnam: "vn", - yemen: "ye", - zambia: "zm", - zimbabwe: "zw", +type CountryEntry = { + slug: string; + title: string; + isoCode: string; + keywords: string[]; }; /** - * Get the ISO 3166-1 alpha-2 code for a committers.top country slug. + * Build the slug → ISO 3166-1 alpha-2 mapping from data/countries.json. + */ +const SLUG_TO_ISO: Record = {}; +for (const entry of countries as CountryEntry[]) { + if (entry.isoCode) { + SLUG_TO_ISO[entry.slug] = entry.isoCode; + } +} + +/** + * Get the ISO 3166-1 alpha-2 code for a country slug. */ export function getCountryCode(slug: string): string | null { return SLUG_TO_ISO[slug] ?? null; -} +} \ No newline at end of file diff --git a/lib/db-store.ts b/lib/db-store.ts index 290ef5a..27bd6a2 100644 --- a/lib/db-store.ts +++ b/lib/db-store.ts @@ -1,4 +1,5 @@ import { Pool, PoolConfig } from "pg"; +import countries from "@/data/countries.json"; // ─── Types ───────────────────────────────────────────────────────────── @@ -112,9 +113,69 @@ export class DatabaseStore { CREATE INDEX IF NOT EXISTS idx_github_users_stale ON github_users(stale_after) WHERE country IS NOT NULL; + + CREATE TABLE IF NOT EXISTS leaderboard_calculation ( + country_slug VARCHAR(100) PRIMARY KEY, + country_title TEXT NOT NULL DEFAULT '', + last_calculated_at TIMESTAMPTZ, + status VARCHAR(20) DEFAULT 'pending', + error_message TEXT, + updated_at TIMESTAMPTZ DEFAULT NOW() + ); `); } + // ── Calculation tracking ────────────────────────────────────────────── + + async seedCalculationCountries(): Promise { + const client = getPool(); + const entries = countries as Array<{ slug: string; title: string; keywords: string[] }>; + const values = entries + .filter((c) => c.slug !== "worldwide") + .map((c) => `('${c.slug.replace(/'/g, "''")}', '${c.title.replace(/'/g, "''")}', 'pending')`) + .join(",\n"); + + await client.query(` + INSERT INTO leaderboard_calculation (country_slug, country_title, status) + VALUES ${values} + ON CONFLICT (country_slug) DO NOTHING; + `); + } + + async getNextCountryToCalculate(): Promise<{ slug: string; title: string } | null> { + const client = getPool(); + const result = await client.query( + `SELECT country_slug, country_title FROM leaderboard_calculation + WHERE status != 'running' + ORDER BY last_calculated_at ASC NULLS FIRST + LIMIT 1`, + ); + if (result.rows.length === 0) return null; + return { slug: result.rows[0].country_slug, title: result.rows[0].country_title }; + } + + async startCalculation(countrySlug: string): Promise { + const client = getPool(); + await client.query( + `UPDATE leaderboard_calculation SET status = 'running', updated_at = NOW() WHERE country_slug = $1`, + [countrySlug], + ); + } + + async finishCalculation(countrySlug: string, errorMessage?: string): Promise { + const client = getPool(); + const status = errorMessage ? 'failed' : 'done'; + await client.query( + `UPDATE leaderboard_calculation SET + status = $1, + last_calculated_at = NOW(), + error_message = $2, + updated_at = NOW() + WHERE country_slug = $3`, + [status, errorMessage ?? null, countrySlug], + ); + } + // ── User operations ───────────────────────────────────────────────── async upsertUser(params: UpsertUserParams): Promise { diff --git a/lib/location-detector.ts b/lib/location-detector.ts index 59691f7..ce60a69 100644 --- a/lib/location-detector.ts +++ b/lib/location-detector.ts @@ -5,62 +5,24 @@ import countries from "@/data/countries.json"; type CountryEntry = { slug: string; title: string; + isoCode: string; + keywords: string[]; }; -// ─── Keyword mapping ──────────────────────────────────────────────────── -// -// GitHub's "location" field is free text. Users might write: -// - "Riyadh, Saudi Arabia" -// - "Saudi Arabia" -// - "Jeddah" -// - "KSA" -// - "🇸🇦" -// - "Earth" (unmatchable) -// - "" or null (absent) -// -// This mapping converts those variations into country slugs -// that match the slugs in data/countries.json. -// -// Keywords are pulled from committers.top presets via data/countries.json. - -type CountryEntry2 = { - slug: string; - title: string; - keywords?: string[]; -}; +// ─── Build keyword mapping from data/countries.json ──────────────────── type CountryMapping = { slug: string; keywords: string[]; }; -// Build COUNTRY_MAPPINGS from countries.json with keywords -const COUNTRY_MAPPINGS: CountryMapping[] = ( - countries as CountryEntry2[] -) - .filter((country) => country.keywords && country.keywords.length > 0) - .map((country) => ({ - slug: country.slug, - keywords: country.keywords || [], +const COUNTRY_MAPPINGS: CountryMapping[] = (countries as CountryEntry[]) + .filter((c) => c.keywords.length > 0) + .map((c) => ({ + slug: c.slug, + keywords: c.keywords, })); -// ─── Validate all slugs exist in countries.json ─────────────────────── - -const VALID_SLUGS = new Set( - (countries as CountryEntry[]).map((c) => c.slug), -); - -// Validate on import in development -if (process.env.NODE_ENV === "development") { - for (const mapping of COUNTRY_MAPPINGS) { - if (!VALID_SLUGS.has(mapping.slug)) { - console.warn( - `[location-detector] Warning: slug "${mapping.slug}" not found in data/countries.json`, - ); - } - } -} - // ─── Helpers ─────────────────────────────────────────────────────────── /** @@ -99,4 +61,4 @@ export function detectCountry(location: string | null): string | null { } return null; -} +} \ No newline at end of file diff --git a/locales/ar.json b/locales/ar.json index f70b7f6..7312fda 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -160,6 +160,7 @@ "methodology.sections.signals.title": "إشارات الشفافية", "methodology.sections.signals.purpose": "الإشارات توضّح ما الذي تم تحليله وما الذي تم تجاهله، مثل PR على مستودعات المستخدم وPR غير المدمجة.", "methodology.sections.signals.examples": "أمثلة: عدد PR الخارجية المدمجة، عدد المستودعات الخارجية الفريدة، وتغطية تطابق اللغة.", + "nav.leaderboard": "لوحة المتصدرين", "language.focus": "تركيز اللغة", "language.match": "تطابق اللغة", "language.optionalNote": "النتيجة اللغوية اختيارية ولا تستبدل النتيجة العامة.", @@ -217,14 +218,18 @@ "a11y.openPullRequest": "افتح طلب السحب {title} على GitHub", "a11y.openRepo": "افتح المستودع {name} على GitHub", "leaderboard.header.eyebrow": "لوحة المتصدرين", - "leaderboard.header.title": "إعادة ترتيب المساهمين حسب الأثر", + "leaderboard.header.title": "ترتيب المطورين حسب الأثر", "leaderboard.header.description": "اختر دولة لعرض المطورين مرتبين حسب أثر المصدر المفتوح الفعلي", + "leaderboard.page.eyebrow": "اكتشاف المطورين عالميًا", + "leaderboard.page.title": "استكشف قائمة المتصدرين حسب الدولة", + "leaderboard.page.description": "اكتشف مطوري المصدر المفتوح حسب الدولة وقارن الأثر من خلال جودة المستودعات وطلبات السحب المدمجة وإشارات المساهمة المجتمعية.", + "leaderboard.page.count": "تتوفر {count} قائمة متصدرين حسب الدولة للتصفح.", + "leaderboard.country.title": "قائمة المتصدرين - {title}", + "leaderboard.country.description": "استكشف المطورين في {title} مرتبين حسب جودة المستودعات وأثر طلبات السحب المدمجة والمساهمات في مجتمع المصدر المفتوح.", "leaderboard.title": "لوحة المتصدرين حسب الأثر", - "leaderboard.description": "تم تحليل {processed} مطور من أصل {total} مسجل على committers.top", + "leaderboard.description": "أفضل {processed} مطورين من حيث الأثر في عالم المصدر المفتوح.", "leaderboard.impactRank": "ترتيب الأثر", "leaderboard.developer": "المطور", - "leaderboard.committersRank": "ترتيب المساهمين", - "leaderboard.change": "التغير", "leaderboard.topImpact": "المطور الأعلى أثرًا", "leaderboard.impactScore": "درجة الأثر", "leaderboard.error.title": "فشل تحميل لوحة المتصدرين", @@ -239,9 +244,9 @@ "leaderboard.searchCountry": "البحث عن دول...", "leaderboard.loadingCountries": "جاري تحميل الدول المتاحة...", "leaderboard.empty.title": "لم يتم العثور على دول", - "leaderboard.empty.description": "ستظهر الدول من committers.top هنا.", + "leaderboard.empty.description": "ستظهر الدول المتاحة هنا.", "leaderboard.noCountriesSearch": "لا توجد دول تطابق بحثك.", - "leaderboard.noDevelopersFor": "لم يتم العثور على مطورين في {title}", + "leaderboard.noDevelopersFor": "لم يتم حساب قائمة المتصدرين في هذه الدولة بعد.", "leaderboard.error.retry": "حاول مرة أخرى", "leaderboard.viewCountry": "عرض لوحة المتصدرين" } diff --git a/locales/en.json b/locales/en.json index 067be4b..7edc97b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -160,6 +160,7 @@ "methodology.sections.signals.title": "Transparency signals", "methodology.sections.signals.purpose": "Signals report what was analyzed and what was ignored, such as own-repo PRs and unmerged PRs.", "methodology.sections.signals.examples": "Examples include merged external PR count, unique external repositories, and language match coverage.", + "nav.leaderboard": "Leaderboard", "language.focus": "Language Focus", "language.match": "Language match", "language.optionalNote": "Language-focused score is optional and does not replace the overall score.", @@ -217,14 +218,18 @@ "a11y.openPullRequest": "Open pull request {title} on GitHub", "a11y.openRepo": "Open repository {name} on GitHub", "leaderboard.header.eyebrow": "Country Leaderboard", - "leaderboard.header.title": "Reorder Committers by Impact", + "leaderboard.header.title": "Rank Developers by Impact", "leaderboard.header.description": "Select a country to view developers ranked by real open-source impact", + "leaderboard.page.eyebrow": "Global developer discovery", + "leaderboard.page.title": "Explore country-based GitHub developer leaderboards", + "leaderboard.page.description": "Discover open-source developers by country and compare impact through repository quality, merged pull requests, and community contribution signals.", + "leaderboard.page.count": "{count} country leaderboards are available to browse.", + "leaderboard.country.title": "{title} GitHub Developer Leaderboard", + "leaderboard.country.description": "Explore developers in {title} ranked by repository quality, merged pull request impact, and open-source community contributions.", "leaderboard.title": "Impact Leaderboard", - "leaderboard.description": "{processed} developers analyzed out of {total} total on committers.top", + "leaderboard.description": "The top {processed} developers with the strongest open-source impact.", "leaderboard.impactRank": "Impact Rank", "leaderboard.developer": "Developer", - "leaderboard.committersRank": "Committers Rank", - "leaderboard.change": "Change", "leaderboard.topImpact": "Highest Impact Developer", "leaderboard.impactScore": "Impact Score", "leaderboard.error.title": "Leaderboard load failed", @@ -239,9 +244,9 @@ "leaderboard.searchCountry": "Search countries...", "leaderboard.loadingCountries": "Loading available countries...", "leaderboard.empty.title": "No countries found", - "leaderboard.empty.description": "Countries from committers.top will appear here.", + "leaderboard.empty.description": "Available countries will appear here.", "leaderboard.noCountriesSearch": "No countries match your search.", - "leaderboard.noDevelopersFor": "No developers found for {title}", + "leaderboard.noDevelopersFor": "No leaderboard has been calculated for this country yet.", "leaderboard.error.retry": "Try again", "leaderboard.viewCountry": "View leaderboard" } From cfd921f902e82eefb9d7e28f7b54be1386be6c37 Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Tue, 21 Jul 2026 05:39:53 +0300 Subject: [PATCH 06/10] feat: add standalone script for calculating next country's leaderboard - Implemented `calculate-next-country.ts` to automate leaderboard calculations. - The script connects to the PostgreSQL database and processes the next country in line. - Added logging for better tracking of the calculation process and results. refactor: update GitHub data fetching in tests - Renamed `fetchGitHubUserData` to `getUserData` for clarity. - Adjusted tests to use the new `getUserData` mock function. - Enhanced error handling in tests for GitHub API rate limits and resource limits. chore: remove unused ContributionTotals type - Deleted `ContributionTotals` type from GitHub types and related fixtures. - Updated user score input to remove contributions from its structure. --- .env.example | 7 + README.md | 6 + app/api/calculate-leaderboard/route.ts | 257 +- app/api/compare/route.ts | 48 +- app/api/leaderboard/route.ts | 118 +- docker-compose.yml | 61 +- lib/calculate-leaderboard.ts | 336 +++ lib/github.ts | 358 ++- lib/leaderboard.ts | 117 + lib/logger.ts | 29 + lib/score.ts | 4 - next-env.d.ts | 2 +- package.json | 6 + pnpm-lock.yaml | 3765 +++++++++++++----------- scripts/calculate-next-country.ts | 93 + test/api/compare.route.test.ts | 91 +- test/fixtures/github.ts | 17 +- types/github.ts | 7 +- 18 files changed, 3094 insertions(+), 2228 deletions(-) create mode 100644 lib/calculate-leaderboard.ts create mode 100644 lib/leaderboard.ts create mode 100644 lib/logger.ts create mode 100644 scripts/calculate-next-country.ts diff --git a/.env.example b/.env.example index 35b9e6c..d4343c2 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,9 @@ GITHUB_PR_COUNT=80 GITHUB_ISSUE_COUNT=20 GITHUB_DISCUSSION_COUNT=10 +# Leaderboard source data +LEADERBOARD_SOURCE_URL_TEMPLATE=https://raw.githubusercontent.com/ashkulz/committers.top/gh-pages/_data/locations/{country}.yml + # Redis caching (optional — strongly recommended for leaderboard performance) # Use either redis://localhost:6379 or include password if enabled: redis://:password@localhost:6379 REDIS_URL= @@ -18,3 +21,7 @@ REDIS_PASSWORD= REDIS_CACHE_NAMESPACE=devimpact:v1 REDIS_CACHE_TTL_SECONDS=604800 REDIS_CONNECT_TIMEOUT_MS=1500 + +# ── PostgreSQL (local development) ───────────────── +DATABASE_URL=postgresql://devimpact:devimpact@localhost:5432/devimpact?sslmode=disable +POSTGRES_PASSWORD=CHANGE_THIS_TO_A_LONG_RANDOM_PASSWORD \ No newline at end of file diff --git a/README.md b/README.md index fec9780..ecb0fe2 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,12 @@ GITHUB_DISCUSSION_COUNT=10 pnpm run dev ``` +To calculate the next leaderboard country from the script, run: + +```bash +pnpm run calculate-next-country +``` + --- diff --git a/app/api/calculate-leaderboard/route.ts b/app/api/calculate-leaderboard/route.ts index 80c8f2a..02ae782 100644 --- a/app/api/calculate-leaderboard/route.ts +++ b/app/api/calculate-leaderboard/route.ts @@ -1,76 +1,8 @@ import { NextResponse } from "next/server"; -import yaml from "js-yaml"; -import { fetchGitHubUserData } from "@/lib/github"; -import { calculateUserScore } from "@/lib/score"; -import { createCacheStore, getCacheConfigFromEnv } from "@/lib/cache-store"; -import { getDatabaseStore } from "@/lib/db-store"; -import { detectCountry } from "@/lib/location-detector"; +import { calculateLeaderboard } from "@/lib/calculate-leaderboard"; export const runtime = "nodejs"; -// ─── Types ───────────────────────────────────────────────────────────── - -type CommitterEntry = { - rank: number; - name: string; - login: string; - avatarUrl: string; - contributions: number; -}; - -type CommitterYaml = { - title?: string; - total_user_count?: number; - users?: CommitterEntry[]; -}; - -type ScoredEntry = { - username: string; - name: string | null; - avatarUrl: string; - repoScore: number; - prScore: number; - contributionScore: number; - finalScore: number; - originalRank: number; - originalContributions: number; - impactRank: number; -}; - -type LeaderboardResult = { - title: string; - totalFromSource: number; - scored: ScoredEntry[]; - errors: string[]; -}; - -// ─── Helpers ──────────────────────────────────────────────────────────── - -function buildLeaderboardCacheKey(country: string, namespace: string): string { - return `${namespace}:leaderboard:${country.trim().toLowerCase()}`; -} - -function getEnvInt(key: string, fallback: number): number { - const raw = process.env[key]?.trim(); - if (!raw) return fallback; - const parsed = Number.parseInt(raw, 10); - return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; -} - -function getStaleDays(): number { - return getEnvInt("LEADERBOARD_USER_STALE_DAYS", 30); -} - -function getSeedLimit(): number { - return getEnvInt("LEADERBOARD_SEED_LIMIT", 256); -} - -function getRefreshLimit(): number { - return getEnvInt("LEADERBOARD_REFRESH_LIMIT", 500); -} - -// ─── POST handler ────────────────────────────────────────────────────── - export async function POST(request: Request) { const { searchParams } = new URL(request.url); const country = searchParams.get("country")?.trim(); @@ -82,197 +14,16 @@ export async function POST(request: Request) { ); } - // Ensure database schema exists - const db = getDatabaseStore(); - await db.initializeSchema(); - - // ── 1. Fetch committers from committers.top ────────────────────────── - let committersData: { - title: string; - totalFromSource: number; - users: CommitterEntry[]; - }; try { - const url = `https://raw.githubusercontent.com/ashkulz/committers.top/gh-pages/_data/locations/${country}.yml`; - const res = await fetch(url, { - headers: { "User-Agent": "DevImpact-Bot" }, - }); - if (!res.ok) { - return NextResponse.json( - { - success: false, - error: `Failed to fetch committers data for "${country}"`, - }, - { status: 502 }, - ); - } - const text = await res.text(); - const data = yaml.load(text) as CommitterYaml; - if (!data?.users) { - return NextResponse.json( - { - success: false, - error: `Invalid or empty committers data for "${country}"`, - }, - { status: 502 }, - ); - } - committersData = { - title: data.title || country, - totalFromSource: data.total_user_count ?? data.users.length, - users: data.users, - }; + const result = await calculateLeaderboard(country); + return NextResponse.json({ success: true, ...result }); } catch (err) { return NextResponse.json( { success: false, - error: - err instanceof Error - ? err.message - : "Failed to fetch committers data", + error: err instanceof Error ? err.message : "Failed to calculate leaderboard", }, { status: 502 }, ); } - - // ── 2a. Seed NEW users from committers.top ─────────────────────────── - const errors: string[] = []; - const seedLimit = getSeedLimit(); - const usersToSeed = committersData.users.slice(0, seedLimit); - let newUsersCount = 0; - let skippedExistingCount = 0; - - for (const user of usersToSeed) { - try { - const exists = await db.userExists(user.login); - if (exists) { - skippedExistingCount += 1; - continue; - } - - const data = await fetchGitHubUserData(user.login); - const score = calculateUserScore(data, user.login); - const countryDetected = detectCountry(data.location); - - await db.upsertUser({ - username: data.login, - name: data.name, - avatarUrl: data.avatarUrl, - location: data.location, - country: countryDetected, - rawData: data, - scores: score, - repoScore: Math.round(score.repoScore), - prScore: Math.round(score.prScore), - contributionScore: Math.round(score.contributionScore), - finalScore: Math.round(score.finalScore), - staleDays: getStaleDays(), - }); - - newUsersCount += 1; - } catch { - errors.push(user.login); - } - } - - // ── 2b. Refresh STALE users from DB top N ─────────────────────────── - const refreshLimit = getRefreshLimit(); - const topUsers = await db.getTopUsers(country, refreshLimit); - let refreshedCount = 0; - - for (const row of topUsers) { - // Check if stale - if (row.stale_after >= new Date()) { - continue; // still fresh - } - - try { - const data = await fetchGitHubUserData(row.username); - const score = calculateUserScore(data, row.username); - const countryDetected = detectCountry(data.location); - - await db.upsertUser({ - username: data.login, - name: data.name, - avatarUrl: data.avatarUrl, - location: data.location, - country: countryDetected, - rawData: data, - scores: score, - repoScore: Math.round(score.repoScore), - prScore: Math.round(score.prScore), - contributionScore: Math.round(score.contributionScore), - finalScore: Math.round(score.finalScore), - staleDays: getStaleDays(), - }); - - refreshedCount += 1; - } catch { - errors.push(row.username); - } - } - - // ── 3. Build & cache the leaderboard result ────────────────────────── - const allUsers = await db.getLeaderboard(country, getEnvInt("LEADERBOARD_DISPLAY_LIMIT", 500)); - const totalCount = await db.getLeaderboardCount(country); - - // Build rank lookup from original committers data - const rankMap = new Map( - committersData.users.map((u) => [ - u.login, - { rank: u.rank, contributions: u.contributions }, - ]), - ); - - const scored: ScoredEntry[] = allUsers.map((row) => { - const original = rankMap.get(row.username) ?? { rank: 0, contributions: 0 }; - return { - username: row.username, - name: row.name, - avatarUrl: row.avatar_url, - repoScore: row.repo_score, - prScore: row.pr_score, - contributionScore: row.contribution_score, - finalScore: row.final_score, - originalRank: original.rank, - originalContributions: original.contributions, - impactRank: 0, - }; - }); - - // Sort and assign impactRank - scored.sort((a, b) => b.finalScore - a.finalScore); - scored.forEach((u, idx) => (u.impactRank = idx + 1)); - - const result: LeaderboardResult = { - title: committersData.title, - totalFromSource: totalCount, - scored, - errors: [...new Set(errors)], // deduplicate - }; - - // ── 4. Invalidate Redis cache (lazy rebuild on next GET) ───────────── - try { - const cacheConfig = getCacheConfigFromEnv(); - const cacheStore = createCacheStore(cacheConfig); - if (cacheStore.enabled && cacheStore.del) { - const cacheKey = buildLeaderboardCacheKey(country, cacheConfig.namespace); - await cacheStore.del(cacheKey); - } - } catch { - // Non-fatal: cache invalidation failure should not block the response - console.warn("Failed to invalidate leaderboard cache"); - } - - return NextResponse.json({ - success: true, - ...result, - _meta: { - newUsers: newUsersCount, - refreshedUsers: refreshedCount, - skippedExisting: skippedExistingCount, - errors: errors.length, - totalInDb: totalCount, - }, - }); } \ No newline at end of file diff --git a/app/api/compare/route.ts b/app/api/compare/route.ts index e4cb70b..b4bdb6f 100644 --- a/app/api/compare/route.ts +++ b/app/api/compare/route.ts @@ -1,13 +1,10 @@ import { NextResponse } from "next/server"; -import { fetchGitHubUserData } from "../../../lib/github"; +import { getUserData } from "../../../lib/github"; import { calculateUserScore } from "../../../lib/score"; import { normalizeSelectedLanguages } from "@/lib/scoring/languageScoring"; import { toSafeApiError } from "@/lib/github-graphql-client"; import { getDatabaseStore } from "@/lib/db-store"; -import { - createCacheStore, - getCacheConfigFromEnv, -} from "@/lib/cache-store"; +import { createCacheStore, getCacheConfigFromEnv } from "@/lib/cache-store"; import { detectCountry } from "@/lib/location-detector"; import type { CompareInsights, SafeApiError } from "@/types/api-response"; import { @@ -17,6 +14,7 @@ import { parseAcceptLanguage, type Locale, } from "@/lib/i18n-core"; +import { GitHubUserData } from "@/types/github"; export const runtime = "nodejs"; @@ -54,7 +52,10 @@ type ComparedUserResult = { explanations: ReturnType["explanations"]; }; -type ClientSafeError = Pick; +type ClientSafeError = Pick< + SafeApiError, + "code" | "message" | "targetUsernames" +>; function parseSelectedLanguagesFromSearchParams( searchParams: URLSearchParams, @@ -66,7 +67,10 @@ function parseSelectedLanguagesFromSearchParams( .map((language) => language.trim()) .filter(Boolean); - return normalizeSelectedLanguages([...(fromRepeated ?? []), ...(fromCsv ?? [])]); + return normalizeSelectedLanguages([ + ...(fromRepeated ?? []), + ...(fromCsv ?? []), + ]); } function calculateWinner(users: ComparedUserResult[]): { @@ -88,7 +92,8 @@ function calculateWinner(users: ComparedUserResult[]): { const [userA, userB] = users; const overallWinner = userA.finalScore >= userB.finalScore ? userA : userB; - const overallLoser = overallWinner.username === userA.username ? userB : userA; + const overallLoser = + overallWinner.username === userA.username ? userB : userA; const overallDifference = Math.abs(userA.finalScore - userB.finalScore); const overallPercentage = overallLoser.finalScore > 0 @@ -120,7 +125,8 @@ function calculateWinner(users: ComparedUserResult[]): { userA.languageScores.finalScore >= userB.languageScores.finalScore ? userA : userB; - const languageLoser = languageWinner.username === userA.username ? userB : userA; + const languageLoser = + languageWinner.username === userA.username ? userB : userA; const winnerLanguageScores = languageWinner.languageScores!; const loserLanguageScores = languageLoser.languageScores!; const languageDifference = Math.abs( @@ -312,9 +318,14 @@ async function compareUsers( const results: ComparedUserResult[] = []; for (const username of usernames) { - let data: Awaited>; + let data: Awaited; try { - data = await fetchGitHubUserData(username); + const { data: userData, metrics } = await getUserData(username, { + cacheInRedis: true, + withMetrics: true, + }); + data = userData; + console.log(metrics); } catch (error: unknown) { throw new CompareUserFetchError(username, error); } @@ -337,7 +348,9 @@ async function compareUsers( finalScore: Math.round(score.finalScore), normalizedRepoScore: Math.round(score.normalizedRepoScore), normalizedPRScore: Math.round(score.normalizedPRScore), - normalizedContributionScore: Math.round(score.normalizedContributionScore), + normalizedContributionScore: Math.round( + score.normalizedContributionScore, + ), normalizedFinalScore: Math.round(score.normalizedFinalScore), topRepos: score.topRepos, topPullRequests: score.topPullRequests, @@ -388,13 +401,17 @@ async function compareUsers( return results; } -function toApiErrorStatus(code: ReturnType["code"]): number { +function toApiErrorStatus( + code: ReturnType["code"], +): number { switch (code) { case "RATE_LIMITED": case "TEMPORARY_THROTTLE": return 429; + case "GITHUB_TIMEOUT": + case "GITHUB_RESOURCE_LIMIT": case "GITHUB_AUTH": - return 401; + return code === "GITHUB_AUTH" ? 401 : 503; case "GITHUB_NOT_FOUND": return 404; case "NETWORK": @@ -428,7 +445,8 @@ export async function GET(request: Request) { try { const locale = resolveLocale(request); - const selectedLanguages = parseSelectedLanguagesFromSearchParams(searchParams); + const selectedLanguages = + parseSelectedLanguagesFromSearchParams(searchParams); const users = await compareUsers(usernames, selectedLanguages); const winnerData = calculateWinner(users); const insights = createComparisonInsights(users, locale); diff --git a/app/api/leaderboard/route.ts b/app/api/leaderboard/route.ts index dade668..cd1e634 100644 --- a/app/api/leaderboard/route.ts +++ b/app/api/leaderboard/route.ts @@ -1,49 +1,8 @@ import { NextResponse } from "next/server"; -import { - createCacheStore, - getCacheConfigFromEnv, -} from "@/lib/cache-store"; -import { getDatabaseStore } from "@/lib/db-store"; +import { getLeaderboardResult } from "@/lib/leaderboard"; export const runtime = "nodejs"; -// ─── Types ───────────────────────────────────────────────────────────── - -type ScoredEntry = { - username: string; - name: string | null; - avatarUrl: string; - repoScore: number; - prScore: number; - contributionScore: number; - finalScore: number; - originalRank: number; - originalContributions: number; - impactRank: number; -}; - -type LeaderboardResult = { - title: string; - totalFromSource: number; - scored: ScoredEntry[]; - errors: string[]; -}; - -// ─── Helpers ──────────────────────────────────────────────────────────── - -function buildLeaderboardCacheKey(country: string, namespace: string): string { - return `${namespace}:leaderboard:${country.trim().toLowerCase()}`; -} - -function getDisplayLimit(): number { - const raw = process.env.LEADERBOARD_DISPLAY_LIMIT?.trim(); - if (!raw) return 500; - const parsed = Number.parseInt(raw, 10); - return Number.isFinite(parsed) && parsed > 0 ? parsed : 500; -} - -// ─── GET handler ─────────────────────────────────────────────────────── - export async function GET(request: Request) { const { searchParams } = new URL(request.url); const country = searchParams.get("country")?.trim(); @@ -55,83 +14,12 @@ export async function GET(request: Request) { ); } - // ── 1. Try Redis cache first ───────────────────────────────────────── - const cacheConfig = getCacheConfigFromEnv(); - const cacheStore = createCacheStore(cacheConfig); - - if (cacheStore.enabled) { - try { - const cacheKey = buildLeaderboardCacheKey(country, cacheConfig.namespace); - const cached = await cacheStore.get(cacheKey); - if (cached) { - return NextResponse.json({ success: true, ...cached }); - } - } catch { - console.warn("Failed to read leaderboard from cache"); - } - } - - // ── 2. Cache miss — query PostgreSQL ───────────────────────────────── try { - const db = getDatabaseStore(); - const displayLimit = getDisplayLimit(); - - // Ensure schema exists (idempotent) - await db.initializeSchema(); - - const rows = await db.getLeaderboard(country, displayLimit); - const totalCount = await db.getLeaderboardCount(country); - - if (rows.length === 0) { - // No data for this country at all — return empty - return NextResponse.json({ - success: true, - title: country, - totalFromSource: 0, - scored: [], - errors: [], - }); - } - - const scored: ScoredEntry[] = rows.map((row) => ({ - username: row.username, - name: row.name, - avatarUrl: row.avatar_url, - repoScore: row.repo_score, - prScore: row.pr_score, - contributionScore: row.contribution_score, - finalScore: row.final_score, - originalRank: 0, - originalContributions: 0, - impactRank: 0, - })); - - // Sort and assign impactRank - scored.sort((a, b) => b.finalScore - a.finalScore); - scored.forEach((u, idx) => (u.impactRank = idx + 1)); - - const result: LeaderboardResult = { - title: country, - totalFromSource: totalCount, - scored, - errors: [], - }; - - // ── 3. Warm the Redis cache ────────────────────────────────────────── - if (cacheStore.enabled) { - try { - const cacheKey = buildLeaderboardCacheKey(country, cacheConfig.namespace); - await cacheStore.set(cacheKey, result, cacheConfig.ttlSeconds); - } catch { - // Non-fatal: cache write failure should not block the response - } - } - + const result = await getLeaderboardResult(country); return NextResponse.json({ success: true, ...result }); } catch (err) { console.error("Leaderboard DB query failed:", err); - // Fallback: return empty data on error return NextResponse.json({ success: true, title: country, @@ -140,4 +28,4 @@ export async function GET(request: Request) { errors: [], }); } -} \ No newline at end of file +} diff --git a/docker-compose.yml b/docker-compose.yml index 809ebdf..45588fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,44 +3,75 @@ services: image: postgres:16-alpine container_name: devimpact-postgres restart: unless-stopped + environment: POSTGRES_DB: devimpact POSTGRES_USER: devimpact POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-devimpact} - # Required for local dev: trust auth allows non-SSL connections from Docker bridge - POSTGRES_HOST_AUTH_METHOD: trust + ports: - "5432:5432" + volumes: - - pgdata:/var/lib/postgresql/data + - postgres-data:/var/lib/postgresql/data + healthcheck: - test: ["CMD-SHELL", "pg_isready -U devimpact"] + test: ["CMD-SHELL", "pg_isready -U devimpact -d devimpact"] interval: 10s timeout: 5s retries: 5 + start_period: 30s + + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" redis: image: redis:7-alpine container_name: devimpact-redis restart: unless-stopped + + command: + [ + "redis-server", + "--appendonly", + "yes", + "--requirepass", + "${REDIS_PASSWORD}", + "--maxmemory", + "512mb", + "--maxmemory-policy", + "allkeys-lru" + ] + ports: - "6379:6379" - environment: - REDIS_PASSWORD: ${REDIS_PASSWORD:-} - command: > - sh -c "if [ -n \"$${REDIS_PASSWORD}\" ]; then - redis-server --appendonly yes --requirepass \"$${REDIS_PASSWORD}\"; - else - redis-server --appendonly yes; - fi" + volumes: - redis-data:/data + healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: + [ + "CMD", + "redis-cli", + "-a", + "${REDIS_PASSWORD}", + "ping" + ] interval: 10s timeout: 3s retries: 5 + start_period: 10s + + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" volumes: - redis-data: - pgdata: \ No newline at end of file + postgres-data: + redis-data: \ No newline at end of file diff --git a/lib/calculate-leaderboard.ts b/lib/calculate-leaderboard.ts new file mode 100644 index 0000000..989a7ae --- /dev/null +++ b/lib/calculate-leaderboard.ts @@ -0,0 +1,336 @@ +import yaml from "js-yaml"; +import { createGitHubUserDataFetcherWithMetrics, getUserData } from "@/lib/github"; +import { calculateUserScore } from "@/lib/score"; +import { createCacheStore, getCacheConfigFromEnv } from "@/lib/cache-store"; +import { getDatabaseStore, type DatabaseStore } from "@/lib/db-store"; +import { detectCountry } from "@/lib/location-detector"; + +// ─── Types ───────────────────────────────────────────────────────────── + +type LeaderboardSourceEntry = { + rank: number; + name: string; + login: string; + avatarUrl: string; + contributions: number; +}; + +type LeaderboardSourceYaml = { + title?: string; + total_user_count?: number; + users?: LeaderboardSourceEntry[]; +}; + +export type ScoredEntry = { + username: string; + name: string | null; + avatarUrl: string; + repoScore: number; + prScore: number; + contributionScore: number; + finalScore: number; + impactRank: number; +}; + +export type LeaderboardResult = { + title: string; + totalFromSource: number; + scored: ScoredEntry[]; + errors: string[]; +}; + +export type LeaderboardMeta = { + newUsers: number; + refreshedUsers: number; + skippedExisting: number; + errors: number; + totalInDb: number; + failedUsernames?: string[]; + totalFetchTime?: number; + successfulFetches?: number; + userFetchErrors?: { username: string; errors: { part: string; reason: string }[] }[]; +}; + +export type CalculateLeaderboardResponse = LeaderboardResult & { + _meta: LeaderboardMeta; +}; + +// ─── Helpers ──────────────────────────────────────────────────────────── + +function buildLeaderboardCacheKey(country: string, namespace: string): string { + return `${namespace}:leaderboard:${country.trim().toLowerCase()}`; +} + +function getEnvInt(key: string, fallback: number): number { + const raw = process.env[key]?.trim(); + if (!raw) return fallback; + const parsed = Number.parseInt(raw, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +} + +function getStaleDays(): number { + return getEnvInt("LEADERBOARD_USER_STALE_DAYS", 30); +} + +function getSeedLimit(): number { + return getEnvInt("LEADERBOARD_SEED_LIMIT", 256); +} + +function getRefreshLimit(): number { + return getEnvInt("LEADERBOARD_REFRESH_LIMIT", 500); +} + +function getSourceUrl(country: string): string { + const template = process.env.LEADERBOARD_SOURCE_URL_TEMPLATE?.trim(); + if (!template) { + throw new Error("Missing LEADERBOARD_SOURCE_URL_TEMPLATE environment variable"); + } + + return template.replace("{country}", encodeURIComponent(country)); +} + +// ─── Core logic ───────────────────────────────────────────────────────── + +export async function fetchCommittersFromTop( + country: string, +): Promise<{ title: string; totalFromSource: number; users: LeaderboardSourceEntry[] }> { + const url = getSourceUrl(country); + const res = await fetch(url, { + headers: { "User-Agent": "DevImpact-Bot" }, + }); + if (!res.ok) { + throw new Error(`Leaderboard source returned ${res.status} for "${country}"`); + } + const text = await res.text(); + const data = yaml.load(text) as LeaderboardSourceYaml; + if (!data?.users) { + throw new Error(`Invalid or empty leaderboard source data for "${country}"`); + } + return { + title: data.title || country, + totalFromSource: data.total_user_count ?? data.users.length, + users: data.users, + }; +} + +export async function seedNewUsers( + db: DatabaseStore, + users: LeaderboardSourceEntry[], + seedLimit: number, + staleDays: number, +): Promise<{ + newUsersCount: number; + skippedExistingCount: number; + errors: { username: string; reason: string }[]; + fetchMetrics: { duration: number; errors: { part: string; reason: string }[] }[]; +}> { + const errors: { username: string; reason: string }[] = []; + let newUsersCount = 0; + let skippedExistingCount = 0; + const fetchMetrics: { duration: number; errors: { part: string; reason: string }[] }[] = []; + + const usersToSeed = users.slice(0, seedLimit); + + for (const user of usersToSeed) { + try { + const exists = await db.userExists(user.login); + if (exists) { + skippedExistingCount += 1; + continue; + } + + const { data, metrics } = await getUserData(user.login, { cacheInRedis: false, withMetrics: true }); + fetchMetrics.push(metrics); + const score = calculateUserScore(data, user.login); + const countryDetected = detectCountry(data.location); + + await db.upsertUser({ + username: data.login, + name: data.name, + avatarUrl: data.avatarUrl, + location: data.location, + country: countryDetected, + rawData: data, + scores: score, + repoScore: Math.round(score.repoScore), + prScore: Math.round(score.prScore), + contributionScore: Math.round(score.contributionScore), + finalScore: Math.round(score.finalScore), + staleDays, + }); + + newUsersCount += 1; + } catch (e) { + errors.push({ username: user.login, reason: e instanceof Error ? e.message : String(e) }); + } + } + + return { newUsersCount, skippedExistingCount, errors, fetchMetrics }; +} + +export async function refreshStaleUsers( + db: DatabaseStore, + country: string, + refreshLimit: number, + staleDays: number, +): Promise<{ + refreshedCount: number; + errors: { username: string; reason: string }[]; + fetchMetrics: { duration: number; errors: { part: string; reason: string }[] }[]; +}> { + const errors: { username: string; reason: string }[] = []; + let refreshedCount = 0; + const fetchMetrics: { duration: number; errors: { part: string; reason: string }[] }[] = []; + + const topUsers = await db.getTopUsers(country, refreshLimit); + + for (const row of topUsers) { + if (row.stale_after >= new Date()) { + continue; + } + + try { + const { data, metrics } = await getUserData(row.username, { cacheInRedis: false, withMetrics: true }); + fetchMetrics.push(metrics); + const score = calculateUserScore(data, row.username); + const countryDetected = detectCountry(data.location); + + await db.upsertUser({ + username: data.login, + name: data.name, + avatarUrl: data.avatarUrl, + location: data.location, + country: countryDetected, + rawData: data, + scores: score, + repoScore: Math.round(score.repoScore), + prScore: Math.round(score.prScore), + contributionScore: Math.round(score.contributionScore), + finalScore: Math.round(score.finalScore), + staleDays, + }); + + refreshedCount += 1; + } catch (e) { + errors.push({ username: row.username, reason: e instanceof Error ? e.message : String(e) }); + } + } + + return { refreshedCount, errors, fetchMetrics }; +} + +export async function buildLeaderboardResult( + db: DatabaseStore, + country: string, + sourceData: { title: string }, + errors: string[], +): Promise<{ result: LeaderboardResult; meta: LeaderboardMeta }> { + const allUsers = await db.getLeaderboard(country, getEnvInt("LEADERBOARD_DISPLAY_LIMIT", 500)); + const totalCount = await db.getLeaderboardCount(country); + + const scored: ScoredEntry[] = allUsers.map((row) => ({ + username: row.username, + name: row.name, + avatarUrl: row.avatar_url, + repoScore: row.repo_score, + prScore: row.pr_score, + contributionScore: row.contribution_score, + finalScore: row.final_score, + impactRank: 0, + })); + + scored.sort((a, b) => b.finalScore - a.finalScore); + scored.forEach((u, idx) => (u.impactRank = idx + 1)); + + const result: LeaderboardResult = { + title: sourceData.title, + totalFromSource: totalCount, + scored, + errors: [...new Set(errors)], + }; + + const meta: LeaderboardMeta = { + newUsers: 0, + refreshedUsers: 0, + skippedExisting: 0, + errors: errors.length, + totalInDb: totalCount, + }; + + return { result, meta }; +} + +export async function invalidateLeaderboardCache(country: string): Promise { + try { + const cacheConfig = getCacheConfigFromEnv(); + const cacheStore = createCacheStore(cacheConfig); + if (cacheStore.enabled && cacheStore.del) { + const cacheKey = buildLeaderboardCacheKey(country, cacheConfig.namespace); + await cacheStore.del(cacheKey); + } + } catch { + console.warn("Failed to invalidate leaderboard cache"); + } +} + +// ─── Orchestrator ─────────────────────────────────────────────────────── + +export async function calculateLeaderboard( + country: string, + overrides?: { + seedLimit?: number; + refreshLimit?: number; + staleDays?: number; + displayLimit?: number; + }, +): Promise { + const db = getDatabaseStore(); + await db.initializeSchema(); + + const staleDays = overrides?.staleDays ?? getStaleDays(); + const seedLimit = overrides?.seedLimit ?? getSeedLimit(); + const refreshLimit = overrides?.refreshLimit ?? getRefreshLimit(); + + // 1. Fetch source users + const sourceData = await fetchCommittersFromTop(country); + + // 2a. Seed new users + const seedResult = await seedNewUsers(db, sourceData.users, seedLimit, staleDays); + + // 2b. Refresh stale users from DB top N + const refreshResult = await refreshStaleUsers(db, country, refreshLimit, staleDays); + + // 3. Build leaderboard result + const allErrors = [...seedResult.errors.map(e => e.username), ...refreshResult.errors.map(e => e.username)]; + const { result, meta } = await buildLeaderboardResult( + db, + country, + sourceData, + allErrors, + ); + + meta.newUsers = seedResult.newUsersCount; + meta.refreshedUsers = refreshResult.refreshedCount; + meta.skippedExisting = seedResult.skippedExistingCount; + meta.failedUsernames = allErrors; + + const allFetchMetrics = [...seedResult.fetchMetrics, ...refreshResult.fetchMetrics]; + meta.totalFetchTime = allFetchMetrics.reduce((sum, m) => sum + m.duration, 0); + meta.successfulFetches = allFetchMetrics.filter(m => m.errors.length === 0).length; + + const userFetchErrors: LeaderboardMeta['userFetchErrors'] = []; + seedResult.errors.forEach(e => userFetchErrors.push({ username: e.username, errors: [{ part: 'seed', reason: e.reason }] })); + refreshResult.errors.forEach(e => userFetchErrors.push({ username: e.username, errors: [{ part: 'refresh', reason: e.reason }] })); + allFetchMetrics.forEach((m, i) => { + if (m.errors.length > 0) userFetchErrors.push({ username: allErrors[i] ?? 'unknown', errors: m.errors }); + }); + meta.userFetchErrors = userFetchErrors; + + // 4. Invalidate Redis cache + await invalidateLeaderboardCache(country); + + return { + ...result, + _meta: meta, + }; +} diff --git a/lib/github.ts b/lib/github.ts index 235999a..bf67eed 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -14,6 +14,11 @@ import type { RepoNode, } from "@/types/github"; +export type UserFetchMetrics = { + duration: number; + errors: { part: string; reason: string }[]; +}; + type Logger = Pick; type GitHubRawUser = { @@ -126,11 +131,6 @@ const USER_QUERY = /* GraphQL */ ` name avatarUrl(size: 80) location - contributionsCollection { - totalCommitContributions - totalPullRequestContributions - totalIssueContributions - } repositories( first: $repoCount privacy: PUBLIC @@ -360,9 +360,7 @@ function isGitHubUserData(value: unknown): value is GitHubUserData { !(typeof candidate.name === "string" || candidate.name === null) || typeof candidate.avatarUrl !== "string" || !Array.isArray(candidate.repos) || - !Array.isArray(candidate.pullRequests) || - !isObject(candidate.contributions) || - !isNumericRecord(candidate.contributions) + !Array.isArray(candidate.pullRequests) ) { return false; } @@ -418,11 +416,14 @@ export function buildGitHubUserCacheKey( async function fetchUserDataFromGitHub( executor: GitHubQueryExecutor, username: string, -): Promise { +): Promise<{ data: GitHubUserData; metrics: UserFetchMetrics }> { const externalPrQuery = `type:pr is:merged author:${username} -user:${username}`; const externalIssueQuery = `type:issue author:${username} -user:${username}`; const externalDiscussionQuery = `author:${username} -user:${username}`; + const startTime = performance.now(); + const fetchErrors: { part: string; reason: string }[] = []; + const repoCount = parseCountEnv( process.env.GITHUB_REPO_COUNT, @@ -448,7 +449,7 @@ const discussionCount = parseCountEnv( MAX_GITHUB_DISCUSSION_COUNT, ); - const [userResponse, pullRequests, issues, discussions] = await Promise.all([ + const [userResult, prResult, issuesResult, discussionsResult] = await Promise.allSettled([ executor.execute< { user: GitHubRawUser | null }, { login: string; repoCount: number } @@ -501,21 +502,140 @@ const discussionCount = parseCountEnv( }), ]); - const user = userResponse.user; - if (!user) { + if (userResult.status === "rejected") { + fetchErrors.push({ part: "user", reason: userResult.reason?.message ?? String(userResult.reason) }); + } + if (prResult.status === "rejected") { + fetchErrors.push({ part: "pullRequests", reason: prResult.reason?.message ?? String(prResult.reason) }); + } + if (issuesResult.status === "rejected") { + fetchErrors.push({ part: "issues", reason: issuesResult.reason?.message ?? String(issuesResult.reason) }); + } + if (discussionsResult.status === "rejected") { + fetchErrors.push({ part: "discussions", reason: discussionsResult.reason?.message ?? String(discussionsResult.reason) }); + } + + if (userResult.status === "rejected") { + throw userResult.reason; + } + + if (!userResult.value.user) { throw new Error("User not found"); } - return { - login: user.login, - name: user.name, - avatarUrl: user.avatarUrl, - location: user.location, - repos: user.repositories.nodes.filter(isDefined), - pullRequests, - contributions: user.contributionsCollection, - issues: issues.map(toIssueNode), - discussions: discussions.map(toDiscussionNode), + const user = userResult.value.user; + const pullRequests = prResult.status === "fulfilled" ? prResult.value : []; + const issues = issuesResult.status === "fulfilled" ? issuesResult.value : []; + const discussions = discussionsResult.status === "fulfilled" ? discussionsResult.value : []; + + const duration = performance.now() - startTime; + + const userData: GitHubUserData = { + login: user.login, + name: user.name, + avatarUrl: user.avatarUrl, + location: user.location, + repos: user.repositories.nodes.filter(isDefined), + pullRequests, + issues: issues.map(toIssueNode), + discussions: discussions.map(toDiscussionNode), + }; + + const metrics: UserFetchMetrics = { + duration, + errors: fetchErrors, + }; + + return { data: userData, metrics }; +} + +// Wrapper to maintain the old function signature for existing callers +async function fetchUserDataFromGitHubWrapper( + executor: GitHubQueryExecutor, + username: string, +): Promise { + const { data } = await fetchUserDataFromGitHub(executor, username); + return data; +} + +// --------------------------------------------------------------------------- +// Fetcher factory (caching + single-flight) +// --------------------------------------------------------------------------- + +export function createGitHubUserDataFetcherWithMetrics( + dependencies: GitHubFetcherDependencies, +): (username: string) => Promise<{ data: GitHubUserData; metrics: UserFetchMetrics }> { + const inFlightByCacheKey = new Map>(); + const logger = dependencies.logger ?? console; + + return async (username: string): Promise<{ data: GitHubUserData; metrics: UserFetchMetrics }> => { + const normalizedUsername = normalizeGitHubUsername(username); + if (!normalizedUsername) { + throw new Error("Username is required"); + } + + const cacheKey = buildGitHubUserCacheKey( + normalizedUsername, + dependencies.cacheConfig.namespace, + ); + + // Caching logic remains the same, but we now cache the { data, metrics } object + if (dependencies.cacheStore.enabled) { + try { + const cached = await dependencies.cacheStore.get(cacheKey); + if (cached !== undefined) { + // A simple check to see if it's our object. + if (isObject(cached) && 'data' in cached && 'metrics' in cached && isGitHubUserData(cached.data)) { + logger.info("cache-hit", { key: cacheKey }); + return cached as { data: GitHubUserData; metrics: UserFetchMetrics }; + } + logger.warn("cache-corrupt", { key: cacheKey }); + await dependencies.cacheStore.del?.(cacheKey); + } else { + logger.info("cache-miss", { key: cacheKey }); + } + } catch (error: unknown) { + logger.warn("cache-read-fail", { + key: cacheKey, + message: error instanceof Error ? error.message : String(error), + }); + } + } + + const inFlight = inFlightByCacheKey.get(cacheKey); + if (inFlight) { + logger.info("single-flight-join", { key: cacheKey }); + return inFlight; + } + + const request = (async () => { + const freshResult = await fetchUserDataFromGitHub( + dependencies.executor, + normalizedUsername, + ); + + if (dependencies.cacheStore.enabled) { + try { + await dependencies.cacheStore.set( + cacheKey, + freshResult, + dependencies.cacheConfig.ttlSeconds, + ); + } catch (error: unknown) { + logger.warn("cache-set-fail", { + key: cacheKey, + message: error instanceof Error ? error.message : String(error), + }); + } + } + + return freshResult; + })().finally(() => { + inFlightByCacheKey.delete(cacheKey); + }); + + inFlightByCacheKey.set(cacheKey, request); + return request; }; } @@ -568,7 +688,7 @@ export function createGitHubUserDataFetcher( } const request = (async () => { - const fresh = await fetchUserDataFromGitHub( + const fresh = await fetchUserDataFromGitHubWrapper( dependencies.executor, normalizedUsername, ); @@ -616,29 +736,179 @@ function createDefaultGitHubExecutor(): GitHubQueryExecutor { }); } -let defaultFetcher: ((username: string) => Promise) | undefined; - -function getDefaultFetcher(): (username: string) => Promise { - if (!defaultFetcher) { - const cacheConfig = getCacheConfigFromEnv(); - const cacheStore = createCacheStore(cacheConfig); - - defaultFetcher = createGitHubUserDataFetcher({ - executor: createDefaultGitHubExecutor(), - cacheStore, - cacheConfig: { - namespace: cacheConfig.namespace, - ttlSeconds: cacheConfig.ttlSeconds || DEFAULT_GITHUB_CACHE_TTL_SECONDS, - }, - logger: console, - }); - } +const executorSingleton = createDefaultGitHubExecutor(); +const cacheConfigSingleton = getCacheConfigFromEnv(); +const cacheStoreSingleton = createCacheStore(cacheConfigSingleton); + +/** + * Redis cache entry shape that includes a fetch timestamp for staleness checks. + */ +type CachedUserEntry = { + data: GitHubUserData; + fetchedAt: string; // ISO 8601 timestamp +}; - return defaultFetcher; +function buildUserCacheKey(username: string): string { + return buildGitHubUserCacheKey(username, cacheConfigSingleton.namespace); } -export async function fetchGitHubUserData( +function getStaleDays(): number { + const raw = process.env.GITHUB_USER_STALE_DAYS?.trim(); + if (!raw) return 14; + const parsed = Number.parseInt(raw, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : 14; +} + +/** + * Fetch fresh user data from the GitHub API. + */ +async function fetchFromGitHubApi(username: string): Promise { + const { data } = await fetchUserDataFromGitHub(executorSingleton, username); + return data; +} + +/** + * Unified function to get GitHub user data. + * + * Flow: + * 1. If cacheInRedis: check Redis → HIT + fresh → return + * 2. Check PostgreSQL + * → HIT + fresh (stale_after > now) → return (optionally warm Redis) + * → HIT + stale OR MISS → fetch from GitHub → upsert DB → return + * 3. If !cacheInRedis and data was refreshed: delete stale Redis key + * + * @param username - GitHub username + * @param options.cacheInRedis - Whether to cache in Redis (default: true) + */ +export async function getUserData( username: string, -): Promise { - return getDefaultFetcher()(username); + options?: { cacheInRedis?: boolean; withMetrics?: false }, +): Promise; +export async function getUserData( + username: string, + options: { cacheInRedis?: boolean; withMetrics: true }, +): Promise<{ data: GitHubUserData; metrics: UserFetchMetrics }>; +export async function getUserData( + username: string, + options?: { cacheInRedis?: boolean; withMetrics?: boolean }, +): Promise { + const normalizedUsername = normalizeGitHubUsername(username); + if (!normalizedUsername) { + throw new Error("Username is required"); + } + + const cacheInRedis = options?.cacheInRedis ?? true; + const withMetrics = options?.withMetrics ?? false; + const staleDays = getStaleDays(); + + // ── 1. Check Redis (if enabled) ─────────────────────────────────────── + if (cacheInRedis && cacheStoreSingleton.enabled) { + try { + const cacheKey = buildUserCacheKey(normalizedUsername); + const cached = await cacheStoreSingleton.get(cacheKey); + if (cached) { + const now = Date.now(); + const fetchedAt = new Date(cached.fetchedAt).getTime(); + const ageDays = (now - fetchedAt) / 86_400_000; + + if (ageDays < staleDays) { + return withMetrics + ? { data: cached.data, metrics: { duration: 0, errors: [] } } // No metrics for cached data + : cached.data; + } + + // Stale — delete and fall through + await cacheStoreSingleton.del?.(cacheKey); + } + } catch { + // Non-fatal: continue to DB + } + } + + // ── 2. Check PostgreSQL ──────────────────────────────────────────────── + try { + const { getDatabaseStore } = await import("@/lib/db-store"); + const db = getDatabaseStore(); + + const row = await db.getUser(normalizedUsername); + if (row) { + const isFresh = row.stale_after > new Date(); + if (isFresh) { + // DB has fresh data — warm Redis if enabled and return + if (cacheInRedis && cacheStoreSingleton.enabled) { + try { + const cacheKey = buildUserCacheKey(normalizedUsername); + const entry: CachedUserEntry = { + data: row.raw_data as GitHubUserData, + fetchedAt: new Date().toISOString(), + }; + await cacheStoreSingleton.set(cacheKey, entry, cacheConfigSingleton.ttlSeconds); + } catch { + // Non-fatal: cache write failure + } + } + const userData = row.raw_data as GitHubUserData; + return withMetrics + ? { data: userData, metrics: { duration: 0, errors: [] } } // No metrics for cached data + : userData; + } + } + } catch { + // Non-fatal: DB failure, fall through to GitHub API + } + + // ── 3. Fetch from GitHub API ────────────────────────────────────────── + const { data: fresh, metrics } = await fetchUserDataFromGitHub(executorSingleton, normalizedUsername); + + // Upsert into PostgreSQL + try { + const { getDatabaseStore: getDb } = await import("@/lib/db-store"); + const { calculateUserScore: calcScore } = await import("@/lib/score"); + + const db = getDb(); + const score = calcScore(fresh, normalizedUsername); + + await db.upsertUser({ + username: fresh.login, + name: fresh.name, + avatarUrl: fresh.avatarUrl, + location: fresh.location, + country: null, + rawData: fresh, + scores: score, + repoScore: Math.round(score.repoScore), + prScore: Math.round(score.prScore), + contributionScore: Math.round(score.contributionScore), + finalScore: Math.round(score.finalScore), + staleDays, + }); + } catch { + // Non-fatal: DB write failure + } + + // 4. Handle Redis cache + if (cacheStoreSingleton.enabled && cacheStoreSingleton.del) { + const cacheKey = buildUserCacheKey(normalizedUsername); + if (cacheInRedis) { + // Warm Redis with fresh data + try { + const entry: CachedUserEntry = { + data: fresh, + fetchedAt: new Date().toISOString(), + }; + await cacheStoreSingleton.set(cacheKey, entry, cacheConfigSingleton.ttlSeconds); + } catch { + // Non-fatal + } + } else { + // Delete stale cache entry (leaderboard calculation path) + try { + await cacheStoreSingleton.del(cacheKey); + } catch { + // Non-fatal + } + } + } + + return withMetrics ? { data: fresh, metrics } : fresh; } diff --git a/lib/leaderboard.ts b/lib/leaderboard.ts new file mode 100644 index 0000000..f5dab77 --- /dev/null +++ b/lib/leaderboard.ts @@ -0,0 +1,117 @@ +import { createCacheStore, getCacheConfigFromEnv } from "@/lib/cache-store"; +import { getDatabaseStore } from "@/lib/db-store"; + +export type ScoredLeaderboardEntry = { + username: string; + name: string | null; + avatarUrl: string; + repoScore: number; + prScore: number; + contributionScore: number; + finalScore: number; + impactRank: number; +}; + +export type LeaderboardResult = { + title: string; + totalFromSource: number; + scored: ScoredLeaderboardEntry[]; + errors: string[]; +}; + +function buildLeaderboardCacheKey(country: string, namespace: string): string { + return `${namespace}:leaderboard:${country.trim().toLowerCase()}`; +} + +function getDisplayLimit(): number { + const raw = process.env.LEADERBOARD_DISPLAY_LIMIT?.trim(); + if (!raw) return 500; + const parsed = Number.parseInt(raw, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : 500; +} + +function getCachedLeaderboard( + cached: LeaderboardResult, + displayLimit: number, +): LeaderboardResult | null { + if (cached.scored.length < Math.min(displayLimit, cached.totalFromSource)) { + return null; + } + + return { + ...cached, + scored: cached.scored.slice(0, displayLimit), + }; +} + +export async function getLeaderboardResult( + country: string, +): Promise { + const displayLimit = getDisplayLimit(); + const cacheConfig = getCacheConfigFromEnv(); + const cacheStore = createCacheStore(cacheConfig); + + if (cacheStore.enabled) { + try { + const cacheKey = buildLeaderboardCacheKey(country, cacheConfig.namespace); + const cached = await cacheStore.get(cacheKey); + if (cached) { + const cachedLeaderboard = getCachedLeaderboard(cached, displayLimit); + if (cachedLeaderboard) { + return cachedLeaderboard; + } + } + } catch { + console.warn("Failed to read leaderboard from cache"); + } + } + + const db = getDatabaseStore(); + await db.initializeSchema(); + + const rows = await db.getLeaderboard(country, displayLimit); + const totalCount = await db.getLeaderboardCount(country); + + if (rows.length === 0) { + return { + title: country, + totalFromSource: 0, + scored: [], + errors: [], + }; + } + + const scored: ScoredLeaderboardEntry[] = rows.map((row) => ({ + username: row.username, + name: row.name, + avatarUrl: row.avatar_url, + repoScore: row.repo_score, + prScore: row.pr_score, + contributionScore: row.contribution_score, + finalScore: row.final_score, + impactRank: 0, + })); + + scored.sort((a, b) => b.finalScore - a.finalScore); + scored.forEach((user, index) => { + user.impactRank = index + 1; + }); + + const result: LeaderboardResult = { + title: country, + totalFromSource: totalCount, + scored, + errors: [], + }; + + if (cacheStore.enabled) { + try { + const cacheKey = buildLeaderboardCacheKey(country, cacheConfig.namespace); + await cacheStore.set(cacheKey, result, cacheConfig.ttlSeconds); + } catch { + // Cache write failures should not block the page/API response. + } + } + + return result; +} diff --git a/lib/logger.ts b/lib/logger.ts new file mode 100644 index 0000000..4185b6a --- /dev/null +++ b/lib/logger.ts @@ -0,0 +1,29 @@ +type LogLevel = "INFO" | "WARN" | "ERROR" | "DEBUG"; + +const LOG_LEVEL_COLORS: Record = { + INFO: "\x1b[32m", // Green + WARN: "\x1b[33m", // Yellow + ERROR: "\x1b[31m", // Red + DEBUG: "\x1b[34m", // Blue +}; + +const RESET_COLOR = "\x1b[0m"; + +function log(level: LogLevel, message: string, data?: Record) { + const timestamp = new Date().toISOString(); + const color = LOG_LEVEL_COLORS[level]; + let logMessage = `${color}[${timestamp}] [${level}]${RESET_COLOR} ${message}`; + + if (data) { + logMessage += ` ${JSON.stringify(data)}`; + } + + console.log(logMessage); +} + +export const logger = { + info: (message: string, data?: Record) => log("INFO", message, data), + warn: (message: string, data?: Record) => log("WARN", message, data), + error: (message: string, data?: Record) => log("ERROR", message, data), + debug: (message: string, data?: Record) => log("DEBUG", message, data), +}; \ No newline at end of file diff --git a/lib/score.ts b/lib/score.ts index 81d526a..0e25d01 100644 --- a/lib/score.ts +++ b/lib/score.ts @@ -1,5 +1,4 @@ import type { - ContributionTotals, DiscussionNode, IssueNode, PullRequestNode, @@ -300,7 +299,6 @@ type CommunityScoreResult = { }; function calculateContributionScore( - _contrib: ContributionTotals | undefined, issues: IssueNode[], discussions: DiscussionNode[], username: string, @@ -591,7 +589,6 @@ export function calculateUserScore( data: { repos: RepoNode[]; pullRequests: PullRequestNode[]; - contributions?: ContributionTotals; issues?: IssueNode[]; discussions?: DiscussionNode[]; referenceDate?: string; @@ -611,7 +608,6 @@ export function calculateUserScore( const repoScore = calculateRepoScore(data.repos, referenceDate); const prScore = calculatePRScore(data.pullRequests, username, referenceDate); const communityScore = calculateContributionScore( - data.contributions, data.issues ?? [], data.discussions ?? [], username, diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package.json b/package.json index 12f072b..9779125 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,10 @@ "test:watch": "vitest --watch", "redis:up": "docker compose up -d redis", "redis:down": "docker compose stop redis", + "db:up": "docker compose up -d postgres", + "db:init": "tsx scripts/init-db.ts", + "db:migrate": "tsx scripts/init-db.ts", + "leaderboard:calculate": "tsx scripts/calculate-next-country.ts", "validate-locales": "node scripts/validate-locales.js" }, "dependencies": { @@ -38,10 +42,12 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "autoprefixer": "^10.4.20", + "dotenv": "^17.4.2", "eslint": "^9.17.0", "eslint-config-next": "^16.2.2", "postcss": "^8.5.11", "tailwindcss": "^3.4.14", + "tsx": "^3.12.7", "typescript": "^6.0.2", "vitest": "^4.1.5" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e3ebf9..6a0b7ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,34 +22,34 @@ importers: version: 7.5.0 js-yaml: specifier: ^4.1.1 - version: 4.1.1 + version: 4.3.0 lucide-react: specifier: ^1.7.0 - version: 1.8.0(react@19.2.5) + version: 1.25.0(react@19.2.7) next: specifier: ^16.2.6 - version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 16.2.10(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-themes: specifier: ^0.3.0 - version: 0.3.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 0.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) pg: specifier: ^8.22.0 version: 8.22.0 radix-ui: specifier: ^1.4.3 - version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 1.6.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: specifier: ^19.2.4 - version: 19.2.5 + version: 19.2.7 react-dom: specifier: ^19.2.4 - version: 19.2.5(react@19.2.5) + version: 19.2.7(react@19.2.7) react-icons: specifier: ^5.6.0 - version: 5.6.0(react@19.2.5) + version: 5.7.0(react@19.2.7) recharts: specifier: ^2.12.7 - version: 2.15.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 2.15.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) redis: specifier: ^5.12.1 version: 5.12.1 @@ -62,37 +62,43 @@ importers: version: 4.0.9 '@types/node': specifier: ^25.5.2 - version: 25.6.0 + version: 25.9.5 '@types/pg': specifier: ^8.20.0 version: 8.20.0 '@types/react': specifier: ^19.2.14 - version: 19.2.14 + version: 19.2.17 '@types/react-dom': specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.14) + version: 19.2.3(@types/react@19.2.17) autoprefixer: specifier: ^10.4.20 - version: 10.5.0(postcss@8.5.14) + version: 10.5.4(postcss@8.5.19) + dotenv: + specifier: ^17.4.2 + version: 17.4.2 eslint: specifier: ^9.17.0 - version: 9.39.4(jiti@1.21.7) + version: 9.39.5(jiti@1.21.7) eslint-config-next: specifier: ^16.2.2 - version: 16.2.3(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2))(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2) + version: 16.2.10(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) postcss: specifier: ^8.5.11 - version: 8.5.14 + version: 8.5.19 tailwindcss: specifier: ^3.4.14 - version: 3.4.19 + version: 3.4.19(tsx@3.14.0) + tsx: + specifier: ^3.12.7 + version: 3.14.0 typescript: specifier: ^6.0.2 - version: 6.0.2 + version: 6.0.3 vitest: specifier: ^4.1.5 - version: 4.1.5(@types/node@25.6.0)(vite@8.0.11(@types/node@25.6.0)(jiti@1.21.7)) + version: 4.1.10(@types/node@25.9.5)(vite@8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0)) packages: @@ -100,92 +106,230 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.29.0': - resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} engines: {node: '>=6.9.0'} - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} engines: {node: '>=6.9.0'} - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.29.2': - resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.2': - resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/runtime@7.29.2': - resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@emnapi/core@1.9.2': - resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@emnapi/runtime@1.9.2': - resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -208,12 +352,12 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.5': - resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + '@eslint/eslintrc@3.3.6': + resolution: {integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.4': - resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + '@eslint/js@9.39.5': + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.7': @@ -224,27 +368,31 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@floating-ui/core@1.7.5': - resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} - '@floating-ui/dom@1.7.6': - resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} - '@floating-ui/react-dom@2.1.8': - resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + '@floating-ui/react-dom@2.1.9': + resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/utils@0.2.11': - resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': @@ -424,69 +572,66 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@napi-rs/wasm-runtime@0.2.12': - resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@next/env@16.2.6': - resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==} + '@next/env@16.2.10': + resolution: {integrity: sha512-zLPxg9M0MEHmygpj5OuxjQ+vHMiy/K7cSp74G8ecYolmgUWw0RwN02tF56npup/+qaI8JB97hQgS/r2Hb6QwVA==} - '@next/eslint-plugin-next@16.2.3': - resolution: {integrity: sha512-nE/b9mht28XJxjTwKs/yk7w4XTaU3t40UHVAky6cjiijdP/SEy3hGsnQMPxmXPTpC7W4/97okm6fngKnvCqVaA==} + '@next/eslint-plugin-next@16.2.10': + resolution: {integrity: sha512-Gs8D2m21VnJeFo9qvYIIqJH94frWerWYu41BprU1pLtRVF7PCQNLiFZZ3fG+iPuj3K83Cwv/rt+msLOy8Qgu3Q==} - '@next/swc-darwin-arm64@16.2.6': - resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==} + '@next/swc-darwin-arm64@16.2.10': + resolution: {integrity: sha512-v9IdJCa0H0mbo+8z5zwUpOk1Vj7RjkcI5uNYf5Ws1y6szf/p3Mzl9hLaST8SCt6L9h8NGnruZcd2+o0NTNwDhA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.6': - resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==} + '@next/swc-darwin-x64@16.2.10': + resolution: {integrity: sha512-17IS0jJRViROGmA9uGdNR8VPJpfbnaVG7E9qhso5jDLkmyd0lSDORWxbcKINzcFqzZqGwGtMSnrFRxBpuUYjLQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.6': - resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==} + '@next/swc-linux-arm64-gnu@16.2.10': + resolution: {integrity: sha512-GRQRsRtuciNJvB54AvvuQTiq0oZtFwa1owQqtZD8wwnGpM2L39MV22kpI72YSXLKIyY40LC66EiLFv4PiicXxg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.6': - resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} + '@next/swc-linux-arm64-musl@16.2.10': + resolution: {integrity: sha512-zkN9MQYS7UQBro+FnISUq1itaQjXI9xqISzuQ+2bc921NcJ1x4yPCqrn77tVN6/dOOXaaWVX3k6/bR07pPwK+A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.2.6': - resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} + '@next/swc-linux-x64-gnu@16.2.10': + resolution: {integrity: sha512-iCVJnwvrPYECvA6WM/7+oo+OiTvedIKLxtCLAZP4xZR3nXa1zmzZyLPbYCmWvpd4CvMYF1EMTafd0ii3DygLvA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.2.6': - resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} + '@next/swc-linux-x64-musl@16.2.10': + resolution: {integrity: sha512-ov2g4H0dHY9bPoOU83m91hWT7Iq5qy13bUnyyshLU3HGR1Ownn0X9QpmDPc5iIUaahTp7f7LeGAhV4DSFtackw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.6': - resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} + '@next/swc-win32-arm64-msvc@16.2.10': + resolution: {integrity: sha512-DwAnhLX76HQiFFQNgWlcK+JzlnD1rZ+UK/WY0ZMI/deXpvgnesjNYrqcfo1JzBuz4Kf7o3brIBL0glI1junatA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.6': - resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==} + '@next/swc-win32-x64-msvc@16.2.10': + resolution: {integrity: sha512-0JXq3b85Jk9Jg4ntLUbXSPvoDw3gpZou7twuKdoFG2jOw635v7+IiXfTaa0TxVMyx78pUjnrVYwLgjKfX4e6/A==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -522,29 +667,24 @@ packages: resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==} engines: {node: '>= 20'} - '@octokit/request@10.0.8': - resolution: {integrity: sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==} + '@octokit/request@10.0.11': + resolution: {integrity: sha512-+s7HUxjfFqOMS9VlIwDffq0MikjSAK0gSpG73W+meAvVAvX4MBrHYTK5Bj3Uot55qFT4gzUtfzE4mGWY4Br8/Q==} engines: {node: '>= 20'} '@octokit/types@16.0.0': resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} - '@oxc-project/types@0.128.0': - resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==} + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} - '@playwright/test@1.60.0': - resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} - engines: {node: '>=18'} - hasBin: true - - '@radix-ui/number@1.1.1': - resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + '@radix-ui/number@1.1.2': + resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==} - '@radix-ui/primitive@1.1.3': - resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/primitive@1.1.5': + resolution: {integrity: sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg==} - '@radix-ui/react-accessible-icon@1.1.7': - resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==} + '@radix-ui/react-accessible-icon@1.1.11': + resolution: {integrity: sha512-HQDOFTKwSnmUij6l54wYJJtxTAnxI71+YJLOrjm2ladFB8HAV5Jt7hwaZPhWTGBkYoW4+ZAOfNZrLDh/qvxSYA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -556,8 +696,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-accordion@1.2.12': - resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + '@radix-ui/react-accordion@1.2.16': + resolution: {integrity: sha512-BpZJNmetujnGgUI6OX0jEhEmlA46WPqgub8Rv09Kyquwd0cc1ndMKpiPYCjmBU6KSSRPAMtgLpEoZSG/tdNIWQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -569,8 +709,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-alert-dialog@1.1.15': - resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + '@radix-ui/react-alert-dialog@1.1.19': + resolution: {integrity: sha512-FA7n1f6D/DwGE0+AWxiY5LacNbbExQuEgMubeG06idEaH+mSLuf9dp/qBNqOnvbTQ+4gZ2ue1RATF1Ub91Mg5g==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -582,8 +722,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-arrow@1.1.7': - resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + '@radix-ui/react-arrow@1.1.11': + resolution: {integrity: sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -595,8 +735,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-aspect-ratio@1.1.7': - resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==} + '@radix-ui/react-aspect-ratio@1.1.11': + resolution: {integrity: sha512-IUAhIVpBUvP5NNICjlaB1OFmtRLGqQqTF3ZOSGPoq3XeLXRFtHiWTRxSVEULgOd9GQR2c7tsYqDnhUennapZnw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -608,8 +748,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-avatar@1.1.10': - resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + '@radix-ui/react-avatar@1.2.2': + resolution: {integrity: sha512-sST0qh8GzOB7besQ3tMLWLyngnRuSk0gc/Hm+667KYKQFCt6Y6ZXv25WlqM7dIDK54ULCh5+CHmk4LIolzfz+A==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -621,8 +761,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-checkbox@1.3.3': - resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + '@radix-ui/react-checkbox@1.3.7': + resolution: {integrity: sha512-JroKHfQBfh+fDuzpPsBC+pESkhuq8ql4hljTguz8MWnS35cISr3d/Jhl9kYrB44FlDtxCArYdDvTx+BSsJ64rQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -634,8 +774,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collapsible@1.1.12': - resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + '@radix-ui/react-collapsible@1.1.16': + resolution: {integrity: sha512-opfXRe6nnzyGmCDPx+l1Aqo/RbqWtQal2FnsBqF9hhePp6j0LsRoBaRxcMOlTv+uYTJVtWYZKg9t9wTe+BA/ZA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -647,8 +787,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collection@1.1.7': - resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + '@radix-ui/react-collection@1.1.12': + resolution: {integrity: sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -660,8 +800,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-compose-refs@1.1.2': - resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + '@radix-ui/react-compose-refs@1.1.3': + resolution: {integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -669,8 +809,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-context-menu@2.2.16': - resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} + '@radix-ui/react-context-menu@2.3.3': + resolution: {integrity: sha512-PS+gKE0z2prJ74Y0sM+brAGK4mYOHIR7TlcV5EJgUQ6E0xMvyswkK2X4yRqyganrzsRL+WCSKAPu0NQITICRWg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -682,8 +822,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-context@1.1.2': - resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + '@radix-ui/react-context@1.2.0': + resolution: {integrity: sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -691,8 +831,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-dialog@1.1.15': - resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + '@radix-ui/react-dialog@1.1.19': + resolution: {integrity: sha512-+HhbN2+YtkRgVirjZ2afMeutQRuGOrdkWR5+EFC58SJojGmtyNQwYzgi6tHBpOxvFHefMtPeHdgtjz0BOGxFQg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -704,8 +844,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-direction@1.1.1': - resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + '@radix-ui/react-direction@1.1.2': + resolution: {integrity: sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -713,8 +853,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-dismissable-layer@1.1.11': - resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + '@radix-ui/react-dismissable-layer@1.1.15': + resolution: {integrity: sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -726,8 +866,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-dropdown-menu@2.1.16': - resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + '@radix-ui/react-dropdown-menu@2.1.20': + resolution: {integrity: sha512-slfm+rRaZRuQBvHq60lXvSVUPhid0IPtjSZzIuUlWZMUs01iYZNlGS3mJgRD3ChLQVBAYlKiL/tFyWGX+dz8Xw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -739,8 +879,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-focus-guards@1.1.3': - resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + '@radix-ui/react-focus-guards@1.1.4': + resolution: {integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -748,8 +888,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-focus-scope@1.1.7': - resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + '@radix-ui/react-focus-scope@1.1.12': + resolution: {integrity: sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -761,8 +901,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-form@0.1.8': - resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==} + '@radix-ui/react-form@0.1.12': + resolution: {integrity: sha512-JTX94E4LDL91rzLg7X0mHPdxr0A8JEdVwZEmeOwZJSMDHCGW5DFtSlTSJozUyUs807IQmnvbfzKZFVCK5DmkqQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -774,8 +914,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-hover-card@1.1.15': - resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + '@radix-ui/react-hover-card@1.1.19': + resolution: {integrity: sha512-2KTgMLQtKvicznQgbindEI2RZ3QbDIwU5gabjUPwFJsormjGDz+rUvO4NANmYwzEEpTcTONUt33vBHIfTIVSfw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -787,8 +927,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-id@1.1.1': - resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + '@radix-ui/react-id@1.1.2': + resolution: {integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -796,8 +936,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-label@2.1.7': - resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + '@radix-ui/react-label@2.1.11': + resolution: {integrity: sha512-3PKvDDxOn62k0oV1n4QtNtD2vpu+zYjXR7ojLBPaO6SPvhy53yg0vAmgNeBQeJW5rV3dffoRG+HYfLBZuzw0CQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -809,8 +949,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-menu@2.1.16': - resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + '@radix-ui/react-menu@2.1.20': + resolution: {integrity: sha512-VsUrXxFe9d2ScbZF0fR/oPR1+qjyeLs5p0jzG8h90puMoA9bq4SirYlXbE+USRg9Q2qTeJSFNqjw2nts8jJe4w==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -822,8 +962,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-menubar@1.1.16': - resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==} + '@radix-ui/react-menubar@1.1.20': + resolution: {integrity: sha512-gzFZvybgmwYsFBWDqanycIoEYnhyk8MMnuLamdFVHUZYGp4COM+sqXiwbnn0VMWqGLeeU7GV7jm+dXRa+Wufag==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -835,8 +975,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-navigation-menu@1.2.14': - resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + '@radix-ui/react-navigation-menu@1.2.18': + resolution: {integrity: sha512-K9HiuxZ6xCwSaHcIuUpxyhy4w5gpwzWjh9dHTSbMN3Ix4qAyVObS9RlU3zMycb0PO3v9Tpk0BXMwWvXOUbVXew==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -848,8 +988,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-one-time-password-field@0.1.8': - resolution: {integrity: sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==} + '@radix-ui/react-one-time-password-field@0.1.12': + resolution: {integrity: sha512-nQLu5OAcORDQp1EHAv6k3mJGV1hjMTw2NTGVAsGE1g/mWeNqAd1R5jyaAs3U+A8ZD/W8XNPY2yKT0ZdQnqo3NA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -861,8 +1001,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-password-toggle-field@0.1.3': - resolution: {integrity: sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==} + '@radix-ui/react-password-toggle-field@0.1.7': + resolution: {integrity: sha512-gB1Mr8vzdv1XzDjrtJTXmL0JORRs1B4g7ngUs0F+H2VvMOwXTZMTmLCl0wZZ3m7ylX8TssI7NCvgiSHmLuTm/A==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -874,8 +1014,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-popover@1.1.15': - resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + '@radix-ui/react-popover@1.1.19': + resolution: {integrity: sha512-jkrTdQVxnIB8fpn0NyyxW9CTB5aCXZZelVz5z+Xmii6g5WxMqS3fInNslZ63puP39+Puu4jYohUK31y3dT87gQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -887,8 +1027,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-popper@1.2.8': - resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + '@radix-ui/react-popper@1.3.3': + resolution: {integrity: sha512-mS7dGpyjv6b+gsDjLF7e0ia1W4Im1B1hSCy2yuXlHuvnZxHKagfDaobt/KAKt27EpZMit2pss8eJBVyVjEWM+g==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -900,8 +1040,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-portal@1.1.9': - resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + '@radix-ui/react-portal@1.1.13': + resolution: {integrity: sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -913,8 +1053,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-presence@1.1.5': - resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + '@radix-ui/react-presence@1.1.7': + resolution: {integrity: sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -926,8 +1066,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-primitive@2.1.3': - resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + '@radix-ui/react-primitive@2.1.7': + resolution: {integrity: sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -939,8 +1079,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-progress@1.1.7': - resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + '@radix-ui/react-progress@1.1.12': + resolution: {integrity: sha512-ZPHyI0JyzoH/rP0tq2uRaIZTj/4s8+kAbqPz+e2N8+ejHvwPJ889dHhqn+vh7PNvNeq+boAoH9yzqeoShzwF2w==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -952,8 +1092,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-radio-group@1.3.8': - resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + '@radix-ui/react-radio-group@1.4.3': + resolution: {integrity: sha512-WwZFjWV4s3aC1QtR3k04R+oANHtX2q6fgKlc7MCEiDNlnTxCZ3H8k3mHtEgVlOejystwk1WQgarQhNOQZ2bK1g==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -965,8 +1105,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-roving-focus@1.1.11': - resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + '@radix-ui/react-roving-focus@1.1.15': + resolution: {integrity: sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -978,8 +1118,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-scroll-area@1.2.10': - resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + '@radix-ui/react-scroll-area@1.2.14': + resolution: {integrity: sha512-bBODCWZK7JTbQLHs0uIP4f73wIWatakK4OS33UzkR1x897wu0PuO658a3f+6P2GEGyDzGYMuHRatMVoAk9WZTw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -991,8 +1131,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-select@2.2.6': - resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + '@radix-ui/react-select@2.3.3': + resolution: {integrity: sha512-L5RQTXz6Anxsf9CCv+pTgiAsUpyVj7rJxsGtmhFaEOJ++cVfXucv4qWfsIO0AIB4NAhi3yovWGVMKKS1Xf1Wrg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1004,8 +1144,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-separator@1.1.7': - resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + '@radix-ui/react-separator@1.1.11': + resolution: {integrity: sha512-jRhe86+8PF7VZ1u14eOWVOuh2BuAhALg/FT1VcMC4OHedMTRUazDnDlKTt+yxo5cRNKHMfmvZ4sSQtWDeMV4CQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1017,8 +1157,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slider@1.3.6': - resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + '@radix-ui/react-slider@1.4.3': + resolution: {integrity: sha512-CWVVj+XaTom0SKCqw1EUgb0NuiLwS+N3OFG73mVEezKEjgNIvZiu0EevMelSSU+CbX3owbqJweG2gPU31WGC5A==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1030,8 +1170,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slot@1.2.3': - resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + '@radix-ui/react-slot@1.3.0': + resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1039,8 +1179,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-switch@1.2.6': - resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + '@radix-ui/react-switch@1.3.3': + resolution: {integrity: sha512-1+mlB4/lxJfk5tgJ4g+R5mUCbRpPE1T9+UsEyeLYbGgMtwiMgmuTnfKz4Mw1nHALHjuwyxw4MLd4cSHn6pNSlQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1052,8 +1192,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-tabs@1.1.13': - resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + '@radix-ui/react-tabs@1.1.17': + resolution: {integrity: sha512-nRyXnrAVCwjeXcHbvEbLS6ndbTeKHG1RqCP4A8Gw5L4cemDzPXdD8rAmr6wet0v57R69wGvuIIsFjHSVkZiMzQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1065,8 +1205,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-toast@1.2.15': - resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==} + '@radix-ui/react-toast@1.2.19': + resolution: {integrity: sha512-SxfVZfVOibWKWdkf0Xx1awW2d09fQu4V4PXDY1j5hi4MVf7MWdJZqTBJMa1KWtOr1S6GGtCk02nniZ0Iia+dHw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1078,8 +1218,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-toggle-group@1.1.11': - resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + '@radix-ui/react-toggle-group@1.1.15': + resolution: {integrity: sha512-gIC5Q+Xljg7lmUdzSuDoy0t97yZn1sZl00Ra37ZvKrYdWnQLU6sWLd09yG8cIB9jUAlQfHgJ2ACAG00MFwsqSQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1091,8 +1231,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-toggle@1.1.10': - resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + '@radix-ui/react-toggle@1.1.14': + resolution: {integrity: sha512-QI/hB65XKWACA66P64A+aHxtLUgHJeJLkaQa+awUNXT6T3swndtY5DojeHA+vldrTspMTtFBd7HfZ9QGbM1Qrw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1104,8 +1244,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-toolbar@1.1.11': - resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==} + '@radix-ui/react-toolbar@1.1.15': + resolution: {integrity: sha512-t/iEuVjUnXXtrsGK40AA43uIx37sn3AqZ7oAVnPICK6lFJP6dzMzWR3U9b6eCfFjb6wtSEqkJ9Rn9xDjiOx20g==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1117,8 +1257,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-tooltip@1.2.8': - resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + '@radix-ui/react-tooltip@1.2.12': + resolution: {integrity: sha512-U3HoftgWnmla78vzQbLvKKb7bUYJxoiiqYFzp1wu/TBMyDqMZSuCl3aRICsD6EfVEwcJD2mumGDGUXLFVqQHKA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1130,8 +1270,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-use-callback-ref@1.1.1': - resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + '@radix-ui/react-use-callback-ref@1.1.2': + resolution: {integrity: sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1139,8 +1279,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-controllable-state@1.2.2': - resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + '@radix-ui/react-use-controllable-state@1.2.3': + resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1148,8 +1288,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-effect-event@0.0.2': - resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + '@radix-ui/react-use-effect-event@0.0.3': + resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1157,8 +1297,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-escape-keydown@1.1.1': - resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + '@radix-ui/react-use-escape-keydown@1.1.3': + resolution: {integrity: sha512-3wEkMiPHXha/2VadZ68rYBcmYnPINVGl4Y3gtcM7fKRjANk0OscK+cdqBgUWdozb7YJxsh0vefM7vgAMHXOjqg==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1166,8 +1306,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-is-hydrated@0.1.0': - resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + '@radix-ui/react-use-is-hydrated@0.1.1': + resolution: {integrity: sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1175,8 +1315,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-layout-effect@1.1.1': - resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + '@radix-ui/react-use-layout-effect@1.1.2': + resolution: {integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1184,8 +1324,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-previous@1.1.1': - resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + '@radix-ui/react-use-previous@1.1.2': + resolution: {integrity: sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1193,8 +1333,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-rect@1.1.1': - resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + '@radix-ui/react-use-rect@1.1.2': + resolution: {integrity: sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1202,8 +1342,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-size@1.1.1': - resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + '@radix-ui/react-use-size@1.1.2': + resolution: {integrity: sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1211,8 +1351,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-visually-hidden@1.2.3': - resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + '@radix-ui/react-visually-hidden@1.2.7': + resolution: {integrity: sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1224,8 +1364,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/rect@1.1.1': - resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@radix-ui/rect@1.1.2': + resolution: {integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==} '@redis/bloom@5.12.1': resolution: {integrity: sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==} @@ -1263,103 +1403,103 @@ packages: peerDependencies: '@redis/client': ^5.12.1 - '@rolldown/binding-android-arm64@1.0.0-rc.18': - resolution: {integrity: sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==} + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.18': - resolution: {integrity: sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==} + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.18': - resolution: {integrity: sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==} + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.18': - resolution: {integrity: sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==} + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': - resolution: {integrity: sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==} + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': - resolution: {integrity: sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==} + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==} + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==} + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==} + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': - resolution: {integrity: sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==} + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': - resolution: {integrity: sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==} + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': - resolution: {integrity: sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==} + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18': - resolution: {integrity: sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==} + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': - resolution: {integrity: sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==} + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.18': - resolution: {integrity: sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==} + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1370,8 +1510,8 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1406,8 +1546,8 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -1418,8 +1558,8 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@25.6.0': - resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + '@types/node@25.9.5': + resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==} '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} @@ -1429,176 +1569,193 @@ packages: peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.14': - resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - '@typescript-eslint/eslint-plugin@8.58.2': - resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==} + '@typescript-eslint/eslint-plugin@8.64.0': + resolution: {integrity: sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.58.2 + '@typescript-eslint/parser': ^8.64.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.58.2': - resolution: {integrity: sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==} + '@typescript-eslint/parser@8.64.0': + resolution: {integrity: sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.58.2': - resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==} + '@typescript-eslint/project-service@8.64.0': + resolution: {integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.58.2': - resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==} + '@typescript-eslint/scope-manager@8.64.0': + resolution: {integrity: sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.58.2': - resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==} + '@typescript-eslint/tsconfig-utils@8.64.0': + resolution: {integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.58.2': - resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==} + '@typescript-eslint/type-utils@8.64.0': + resolution: {integrity: sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.58.2': - resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==} + '@typescript-eslint/types@8.64.0': + resolution: {integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.58.2': - resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==} + '@typescript-eslint/typescript-estree@8.64.0': + resolution: {integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.58.2': - resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==} + '@typescript-eslint/utils@8.64.0': + resolution: {integrity: sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.58.2': - resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==} + '@typescript-eslint/visitor-keys@8.64.0': + resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} cpu: [arm] os: [android] - '@unrs/resolver-binding-android-arm64@1.11.1': - resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} cpu: [arm64] os: [android] - '@unrs/resolver-binding-darwin-arm64@1.11.1': - resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} cpu: [arm64] os: [darwin] - '@unrs/resolver-binding-darwin-x64@1.11.1': - resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} cpu: [x64] os: [darwin] - '@unrs/resolver-binding-freebsd-x64@1.11.1': - resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} cpu: [x64] os: [freebsd] - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} cpu: [arm64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} cpu: [arm64] os: [linux] libc: [musl] - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} cpu: [ppc64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} cpu: [riscv64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} cpu: [riscv64] os: [linux] libc: [musl] - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} cpu: [s390x] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} cpu: [x64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} cpu: [x64] os: [linux] libc: [musl] - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} cpu: [arm64] os: [win32] - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} cpu: [ia32] os: [win32] - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} cpu: [x64] os: [win32] - '@vitest/expect@4.1.5': - resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - '@vitest/mocker@4.1.5': - resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1608,33 +1765,33 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.5': - resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - '@vitest/runner@4.1.5': - resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - '@vitest/snapshot@4.1.5': - resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - '@vitest/spy@4.1.5': - resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@vitest/utils@4.1.5': - resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} hasBin: true - ajv@6.14.0: - resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} @@ -1704,8 +1861,8 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} - autoprefixer@10.5.0: - resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} + autoprefixer@10.5.4: + resolution: {integrity: sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -1715,8 +1872,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axe-core@4.11.3: - resolution: {integrity: sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==} + axe-core@4.12.1: + resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} engines: {node: '>=4'} axobject-query@4.1.0: @@ -1730,8 +1887,8 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - baseline-browser-mapping@2.10.18: - resolution: {integrity: sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==} + baseline-browser-mapping@2.10.43: + resolution: {integrity: sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -1739,22 +1896,25 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - brace-expansion@1.1.14: - resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + brace-expansion@1.1.16: + resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + browserslist@4.28.6: + resolution: {integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1775,8 +1935,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001787: - resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} + caniuse-lite@1.0.30001806: + resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} @@ -1818,6 +1978,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1943,16 +2107,24 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.336: - resolution: {integrity: sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==} + electron-to-chromium@1.5.393: + resolution: {integrity: sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + es-abstract@1.24.2: resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} engines: {node: '>= 0.4'} @@ -1965,15 +2137,15 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-iterator-helpers@1.3.2: - resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==} + es-iterator-helpers@1.4.0: + resolution: {integrity: sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==} engines: {node: '>= 0.4'} - es-module-lexer@2.1.0: - resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} es-set-tostringtag@2.1.0: @@ -1984,10 +2156,15 @@ packages: resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} engines: {node: '>= 0.4'} - es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} engines: {node: '>= 0.4'} + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1996,8 +2173,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-next@16.2.3: - resolution: {integrity: sha512-Dnkrylzjof/Az7iNoIQJqD18zTxQZcngir19KJaiRsMnnjpQSVoa6aEg/1Q4hQC+cW90uTlgQYadwL1CYNwFWA==} + eslint-config-next@16.2.10: + resolution: {integrity: sha512-HSybLOY0QKf39i4FWUqPN0xWiNDi6A6UqJmZtgDkS3zMqjXTqULvj/sueXx3cdCG0mVG+qH6k5/qdegklH1d1w==} peerDependencies: eslint: '>=9.0.0' typescript: '>=3.3.1' @@ -2021,8 +2198,8 @@ packages: eslint-plugin-import-x: optional: true - eslint-module-utils@2.12.1: - resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + eslint-module-utils@2.14.0: + resolution: {integrity: sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -2058,11 +2235,11 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-react-hooks@7.0.1: - resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} engines: {node: '>=18'} peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 eslint-plugin-react@7.37.5: resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} @@ -2086,8 +2263,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@9.39.4: - resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + eslint@9.39.5: + resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -2122,18 +2299,15 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} - fast-content-type-parse@3.0.0: - resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-equals@5.4.0: - resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + fast-equals@5.4.1: + resolution: {integrity: sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==} engines: {node: '>=6.0.0'} fast-glob@3.3.1: @@ -2191,11 +2365,6 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2204,8 +2373,8 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - function.prototype.name@1.1.8: - resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} engines: {node: '>= 0.4'} functions-have-names@1.2.3: @@ -2235,8 +2404,8 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.13.7: - resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2285,8 +2454,8 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} hermes-estree@0.25.1: @@ -2299,8 +2468,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} engines: {node: '>= 4'} import-fresh@3.3.1: @@ -2346,8 +2515,8 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} is-data-view@1.0.2: @@ -2358,6 +2527,10 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2443,8 +2616,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true jsesc@3.1.0: @@ -2461,8 +2634,8 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json-with-bigint@3.5.8: - resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} + json-with-bigint@3.5.10: + resolution: {integrity: sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w==} json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} @@ -2589,8 +2762,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@1.8.0: - resolution: {integrity: sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==} + lucide-react@1.25.0: + resolution: {integrity: sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2625,8 +2798,8 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -2644,8 +2817,8 @@ packages: react: ^16.8 || ^17 || ^18 react-dom: ^16.8 || ^17 || ^18 - next@16.2.6: - resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==} + next@16.2.10: + resolution: {integrity: sha512-2som5AVXb3kE6Yjine3/mNbBayYF58eguBWIVVUdr1y/L426xyVEgYxgBG+1QC34P2x5E+tcDup6XkuOAX3dCA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -2665,12 +2838,13 @@ packages: sass: optional: true - node-exports-info@1.6.0: - resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + node-exports-info@1.6.2: + resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} engines: {node: '>= 0.4'} - node-releases@2.0.37: - resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + engines: {node: '>=18'} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -2712,8 +2886,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} @@ -2790,8 +2965,8 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} pify@2.3.0: @@ -2802,16 +2977,6 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - playwright-core@1.60.0: - resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} - engines: {node: '>=18'} - hasBin: true - - playwright@1.60.0: - resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} - engines: {node: '>=18'} - hasBin: true - possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -2852,8 +3017,8 @@ packages: peerDependencies: postcss: ^8.2.14 - postcss-selector-parser@6.1.2: - resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + postcss-selector-parser@6.1.4: + resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} engines: {node: '>=4'} postcss-value-parser@4.2.0: @@ -2863,8 +3028,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.14: - resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + postcss@8.5.19: + resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -2897,8 +3062,8 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - radix-ui@1.4.3: - resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} + radix-ui@1.6.2: + resolution: {integrity: sha512-OwYUjzMwiInCUxgAWpPsavXC3Kh4iyi/49uU1/qZTG3RQDlvegyk1GOMiGvSkjua1RDb3JD3fo3eroL9FV4GQw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -2910,13 +3075,13 @@ packages: '@types/react-dom': optional: true - react-dom@19.2.5: - resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} peerDependencies: - react: ^19.2.5 + react: ^19.2.7 - react-icons@5.6.0: - resolution: {integrity: sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==} + react-icons@5.7.0: + resolution: {integrity: sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==} peerDependencies: react: '*' @@ -2968,8 +3133,8 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' - react@19.2.5: - resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -2985,6 +3150,7 @@ packages: recharts@2.15.4: resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} engines: {node: '>=14'} + deprecated: 1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide peerDependencies: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -3013,8 +3179,8 @@ packages: engines: {node: '>= 0.4'} hasBin: true - resolve@2.0.0-next.6: - resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} engines: {node: '>= 0.4'} hasBin: true @@ -3022,16 +3188,16 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rolldown@1.0.0-rc.18: - resolution: {integrity: sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==} + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} safe-push-apply@1.0.0: @@ -3049,8 +3215,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true @@ -3090,8 +3256,8 @@ packages: resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} engines: {node: '>= 0.4'} - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} siginfo@2.0.0: @@ -3101,6 +3267,13 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -3111,8 +3284,8 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@4.1.0: - resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} @@ -3129,12 +3302,12 @@ packages: string.prototype.repeat@1.0.0: resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} - string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} engines: {node: '>= 0.4'} - string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} engines: {node: '>= 0.4'} string.prototype.trimstart@1.0.8: @@ -3196,12 +3369,12 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.1.2: - resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} tinyrainbow@3.1.0: @@ -3227,6 +3400,10 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@3.14.0: + resolution: {integrity: sha512-xHtFaKtHxM9LOklMmJdI3BEnQq/D5F73Of2E1GDrITi9sgoVkvIsrQUTY1G8FlmGtA+awCI4EBlTRRYxkL2sRg==} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -3243,19 +3420,19 @@ packages: resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} engines: {node: '>= 0.4'} - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} engines: {node: '>= 0.4'} - typescript-eslint@8.58.2: - resolution: {integrity: sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==} + typescript-eslint@8.64.0: + resolution: {integrity: sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - typescript@6.0.2: - resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true @@ -3263,14 +3440,14 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@7.19.2: - resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} - unrs-resolver@1.11.1: - resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} @@ -3301,24 +3478,19 @@ packages: '@types/react': optional: true - use-sync-external-store@1.6.0: - resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} - vite@8.0.11: - resolution: {integrity: sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==} + vite@8.1.5: + resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 + '@vitejs/devtools': ^0.3.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -3355,20 +3527,20 @@ packages: yaml: optional: true - vitest@4.1.5: - resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.5 - '@vitest/browser-preview': 4.1.5 - '@vitest/browser-webdriverio': 4.1.5 - '@vitest/coverage-istanbul': 4.1.5 - '@vitest/coverage-v8': 4.1.5 - '@vitest/ui': 4.1.5 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -3408,8 +3580,8 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-typed-array@1.1.20: - resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} engines: {node: '>= 0.4'} which@2.0.2: @@ -3443,32 +3615,32 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} snapshots: '@alloc/quick-lru@5.2.0': {} - '@babel/code-frame@7.29.0': + '@babel/code-frame@7.29.7': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.29.0': {} + '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.0': + '@babel/core@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.2 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -3478,79 +3650,79 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.1': + '@babel/generator@7.29.7': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.28.6': + '@babel/helper-compilation-targets@7.29.7': dependencies: - '@babel/compat-data': 7.29.0 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.2 + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.6 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-globals@7.28.0': {} + '@babel/helper-globals@7.29.7': {} - '@babel/helper-module-imports@7.28.6': + '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} - '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} - '@babel/helper-validator-option@7.27.1': {} + '@babel/helper-validator-option@7.29.7': {} - '@babel/helpers@7.29.2': + '@babel/helpers@7.29.7': dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 - '@babel/parser@7.29.2': + '@babel/parser@7.29.7': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 - '@babel/runtime@7.29.2': {} + '@babel/runtime@7.29.7': {} - '@babel/template@7.28.6': + '@babel/template@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 - '@babel/traverse@7.29.0': + '@babel/traverse@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.2 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.29.0': + '@babel/types@7.29.7': dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 '@emnapi/core@1.10.0': dependencies: @@ -3558,9 +3730,9 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/core@1.9.2': + '@emnapi/core@1.11.1': dependencies: - '@emnapi/wasi-threads': 1.2.1 + '@emnapi/wasi-threads': 1.2.2 tslib: 2.8.1 optional: true @@ -3569,7 +3741,12 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.9.2': + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.2': dependencies: tslib: 2.8.1 optional: true @@ -3579,9 +3756,80 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@1.21.7))': + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.5(jiti@1.21.7))': dependencies: - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.5(jiti@1.21.7) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -3602,21 +3850,21 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.5': + '@eslint/eslintrc@3.3.6': dependencies: - ajv: 6.14.0 + ajv: 6.15.0 debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.3.0 minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - '@eslint/js@9.39.4': {} + '@eslint/js@9.39.5': {} '@eslint/object-schema@2.1.7': {} @@ -3625,30 +3873,35 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 - '@floating-ui/core@1.7.5': + '@floating-ui/core@1.8.0': dependencies: - '@floating-ui/utils': 0.2.11 + '@floating-ui/utils': 0.2.12 - '@floating-ui/dom@1.7.6': + '@floating-ui/dom@1.8.0': dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 - '@floating-ui/react-dom@2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@floating-ui/react-dom@2.1.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@floating-ui/dom': 1.7.6 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@floating-ui/dom': 1.8.0 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@floating-ui/utils@0.2.11': {} + '@floating-ui/utils@0.2.12': {} - '@humanfs/core@0.19.1': {} + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 - '@humanfs/node@0.16.7': + '@humanfs/node@0.16.8': dependencies: - '@humanfs/core': 0.19.1 + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 '@humanwhocodes/retry': 0.4.3 + '@humanfs/types@0.15.0': {} + '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/retry@0.4.3': {} @@ -3738,7 +3991,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.10.0 + '@emnapi/runtime': 1.11.2 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -3769,48 +4022,48 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@napi-rs/wasm-runtime@0.2.12': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 - '@tybys/wasm-util': 0.10.1 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 optional: true - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.1 + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 optional: true - '@next/env@16.2.6': {} + '@next/env@16.2.10': {} - '@next/eslint-plugin-next@16.2.3': + '@next/eslint-plugin-next@16.2.10': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@16.2.6': + '@next/swc-darwin-arm64@16.2.10': optional: true - '@next/swc-darwin-x64@16.2.6': + '@next/swc-darwin-x64@16.2.10': optional: true - '@next/swc-linux-arm64-gnu@16.2.6': + '@next/swc-linux-arm64-gnu@16.2.10': optional: true - '@next/swc-linux-arm64-musl@16.2.6': + '@next/swc-linux-arm64-musl@16.2.10': optional: true - '@next/swc-linux-x64-gnu@16.2.6': + '@next/swc-linux-x64-gnu@16.2.10': optional: true - '@next/swc-linux-x64-musl@16.2.6': + '@next/swc-linux-x64-musl@16.2.10': optional: true - '@next/swc-win32-arm64-msvc@16.2.6': + '@next/swc-win32-arm64-msvc@16.2.10': optional: true - '@next/swc-win32-x64-msvc@16.2.6': + '@next/swc-win32-x64-msvc@16.2.10': optional: true '@nodelib/fs.scandir@2.1.5': @@ -3834,7 +4087,7 @@ snapshots: '@octokit/graphql@9.0.3': dependencies: - '@octokit/request': 10.0.8 + '@octokit/request': 10.0.11 '@octokit/types': 16.0.0 universal-user-agent: 7.0.3 @@ -3844,772 +4097,766 @@ snapshots: dependencies: '@octokit/types': 16.0.0 - '@octokit/request@10.0.8': + '@octokit/request@10.0.11': dependencies: '@octokit/endpoint': 11.0.3 '@octokit/request-error': 7.1.0 '@octokit/types': 16.0.0 - fast-content-type-parse: 3.0.0 - json-with-bigint: 3.5.8 + content-type: 2.0.0 + json-with-bigint: 3.5.10 universal-user-agent: 7.0.3 '@octokit/types@16.0.0': dependencies: '@octokit/openapi-types': 27.0.0 - '@oxc-project/types@0.128.0': {} - - '@playwright/test@1.60.0': - dependencies: - playwright: 1.60.0 - optional: true + '@oxc-project/types@0.139.0': {} - '@radix-ui/number@1.1.1': {} + '@radix-ui/number@1.1.2': {} - '@radix-ui/primitive@1.1.3': {} + '@radix-ui/primitive@1.1.5': {} - '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@radix-ui/react-accessible-icon@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-accordion@1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-collapsible': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-collection': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-alert-dialog@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dialog': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@radix-ui/react-arrow@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@radix-ui/react-aspect-ratio@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-avatar@1.2.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-checkbox@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-collapsible@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@radix-ui/react-collection@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.5)': + '@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.17)(react@19.2.7)': dependencies: - react: 19.2.5 + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + + '@radix-ui/react-context-menu@2.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu': 2.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.5)': + '@radix-ui/react-context@1.2.0(@types/react@19.2.17)(react@19.2.7)': dependencies: - react: 19.2.5 + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@types/react': 19.2.17 + + '@radix-ui/react-dialog@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) aria-hidden: 1.2.6 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.5)': + '@radix-ui/react-direction@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: - react: 19.2.5 + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@radix-ui/react-dismissable-layer@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-dropdown-menu@2.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu': 2.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.5)': + '@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.17)(react@19.2.7)': dependencies: - react: 19.2.5 + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@radix-ui/react-focus-scope@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-form@0.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-label': 2.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-hover-card@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-popper': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.5)': + '@radix-ui/react-id@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@radix-ui/react-label@2.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-menu@2.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-collection': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) aria-hidden: 1.2.6 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-menubar@1.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-collection': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu': 2.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-navigation-menu@1.2.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-collection': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/number': 1.1.1 - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-one-time-password-field@0.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-collection': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-password-toggle-field@0.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-popover@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) aria-hidden: 1.2.6 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/rect': 1.1.1 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-popper@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/react-dom': 2.1.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-arrow': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-rect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/rect': 1.1.2 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@radix-ui/react-portal@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@radix-ui/react-presence@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@radix-ui/react-primitive@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@radix-ui/react-progress@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-radio-group@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-roving-focus@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-collection': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/number': 1.1.1 - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-scroll-area@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/number': 1.1.1 - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-select@2.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-collection': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) aria-hidden: 1.2.6 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@radix-ui/react-separator@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/number': 1.1.1 - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-slider@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-collection': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.5)': + '@radix-ui/react-slot@1.3.0(@types/react@19.2.17)(react@19.2.7)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + + '@radix-ui/react-switch@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-tabs@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-toast@1.2.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-collection': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-toggle-group@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toggle': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@radix-ui/react-toggle@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-toolbar@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-separator': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toggle-group': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-tooltip@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.5)': + '@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: - react: 19.2.5 + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.5)': + '@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.2.17)(react@19.2.7)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.5)': + '@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.17)(react@19.2.7)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.5)': + '@radix-ui/react-use-escape-keydown@1.1.3(@types/react@19.2.17)(react@19.2.7)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.14)(react@19.2.5)': + '@radix-ui/react-use-is-hydrated@0.1.1(@types/react@19.2.17)(react@19.2.7)': dependencies: - react: 19.2.5 - use-sync-external-store: 1.6.0(react@19.2.5) + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.5)': + '@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: - react: 19.2.5 + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.5)': + '@radix-ui/react-use-previous@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: - react: 19.2.5 + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.5)': + '@radix-ui/react-use-rect@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: - '@radix-ui/rect': 1.1.1 - react: 19.2.5 + '@radix-ui/rect': 1.1.2 + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.5)': + '@radix-ui/react-use-size@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - react: 19.2.5 + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@radix-ui/react-visually-hidden@1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@radix-ui/rect@1.1.1': {} + '@radix-ui/rect@1.1.2': {} '@redis/bloom@5.12.1(@redis/client@5.12.1)': dependencies: @@ -4631,56 +4878,56 @@ snapshots: dependencies: '@redis/client': 5.12.1 - '@rolldown/binding-android-arm64@1.0.0-rc.18': + '@rolldown/binding-android-arm64@1.1.5': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.18': + '@rolldown/binding-darwin-arm64@1.1.5': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.18': + '@rolldown/binding-darwin-x64@1.1.5': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.18': + '@rolldown/binding-freebsd-x64@1.1.5': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': + '@rolldown/binding-linux-arm64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': + '@rolldown/binding-linux-arm64-musl@1.1.5': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': + '@rolldown/binding-linux-ppc64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': + '@rolldown/binding-linux-s390x-gnu@1.1.5': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': + '@rolldown/binding-linux-x64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': + '@rolldown/binding-linux-x64-musl@1.1.5': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': + '@rolldown/binding-openharmony-arm64@1.1.5': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': + '@rolldown/binding-wasm32-wasi@1.1.5': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18': + '@rolldown/binding-win32-arm64-msvc@1.1.5': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': + '@rolldown/binding-win32-x64-msvc@1.1.5': optional: true - '@rolldown/pluginutils@1.0.0-rc.18': {} + '@rolldown/pluginutils@1.0.1': {} '@rtsao/scc@1.1.0': {} @@ -4690,7 +4937,7 @@ snapshots: dependencies: tslib: 2.8.1 - '@tybys/wasm-util@0.10.1': + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true @@ -4726,7 +4973,7 @@ snapshots: '@types/deep-eql@4.0.2': {} - '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} '@types/js-yaml@4.0.9': {} @@ -4734,222 +4981,233 @@ snapshots: '@types/json5@0.0.29': {} - '@types/node@25.6.0': + '@types/node@25.9.5': dependencies: - undici-types: 7.19.2 + undici-types: 7.24.6 '@types/pg@8.20.0': dependencies: - '@types/node': 25.6.0 + '@types/node': 25.9.5 pg-protocol: 1.15.0 pg-types: 2.2.0 - '@types/react-dom@19.2.3(@types/react@19.2.14)': + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - '@types/react@19.2.14': + '@types/react@19.2.17': dependencies: csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2))(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2)': + '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2) - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/type-utils': 8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2) - '@typescript-eslint/utils': 8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2) - '@typescript-eslint/visitor-keys': 8.58.2 - eslint: 9.39.4(jiti@1.21.7) - ignore: 7.0.5 + '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/type-utils': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/utils': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.64.0 + eslint: 9.39.5(jiti@1.21.7) + ignore: 7.0.6 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@6.0.2) - typescript: 6.0.2 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2)': + '@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.64.0 debug: 4.4.3 - eslint: 9.39.4(jiti@1.21.7) - typescript: 6.0.2 + eslint: 9.39.5(jiti@1.21.7) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.58.2(typescript@6.0.2)': + '@typescript-eslint/project-service@8.64.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) - '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@6.0.3) + '@typescript-eslint/types': 8.64.0 debug: 4.4.3 - typescript: 6.0.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.58.2': + '@typescript-eslint/scope-manager@8.64.0': dependencies: - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/visitor-keys': 8.64.0 - '@typescript-eslint/tsconfig-utils@8.58.2(typescript@6.0.2)': + '@typescript-eslint/tsconfig-utils@8.64.0(typescript@6.0.3)': dependencies: - typescript: 6.0.2 + typescript: 6.0.3 - '@typescript-eslint/type-utils@8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2)': + '@typescript-eslint/type-utils@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - '@typescript-eslint/utils': 8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2) + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) debug: 4.4.3 - eslint: 9.39.4(jiti@1.21.7) - ts-api-utils: 2.5.0(typescript@6.0.2) - typescript: 6.0.2 + eslint: 9.39.5(jiti@1.21.7) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.58.2': {} + '@typescript-eslint/types@8.64.0': {} - '@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2)': + '@typescript-eslint/typescript-estree@8.64.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.58.2(typescript@6.0.2) - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/project-service': 8.64.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@6.0.3) + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/visitor-keys': 8.64.0 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.7.4 - tinyglobby: 0.2.16 - ts-api-utils: 2.5.0(typescript@6.0.2) - typescript: 6.0.2 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2)': + '@typescript-eslint/utils@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - eslint: 9.39.4(jiti@1.21.7) - typescript: 6.0.2 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(typescript@6.0.3) + eslint: 9.39.5(jiti@1.21.7) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.58.2': + '@typescript-eslint/visitor-keys@8.64.0': dependencies: - '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/types': 8.64.0 eslint-visitor-keys: 5.0.1 - '@unrs/resolver-binding-android-arm-eabi@1.11.1': + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': optional: true - '@unrs/resolver-binding-android-arm64@1.11.1': + '@unrs/resolver-binding-darwin-arm64@1.12.2': optional: true - '@unrs/resolver-binding-darwin-arm64@1.11.1': + '@unrs/resolver-binding-darwin-x64@1.12.2': optional: true - '@unrs/resolver-binding-darwin-x64@1.11.1': + '@unrs/resolver-binding-freebsd-x64@1.12.2': optional: true - '@unrs/resolver-binding-freebsd-x64@1.11.1': + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': optional: true - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': optional: true - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': optional: true - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': optional: true - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': optional: true - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': optional: true - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': optional: true - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': optional: true - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': optional: true - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': optional: true - '@unrs/resolver-binding-linux-x64-musl@1.11.1': + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': optional: true - '@unrs/resolver-binding-wasm32-wasi@1.11.1': + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': dependencies: - '@napi-rs/wasm-runtime': 0.2.12 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': optional: true - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': optional: true - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true - '@vitest/expect@4.1.5': + '@vitest/expect@4.1.10': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.5 - '@vitest/utils': 4.1.5 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.5(vite@8.0.11(@types/node@25.6.0)(jiti@1.21.7))': + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0))': dependencies: - '@vitest/spy': 4.1.5 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.11(@types/node@25.6.0)(jiti@1.21.7) + vite: 8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0) - '@vitest/pretty-format@4.1.5': + '@vitest/pretty-format@4.1.10': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.5': + '@vitest/runner@4.1.10': dependencies: - '@vitest/utils': 4.1.5 + '@vitest/utils': 4.1.10 pathe: 2.0.3 - '@vitest/snapshot@4.1.5': + '@vitest/snapshot@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.5 - '@vitest/utils': 4.1.5 + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.5': {} + '@vitest/spy@4.1.10': {} - '@vitest/utils@4.1.5': + '@vitest/utils@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.5 + '@vitest/pretty-format': 4.1.10 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - acorn-jsx@5.3.2(acorn@8.16.0): + acorn-jsx@5.3.2(acorn@8.17.0): dependencies: - acorn: 8.16.0 + acorn: 8.17.0 - acorn@8.16.0: {} + acorn@8.17.0: {} - ajv@6.14.0: + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 @@ -4988,7 +5246,7 @@ snapshots: call-bound: 1.0.4 define-properties: 1.2.1 es-abstract: 1.24.2 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 is-string: 1.1.1 math-intrinsics: 1.1.0 @@ -4999,7 +5257,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 es-shim-unscopables: 1.1.0 array.prototype.findlastindex@1.2.6: @@ -5009,7 +5267,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 es-shim-unscopables: 1.1.0 array.prototype.flat@1.3.3: @@ -5050,20 +5308,20 @@ snapshots: async-function@1.0.0: {} - autoprefixer@10.5.0(postcss@8.5.14): + autoprefixer@10.5.4(postcss@8.5.19): dependencies: - browserslist: 4.28.2 - caniuse-lite: 1.0.30001787 + browserslist: 4.28.6 + caniuse-lite: 1.0.30001806 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.14 + postcss: 8.5.19 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 - axe-core@4.11.3: {} + axe-core@4.12.1: {} axobject-query@4.1.0: {} @@ -5071,16 +5329,16 @@ snapshots: balanced-match@4.0.4: {} - baseline-browser-mapping@2.10.18: {} + baseline-browser-mapping@2.10.43: {} binary-extensions@2.3.0: {} - brace-expansion@1.1.14: + brace-expansion@1.1.16: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.5: + brace-expansion@5.0.7: dependencies: balanced-match: 4.0.4 @@ -5088,13 +5346,15 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.2: + browserslist@4.28.6: dependencies: - baseline-browser-mapping: 2.10.18 - caniuse-lite: 1.0.30001787 - electron-to-chromium: 1.5.336 - node-releases: 2.0.37 - update-browserslist-db: 1.2.3(browserslist@4.28.2) + baseline-browser-mapping: 2.10.43 + caniuse-lite: 1.0.30001806 + electron-to-chromium: 1.5.393 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.6) + + buffer-from@1.1.2: {} call-bind-apply-helpers@1.0.2: dependencies: @@ -5117,7 +5377,7 @@ snapshots: camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001787: {} + caniuse-lite@1.0.30001806: {} chai@6.2.2: {} @@ -5158,6 +5418,8 @@ snapshots: concat-map@0.0.1: {} + content-type@2.0.0: {} + convert-source-map@2.0.0: {} cross-spawn@7.0.6: @@ -5266,19 +5528,28 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 csstype: 3.2.3 + dotenv@17.4.2: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.336: {} + electron-to-chromium@1.5.393: {} emoji-regex@9.2.2: {} + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + es-abstract@1.24.2: dependencies: array-buffer-byte-length: 1.0.2 @@ -5291,10 +5562,10 @@ snapshots: data-view-byte-offset: 1.0.1 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 + es-to-primitive: 1.3.4 + function.prototype.name: 1.2.0 get-intrinsic: 1.3.0 get-proto: 1.0.1 get-symbol-description: 1.1.0 @@ -5303,7 +5574,7 @@ snapshots: has-property-descriptors: 1.0.2 has-proto: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.4 internal-slot: 1.1.0 is-array-buffer: 3.0.5 is-callable: 1.2.7 @@ -5321,26 +5592,26 @@ snapshots: object.assign: 4.1.7 own-keys: 1.0.1 regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 + safe-array-concat: 1.1.4 safe-push-apply: 1.0.0 safe-regex-test: 1.1.0 set-proto: 1.0.0 stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 string.prototype.trimstart: 1.0.8 typed-array-buffer: 1.0.3 typed-array-byte-length: 1.0.3 typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 + typed-array-length: 1.0.8 unbox-primitive: 1.1.0 - which-typed-array: 1.1.20 + which-typed-array: 1.1.22 es-define-property@1.0.1: {} es-errors@1.3.0: {} - es-iterator-helpers@1.3.2: + es-iterator-helpers@1.4.0: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 @@ -5359,9 +5630,9 @@ snapshots: iterator.prototype: 1.1.5 math-intrinsics: 1.1.0 - es-module-lexer@2.1.0: {} + es-module-lexer@2.3.1: {} - es-object-atoms@1.1.1: + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -5370,36 +5641,64 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.2 + hasown: 2.0.4 es-shim-unscopables@1.1.0: dependencies: - hasown: 2.0.2 + hasown: 2.0.4 - es-to-primitive@1.3.0: + es-to-primitive@1.3.4: dependencies: + es-abstract-get: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 is-callable: 1.2.7 is-date-object: 1.1.0 is-symbol: 1.1.1 + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} - eslint-config-next@16.2.3(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2))(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2): + eslint-config-next@16.2.10(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3): dependencies: - '@next/eslint-plugin-next': 16.2.3 - eslint: 9.39.4(jiti@1.21.7) + '@next/eslint-plugin-next': 16.2.10 + eslint: 9.39.5(jiti@1.21.7) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-react-hooks: 7.0.1(eslint@9.39.4(jiti@1.21.7)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@1.21.7)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@1.21.7)) + eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@1.21.7)) + eslint-plugin-react-hooks: 7.1.1(eslint@9.39.5(jiti@1.21.7)) globals: 16.4.0 - typescript-eslint: 8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2) + typescript-eslint: 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) optionalDependencies: - typescript: 6.0.2 + typescript: 6.0.3 transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-webpack @@ -5409,38 +5708,38 @@ snapshots: eslint-import-resolver-node@0.3.10: dependencies: debug: 3.2.7 - is-core-module: 2.16.1 - resolve: 2.0.0-next.6 + is-core-module: 2.16.2 + resolve: 2.0.0-next.7 transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@1.21.7)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 - eslint: 9.39.4(jiti@1.21.7) - get-tsconfig: 4.13.7 + eslint: 9.39.5(jiti@1.21.7) + get-tsconfig: 4.14.0 is-bun-module: 2.0.0 stable-hash: 0.0.5 - tinyglobby: 0.2.16 - unrs-resolver: 1.11.1 + tinyglobby: 0.2.17 + unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@1.21.7)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7)): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2) - eslint: 9.39.4(jiti@1.21.7) + '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) + eslint: 9.39.5(jiti@1.21.7) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@1.21.7)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -5449,38 +5748,38 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.5(jiti@1.21.7) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7)) - hasown: 2.0.2 - is-core-module: 2.16.1 + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@1.21.7)) + hasown: 2.0.4 + is-core-module: 2.16.2 is-glob: 4.0.3 minimatch: 3.1.5 object.fromentries: 2.0.8 object.groupby: 1.0.3 object.values: 1.2.1 semver: 6.3.1 - string.prototype.trimend: 1.0.9 + string.prototype.trimend: 1.0.10 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2) + '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(jiti@1.21.7)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 - axe-core: 4.11.3 + axe-core: 4.12.1 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.4(jiti@1.21.7) - hasown: 2.0.2 + eslint: 9.39.5(jiti@1.21.7) + hasown: 2.0.4 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 minimatch: 3.1.5 @@ -5488,35 +5787,35 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@7.0.1(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-react-hooks@7.1.1(eslint@9.39.5(jiti@1.21.7)): dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.2 - eslint: 9.39.4(jiti@1.21.7) + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + eslint: 9.39.5(jiti@1.21.7) hermes-parser: 0.25.1 - zod: 4.3.6 - zod-validation-error: 4.0.2(zod@4.3.6) + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) transitivePeerDependencies: - supports-color - eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-react@7.37.5(eslint@9.39.5(jiti@1.21.7)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 array.prototype.flatmap: 1.3.3 array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 - es-iterator-helpers: 1.3.2 - eslint: 9.39.4(jiti@1.21.7) + es-iterator-helpers: 1.4.0 + eslint: 9.39.5(jiti@1.21.7) estraverse: 5.3.0 - hasown: 2.0.2 + hasown: 2.0.4 jsx-ast-utils: 3.3.5 minimatch: 3.1.5 object.entries: 1.1.9 object.fromentries: 2.0.8 object.values: 1.2.1 prop-types: 15.8.1 - resolve: 2.0.0-next.6 + resolve: 2.0.0-next.7 semver: 6.3.1 string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 @@ -5532,21 +5831,21 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.4(jiti@1.21.7): + eslint@9.39.5(jiti@1.21.7): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@1.21.7)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.5 - '@eslint/js': 9.39.4 + '@eslint/eslintrc': 3.3.6 + '@eslint/js': 9.39.5 '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.7 + '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.14.0 + '@types/estree': 1.0.9 + ajv: 6.15.0 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 @@ -5575,8 +5874,8 @@ snapshots: espree@10.4.0: dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) eslint-visitor-keys: 4.2.1 esquery@1.7.0: @@ -5591,19 +5890,17 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 esutils@2.0.3: {} eventemitter3@4.0.7: {} - expect-type@1.3.0: {} - - fast-content-type-parse@3.0.0: {} + expect-type@1.4.0: {} fast-deep-equal@3.1.3: {} - fast-equals@5.4.0: {} + fast-equals@5.4.1: {} fast-glob@3.3.1: dependencies: @@ -5629,9 +5926,9 @@ snapshots: dependencies: reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.4): + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: - picomatch: 4.0.4 + picomatch: 4.0.5 file-entry-cache@8.0.0: dependencies: @@ -5661,22 +5958,22 @@ snapshots: fraction.js@5.3.4: {} - fsevents@2.3.2: - optional: true - fsevents@2.3.3: optional: true function-bind@1.1.2: {} - function.prototype.name@1.1.8: + function.prototype.name@1.2.0: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 - define-properties: 1.2.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 functions-have-names: 1.2.3 - hasown: 2.0.2 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 is-callable: 1.2.7 + is-document.all: 1.0.0 functions-have-names@1.2.3: {} @@ -5689,12 +5986,12 @@ snapshots: call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 function-bind: 1.1.2 get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.4 math-intrinsics: 1.1.0 get-nonce@1.0.1: {} @@ -5702,7 +5999,7 @@ snapshots: get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-symbol-description@1.1.0: dependencies: @@ -5710,7 +6007,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-tsconfig@4.13.7: + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -5751,7 +6048,7 @@ snapshots: dependencies: has-symbols: 1.1.0 - hasown@2.0.2: + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -5763,7 +6060,7 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.5: {} + ignore@7.0.6: {} import-fresh@3.3.1: dependencies: @@ -5775,8 +6072,8 @@ snapshots: internal-slot@1.1.0: dependencies: es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 + hasown: 2.0.4 + side-channel: 1.1.1 internmap@2.0.3: {} @@ -5809,13 +6106,13 @@ snapshots: is-bun-module@2.0.0: dependencies: - semver: 7.7.4 + semver: 7.8.5 is-callable@1.2.7: {} - is-core-module@2.16.1: + is-core-module@2.16.2: dependencies: - hasown: 2.0.2 + hasown: 2.0.4 is-data-view@1.0.2: dependencies: @@ -5828,6 +6125,10 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -5862,7 +6163,7 @@ snapshots: call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 - hasown: 2.0.2 + hasown: 2.0.4 is-set@2.0.3: {} @@ -5883,7 +6184,7 @@ snapshots: is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.20 + which-typed-array: 1.1.22 is-weakmap@2.0.2: {} @@ -5903,7 +6204,7 @@ snapshots: iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 get-proto: 1.0.1 has-symbols: 1.1.0 @@ -5913,7 +6214,7 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.1.1: + js-yaml@4.3.0: dependencies: argparse: 2.0.1 @@ -5925,7 +6226,7 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - json-with-bigint@3.5.8: {} + json-with-bigint@3.5.10: {} json5@1.0.2: dependencies: @@ -6024,9 +6325,9 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@1.8.0(react@19.2.5): + lucide-react@1.25.0(react@19.2.7): dependencies: - react: 19.2.5 + react: 19.2.7 magic-string@0.30.21: dependencies: @@ -6043,11 +6344,11 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 5.0.7 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.14 + brace-expansion: 1.1.16 minimist@1.2.8: {} @@ -6059,50 +6360,49 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.11: {} + nanoid@3.3.16: {} napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} - next-themes@0.3.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + next-themes@0.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - next@16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + next@16.2.10(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - '@next/env': 16.2.6 + '@next/env': 16.2.10 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.18 - caniuse-lite: 1.0.30001787 + baseline-browser-mapping: 2.10.43 + caniuse-lite: 1.0.30001806 postcss: 8.4.31 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.5) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.7) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.6 - '@next/swc-darwin-x64': 16.2.6 - '@next/swc-linux-arm64-gnu': 16.2.6 - '@next/swc-linux-arm64-musl': 16.2.6 - '@next/swc-linux-x64-gnu': 16.2.6 - '@next/swc-linux-x64-musl': 16.2.6 - '@next/swc-win32-arm64-msvc': 16.2.6 - '@next/swc-win32-x64-msvc': 16.2.6 - '@playwright/test': 1.60.0 + '@next/swc-darwin-arm64': 16.2.10 + '@next/swc-darwin-x64': 16.2.10 + '@next/swc-linux-arm64-gnu': 16.2.10 + '@next/swc-linux-arm64-musl': 16.2.10 + '@next/swc-linux-x64-gnu': 16.2.10 + '@next/swc-linux-x64-musl': 16.2.10 + '@next/swc-win32-arm64-msvc': 16.2.10 + '@next/swc-win32-x64-msvc': 16.2.10 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - node-exports-info@1.6.0: + node-exports-info@1.6.2: dependencies: array.prototype.flatmap: 1.3.3 es-errors: 1.3.0 object.entries: 1.1.9 semver: 6.3.1 - node-releases@2.0.37: {} + node-releases@2.0.51: {} normalize-path@3.0.0: {} @@ -6119,7 +6419,7 @@ snapshots: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 has-symbols: 1.1.0 object-keys: 1.1.1 @@ -6128,14 +6428,14 @@ snapshots: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 object.fromentries@2.0.8: dependencies: call-bind: 1.0.9 define-properties: 1.2.1 es-abstract: 1.24.2 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 object.groupby@1.0.3: dependencies: @@ -6148,9 +6448,9 @@ snapshots: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 - obug@2.1.1: {} + obug@2.1.4: {} optionator@0.9.4: dependencies: @@ -6226,49 +6526,40 @@ snapshots: picomatch@2.3.2: {} - picomatch@4.0.4: {} + picomatch@4.0.5: {} pify@2.3.0: {} pirates@4.0.7: {} - playwright-core@1.60.0: - optional: true - - playwright@1.60.0: - dependencies: - playwright-core: 1.60.0 - optionalDependencies: - fsevents: 2.3.2 - optional: true - possible-typed-array-names@1.1.0: {} - postcss-import@15.1.0(postcss@8.5.14): + postcss-import@15.1.0(postcss@8.5.19): dependencies: - postcss: 8.5.14 + postcss: 8.5.19 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.12 - postcss-js@4.1.0(postcss@8.5.14): + postcss-js@4.1.0(postcss@8.5.19): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.14 + postcss: 8.5.19 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.14): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.19)(tsx@3.14.0): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 - postcss: 8.5.14 + postcss: 8.5.19 + tsx: 3.14.0 - postcss-nested@6.2.0(postcss@8.5.14): + postcss-nested@6.2.0(postcss@8.5.19): dependencies: - postcss: 8.5.14 - postcss-selector-parser: 6.1.2 + postcss: 8.5.19 + postcss-selector-parser: 6.1.4 - postcss-selector-parser@6.1.2: + postcss-selector-parser@6.1.4: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 @@ -6277,13 +6568,13 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.14: + postcss@8.5.19: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -6309,127 +6600,127 @@ snapshots: queue-microtask@1.2.3: {} - radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + radix-ui@1.6.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + '@radix-ui/primitive': 1.1.5 + '@radix-ui/react-accessible-icon': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-accordion': 1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-alert-dialog': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-arrow': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-aspect-ratio': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-avatar': 1.2.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-checkbox': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-collapsible': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-collection': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context-menu': 2.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-dialog': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-dropdown-menu': 2.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-form': 0.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-hover-card': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-label': 2.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-menu': 2.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-menubar': 1.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-navigation-menu': 1.2.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-one-time-password-field': 0.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-password-toggle-field': 0.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-popover': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-popper': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-progress': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-radio-group': 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-scroll-area': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-select': 2.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-separator': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slider': 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-switch': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-tabs': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toast': 1.2.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toggle': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toggle-group': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toolbar': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-tooltip': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-escape-keydown': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - react-dom@19.2.5(react@19.2.5): + react-dom@19.2.7(react@19.2.7): dependencies: - react: 19.2.5 + react: 19.2.7 scheduler: 0.27.0 - react-icons@5.6.0(react@19.2.5): + react-icons@5.7.0(react@19.2.7): dependencies: - react: 19.2.5 + react: 19.2.7 react-is@16.13.1: {} react-is@18.3.1: {} - react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.5): + react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.7): dependencies: - react: 19.2.5 - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.7 + react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.5): + react-remove-scroll@2.7.2(@types/react@19.2.17)(react@19.2.7): dependencies: - react: 19.2.5 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.5) - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.7 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.17)(react@19.2.7) + react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.5) - use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.5) + use-callback-ref: 1.3.3(@types/react@19.2.17)(react@19.2.7) + use-sidecar: 1.1.3(@types/react@19.2.17)(react@19.2.7) optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - react-smooth@4.0.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + react-smooth@4.0.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - fast-equals: 5.4.0 + fast-equals: 5.4.1 prop-types: 15.8.1 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - react-transition-group: 4.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-transition-group: 4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.5): + react-style-singleton@2.2.3(@types/react@19.2.17)(react@19.2.7): dependencies: get-nonce: 1.0.1 - react: 19.2.5 + react: 19.2.7 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - react-transition-group@4.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + react-transition-group@4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - react@19.2.5: {} + react@19.2.7: {} read-cache@1.0.0: dependencies: @@ -6443,15 +6734,15 @@ snapshots: dependencies: decimal.js-light: 2.5.1 - recharts@2.15.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + recharts@2.15.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: clsx: 2.1.1 eventemitter3: 4.0.7 lodash: 4.18.1 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) react-is: 18.3.1 - react-smooth: 4.0.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react-smooth: 4.0.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) recharts-scale: 0.4.5 tiny-invariant: 1.3.3 victory-vendor: 36.9.2 @@ -6473,7 +6764,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 get-proto: 1.0.1 which-builtin-type: 1.2.1 @@ -6494,47 +6785,47 @@ snapshots: resolve@1.22.12: dependencies: es-errors: 1.3.0 - is-core-module: 2.16.1 + is-core-module: 2.16.2 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@2.0.0-next.6: + resolve@2.0.0-next.7: dependencies: es-errors: 1.3.0 - is-core-module: 2.16.1 - node-exports-info: 1.6.0 + is-core-module: 2.16.2 + node-exports-info: 1.6.2 object-keys: 1.1.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 reusify@1.1.0: {} - rolldown@1.0.0-rc.18: + rolldown@1.1.5: dependencies: - '@oxc-project/types': 0.128.0 - '@rolldown/pluginutils': 1.0.0-rc.18 + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.18 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.18 - '@rolldown/binding-darwin-x64': 1.0.0-rc.18 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.18 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.18 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.18 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.18 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.18 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.18 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.18 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.18 + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - safe-array-concat@1.1.3: + safe-array-concat@1.1.4: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 @@ -6557,7 +6848,7 @@ snapshots: semver@6.3.1: {} - semver@7.7.4: {} + semver@7.8.5: {} set-function-length@1.2.2: dependencies: @@ -6579,13 +6870,13 @@ snapshots: dependencies: dunder-proto: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 sharp@0.34.5: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.7.4 + semver: 7.8.5 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 @@ -6639,7 +6930,7 @@ snapshots: object-inspect: 1.13.4 side-channel-map: 1.0.1 - side-channel@1.1.0: + side-channel@1.1.1: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 @@ -6651,13 +6942,20 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + split2@4.2.0: {} stable-hash@0.0.5: {} stackback@0.0.2: {} - std-env@4.1.0: {} + std-env@4.2.0: {} stop-iteration-iterator@1.1.0: dependencies: @@ -6677,53 +6975,54 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 gopd: 1.2.0 has-symbols: 1.1.0 internal-slot: 1.1.0 regexp.prototype.flags: 1.5.4 set-function-name: 2.0.2 - side-channel: 1.1.0 + side-channel: 1.1.1 string.prototype.repeat@1.0.0: dependencies: define-properties: 1.2.1 es-abstract: 1.24.2 - string.prototype.trim@1.2.10: + string.prototype.trim@1.2.11: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 es-abstract: 1.24.2 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 - string.prototype.trimend@1.0.9: + string.prototype.trimend@1.0.10: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 string.prototype.trimstart@1.0.8: dependencies: call-bind: 1.0.9 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 strip-bom@3.0.0: {} strip-json-comments@3.1.1: {} - styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.5): + styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.7): dependencies: client-only: 0.0.1 - react: 19.2.5 + react: 19.2.7 optionalDependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.7 sucrase@3.35.1: dependencies: @@ -6732,7 +7031,7 @@ snapshots: lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.7 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 ts-interface-checker: 0.1.13 supports-color@7.2.0: @@ -6743,7 +7042,7 @@ snapshots: tailwind-merge@2.6.1: {} - tailwindcss@3.4.19: + tailwindcss@3.4.19(tsx@3.14.0): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -6759,12 +7058,12 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.14 - postcss-import: 15.1.0(postcss@8.5.14) - postcss-js: 4.1.0(postcss@8.5.14) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.14) - postcss-nested: 6.2.0(postcss@8.5.14) - postcss-selector-parser: 6.1.2 + postcss: 8.5.19 + postcss-import: 15.1.0(postcss@8.5.19) + postcss-js: 4.1.0(postcss@8.5.19) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.19)(tsx@3.14.0) + postcss-nested: 6.2.0(postcss@8.5.19) + postcss-selector-parser: 6.1.4 resolve: 1.22.12 sucrase: 3.35.1 transitivePeerDependencies: @@ -6783,12 +7082,12 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.1.2: {} + tinyexec@1.2.4: {} - tinyglobby@0.2.16: + tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinyrainbow@3.1.0: {} @@ -6796,9 +7095,9 @@ snapshots: dependencies: is-number: 7.0.0 - ts-api-utils@2.5.0(typescript@6.0.2): + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: - typescript: 6.0.2 + typescript: 6.0.3 ts-interface-checker@0.1.13: {} @@ -6811,6 +7110,14 @@ snapshots: tslib@2.8.1: {} + tsx@3.14.0: + dependencies: + esbuild: 0.18.20 + get-tsconfig: 4.14.0 + source-map-support: 0.5.21 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -6839,7 +7146,7 @@ snapshots: is-typed-array: 1.1.15 reflect.getprototypeof: 1.0.10 - typed-array-length@1.0.7: + typed-array-length@1.0.8: dependencies: call-bind: 1.0.9 for-each: 0.3.5 @@ -6848,18 +7155,18 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2): + typescript-eslint@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2))(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2) - '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2) - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - '@typescript-eslint/utils': 8.58.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.2) - eslint: 9.39.4(jiti@1.21.7) - typescript: 6.0.2 + '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3))(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/parser': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.64.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.64.0(eslint@9.39.5(jiti@1.21.7))(typescript@6.0.3) + eslint: 9.39.5(jiti@1.21.7) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - typescript@6.0.2: {} + typescript@6.0.3: {} unbox-primitive@1.1.0: dependencies: @@ -6868,37 +7175,40 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@7.19.2: {} + undici-types@7.24.6: {} universal-user-agent@7.0.3: {} - unrs-resolver@1.11.1: + unrs-resolver@1.12.2: dependencies: napi-postinstall: 0.3.4 optionalDependencies: - '@unrs/resolver-binding-android-arm-eabi': 1.11.1 - '@unrs/resolver-binding-android-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-x64': 1.11.1 - '@unrs/resolver-binding-freebsd-x64': 1.11.1 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 - '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-musl': 1.11.1 - '@unrs/resolver-binding-wasm32-wasi': 1.11.1 - '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 - '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 - '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - - update-browserslist-db@1.2.3(browserslist@4.28.2): - dependencies: - browserslist: 4.28.2 + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + + update-browserslist-db@1.2.3(browserslist@4.28.6): + dependencies: + browserslist: 4.28.6 escalade: 3.2.0 picocolors: 1.1.1 @@ -6906,24 +7216,20 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.5): + use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.7): dependencies: - react: 19.2.5 + react: 19.2.7 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.17 - use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.5): + use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.7): dependencies: detect-node-es: 1.1.0 - react: 19.2.5 + react: 19.2.7 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.14 - - use-sync-external-store@1.6.0(react@19.2.5): - dependencies: - react: 19.2.5 + '@types/react': 19.2.17 util-deprecate@1.0.2: {} @@ -6944,42 +7250,43 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@8.0.11(@types/node@25.6.0)(jiti@1.21.7): + vite@8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0): dependencies: lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.14 - rolldown: 1.0.0-rc.18 - tinyglobby: 0.2.16 + picomatch: 4.0.5 + postcss: 8.5.19 + rolldown: 1.1.5 + tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.6.0 + '@types/node': 25.9.5 fsevents: 2.3.3 jiti: 1.21.7 - - vitest@4.1.5(@types/node@25.6.0)(vite@8.0.11(@types/node@25.6.0)(jiti@1.21.7)): - dependencies: - '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(vite@8.0.11(@types/node@25.6.0)(jiti@1.21.7)) - '@vitest/pretty-format': 4.1.5 - '@vitest/runner': 4.1.5 - '@vitest/snapshot': 4.1.5 - '@vitest/spy': 4.1.5 - '@vitest/utils': 4.1.5 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 + tsx: 3.14.0 + + vitest@4.1.10(@types/node@25.9.5)(vite@8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 magic-string: 0.30.21 - obug: 2.1.1 + obug: 2.1.4 pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.1.0 + picomatch: 4.0.5 + std-env: 4.2.0 tinybench: 2.9.0 - tinyexec: 1.1.2 - tinyglobby: 0.2.16 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.11(@types/node@25.6.0)(jiti@1.21.7) + vite: 8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.6.0 + '@types/node': 25.9.5 transitivePeerDependencies: - msw @@ -6994,7 +7301,7 @@ snapshots: which-builtin-type@1.2.1: dependencies: call-bound: 1.0.4 - function.prototype.name: 1.1.8 + function.prototype.name: 1.2.0 has-tostringtag: 1.0.2 is-async-function: 2.1.1 is-date-object: 1.1.0 @@ -7005,7 +7312,7 @@ snapshots: isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.20 + which-typed-array: 1.1.22 which-collection@1.0.2: dependencies: @@ -7014,7 +7321,7 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-typed-array@1.1.20: + which-typed-array@1.1.22: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.9 @@ -7041,8 +7348,8 @@ snapshots: yocto-queue@0.1.0: {} - zod-validation-error@4.0.2(zod@4.3.6): + zod-validation-error@4.0.2(zod@4.4.3): dependencies: - zod: 4.3.6 + zod: 4.4.3 - zod@4.3.6: {} + zod@4.4.3: {} diff --git a/scripts/calculate-next-country.ts b/scripts/calculate-next-country.ts new file mode 100644 index 0000000..b247809 --- /dev/null +++ b/scripts/calculate-next-country.ts @@ -0,0 +1,93 @@ +/** + * Standalone script to calculate the next country's leaderboard. + * + * This script: + * 1. Loads .env file automatically + * 2. Connects to the shared PostgreSQL database (via DATABASE_URL) + * 3. Picks the country that was calculated longest ago + * 4. Calls the same calculateLeaderboard() function used by the API + * 5. Updates the leaderboard_calculation tracking table + * 6. Exits + * + * Usage: + * npx tsx scripts/calculate-next-country.ts + * + * Prerequisites: + * - DATABASE_URL must be set (in .env or environment) + * - GITHUB_TOKEN must be set (in .env or environment) + */ +// Load .env file for standalone execution. This must be the first import. +import "dotenv/config"; + +import { getDatabaseStore } from "@/lib/db-store"; +import { calculateLeaderboard, type LeaderboardResult } from "@/lib/calculate-leaderboard"; +import { logger } from "@/lib/logger"; + +async function main() { + const overallStartTime = performance.now(); + logger.info("=== DevImpact Leaderboard Calculator Start ==="); + logger.info(`DB: ${(process.env.DATABASE_URL ?? "").slice(0, 40)}...`); + + const db = getDatabaseStore(); + await db.initializeSchema(); + await db.seedCalculationCountries(); + + // 1. Pick the next country to calculate + logger.info("Picking next country to calculate..."); + const next = await db.getNextCountryToCalculate(); + if (!next) { + logger.info("No countries to calculate. All are up-to-date or running."); + process.exit(0); + } + + logger.info(`Selected: ${next.title} (${next.slug})`); + + // 2. Mark as running + await db.startCalculation(next.slug); + logger.info(`Marked '${next.slug}' as running.`); + + try { + // 3. Run the calculation (same function used by the API endpoint) + logger.info("Starting leaderboard calculation..."); + const result = await calculateLeaderboard(next.slug); + + // 4. Mark as done + const errorCount = result._meta.errors; + const errorMessage = errorCount > 0 ? `${errorCount} users failed` : undefined; + + await db.finishCalculation(next.slug, errorMessage); + logger.info(`Finished calculation for '${next.slug}'.`); + + // 5. Log detailed summary + const overallDuration = (performance.now() - overallStartTime) / 1000; + const totalFetchTime = result._meta.totalFetchTime ?? 0; + const successfulFetches = result._meta.successfulFetches ?? 0; + const averageFetchTime = successfulFetches > 0 ? (totalFetchTime / successfulFetches / 1000).toFixed(2) : "N/A"; + + logger.info("=== Calculation Summary ===", { + country: next.title, + newUsers: result._meta.newUsers, + skippedExisting: result._meta.skippedExisting, + refreshedUsers: result._meta.refreshedUsers, + totalInDb: result._meta.totalInDb, + errors: errorCount, + failedUsernames: result._meta.failedUsernames, + totalFetchTime: `${(totalFetchTime / 1000).toFixed(2)}s`, + averageFetchTime: `${averageFetchTime}s`, + overallDuration: `${overallDuration.toFixed(2)}s`, + }); + + if (result._meta.userFetchErrors && result._meta.userFetchErrors.length > 0) { + logger.warn("Detailed user fetch errors:", { errors: result._meta.userFetchErrors }); + } + + process.exit(0); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + logger.error(`FATAL: ${message}`); + await db.finishCalculation(next.slug, message); + process.exit(1); + } +} + +main(); diff --git a/test/api/compare.route.test.ts b/test/api/compare.route.test.ts index eb616ee..5314170 100644 --- a/test/api/compare.route.test.ts +++ b/test/api/compare.route.test.ts @@ -2,12 +2,12 @@ import { beforeEach, describe, expect, test, vi } from "vitest"; import { GitHubApiError } from "@/lib/github-graphql-client"; const mocks = vi.hoisted(() => ({ - fetchGitHubUserData: vi.fn(), + getUserData: vi.fn(), calculateUserScore: vi.fn(), })); vi.mock("@/lib/github", () => ({ - fetchGitHubUserData: mocks.fetchGitHubUserData, + getUserData: mocks.getUserData, })); vi.mock("@/lib/score", () => ({ @@ -33,6 +33,24 @@ function makeRequest(params: Record): Request { }); } +function makeUser(login: string, name: string) { + return { + login, + name, + avatarUrl: `https://example.com/${login}.png`, + location: "Cairo, Egypt", + repos: [], + pullRequests: [], + contributions: { + totalCommitContributions: 0, + totalPullRequestContributions: 0, + totalIssueContributions: 0, + }, + issues: [], + discussions: [], + }; +} + function makeScore(finalScore: number) { return { repoScore: 10, @@ -70,12 +88,12 @@ function makeScore(finalScore: number) { describe("GET /api/compare", () => { beforeEach(() => { - mocks.fetchGitHubUserData.mockReset(); + mocks.getUserData.mockReset(); mocks.calculateUserScore.mockReset(); }); test("returns structured friendly error when GitHub rate limit is hit", async () => { - mocks.fetchGitHubUserData.mockRejectedValueOnce( + mocks.getUserData.mockRejectedValueOnce( new GitHubApiError({ message: "API rate limit exceeded for user.", kind: "PRIMARY_RATE_LIMIT", @@ -98,7 +116,6 @@ describe("GET /api/compare", () => { ); const body = (await response.json()) as { success: boolean; - error?: string; errorDetails?: { code?: string; retryAfterSeconds?: number; rateLimit?: unknown }; }; @@ -109,32 +126,46 @@ describe("GET /api/compare", () => { expect(body.errorDetails?.rateLimit).toBeUndefined(); }); + test("returns resource-limit errors instead of masking them as not found", async () => { + mocks.getUserData.mockRejectedValueOnce( + new GitHubApiError({ + message: "Resource limits for this query exceeded.", + kind: "RESOURCE_LIMIT", + status: 200, + rateLimit: { + limit: 5000, + remaining: 4993, + used: 7, + resetAt: Math.floor(Date.now() / 1000) + 60, + resource: "graphql", + }, + }), + ); + + const response = await GET( + makeRequest({ + username: ["petebacondarwin", "o2sa"], + }), + ); + const body = (await response.json()) as { + success: boolean; + errorDetails?: { code?: string; targetUsernames?: string[] }; + }; + + expect(response.status).toBe(503); + expect(body.success).toBe(false); + expect(body.errorDetails?.code).toBe("GITHUB_RESOURCE_LIMIT"); + expect(body.errorDetails?.targetUsernames).toBeUndefined(); + }); + test("returns success payload when both users are processed", async () => { - mocks.fetchGitHubUserData.mockResolvedValueOnce({ - name: "User A", - avatarUrl: "https://example.com/a.png", - repos: [], - pullRequests: [], - contributions: { - totalCommitContributions: 0, - totalPullRequestContributions: 0, - totalIssueContributions: 0, - }, - issues: [], - discussions: [], + mocks.getUserData.mockResolvedValueOnce({ + data: makeUser("user-a", "User A"), + metrics: { duration: 0, errors: [] }, }); - mocks.fetchGitHubUserData.mockResolvedValueOnce({ - name: "User B", - avatarUrl: "https://example.com/b.png", - repos: [], - pullRequests: [], - contributions: { - totalCommitContributions: 0, - totalPullRequestContributions: 0, - totalIssueContributions: 0, - }, - issues: [], - discussions: [], + mocks.getUserData.mockResolvedValueOnce({ + data: makeUser("user-b", "User B"), + metrics: { duration: 0, errors: [] }, }); mocks.calculateUserScore.mockReturnValueOnce(makeScore(20)); @@ -158,7 +189,7 @@ describe("GET /api/compare", () => { }); test("returns targeted username for not-found errors", async () => { - mocks.fetchGitHubUserData.mockRejectedValueOnce(new Error("User not found")); + mocks.getUserData.mockRejectedValueOnce(new Error("User not found")); const response = await GET( makeRequest({ diff --git a/test/fixtures/github.ts b/test/fixtures/github.ts index f9e119f..34885ee 100644 --- a/test/fixtures/github.ts +++ b/test/fixtures/github.ts @@ -1,5 +1,4 @@ import type { - ContributionTotals, DiscussionNode, IssueNode, PullRequestNode, @@ -10,7 +9,6 @@ import type { export type UserScoreInput = { repos: RepoNode[]; pullRequests: PullRequestNode[]; - contributions: ContributionTotals; issues?: IssueNode[]; discussions?: DiscussionNode[]; referenceDate?: string; @@ -49,11 +47,6 @@ const defaultPullRequest: PullRequestNode = { }, }; -const defaultContributions: ContributionTotals = { - totalCommitContributions: 0, - totalPullRequestContributions: 0, - totalIssueContributions: 0, -}; const defaultIssue: IssueNode = { title: "Issue about improving docs", @@ -135,14 +128,7 @@ export function makeDiscussion( }; } -export function makeContributions( - overrides: Partial = {}, -): ContributionTotals { - return { - ...defaultContributions, - ...overrides, - }; -} + export function makeUserScoreInput( overrides: Partial = {}, @@ -150,7 +136,6 @@ export function makeUserScoreInput( return { repos: overrides.repos ?? [makeRepo()], pullRequests: overrides.pullRequests ?? [makePullRequest()], - contributions: overrides.contributions ?? makeContributions(), issues: overrides.issues ?? [], discussions: overrides.discussions ?? [], referenceDate: overrides.referenceDate, diff --git a/types/github.ts b/types/github.ts index 0547eff..292e9e7 100644 --- a/types/github.ts +++ b/types/github.ts @@ -58,11 +58,7 @@ export type PullRequestNode = { }; }; -export type ContributionTotals = { - totalCommitContributions: number; - totalPullRequestContributions: number; - totalIssueContributions: number; -}; + export type GitHubUserData = { login: string; @@ -71,7 +67,6 @@ export type GitHubUserData = { location: string | null; repos: RepoNode[]; pullRequests: PullRequestNode[]; - contributions: ContributionTotals; issues?: IssueNode[]; discussions?: DiscussionNode[]; }; From e9cfbf30ed076b32a8562a2015d6005d40ab4b4d Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Tue, 21 Jul 2026 06:56:30 +0300 Subject: [PATCH 07/10] test: add dotenv configuration to test files and remove unused contributions mock --- lib/github.test.ts | 3 ++ test/github/github-cache.test.ts | 36 +++++++++++-------- test/github/github-graphql-client.test.ts | 2 ++ .../calculateUserScore.contribution.test.ts | 11 ------ .../calculateUserScore.language.test.ts | 2 -- test/scoring/calculateUserScore.repo.test.ts | 3 +- .../calculateUserScore.scenario.test.ts | 2 -- 7 files changed, 27 insertions(+), 32 deletions(-) diff --git a/lib/github.test.ts b/lib/github.test.ts index 0de8090..699c7d9 100644 --- a/lib/github.test.ts +++ b/lib/github.test.ts @@ -1,3 +1,6 @@ +import "dotenv/config"; + + import {describe, expect, it} from "vitest"; import {parseCountEnv} from "./github"; diff --git a/test/github/github-cache.test.ts b/test/github/github-cache.test.ts index 933fc36..1d035ee 100644 --- a/test/github/github-cache.test.ts +++ b/test/github/github-cache.test.ts @@ -1,3 +1,5 @@ +import "dotenv/config"; + import { afterEach, describe, expect, test, vi } from "vitest"; import { createGitHubUserDataFetcher, @@ -19,10 +21,7 @@ function makeExecutor( delayMs = 0, ): GitHubFetcherDependencies["executor"] { return { - async execute< - TData, - TVariables extends Record, - >(params: { + async execute>(params: { operationName: string; query: string; variables: TVariables; @@ -174,8 +173,7 @@ function isGitHubUserData(value: unknown): value is GitHubUserData { value !== null && "avatarUrl" in value && "repos" in value && - "pullRequests" in value && - "contributions" in value + "pullRequests" in value ); } @@ -186,14 +184,11 @@ describe("GitHub user data caching", () => { test("cache hit skips GitHub calls", async () => { const cachedPayload: GitHubUserData = { name: "Cached", + location: "any", + login: "Cached", avatarUrl: "https://example.com/cached.png", repos: [], pullRequests: [], - contributions: { - totalCommitContributions: 0, - totalPullRequestContributions: 0, - totalIssueContributions: 0, - }, issues: [], discussions: [], }; @@ -266,10 +261,16 @@ describe("GitHub user data caching", () => { vi.stubEnv("GITHUB_ISSUE_COUNT", "2"); vi.stubEnv("GITHUB_DISCUSSION_COUNT", "2"); - const calls: Array<{ operationName: string; variables: Record }> = []; + const calls: Array<{ + operationName: string; + variables: Record; + }> = []; const fetcher = createGitHubUserDataFetcher({ executor: { - async execute>(params: { + async execute< + TData, + TVariables extends Record, + >(params: { operationName: string; query: string; variables: TVariables; @@ -416,8 +417,13 @@ describe("GitHub user data caching", () => { const result = await fetcher("testuser"); expect(result.pullRequests).toHaveLength(2); - expect(calls.filter((call) => call.operationName === "FetchUserPullRequests")).toHaveLength(1); - expect(calls.find((call) => call.operationName === "FetchUserPullRequests")?.variables.prCount).toBe(2); + expect( + calls.filter((call) => call.operationName === "FetchUserPullRequests"), + ).toHaveLength(1); + expect( + calls.find((call) => call.operationName === "FetchUserPullRequests") + ?.variables.prCount, + ).toBe(2); }); test("cache write failure does not fail request", async () => { diff --git a/test/github/github-graphql-client.test.ts b/test/github/github-graphql-client.test.ts index a41bcd3..bd6088f 100644 --- a/test/github/github-graphql-client.test.ts +++ b/test/github/github-graphql-client.test.ts @@ -1,3 +1,5 @@ +import "dotenv/config"; + import { describe, expect, test } from "vitest"; import { GitHubApiError, diff --git a/test/scoring/calculateUserScore.contribution.test.ts b/test/scoring/calculateUserScore.contribution.test.ts index eefb19e..785577a 100644 --- a/test/scoring/calculateUserScore.contribution.test.ts +++ b/test/scoring/calculateUserScore.contribution.test.ts @@ -2,7 +2,6 @@ import { describe, expect, test } from "vitest"; import { calculateUserScore } from "@/lib/score"; import { - makeContributions, makeDiscussion, makeIssue, makePullRequest, @@ -17,11 +16,6 @@ describe("calculateUserScore - contribution scoring", () => { makeUserScoreInput({ repos: [], pullRequests: [], - contributions: makeContributions({ - totalCommitContributions: 10_000, - totalPullRequestContributions: 0, - totalIssueContributions: 0, - }), }), "octocat", ); @@ -34,11 +28,6 @@ describe("calculateUserScore - contribution scoring", () => { makeUserScoreInput({ repos: [], pullRequests: [], - contributions: makeContributions({ - totalCommitContributions: 0, - totalPullRequestContributions: 10_000, - totalIssueContributions: 0, - }), }), "octocat", ); diff --git a/test/scoring/calculateUserScore.language.test.ts b/test/scoring/calculateUserScore.language.test.ts index 800e839..43ce927 100644 --- a/test/scoring/calculateUserScore.language.test.ts +++ b/test/scoring/calculateUserScore.language.test.ts @@ -3,7 +3,6 @@ import { describe, expect, test } from "vitest"; import { calculateUserScore } from "@/lib/score"; import { getLanguageFactor } from "@/lib/scoring/languageScoring"; import { - makeContributions, makePullRequest, makeRepo, makeRepoLanguages, @@ -313,7 +312,6 @@ describe("calculateUserScore - language scoring", () => { }, }), ], - contributions: makeContributions(), selectedLanguages: ["TypeScript"], }), "octocat", diff --git a/test/scoring/calculateUserScore.repo.test.ts b/test/scoring/calculateUserScore.repo.test.ts index 5081e55..878d486 100644 --- a/test/scoring/calculateUserScore.repo.test.ts +++ b/test/scoring/calculateUserScore.repo.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "vitest"; import { calculateUserScore } from "@/lib/score"; -import { makeContributions, makeRepo, makeUserScoreInput } from "@/test/fixtures/github"; +import { makeRepo, makeUserScoreInput } from "@/test/fixtures/github"; import { expectedRepoScore, sumRepoScores } from "@/test/helpers/score"; describe("calculateUserScore - repository scoring", () => { @@ -10,7 +10,6 @@ describe("calculateUserScore - repository scoring", () => { { repos: [], pullRequests: [], - contributions: makeContributions(), }, "octocat", ); diff --git a/test/scoring/calculateUserScore.scenario.test.ts b/test/scoring/calculateUserScore.scenario.test.ts index a0be311..9426bc0 100644 --- a/test/scoring/calculateUserScore.scenario.test.ts +++ b/test/scoring/calculateUserScore.scenario.test.ts @@ -2,7 +2,6 @@ import { describe, expect, test } from "vitest"; import { calculateUserScore } from "@/lib/score"; import { - makeContributions, makeIssue, makePullRequest, makeRepo, @@ -95,7 +94,6 @@ describe("calculateUserScore - final score behavior", () => { { repos: [], pullRequests: [], - contributions: makeContributions(), }, "ghost", ); From 00c72053522edad7be115dc4bb481f241d82fa65 Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:50:14 +0300 Subject: [PATCH 08/10] refactor: optimize imports and improve URL synchronization in HomePageClient --- components/home-page-client.tsx | 13 +++++---- lib/calculate-leaderboard.ts | 2 +- lib/github.ts | 47 ------------------------------- scripts/calculate-next-country.ts | 2 +- 4 files changed, 9 insertions(+), 55 deletions(-) diff --git a/components/home-page-client.tsx b/components/home-page-client.tsx index 4130086..7bcf2fb 100644 --- a/components/home-page-client.tsx +++ b/components/home-page-client.tsx @@ -343,11 +343,13 @@ export function HomePageClient() { useEffect(() => { const params = searchParams.getAll("username"); const urlLanguages = sanitizeSelectedLanguages(searchParams.getAll("selectedLanguage")); - syncToUrl( - params[0] ?? "", - params[1] ?? "", - urlLanguages, - ); + queueMicrotask(() => { + syncToUrl( + params[0] ?? "", + params[1] ?? "", + urlLanguages, + ); + }); }, [searchParams]); useEffect(() => { @@ -357,7 +359,6 @@ export function HomePageClient() { } if (data) { - setDisplayData(data); return; } diff --git a/lib/calculate-leaderboard.ts b/lib/calculate-leaderboard.ts index 989a7ae..014eee0 100644 --- a/lib/calculate-leaderboard.ts +++ b/lib/calculate-leaderboard.ts @@ -1,5 +1,5 @@ import yaml from "js-yaml"; -import { createGitHubUserDataFetcherWithMetrics, getUserData } from "@/lib/github"; +import { getUserData } from "@/lib/github"; import { calculateUserScore } from "@/lib/score"; import { createCacheStore, getCacheConfigFromEnv } from "@/lib/cache-store"; import { getDatabaseStore, type DatabaseStore } from "@/lib/db-store"; diff --git a/lib/github.ts b/lib/github.ts index bf67eed..eeca40a 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -1,5 +1,4 @@ import { - DEFAULT_GITHUB_CACHE_TTL_SECONDS, createCacheStore, getCacheConfigFromEnv, type CacheConfig, @@ -61,35 +60,6 @@ type PageInfo = { endCursor: string | null; }; -type FetchUserAndPullRequestsResponse = { - user: GitHubRawUser | null; - pullRequests: { - nodes: Array; - pageInfo: PageInfo; - }; -}; - -type FetchPullRequestsPageResponse = { - pullRequests: { - nodes: Array; - pageInfo: PageInfo; - }; -}; - -type FetchIssuesPageResponse = { - issues: { - nodes: Array; - pageInfo: PageInfo; - }; -}; - -type FetchDiscussionsPageResponse = { - discussions: { - nodes: Array; - pageInfo: PageInfo; - }; -}; - type GitHubQueryExecutor = Pick; export type GitHubFetcherDependencies = { @@ -341,15 +311,6 @@ function isObject(value: unknown): value is Record { return typeof value === "object" && value !== null; } -function isNumericRecord(value: unknown): value is Record { - return ( - isObject(value) && - Object.values(value).every( - (item) => typeof item === "number" && Number.isFinite(item), - ) - ); -} - function isGitHubUserData(value: unknown): value is GitHubUserData { if (!isObject(value)) { return false; @@ -759,14 +720,6 @@ function getStaleDays(): number { return Number.isFinite(parsed) && parsed > 0 ? parsed : 14; } -/** - * Fetch fresh user data from the GitHub API. - */ -async function fetchFromGitHubApi(username: string): Promise { - const { data } = await fetchUserDataFromGitHub(executorSingleton, username); - return data; -} - /** * Unified function to get GitHub user data. * diff --git a/scripts/calculate-next-country.ts b/scripts/calculate-next-country.ts index b247809..d97a399 100644 --- a/scripts/calculate-next-country.ts +++ b/scripts/calculate-next-country.ts @@ -20,7 +20,7 @@ import "dotenv/config"; import { getDatabaseStore } from "@/lib/db-store"; -import { calculateLeaderboard, type LeaderboardResult } from "@/lib/calculate-leaderboard"; +import { calculateLeaderboard } from "@/lib/calculate-leaderboard"; import { logger } from "@/lib/logger"; async function main() { From 6570de01389e1df2a5ba067977575548d63f07ef Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:17:04 +0300 Subject: [PATCH 09/10] chore: add leaderboard scheduler workflow for calculating next country leaderboard --- .github/workflows/leaderboard-scheduler.yml | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/leaderboard-scheduler.yml diff --git a/.github/workflows/leaderboard-scheduler.yml b/.github/workflows/leaderboard-scheduler.yml new file mode 100644 index 0000000..da8108a --- /dev/null +++ b/.github/workflows/leaderboard-scheduler.yml @@ -0,0 +1,59 @@ +name: Leaderboard Scheduler + +on: + schedule: + - cron: "0 17 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: leaderboard-scheduler + cancel-in-progress: false + +jobs: + calculate-next-country: + name: Calculate next country leaderboard + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN_DEVIMPACT }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + LEADERBOARD_SOURCE_URL_TEMPLATE: ${{ vars.LEADERBOARD_SOURCE_URL_TEMPLATE || 'https://raw.githubusercontent.com/ashkulz/committers.top/gh-pages/_data/locations/{country}.yml' }} + GITHUB_REPO_COUNT: ${{ vars.GITHUB_REPO_COUNT || '50' }} + GITHUB_PR_COUNT: ${{ vars.GITHUB_PR_COUNT || '300' }} + GITHUB_ISSUE_COUNT: ${{ vars.GITHUB_ISSUE_COUNT || '100' }} + GITHUB_DISCUSSION_COUNT: ${{ vars.GITHUB_DISCUSSION_COUNT || '50' }} + REDIS_URL: ${{ secrets.REDIS_URL }} + REDIS_ENABLED: ${{ vars.REDIS_ENABLED || 'false' }} + REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }} + REDIS_CACHE_NAMESPACE: ${{ vars.REDIS_CACHE_NAMESPACE || 'devimpact:v1' }} + REDIS_CACHE_TTL_SECONDS: ${{ vars.REDIS_CACHE_TTL_SECONDS || '604800' }} + REDIS_CONNECT_TIMEOUT_MS: ${{ vars.REDIS_CONNECT_TIMEOUT_MS || '1500' }} + LEADERBOARD_SEED_LIMIT: ${{ vars.LEADERBOARD_SEED_LIMIT || '256' }} + LEADERBOARD_REFRESH_LIMIT: ${{ vars.LEADERBOARD_REFRESH_LIMIT || '500' }} + LEADERBOARD_USER_STALE_DAYS: ${{ vars.LEADERBOARD_USER_STALE_DAYS || '30' }} + GITHUB_USER_STALE_DAYS: ${{ vars.GITHUB_USER_STALE_DAYS || '14' }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.29.2 + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24.x + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Calculate next country + run: pnpm leaderboard:calculate From a2a25c6a54f43eef19bfef88c981cb6ed2a22f38 Mon Sep 17 00:00:00 2001 From: Osama Mabkhot <99215291+O2sa@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:25:31 +0300 Subject: [PATCH 10/10] refactor: implement lazy initialization for GitHub executor singleton --- lib/github.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/github.ts b/lib/github.ts index eeca40a..c86e086 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -697,10 +697,18 @@ function createDefaultGitHubExecutor(): GitHubQueryExecutor { }); } -const executorSingleton = createDefaultGitHubExecutor(); +let executorSingleton: GitHubQueryExecutor | undefined; const cacheConfigSingleton = getCacheConfigFromEnv(); const cacheStoreSingleton = createCacheStore(cacheConfigSingleton); +function getDefaultGitHubExecutor(): GitHubQueryExecutor { + if (!executorSingleton) { + executorSingleton = createDefaultGitHubExecutor(); + } + + return executorSingleton; +} + /** * Redis cache entry shape that includes a fetch timestamp for staleness checks. */ @@ -811,7 +819,10 @@ export async function getUserData( } // ── 3. Fetch from GitHub API ────────────────────────────────────────── - const { data: fresh, metrics } = await fetchUserDataFromGitHub(executorSingleton, normalizedUsername); + const { data: fresh, metrics } = await fetchUserDataFromGitHub( + getDefaultGitHubExecutor(), + normalizedUsername, + ); // Upsert into PostgreSQL try {