diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx
index 0c0abd9..401c584 100644
--- a/components/theme-toggle.tsx
+++ b/components/theme-toggle.tsx
@@ -6,7 +6,7 @@ import { useSyncExternalStore } from "react";
import { useTranslation } from "./language-provider";
import { Button } from "./ui/button";
-const emptySubscribe = () => () => { };
+const emptySubscribe = () => () => {};
type ViewTransitionDocument = Document & {
startViewTransition?: (callback: () => void) => { finished: Promise
};
@@ -15,7 +15,11 @@ type ViewTransitionDocument = Document & {
export function ThemeToggle() {
const { t } = useTranslation();
const { theme, setTheme, resolvedTheme } = useTheme();
- const mounted = useSyncExternalStore(emptySubscribe, () => true, () => false);
+ const mounted = useSyncExternalStore(
+ emptySubscribe,
+ () => true,
+ () => false,
+ );
const getThemeTransitionMs = () => {
if (typeof window === "undefined") {
return 420;
diff --git a/components/top-list.tsx b/components/top-list.tsx
index 1d5e35b..4e87596 100644
--- a/components/top-list.tsx
+++ b/components/top-list.tsx
@@ -8,13 +8,7 @@ import {
MessageSquare,
Star,
} from "lucide-react";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "./ui/card";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
import { UserResult } from "@/types/user-result";
import { useTranslation } from "./language-provider";
@@ -61,15 +55,7 @@ function getLanguageColor(name: string): string {
return "bg-slate-500";
}
-function StatChip({
- icon,
- label,
- value,
-}: {
- icon: ReactNode;
- label: string;
- value: number;
-}) {
+function StatChip({ icon, label, value }: { icon: ReactNode; label: string; value: number }) {
return (
{icon}
@@ -79,18 +65,12 @@ function StatChip({
);
}
-function LanguageBreakdown({
- topLanguages,
-}: {
- topLanguages?: LanguageEntry[];
-}) {
+function LanguageBreakdown({ topLanguages }: { topLanguages?: LanguageEntry[] }) {
if (!topLanguages || topLanguages.length === 0) {
return null;
}
- const normalized = topLanguages
- .slice(0, 5)
- .filter((language) => language.percentage > 0);
+ const normalized = topLanguages.slice(0, 5).filter((language) => language.percentage > 0);
if (normalized.length === 0) {
return null;
@@ -116,9 +96,7 @@ function LanguageBreakdown({
key={`legend-${language.name}-${language.percentage}`}
className="inline-flex items-center gap-1"
>
-
+
{language.name} {language.percentage}%
@@ -187,9 +165,7 @@ function findPrLanguageMeta(
pr: UserResult["topPullRequests"][number],
): LanguageMeta {
const languagePrs = user.languageScores?.topPullRequests ?? [];
- const byUrl = pr.url
- ? languagePrs.find((item) => item.url && item.url === pr.url)
- : undefined;
+ const byUrl = pr.url ? languagePrs.find((item) => item.url && item.url === pr.url) : undefined;
const byTitleAndRepo = languagePrs.find(
(item) => item.title === pr.title && item.repo === pr.repo,
);
@@ -275,7 +251,8 @@ export function TopList({ userResults, selectedLanguages = [] }: Props) {
{typeof languageMeta.languageMatch === "number" ? (
- {t("language.match")}: {formatLanguageMatch(languageMeta.languageMatch)}
+ {t("language.match")}:{" "}
+ {formatLanguageMatch(languageMeta.languageMatch)}
) : null}
@@ -358,7 +335,8 @@ export function TopList({ userResults, selectedLanguages = [] }: Props) {
{typeof languageMeta.languageMatch === "number" ? (
- {t("language.match")}: {formatLanguageMatch(languageMeta.languageMatch)}
+ {t("language.match")}:{" "}
+ {formatLanguageMatch(languageMeta.languageMatch)}
) : null}
diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx
index f99164e..90d0a68 100644
--- a/components/ui/alert.tsx
+++ b/components/ui/alert.tsx
@@ -1,7 +1,7 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
@@ -16,8 +16,8 @@ const alertVariants = cva(
defaultVariants: {
variant: "default",
},
- }
-)
+ },
+);
function Alert({
className,
@@ -31,36 +31,30 @@ function Alert({
className={cn(alertVariants({ variant }), className)}
{...props}
/>
- )
+ );
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
- )
+ );
}
-function AlertDescription({
- className,
- ...props
-}: React.ComponentProps<"div">) {
+function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
- )
+ );
}
-export { Alert, AlertTitle, AlertDescription }
+export { Alert, AlertTitle, AlertDescription };
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
index bec75c3..519b843 100644
--- a/components/ui/button.tsx
+++ b/components/ui/button.tsx
@@ -24,17 +24,11 @@ const buttonVariants = cva(
variant: "primary",
size: "md",
},
- }
+ },
);
-type ButtonProps = ButtonHTMLAttributes
&
- VariantProps;
+type ButtonProps = ButtonHTMLAttributes & VariantProps;
export function Button({ className, variant, size, ...props }: ButtonProps) {
- return (
-
- );
-}
\ No newline at end of file
+ return ;
+}
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
index a733720..3f6c849 100644
--- a/components/ui/card.tsx
+++ b/components/ui/card.tsx
@@ -1,6 +1,6 @@
-import * as React from "react"
+import * as React from "react";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Card({
className,
@@ -12,12 +12,12 @@ function Card({
data-slot="card"
data-size={size}
className={cn(
- "group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
- className
+ "group/card has-data-[slot=card-footer]:pb-0 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3",
+ className,
)}
{...props}
/>
- )
+ );
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -25,12 +25,12 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
- )
+ );
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
@@ -38,12 +38,12 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
- )
+ );
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
@@ -53,20 +53,17 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
- )
+ );
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
- )
+ );
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -76,7 +73,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
- )
+ );
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -85,19 +82,11 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
-export {
- Card,
- CardHeader,
- CardFooter,
- CardTitle,
- CardAction,
- CardDescription,
- CardContent,
-}
+export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
index e969dd9..ecbf9eb 100644
--- a/components/ui/input.tsx
+++ b/components/ui/input.tsx
@@ -10,7 +10,7 @@ const Input = React.forwardRef>(
type={type}
data-slot="input"
className={cn(
- "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
+ "focus-visible:ring-3 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base outline-none transition-colors file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 dark:bg-input/30 dark:disabled:bg-input/80 md:text-sm",
className,
)}
{...props}
diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx
index 584011b..588d3bd 100644
--- a/components/ui/progress.tsx
+++ b/components/ui/progress.tsx
@@ -1,9 +1,9 @@
-"use client"
+"use client";
-import * as React from "react"
-import { Progress as ProgressPrimitive } from "radix-ui"
+import * as React from "react";
+import { Progress as ProgressPrimitive } from "radix-ui";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Progress({
className,
@@ -15,7 +15,7 @@ function Progress({
data-slot="progress"
className={cn(
"relative flex h-1 w-full items-center overflow-x-hidden rounded-full bg-muted",
- className
+ className,
)}
{...props}
>
@@ -25,7 +25,7 @@ function Progress({
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
- )
+ );
}
-export { Progress }
+export { Progress };
diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx
index 8fc953e..a8a4d08 100644
--- a/components/ui/skeleton.tsx
+++ b/components/ui/skeleton.tsx
@@ -1,4 +1,4 @@
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
@@ -10,7 +10,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
)}
{...props}
/>
- )
+ );
}
-export { Skeleton }
+export { Skeleton };
diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx
index bb1ea52..2db89a1 100644
--- a/components/ui/tooltip.tsx
+++ b/components/ui/tooltip.tsx
@@ -1,9 +1,9 @@
-"use client"
+"use client";
-import * as React from "react"
-import { Tooltip as TooltipPrimitive } from "radix-ui"
+import * as React from "react";
+import { Tooltip as TooltipPrimitive } from "radix-ui";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function TooltipProvider({
delayDuration = 0,
@@ -15,19 +15,15 @@ function TooltipProvider({
delayDuration={delayDuration}
{...props}
/>
- )
+ );
}
-function Tooltip({
- ...props
-}: React.ComponentProps) {
- return
+function Tooltip({ ...props }: React.ComponentProps) {
+ return ;
}
-function TooltipTrigger({
- ...props
-}: React.ComponentProps) {
- return
+function TooltipTrigger({ ...props }: React.ComponentProps) {
+ return ;
}
function TooltipContent({
@@ -42,8 +38,8 @@ function TooltipContent({
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
- "z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
- className
+ "origin-(--radix-tooltip-content-transform-origin) has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 inline-flex w-fit max-w-xs items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background",
+ className,
)}
{...props}
>
@@ -51,7 +47,7 @@ function TooltipContent({
- )
+ );
}
-export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
+export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
diff --git a/data/countries.json b/data/countries.json
index 25f8139..577fec2 100644
--- a/data/countries.json
+++ b/data/countries.json
@@ -1,152 +1,2280 @@
[
- { "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": "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": "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": "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": "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": "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": "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": "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": "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": "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
+ {
+ "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"]
+ }
+]
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 6d22b8e..cebadf8 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,17 +1,13 @@
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import nextTypescript from "eslint-config-next/typescript";
+import eslintConfigPrettier from "eslint-config-prettier";
const eslintConfig = [
...nextCoreWebVitals,
...nextTypescript,
+ eslintConfigPrettier,
{
- ignores: [
- "node_modules/**",
- ".next/**",
- "out/**",
- "build/**",
- "next-env.d.ts",
- ],
+ ignores: ["node_modules/**", ".next/**", "out/**", "build/**", "next-env.d.ts"],
},
];
diff --git a/github-issues.json b/github-issues.json
deleted file mode 100644
index d75818f..0000000
--- a/github-issues.json
+++ /dev/null
@@ -1,8 +0,0 @@
-[
-
- {
- "title": "refactor: Extract ApiResponse type to the types directory",
- "body": "The `ApiResponse` type is currently defined directly inside `app/page.tsx`. To improve code organization and make the type reusable across other files (such as API routes), it should be extracted into its own file within the `types` directory.\n\n### Tasks\n- [ ] Create a new file `types/api-response.ts`.\n- [ ] Move the `ApiResponse` type definition from `app/page.tsx` into this new file and `export` it.\n- [ ] Update `app/page.tsx` to import the `ApiResponse` type from the new file.",
- "labels": "help wanted,good first issue,refactor,easy,beginner friendly"
- }
-]
\ No newline at end of file
diff --git a/lib/cache-store.ts b/lib/cache-store.ts
index d31c84c..9628ce0 100644
--- a/lib/cache-store.ts
+++ b/lib/cache-store.ts
@@ -44,9 +44,7 @@ function parsePositiveInt(
return parsed;
}
-export function getCacheTtlSecondsFromEnv(
- env: NodeJS.ProcessEnv = process.env,
-): number {
+export function getCacheTtlSecondsFromEnv(env: NodeJS.ProcessEnv = process.env): number {
return (
parsePositiveInt(env.REDIS_CACHE_TTL_SECONDS, MAX_CACHE_TTL_SECONDS) ??
parsePositiveInt(env.CACHE_TTL_SECONDS, MAX_CACHE_TTL_SECONDS) ??
@@ -54,19 +52,13 @@ export function getCacheTtlSecondsFromEnv(
);
}
-export function getCacheNamespaceFromEnv(
- env: NodeJS.ProcessEnv = process.env,
-): string {
+export function getCacheNamespaceFromEnv(env: NodeJS.ProcessEnv = process.env): string {
return (
- env.REDIS_CACHE_NAMESPACE?.trim() ||
- env.CACHE_NAMESPACE?.trim() ||
- DEFAULT_CACHE_NAMESPACE
+ env.REDIS_CACHE_NAMESPACE?.trim() || env.CACHE_NAMESPACE?.trim() || DEFAULT_CACHE_NAMESPACE
);
}
-export function getCacheConfigFromEnv(
- env: NodeJS.ProcessEnv = process.env,
-): CacheConfig {
+export function getCacheConfigFromEnv(env: NodeJS.ProcessEnv = process.env): CacheConfig {
const redisUrl = env.REDIS_URL?.trim() || undefined;
const enabledFromEnv = parseBoolean(env.REDIS_ENABLED);
const enabled = enabledFromEnv ?? Boolean(redisUrl);
diff --git a/lib/calculate-leaderboard.ts b/lib/calculate-leaderboard.ts
index 014eee0..1987526 100644
--- a/lib/calculate-leaderboard.ts
+++ b/lib/calculate-leaderboard.ts
@@ -139,7 +139,10 @@ export async function seedNewUsers(
continue;
}
- const { data, metrics } = await getUserData(user.login, { cacheInRedis: false, withMetrics: true });
+ 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);
@@ -190,7 +193,10 @@ export async function refreshStaleUsers(
}
try {
- const { data, metrics } = await getUserData(row.username, { cacheInRedis: false, withMetrics: true });
+ 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);
@@ -301,13 +307,11 @@ export async function calculateLeaderboard(
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,
- );
+ 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;
@@ -316,13 +320,18 @@ export async function calculateLeaderboard(
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;
+ 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 }] }));
+ 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 });
+ if (m.errors.length > 0)
+ userFetchErrors.push({ username: allErrors[i] ?? "unknown", errors: m.errors });
});
meta.userFetchErrors = userFetchErrors;
diff --git a/lib/compare-request.ts b/lib/compare-request.ts
index 49710c5..314096b 100644
--- a/lib/compare-request.ts
+++ b/lib/compare-request.ts
@@ -40,9 +40,7 @@ export function createComparisonRequest(
): ComparisonPresentationRequest {
const sanitizedLanguages = sanitizeSelectedLanguages(selectedLanguages);
const canonicalUsers = [normalizeUsername(user1), normalizeUsername(user2)].sort();
- const canonicalLanguages = sanitizedLanguages
- .map((language) => language.toLowerCase())
- .sort();
+ const canonicalLanguages = sanitizedLanguages.map((language) => language.toLowerCase()).sort();
return {
user1: user1.trim(),
user2: user2.trim(),
diff --git a/lib/country-flags.ts b/lib/country-flags.ts
index 5fc8c3b..0724fcc 100644
--- a/lib/country-flags.ts
+++ b/lib/country-flags.ts
@@ -22,4 +22,4 @@ for (const entry of countries as CountryEntry[]) {
*/
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 a9882a7..ab68d82 100644
--- a/lib/db-store.ts
+++ b/lib/db-store.ts
@@ -174,7 +174,7 @@ export class DatabaseStore {
async finishCalculation(countrySlug: string, errorMessage?: string): Promise {
const client = getPool();
- const status = errorMessage ? 'failed' : 'done';
+ const status = errorMessage ? "failed" : "done";
await client.query(
`UPDATE leaderboard_calculation SET
status = $1,
@@ -256,10 +256,7 @@ export class DatabaseStore {
// ββ Leaderboard operations ββββββββββββββββββββββββββββββββββββββββββ
- async getLeaderboard(
- country: string,
- limit: number = 500,
- ): Promise {
+ async getLeaderboard(country: string, limit: number = 500): Promise {
const client = getPool();
const result = await client.query(
`SELECT *
@@ -289,10 +286,7 @@ export class DatabaseStore {
* 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 {
+ async getTopStaleUsers(country: string, limit: number = 500): Promise {
const client = getPool();
const result = await client.query(
`SELECT *
@@ -313,10 +307,7 @@ export class DatabaseStore {
* 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 {
+ async getTopUsers(country: string, limit: number = 500): Promise {
const client = getPool();
const result = await client.query(
`SELECT *
diff --git a/lib/github-graphql-client.ts b/lib/github-graphql-client.ts
index 2ed05a1..fcd2c63 100644
--- a/lib/github-graphql-client.ts
+++ b/lib/github-graphql-client.ts
@@ -49,9 +49,7 @@ function normalizeMessage(value: string): string {
}
function messageIncludes(messages: string[], tokens: string[]): boolean {
- return messages.some((message) =>
- tokens.some((token) => message.includes(token)),
- );
+ return messages.some((message) => tokens.some((token) => message.includes(token)));
}
function isPrimaryRateLimit(
@@ -113,21 +111,12 @@ export function classifyGitHubError(input: {
}
if (
- messageIncludes(messages, [
- "couldn't respond to your request in time",
- "timed out",
- "timeout",
- ])
+ messageIncludes(messages, ["couldn't respond to your request in time", "timed out", "timeout"])
) {
return "TIMEOUT";
}
- if (
- messageIncludes(messages, [
- "resource limits were exceeded",
- "resource limit exceeded",
- ])
- ) {
+ if (messageIncludes(messages, ["resource limits were exceeded", "resource limit exceeded"])) {
return "RESOURCE_LIMIT";
}
@@ -290,23 +279,19 @@ 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<
- TData,
- TVariables extends Record,
- >(params: ExecuteQueryParams): Promise {
+ private async executeWithRetries>(
+ params: ExecuteQueryParams,
+ ): Promise {
for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
try {
await this.applyAdaptiveDelay();
return await this.executeOnce(params);
} catch (error: unknown) {
const normalizedError = this.normalizeError(error);
- const shouldRetry =
- normalizedError.retriable && attempt < this.maxRetries;
+ const shouldRetry = normalizedError.retriable && attempt < this.maxRetries;
console.warn("github_graphql_error", {
operationName: params.operationName,
@@ -370,14 +355,11 @@ export class GitHubGraphQLClient {
payload.errors
?.map((error) => error.message)
.filter(Boolean)
- .join(" | ") ||
- `GitHub GraphQL request failed with status ${response.status}`,
+ .join(" | ") || `GitHub GraphQL request failed with status ${response.status}`,
kind,
status: response.status,
rateLimit,
- retryAfterMs: rateLimit.retryAfterSeconds
- ? rateLimit.retryAfterSeconds * 1000
- : undefined,
+ retryAfterMs: rateLimit.retryAfterSeconds ? rateLimit.retryAfterSeconds * 1000 : undefined,
});
}
@@ -403,8 +385,7 @@ 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",
@@ -420,14 +401,8 @@ export class GitHubGraphQLClient {
if (rateLimit.remaining === 0 && rateLimit.resetAt) {
delayMs = Math.max(delayMs, rateLimit.resetAt * 1000 - now);
- } else if (
- typeof rateLimit.remaining === "number" &&
- typeof rateLimit.resetAt === "number"
- ) {
- const secondsToReset = Math.max(
- 1,
- rateLimit.resetAt - Math.floor(now / 1000),
- );
+ } else if (typeof rateLimit.remaining === "number" && typeof rateLimit.resetAt === "number") {
+ const secondsToReset = Math.max(1, rateLimit.resetAt - Math.floor(now / 1000));
const budgetPerSecond = rateLimit.remaining / secondsToReset;
if (budgetPerSecond < 0.25) {
@@ -477,24 +452,21 @@ 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.test.ts b/lib/github.test.ts
index 699c7d9..430ba42 100644
--- a/lib/github.test.ts
+++ b/lib/github.test.ts
@@ -1,9 +1,7 @@
import "dotenv/config";
-
-import {describe, expect, it} from "vitest";
-import {parseCountEnv} from "./github";
-
+import { describe, expect, it } from "vitest";
+import { parseCountEnv } from "./github";
describe("parseCountEnv", () => {
it("uses fallback for undefined", () => {
@@ -29,4 +27,4 @@ describe("parseCountEnv", () => {
it("clamps values above the maximum", () => {
expect(parseCountEnv("500", 30, 100)).toBe(100);
});
-});
\ No newline at end of file
+});
diff --git a/lib/github.ts b/lib/github.ts
index c86e086..3a2f565 100644
--- a/lib/github.ts
+++ b/lib/github.ts
@@ -20,7 +20,7 @@ export type UserFetchMetrics = {
type Logger = Pick;
-type GitHubRawUser = {
+type GitHubRawUser = {
login: string;
name: string | null;
avatarUrl: string;
@@ -74,7 +74,6 @@ const DEFAULT_GITHUB_PR_COUNT = 300;
const DEFAULT_GITHUB_ISSUE_COUNT = 100;
const DEFAULT_GITHUB_DISCUSSION_COUNT = 50;
-
const MAX_GITHUB_REPO_COUNT = 100;
const MAX_GITHUB_PR_COUNT = 1000;
const MAX_GITHUB_ISSUE_COUNT = 100;
@@ -84,14 +83,12 @@ export function parseCountEnv(
value: string | undefined,
fallback: number,
maxValue: number,
-): number{
-
+): number {
const parsed = Number.parseInt(value ?? "", 10);
- if(!Number.isInteger(parsed)|| parsed<=0){
+ if (!Number.isInteger(parsed) || parsed <= 0) {
return fallback;
}
return Math.min(parsed, maxValue);
-
}
const USER_QUERY = /* GraphQL */ `
@@ -171,9 +168,21 @@ const PULL_REQUESTS_QUERY = /* GraphQL */ `
`;
const ISSUES_QUERY = /* GraphQL */ `
- query FetchUserIssues($issueCount: Int = 100, $externalIssueQuery: String!, $issueCursor: String) {
- issues: search(query: $externalIssueQuery, type: ISSUE, first: $issueCount, after: $issueCursor) {
- pageInfo { hasNextPage endCursor }
+ 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
@@ -206,7 +215,10 @@ const DISCUSSIONS_QUERY = /* GraphQL */ `
first: $discussionCount
after: $discussionCursor
) {
- pageInfo { hasNextPage endCursor }
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
nodes {
... on Discussion {
title
@@ -235,10 +247,12 @@ type SearchPaginateParams = {
operationName: string;
query: string;
buildVariables: (cursor: string | null, pageSize: number) => Record;
- extractField: (data: Record) => {
- nodes: Array;
- pageInfo: PageInfo;
- } | undefined;
+ extractField: (data: Record) =>
+ | {
+ nodes: Array;
+ pageInfo: PageInfo;
+ }
+ | undefined;
maxPages?: number;
maxItems?: number;
};
@@ -263,15 +277,10 @@ async function paginateSearch(
}
const pageSize =
- remainingItems === undefined
- ? SEARCH_PAGE_SIZE
- : Math.min(SEARCH_PAGE_SIZE, remainingItems);
+ remainingItems === undefined ? SEARCH_PAGE_SIZE : Math.min(SEARCH_PAGE_SIZE, remainingItems);
const variables = params.buildVariables(cursor, pageSize);
- const data = await executor.execute<
- Record,
- Record
- >({
+ const data = await executor.execute, Record>({
operationName: params.operationName,
query: params.query,
variables,
@@ -321,7 +330,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)
+ !Array.isArray(candidate.pullRequests)
) {
return false;
}
@@ -329,10 +338,7 @@ function isGitHubUserData(value: unknown): value is GitHubUserData {
if (candidate.issues !== undefined && !Array.isArray(candidate.issues)) {
return false;
}
- if (
- candidate.discussions !== undefined &&
- !Array.isArray(candidate.discussions)
- ) {
+ if (candidate.discussions !== undefined && !Array.isArray(candidate.discussions)) {
return false;
}
@@ -361,15 +367,10 @@ export function normalizeGitHubUsername(username: string): string {
return username.trim().toLowerCase();
}
-export function buildGitHubUserCacheKey(
- username: string,
- namespace: string,
-): string {
+export function buildGitHubUserCacheKey(username: string, namespace: string): string {
return `${namespace}:github-user:${normalizeGitHubUsername(username)}`;
}
-
-
// ---------------------------------------------------------------------------
// Core data fetch
// ---------------------------------------------------------------------------
@@ -385,36 +386,32 @@ async function fetchUserDataFromGitHub(
const startTime = performance.now();
const fetchErrors: { part: string; reason: string }[] = [];
-
-const repoCount = parseCountEnv(
- process.env.GITHUB_REPO_COUNT,
- DEFAULT_GITHUB_REPO_COUNT,
- MAX_GITHUB_REPO_COUNT,
-);
-
-const prCount = parseCountEnv(
- process.env.GITHUB_PR_COUNT,
- DEFAULT_GITHUB_PR_COUNT,
- MAX_GITHUB_PR_COUNT,
-);
-
-const issueCount = parseCountEnv(
- process.env.GITHUB_ISSUE_COUNT,
- DEFAULT_GITHUB_ISSUE_COUNT,
- MAX_GITHUB_ISSUE_COUNT,
-);
-
-const discussionCount = parseCountEnv(
- process.env.GITHUB_DISCUSSION_COUNT,
- DEFAULT_GITHUB_DISCUSSION_COUNT,
- MAX_GITHUB_DISCUSSION_COUNT,
-);
+ const repoCount = parseCountEnv(
+ process.env.GITHUB_REPO_COUNT,
+ DEFAULT_GITHUB_REPO_COUNT,
+ MAX_GITHUB_REPO_COUNT,
+ );
+
+ const prCount = parseCountEnv(
+ process.env.GITHUB_PR_COUNT,
+ DEFAULT_GITHUB_PR_COUNT,
+ MAX_GITHUB_PR_COUNT,
+ );
+
+ const issueCount = parseCountEnv(
+ process.env.GITHUB_ISSUE_COUNT,
+ DEFAULT_GITHUB_ISSUE_COUNT,
+ MAX_GITHUB_ISSUE_COUNT,
+ );
+
+ const discussionCount = parseCountEnv(
+ process.env.GITHUB_DISCUSSION_COUNT,
+ DEFAULT_GITHUB_DISCUSSION_COUNT,
+ MAX_GITHUB_DISCUSSION_COUNT,
+ );
const [userResult, prResult, issuesResult, discussionsResult] = await Promise.allSettled([
- executor.execute<
- { user: GitHubRawUser | null },
- { login: string; repoCount: number }
- >({
+ executor.execute<{ user: GitHubRawUser | null }, { login: string; repoCount: number }>({
operationName: "FetchUser",
query: USER_QUERY,
variables: { login: username, repoCount },
@@ -429,8 +426,7 @@ const discussionCount = parseCountEnv(
}),
extractField: (data) =>
data.pullRequests as
- | { nodes: Array; pageInfo: PageInfo }
- | undefined,
+ { nodes: Array; pageInfo: PageInfo } | undefined,
maxItems: prCount,
}),
paginateSearch(executor, {
@@ -442,9 +438,7 @@ const discussionCount = parseCountEnv(
issueCursor: cursor,
}),
extractField: (data) =>
- data.issues as
- | { nodes: Array; pageInfo: PageInfo }
- | undefined,
+ data.issues as { nodes: Array; pageInfo: PageInfo } | undefined,
maxItems: issueCount,
}),
paginateSearch(executor, {
@@ -457,23 +451,34 @@ const discussionCount = parseCountEnv(
}),
extractField: (data) =>
data.discussions as
- | { nodes: Array; pageInfo: PageInfo }
- | undefined,
+ { nodes: Array; pageInfo: PageInfo } | undefined,
maxItems: discussionCount,
}),
]);
if (userResult.status === "rejected") {
- fetchErrors.push({ part: "user", reason: userResult.reason?.message ?? String(userResult.reason) });
+ 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) });
+ 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) });
+ 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) });
+ fetchErrors.push({
+ part: "discussions",
+ reason: discussionsResult.reason?.message ?? String(discussionsResult.reason),
+ });
}
if (userResult.status === "rejected") {
@@ -492,14 +497,14 @@ const discussionCount = parseCountEnv(
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),
+ 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 = {
@@ -526,7 +531,10 @@ async function fetchUserDataFromGitHubWrapper(
export function createGitHubUserDataFetcherWithMetrics(
dependencies: GitHubFetcherDependencies,
): (username: string) => Promise<{ data: GitHubUserData; metrics: UserFetchMetrics }> {
- const inFlightByCacheKey = new Map>();
+ const inFlightByCacheKey = new Map<
+ string,
+ Promise<{ data: GitHubUserData; metrics: UserFetchMetrics }>
+ >();
const logger = dependencies.logger ?? console;
return async (username: string): Promise<{ data: GitHubUserData; metrics: UserFetchMetrics }> => {
@@ -546,7 +554,12 @@ export function createGitHubUserDataFetcherWithMetrics(
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)) {
+ 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 };
}
@@ -570,10 +583,7 @@ export function createGitHubUserDataFetcherWithMetrics(
}
const request = (async () => {
- const freshResult = await fetchUserDataFromGitHub(
- dependencies.executor,
- normalizedUsername,
- );
+ const freshResult = await fetchUserDataFromGitHub(dependencies.executor, normalizedUsername);
if (dependencies.cacheStore.enabled) {
try {
@@ -649,18 +659,11 @@ export function createGitHubUserDataFetcher(
}
const request = (async () => {
- const fresh = await fetchUserDataFromGitHubWrapper(
- dependencies.executor,
- normalizedUsername,
- );
+ const fresh = await fetchUserDataFromGitHubWrapper(dependencies.executor, normalizedUsername);
if (dependencies.cacheStore.enabled) {
try {
- await dependencies.cacheStore.set(
- cacheKey,
- fresh,
- dependencies.cacheConfig.ttlSeconds,
- );
+ await dependencies.cacheStore.set(cacheKey, fresh, dependencies.cacheConfig.ttlSeconds);
} catch (error: unknown) {
logger.warn("cache-set-fail", {
key: cacheKey,
diff --git a/lib/i18n-core.ts b/lib/i18n-core.ts
index 80063d6..28c4cbf 100644
--- a/lib/i18n-core.ts
+++ b/lib/i18n-core.ts
@@ -15,7 +15,7 @@ export function isSupportedLocale(value: string | null | undefined): value is Lo
export function parseAcceptLanguage(
header: string | null | undefined,
supported: readonly T[],
- fallback: T
+ fallback: T,
): T {
if (!header) return fallback;
diff --git a/lib/i18n.ts b/lib/i18n.ts
index 421e82b..3478459 100644
--- a/lib/i18n.ts
+++ b/lib/i18n.ts
@@ -81,7 +81,7 @@ export function useI18nProvider(initialLocale: Locale = DEFAULT_LOCALE) {
(next: Locale) => {
changeLocale(next);
},
- [changeLocale]
+ [changeLocale],
);
const t = useCallback(
@@ -93,16 +93,16 @@ export function useI18nProvider(initialLocale: Locale = DEFAULT_LOCALE) {
if (!params) return template;
return Object.keys(params).reduce(
(acc, k) => acc.replace(`{${k}}`, String(params[k])),
- template
+ template,
);
},
- [messages, locale, ready]
+ [messages, locale, ready],
);
const dir = useMemo(() => localeMeta[locale]?.dir ?? "ltr", [locale]);
const locales = useMemo(
() => supportedLocales.map((lc) => ({ value: lc, label: localeMeta[lc].label })),
- []
+ [],
);
return { locale, setLocale, t, dir, locales, ready };
diff --git a/lib/leaderboard.ts b/lib/leaderboard.ts
index f5dab77..ee13438 100644
--- a/lib/leaderboard.ts
+++ b/lib/leaderboard.ts
@@ -44,9 +44,7 @@ function getCachedLeaderboard(
};
}
-export async function getLeaderboardResult(
- country: string,
-): Promise {
+export async function getLeaderboardResult(country: string): Promise {
const displayLimit = getDisplayLimit();
const cacheConfig = getCacheConfigFromEnv();
const cacheStore = createCacheStore(cacheConfig);
diff --git a/lib/location-detector.ts b/lib/location-detector.ts
index ce60a69..0b2743d 100644
--- a/lib/location-detector.ts
+++ b/lib/location-detector.ts
@@ -61,4 +61,4 @@ export function detectCountry(location: string | null): string | null {
}
return null;
-}
\ No newline at end of file
+}
diff --git a/lib/logger.ts b/lib/logger.ts
index 4185b6a..8689339 100644
--- a/lib/logger.ts
+++ b/lib/logger.ts
@@ -26,4 +26,4 @@ export const logger = {
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 0e25d01..fb995da 100644
--- a/lib/score.ts
+++ b/lib/score.ts
@@ -1,9 +1,4 @@
-import type {
- DiscussionNode,
- IssueNode,
- PullRequestNode,
- RepoNode,
-} from "@/types/github";
+import type { DiscussionNode, IssueNode, PullRequestNode, RepoNode } from "@/types/github";
import type {
CommunityContributionDetail,
PullRequestScoreDetail,
@@ -111,10 +106,7 @@ function getDaysSince(dateValue: string, referenceDate: Date): number | null {
return Math.max(0, diff / MS_PER_DAY);
}
-function getRepoActivityFactor(
- pushedAt: string | undefined,
- referenceDate: Date,
-): number {
+function getRepoActivityFactor(pushedAt: string | undefined, referenceDate: Date): number {
if (!pushedAt) {
return 0.8;
}
@@ -275,9 +267,7 @@ function calculatePRScore(
};
}
-function calculateCommunityItemScore(
- item: IssueNode | DiscussionNode,
-): number {
+function calculateCommunityItemScore(item: IssueNode | DiscussionNode): number {
const repoStars = Math.max(0, item.repository.stargazerCount);
const comments = Math.max(0, item.comments.totalCount);
let score = safeLog(repoStars) * safeLog(comments);
@@ -324,9 +314,7 @@ function calculateContributionScore(
}
for (const discussion of discussions) {
- if (
- discussion.repository.owner.login.toLowerCase() === normalizedUsername
- ) {
+ if (discussion.repository.owner.login.toLowerCase() === normalizedUsername) {
continue;
}
@@ -424,10 +412,7 @@ function calculateLanguagePRScore(
>();
for (const item of prDetails) {
- const languageMatch = getLanguageMatch(
- item.pr.repository.languages,
- selectedLanguages,
- );
+ const languageMatch = getLanguageMatch(item.pr.repository.languages, selectedLanguages);
const languageFactor = getLanguageFactor(languageMatch);
const score = sanitizeNumber(item.score * languageFactor);
const key = item.pr.repository.nameWithOwner;
@@ -614,42 +599,28 @@ export function calculateUserScore(
);
let contributionScore = communityScore.total;
- contributionScore = Math.min(
- contributionScore,
- 0.3 * (repoScore.total + prScore.total),
- );
+ contributionScore = Math.min(contributionScore, 0.3 * (repoScore.total + prScore.total));
contributionScore = sanitizeNumber(contributionScore);
- const finalScore =
- repoScore.total * 0.45 + prScore.total * 0.45 + contributionScore * 0.1;
+ const finalScore = repoScore.total * 0.45 + prScore.total * 0.45 + contributionScore * 0.1;
const normalizedRepoScore = normalizeScore(repoScore.total, 100);
const normalizedPRScore = normalizeScore(prScore.total, 300);
const normalizedContributionScore = normalizeScore(contributionScore, 100);
const normalizedFinalScore =
- normalizedRepoScore * 0.45 +
- normalizedPRScore * 0.45 +
- normalizedContributionScore * 0.1;
+ normalizedRepoScore * 0.45 + normalizedPRScore * 0.45 + normalizedContributionScore * 0.1;
let languageScores: LanguageScores | undefined;
let languageRepoSignals: Pick<
ScoringSignals,
"reposWithLanguageData" | "averageRepoLanguageMatch"
> = {};
- let languagePRSignals: Pick<
- ScoringSignals,
- "prsWithLanguageData" | "averagePRLanguageMatch"
- > = {};
+ let languagePRSignals: Pick =
+ {};
if (hasSelectedLanguages) {
- const languageRepoScore = calculateLanguageRepoScore(
- repoScore.details,
- selectedLanguages,
- );
- const languagePRScore = calculateLanguagePRScore(
- prScore.details,
- selectedLanguages,
- );
+ const languageRepoScore = calculateLanguageRepoScore(repoScore.details, selectedLanguages);
+ const languagePRScore = calculateLanguagePRScore(prScore.details, selectedLanguages);
let languageContributionScore = contributionScore;
languageContributionScore = Math.min(
@@ -665,10 +636,7 @@ export function calculateUserScore(
const normalizedLanguageRepoScore = normalizeScore(languageRepoScore.total, 100);
const normalizedLanguagePRScore = normalizeScore(languagePRScore.total, 300);
- const normalizedLanguageContributionScore = normalizeScore(
- languageContributionScore,
- 100,
- );
+ const normalizedLanguageContributionScore = normalizeScore(languageContributionScore, 100);
const normalizedLanguageFinalScore =
normalizedLanguageRepoScore * 0.45 +
normalizedLanguagePRScore * 0.45 +
diff --git a/lib/scoring/languageScoring.ts b/lib/scoring/languageScoring.ts
index a6e7a04..853e358 100644
--- a/lib/scoring/languageScoring.ts
+++ b/lib/scoring/languageScoring.ts
@@ -30,9 +30,7 @@ export function normalizeSelectedLanguages(languages?: string[]): string[] {
return unique;
}
-export function getLanguageDistribution(
- languages?: RepoLanguages,
-): Record {
+export function getLanguageDistribution(languages?: RepoLanguages): Record {
const edges = languages?.edges ?? [];
if (edges.length === 0) {
return {};
@@ -82,10 +80,7 @@ export function getLanguageMatch(
return Math.max(0, Math.min(1, match));
}
-export function getLanguageFactor(
- languageMatch: number,
- minFactor = 0.25,
-): number {
+export function getLanguageFactor(languageMatch: number, minFactor = 0.25): number {
const boundedMatch = Math.max(0, Math.min(1, languageMatch));
const boundedMinFactor = Math.max(0, Math.min(1, minFactor));
return boundedMinFactor + (1 - boundedMinFactor) * boundedMatch;
diff --git a/lib/seo.ts b/lib/seo.ts
index 1455550..489cb56 100644
--- a/lib/seo.ts
+++ b/lib/seo.ts
@@ -25,10 +25,7 @@ export function getMetadataBase(env: NodeJS.ProcessEnv = process.env): URL {
return new URL(getSiteUrl(env));
}
-export function toAbsoluteUrl(
- path: string,
- env: NodeJS.ProcessEnv = process.env,
-): string {
+export function toAbsoluteUrl(path: string, env: NodeJS.ProcessEnv = process.env): string {
const safePath = path.startsWith("/") ? path : `/${path}`;
return new URL(safePath, getMetadataBase(env)).toString();
}
diff --git a/middleware.ts b/middleware.ts
index 284c84e..f0f6adb 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -16,7 +16,7 @@ export function middleware(request: NextRequest) {
const locale = parseAcceptLanguage(
request.headers.get("accept-language"),
supportedLocales,
- DEFAULT_LOCALE
+ DEFAULT_LOCALE,
);
response.cookies.set(LOCALE_COOKIE, locale, { path: "/" });
diff --git a/ops/README.md b/ops/README.md
index bc03de8..ce0d113 100644
--- a/ops/README.md
+++ b/ops/README.md
@@ -7,9 +7,11 @@ This directory contains the infrastructure, Docker configuration, cron job defin
## Overview
### What is the Leaderboard Worker?
+
The Leaderboard Worker is a standalone service that periodically fetches contributor metadata from the GitHub GraphQL/REST APIs, recalculates scores, and updates the shared PostgreSQL database and Redis cache.
### Why is it separate from the web application?
+
Calculating leaderboard scores involves heavy API querying, rate limit tracking, and database bulk operations. Running this work asynchronously via a background worker ensures that the Next.js web application remains fast, responsive, and unaffected by calculation spikes.
---
@@ -117,6 +119,7 @@ bash ops/deploy/deploy-leaderboard.sh
```
This script safely executes:
+
1. `docker compose -f ops/docker/leaderboard-compose.yml pull`
2. **Waits for any active calculation job to finish**: Checks if `devimpact-leaderboard-cron` is currently running a calculation job (`calculate-next-country`) and polls until the job completes naturally.
3. `docker compose -f ops/docker/leaderboard-compose.yml up -d --remove-orphans` once no calculation is running.
diff --git a/ops/docker/docker-compose.yml b/ops/docker/docker-compose.yml
index b35b5b4..0db027e 100644
--- a/ops/docker/docker-compose.yml
+++ b/ops/docker/docker-compose.yml
@@ -37,21 +37,14 @@ services:
"--maxmemory",
"512mb",
"--maxmemory-policy",
- "allkeys-lru"
+ "allkeys-lru",
]
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
- test:
- [
- "CMD",
- "redis-cli",
- "-a",
- "${REDIS_PASSWORD}",
- "ping"
- ]
+ test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 3s
retries: 5
diff --git a/package.json b/package.json
index e0a21d4..019332c 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,9 @@
"build": "next build",
"start": "next start",
"lint": "eslint .",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
+ "prepare": "husky",
"test": "vitest",
"test:watch": "vitest --watch",
"redis:up": "docker compose -f ops/docker/docker-compose.yml up -d redis",
@@ -17,6 +20,15 @@
"leaderboard:calculate": "tsx scripts/calculate-next-country.ts",
"validate-locales": "node scripts/validate-locales.js"
},
+ "lint-staged": {
+ "*.{js,jsx,ts,tsx}": [
+ "eslint --fix",
+ "prettier --write"
+ ],
+ "*.{json,md,yml,yaml,css,scss}": [
+ "prettier --write"
+ ]
+ },
"dependencies": {
"@octokit/graphql": "^9.0.3",
"class-variance-authority": "^0.7.0",
@@ -36,6 +48,8 @@
"tailwind-merge": "^2.5.3"
},
"devDependencies": {
+ "@commitlint/cli": "^21.2.2",
+ "@commitlint/config-conventional": "^21.2.2",
"@types/js-yaml": "^4.0.9",
"@types/node": "^25.5.2",
"@types/pg": "^8.20.0",
@@ -45,7 +59,12 @@
"dotenv": "^17.4.2",
"eslint": "^9.17.0",
"eslint-config-next": "^16.2.2",
+ "eslint-config-prettier": "^10.1.8",
+ "husky": "^9.1.7",
+ "lint-staged": "^17.3.0",
"postcss": "^8.5.24",
+ "prettier": "^3.9.6",
+ "prettier-plugin-tailwindcss": "^0.8.1",
"tailwindcss": "^3.4.14",
"tsx": "^3.12.7",
"typescript": "^6.0.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dbbe8db..5978f18 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -57,6 +57,12 @@ importers:
specifier: ^2.5.3
version: 2.6.1
devDependencies:
+ '@commitlint/cli':
+ specifier: ^21.2.2
+ version: 21.2.2(@types/node@25.9.5)(conventional-commits-parser@7.1.2)(typescript@6.0.3)
+ '@commitlint/config-conventional':
+ specifier: ^21.2.2
+ version: 21.2.2
'@types/js-yaml':
specifier: ^4.0.9
version: 4.0.9
@@ -84,12 +90,27 @@ importers:
eslint-config-next:
specifier: ^16.2.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)
+ eslint-config-prettier:
+ specifier: ^10.1.8
+ version: 10.1.8(eslint@9.39.5(jiti@1.21.7))
+ husky:
+ specifier: ^9.1.7
+ version: 9.1.7
+ lint-staged:
+ specifier: ^17.3.0
+ version: 17.3.0
postcss:
specifier: ^8.5.24
version: 8.5.26
+ prettier:
+ specifier: ^3.9.6
+ version: 3.9.6
+ prettier-plugin-tailwindcss:
+ specifier: ^0.8.1
+ version: 0.8.1(prettier@3.9.6)
tailwindcss:
specifier: ^3.4.14
- version: 3.4.19(tsx@3.14.0)
+ version: 3.4.19(tsx@3.14.0)(yaml@2.9.0)
tsx:
specifier: ^3.12.7
version: 3.14.0
@@ -98,7 +119,7 @@ importers:
version: 6.0.3
vitest:
specifier: ^4.1.5
- 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))
+ 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)(yaml@2.9.0))
packages:
@@ -177,6 +198,91 @@ packages:
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
+ '@commitlint/cli@21.2.2':
+ resolution: {integrity: sha512-a+6hQxIxnpdvSvS2apvttPNbEliYsVC3PqFYDiiB2kjbwIsQsj1urvQ4Tkf70pKYozPalKAuRQmm/GHwndduqA==}
+ engines: {node: '>=22.12.0'}
+ hasBin: true
+
+ '@commitlint/config-conventional@21.2.2':
+ resolution: {integrity: sha512-NxA37SZviusFUEYOQZ5hNnZ1h7O/KiemPkxjOlpzKJNnWxThiwc6/SaZhaPa8fyLvfRBAywhQhJJk8XESHWlpQ==}
+ engines: {node: '>=22.12.0'}
+
+ '@commitlint/config-validator@21.2.0':
+ resolution: {integrity: sha512-t7AzNHAKeIdo/3NRGwzpufKHsKkPHmFs/56N2Fnsh0/r0rGtnQzTxk6vnFgjaGr4hdSQKNB50/KAhR9Yk4LJKA==}
+ engines: {node: '>=22.12.0'}
+
+ '@commitlint/ensure@21.2.0':
+ resolution: {integrity: sha512-76IF9vDNS13lAzEEik9eKwzt8f9hYhWiwVXZ2AnyLCz5/f511FsEQ3pw1X3/zSQpdRLQU7i5qDMVKyXi1GWjSg==}
+ engines: {node: '>=22.12.0'}
+
+ '@commitlint/execute-rule@21.0.1':
+ resolution: {integrity: sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA==}
+ engines: {node: '>=22.12.0'}
+
+ '@commitlint/format@21.2.2':
+ resolution: {integrity: sha512-v6fvxZSc/AvVMROlr3H34+1766bZSYApRUSCAMjWamStPjKMvZ8GdvVA5YW/VQNgbFTmcMz6OYmSTJEvIjPrfA==}
+ engines: {node: '>=22.12.0'}
+
+ '@commitlint/is-ignored@21.2.2':
+ resolution: {integrity: sha512-9UoKNgfFE3LU7FrzierCvk3CdDfMDeVGC86qZiT/n0TIjfq/dmZ9MHuXd45OTNRa26ZanmJRxEtmiXk/lEJihg==}
+ engines: {node: '>=22.12.0'}
+
+ '@commitlint/lint@21.2.2':
+ resolution: {integrity: sha512-Fy8JxEBzdmsYWFude/61GxXu5O+wEymwiRK2z9GL9R8mCsXphCoGxAFc5iHn5mjlfcSrhiiONE+ksf4KOjnaPg==}
+ engines: {node: '>=22.12.0'}
+
+ '@commitlint/load@21.2.2':
+ resolution: {integrity: sha512-0Tt6wDPX167cjKC5D4zhm0+20wJJG+TN/TKovMOspfSe78rOnKX+MNzlVNiu6HyQPZChPJ8QBH31MVt6Bb8fCg==}
+ engines: {node: '>=22.12.0'}
+
+ '@commitlint/message@21.2.0':
+ resolution: {integrity: sha512-YxGoiXD/HXNXLJPrQwE5poXa+XH0CBEm+mdvbHQP0g6MV/dmJyUFCzPNzZbxL93GvZ70TmtTK0Z0/IBpAqHv8g==}
+ engines: {node: '>=22.12.0'}
+
+ '@commitlint/parse@21.2.2':
+ resolution: {integrity: sha512-MEkobPfvRp+z06Wro8HMG1BDGHzZmj82A1LH1nWeG3ipHpg/x4m6v3wEDvMBIKjRFUnfR3nBeFs3MVCr7UdAmg==}
+ engines: {node: '>=22.12.0'}
+
+ '@commitlint/read@21.2.1':
+ resolution: {integrity: sha512-hUW7EJQnNTL0vPOmVMNK4CrnrNBN0nN+JJHReFkdHO5y4iyHeEmTBwuC15OCqUTjxWo7idnH1LftfpWVIaPWIA==}
+ engines: {node: '>=22.12.0'}
+
+ '@commitlint/resolve-extends@21.2.2':
+ resolution: {integrity: sha512-RPkJ/IFi7sMUUVbZLqwWFtWw/zRDcfFsmrPSiTMrt5wb7AdxOr86EGQFvmGzef5QKV5IPBWWCujqVTw1RWX44A==}
+ engines: {node: '>=22.12.0'}
+
+ '@commitlint/rules@21.2.2':
+ resolution: {integrity: sha512-eplQzyYkBjYB1HyyRj8hkcK11Y9DU9nuBz7uOKEd6NpE9NGDytLFCAnlRE+OoiK/5sHEJsaz2RGhuWBvYzIbNA==}
+ engines: {node: '>=22.12.0'}
+
+ '@commitlint/to-lines@21.0.1':
+ resolution: {integrity: sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw==}
+ engines: {node: '>=22.12.0'}
+
+ '@commitlint/top-level@21.2.0':
+ resolution: {integrity: sha512-Y5gmQ+KxzqCrBFJfLvFEPvvwD3LDiNZoTT2yeFBm96M8qhmqSzQc5DvX3rheAaAMjyIvMXOCLS/mWfdpONsjyQ==}
+ engines: {node: '>=22.12.0'}
+
+ '@commitlint/types@21.2.0':
+ resolution: {integrity: sha512-7zVFCDB2reMvJH5dmbKnOQPjZEvjdJTH8jc0U/PIPU1r3/+vf5pD1HlfitV2MWsWXrvu7u39iY1lyLUPOaN0Gw==}
+ engines: {node: '>=22.12.0'}
+
+ '@conventional-changelog/git-client@3.1.2':
+ resolution: {integrity: sha512-jZqwnJwf7nboIlAcw/mkOjVa6DexCcUOgT2oOQgkoi3z9vR8tGFkcMy2BFcYwjhL9sYcDDXkRQDayiDieCoW7A==}
+ engines: {node: '>=22'}
+ peerDependencies:
+ conventional-commits-filter: ^6.0.1
+ conventional-commits-parser: ^7.1.2
+ peerDependenciesMeta:
+ conventional-commits-filter:
+ optional: true
+ conventional-commits-parser:
+ optional: true
+
+ '@conventional-changelog/template@1.4.0':
+ resolution: {integrity: sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg==}
+ engines: {node: '>=22'}
+
'@emnapi/core@1.10.0':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
@@ -1511,6 +1617,14 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
+ '@simple-libs/child-process-utils@2.0.0':
+ resolution: {integrity: sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==}
+ engines: {node: '>=22'}
+
+ '@simple-libs/stream-utils@2.0.0':
+ resolution: {integrity: sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==}
+ engines: {node: '>=22'}
+
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -1800,10 +1914,21 @@ packages:
ajv@6.15.0:
resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
+ ajv@8.20.0:
+ resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
+
+ ansi-regex@6.3.0:
+ resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==}
+ engines: {node: '>=12'}
+
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
+ ansi-styles@6.2.3:
+ resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
+ engines: {node: '>=12'}
+
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
@@ -1817,6 +1942,10 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ argue-cli@3.1.0:
+ resolution: {integrity: sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw==}
+ engines: {node: '>=22'}
+
aria-hidden@1.2.6:
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
engines: {node: '>=10'}
@@ -1968,6 +2097,10 @@ packages:
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+ cliui@9.0.1:
+ resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
+ engines: {node: '>=20'}
+
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@@ -1994,9 +2127,39 @@ packages:
resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==}
engines: {node: '>=18'}
+ conventional-changelog-angular@9.4.0:
+ resolution: {integrity: sha512-HdxRxuS8bBXVIuo4V82gvSwAXT0vYQUizrjs/izmPg5JdDstr8v8I5hduGL3iQbG+o310dUDxC4+LetuS5hu9w==}
+ engines: {node: '>=22'}
+
+ conventional-changelog-conventionalcommits@10.4.0:
+ resolution: {integrity: sha512-Rriac6ZrAlVm6cy9Bz4NSp+WMHpwNXoPIYex+HjCgduAVUSbnew29DQjQw0C4g9u3HtSYzGiGY+pdBXAZo+4aA==}
+ engines: {node: '>=22'}
+
+ conventional-commits-parser@7.1.2:
+ resolution: {integrity: sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==}
+ engines: {node: '>=22'}
+ hasBin: true
+
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cosmiconfig-typescript-loader@6.3.0:
+ resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==}
+ engines: {node: '>=v18'}
+ peerDependencies:
+ '@types/node': '*'
+ cosmiconfig: '>=9'
+ typescript: '>=5'
+
+ cosmiconfig@9.0.2:
+ resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ typescript: '>=4.9.5'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -2130,9 +2293,19 @@ packages:
electron-to-chromium@1.5.393:
resolution: {integrity: sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==}
+ emoji-regex@10.6.0:
+ resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
+
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+ env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+
+ error-ex@1.3.4:
+ resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
+
es-abstract-get@1.0.0:
resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==}
engines: {node: '>= 0.4'}
@@ -2172,6 +2345,9 @@ packages:
resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==}
engines: {node: '>= 0.4'}
+ es-toolkit@1.51.0:
+ resolution: {integrity: sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==}
+
esbuild@0.18.20:
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
engines: {node: '>=12'}
@@ -2194,6 +2370,12 @@ packages:
typescript:
optional: true
+ eslint-config-prettier@10.1.8:
+ resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
+ hasBin: true
+ peerDependencies:
+ eslint: '>=7.0.0'
+
eslint-import-resolver-node@0.3.10:
resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==}
@@ -2336,6 +2518,9 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+ fast-uri@3.1.5:
+ resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==}
+
fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
@@ -2400,6 +2585,14 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
+ get-east-asian-width@1.6.0:
+ resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
+ engines: {node: '>=18'}
+
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@@ -2427,6 +2620,10 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
+ global-directory@5.0.0:
+ resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==}
+ engines: {node: '>=20'}
+
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
@@ -2476,6 +2673,11 @@ packages:
hermes-parser@0.25.1:
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+ husky@9.1.7:
+ resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -2492,6 +2694,10 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
+ ini@6.0.0:
+ resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==}
+ engines: {node: ^20.17.0 || >=22.9.0}
+
internal-slot@1.1.0:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
@@ -2504,6 +2710,9 @@ packages:
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
engines: {node: '>= 0.4'}
+ is-arrayish@0.2.1:
+ resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+
is-async-function@2.1.1:
resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
engines: {node: '>= 0.4'}
@@ -2575,6 +2784,10 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
+ is-plain-obj@4.1.0:
+ resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
+ engines: {node: '>=12'}
+
is-regex@1.2.1:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'}
@@ -2625,6 +2838,10 @@ packages:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true
+ jiti@2.6.1:
+ resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
+ hasBin: true
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -2640,9 +2857,15 @@ packages:
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+ json-parse-even-better-errors@2.3.1:
+ resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
@@ -2757,6 +2980,11 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+ lint-staged@17.3.0:
+ resolution: {integrity: sha512-woZS3vNe3UKqBaLPvbLOtKRY4tLANpWQhom12MGWqC8Mh1lCOO+WgSwmX2amjJAqTY9BkXYW87fCUH5H9Ph6xw==}
+ engines: {node: '>=22.22.1'}
+ hasBin: true
+
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -2922,6 +3150,10 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
+ parse-json@5.2.0:
+ resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
+ engines: {node: '>=8'}
+
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -3064,6 +3296,66 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
+ prettier-plugin-tailwindcss@0.8.1:
+ resolution: {integrity: sha512-iaFMYqDsE4ffdDkn5qup0j5f2aCEBFZrdrZnvu9QKTlWx/iGPeQ4HHu7b7fCPMxeo9nwQBiOAh2nSypdFYWJkw==}
+ engines: {node: '>=20.19'}
+ peerDependencies:
+ '@ianvs/prettier-plugin-sort-imports': '*'
+ '@prettier/plugin-hermes': '*'
+ '@prettier/plugin-oxc': '*'
+ '@prettier/plugin-pug': '*'
+ '@shopify/prettier-plugin-liquid': '*'
+ '@trivago/prettier-plugin-sort-imports': '*'
+ '@zackad/prettier-plugin-twig': '*'
+ prettier: ^3.0
+ prettier-plugin-astro: '*'
+ prettier-plugin-css-order: '*'
+ prettier-plugin-jsdoc: '*'
+ prettier-plugin-marko: '*'
+ prettier-plugin-multiline-arrays: '*'
+ prettier-plugin-organize-attributes: '*'
+ prettier-plugin-organize-imports: '*'
+ prettier-plugin-sort-imports: '*'
+ prettier-plugin-svelte: '*'
+ peerDependenciesMeta:
+ '@ianvs/prettier-plugin-sort-imports':
+ optional: true
+ '@prettier/plugin-hermes':
+ optional: true
+ '@prettier/plugin-oxc':
+ optional: true
+ '@prettier/plugin-pug':
+ optional: true
+ '@shopify/prettier-plugin-liquid':
+ optional: true
+ '@trivago/prettier-plugin-sort-imports':
+ optional: true
+ '@zackad/prettier-plugin-twig':
+ optional: true
+ prettier-plugin-astro:
+ optional: true
+ prettier-plugin-css-order:
+ optional: true
+ prettier-plugin-jsdoc:
+ optional: true
+ prettier-plugin-marko:
+ optional: true
+ prettier-plugin-multiline-arrays:
+ optional: true
+ prettier-plugin-organize-attributes:
+ optional: true
+ prettier-plugin-organize-imports:
+ optional: true
+ prettier-plugin-sort-imports:
+ optional: true
+ prettier-plugin-svelte:
+ optional: true
+
+ prettier@3.9.6:
+ resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==}
+ engines: {node: '>=14'}
+ hasBin: true
+
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
@@ -3179,10 +3471,18 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
+ resolve-from@5.0.0:
+ resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
+ engines: {node: '>=8'}
+
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
@@ -3303,6 +3603,18 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
+ string-argv@0.3.2:
+ resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
+ engines: {node: '>=0.6.19'}
+
+ string-width@7.2.0:
+ resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
+ engines: {node: '>=18'}
+
+ string-width@8.2.2:
+ resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==}
+ engines: {node: '>=20'}
+
string.prototype.includes@2.0.1:
resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==}
engines: {node: '>= 0.4'}
@@ -3326,6 +3638,10 @@ packages:
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
engines: {node: '>= 0.4'}
+ strip-ansi@7.2.0:
+ resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
+ engines: {node: '>=12'}
+
strip-bom@3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
@@ -3610,13 +3926,34 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
+ wrap-ansi@9.0.2:
+ resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
+ engines: {node: '>=18'}
+
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
+ y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+ yaml@2.9.0:
+ resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
+ engines: {node: '>= 14.6'}
+ hasBin: true
+
+ yargs-parser@22.0.0:
+ resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+
+ yargs@18.1.0:
+ resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@@ -3736,6 +4073,125 @@ snapshots:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
+ '@commitlint/cli@21.2.2(@types/node@25.9.5)(conventional-commits-parser@7.1.2)(typescript@6.0.3)':
+ dependencies:
+ '@commitlint/config-conventional': 21.2.2
+ '@commitlint/format': 21.2.2
+ '@commitlint/lint': 21.2.2
+ '@commitlint/load': 21.2.2(@types/node@25.9.5)(typescript@6.0.3)
+ '@commitlint/read': 21.2.1(conventional-commits-parser@7.1.2)
+ '@commitlint/types': 21.2.0
+ tinyexec: 1.2.4
+ yargs: 18.1.0
+ transitivePeerDependencies:
+ - '@types/node'
+ - conventional-commits-filter
+ - conventional-commits-parser
+ - typescript
+
+ '@commitlint/config-conventional@21.2.2':
+ dependencies:
+ '@commitlint/types': 21.2.0
+ conventional-changelog-conventionalcommits: 10.4.0
+
+ '@commitlint/config-validator@21.2.0':
+ dependencies:
+ '@commitlint/types': 21.2.0
+ ajv: 8.20.0
+
+ '@commitlint/ensure@21.2.0':
+ dependencies:
+ '@commitlint/types': 21.2.0
+ es-toolkit: 1.51.0
+
+ '@commitlint/execute-rule@21.0.1': {}
+
+ '@commitlint/format@21.2.2':
+ dependencies:
+ '@commitlint/types': 21.2.0
+ picocolors: 1.1.1
+
+ '@commitlint/is-ignored@21.2.2':
+ dependencies:
+ '@commitlint/types': 21.2.0
+ semver: 7.8.5
+
+ '@commitlint/lint@21.2.2':
+ dependencies:
+ '@commitlint/is-ignored': 21.2.2
+ '@commitlint/parse': 21.2.2
+ '@commitlint/rules': 21.2.2
+ '@commitlint/types': 21.2.0
+
+ '@commitlint/load@21.2.2(@types/node@25.9.5)(typescript@6.0.3)':
+ dependencies:
+ '@commitlint/config-validator': 21.2.0
+ '@commitlint/execute-rule': 21.0.1
+ '@commitlint/resolve-extends': 21.2.2
+ '@commitlint/types': 21.2.0
+ cosmiconfig: 9.0.2(typescript@6.0.3)
+ cosmiconfig-typescript-loader: 6.3.0(@types/node@25.9.5)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3)
+ es-toolkit: 1.51.0
+ is-plain-obj: 4.1.0
+ picocolors: 1.1.1
+ transitivePeerDependencies:
+ - '@types/node'
+ - typescript
+
+ '@commitlint/message@21.2.0': {}
+
+ '@commitlint/parse@21.2.2':
+ dependencies:
+ '@commitlint/types': 21.2.0
+ conventional-changelog-angular: 9.4.0
+ conventional-commits-parser: 7.1.2
+
+ '@commitlint/read@21.2.1(conventional-commits-parser@7.1.2)':
+ dependencies:
+ '@commitlint/top-level': 21.2.0
+ '@commitlint/types': 21.2.0
+ '@conventional-changelog/git-client': 3.1.2(conventional-commits-parser@7.1.2)
+ tinyexec: 1.2.4
+ transitivePeerDependencies:
+ - conventional-commits-filter
+ - conventional-commits-parser
+
+ '@commitlint/resolve-extends@21.2.2':
+ dependencies:
+ '@commitlint/config-validator': 21.2.0
+ '@commitlint/types': 21.2.0
+ es-toolkit: 1.51.0
+ global-directory: 5.0.0
+ resolve-from: 5.0.0
+
+ '@commitlint/rules@21.2.2':
+ dependencies:
+ '@commitlint/ensure': 21.2.0
+ '@commitlint/message': 21.2.0
+ '@commitlint/to-lines': 21.0.1
+ '@commitlint/types': 21.2.0
+
+ '@commitlint/to-lines@21.0.1': {}
+
+ '@commitlint/top-level@21.2.0':
+ dependencies:
+ escalade: 3.2.0
+
+ '@commitlint/types@21.2.0':
+ dependencies:
+ conventional-commits-parser: 7.1.2
+ picocolors: 1.1.1
+
+ '@conventional-changelog/git-client@3.1.2(conventional-commits-parser@7.1.2)':
+ dependencies:
+ '@simple-libs/child-process-utils': 2.0.0
+ '@simple-libs/stream-utils': 2.0.0
+ semver: 7.8.5
+ optionalDependencies:
+ conventional-commits-parser: 7.1.2
+
+ '@conventional-changelog/template@1.4.0': {}
+
'@emnapi/core@1.10.0':
dependencies:
'@emnapi/wasi-threads': 1.2.1
@@ -4943,6 +5399,12 @@ snapshots:
'@rtsao/scc@1.1.0': {}
+ '@simple-libs/child-process-utils@2.0.0':
+ dependencies:
+ '@simple-libs/stream-utils': 2.0.0
+
+ '@simple-libs/stream-utils@2.0.0': {}
+
'@standard-schema/spec@1.1.0': {}
'@swc/helpers@0.5.15':
@@ -5181,13 +5643,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.0
- '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0))':
+ '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0)(yaml@2.9.0))':
dependencies:
'@vitest/spy': 4.1.10
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0)
+ vite: 8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0)(yaml@2.9.0)
'@vitest/pretty-format@4.1.10':
dependencies:
@@ -5226,10 +5688,21 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
+ ajv@8.20.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-uri: 3.1.5
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+
+ ansi-regex@6.3.0: {}
+
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
+ ansi-styles@6.2.3: {}
+
any-promise@1.3.0: {}
anymatch@3.1.3:
@@ -5241,6 +5714,8 @@ snapshots:
argparse@2.0.1: {}
+ argue-cli@3.1.0: {}
+
aria-hidden@1.2.6:
dependencies:
tslib: 2.8.1
@@ -5418,6 +5893,12 @@ snapshots:
client-only@0.0.1: {}
+ cliui@9.0.1:
+ dependencies:
+ string-width: 7.2.0
+ strip-ansi: 7.2.0
+ wrap-ansi: 9.0.2
+
clsx@2.1.1: {}
cluster-key-slot@1.1.2: {}
@@ -5434,8 +5915,37 @@ snapshots:
content-type@2.0.0: {}
+ conventional-changelog-angular@9.4.0:
+ dependencies:
+ '@conventional-changelog/template': 1.4.0
+
+ conventional-changelog-conventionalcommits@10.4.0:
+ dependencies:
+ '@conventional-changelog/template': 1.4.0
+
+ conventional-commits-parser@7.1.2:
+ dependencies:
+ '@simple-libs/stream-utils': 2.0.0
+ argue-cli: 3.1.0
+
convert-source-map@2.0.0: {}
+ cosmiconfig-typescript-loader@6.3.0(@types/node@25.9.5)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3):
+ dependencies:
+ '@types/node': 25.9.5
+ cosmiconfig: 9.0.2(typescript@6.0.3)
+ jiti: 2.6.1
+ typescript: 6.0.3
+
+ cosmiconfig@9.0.2(typescript@6.0.3):
+ dependencies:
+ env-paths: 2.2.1
+ import-fresh: 3.3.1
+ js-yaml: 4.3.1
+ parse-json: 5.2.0
+ optionalDependencies:
+ typescript: 6.0.3
+
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -5555,8 +6065,16 @@ snapshots:
electron-to-chromium@1.5.393: {}
+ emoji-regex@10.6.0: {}
+
emoji-regex@9.2.2: {}
+ env-paths@2.2.1: {}
+
+ error-ex@1.3.4:
+ dependencies:
+ is-arrayish: 0.2.1
+
es-abstract-get@1.0.0:
dependencies:
es-errors: 1.3.0
@@ -5670,6 +6188,8 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
+ es-toolkit@1.51.0: {}
+
esbuild@0.18.20:
optionalDependencies:
'@esbuild/android-arm': 0.18.20
@@ -5719,6 +6239,10 @@ snapshots:
- eslint-plugin-import-x
- supports-color
+ eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@1.21.7)):
+ dependencies:
+ eslint: 9.39.5(jiti@1.21.7)
+
eslint-import-resolver-node@0.3.10:
dependencies:
debug: 3.2.7
@@ -5936,6 +6460,8 @@ snapshots:
fast-levenshtein@2.0.6: {}
+ fast-uri@3.1.5: {}
+
fastq@1.20.1:
dependencies:
reusify: 1.1.0
@@ -5995,6 +6521,10 @@ snapshots:
gensync@1.0.0-beta.2: {}
+ get-caller-file@2.0.5: {}
+
+ get-east-asian-width@1.6.0: {}
+
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -6033,6 +6563,10 @@ snapshots:
dependencies:
is-glob: 4.0.3
+ global-directory@5.0.0:
+ dependencies:
+ ini: 6.0.0
+
globals@14.0.0: {}
globals@16.4.0: {}
@@ -6072,6 +6606,8 @@ snapshots:
dependencies:
hermes-estree: 0.25.1
+ husky@9.1.7: {}
+
ignore@5.3.2: {}
ignore@7.0.6: {}
@@ -6083,6 +6619,8 @@ snapshots:
imurmurhash@0.1.4: {}
+ ini@6.0.0: {}
+
internal-slot@1.1.0:
dependencies:
es-errors: 1.3.0
@@ -6097,6 +6635,8 @@ snapshots:
call-bound: 1.0.4
get-intrinsic: 1.3.0
+ is-arrayish@0.2.1: {}
+
is-async-function@2.1.1:
dependencies:
async-function: 1.0.0
@@ -6172,6 +6712,8 @@ snapshots:
is-number@7.0.0: {}
+ is-plain-obj@4.1.0: {}
+
is-regex@1.2.1:
dependencies:
call-bound: 1.0.4
@@ -6226,6 +6768,8 @@ snapshots:
jiti@1.21.7: {}
+ jiti@2.6.1: {}
+
js-tokens@4.0.0: {}
js-yaml@4.3.1:
@@ -6236,8 +6780,12 @@ snapshots:
json-buffer@3.0.1: {}
+ json-parse-even-better-errors@2.3.1: {}
+
json-schema-traverse@0.4.1: {}
+ json-schema-traverse@1.0.0: {}
+
json-stable-stringify-without-jsonify@1.0.1: {}
json-with-bigint@3.5.10: {}
@@ -6323,6 +6871,14 @@ snapshots:
lines-and-columns@1.2.4: {}
+ lint-staged@17.3.0:
+ dependencies:
+ picomatch: 4.0.5
+ string-argv: 0.3.2
+ tinyexec: 1.2.4
+ optionalDependencies:
+ yaml: 2.9.0
+
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -6493,6 +7049,13 @@ snapshots:
dependencies:
callsites: 3.1.0
+ parse-json@5.2.0:
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ error-ex: 1.3.4
+ json-parse-even-better-errors: 2.3.1
+ lines-and-columns: 1.2.4
+
path-exists@4.0.0: {}
path-key@3.1.1: {}
@@ -6560,13 +7123,14 @@ snapshots:
camelcase-css: 2.0.1
postcss: 8.5.26
- postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.26)(tsx@3.14.0):
+ postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.26)(tsx@3.14.0)(yaml@2.9.0):
dependencies:
lilconfig: 3.1.3
optionalDependencies:
jiti: 1.21.7
postcss: 8.5.26
tsx: 3.14.0
+ yaml: 2.9.0
postcss-nested@6.2.0(postcss@8.5.26):
dependencies:
@@ -6604,6 +7168,12 @@ snapshots:
prelude-ls@1.2.1: {}
+ prettier-plugin-tailwindcss@0.8.1(prettier@3.9.6):
+ dependencies:
+ prettier: 3.9.6
+
+ prettier@3.9.6: {}
+
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
@@ -6792,8 +7362,12 @@ snapshots:
gopd: 1.2.0
set-function-name: 2.0.2
+ require-from-string@2.0.2: {}
+
resolve-from@4.0.0: {}
+ resolve-from@5.0.0: {}
+
resolve-pkg-maps@1.0.0: {}
resolve@1.22.12:
@@ -6976,6 +7550,19 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
+ string-argv@0.3.2: {}
+
+ string-width@7.2.0:
+ dependencies:
+ emoji-regex: 10.6.0
+ get-east-asian-width: 1.6.0
+ strip-ansi: 7.2.0
+
+ string-width@8.2.2:
+ dependencies:
+ get-east-asian-width: 1.6.0
+ strip-ansi: 7.2.0
+
string.prototype.includes@2.0.1:
dependencies:
call-bind: 1.0.9
@@ -7027,6 +7614,10 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.2
+ strip-ansi@7.2.0:
+ dependencies:
+ ansi-regex: 6.3.0
+
strip-bom@3.0.0: {}
strip-json-comments@3.1.1: {}
@@ -7056,7 +7647,7 @@ snapshots:
tailwind-merge@2.6.1: {}
- tailwindcss@3.4.19(tsx@3.14.0):
+ tailwindcss@3.4.19(tsx@3.14.0)(yaml@2.9.0):
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
@@ -7075,7 +7666,7 @@ snapshots:
postcss: 8.5.26
postcss-import: 15.1.0(postcss@8.5.26)
postcss-js: 4.1.0(postcss@8.5.26)
- postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.26)(tsx@3.14.0)
+ postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.26)(tsx@3.14.0)(yaml@2.9.0)
postcss-nested: 6.2.0(postcss@8.5.26)
postcss-selector-parser: 6.1.4
resolve: 1.22.12
@@ -7264,7 +7855,7 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
- vite@8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0):
+ vite@8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0)(yaml@2.9.0):
dependencies:
lightningcss: 1.33.0
picomatch: 4.0.5
@@ -7276,11 +7867,12 @@ snapshots:
fsevents: 2.3.3
jiti: 1.21.7
tsx: 3.14.0
+ yaml: 2.9.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)):
+ 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)(yaml@2.9.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/mocker': 4.1.10(vite@8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.10
'@vitest/runner': 4.1.10
'@vitest/snapshot': 4.1.10
@@ -7297,7 +7889,7 @@ snapshots:
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
- vite: 8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0)
+ vite: 8.1.5(@types/node@25.9.5)(jiti@1.21.7)(tsx@3.14.0)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 25.9.5
@@ -7356,10 +7948,32 @@ snapshots:
word-wrap@1.2.5: {}
+ wrap-ansi@9.0.2:
+ dependencies:
+ ansi-styles: 6.2.3
+ string-width: 7.2.0
+ strip-ansi: 7.2.0
+
xtend@4.0.2: {}
+ y18n@5.0.8: {}
+
yallist@3.1.1: {}
+ yaml@2.9.0:
+ optional: true
+
+ yargs-parser@22.0.0: {}
+
+ yargs@18.1.0:
+ dependencies:
+ cliui: 9.0.1
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ string-width: 8.2.2
+ y18n: 5.0.8
+ yargs-parser: 22.0.0
+
yocto-queue@0.1.0: {}
zod-validation-error@4.0.2(zod@4.4.3):
diff --git a/scripts/calculate-next-country.ts b/scripts/calculate-next-country.ts
index b328fd7..a564d06 100644
--- a/scripts/calculate-next-country.ts
+++ b/scripts/calculate-next-country.ts
@@ -24,10 +24,11 @@ import { calculateLeaderboard } from "@/lib/calculate-leaderboard";
import { logger } from "@/lib/logger";
let activeCountrySlug: string | null = null;
-let isCalculating = false;
const handleShutdownSignal = (signal: string) => {
- logger.warn(`Received ${signal}. Graceful shutdown initiated. Waiting for active calculation (${activeCountrySlug ?? "none"}) to finish...`);
+ logger.warn(
+ `Received ${signal}. Graceful shutdown initiated. Waiting for active calculation (${activeCountrySlug ?? "none"}) to finish...`,
+ );
};
process.on("SIGTERM", () => handleShutdownSignal("SIGTERM"));
@@ -35,7 +36,8 @@ process.on("SIGINT", () => handleShutdownSignal("SIGINT"));
async function main() {
const overallStartTime = performance.now();
- const workerVersion = process.env.DEVIMPACT_VERSION || process.env.GIT_COMMIT_SHA || "development";
+ const workerVersion =
+ process.env.DEVIMPACT_VERSION || process.env.GIT_COMMIT_SHA || "development";
logger.info("=== DevImpact Leaderboard Calculator Start ===");
logger.info(`Version: ${workerVersion}`);
logger.info(`DB: ${(process.env.DATABASE_URL ?? "").slice(0, 40)}...`);
@@ -53,7 +55,6 @@ async function main() {
}
activeCountrySlug = next.slug;
- isCalculating = true;
logger.info(`Selected: ${next.title} (${next.slug})`);
@@ -77,7 +78,8 @@ async function main() {
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";
+ const averageFetchTime =
+ successfulFetches > 0 ? (totalFetchTime / successfulFetches / 1000).toFixed(2) : "N/A";
logger.info("=== Calculation Summary ===", {
country: next.title,
diff --git a/scripts/init-db.ts b/scripts/init-db.ts
index 214ab06..07e22ca 100644
--- a/scripts/init-db.ts
+++ b/scripts/init-db.ts
@@ -37,4 +37,4 @@ async function main() {
main().catch((err) => {
console.error("Schema initialization failed:", err);
process.exit(1);
-});
\ No newline at end of file
+});
diff --git a/scripts/validate-locales.js b/scripts/validate-locales.js
index e34f91a..4d66522 100644
--- a/scripts/validate-locales.js
+++ b/scripts/validate-locales.js
@@ -1,21 +1,27 @@
/* eslint-disable @typescript-eslint/no-require-imports */
-const fs = require('fs');
-const path = require('path');
+const fs = require("fs");
+const path = require("path");
-const localesDir = path.join(__dirname, '..', 'locales');
-const enKeys = Object.keys(JSON.parse(fs.readFileSync(path.join(localesDir, 'en.json'), 'utf8'))).sort();
+const localesDir = path.join(__dirname, "..", "locales");
+const enKeys = Object.keys(
+ JSON.parse(fs.readFileSync(path.join(localesDir, "en.json"), "utf8")),
+).sort();
let hasError = false;
-fs.readdirSync(localesDir).forEach(file => {
- if (file === 'en.json') return;
+fs.readdirSync(localesDir).forEach((file) => {
+ if (file === "en.json") return;
- const fileKeys = Object.keys(JSON.parse(fs.readFileSync(path.join(localesDir, file), 'utf8'))).sort();
- const missing = enKeys.filter(k => !fileKeys.includes(k));
- const extra = fileKeys.filter(k => !enKeys.includes(k));
+ const fileKeys = Object.keys(
+ JSON.parse(fs.readFileSync(path.join(localesDir, file), "utf8")),
+ ).sort();
+ const missing = enKeys.filter((k) => !fileKeys.includes(k));
+ const extra = fileKeys.filter((k) => !enKeys.includes(k));
if (missing.length || extra.length) {
hasError = true;
- console.error(`${file}: ${missing.length ? `Missing keys: ${missing.join(', ')}` : ''}${extra.length ? ` Extra keys: ${extra.join(', ')}` : ''}`);
+ console.error(
+ `${file}: ${missing.length ? `Missing keys: ${missing.join(", ")}` : ""}${extra.length ? ` Extra keys: ${extra.join(", ")}` : ""}`,
+ );
}
});
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 811389d..6bb31bb 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -2,11 +2,7 @@ import type { Config } from "tailwindcss";
const config: Config = {
darkMode: ["class"],
- content: [
- "./app/**/*.{ts,tsx}",
- "./components/**/*.{ts,tsx}",
- "./lib/**/*.{ts,tsx}",
- ],
+ content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./lib/**/*.{ts,tsx}"],
theme: {
extend: {
colors: {
diff --git a/test/api/compare.route.test.ts b/test/api/compare.route.test.ts
index d3c0d1e..596cc13 100644
--- a/test/api/compare.route.test.ts
+++ b/test/api/compare.route.test.ts
@@ -67,10 +67,7 @@ function makeLanguageScores(finalScore: number) {
};
}
-function makeScore(
- finalScore: number,
- languageScores?: ReturnType,
-) {
+function makeScore(finalScore: number, languageScores?: ReturnType) {
return {
repoScore: 10,
prScore: 20,
diff --git a/test/fixtures/github.ts b/test/fixtures/github.ts
index 34885ee..4fc98c0 100644
--- a/test/fixtures/github.ts
+++ b/test/fixtures/github.ts
@@ -47,7 +47,6 @@ const defaultPullRequest: PullRequestNode = {
},
};
-
const defaultIssue: IssueNode = {
title: "Issue about improving docs",
url: "https://example.com/external-owner/repo/issues/1",
@@ -79,15 +78,13 @@ export function makeRepo(overrides: Partial = {}): RepoNode {
};
}
-export function makePullRequest(
- overrides: Partial = {},
-): PullRequestNode {
+export function makePullRequest(overrides: Partial = {}): PullRequestNode {
const repository = overrides.repository
? {
- ...defaultPullRequest.repository,
- ...overrides.repository,
- languages: overrides.repository.languages ?? defaultPullRequest.repository.languages,
- }
+ ...defaultPullRequest.repository,
+ ...overrides.repository,
+ languages: overrides.repository.languages ?? defaultPullRequest.repository.languages,
+ }
: defaultPullRequest.repository;
return {
@@ -97,9 +94,7 @@ export function makePullRequest(
};
}
-export function makeRepoLanguages(
- edges: Array<{ size: number; name: string }>,
-): RepoLanguages {
+export function makeRepoLanguages(edges: Array<{ size: number; name: string }>): RepoLanguages {
return {
edges: edges.map((edge) => ({
size: edge.size,
@@ -117,9 +112,7 @@ export function makeIssue(overrides: Partial = {}): IssueNode {
};
}
-export function makeDiscussion(
- overrides: Partial = {},
-): DiscussionNode {
+export function makeDiscussion(overrides: Partial = {}): DiscussionNode {
return {
...defaultDiscussion,
...overrides,
@@ -128,11 +121,7 @@ export function makeDiscussion(
};
}
-
-
-export function makeUserScoreInput(
- overrides: Partial = {},
-): UserScoreInput {
+export function makeUserScoreInput(overrides: Partial = {}): UserScoreInput {
return {
repos: overrides.repos ?? [makeRepo()],
pullRequests: overrides.pullRequests ?? [makePullRequest()],
diff --git a/test/github/github-cache.test.ts b/test/github/github-cache.test.ts
index ac99380..b661235 100644
--- a/test/github/github-cache.test.ts
+++ b/test/github/github-cache.test.ts
@@ -1,10 +1,7 @@
import "dotenv/config";
import { afterEach, describe, expect, test, vi } from "vitest";
-import {
- createGitHubUserDataFetcher,
- type GitHubFetcherDependencies,
-} from "@/lib/github";
+import { createGitHubUserDataFetcher, type GitHubFetcherDependencies } from "@/lib/github";
import {
DEFAULT_CACHE_NAMESPACE,
DEFAULT_GITHUB_CACHE_TTL_SECONDS,
@@ -20,16 +17,11 @@ type ExecuteCall = {
operationName: string;
};
-function makeProcessEnv(
- values: Record = {},
-): NodeJS.ProcessEnv {
+function makeProcessEnv(values: Record = {}): NodeJS.ProcessEnv {
return { NODE_ENV: "test", ...values };
}
-function makeExecutor(
- calls: ExecuteCall[],
- delayMs = 0,
-): GitHubFetcherDependencies["executor"] {
+function makeExecutor(calls: ExecuteCall[], delayMs = 0): GitHubFetcherDependencies["executor"] {
return {
async execute>(params: {
operationName: string;
@@ -277,10 +269,7 @@ describe("GitHub user data caching", () => {
}> = [];
const fetcher = createGitHubUserDataFetcher({
executor: {
- async execute<
- TData,
- TVariables extends Record,
- >(params: {
+ async execute>(params: {
operationName: string;
query: string;
variables: TVariables;
@@ -427,12 +416,9 @@ 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.filter((call) => call.operationName === "FetchUserPullRequests"),
- ).toHaveLength(1);
- expect(
- calls.find((call) => call.operationName === "FetchUserPullRequests")
- ?.variables.prCount,
+ calls.find((call) => call.operationName === "FetchUserPullRequests")?.variables.prCount,
).toBe(2);
});
@@ -493,10 +479,7 @@ describe("GitHub user data caching", () => {
},
});
- const [first, second] = await Promise.all([
- fetcher("testuser"),
- fetcher("TestUser"),
- ]);
+ const [first, second] = await Promise.all([fetcher("testuser"), fetcher("TestUser")]);
expect(first.avatarUrl).toBe(second.avatarUrl);
expect(calls).toHaveLength(4);
@@ -509,15 +492,19 @@ describe("GitHub user data caching", () => {
test("reads cache TTL aliases with Redis-specific precedence", () => {
expect(
- getCacheTtlSecondsFromEnv(makeProcessEnv({
- REDIS_CACHE_TTL_SECONDS: "3600",
- CACHE_TTL_SECONDS: "7200",
- })),
+ getCacheTtlSecondsFromEnv(
+ makeProcessEnv({
+ REDIS_CACHE_TTL_SECONDS: "3600",
+ CACHE_TTL_SECONDS: "7200",
+ }),
+ ),
).toBe(3600);
expect(
- getCacheTtlSecondsFromEnv(makeProcessEnv({
- CACHE_TTL_SECONDS: "7200",
- })),
+ getCacheTtlSecondsFromEnv(
+ makeProcessEnv({
+ CACHE_TTL_SECONDS: "7200",
+ }),
+ ),
).toBe(7200);
});
@@ -525,48 +512,60 @@ describe("GitHub user data caching", () => {
"rejects invalid cache TTL %s",
(value) => {
expect(
- getCacheTtlSecondsFromEnv(makeProcessEnv({
- REDIS_CACHE_TTL_SECONDS: value,
- })),
+ getCacheTtlSecondsFromEnv(
+ makeProcessEnv({
+ REDIS_CACHE_TTL_SECONDS: value,
+ }),
+ ),
).toBe(DEFAULT_GITHUB_CACHE_TTL_SECONDS);
},
);
test("falls through to the TTL alias when the preferred value is invalid", () => {
expect(
- getCacheTtlSecondsFromEnv(makeProcessEnv({
- REDIS_CACHE_TTL_SECONDS: "invalid",
- CACHE_TTL_SECONDS: "1800",
- })),
+ getCacheTtlSecondsFromEnv(
+ makeProcessEnv({
+ REDIS_CACHE_TTL_SECONDS: "invalid",
+ CACHE_TTL_SECONDS: "1800",
+ }),
+ ),
).toBe(1800);
});
test("accepts the maximum cache TTL", () => {
expect(
- getCacheTtlSecondsFromEnv(makeProcessEnv({
- REDIS_CACHE_TTL_SECONDS: `${MAX_CACHE_TTL_SECONDS}`,
- })),
+ getCacheTtlSecondsFromEnv(
+ makeProcessEnv({
+ REDIS_CACHE_TTL_SECONDS: `${MAX_CACHE_TTL_SECONDS}`,
+ }),
+ ),
).toBe(MAX_CACHE_TTL_SECONDS);
});
test("reads, trims, and validates cache namespace aliases", () => {
expect(
- getCacheNamespaceFromEnv(makeProcessEnv({
- REDIS_CACHE_NAMESPACE: " deployment:v2 ",
- CACHE_NAMESPACE: "fallback:v1",
- })),
+ getCacheNamespaceFromEnv(
+ makeProcessEnv({
+ REDIS_CACHE_NAMESPACE: " deployment:v2 ",
+ CACHE_NAMESPACE: "fallback:v1",
+ }),
+ ),
).toBe("deployment:v2");
expect(
- getCacheNamespaceFromEnv(makeProcessEnv({
- REDIS_CACHE_NAMESPACE: " ",
- CACHE_NAMESPACE: " fallback:v1 ",
- })),
+ getCacheNamespaceFromEnv(
+ makeProcessEnv({
+ REDIS_CACHE_NAMESPACE: " ",
+ CACHE_NAMESPACE: " fallback:v1 ",
+ }),
+ ),
).toBe("fallback:v1");
expect(
- getCacheNamespaceFromEnv(makeProcessEnv({
- REDIS_CACHE_NAMESPACE: " ",
- CACHE_NAMESPACE: "",
- })),
+ getCacheNamespaceFromEnv(
+ makeProcessEnv({
+ REDIS_CACHE_NAMESPACE: " ",
+ CACHE_NAMESPACE: "",
+ }),
+ ),
).toBe(DEFAULT_CACHE_NAMESPACE);
});
});
diff --git a/test/helpers/score.ts b/test/helpers/score.ts
index fbc9ca9..f76d0c4 100644
--- a/test/helpers/score.ts
+++ b/test/helpers/score.ts
@@ -1,9 +1,4 @@
-import type {
- DiscussionNode,
- IssueNode,
- PullRequestNode,
- RepoNode,
-} from "@/types/github";
+import type { DiscussionNode, IssueNode, PullRequestNode, RepoNode } from "@/types/github";
const MS_PER_DAY = 86_400_000;
const DEFAULT_REFERENCE_DATE = new Date("2026-05-10T00:00:00.000Z");
@@ -38,10 +33,7 @@ function getDaysSince(pushedAt: string, referenceDate: Date): number | null {
return Math.max(0, (referenceDate.getTime() - pushed.getTime()) / MS_PER_DAY);
}
-function getRepoActivityFactor(
- pushedAt: string | undefined,
- referenceDate: Date,
-): number {
+function getRepoActivityFactor(pushedAt: string | undefined, referenceDate: Date): number {
if (!pushedAt) {
return 0.8;
}
@@ -57,10 +49,7 @@ function getRepoActivityFactor(
return 0.4;
}
-function getPRActivityFactor(
- pushedAt: string | undefined,
- referenceDate: Date,
-): number {
+function getPRActivityFactor(pushedAt: string | undefined, referenceDate: Date): number {
if (!pushedAt) {
return 0.9;
}
@@ -174,9 +163,7 @@ export function sumPRScores(
return total;
}
-export function expectedCommunityScore(
- item: IssueNode | DiscussionNode,
-): number {
+export function expectedCommunityScore(item: IssueNode | DiscussionNode): number {
const comments = Math.max(0, item.comments.totalCount);
let score = safeLog(item.repository.stargazerCount) * safeLog(comments);
diff --git a/test/scoring/calculateUserScore.contribution.test.ts b/test/scoring/calculateUserScore.contribution.test.ts
index 785577a..934778d 100644
--- a/test/scoring/calculateUserScore.contribution.test.ts
+++ b/test/scoring/calculateUserScore.contribution.test.ts
@@ -244,9 +244,6 @@ describe("calculateUserScore - contribution scoring", () => {
"octocat",
);
- expect(result.contributionScore).toBeCloseTo(
- 0.3 * (result.repoScore + result.prScore),
- 10,
- );
+ expect(result.contributionScore).toBeCloseTo(0.3 * (result.repoScore + result.prScore), 10);
});
});
diff --git a/test/scoring/calculateUserScore.language.test.ts b/test/scoring/calculateUserScore.language.test.ts
index 43ce927..1ff5c71 100644
--- a/test/scoring/calculateUserScore.language.test.ts
+++ b/test/scoring/calculateUserScore.language.test.ts
@@ -256,7 +256,7 @@ describe("calculateUserScore - language scoring", () => {
const top = result.languageScores?.topPullRequests ?? [];
expect(top).toHaveLength(2);
- expect((result.languageScores?.prScore ?? 0)).toBeLessThanOrEqual(result.prScore);
+ expect(result.languageScores?.prScore ?? 0).toBeLessThanOrEqual(result.prScore);
});
test("topLanguagePullRequests includes languageMatch and topLanguages", () => {
diff --git a/test/scoring/calculateUserScore.pr.test.ts b/test/scoring/calculateUserScore.pr.test.ts
index fa490e6..ef38673 100644
--- a/test/scoring/calculateUserScore.pr.test.ts
+++ b/test/scoring/calculateUserScore.pr.test.ts
@@ -253,11 +253,7 @@ describe("calculateUserScore - pull request scoring", () => {
);
expect(result.topPullRequests).toHaveLength(3);
- expect(result.topPullRequests[0].score).toBeGreaterThanOrEqual(
- result.topPullRequests[1].score,
- );
- expect(result.topPullRequests[1].score).toBeGreaterThanOrEqual(
- result.topPullRequests[2].score,
- );
+ expect(result.topPullRequests[0].score).toBeGreaterThanOrEqual(result.topPullRequests[1].score);
+ expect(result.topPullRequests[1].score).toBeGreaterThanOrEqual(result.topPullRequests[2].score);
});
});
diff --git a/test/scoring/calculateUserScore.repo.test.ts b/test/scoring/calculateUserScore.repo.test.ts
index 878d486..060d1f2 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 { makeRepo, makeUserScoreInput } from "@/test/fixtures/github";
+import { makeRepo, makeUserScoreInput } from "@/test/fixtures/github";
import { expectedRepoScore, sumRepoScores } from "@/test/helpers/score";
describe("calculateUserScore - repository scoring", () => {
@@ -70,10 +70,7 @@ describe("calculateUserScore - repository scoring", () => {
}),
);
- const result = calculateUserScore(
- makeUserScoreInput({ repos, pullRequests: [] }),
- "octocat",
- );
+ const result = calculateUserScore(makeUserScoreInput({ repos, pullRequests: [] }), "octocat");
expect(result.repoScore).toBeCloseTo(sumRepoScores(repos), 10);
});
@@ -88,10 +85,7 @@ describe("calculateUserScore - repository scoring", () => {
}),
);
- const result = calculateUserScore(
- makeUserScoreInput({ repos, pullRequests: [] }),
- "octocat",
- );
+ const result = calculateUserScore(makeUserScoreInput({ repos, pullRequests: [] }), "octocat");
expect(result.repoScore).toBeCloseTo(sumRepoScores(repos), 10);
});
diff --git a/test/scoring/calculateUserScore.scenario.test.ts b/test/scoring/calculateUserScore.scenario.test.ts
index 9426bc0..81713a4 100644
--- a/test/scoring/calculateUserScore.scenario.test.ts
+++ b/test/scoring/calculateUserScore.scenario.test.ts
@@ -1,12 +1,7 @@
import { describe, expect, test } from "vitest";
import { calculateUserScore } from "@/lib/score";
-import {
- makeIssue,
- makePullRequest,
- makeRepo,
- makeUserScoreInput,
-} from "@/test/fixtures/github";
+import { makeIssue, makePullRequest, makeRepo, makeUserScoreInput } from "@/test/fixtures/github";
describe("calculateUserScore - final score behavior", () => {
test("final score uses 45/45/10 weights", () => {
@@ -46,9 +41,7 @@ describe("calculateUserScore - final score behavior", () => {
);
expect(result.finalScore).toBeCloseTo(
- result.repoScore * 0.45 +
- result.prScore * 0.45 +
- result.contributionScore * 0.1,
+ result.repoScore * 0.45 + result.prScore * 0.45 + result.contributionScore * 0.1,
10,
);
});
diff --git a/test/seo/seo.test.ts b/test/seo/seo.test.ts
index 7bec353..d03796d 100644
--- a/test/seo/seo.test.ts
+++ b/test/seo/seo.test.ts
@@ -12,25 +12,32 @@ function makeEnv(values: Record): NodeJS.ProcessEnv {
describe("seo helpers", () => {
test("getSiteUrl uses NEXT_PUBLIC_SITE_URL when valid", () => {
- const result = getSiteUrl(makeEnv({
- NEXT_PUBLIC_SITE_URL: "https://devimpact.example.com/",
- }));
+ const result = getSiteUrl(
+ makeEnv({
+ NEXT_PUBLIC_SITE_URL: "https://devimpact.example.com/",
+ }),
+ );
expect(result).toBe("https://devimpact.example.com");
});
test("getSiteUrl falls back when url is invalid", () => {
- const result = getSiteUrl(makeEnv({
- NEXT_PUBLIC_SITE_URL: "invalid-url",
- }));
+ const result = getSiteUrl(
+ makeEnv({
+ NEXT_PUBLIC_SITE_URL: "invalid-url",
+ }),
+ );
expect(result).toBe("http://localhost:3000");
});
test("toAbsoluteUrl builds absolute path", () => {
- const result = toAbsoluteUrl("/scoring-methodology", makeEnv({
- NEXT_PUBLIC_SITE_URL: "https://devimpact.example.com",
- }));
+ const result = toAbsoluteUrl(
+ "/scoring-methodology",
+ makeEnv({
+ NEXT_PUBLIC_SITE_URL: "https://devimpact.example.com",
+ }),
+ );
expect(result).toBe("https://devimpact.example.com/scoring-methodology");
});
diff --git a/test/ui/compare-request.test.ts b/test/ui/compare-request.test.ts
index 0901c4a..23c2f34 100644
--- a/test/ui/compare-request.test.ts
+++ b/test/ui/compare-request.test.ts
@@ -28,7 +28,9 @@ describe("comparison request identity", () => {
test("keeps true user and language changes distinct", () => {
const original = createComparisonRequest("alice", "bob", ["TypeScript"]);
- expect(createComparisonRequest("alice", "carol", ["TypeScript"]).fetchKey).not.toBe(original.fetchKey);
+ expect(createComparisonRequest("alice", "carol", ["TypeScript"]).fetchKey).not.toBe(
+ original.fetchKey,
+ );
expect(createComparisonRequest("alice", "bob", ["Rust"]).fetchKey).not.toBe(original.fetchKey);
});
@@ -46,9 +48,13 @@ describe("comparison request identity", () => {
});
test("keeps sanitizer limits and case-insensitive deduplication", () => {
- expect(
- sanitizeSelectedLanguages([" A ", "a", "B", "C", "D", "E", "F"]),
- ).toEqual(["A", "B", "C", "D", "E"]);
+ expect(sanitizeSelectedLanguages([" A ", "a", "B", "C", "D", "E", "F"])).toEqual([
+ "A",
+ "B",
+ "C",
+ "D",
+ "E",
+ ]);
});
test("two swaps restore the original presentation and identity", () => {
@@ -62,10 +68,10 @@ describe("comparison request identity", () => {
const original = createComparisonRequest("alice", "bob", ["Go"]);
const swapped = swapComparisonRequest(original);
- expect(isComparisonFetchDuplicate(swapped.fetchKey, original.fetchKey, null, true)).toBe(
- true,
- );
- expect(isComparisonFetchDuplicate(swapped.fetchKey, original.fetchKey, original.fetchKey, false)).toBe(true);
+ expect(isComparisonFetchDuplicate(swapped.fetchKey, original.fetchKey, null, true)).toBe(true);
+ expect(
+ isComparisonFetchDuplicate(swapped.fetchKey, original.fetchKey, original.fetchKey, false),
+ ).toBe(true);
expect(isComparisonFetchDuplicate(swapped.fetchKey, null, null, false)).toBe(false);
});
});
@@ -122,11 +128,23 @@ describe("comparison response reconciliation", () => {
expect(source).toMatch(
/reconcileComparisonData\(\s*nextData,\s*fetchKey,\s*latestRequestRef\.current/,
);
- expect(source).toMatch(/if \(!body\.success \|\| !users\) \{\s*if \(latestRequestRef\.current\.fetchKey !== fetchKey\)/);
- expect(source).toMatch(/const reset = \(\) => \{[\s\S]*?latestRequestRef\.current = createComparisonRequest\("", "", \[\]\)/);
- expect(source).toMatch(/if \(!res\.ok\) \{\s*if \(latestRequestRef\.current\.fetchKey !== fetchKey\)/);
- expect(source).toMatch(/catch \(err: unknown\) \{\s*if \(latestRequestRef\.current\.fetchKey !== fetchKey\)/);
- expect(source).toMatch(/applyApiError\(latestRequestRef\.current\.user1, latestRequestRef\.current\.user2, body\)/);
- expect(source).toMatch(/if \(!reconciled\) \{\s*if \(latestRequestRef\.current\.fetchKey === fetchKey\) \{\s*setData\(null\);\s*setGeneralError\(t\("error\.generic"\)\)/);
+ expect(source).toMatch(
+ /if \(!body\.success \|\| !users\) \{\s*if \(latestRequestRef\.current\.fetchKey !== fetchKey\)/,
+ );
+ expect(source).toMatch(
+ /const reset = \(\) => \{[\s\S]*?latestRequestRef\.current = createComparisonRequest\("", "", \[\]\)/,
+ );
+ expect(source).toMatch(
+ /if \(!res\.ok\) \{\s*if \(latestRequestRef\.current\.fetchKey !== fetchKey\)/,
+ );
+ expect(source).toMatch(
+ /catch \(err: unknown\) \{\s*if \(latestRequestRef\.current\.fetchKey !== fetchKey\)/,
+ );
+ expect(source).toMatch(
+ /applyApiError\(latestRequestRef\.current\.user1, latestRequestRef\.current\.user2, body\)/,
+ );
+ expect(source).toMatch(
+ /if \(!reconciled\) \{\s*if \(latestRequestRef\.current\.fetchKey === fetchKey\) \{\s*setData\(null\);\s*setGeneralError\(t\("error\.generic"\)\)/,
+ );
});
});
diff --git a/test/ui/scoring-methodology.test.ts b/test/ui/scoring-methodology.test.ts
index bcef45c..8354cf9 100644
--- a/test/ui/scoring-methodology.test.ts
+++ b/test/ui/scoring-methodology.test.ts
@@ -49,22 +49,13 @@ describe("scoring methodology localization", () => {
});
test("result dashboard links to methodology page", () => {
- const dashboardPath = resolve(
- process.cwd(),
- "components",
- "result-dashboard.tsx",
- );
+ const dashboardPath = resolve(process.cwd(), "components", "result-dashboard.tsx");
const source = readFileSync(dashboardPath, "utf8");
expect(source.includes("/scoring-methodology")).toBe(true);
});
test("methodology route file exists", () => {
- const routePath = resolve(
- process.cwd(),
- "app",
- "scoring-methodology",
- "page.tsx",
- );
+ const routePath = resolve(process.cwd(), "app", "scoring-methodology", "page.tsx");
const source = readFileSync(routePath, "utf8");
expect(source.includes("ScoringMethodologyPage")).toBe(true);
});
diff --git a/tsconfig.json b/tsconfig.json
index 93912b1..019858c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
- "lib": [
- "DOM",
- "DOM.Iterable",
- "ES2020"
- ],
+ "lib": ["DOM", "DOM.Iterable", "ES2020"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
@@ -18,18 +14,14 @@
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
- "types": [
- "@types/node"
- ],
+ "types": ["@types/node"],
"plugins": [
{
"name": "next"
}
],
"paths": {
- "@/*": [
- "./*"
- ]
+ "@/*": ["./*"]
}
},
"include": [
@@ -39,7 +31,5 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
- "exclude": [
- "node_modules"
- ],
-}
\ No newline at end of file
+ "exclude": ["node_modules"]
+}
diff --git a/types/github.ts b/types/github.ts
index 292e9e7..a3c55e9 100644
--- a/types/github.ts
+++ b/types/github.ts
@@ -1,4 +1,3 @@
-
export type RepoLanguageEdge = {
size: number;
node: {
@@ -58,8 +57,6 @@ export type PullRequestNode = {
};
};
-
-
export type GitHubUserData = {
login: string;
name: string | null;
diff --git a/types/i18n.ts b/types/i18n.ts
index 8e713dc..2255d68 100644
--- a/types/i18n.ts
+++ b/types/i18n.ts
@@ -1,10 +1,10 @@
import { Locale } from "../lib/i18n";
export type I18nContextValue = {
- locale: Locale;
- setLocale: (l: Locale) => void;
- t: (key: string, params?: Record) => string;
- dir: "ltr" | "rtl";
- locales: { value: Locale; label: string }[];
- ready: boolean;
-};
\ No newline at end of file
+ locale: Locale;
+ setLocale: (l: Locale) => void;
+ t: (key: string, params?: Record) => string;
+ dir: "ltr" | "rtl";
+ locales: { value: Locale; label: string }[];
+ ready: boolean;
+};
diff --git a/types/score.ts b/types/score.ts
index 7100eb8..1f7b750 100644
--- a/types/score.ts
+++ b/types/score.ts
@@ -1,9 +1,4 @@
-import {
- DiscussionNode,
- IssueNode,
- PullRequestNode,
- RepoNode,
-} from "./github";
+import { DiscussionNode, IssueNode, PullRequestNode, RepoNode } from "./github";
export type RepoScoreDetail = {
repo: RepoNode;