diff --git a/README.md b/README.md index b4e2a7a..a7f95e9 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,28 @@ README에 아래 코드를 추가하고 `사용자명`을 본인의 Dreamhack --- +## Themes + +`theme` 파라미터로 테마를 변경할 수 있습니다. + +```markdown +![Dreamhack Stats](https://dreamhack-readme-stats.vercel.app/api/stats?username=사용자명&theme=dark) +``` + +| Theme | Wargame Stats | Most Solved Categories | +|-------|---------------|------------------------| +| `light` | ![Stats Light](https://dreamhack-readme-stats.vercel.app/api/stats?username=weakness&theme=light) | ![Categories Light](https://dreamhack-readme-stats.vercel.app/api/most-solved?username=weakness&theme=light) | +| `dark` | ![Stats Dark](https://dreamhack-readme-stats.vercel.app/api/stats?username=weakness&theme=dark) | ![Categories Dark](https://dreamhack-readme-stats.vercel.app/api/most-solved?username=weakness&theme=dark) | + +--- + ## Features | Feature | Description | |---------|-------------| | **Wargame Stats** | 해결한 문제 수, 랭킹, 점수, TOP % 표시 | | **Category Chart** | 카테고리별 점수 분포를 파이 차트로 시각화 | +| **Themes** | Light/Dark 테마 지원 | | **Auto Update** | 실시간으로 최신 통계 반영 | | **Caching** | Redis 캐싱으로 빠른 응답 속도 | diff --git a/src/__tests__/pages/api/stats.test.ts b/src/__tests__/pages/api/stats.test.ts index 2cf53cd..1e28941 100644 --- a/src/__tests__/pages/api/stats.test.ts +++ b/src/__tests__/pages/api/stats.test.ts @@ -75,7 +75,52 @@ describe('stats API 엔드포인트 테스트', () => { wargame_rank: `${mockUserData.wargame.rank}/${mockLastRank}`, wargameRankPercentage: '20.00', wargame_score: mockUserData.wargame.score, + }, 'light'); + }); + + it('dark 테마 파라미터로 SVG를 생성해야 함', async () => { + // 모킹된 데이터 설정 + const mockUserId = 20691; + const mockLastRank = 1000; + const mockUserData: TuserData = { + nickname: 'weakness', + contributions: { level: 1, rank: 100 }, + exp: 1000, + total_wargame: 50, + wargame: { solved: 50, rank: 200, score: 5000 }, + ctf: { rank: 300, tier: 'Gold', rating: 2000 }, + profile_image: 'image.jpg' + }; + const mockSvg = 'Mock Dark SVG'; + + // 모킹된 함수 구현 + (dreamhackUtils.getUserId as jest.Mock).mockResolvedValueOnce(mockUserId); + (dreamhackUtils.getLastRank as jest.Mock).mockResolvedValueOnce(mockLastRank); + (dreamhackUtils.getUserData as jest.Mock).mockResolvedValueOnce(mockUserData); + (dreamhackUtils.calculateTopPercentage as jest.Mock).mockReturnValueOnce('20.00'); + (generateSvgUtils.generateStatsSvg as jest.Mock).mockReturnValueOnce(mockSvg); + + // HTTP 요청 모킹 (theme=dark 포함) + const { req, res } = createMocks({ + method: 'GET', + query: { + username: 'weakness', + theme: 'dark', + }, }); + + // API 핸들러 호출 + await handler(req, res); + + // 응답 검증 + expect(res._getStatusCode()).toBe(200); + expect(res._getData()).toBe(mockSvg); + + // dark 테마로 호출되었는지 확인 + expect(generateSvgUtils.generateStatsSvg).toHaveBeenCalledWith( + expect.objectContaining({ nickname: 'weakness' }), + 'dark' + ); }); it('사용자 이름이 없을 때 400 에러를 반환해야 함', async () => { diff --git a/src/__tests__/utils/generateCategorySvg.test.ts b/src/__tests__/utils/generateCategorySvg.test.ts index 81efe9d..be828c6 100644 --- a/src/__tests__/utils/generateCategorySvg.test.ts +++ b/src/__tests__/utils/generateCategorySvg.test.ts @@ -112,4 +112,38 @@ describe('generateCategorySvg 유틸리티 함수 테스트', () => { expect(percentLabels.length).toBe(1); expect(percentLabels[0]).toContain('93%'); }); + + it('light 테마(기본)로 SVG를 생성해야 함', () => { + const mockStats: TCategoryStats = { + nickname: 'weakness', + total_score: 5000, + categories: [ + { name: 'web', score: 2000, rank: 50, color: '#ff6b6b' } + ] + }; + + const result = generateCategorySvg(mockStats); + + // light 테마 색상 확인 + expect(result).toContain('fill="#ffffff"'); // background + expect(result).toContain('fill="#f8fafc"'); // cardBackground + expect(result).toContain('stroke="#e2e8f0"'); // border + }); + + it('dark 테마로 SVG를 생성해야 함', () => { + const mockStats: TCategoryStats = { + nickname: 'weakness', + total_score: 5000, + categories: [ + { name: 'web', score: 2000, rank: 50, color: '#ff6b6b' } + ] + }; + + const result = generateCategorySvg(mockStats, 'dark'); + + // dark 테마 색상 확인 + expect(result).toContain('fill="#0d1117"'); // background + expect(result).toContain('fill="#21262d"'); // cardBackground + expect(result).toContain('stroke="#30363d"'); // border + }); }); \ No newline at end of file diff --git a/src/__tests__/utils/generateStatsSvg.test.ts b/src/__tests__/utils/generateStatsSvg.test.ts index 06d42a9..8ce86c2 100644 --- a/src/__tests__/utils/generateStatsSvg.test.ts +++ b/src/__tests__/utils/generateStatsSvg.test.ts @@ -2,15 +2,15 @@ import { generateStatsSvg } from '../../utils/generateStatsSvg'; import { Tstats } from '../../types'; describe('generateStatsSvg 유틸리티 함수 테스트', () => { - it('유효한 통계 데이터로 SVG를 생성해야 함', () => { - const mockStats: Tstats = { - nickname: 'testuser', - wargame_solved: 50, - wargame_rank: '200/1000', - wargameRankPercentage: '20.00', - wargame_score: 5000 - }; + const mockStats: Tstats = { + nickname: 'testuser', + wargame_solved: 50, + wargame_rank: '200/1000', + wargameRankPercentage: '20.00', + wargame_score: 5000 + }; + it('유효한 통계 데이터로 SVG를 생성해야 함', () => { const result = generateStatsSvg(mockStats); // SVG 문자열이 반환되었는지 확인 @@ -26,7 +26,7 @@ describe('generateStatsSvg 유틸리티 함수 테스트', () => { }); it('특수 문자가 포함된 사용자 이름을 올바르게 처리해야 함', () => { - const mockStats: Tstats = { + const specialStats: Tstats = { nickname: 'test', wargame_solved: 50, wargame_rank: '200/1000', @@ -34,14 +34,14 @@ describe('generateStatsSvg 유틸리티 함수 테스트', () => { wargame_score: 5000 }; - const result = generateStatsSvg(mockStats); + const result = generateStatsSvg(specialStats); // 특수 문자가 포함된 사용자 이름이 SVG에 포함되어 있는지 확인 expect(result).toContain('test'); }); it('긴 사용자 이름을 처리할 수 있어야 함', () => { - const mockStats: Tstats = { + const longNameStats: Tstats = { nickname: 'verylongusernamethatmightcauseissueswithsvgrendering', wargame_solved: 50, wargame_rank: '200/1000', @@ -49,9 +49,27 @@ describe('generateStatsSvg 유틸리티 함수 테스트', () => { wargame_score: 5000 }; - const result = generateStatsSvg(mockStats); - + const result = generateStatsSvg(longNameStats); + // 긴 사용자 이름이 SVG에 포함되어 있는지 확인 expect(result).toContain('verylongusernamethatmightcauseissueswithsvgrendering'); }); + + it('light 테마(기본)로 SVG를 생성해야 함', () => { + const result = generateStatsSvg(mockStats); + + // light 테마 색상 확인 + expect(result).toContain('fill="#ffffff"'); // background + expect(result).toContain('fill="#f8fafc"'); // cardBackground + expect(result).toContain('stroke="#e2e8f0"'); // border + }); + + it('dark 테마로 SVG를 생성해야 함', () => { + const result = generateStatsSvg(mockStats, 'dark'); + + // dark 테마 색상 확인 + expect(result).toContain('fill="#0d1117"'); // background + expect(result).toContain('fill="#21262d"'); // cardBackground + expect(result).toContain('stroke="#30363d"'); // border + }); }); \ No newline at end of file diff --git a/src/pages/api/most-solved.ts b/src/pages/api/most-solved.ts index 426163b..77b4f17 100644 --- a/src/pages/api/most-solved.ts +++ b/src/pages/api/most-solved.ts @@ -1,5 +1,5 @@ import { NextApiRequest, NextApiResponse } from 'next'; -import { TCategoryData, TCategoryStats } from '../../types'; +import { TCategoryData, TCategoryStats, Theme } from '../../types'; import { generateCategorySvg } from '../../utils/generateCategorySvg'; import { getUserId, getUserData } from '../../utils/dreamhack'; @@ -31,12 +31,15 @@ const defaultColors = [ export default async function handler(req: NextApiRequest, res: NextApiResponse) { console.time('⏱️ 전체 API 실행 시간'); - const { username } = req.query; + const { username, theme: themeParam } = req.query; if (!username || typeof username !== 'string') { return res.status(400).json({ error: 'Username is required' }); } + // 테마 파라미터 검증 + const theme: Theme = themeParam === 'dark' ? 'dark' : 'light'; + try { // 개별 API 호출 시간 측정 const userId = await measureTime('getUserId', () => getUserId(username as string)); @@ -69,8 +72,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) total_score: wargame.score || 0, categories: [] }; - - const svg = generateCategorySvg(emptyStats); + + const svg = generateCategorySvg(emptyStats, theme); res.setHeader('Content-Type', 'image/svg+xml'); res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); console.timeEnd('⏱️ 전체 API 실행 시간'); @@ -99,7 +102,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) categories }; - const svg = generateCategorySvg(categoryStats); + const svg = generateCategorySvg(categoryStats, theme); console.timeEnd('⏱️ 데이터 가공 및 SVG 생성'); res.setHeader('Content-Type', 'image/svg+xml'); diff --git a/src/pages/api/stats.ts b/src/pages/api/stats.ts index c6c32d3..fe81df1 100644 --- a/src/pages/api/stats.ts +++ b/src/pages/api/stats.ts @@ -1,5 +1,5 @@ import { NextApiRequest, NextApiResponse } from 'next'; -import { Tstats } from '../../types'; +import { Tstats, Theme } from '../../types'; import { generateStatsSvg } from '../../utils/generateStatsSvg'; import { getLastRank, @@ -20,12 +20,15 @@ const measureTime = async (name: string, fn: () => Promise): Promise => export default async function handler(req: NextApiRequest, res: NextApiResponse) { console.time('⏱️ 전체 API 실행 시간'); - const { username } = req.query; + const { username, theme: themeParam } = req.query; if (!username || typeof username !== 'string') { return res.status(400).json({ error: 'Username is required' }); } + // 테마 파라미터 검증 + const theme: Theme = themeParam === 'dark' ? 'dark' : 'light'; + try { // 개별 API 호출 시간 측정 const userId = await measureTime('getUserId', () => getUserId(username as string)); @@ -64,7 +67,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) wargame_score: wargame.score, }; - const svg = generateStatsSvg(stats); + const svg = generateStatsSvg(stats, theme); console.timeEnd('⏱️ 데이터 가공 및 SVG 생성'); res.setHeader('Content-Type', 'image/svg+xml'); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index b765614..0272762 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,8 +1,119 @@ -import React from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import Head from 'next/head'; import styles from '../styles/Home.module.css'; +type Theme = 'light' | 'dark'; +type CardType = 'stats' | 'most-solved'; + +interface DropdownOption { + value: T; + label: string; +} + +interface CustomDropdownProps { + options: DropdownOption[]; + value: T; + onChange: (value: T) => void; + placeholder?: string; +} + +function CustomDropdown({ options, value, onChange, placeholder }: CustomDropdownProps) { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + const selectedOption = options.find(opt => opt.value === value); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( +
+ + {isOpen && ( +
    + {options.map((option) => ( +
  • { + onChange(option.value); + setIsOpen(false); + }} + > + {option.label} + {option.value === value && ( + + + + )} +
  • + ))} +
+ )} +
+ ); +} + export default function Home() { + const [username, setUsername] = useState(''); + const [confirmedUsername, setConfirmedUsername] = useState(''); + const [selectedTheme, setSelectedTheme] = useState('light'); + const [selectedCard, setSelectedCard] = useState('stats'); + const [previewKey, setPreviewKey] = useState(0); + const [isLoading, setIsLoading] = useState(false); + + const cardOptions: DropdownOption[] = [ + { value: 'stats', label: 'Wargame Stats' }, + { value: 'most-solved', label: 'Most Solved Categories' }, + ]; + + const themeOptions: DropdownOption[] = [ + { value: 'light', label: 'Light' }, + { value: 'dark', label: 'Dark' }, + ]; + + const baseUrl = 'https://dreamhack-readme-stats.vercel.app'; + + const getApiUrl = (card: CardType, theme: Theme, user: string) => { + const endpoint = card === 'stats' ? 'stats' : 'most-solved'; + return `${baseUrl}/api/${endpoint}?username=${user || '사용자명'}&theme=${theme}`; + }; + + const getPreviewUrl = (card: CardType, theme: Theme, user: string) => { + if (!user) return ''; + const endpoint = card === 'stats' ? 'stats' : 'most-solved'; + return `/api/${endpoint}?username=${user}&theme=${theme}`; + }; + + const getMarkdownCode = () => { + const altText = selectedCard === 'stats' ? 'Dreamhack Stats' : 'Dreamhack Category Chart'; + return `![${altText}](${getApiUrl(selectedCard, selectedTheme, confirmedUsername)})`; + }; + + const getHtmlCode = () => { + const altText = selectedCard === 'stats' ? 'Dreamhack Stats' : 'Dreamhack Category Chart'; + const user = confirmedUsername || '사용자명'; + return ` + ${altText} +`; + }; + const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text).then(() => { alert('코드가 클립보드에 복사되었습니다!'); @@ -11,147 +122,166 @@ export default function Home() { }); }; + const handlePreview = () => { + if (!username.trim()) { + alert('사용자명을 입력해주세요.'); + return; + } + setConfirmedUsername(username); + setIsLoading(true); + setPreviewKey(prev => prev + 1); + }; + + const handleImageLoad = () => { + setIsLoading(false); + }; + + const handleImageError = () => { + setIsLoading(false); + }; + return ( <> Dreamhack Readme Stats - GitHub 프로필에 Dreamhack 통계 표시하기 - - + - - - - - - - - - - - - - - +

Dreamhack Readme Stats

- 사용자 이름을 입력하여 Dreamhack 워게임 통계를 확인하세요. + GitHub README에 Dreamhack 워게임 통계를 표시하세요

-
-

사용 방법:

-
-
-

Markdown:

- + + {/* 설정 패널 */} +
+
+
+ + setUsername(e.target.value)} + placeholder="Dreamhack 닉네임 입력" + className={styles.textInput} + />
- - ![Dreamhack Stats](https://dreamhack-readme-stats.vercel.app/api/stats?username=사용자명) - -
-
-
-

HTML:

- + +
+ + +
+ +
+ +
- - {` - Dreamhack Stats -`} - + +
-
-

예시:

- - Dreamhack stats example - + {/* 미리보기 */} +
+

미리보기

+
+ {confirmedUsername && previewKey > 0 ? ( + Preview + ) : ( +
+ 사용자명을 입력하고 미리보기를 클릭하세요 +
+ )} +
-
-

카테고리 차트:

+ {/* 코드 생성 */} +
+

{previewKey > 0 ? '생성된 코드' : '예제 코드'}

+
-

Markdown:

-
- - ![Dreamhack Category Chart](https://dreamhack-readme-stats.vercel.app/api/most-solved?username=사용자명) - + {getMarkdownCode()}
+
-

HTML:

-
- - {` - Dreamhack Category Chart -`} - + {getHtmlCode()}
-
-

카테고리 차트 예시:

- - Dreamhack category chart example - + {/* 테마 비교 */} +
+

테마 비교

+
+
+ Light + Light theme example +
+
+ Dark + Dark theme example +
+

- +
); -} \ No newline at end of file +} diff --git a/src/styles/Home.module.css b/src/styles/Home.module.css index 3c0ea26..01b0b0a 100644 --- a/src/styles/Home.module.css +++ b/src/styles/Home.module.css @@ -205,4 +205,252 @@ .themeGrid { grid-template-columns: 1fr; } +} + +/* Config Panel */ +.configPanel { + max-width: 900px; + width: 100%; + padding: 1.5rem; + background: #fafafa; + border: 1px solid #eaeaea; + border-radius: 10px; + margin: 1rem 0; +} + +.configRow { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: flex-end; +} + +.inputGroup { + display: flex; + flex-direction: column; + gap: 0.5rem; + flex: 1; + min-width: 150px; +} + +.inputGroup label { + font-size: 0.9rem; + font-weight: 500; + color: #333; +} + +.textInput { + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 1rem; + transition: border-color 0.2s ease; +} + +.textInput:focus { + outline: none; + border-color: #6e45e2; +} + +/* Custom Dropdown */ +.dropdown { + position: relative; + width: 100%; +} + +.dropdownTrigger { + width: 100%; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 1rem; + background: white; + cursor: pointer; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + display: flex; + justify-content: space-between; + align-items: center; + text-align: left; +} + +.dropdownTrigger:hover { + border-color: #bbb; +} + +.dropdownTrigger.dropdownOpen { + border-color: #6e45e2; + box-shadow: 0 0 0 3px rgba(110, 69, 226, 0.1); +} + +.dropdownArrow { + width: 16px; + height: 16px; + color: #666; + transition: transform 0.2s ease; +} + +.dropdownOpen .dropdownArrow { + transform: rotate(180deg); +} + +.dropdownMenu { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + background: white; + border: 1px solid #ddd; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + list-style: none; + padding: 4px; + margin: 0; + z-index: 100; + animation: dropdownFadeIn 0.15s ease; +} + +@keyframes dropdownFadeIn { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.dropdownItem { + padding: 0.65rem 0.75rem; + cursor: pointer; + border-radius: 4px; + transition: background-color 0.15s ease; + display: flex; + justify-content: space-between; + align-items: center; +} + +.dropdownItem:hover { + background: #f5f5f5; +} + +.dropdownItemSelected { + background: #f0ebff; + color: #6e45e2; + font-weight: 500; +} + +.dropdownItemSelected:hover { + background: #e8e0ff; +} + +.checkIcon { + width: 16px; + height: 16px; + color: #6e45e2; +} + +.previewButton { + padding: 0.75rem 1.5rem; + background: #6e45e2; + color: white; + border: none; + border-radius: 6px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease; + white-space: nowrap; +} + +.previewButton:hover { + background: #5a37c0; +} + +.previewButton:disabled { + background: #999; + cursor: not-allowed; +} + +/* Preview Section */ +.previewSection { + max-width: 900px; + width: 100%; + margin: 1.5rem 0; +} + +.previewSection h2 { + margin: 0 0 1rem 0; + font-size: 1.3rem; +} + +.previewContainer { + display: flex; + justify-content: center; + align-items: center; + min-height: 200px; + padding: 2rem; + background: #f5f5f5; + border: 1px solid #eaeaea; + border-radius: 10px; +} + +.previewImage { + max-width: 100%; + height: auto; +} + +.previewPlaceholder { + color: #999; + font-size: 1rem; +} + +/* Code Panel */ +.codePanel { + max-width: 900px; + width: 100%; + padding: 1.5rem; + background: #fafafa; + border: 1px solid #eaeaea; + border-radius: 10px; + margin: 1rem 0; +} + +.codePanel h2 { + margin: 0 0 1rem 0; + font-size: 1.3rem; +} + +/* Theme Comparison */ +.themeComparison { + max-width: 900px; + width: 100%; + margin: 2rem 0; +} + +.themeComparison h2 { + margin: 0 0 1rem 0; + font-size: 1.3rem; +} + +.themeLabel { + display: block; + font-weight: 500; + margin-bottom: 0.75rem; + color: #333; +} + +/* Responsive adjustments for config panel */ +@media (max-width: 600px) { + .configRow { + flex-direction: column; + } + + .inputGroup { + width: 100%; + } + + .previewButton { + width: 100%; + } } \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 8b97c73..dab1400 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -73,4 +73,16 @@ export interface TCategoryStats { nickname: string; total_score: number; categories: TCategoryData[]; +} + +export type Theme = 'light' | 'dark'; + +export interface ThemeColors { + background: string; + cardBackground: string; + border: string; + title: string; + text: string; + subText: string; + accent: string; } \ No newline at end of file diff --git a/src/utils/generateCategorySvg.ts b/src/utils/generateCategorySvg.ts index 3bf34d3..52be5f1 100644 --- a/src/utils/generateCategorySvg.ts +++ b/src/utils/generateCategorySvg.ts @@ -1,6 +1,28 @@ -import { TCategoryStats } from '../types'; - -export function generateCategorySvg(stats: TCategoryStats): string { +import { TCategoryStats, Theme, ThemeColors } from '../types'; + +const themes: Record = { + light: { + background: '#ffffff', + cardBackground: '#f8fafc', + border: '#e2e8f0', + title: '#64748b', + text: '#0f172a', + subText: '#94a3b8', + accent: '#3b82f6', + }, + dark: { + background: '#0d1117', + cardBackground: '#21262d', + border: '#30363d', + title: '#8b949e', + text: '#e6edf3', + subText: '#7d8590', + accent: '#58a6ff', + }, +}; + +export function generateCategorySvg(stats: TCategoryStats, theme: Theme = 'light'): string { + const colors = themes[theme]; // SVG 크기 및 차트 설정 const width = 390; const height = 190; @@ -27,7 +49,7 @@ export function generateCategorySvg(stats: TCategoryStats): string { // 카테고리가 없는 경우 if (categories.length === 0 || totalCategoryScore === 0) { - pieChart = ` + pieChart = ` No data`; legends = `No category data`; } else { @@ -50,7 +72,7 @@ export function generateCategorySvg(stats: TCategoryStats): string { `; @@ -82,7 +104,7 @@ export function generateCategorySvg(stats: TCategoryStats): string { // 중앙 총점 표시 (원형 차트 내부에) const centerCircle = ` - + Total ${totalScore} `; @@ -92,23 +114,23 @@ export function generateCategorySvg(stats: TCategoryStats): string { - + Most Solved Categories - + @@ -122,7 +144,7 @@ export function generateCategorySvg(stats: TCategoryStats): string { - + `; } \ No newline at end of file diff --git a/src/utils/generateStatsSvg.ts b/src/utils/generateStatsSvg.ts index 0c539fb..1b6c85f 100644 --- a/src/utils/generateStatsSvg.ts +++ b/src/utils/generateStatsSvg.ts @@ -1,20 +1,43 @@ -import { Tstats } from '../types'; +import { Tstats, Theme, ThemeColors } from '../types'; + +const themes: Record = { + light: { + background: '#ffffff', + cardBackground: '#f8fafc', + border: '#e2e8f0', + title: '#64748b', + text: '#0f172a', + subText: '#94a3b8', + accent: '#3b82f6', + }, + dark: { + background: '#0d1117', + cardBackground: '#21262d', + border: '#30363d', + title: '#8b949e', + text: '#e6edf3', + subText: '#7d8590', + accent: '#58a6ff', + }, +}; + +export function generateStatsSvg(stats: Tstats, theme: Theme = 'light'): string { + const colors = themes[theme]; -export function generateStatsSvg(stats: Tstats): string { return ` - + Dreamhack Wargame Stats @@ -30,25 +53,25 @@ export function generateStatsSvg(stats: Tstats): string { - + Solved Challenges ${stats.wargame_solved} - + Rank ${stats.wargame_rank} - + Score ${stats.wargame_score} - + `; }