Skip to content

Commit 224cf57

Browse files
committed
Theme Support
Added theme support for 35 different themes.
1 parent ac6a27b commit 224cf57

File tree

5 files changed

+695
-14
lines changed

5 files changed

+695
-14
lines changed

src/App.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import React, { useState, useCallback } from 'react';
2-
import { ThemeProvider } from 'styled-components';
32
import styled from 'styled-components';
43

54
// Theme
6-
import { theme } from './styles/theme';
5+
import { ThemeProvider } from './styles/ThemeContext';
76

87
// Components
98
import Navbar from './components/Navbar/Navbar';
@@ -46,10 +45,10 @@ const App: React.FC = () => {
4645
}, []);
4746

4847
// Check if we're in a keyboard test-related tab (keyTest, layout, or type)
49-
const isKeyboardRelatedTab = ['keyTest', 'layout', 'type'].includes(activeTab);
48+
const isKeyboardRelatedTab = ['keyTest', 'layout', 'type', 'themes'].includes(activeTab);
5049

5150
return (
52-
<ThemeProvider theme={theme}>
51+
<ThemeProvider>
5352
<AppWrapper>
5453
<Navbar />
5554
<MainContent>

src/components/InfoSection/InfoSection.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ const InfoSection: React.FC<InfoSectionProps> = ({ activeTab }) => {
352352
case 'keyTest':
353353
case 'layout':
354354
case 'type':
355+
case 'themes':
355356
return renderKeyTestInfo();
356357
case 'rolloverTest':
357358
return renderRolloverTestInfo();
@@ -363,7 +364,7 @@ const InfoSection: React.FC<InfoSectionProps> = ({ activeTab }) => {
363364
};
364365

365366
// Only render if we have content for this tab
366-
if (!['keyTest', 'rolloverTest', 'typingTest', 'layout', 'type'].includes(activeTab)) {
367+
if (!['keyTest', 'rolloverTest', 'typingTest', 'layout', 'type', 'themes'].includes(activeTab)) {
367368
return null;
368369
}
369370

src/components/TestContainer/TestContainer.tsx

Lines changed: 117 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { KeyboardSelector, KeyboardType } from '../Keyboards';
55
import { KeyboardLayoutType } from '../Keyboards/keyboardTypes';
66
import RolloverTest from '../RolloverTest/RolloverTest';
77
import TypingTest from '../TypingTest/TypingTest';
8+
import { ThemeName, themeMetadata } from '../../styles/themeTypes';
9+
import { useTheme } from '../../styles/ThemeContext';
810

911
interface TestContainerProps {
1012
onKeyPress?: (key: string) => void;
@@ -120,6 +122,81 @@ const ResetButton = styled(Tab)`
120122
}
121123
`;
122124

125+
const ThemeGrid = styled.div`
126+
display: grid;
127+
grid-template-columns: repeat(7, 1fr);
128+
gap: 0.75rem;
129+
width: 100%;
130+
max-width: 1000px;
131+
margin: 0 auto;
132+
`;
133+
134+
const ThemeCard = styled.div<{ active: boolean }>`
135+
padding: 0.75rem;
136+
border-radius: 8px;
137+
border: 1px solid ${props => props.active ? props.theme.colors.primary : props.theme.colors.primary + '40'};
138+
background: ${props => props.active ? props.theme.colors.primary + '20' : 'transparent'};
139+
cursor: pointer;
140+
transition: all 0.2s ease;
141+
display: flex;
142+
flex-direction: column;
143+
align-items: center;
144+
145+
&:hover {
146+
background: ${props => props.active ? props.theme.colors.primary + '30' : props.theme.colors.primary + '10'};
147+
transform: translateY(-2px);
148+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
149+
}
150+
`;
151+
152+
const ThemePreview = styled.div<{ colors: { background: string, primary: string, text: string } }>`
153+
width: 100%;
154+
height: 50px;
155+
border-radius: 6px;
156+
margin-bottom: 0.75rem;
157+
background: ${props => props.colors.background};
158+
position: relative;
159+
overflow: hidden;
160+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
161+
162+
&::before {
163+
content: '';
164+
position: absolute;
165+
top: 50%;
166+
left: 50%;
167+
transform: translate(-50%, -50%);
168+
width: 30px;
169+
height: 30px;
170+
border-radius: 50%;
171+
background: ${props => props.colors.primary};
172+
}
173+
174+
&::after {
175+
content: 'Aa';
176+
position: absolute;
177+
top: 50%;
178+
left: 50%;
179+
transform: translate(-50%, -50%);
180+
color: ${props => props.colors.text};
181+
font-weight: bold;
182+
font-size: 12px;
183+
}
184+
`;
185+
186+
const ThemeLabel = styled.h4`
187+
margin: 0;
188+
font-size: 0.8rem;
189+
text-align: center;
190+
color: ${props => props.theme.colors.text};
191+
`;
192+
193+
const ThemeContainer = styled.div`
194+
width: 100%;
195+
display: flex;
196+
flex-direction: column;
197+
align-items: center;
198+
`;
199+
123200
const slideVariants = {
124201
enter: (direction: number) => ({
125202
x: direction > 0 ? 1000 : -1000,
@@ -152,9 +229,10 @@ const TestContainer: React.FC<TestContainerProps> = ({ onKeyPress, onReset, onTa
152229
const [currentLayout, setCurrentLayout] = useState<KeyboardType>('75%');
153230
const [currentType, setCurrentType] = useState<KeyboardLayoutType>('qwerty');
154231
const [keyboardKey, setKeyboardKey] = useState(0);
232+
const { currentTheme, setTheme } = useTheme();
155233

156234
const handleTabClick = (tabId: string) => {
157-
const tabOrder = ['keyTest', 'rolloverTest', 'typingTest', 'layout', 'type', 'themes', 'language'];
235+
const tabOrder = ['keyTest', 'rolloverTest', 'typingTest', 'layout', 'type', 'themes'];
158236
const currentIndex = tabOrder.indexOf(activeTab);
159237
const newIndex = tabOrder.indexOf(tabId);
160238
setDirection(newIndex > currentIndex ? 1 : -1);
@@ -179,6 +257,10 @@ const TestContainer: React.FC<TestContainerProps> = ({ onKeyPress, onReset, onTa
179257
setKeyboardKey(prevKey => prevKey + 1);
180258
};
181259

260+
const handleThemeChange = (themeName: ThemeName) => {
261+
setTheme(themeName);
262+
};
263+
182264
const handleReset = () => {
183265
if (onReset) {
184266
onReset();
@@ -310,9 +392,40 @@ const TestContainer: React.FC<TestContainerProps> = ({ onKeyPress, onReset, onTa
310392
</LayoutPreview>
311393
);
312394
case 'themes':
313-
return <div>Themes Content</div>;
314-
case 'language':
315-
return <div>Language Content</div>;
395+
// Get all themes including original
396+
const allThemes = (Object.keys(themeMetadata) as ThemeName[]);
397+
398+
// Sort themes alphabetically (excluding original)
399+
const sortedThemes = [
400+
'original' as ThemeName,
401+
...allThemes
402+
.filter(name => name !== 'original')
403+
.sort((a, b) => themeMetadata[a].name.localeCompare(themeMetadata[b].name))
404+
];
405+
406+
// Limit to 35 themes (7x5 grid)
407+
const displayThemes = sortedThemes.slice(0, 35);
408+
409+
return (
410+
<LayoutPreview>
411+
<h3>Select a Theme</h3>
412+
413+
<ThemeContainer>
414+
<ThemeGrid>
415+
{displayThemes.map((themeName: ThemeName) => (
416+
<ThemeCard
417+
key={themeName}
418+
active={currentTheme === themeName}
419+
onClick={() => handleThemeChange(themeName)}
420+
>
421+
<ThemePreview colors={themeMetadata[themeName].colors} />
422+
<ThemeLabel>{themeMetadata[themeName].name}</ThemeLabel>
423+
</ThemeCard>
424+
))}
425+
</ThemeGrid>
426+
</ThemeContainer>
427+
</LayoutPreview>
428+
);
316429
default:
317430
return <div>Select a tab</div>;
318431
}
@@ -347,11 +460,6 @@ const TestContainer: React.FC<TestContainerProps> = ({ onKeyPress, onReset, onTa
347460
label: 'Type',
348461
onClick: () => handleTabClick('type')
349462
},
350-
{
351-
id: 'language',
352-
label: 'Language',
353-
onClick: () => handleTabClick('language')
354-
},
355463
{
356464
id: 'themes',
357465
label: 'Themes',

src/styles/ThemeContext.tsx

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import React, { createContext, useState, useContext, ReactNode } from 'react';
2+
import { ThemeProvider as StyledThemeProvider } from 'styled-components';
3+
import { ThemeName, themeMetadata } from './themeTypes';
4+
5+
// Define the theme interface
6+
export interface Theme {
7+
colors: {
8+
text: string;
9+
background: string;
10+
primary: string;
11+
secondary: string;
12+
accent: string;
13+
glass: string;
14+
};
15+
gradients: {
16+
logo: string;
17+
header: string;
18+
};
19+
shadows: {
20+
main: string;
21+
hover: string;
22+
navbar: string;
23+
};
24+
transitions: {
25+
default: string;
26+
};
27+
effects: {
28+
blur: string;
29+
};
30+
}
31+
32+
// Create a default theme based on the original app theme
33+
const defaultTheme: Theme = {
34+
colors: {
35+
text: '#d4d7f9',
36+
background: '#01030a',
37+
primary: '#7f8eee',
38+
secondary: '#9c1579',
39+
accent: '#e53d63',
40+
glass: 'rgba(1, 3, 10, 0.8)'
41+
},
42+
gradients: {
43+
logo: 'linear-gradient(to right, #7f8eee, #9c1579)',
44+
header: 'linear-gradient(to right, #7f8eee, #e53d63)'
45+
},
46+
shadows: {
47+
main: '0 4px 20px rgba(127, 142, 238, 0.15)',
48+
hover: '0 6px 25px rgba(127, 142, 238, 0.25)',
49+
navbar: '0 2px 10px rgba(0, 0, 0, 0.2)'
50+
},
51+
transitions: {
52+
default: '0.3s ease-in-out'
53+
},
54+
effects: {
55+
blur: 'blur(10px)'
56+
}
57+
};
58+
59+
// Create the theme context
60+
interface ThemeContextType {
61+
currentTheme: ThemeName;
62+
theme: Theme;
63+
setTheme: (themeName: ThemeName) => void;
64+
}
65+
66+
const ThemeContext = createContext<ThemeContextType>({
67+
currentTheme: 'original',
68+
theme: defaultTheme,
69+
setTheme: () => {}
70+
});
71+
72+
// Create the theme provider component
73+
interface ThemeProviderProps {
74+
children: ReactNode;
75+
}
76+
77+
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
78+
const [currentTheme, setCurrentTheme] = useState<ThemeName>('original');
79+
const [theme, setThemeObject] = useState<Theme>(defaultTheme);
80+
81+
const setTheme = (themeName: ThemeName) => {
82+
setCurrentTheme(themeName);
83+
84+
// If it's the original theme, use the default theme
85+
if (themeName === 'original') {
86+
setThemeObject(defaultTheme);
87+
return;
88+
}
89+
90+
// Get the theme metadata
91+
const meta = themeMetadata[themeName];
92+
93+
// Create a new theme object based on the selected theme
94+
const newTheme: Theme = {
95+
colors: {
96+
text: meta.colors.text,
97+
background: meta.colors.background,
98+
primary: meta.colors.primary,
99+
secondary: meta.colors.secondary || meta.colors.primary,
100+
accent: meta.colors.accent || meta.colors.primary,
101+
glass: `rgba(${hexToRgb(meta.colors.background)}, 0.8)`
102+
},
103+
gradients: {
104+
logo: `linear-gradient(to right, ${meta.colors.primary}, ${meta.colors.secondary || meta.colors.primary})`,
105+
header: `linear-gradient(to right, ${meta.colors.primary}, ${meta.colors.accent || meta.colors.primary})`
106+
},
107+
shadows: {
108+
main: `0 4px 20px rgba(${hexToRgb(meta.colors.primary)}, 0.15)`,
109+
hover: `0 6px 25px rgba(${hexToRgb(meta.colors.primary)}, 0.25)`,
110+
navbar: '0 2px 10px rgba(0, 0, 0, 0.2)'
111+
},
112+
transitions: {
113+
default: '0.3s ease-in-out'
114+
},
115+
effects: {
116+
blur: 'blur(10px)'
117+
}
118+
};
119+
120+
setThemeObject(newTheme);
121+
};
122+
123+
return (
124+
<ThemeContext.Provider value={{ currentTheme, theme, setTheme }}>
125+
<StyledThemeProvider theme={theme}>
126+
{children}
127+
</StyledThemeProvider>
128+
</ThemeContext.Provider>
129+
);
130+
};
131+
132+
// Helper function to convert hex color to RGB
133+
function hexToRgb(hex: string): string {
134+
// Remove the hash if it exists
135+
hex = hex.replace('#', '');
136+
137+
// Parse the hex values
138+
const r = parseInt(hex.substring(0, 2), 16);
139+
const g = parseInt(hex.substring(2, 4), 16);
140+
const b = parseInt(hex.substring(4, 6), 16);
141+
142+
return `${r}, ${g}, ${b}`;
143+
}
144+
145+
// Custom hook to use the theme context
146+
export const useTheme = () => useContext(ThemeContext);

0 commit comments

Comments
 (0)