Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/calculate-next-country.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import "dotenv/config";

import { getDatabaseStore } from "@/lib/db";
import { calculateLeaderboard } from "@/features/leaderboard";
import { calculateLeaderboard } from "@/features/leaderboard/services";
import { logger } from "@/lib/logger";

let activeCountrySlug: string | null = null;
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/compare/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
createComparisonInsights,
parseSelectedLanguagesFromSearchParams,
resolveLocale,
} from "@/features/comparison";
} from "@/features/comparison/services";
import { toSafeApiError } from "@/lib/github";
import type { ClientSafeError, SafeApiError } from "@/types/api";

Expand Down
2 changes: 1 addition & 1 deletion src/app/api/leaderboard/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
import { getLeaderboardResult } from "@/features/leaderboard";
import { getLeaderboardResult } from "@/features/leaderboard/services";

export const runtime = "nodejs";

Expand Down
2 changes: 1 addition & 1 deletion src/app/api/user/[username]/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
import { getUserProfile, UserFetchError } from "@/features/developer";
import { getUserProfile, UserFetchError } from "@/features/developer/services";
import { normalizeSelectedLanguages } from "@/features/scoring";
import { toSafeApiError } from "@/lib/github";
import type { SafeApiError } from "@/types/api";
Expand Down
3 changes: 2 additions & 1 deletion src/app/leaderboard/[country]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Metadata } from "next";
import countriesData from "@/data/countries.json";
import { JsonLd } from "@/components/seo/json-ld";
import { getLeaderboardResult, CountryLeaderboardClient } from "@/features/leaderboard";
import { CountryLeaderboardClient } from "@/features/leaderboard";
import { getLeaderboardResult } from "@/features/leaderboard/services";
import { toAbsoluteUrl } from "@/lib/seo";

type CountryInfo = {
Expand Down
3 changes: 2 additions & 1 deletion src/app/user/[username]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { JsonLd } from "@/components/seo/json-ld";
import { UserProfileClient, UserNotFoundCard, getUserProfile } from "@/features/developer";
import { UserProfileClient, UserNotFoundCard } from "@/features/developer";
import { getUserProfile } from "@/features/developer/services";
import { AppHeader } from "@/components/layout/app-header";
import { AppFooter } from "@/components/layout/app-footer";
import { toAbsoluteUrl } from "@/lib/seo";
Expand Down
85 changes: 85 additions & 0 deletions src/components/cards/community-card-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"use client";

import { MessageSquare, Star } from "lucide-react";
import { useTranslation } from "@/components/providers/language-provider";
import type { UserResult } from "@/features/developer/types";
import { StatChip } from "./work-card-helpers";

export type CommunityCardItemProps = {
item: NonNullable<UserResult["topCommunityContributions"]>[number];
rankIndex?: number;
showRank?: boolean;
className?: string;
};

export function CommunityCardItem({
item,
rankIndex,
showRank = true,
className = "",
}: CommunityCardItemProps) {
const { t } = useTranslation();
const itemTitle = item.title || t("untitled");

return (
<article
className={`rounded-xl border border-border/80 bg-gradient-to-br from-card via-card to-muted/25 p-4 transition-all duration-200 hover:border-primary/40 hover:shadow-sm ${className}`}
>
<div className="flex items-start justify-between gap-3 sm:gap-4">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
{showRank && typeof rankIndex === "number" ? (
<span className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-[10px] font-bold text-muted-foreground">
#{rankIndex + 1}
</span>
) : null}

<span className="inline-flex rounded-full bg-secondary px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-secondary-foreground sm:text-[11px]">
{item.type === "issue" ? t("community.issue") : t("community.discussion")}
</span>
</div>

{item.url ? (
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="mt-1.5 block break-words font-semibold leading-snug text-primary hover:underline"
aria-label={t("a11y.openCommunityContribution", {
title: itemTitle,
})}
>
{itemTitle}
</a>
) : (
<p className="mt-1.5 break-words font-semibold leading-snug">{itemTitle}</p>
)}

<p className="mt-1 break-words text-xs font-medium text-muted-foreground">
{item.repo || t("unknown.repo")}
</p>

<div className="mt-2.5 flex flex-wrap gap-1.5">
<StatChip
icon={<Star className="h-3 w-3" />}
label={t("topwork.stars")}
value={item.stars ?? 0}
/>
<StatChip
icon={<MessageSquare className="h-3 w-3" />}
label={t("community.comments")}
value={item.comments ?? 0}
/>
</div>
</div>

<div className="shrink-0 rounded-lg border border-primary/25 bg-primary/5 px-2.5 py-1.5 text-right sm:px-3 sm:py-2">
<p className="text-lg font-bold text-primary sm:text-xl">{item.score ?? 0}</p>
<p className="text-[10px] text-muted-foreground sm:text-[11px]">
{t("comparsion.score")}
</p>
</div>
</div>
</article>
);
}
5 changes: 5 additions & 0 deletions src/components/cards/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./work-card-helpers";
export * from "./repo-card-item";
export * from "./pr-card-item";
export * from "./community-card-item";
export * from "./score-card";
107 changes: 107 additions & 0 deletions src/components/cards/pr-card-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"use client";

import { ArrowDown, ArrowUp, Star } from "lucide-react";
import { useTranslation } from "@/components/providers/language-provider";
import type { UserResult } from "@/features/developer/types";
import {
formatLanguageMatch,
LanguageBreakdown,
SelectedLanguageRow,
StatChip,
} from "./work-card-helpers";

export type PullRequestCardItemProps = {
pr: UserResult["topPullRequests"][number];
rankIndex?: number;
selectedLanguages?: string[];
showRank?: boolean;
className?: string;
};

export function PullRequestCardItem({
pr,
rankIndex,
selectedLanguages,
showRank = true,
className = "",
}: PullRequestCardItemProps) {
const { t } = useTranslation();
const prTitle = pr.title || t("untitled");
const targetRepo = pr.repo || t("unknown.repo");

return (
<article
className={`rounded-xl border border-border/80 bg-gradient-to-br from-card via-card to-muted/25 p-4 transition-all duration-200 hover:border-primary/40 hover:shadow-sm ${className}`}
>
<div className="flex items-start justify-between gap-3 sm:gap-4">
<div className="min-w-0 flex-1">
<div className="flex items-start gap-2">
{showRank && typeof rankIndex === "number" ? (
<span className="mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-[10px] font-bold text-muted-foreground">
#{rankIndex + 1}
</span>
) : null}

{pr.url ? (
<a
href={pr.url}
target="_blank"
rel="noopener noreferrer"
className="break-words font-semibold leading-snug text-primary hover:underline"
aria-label={t("a11y.openPullRequest", { title: prTitle })}
>
{prTitle}
</a>
) : (
<p className="break-words font-semibold leading-snug">{prTitle}</p>
)}
</div>

<p className="mt-1 break-words text-xs font-medium text-muted-foreground">
{t("topwork.inRepo", { repo: targetRepo })}
</p>

<div className="mt-2.5 flex flex-wrap items-center gap-1.5">
<StatChip
icon={<Star className="h-3 w-3" />}
label={t("topwork.pr.repo.stars")}
value={pr.stars ?? 0}
/>
<div className="inline-flex items-center gap-1.5 rounded-full border border-border/80 bg-background/80 px-2.5 py-1 text-xs">
<span className="inline-flex items-center gap-0.5 font-semibold text-emerald-600 dark:text-emerald-400">
<ArrowUp className="h-3 w-3" />+{pr.additions ?? 0}
</span>
<span className="text-muted-foreground">/</span>
<span className="inline-flex items-center gap-0.5 font-semibold text-rose-600 dark:text-rose-400">
<ArrowDown className="h-3 w-3" />-{pr.deletions ?? 0}
</span>
</div>
</div>

<LanguageBreakdown topLanguages={pr.topLanguages} />

{selectedLanguages && selectedLanguages.length > 0 ? (
<SelectedLanguageRow
topLanguages={pr.topLanguages}
selectedLanguages={selectedLanguages}
label={t("topwork.selectedLang")}
/>
) : null}

{typeof pr.languageMatch === "number" ? (
<p className="mt-2 text-xs text-muted-foreground">
{t("language.match")}: {formatLanguageMatch(pr.languageMatch)}
</p>
) : null}
</div>

<div className="shrink-0 rounded-lg border border-primary/25 bg-primary/5 px-2.5 py-1.5 text-right sm:px-3 sm:py-2">
<p className="text-lg font-bold text-primary sm:text-xl">{pr.score ?? 0}</p>
<p className="text-[10px] text-muted-foreground sm:text-[11px]">
{t("comparsion.score")}
</p>
</div>
</div>
</article>
);
}
103 changes: 103 additions & 0 deletions src/components/cards/repo-card-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"use client";

import { Eye, GitFork, Star } from "lucide-react";
import { useTranslation } from "@/components/providers/language-provider";
import type { UserResult } from "@/features/developer/types";
import {
formatLanguageMatch,
LanguageBreakdown,
SelectedLanguageRow,
StatChip,
} from "./work-card-helpers";

export type RepoCardItemProps = {
repo: UserResult["topRepos"][number];
rankIndex?: number;
selectedLanguages?: string[];
showRank?: boolean;
className?: string;
};

export function RepoCardItem({
repo,
rankIndex,
selectedLanguages,
showRank = true,
className = "",
}: RepoCardItemProps) {
const { t } = useTranslation();
const repoName = repo.name || t("untitled");

return (
<article
className={`rounded-xl border border-border/80 bg-gradient-to-br from-card via-card to-muted/25 p-4 transition-all duration-200 hover:border-primary/40 hover:shadow-sm ${className}`}
>
<div className="flex items-start justify-between gap-3 sm:gap-4">
<div className="min-w-0 flex-1">
<div className="flex items-start gap-2">
{showRank && typeof rankIndex === "number" ? (
<span className="mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-[10px] font-bold text-muted-foreground">
#{rankIndex + 1}
</span>
) : null}

{repo.url ? (
<a
href={repo.url}
target="_blank"
rel="noopener noreferrer"
className="break-words font-semibold leading-snug text-primary hover:underline"
aria-label={t("a11y.openRepo", { name: repoName })}
>
{repoName}
</a>
) : (
<p className="break-words font-semibold leading-snug">{repoName}</p>
)}
</div>

<div className="mt-2.5 flex flex-wrap gap-1.5">
<StatChip
icon={<Star className="h-3 w-3" />}
label={t("topwork.stars")}
value={repo.stars ?? 0}
/>
<StatChip
icon={<GitFork className="h-3 w-3" />}
label={t("topwork.forks")}
value={repo.forks ?? 0}
/>
<StatChip
icon={<Eye className="h-3 w-3" />}
label={t("topwork.watchers")}
value={repo.watchers ?? 0}
/>
</div>

<LanguageBreakdown topLanguages={repo.topLanguages} />

{selectedLanguages && selectedLanguages.length > 0 ? (
<SelectedLanguageRow
topLanguages={repo.topLanguages}
selectedLanguages={selectedLanguages}
label={t("topwork.selectedLang")}
/>
) : null}

{typeof repo.languageMatch === "number" ? (
<p className="mt-2 text-xs text-muted-foreground">
{t("language.match")}: {formatLanguageMatch(repo.languageMatch)}
</p>
) : null}
</div>

<div className="shrink-0 rounded-lg border border-primary/25 bg-primary/5 px-2.5 py-1.5 text-right sm:px-3 sm:py-2">
<p className="text-lg font-bold text-primary sm:text-xl">{repo.score ?? 0}</p>
<p className="text-[10px] text-muted-foreground sm:text-[11px]">
{t("comparsion.score")}
</p>
</div>
</div>
</article>
);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"use client";

import { cn } from "@/utils/cn";
import { useTranslation } from "@/components/providers/language-provider";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";

type ScoreCardProps = {
export type ScoreCardProps = {
title: string;
rawValue: number;
normalizedValue?: number;
Expand Down
Loading
Loading