Skip to content

Commit ec71f1f

Browse files
committed
new(app): color contrast checker
1 parent 4016765 commit ec71f1f

File tree

3 files changed

+221
-0
lines changed

3 files changed

+221
-0
lines changed

src/components/AnimatedRoutes.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import CodenameGeneratorPage from '../pages/apps/CodenameGeneratorPage';
3535
import ImageToolkitPage from '../pages/apps/ImageToolkitPage';
3636
import PasswordGeneratorPage from '../pages/apps/PasswordGeneratorPage';
3737
import JsonFormatterPage from '../pages/apps/JsonFormatterPage';
38+
import ColorContrastCheckerPage from '../pages/apps/ColorContrastCheckerPage';
3839

3940
import UsefulLinksPage from '../pages/UsefulLinksPage';
4041

@@ -322,6 +323,7 @@ function AnimatedRoutes() {
322323
<Route path="/apps::itk" element={<Navigate to="/apps/image-toolkit" replace />} />
323324
<Route path="/apps::pg" element={<Navigate to="/apps/password-generator" replace />} />
324325
<Route path="/apps::jf" element={<Navigate to="/apps/json-formatter" replace />} />
326+
<Route path="/apps::ccc" element={<Navigate to="/apps/color-contrast-checker" replace />} />
325327
{/* End of hardcoded redirects */}
326328
<Route
327329
path="/apps/ip"
@@ -576,6 +578,20 @@ function AnimatedRoutes() {
576578
</motion.div>
577579
}
578580
/>
581+
<Route
582+
path="/apps/color-contrast-checker"
583+
element={
584+
<motion.div
585+
initial="initial"
586+
animate="in"
587+
exit="out"
588+
variants={pageVariants}
589+
transition={pageTransition}
590+
>
591+
<ColorContrastCheckerPage />
592+
</motion.div>
593+
}
594+
/>
579595
{/* D&D specific 404 page */}
580596
<Route
581597
path="/dnd/*"

src/pages/AppPage.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ const apps = [
9595
description: 'Format and validate JSON data for readability and correctness.',
9696
icon: Code,
9797
},
98+
{
99+
to: '/apps/color-contrast-checker',
100+
title: 'Color Contrast Checker',
101+
description: 'Check WCAG color contrast ratios for accessibility.',
102+
icon: Palette,
103+
},
98104
{
99105
to: '/apps/color-palette-generator',
100106
title: 'Color Palette Generator',
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import React, { useState, useCallback, useEffect } from 'react';
2+
import usePageTitle from '../../utils/usePageTitle';
3+
import { Link } from 'react-router-dom';
4+
import { ArrowLeftIcon, Palette, CheckCircle, XCircle } from '@phosphor-icons/react'; // Using Palette for now
5+
import colors from '../../config/colors';
6+
import { useToast } from '../../hooks/useToast';
7+
import '../../styles/app-buttons.css';
8+
9+
// Helper function to convert hex to RGB
10+
const hexToRgb = (hex) => {
11+
const r = parseInt(hex.substring(1, 3), 16);
12+
const g = parseInt(hex.substring(3, 5), 16);
13+
const b = parseInt(hex.substring(5, 7), 16);
14+
return [r, g, b];
15+
};
16+
17+
// Helper function to calculate luminance
18+
const getLuminance = (r, g, b) => {
19+
const a = [r, g, b].map((v) => {
20+
v /= 255;
21+
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
22+
});
23+
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
24+
};
25+
26+
// Helper function to calculate contrast ratio
27+
const getContrastRatio = (rgb1, rgb2) => {
28+
const lum1 = getLuminance(rgb1[0], rgb1[1], rgb1[2]);
29+
const lum2 = getLuminance(rgb2[0], rgb2[1], rgb2[2]);
30+
const brightest = Math.max(lum1, lum2);
31+
const darkest = Math.min(lum1, lum2);
32+
return (brightest + 0.05) / (darkest + 0.05);
33+
};
34+
35+
const ColorContrastCheckerPage = () => {
36+
usePageTitle('Color Contrast Checker');
37+
const { addToast } = useToast();
38+
39+
const [foregroundColor, setForegroundColor] = useState('#FFFFFF'); // Default white
40+
const [backgroundColor, setBackgroundColor] = useState('#000000'); // Default black
41+
const [contrastRatio, setContrastRatio] = useState(0);
42+
const [wcagStatus, setWcagStatus] = useState({ aa: false, aaa: false });
43+
44+
const cardStyle = {
45+
backgroundColor: colors['app-alpha-10'],
46+
borderColor: colors['app-alpha-50'],
47+
color: colors.app,
48+
};
49+
50+
const inputStyle = `mt-1 block w-full p-2 border rounded-md bg-gray-700 text-white focus:ring-blue-500 focus:border-blue-500 border-gray-600`;
51+
52+
const calculateContrast = useCallback(() => {
53+
try {
54+
const fgRgb = hexToRgb(foregroundColor);
55+
const bgRgb = hexToRgb(backgroundColor);
56+
const ratio = getContrastRatio(fgRgb, bgRgb);
57+
setContrastRatio(ratio.toFixed(2));
58+
59+
// WCAG 2.1 Guidelines
60+
// Normal Text: AA (4.5:1), AAA (7:1)
61+
// Large Text (18pt or 14pt bold): AA (3:1), AAA (4.5:1)
62+
// For simplicity, we'll check for normal text here.
63+
setWcagStatus({
64+
aa: ratio >= 4.5,
65+
aaa: ratio >= 7,
66+
});
67+
} catch (error) {
68+
setContrastRatio(0);
69+
setWcagStatus({ aa: false, aaa: false });
70+
addToast({ title: 'Error', message: 'Invalid hex color format.', duration: 3000, type: 'error' });
71+
}
72+
}, [foregroundColor, backgroundColor, addToast]);
73+
74+
useEffect(() => {
75+
calculateContrast();
76+
}, [calculateContrast]);
77+
78+
return (
79+
<div className="py-16 sm:py-24">
80+
<div className="mx-auto max-w-7xl px-6 lg:px-8 text-gray-300">
81+
<Link
82+
to="/apps"
83+
className="text-article hover:underline flex items-center justify-center gap-2 text-lg mb-4"
84+
>
85+
<ArrowLeftIcon size={24} /> Back to Apps
86+
</Link>
87+
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl mb-4 flex items-center">
88+
<span className="codex-color">fc</span>
89+
<span className="separator-color">::</span>
90+
<span className="apps-color">apps</span>
91+
<span className="separator-color">::</span>
92+
<span className="single-app-color">ccc</span>
93+
</h1>
94+
<hr className="border-gray-700" />
95+
<div className="flex justify-center items-center mt-16">
96+
<div
97+
className="group bg-transparent border rounded-lg shadow-2xl p-6 flex flex-col justify-between relative transform overflow-hidden h-full w-full max-w-4xl"
98+
style={cardStyle}
99+
>
100+
<div
101+
className="absolute top-0 left-0 w-full h-full opacity-10"
102+
style={{
103+
backgroundImage:
104+
'radial-gradient(circle, white 1px, transparent 1px)',
105+
backgroundSize: '10px 10px',
106+
}}
107+
></div>
108+
<div className="relative z-10 p-1">
109+
<h1 className="text-3xl font-arvo font-normal mb-4 text-app"> Color Contrast Checker </h1>
110+
<hr className="border-gray-700 mb-4" />
111+
112+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 mb-6">
113+
<div>
114+
<label htmlFor="foregroundColor" className="block text-sm font-medium text-gray-300 mb-2">
115+
Foreground Color (Hex)
116+
</label>
117+
<input
118+
type="color"
119+
id="foregroundColorPicker"
120+
value={foregroundColor}
121+
onChange={(e) => setForegroundColor(e.target.value.toUpperCase())}
122+
className="w-full h-10 mb-2 rounded-md cursor-pointer"
123+
title="Pick Foreground Color"
124+
/>
125+
<input
126+
type="text"
127+
id="foregroundColor"
128+
value={foregroundColor}
129+
onChange={(e) => setForegroundColor(e.target.value.toUpperCase())}
130+
className={inputStyle}
131+
placeholder="#FFFFFF"
132+
/>
133+
</div>
134+
<div>
135+
<label htmlFor="backgroundColor" className="block text-sm font-medium text-gray-300 mb-2">
136+
Background Color (Hex)
137+
</label>
138+
<input
139+
type="color"
140+
id="backgroundColorPicker"
141+
value={backgroundColor}
142+
onChange={(e) => setBackgroundColor(e.target.value.toUpperCase())}
143+
className="w-full h-10 mb-2 rounded-md cursor-pointer"
144+
title="Pick Background Color"
145+
/>
146+
<input
147+
type="text"
148+
id="backgroundColor"
149+
value={backgroundColor}
150+
onChange={(e) => setBackgroundColor(e.target.value.toUpperCase())}
151+
className={inputStyle}
152+
placeholder="#000000"
153+
/>
154+
</div>
155+
</div>
156+
157+
<div
158+
className="w-full h-32 rounded-md flex items-center justify-center text-2xl font-bold mb-6"
159+
style={{
160+
backgroundColor: backgroundColor,
161+
color: foregroundColor,
162+
border: `1px solid ${colors['app-alpha-50']}`
163+
}}
164+
>
165+
Aa
166+
</div>
167+
168+
<div className="mb-6">
169+
<p className="text-lg font-medium text-gray-300 mb-2">
170+
Contrast Ratio: <span className="text-white">{contrastRatio}:1</span>
171+
</p>
172+
<div className="flex items-center gap-4">
173+
<span className="text-base font-medium text-gray-300">
174+
WCAG AA:
175+
</span>
176+
{wcagStatus.aa ? (
177+
<CheckCircle size={24} className="text-green-500" />
178+
) : (
179+
<XCircle size={24} className="text-red-500" />
180+
)}
181+
<span className="text-base font-medium text-gray-300 ml-4">
182+
WCAG AAA:
183+
</span>
184+
{wcagStatus.aaa ? (
185+
<CheckCircle size={24} className="text-green-500" />
186+
) : (
187+
<XCircle size={24} className="text-red-500" />
188+
)}
189+
</div>
190+
</div>
191+
</div>
192+
</div>
193+
</div>
194+
</div>
195+
</div>
196+
);
197+
};
198+
199+
export default ColorContrastCheckerPage;

0 commit comments

Comments
 (0)