History, more icons, color palette component
This commit is contained in:
parent
7851d2c0f6
commit
e21173c8e4
79
app/components/ColorPalette.tsx
Normal file
79
app/components/ColorPalette.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
|
||||
interface ColorPaletteProps {
|
||||
colors: string[];
|
||||
size?: 'small' | 'large';
|
||||
}
|
||||
|
||||
const ColorPalette: React.FC<ColorPaletteProps> = ({ colors, size = 'small' }) => {
|
||||
const [copiedColor, setCopiedColor] = useState<string | null>(null);
|
||||
const [hoveredColor, setHoveredColor] = useState<string | null>(null);
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const handleColorClick = (color: string) => {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(color).then(() => {
|
||||
setCopiedColor(color);
|
||||
setTimeout(() => setCopiedColor(null), 1500);
|
||||
});
|
||||
} else {
|
||||
// Fallback method for iOS and other unsupported browsers
|
||||
const textArea = textAreaRef.current;
|
||||
if (textArea) {
|
||||
textArea.value = color;
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
setCopiedColor(color);
|
||||
setTimeout(() => setCopiedColor(null), 1500);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy color: ', err);
|
||||
}
|
||||
textArea.blur();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sizeClasses = size === 'small' ? 'w-full pt-full' : 'w-8 h-8 pt-full';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`grid grid-cols-8 gap-2 ${size === 'large' ? 'mb-4' : 'mb-2'} z-10`}>
|
||||
{colors.map((color, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`${sizeClasses} rounded-sm cursor-pointer relative group`}
|
||||
style={{backgroundColor: color}}
|
||||
onClick={() => handleColorClick(color)}
|
||||
onMouseEnter={() => setHoveredColor(color)}
|
||||
onMouseLeave={() => setHoveredColor(null)}
|
||||
>
|
||||
{size === 'small' && hoveredColor === color && (
|
||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-white dark:bg-gray-800 rounded shadow-lg z-10">
|
||||
<div className="w-4 h-4 rounded-sm mb-1" style={{backgroundColor: color}}></div>
|
||||
<span className="text-xs">{color}</span>
|
||||
</div>
|
||||
)}
|
||||
{size === 'large' && (
|
||||
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-black bg-opacity-50 text-white text-[8px]">
|
||||
{color}
|
||||
</div>
|
||||
)}
|
||||
{copiedColor === color && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-70 text-white text-xs">
|
||||
Copied!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
style={{ position: 'absolute', left: '-9999px' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorPalette;
|
@ -4,6 +4,7 @@ import { ColorScheme } from '../utils/colorSchemes';
|
||||
import { generateYAML } from '../utils/yamlExport';
|
||||
import { Highlight, themes } from 'prism-react-renderer';
|
||||
import { motion, useAnimation } from 'framer-motion';
|
||||
import ColorPalette from './ColorPalette';
|
||||
|
||||
interface ColorSchemeCardProps {
|
||||
scheme: ColorScheme;
|
||||
@ -127,19 +128,10 @@ fetchData().then(data => console.log(data)); `;
|
||||
)}
|
||||
</Highlight>
|
||||
</div>
|
||||
<div className="grid grid-cols-8 gap-2 mb-4 z-10">
|
||||
{Object.values(scheme.colors.normal).concat(Object.values(scheme.colors.bright)).map((color, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-full pt-full rounded-sm transition-colors duration-300 relative group"
|
||||
style={{backgroundColor: color}}
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-black bg-opacity-50 text-white text-[8px]">
|
||||
{color}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ColorPalette
|
||||
colors={Object.values(scheme.colors.normal).concat(Object.values(scheme.colors.bright))}
|
||||
size="large"
|
||||
/>
|
||||
<div className="flex justify-center space-x-8 mt-4 z-10">
|
||||
<button
|
||||
className="bg-red-500 text-white p-3 rounded-full shadow-lg hover:bg-red-600 transition-colors duration-300"
|
||||
|
69
app/components/HistoryPopup.tsx
Normal file
69
app/components/HistoryPopup.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ColorScheme } from '../utils/colorSchemes';
|
||||
import { generateYAML } from '../utils/yamlExport';
|
||||
import Image from 'next/image';
|
||||
import ColorPalette from './ColorPalette';
|
||||
|
||||
interface HistoryPopupProps {
|
||||
likedSchemes: ColorScheme[];
|
||||
dislikedSchemes: ColorScheme[];
|
||||
onClose: () => void;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
const HistoryPopup: React.FC<HistoryPopupProps> = ({ likedSchemes, dislikedSchemes, onClose, isDarkMode }) => {
|
||||
const [copiedColor, setCopiedColor] = useState<string | null>(null);
|
||||
|
||||
const handleDownload = (scheme: ColorScheme) => {
|
||||
const yaml = generateYAML(scheme);
|
||||
const blob = new Blob([yaml], { type: 'text/yaml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${scheme.name.replace(/\s+/g, '_').toLowerCase()}.yaml`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const renderSchemeGrid = (schemes: ColorScheme[], title: string) => (
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-3">{title}</h3>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 mb-6">
|
||||
{schemes.map((scheme, index) => (
|
||||
<div key={`${scheme.name}-${index}`} className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg">
|
||||
<h3 className="text-sm font-semibold mb-2 truncate">{scheme.name}</h3>
|
||||
<ColorPalette
|
||||
colors={Object.values(scheme.colors.normal).concat(Object.values(scheme.colors.bright))}
|
||||
size="small"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleDownload(scheme)}
|
||||
className="w-full bg-blue-500 text-white text-xs py-1 px-2 rounded hover:bg-blue-600 transition-colors duration-300"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-xl w-[90vw] h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-bold">Color Scheme History</h2>
|
||||
<button onClick={onClose} className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<Image src={isDarkMode ? "/close-icon-dark.svg" : "/close-icon-light.svg"} alt="Close" width={24} height={24} />
|
||||
</button>
|
||||
</div>
|
||||
{renderSchemeGrid(likedSchemes, "Liked Schemes")}
|
||||
{renderSchemeGrid(dislikedSchemes, "Disliked Schemes")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HistoryPopup;
|
34
app/page.tsx
34
app/page.tsx
@ -3,6 +3,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import ColorSchemeCard from "./components/ColorSchemeCard";
|
||||
import HistoryPopup from "./components/HistoryPopup";
|
||||
import { ColorScheme, knownSchemes, generateRandomScheme, generateSchemeFromGeneticAlgorithm } from './utils/colorSchemes';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
|
||||
@ -11,6 +12,7 @@ export default function Home() {
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
const [likedSchemes, setLikedSchemes] = useState<ColorScheme[]>([]);
|
||||
const [dislikedSchemes, setDislikedSchemes] = useState<ColorScheme[]>([]);
|
||||
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
generateNewSchemes(8);
|
||||
@ -86,12 +88,30 @@ export default function Home() {
|
||||
setIsDarkMode(prev => !prev);
|
||||
};
|
||||
|
||||
const toggleHistory = () => {
|
||||
setIsHistoryOpen(!isHistoryOpen);
|
||||
};
|
||||
|
||||
const getAllSchemes = () => {
|
||||
const allSchemes = [...likedSchemes, ...dislikedSchemes];
|
||||
const uniqueSchemes = allSchemes.filter((scheme, index, self) =>
|
||||
index === self.findIndex((t) => t.name === scheme.name)
|
||||
);
|
||||
return uniqueSchemes.reverse(); // Most recent first
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen overflow-hidden p-8 font-[family-name:var(--font-geist-sans)] dark:bg-gray-900 dark:text-white transition-colors duration-300">
|
||||
<header className="flex items-center mb-8">
|
||||
<div className="h-screen w-screen overflow-hidden font-[family-name:var(--font-geist-sans)] dark:bg-gray-900 dark:text-white transition-colors duration-300">
|
||||
<header className="absolute top-4 left-4 z-10">
|
||||
<Image src="/app-icon.svg" alt="App Icon" width={40} height={40} />
|
||||
</header>
|
||||
<main className="flex flex-col items-center justify-center h-[calc(100vh-100px)]">
|
||||
<button
|
||||
className="absolute top-4 right-4 z-10"
|
||||
onClick={toggleHistory}
|
||||
>
|
||||
<Image src={isDarkMode ? "/history-icon-dark.svg" : "/history-icon-light.svg"} alt="History" width={32} height={32} />
|
||||
</button>
|
||||
<main className="flex flex-col items-center justify-center h-full">
|
||||
<AnimatePresence>
|
||||
{schemes.slice(0, 3).map((scheme, index) => (
|
||||
<ColorSchemeCard
|
||||
@ -105,6 +125,14 @@ export default function Home() {
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</main>
|
||||
{isHistoryOpen && (
|
||||
<HistoryPopup
|
||||
likedSchemes={likedSchemes}
|
||||
dislikedSchemes={dislikedSchemes}
|
||||
onClose={toggleHistory}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
7
public/close-icon-dark.svg
Normal file
7
public/close-icon-dark.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <path d="M6 6L18 18M18 6L6 18" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </g>
|
||||
</svg>
|
After Width: | Height: | Size: 569 B |
7
public/close-icon-light.svg
Normal file
7
public/close-icon-light.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <path d="M6 6L18 18M18 6L6 18" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </g>
|
||||
</svg>
|
After Width: | Height: | Size: 569 B |
7
public/history-icon-dark.svg
Normal file
7
public/history-icon-dark.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <path d="M4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C9.61061 4 7.46589 5.04751 6 6.70835C5.91595 6.80358 5.83413 6.90082 5.75463 7M12 8V12L14.5 14.5M5.75391 4.00391V7.00391H8.75391" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </g>
|
||||
</svg>
|
After Width: | Height: | Size: 763 B |
7
public/history-icon-light.svg
Normal file
7
public/history-icon-light.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g id="SVGRepo_iconCarrier"> <path d="M4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C9.61061 4 7.46589 5.04751 6 6.70835C5.91595 6.80358 5.83413 6.90082 5.75463 7M12 8V12L14.5 14.5M5.75391 4.00391V7.00391H8.75391" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </g>
|
||||
</svg>
|
After Width: | Height: | Size: 763 B |
Loading…
Reference in New Issue
Block a user