diff --git a/apps/fronted/src/components/editor/basic/BasicPanel.tsx b/apps/fronted/src/components/editor/basic/BasicPanel.tsx index b81f6d9..24e56a6 100644 --- a/apps/fronted/src/components/editor/basic/BasicPanel.tsx +++ b/apps/fronted/src/components/editor/basic/BasicPanel.tsx @@ -3,14 +3,15 @@ import React, { useState } from "react"; import { PlusCircle, GripVertical, Trash2, Eye, EyeOff } from "lucide-react"; import { Reorder, AnimatePresence, motion } from "framer-motion"; import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; import PhotoUpload from "@/components/shared/PhotoSelector"; +import IconSelector from "../IconSelector"; import Field from "../Field"; +import { cn } from "@/lib/utils"; +import { DEFAULT_FIELD_ORDER } from "@/config"; import { useResumeStore } from "@/store/useResumeStore"; import { BasicFieldType, CustomFieldType } from "@/types/resume"; -import { DEFAULT_FIELD_ORDER } from "@/config"; -import IconSelector from "../IconSelector"; - interface CustomFieldProps { field: CustomFieldType; onUpdate: (field: CustomFieldType) => void; @@ -347,7 +348,6 @@ const BasicPanel: React.FC = () => { ))} - { + + + + + Github 贡献图 + + + + updateBasicInfo({ + ...basic, + githubContributionsVisible: checked, + }) + } + /> + + + + + Access Token + + updateBasicInfo({ + ...basic, + githubKey: e.target.value, + }) + } + /> + + + UseName + + updateBasicInfo({ + ...basic, + githubUseName: e.target.value, + }) + } + /> + + + + ); diff --git a/apps/fronted/src/components/preview/BaseInfo.tsx b/apps/fronted/src/components/preview/BaseInfo.tsx index 579e62c..49601d2 100644 --- a/apps/fronted/src/components/preview/BaseInfo.tsx +++ b/apps/fronted/src/components/preview/BaseInfo.tsx @@ -137,7 +137,6 @@ export function BaseInfo({ )} - {/* 职位 - 仅在可见时显示 */} {titleField && basic[titleField.key] && ( { switch (fontFamily) { case "serif": @@ -280,6 +281,15 @@ export function PreviewPanel() { + {basic?.githubContributionsVisible && ( + + + + )} + {menuSections .filter((section) => section.enabled) .sort((a, b) => a.order - b.order) diff --git a/apps/fronted/src/components/shared/GithubContribution.tsx b/apps/fronted/src/components/shared/GithubContribution.tsx new file mode 100644 index 0000000..a530a11 --- /dev/null +++ b/apps/fronted/src/components/shared/GithubContribution.tsx @@ -0,0 +1,240 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { cn } from "@/lib/utils"; + +interface ContributionDay { + date: string; + count: number; +} + +interface GithubContributionsProps { + username?: string; + githubKey?: string; + className?: string; + year?: number; +} + +const colorLevels = [ + "bg-[#ebedf0]", // 0 contributions + "bg-[#9be9a8]", // 1-3 contributions + "bg-[#40c463]", // 4-7 contributions + "bg-[#30a14e]", // 8-12 contributions + "bg-[#216e39]", // 13+ contributions +]; + +const getColorLevel = (count: number): string => { + if (count === 0) return colorLevels[0]; + if (count <= 3) return colorLevels[1]; + if (count <= 7) return colorLevels[2]; + if (count <= 12) return colorLevels[3]; + return colorLevels[4]; +}; + +const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleDateString("zh-CN", { + year: "numeric", + month: "long", + day: "numeric", + weekday: "long", + }); +}; + +async function fetchGithubContributions( + username?: string, + githubKey?: string +): Promise { + const token = githubKey; + + if (!token) { + throw new Error("GitHub token is required"); + } + + const query = ` + query($username: String!) { + user(login: $username) { + contributionsCollection { + contributionCalendar { + totalContributions + weeks { + contributionDays { + contributionCount + date + } + } + } + } + } + } + `; + + try { + const response = await fetch("https://api.github.com/graphql", { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query, + variables: { username }, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + console.error("GitHub API Error:", errorData); + throw new Error(errorData.message || "Failed to fetch GitHub data"); + } + + const data = await response.json(); + + if (data.errors) { + console.error("GitHub API Error:", data.errors); + throw new Error(data.errors[0]?.message || "GitHub API Error"); + } + + const calendar = + data.data?.user?.contributionsCollection?.contributionCalendar; + + if (!calendar) { + throw new Error("No contribution data found"); + } + + const contributions: ContributionDay[] = []; + + calendar.weeks.forEach((week: any) => { + week.contributionDays.forEach((day: any) => { + contributions.push({ + date: day.date, + count: day.contributionCount, + }); + }); + }); + + return contributions; + } catch (error) { + console.error("Error fetching GitHub contributions:", error); + throw error; + } +} + +const GithubContributions: React.FC = ({ + username, + githubKey, + className, + year = 2024, +}) => { + const [weeks, setWeeks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function loadContributions() { + try { + setLoading(true); + const contributions = await fetchGithubContributions( + username, + githubKey + ); + + const yearContributions = contributions.filter((day) => { + const date = new Date(day.date); + return date.getFullYear() === year; + }); + + const groupedWeeks: ContributionDay[][] = []; + let currentWeek: ContributionDay[] = []; + + yearContributions.forEach((day, index) => { + currentWeek.push(day); + if ( + currentWeek.length === 7 || + index === yearContributions.length - 1 + ) { + // 如果是最后一周且不满7天,用空数据填充 + if (currentWeek.length < 7) { + const emptyDays = 7 - currentWeek.length; + for (let i = 0; i < emptyDays; i++) { + currentWeek.push({ + date: "", + count: 0, + }); + } + } + groupedWeeks.push([...currentWeek]); + currentWeek = []; + } + }); + + setWeeks(groupedWeeks); + setError(null); + } catch (err) { + setError("Failed to load GitHub contributions"); + } finally { + setLoading(false); + } + } + + if (username) { + loadContributions(); + } + }, [username, year]); + + if (loading) { + return ; + } + + if (error) { + return {error}; + } + + const goGithub = () => { + window.open(`https://github.com/${username}`, "_blank"); + }; + + return ( + + + {weeks.map((week, weekIndex) => ( + + {week.map((day, dayIndex) => ( + + {day.date && ( + + {formatDate(day.date)}: {day.count} contributions + + + )} + + ))} + + ))} + + + ); +}; + +export default GithubContributions; diff --git a/apps/fronted/src/store/useResumeStore.ts b/apps/fronted/src/store/useResumeStore.ts index 43066a3..9a75de6 100644 --- a/apps/fronted/src/store/useResumeStore.ts +++ b/apps/fronted/src/store/useResumeStore.ts @@ -102,6 +102,9 @@ const initialResumeState: Omit = { }, ], photo: "/avatar.svg", + githubKey: "", + githubUseName: "", + githubContributionsVisible: false, }, education: [ { diff --git a/apps/fronted/src/types/resume.ts b/apps/fronted/src/types/resume.ts index f2edd85..50d2108 100644 --- a/apps/fronted/src/types/resume.ts +++ b/apps/fronted/src/types/resume.ts @@ -78,6 +78,9 @@ export interface BasicInfo { icon?: string; visible?: boolean; }>; + githubKey: string; + githubUseName: string; + githubContributionsVisible: boolean; } export interface Education {