-
- {weeks.map((daysInWeek, weekIndex) => (
- // biome-ignore lint/suspicious/noArrayIndexKey:
-
- {daysInWeek.map((day, dayIndex) => {
- const finalColor = getContributionColor(day.contributionCount);
-
- // セルごとの遅延
- const cellDelay = (weekIndex * 7 + dayIndex) * 0.03;
-
- // 最後のセルかどうかを判定
- const isLastCell =
- weekIndex === weeks.length - 1 &&
- dayIndex === daysInWeek.length - 1;
-
- // スキップON or アニメーション終了後は最終色を即時表示
- const isFinal = skipAnimation || hasAnimationEnded;
-
- // スキップOFFかつアニメーションがまだ終わっていない場合のみ、animation適用
- const classNameWhenNotSkipped =
- "w-3 h-3 rounded-[2px] animate-fadeInBg";
- const classNameWhenSkipped = "w-3 h-3 rounded-[2px]";
-
- // スタイル切り替え
- const styleWhenAnimating = {
- ["--final-bg" as string]: finalColor,
- animationDelay: `${cellDelay}s`,
- };
- const styleWhenDone = {
- backgroundColor: finalColor,
- };
-
- return (
-
- key={dayIndex}
- className={cn(
- isFinal ? classNameWhenSkipped : classNameWhenNotSkipped,
- )}
- style={isFinal ? styleWhenDone : styleWhenAnimating}
- title={`${day.date}: ${day.contributionCount} contributions`}
- // 最後のセルだけアニメーション終了をキャッチして hasAnimationEnded = true にする
- onAnimationEnd={
- // スキップOFFかつまだアニメが終わっていなくて、かつ最後のセル
- !skipAnimation && !hasAnimationEnded && isLastCell
- ? handleLastCellAnimationEnd
- : undefined
- }
- />
- );
- })}
-
- ))}
-
-
-
-
-
-
- );
-};
-
-/**
- * アクティビティレベルのインジケータ
- */
-const ActivityLevelIndicator = () => {
- return (
-
-
Less
- {contributionColors.map(({ min, max, color }) => (
-
- ))}
-
More
-
- );
-};
-
-const contributionColors = [
- { min: 0, max: 0, color: "#ebedf0" },
- { min: 1, max: 11, color: "#9be9a8" },
- { min: 12, max: 22, color: "#40c463" },
- { min: 23, max: 34, color: "#30a14e" },
- { min: 35, max: Number.POSITIVE_INFINITY, color: "#216e39" },
-];
-
-const getContributionColor = (count: number) => {
- const colorObj = contributionColors.find(
- ({ min, max }) => count >= min && count <= max,
- );
- return colorObj ? colorObj.color : "#ebedf0";
-};
-
-/**
- * 連続した日付配列を「日曜始まり」に揃えてから、7日(1週間)ずつに区切る関数
- * ・最初の日の曜日を取得し、もし日曜でなければダミー要素を先頭に追加
- * ・7日ごとにチャンク
- */
-const chunkByWeek = (
- contributions: { date: string; contributionCount: number }[],
-) => {
- // 1. 日付昇順にソート
- const sorted = [...contributions].sort(
- (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
- );
-
- // 2. 先頭日の曜日を取得(0=日, 1=月, ... 6=土)
- const firstDay = new Date(sorted[0].date).getDay();
-
- // 3. 先頭日が日曜日でなければ、日曜スタートに合わせるためのダミーを追加
- const placeholders = [];
- for (let i = 0; i < firstDay; i++) {
- placeholders.push({
- date: "", // 日付なし
- contributionCount: 0,
- });
- }
-
- // 4. ダミーを先頭に連結
- const aligned = [...placeholders, ...sorted];
-
- // 5. 7日ごとに分割
- const chunked: { date: string; contributionCount: number }[][] = [];
- for (let i = 0; i < aligned.length; i += 7) {
- chunked.push(aligned.slice(i, i + 7));
- }
-
- return chunked;
-};
diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx
new file mode 100644
index 0000000..20333de
--- /dev/null
+++ b/components/ui/tooltip.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const TooltipProvider = TooltipPrimitive.Provider;
+
+const Tooltip = TooltipPrimitive.Root;
+
+const TooltipTrigger = TooltipPrimitive.Trigger;
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef
,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+TooltipContent.displayName = TooltipPrimitive.Content.displayName;
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
diff --git a/package.json b/package.json
index 6245e64..3fa8a0f 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"@octokit/graphql": "^8.1.1",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
+ "@radix-ui/react-tooltip": "^1.1.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.469.0",
From a1121ab184be7890858a11fe825485d61ed318d9 Mon Sep 17 00:00:00 2001
From: namidapoo <83203852+ve1997@users.noreply.github.com>
Date: Sun, 29 Dec 2024 08:26:45 +0900
Subject: [PATCH 06/17] =?UTF-8?q?feat:=20=E8=A8=80=E8=AA=9E=E4=BD=BF?=
=?UTF-8?q?=E7=94=A8=E7=8E=87=E3=81=AE=E8=A1=A8=E7=A4=BA=E8=BF=BD=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/globals.css | 10 +-
app/recap/components/graph/languages.tsx | 155 ++++++++++
components/ui/card.tsx | 83 ++++++
components/ui/chart.tsx | 363 +++++++++++++++++++++++
package.json | 1 +
5 files changed, 607 insertions(+), 5 deletions(-)
create mode 100644 app/recap/components/graph/languages.tsx
create mode 100644 components/ui/card.tsx
create mode 100644 components/ui/chart.tsx
diff --git a/app/globals.css b/app/globals.css
index 4667aef..bc94690 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -27,11 +27,11 @@ body {
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
- --chart-1: 12 76% 61%;
- --chart-2: 173 58% 39%;
- --chart-3: 197 37% 24%;
- --chart-4: 43 74% 66%;
- --chart-5: 27 87% 67%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
--radius: 0.5rem;
}
.dark {
diff --git a/app/recap/components/graph/languages.tsx b/app/recap/components/graph/languages.tsx
new file mode 100644
index 0000000..f269407
--- /dev/null
+++ b/app/recap/components/graph/languages.tsx
@@ -0,0 +1,155 @@
+"use client";
+import type { Stats } from "@/app/recap/fetchGitHubStats";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import {
+ type ChartConfig,
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
+import { useMemo } from "react";
+import type { FC } from "react";
+import { Label, Pie, PieChart } from "recharts";
+
+type Props = {
+ data: Stats["languagesByCommitCount"];
+};
+
+export const LanguagesUsageGraph: FC = ({ data }) => {
+ const limitedData = useMemo(() => {
+ if (data.length <= 6) {
+ return data;
+ }
+ const mainItems = data.slice(0, 5);
+ const others = data.slice(5);
+
+ const othersCommitSum = others.reduce(
+ (acc, item) => acc + item.commitCount,
+ 0,
+ );
+
+ return [
+ ...mainItems,
+ {
+ language: "Others",
+ commitCount: othersCommitSum,
+ },
+ ];
+ }, [data]);
+
+ const colorPalette = useMemo(
+ () => [
+ "hsl(var(--chart-1))",
+ "hsl(var(--chart-2))",
+ "hsl(var(--chart-3))",
+ "hsl(var(--chart-4))",
+ "hsl(var(--chart-5))",
+ "hsl(var(--chart-6))",
+ ],
+ [],
+ );
+
+ const totalCommits = useMemo(() => {
+ return limitedData.reduce((acc, curr) => acc + curr.commitCount, 0);
+ }, [limitedData]);
+
+ const chartData = useMemo(() => {
+ if (totalCommits === 0) {
+ return limitedData.map((item, index) => ({
+ language: item.language,
+ share: 0,
+ fill: colorPalette[index % colorPalette.length],
+ }));
+ }
+ return limitedData.map((item, index) => ({
+ language: item.language,
+ share: (item.commitCount / totalCommits) * 100,
+ fill: colorPalette[index % colorPalette.length],
+ }));
+ }, [limitedData, totalCommits, colorPalette]);
+
+ const chartConfig = useMemo(() => {
+ const dynamicLangConfig = limitedData.reduce((acc, item, index) => {
+ acc[item.language] = {
+ label: item.language,
+ color: colorPalette[index % colorPalette.length],
+ };
+ return acc;
+ }, {} as ChartConfig);
+
+ return {
+ visitors: {
+ label: "Share",
+ },
+ ...dynamicLangConfig,
+ };
+ }, [limitedData, colorPalette]);
+
+ return (
+
+
+ Yearly Language Usage
+
+ This chart represents your yearly commit share by language.
+
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+ );
+};
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000..6cee7ed
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,83 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = "Card";
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = "CardHeader";
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = "CardTitle";
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = "CardDescription";
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = "CardContent";
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = "CardFooter";
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardDescription,
+ CardContent,
+};
diff --git a/components/ui/chart.tsx b/components/ui/chart.tsx
new file mode 100644
index 0000000..ccfb8a4
--- /dev/null
+++ b/components/ui/chart.tsx
@@ -0,0 +1,363 @@
+"use client";
+
+import * as React from "react";
+import * as RechartsPrimitive from "recharts";
+
+import { cn } from "@/lib/utils";
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const;
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode;
+ icon?: React.ComponentType;
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ );
+};
+
+type ChartContextProps = {
+ config: ChartConfig;
+};
+
+const ChartContext = React.createContext(null);
+
+function useChart() {
+ const context = React.useContext(ChartContext);
+
+ if (!context) {
+ throw new Error("useChart must be used within a ");
+ }
+
+ return context;
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig;
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"];
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId();
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ );
+});
+ChartContainer.displayName = "Chart";
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color,
+ );
+
+ if (!colorConfig.length) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+
+const ChartTooltip = RechartsPrimitive.Tooltip;
+
+const ChartTooltipContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps &
+ React.ComponentProps<"div"> & {
+ hideLabel?: boolean;
+ hideIndicator?: boolean;
+ indicator?: "line" | "dot" | "dashed";
+ nameKey?: string;
+ labelKey?: string;
+ }
+>(
+ (
+ {
+ active,
+ payload,
+ className,
+ indicator = "dot",
+ hideLabel = false,
+ hideIndicator = false,
+ label,
+ labelFormatter,
+ labelClassName,
+ formatter,
+ color,
+ nameKey,
+ labelKey,
+ },
+ ref,
+ ) => {
+ const { config } = useChart();
+
+ const tooltipLabel = React.useMemo(() => {
+ if (hideLabel || !payload?.length) {
+ return null;
+ }
+
+ const [item] = payload;
+ const key = `${labelKey || item.dataKey || item.name || "value"}`;
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
+ const value =
+ !labelKey && typeof label === "string"
+ ? config[label as keyof typeof config]?.label || label
+ : itemConfig?.label;
+
+ if (labelFormatter) {
+ return (
+
+ {labelFormatter(value, payload)}
+
+ );
+ }
+
+ if (!value) {
+ return null;
+ }
+
+ return {value}
;
+ }, [
+ label,
+ labelFormatter,
+ payload,
+ hideLabel,
+ labelClassName,
+ config,
+ labelKey,
+ ]);
+
+ if (!active || !payload?.length) {
+ return null;
+ }
+
+ const nestLabel = payload.length === 1 && indicator !== "dot";
+
+ return (
+
+ {!nestLabel ? tooltipLabel : null}
+
+ {payload.map((item, index) => {
+ const key = `${nameKey || item.name || item.dataKey || "value"}`;
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
+ const indicatorColor = color || item.payload.fill || item.color;
+
+ return (
+
svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
+ indicator === "dot" && "items-center",
+ )}
+ >
+ {formatter && item?.value !== undefined && item.name ? (
+ formatter(item.value, item.name, item, index, item.payload)
+ ) : (
+ <>
+ {itemConfig?.icon ? (
+
+ ) : (
+ !hideIndicator && (
+
+ )
+ )}
+
+
+ {nestLabel ? tooltipLabel : null}
+
+ {itemConfig?.label || item.name}
+
+
+ {item.value && (
+
+ {item.value.toLocaleString()}
+
+ )}
+
+ >
+ )}
+
+ );
+ })}
+
+
+ );
+ },
+);
+ChartTooltipContent.displayName = "ChartTooltip";
+
+const ChartLegend = RechartsPrimitive.Legend;
+
+const ChartLegendContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> &
+ Pick & {
+ hideIcon?: boolean;
+ nameKey?: string;
+ }
+>(
+ (
+ { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
+ ref,
+ ) => {
+ const { config } = useChart();
+
+ if (!payload?.length) {
+ return null;
+ }
+
+ return (
+
+ {payload.map((item) => {
+ const key = `${nameKey || item.dataKey || "value"}`;
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
+
+ return (
+
svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
+ )}
+ >
+ {itemConfig?.icon && !hideIcon ? (
+
+ ) : (
+
+ )}
+ {itemConfig?.label}
+
+ );
+ })}
+
+ );
+ },
+);
+ChartLegendContent.displayName = "ChartLegend";
+
+// Helper to extract item config from a payload.
+function getPayloadConfigFromPayload(
+ config: ChartConfig,
+ payload: unknown,
+ key: string,
+) {
+ if (typeof payload !== "object" || payload === null) {
+ return undefined;
+ }
+
+ const payloadPayload =
+ "payload" in payload &&
+ typeof payload.payload === "object" &&
+ payload.payload !== null
+ ? payload.payload
+ : undefined;
+
+ let configLabelKey: string = key;
+
+ if (
+ key in payload &&
+ typeof payload[key as keyof typeof payload] === "string"
+ ) {
+ configLabelKey = payload[key as keyof typeof payload] as string;
+ } else if (
+ payloadPayload &&
+ key in payloadPayload &&
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
+ ) {
+ configLabelKey = payloadPayload[
+ key as keyof typeof payloadPayload
+ ] as string;
+ }
+
+ return configLabelKey in config
+ ? config[configLabelKey]
+ : config[key as keyof typeof config];
+}
+
+export {
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+ ChartLegend,
+ ChartLegendContent,
+ ChartStyle,
+};
diff --git a/package.json b/package.json
index 3fa8a0f..36e9c0f 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"next-themes": "^0.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "recharts": "^2.15.0",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7"
},
From f305784fa273e58c0991f57706ea81c4832b503a Mon Sep 17 00:00:00 2001
From: namidapoo <83203852+ve1997@users.noreply.github.com>
Date: Sun, 29 Dec 2024 08:27:22 +0900
Subject: [PATCH 07/17] =?UTF-8?q?feat:=20=E6=9C=88=E5=88=A5=E3=81=A8?=
=?UTF-8?q?=E6=9B=9C=E6=97=A5=E5=88=A5=E3=81=AE=E7=B5=B1=E8=A8=88=E8=A1=A8?=
=?UTF-8?q?=E7=A4=BA=E3=82=92=E8=BF=BD=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/recap/components/graph/monthly.tsx | 101 ++++++++++++++++++++++++
app/recap/components/graph/weekly.tsx | 102 +++++++++++++++++++++++++
2 files changed, 203 insertions(+)
create mode 100644 app/recap/components/graph/monthly.tsx
create mode 100644 app/recap/components/graph/weekly.tsx
diff --git a/app/recap/components/graph/monthly.tsx b/app/recap/components/graph/monthly.tsx
new file mode 100644
index 0000000..dc79717
--- /dev/null
+++ b/app/recap/components/graph/monthly.tsx
@@ -0,0 +1,101 @@
+"use client";
+import type { Stats } from "@/app/recap/fetchGitHubStats";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import {
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
+import { type FC, useMemo } from "react";
+import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
+
+const chartConfig = {
+ monthly: {
+ label: "Monthly",
+ },
+};
+
+type Props = {
+ monthlyData: Stats["monthlyContributions"];
+};
+
+export const MonthlyContributionsGraph: FC = ({ monthlyData }) => {
+ const chartData = useMemo(() => {
+ return monthlyData.map((item) => ({
+ x: item.month,
+ y: item.contributionCount,
+ }));
+ }, [monthlyData]);
+
+ const maxMonthData = useMemo(() => {
+ if (monthlyData.length === 0) {
+ return { month: "-", contributionCount: 0 };
+ }
+ return monthlyData.reduce((acc, curr) =>
+ curr.contributionCount > acc.contributionCount ? curr : acc,
+ );
+ }, [monthlyData]);
+
+ const dynamicConfig = useMemo(() => {
+ return { monthly: chartConfig.monthly };
+ }, []);
+
+ return (
+
+
+
+ Monthly Contributions
+
+ {maxMonthData.month}: {maxMonthData.contributionCount} Contributions
+
+
+
+
+
+
+
+
+
+
+ String(label)}
+ formatter={(value) => [
+
+ {value}
+ ,
+ "Contributions",
+ ]}
+ />
+ }
+ />
+
+
+
+
+
+ );
+};
diff --git a/app/recap/components/graph/weekly.tsx b/app/recap/components/graph/weekly.tsx
new file mode 100644
index 0000000..9fb2db6
--- /dev/null
+++ b/app/recap/components/graph/weekly.tsx
@@ -0,0 +1,102 @@
+"use client";
+import type { Stats } from "@/app/recap/fetchGitHubStats";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import {
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
+import { type FC, useMemo } from "react";
+import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
+
+const chartConfig = {
+ dayOfWeek: {
+ label: "Day of Week",
+ },
+};
+
+type Props = {
+ dayOfWeekData: Stats["averageContributionsByDayOfWeek"];
+};
+
+export const WeeklyContributionsGraph: FC = ({ dayOfWeekData }) => {
+ const chartData = useMemo(() => {
+ return dayOfWeekData.map((item) => ({
+ x: item.dayOfWeek,
+ y: item.averageContributions,
+ }));
+ }, [dayOfWeekData]);
+
+ const maxMonthData = useMemo(() => {
+ if (dayOfWeekData.length === 0) {
+ return { dayOfWeek: "-", averageContributions: 0 };
+ }
+ return dayOfWeekData.reduce((acc, curr) =>
+ curr.averageContributions > acc.averageContributions ? curr : acc,
+ );
+ }, [dayOfWeekData]);
+
+ const dynamicConfig = useMemo(() => {
+ return { dayOfWeek: chartConfig.dayOfWeek };
+ }, []);
+
+ return (
+
+
+
+ Average Contributions by Day of Week
+
+ {maxMonthData.dayOfWeek}: {maxMonthData.averageContributions}{" "}
+ Contributions
+
+
+
+
+
+
+
+
+
+
+ String(label)}
+ formatter={(value) => [
+
+ {value}
+ ,
+ "Contributions",
+ ]}
+ />
+ }
+ />
+
+
+
+
+
+ );
+};
From dcc0161c1d08992105780f48c971a957da08cea0 Mon Sep 17 00:00:00 2001
From: namidapoo <83203852+ve1997@users.noreply.github.com>
Date: Sun, 29 Dec 2024 08:31:53 +0900
Subject: [PATCH 08/17] =?UTF-8?q?feat:=20Overview=E8=BF=BD=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/recap/components/overview.tsx | 207 ++++++++++++++++++++++++++++++
app/recap/page.tsx | 13 +-
bun.lockb | Bin 204606 -> 239216 bytes
components/ui/avatar.tsx | 50 ++++++++
package.json | 3 +
5 files changed, 263 insertions(+), 10 deletions(-)
create mode 100644 app/recap/components/overview.tsx
create mode 100644 components/ui/avatar.tsx
diff --git a/app/recap/components/overview.tsx b/app/recap/components/overview.tsx
new file mode 100644
index 0000000..e1a8616
--- /dev/null
+++ b/app/recap/components/overview.tsx
@@ -0,0 +1,207 @@
+import { ContributionGrass } from "@/app/recap/components/grass";
+import type { Stats } from "@/app/recap/fetchGitHubStats";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { auth } from "@/lib/auth";
+import { format } from "date-fns";
+import {
+ GitCommitHorizontal,
+ GitPullRequestCreate,
+ GitPullRequestCreateArrow,
+ SquareKanban,
+ UsersRound,
+} from "lucide-react";
+import type { FC } from "react";
+import { LanguagesUsageGraph } from "./graph/languages";
+import { MonthlyContributionsGraph } from "./graph/monthly";
+import { WeeklyContributionsGraph } from "./graph/weekly";
+import { ReposContributions } from "./repos-contributions";
+
+type Props = {
+ data: Stats;
+};
+
+export const OverView: FC = async ({ data }) => {
+ const session = await auth();
+ if (!session) return null;
+
+ return (
+
+
+
+
+
+ {session.user?.login}
+
+
+
+
+ {session.user.login}
+
+
+ Joined on{" "}
+ {format(new Date(data.userProfile.joinedDate), "MMMM dd, yyyy")}
+
+
+
+
+
+ "{data.userProfile.bio}"
+
+
+
+ {data.userProfile.followersCount.toLocaleString()}{" "}
+ followers ·{" "}
+ {data.userProfile.followingCount.toLocaleString()}{" "}
+ following
+
+
+
+
+
+
+
+
+
+
+
+ Total Commits
+
+
+
+
+
+
+ {data.totalCommitCount.toLocaleString()}
+
+
+ +
+ {(
+ ((data.totalCommitCount -
+ data.previousYearStats.totalCommitCount) /
+ data.previousYearStats.totalCommitCount) *
+ 100
+ ).toFixed(2)}
+ % from last year
+
+
+
+
+
+
+ Completed Issues
+
+
+
+
+
+ {data.closedIssuesAssigned.toLocaleString()}
+
+
+ +
+ {(
+ ((data.closedIssuesAssigned -
+ data.previousYearStats.closedIssuesAssignedCount) /
+ data.previousYearStats.closedIssuesAssignedCount) *
+ 100
+ ).toFixed(2)}
+ % from last year
+
+
+
+
+
+ Created PRs
+
+
+
+
+ {data.openedPullRequests.toLocaleString()}
+
+
+ +
+ {(
+ ((data.openedPullRequests -
+ data.previousYearStats.openedPullRequests) /
+ data.previousYearStats.openedPullRequests) *
+ 100
+ ).toFixed(2)}
+ % from last year
+
+
+
+
+
+
+ Reviewed PRs
+
+
+
+
+
+ {data.reviewedPullRequests.toLocaleString()}
+
+
+ +
+ {(
+ ((data.reviewedPullRequests -
+ data.previousYearStats.reviewedPullRequests) /
+ data.previousYearStats.reviewedPullRequests) *
+ 100
+ ).toFixed(2)}
+ % from last year
+
+
+
+
+
+
+
+
+
+
+ Repositories with Contributions
+
+ Top 5 repositories by contributions count.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/app/recap/page.tsx b/app/recap/page.tsx
index 9c9f8de..39e8aa8 100644
--- a/app/recap/page.tsx
+++ b/app/recap/page.tsx
@@ -1,10 +1,10 @@
import { auth } from "@/lib/auth";
+import { OverView } from "./components/overview";
import { fetchGitHubStats } from "./fetchGitHubStats";
-import { GitHubGrass } from "./github-grass";
export const runtime = "edge";
-export default async function Page() {
+export default async function page() {
const session = await auth();
if (!session?.user) return null;
const token = process.env.GITHUB_TOKEN ?? session.accessToken;
@@ -14,12 +14,5 @@ export default async function Page() {
login: session.user.login,
});
- return (
-
-
Contributions for @{session.user.login}
-
-
-
-
- );
+ return ;
}
diff --git a/bun.lockb b/bun.lockb
index e10d9d31b833f62ead1dc4f0ce80e908996397f5..aaaa1d34a12e0fae9d6203c20a61de213b691558 100755
GIT binary patch
delta 57159
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