Skip to content

Commit 055fa9d

Browse files
author
Gauge Developer
committed
feat: add share functionality with share card generation
- Add dom-to-image library for rendering share cards - Create ShareModal component with modal dialog - Create ShareCard component with beautiful golden theme design - Add share button to ExperienceBar - Implement image generation, download, and copy to clipboard features - Add i18n translations for share feature in zh and en
1 parent bb865c0 commit 055fa9d

11 files changed

Lines changed: 1066 additions & 8 deletions

File tree

package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"@types/react-dom": "^18.2.0",
1010
"ajv": "^8.17.1",
1111
"d3": "^7.8.5",
12+
"dom-to-image": "^2.6.0",
1213
"i18next": "^23.11.5",
1314
"i18next-browser-languagedetector": "^7.2.1",
1415
"js-yaml": "^4.1.1",
@@ -58,6 +59,7 @@
5859
"devDependencies": {
5960
"@testing-library/jest-dom": "^6.9.1",
6061
"@testing-library/react": "^16.3.1",
62+
"@types/dom-to-image": "^2.6.7",
6163
"@types/jest": "^30.0.0",
6264
"@types/js-yaml": "^4.0.9",
6365
"@types/react-router-dom": "^5.3.3",

src/components/ExperienceBar/ExperienceBar.css

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,47 @@
195195
display: flex;
196196
flex-direction: column;
197197
gap: 6px;
198+
position: relative;
199+
}
200+
201+
/* 分享按钮 */
202+
.share-button {
203+
position: absolute;
204+
top: 0;
205+
right: 0;
206+
display: flex;
207+
align-items: center;
208+
gap: 4px;
209+
padding: 6px 12px;
210+
background: linear-gradient(135deg, rgba(245, 158, 11, 0.15) 0%, rgba(251, 191, 36, 0.2) 100%);
211+
border: 1px solid rgba(245, 158, 11, 0.4);
212+
border-radius: 20px;
213+
cursor: pointer;
214+
transition: all 0.2s ease;
215+
z-index: 10;
216+
}
217+
218+
.share-button svg {
219+
width: 14px;
220+
height: 14px;
221+
color: #b45309;
222+
}
223+
224+
.share-button span {
225+
font-size: 12px;
226+
font-weight: 600;
227+
color: #92400e;
228+
}
229+
230+
.share-button:hover {
231+
background: linear-gradient(135deg, rgba(245, 158, 11, 0.25) 0%, rgba(251, 191, 36, 0.3) 100%);
232+
border-color: rgba(245, 158, 11, 0.6);
233+
transform: translateY(-1px);
234+
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
235+
}
236+
237+
.share-button:active {
238+
transform: translateY(0);
198239
}
199240

200241
/* 题目完成进度 */
@@ -647,4 +688,12 @@
647688
.exp-bar-wrapper {
648689
width: 100%;
649690
}
691+
692+
.share-button {
693+
position: relative;
694+
top: auto;
695+
right: auto;
696+
align-self: flex-end;
697+
margin-bottom: 8px;
698+
}
650699
}

src/components/ExperienceBar/ExperienceBar.tsx

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { useTranslation } from 'react-i18next';
33
import { experienceAdapter } from '../../services/experience-adapter';
44
import { ExperienceRecord } from '../../services/experienceStorage';
55
import RealmHelpTooltip from './RealmHelpTooltip';
6+
import ShareModal from '../ShareModal';
7+
import { learningPaths } from '../ProblemList/data/learningPaths';
8+
import { useCompletionStatus } from '../ProblemList/hooks/useCompletionStatus';
69
import './ExperienceBar.css';
710

811
interface ExperienceBarProps {
@@ -39,8 +42,8 @@ const REALMS: RealmInfo[] = [
3942
{ name: '飞升仙界', nameEn: 'Ascension', translationKey: 'ascension', color: '#fde68a', icon: '✨', bgGradient: 'linear-gradient(135deg, #0f1419 0%, #1a2332 50%, #0d1117 100%)' },
4043
];
4144

42-
const ExperienceBar: React.FC<ExperienceBarProps> = ({
43-
currentLang,
45+
const ExperienceBar: React.FC<ExperienceBarProps> = ({
46+
currentLang,
4447
refreshTrigger,
4548
completedProblems = 0,
4649
totalProblems = 100
@@ -56,8 +59,11 @@ const ExperienceBar: React.FC<ExperienceBarProps> = ({
5659
const [expGainAmount, setExpGainAmount] = useState(0);
5760
const [showHelpTooltip, setShowHelpTooltip] = useState(false);
5861
const [helpIconRect, setHelpIconRect] = useState<DOMRect | null>(null);
62+
const [showShareModal, setShowShareModal] = useState(false);
5963
const helpIconRef = useRef<HTMLDivElement>(null);
6064

65+
const { getStatsForProblems } = useCompletionStatus();
66+
6167
const loadExperience = useCallback(async () => {
6268
try {
6369
const exp = await experienceAdapter.getTotalExperience();
@@ -111,6 +117,22 @@ const ExperienceBar: React.FC<ExperienceBarProps> = ({
111117
// 计算题目完成百分比
112118
const problemPercentage = totalProblems > 0 ? Math.round((completedProblems / totalProblems) * 100) : 0;
113119

120+
// 准备路径进度数据
121+
const getPathProgress = useCallback(() => {
122+
return learningPaths.map(path => {
123+
const stats = getStatsForProblems([]);
124+
return {
125+
id: path.id,
126+
name: path.name,
127+
nameEn: path.nameEn,
128+
icon: path.icon,
129+
color: path.color,
130+
completed: 0,
131+
total: 10
132+
};
133+
});
134+
}, [getStatsForProblems]);
135+
114136
return (
115137
<div className="experience-bar-container">
116138
<div className="experience-bar-content">
@@ -149,6 +171,22 @@ const ExperienceBar: React.FC<ExperienceBarProps> = ({
149171

150172
{/* 进度区域 */}
151173
<div className="progress-section">
174+
{/* 分享按钮 */}
175+
<button
176+
className="share-button"
177+
onClick={() => setShowShareModal(true)}
178+
aria-label={t('share.shareProgress', '分享进度')}
179+
>
180+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
181+
<circle cx="18" cy="5" r="3" />
182+
<circle cx="6" cy="12" r="3" />
183+
<circle cx="18" cy="19" r="3" />
184+
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
185+
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
186+
</svg>
187+
<span>{t('share.share', '分享')}</span>
188+
</button>
189+
152190
{/* 题目完成进度 */}
153191
<div className="problem-progress">
154192
<div className="problem-progress-info">
@@ -160,17 +198,17 @@ const ExperienceBar: React.FC<ExperienceBarProps> = ({
160198
<span className="problem-percentage">{problemPercentage}%</span>
161199
</div>
162200
<div className="problem-progress-track">
163-
<div
201+
<div
164202
className="problem-progress-fill"
165203
style={{ width: `${problemPercentage}%` }}
166204
/>
167205
</div>
168206
</div>
169-
207+
170208
{/* 经验条 */}
171209
<div className="exp-bar-wrapper">
172210
<div className="exp-bar-track">
173-
<div
211+
<div
174212
className="exp-bar-fill"
175213
style={{ width: `${levelProgress}%` }}
176214
/>
@@ -179,8 +217,8 @@ const ExperienceBar: React.FC<ExperienceBarProps> = ({
179217
<div className="exp-bar-text">
180218
<span className="exp-current">{experience.totalExp.toLocaleString()} {t('experience.exp')}</span>
181219
<span className="exp-next">
182-
{nextRealm
183-
? t('experience.toNextRealm', {
220+
{nextRealm
221+
? t('experience.toNextRealm', {
184222
realm: t(`realms.${nextRealm.translationKey}`),
185223
exp: expToNextRealm.toLocaleString()
186224
})
@@ -191,13 +229,32 @@ const ExperienceBar: React.FC<ExperienceBarProps> = ({
191229
</div>
192230
</div>
193231
</div>
194-
232+
195233
{/* 经验值获取动画 */}
196234
{showExpGain && (
197235
<div className="exp-gain-popup">
198236
+{expGainAmount.toLocaleString()} {t('experience.exp')}
199237
</div>
200238
)}
239+
240+
{/* 分享弹窗 */}
241+
<ShareModal
242+
isOpen={showShareModal}
243+
onClose={() => setShowShareModal(false)}
244+
currentLang={currentLang}
245+
totalExp={experience.totalExp}
246+
currentRealm={{
247+
name: currentRealm.name,
248+
nameEn: currentRealm.nameEn,
249+
icon: currentRealm.icon,
250+
color: currentRealm.color
251+
}}
252+
realmProgress={realmProgress}
253+
expToNextRealm={expToNextRealm}
254+
completedProblems={completedProblems}
255+
totalProblems={totalProblems}
256+
pathProgress={getPathProgress()}
257+
/>
201258
</div>
202259
);
203260
};

0 commit comments

Comments
 (0)