Skip to content

Commit 29ea716

Browse files
zmrlftCail Gainey
andauthored
feat: built-in character patterns to real-time preview drawing
Co-authored-by: Cail Gainey <[email protected]>
1 parent 06c108d commit 29ea716

File tree

6 files changed

+520
-7
lines changed

6 files changed

+520
-7
lines changed

frontend/src/components/CalendarControls.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from "react";
22
import clsx from "clsx";
33
import { useTranslations } from "../i18n";
4+
import { CharacterSelector } from "./CharacterSelector";
45

56
type Props = {
67
year?: number;
@@ -19,6 +20,10 @@ type Props = {
1920
isGeneratingRepo?: boolean;
2021
onExportContributions?: () => void;
2122
onImportContributions?: () => void;
23+
// 字符预览相关
24+
onStartCharacterPreview?: (char: string) => void;
25+
previewMode?: boolean;
26+
onCancelCharacterPreview?: () => void;
2227
};
2328

2429
export const CalendarControls: React.FC<Props> = ({
@@ -38,12 +43,19 @@ export const CalendarControls: React.FC<Props> = ({
3843
isGeneratingRepo,
3944
onExportContributions,
4045
onImportContributions,
46+
// 字符预览相关
47+
onStartCharacterPreview,
48+
previewMode,
49+
onCancelCharacterPreview,
4150
}) => {
4251
const { t } = useTranslations();
4352
const [yearInput, setYearInput] = React.useState<string>(() =>
4453
typeof year === "number" ? String(year) : "",
4554
);
4655

56+
// 字符选择状态
57+
const [showCharacterSelector, setShowCharacterSelector] = React.useState<boolean>(false);
58+
4759
React.useEffect(() => {
4860
setYearInput(typeof year === "number" ? String(year) : "");
4961
}, [year]);
@@ -85,6 +97,20 @@ export const CalendarControls: React.FC<Props> = ({
8597
onGenerateRepo();
8698
};
8799

100+
const handleCharacterSelect = (char: string) => {
101+
if (onStartCharacterPreview) {
102+
onStartCharacterPreview(char);
103+
}
104+
};
105+
106+
const handleCharacterButtonClick = () => {
107+
if (previewMode && onCancelCharacterPreview) {
108+
onCancelCharacterPreview();
109+
} else {
110+
setShowCharacterSelector(true);
111+
}
112+
};
113+
88114
return (
89115
<div className="flex w-full flex-col gap-4">
90116
<div className="flex w-full flex-col gap-4 sm:flex-row sm:flex-nowrap sm:gap-4">
@@ -183,6 +209,23 @@ export const CalendarControls: React.FC<Props> = ({
183209
</div>
184210
</div>
185211

212+
<div className="flex w-full flex-col space-y-2 sm:w-auto">
213+
<span className="text-sm font-medium text-black">{t("characterSelector.characterTool")}</span>
214+
<button
215+
type="button"
216+
onClick={handleCharacterButtonClick}
217+
className={clsx(
218+
"flex w-full items-center justify-center gap-2 rounded-none px-3 py-2 text-sm font-medium transition-all duration-200 sm:w-auto",
219+
previewMode
220+
? "scale-105 transform bg-orange-600 text-white shadow-lg"
221+
: "border border-black bg-white text-black hover:bg-gray-100",
222+
)}
223+
title={previewMode ? t("characterSelector.cancelPreview") : t("characterSelector.character")}
224+
>
225+
{previewMode ? t("characterSelector.cancelPreview") : t("characterSelector.character")}
226+
</button>
227+
</div>
228+
186229
<div className="flex w-full flex-col gap-2 sm:ml-auto sm:w-auto sm:items-end">
187230
<span className="text-sm font-medium text-black sm:invisible">{t("labels.dataActions")}</span>
188231
<div className="flex w-full flex-col gap-2 sm:flex-row sm:justify-end sm:gap-3">
@@ -241,6 +284,14 @@ export const CalendarControls: React.FC<Props> = ({
241284
</div>
242285
</div>
243286
</div>
287+
288+
{/* 字符选择弹窗 */}
289+
{showCharacterSelector && (
290+
<CharacterSelector
291+
onSelect={handleCharacterSelect}
292+
onClose={() => setShowCharacterSelector(false)}
293+
/>
294+
)}
244295
</div>
245296
);
246297
};
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import React from "react";
2+
import clsx from "clsx";
3+
import { characterPatterns, getPatternsByCategory, type CharacterPattern } from "../data/characterPatterns";
4+
import { useTranslations } from "../i18n";
5+
6+
type CharacterTab = CharacterPattern['category'];
7+
8+
type Props = {
9+
onSelect: (char: string) => void;
10+
onClose: () => void;
11+
};
12+
13+
/**
14+
* 字符选择弹窗组件
15+
* 显示A-Z、a-z、0-9和符号的选择界面,每个字符显示为像素图案预览
16+
*/
17+
export const CharacterSelector: React.FC<Props> = ({ onSelect, onClose }) => {
18+
const { t } = useTranslations();
19+
const [activeTab, setActiveTab] = React.useState<CharacterTab>('uppercase');
20+
21+
// 获取当前标签页的字符图案
22+
const currentPatterns = React.useMemo(() => {
23+
return getPatternsByCategory(activeTab);
24+
}, [activeTab]);
25+
26+
const handleCharacterClick = (char: string) => {
27+
onSelect(char);
28+
onClose();
29+
};
30+
31+
return (
32+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
33+
<div className="relative w-full max-w-4xl rounded-none border-2 border-black bg-white shadow-2xl">
34+
{/* 标题栏 */}
35+
<div className="flex items-center justify-between border-b-2 border-black px-6 py-4">
36+
<h2 className="text-2xl font-bold text-black">{t('characterSelector.title')}</h2>
37+
<button
38+
onClick={onClose}
39+
className="text-gray-500 hover:text-black transition-colors"
40+
aria-label={t('gitInstall.close')}
41+
>
42+
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
43+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
44+
</svg>
45+
</button>
46+
</div>
47+
48+
{/* 标签页 */}
49+
<div className="flex gap-2 border-b border-gray-200 px-6 pt-4">
50+
<button
51+
onClick={() => setActiveTab('uppercase')}
52+
className={clsx(
53+
"rounded-t-lg px-6 py-2 text-sm font-medium transition-all",
54+
activeTab === 'uppercase'
55+
? "bg-black text-white"
56+
: "bg-white text-black hover:bg-gray-100"
57+
)}
58+
>
59+
{t('characterSelector.tabUppercase')}
60+
</button>
61+
<button
62+
onClick={() => setActiveTab('lowercase')}
63+
className={clsx(
64+
"rounded-t-lg px-6 py-2 text-sm font-medium transition-all",
65+
activeTab === 'lowercase'
66+
? "bg-black text-white"
67+
: "bg-white text-black hover:bg-gray-100"
68+
)}
69+
>
70+
{t('characterSelector.tabLowercase')}
71+
</button>
72+
<button
73+
onClick={() => setActiveTab('numbers')}
74+
className={clsx(
75+
"rounded-t-lg px-6 py-2 text-sm font-medium transition-all",
76+
activeTab === 'numbers'
77+
? "bg-black text-white"
78+
: "bg-white text-black hover:bg-gray-100"
79+
)}
80+
>
81+
{t('characterSelector.tabNumbers')}
82+
</button>
83+
<button
84+
onClick={() => setActiveTab('symbols')}
85+
className={clsx(
86+
"rounded-t-lg px-6 py-2 text-sm font-medium transition-all",
87+
activeTab === 'symbols'
88+
? "bg-black text-white"
89+
: "bg-white text-black hover:bg-gray-100"
90+
)}
91+
>
92+
{t('characterSelector.tabSymbols')}
93+
</button>
94+
</div>
95+
96+
{/* 字符网格 */}
97+
<div className="max-h-[500px] overflow-y-auto p-6">
98+
<div className="grid grid-cols-4 gap-4 sm:grid-cols-6 md:grid-cols-8">
99+
{currentPatterns.map((pattern) => {
100+
return (
101+
<button
102+
key={pattern.id}
103+
onClick={() => handleCharacterClick(pattern.id)}
104+
className="group flex flex-col items-center gap-2 rounded-lg border-2 border-gray-200 bg-white p-3 transition-all hover:border-black hover:shadow-lg"
105+
title={`${t('characterSelector.selectCharacter')} ${pattern.name}`}
106+
>
107+
{/* 字符标签 */}
108+
<div className="text-sm font-bold text-black">{pattern.name}</div>
109+
110+
{/* 像素预览 */}
111+
<div className="grid grid-cols-5 gap-[2px] rounded-md bg-gray-100 p-1">
112+
{pattern.grid.map((row, y) =>
113+
row.map((pixel, x) => (
114+
<div
115+
key={`${y}-${x}`}
116+
className={clsx(
117+
"h-2.5 w-2.5 rounded-sm transition-all duration-200",
118+
pixel === 1
119+
? "bg-[#216e39] shadow-sm group-hover:bg-[#1a5a2e] group-hover:scale-110"
120+
: "bg-[#ebedf0] group-hover:bg-gray-300"
121+
)}
122+
style={{
123+
boxShadow: pixel === 1 ? '0 1px 2px rgba(33, 110, 57, 0.2)' : 'none'
124+
}}
125+
/>
126+
))
127+
)}
128+
</div>
129+
</button>
130+
);
131+
})}
132+
</div>
133+
</div>
134+
</div>
135+
</div>
136+
);
137+
};

frontend/src/components/ContributionCalendar.module.scss

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@import "../css/mixins.scss";
1+
@import "../css/mixins.scss";
22
.container {
33
--cell: 10px;
44
--gap: 3px;
@@ -144,6 +144,27 @@
144144
box-shadow: none;
145145
z-index: 10;
146146
}
147+
148+
// 预览状态样式
149+
&.preview {
150+
background: #216e39 !important;
151+
border-color: rgba(33, 110, 57, 0.8) !important;
152+
outline: 2px solid #ff6b35 !important;
153+
outline-offset: 1px;
154+
animation: pulse 1.5s infinite;
155+
}
156+
157+
@keyframes pulse {
158+
0% {
159+
opacity: 1;
160+
}
161+
50% {
162+
opacity: 0.7;
163+
}
164+
100% {
165+
opacity: 1;
166+
}
167+
}
147168
}
148169

149170
.total {

0 commit comments

Comments
 (0)