diff --git a/app/playground/page.tsx b/app/playground/page.tsx index 485a8af..458dcb3 100644 --- a/app/playground/page.tsx +++ b/app/playground/page.tsx @@ -1,15 +1,26 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect, Suspense } from 'react'; +import { useSearchParams, useRouter } from 'next/navigation'; import { ColorPicker } from '@/components/color/ColorPicker'; import { ColorDisplay } from '@/components/color/ColorDisplay'; import { ColorInfo } from '@/components/color/ColorInfo'; import { ManipulationPanel } from '@/components/tools/ManipulationPanel'; import { useColorInfo } from '@/lib/api/queries'; -import { Loader2 } from 'lucide-react'; +import { useKeyboard } from '@/lib/hooks/useKeyboard'; +import { useColorHistory } from '@/lib/stores/historyStore'; +import { Loader2, Share2, History, X } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { toast } from 'sonner'; -export default function PlaygroundPage() { - const [color, setColor] = useState('#ff0099'); +function PlaygroundContent() { + const searchParams = useSearchParams(); + const router = useRouter(); + const [color, setColor] = useState(() => { + // Initialize from URL if available + const urlColor = searchParams.get('color'); + return urlColor ? `#${urlColor.replace('#', '')}` : '#ff0099'; + }); const { data, isLoading, isError, error } = useColorInfo({ colors: [color], @@ -17,6 +28,58 @@ export default function PlaygroundPage() { const colorInfo = data?.colors[0]; + // Color history + const { history, addColor, removeColor, clearHistory, getRecent } = useColorHistory(); + const recentColors = getRecent(10); + + // Update URL and history when color changes + useEffect(() => { + const hex = color.replace('#', ''); + router.push(`/playground?color=${hex}`, { scroll: false }); + addColor(color); + }, [color, router, addColor]); + + // Share color via URL + const handleShare = () => { + const url = `${window.location.origin}/playground?color=${color.replace('#', '')}`; + navigator.clipboard.writeText(url); + toast.success('Link copied to clipboard!'); + }; + + // Copy color to clipboard + const handleCopyColor = () => { + navigator.clipboard.writeText(color); + toast.success('Color copied to clipboard!'); + }; + + // Random color generation + const handleRandomColor = () => { + const randomHex = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0'); + setColor(randomHex); + }; + + // Keyboard shortcuts + useKeyboard([ + { + key: 'c', + meta: true, + handler: handleCopyColor, + description: 'Copy color', + }, + { + key: 's', + meta: true, + handler: handleShare, + description: 'Share color', + }, + { + key: 'r', + meta: true, + handler: handleRandomColor, + description: 'Random color', + }, + ]); + return (
@@ -25,6 +88,14 @@ export default function PlaygroundPage() {

Interactive color manipulation and analysis tool

+
+ ⌘C + Copy + ⌘S + Share + ⌘R + Random +
@@ -36,11 +107,60 @@ export default function PlaygroundPage() {
-

Preview

+
+

Preview

+ +
+ + {recentColors.length > 0 && ( +
+
+
+ +

Recent Colors

+
+ +
+
+ {recentColors.map((entry) => ( + +
+ + ))} +
+
+ )} {/* Right Column: Color Information */} @@ -74,3 +194,17 @@ export default function PlaygroundPage() { ); } + +export default function PlaygroundPage() { + return ( + +
+ +
+ + }> + +
+ ); +} diff --git a/lib/hooks/useKeyboard.ts b/lib/hooks/useKeyboard.ts new file mode 100644 index 0000000..60e6a98 --- /dev/null +++ b/lib/hooks/useKeyboard.ts @@ -0,0 +1,91 @@ +import { useEffect } from 'react'; + +export interface KeyboardShortcut { + key: string; + ctrl?: boolean; + shift?: boolean; + alt?: boolean; + meta?: boolean; + handler: (event: KeyboardEvent) => void; + description?: string; +} + +/** + * Hook to register keyboard shortcuts + * + * @example + * ```tsx + * useKeyboard([ + * { + * key: 'c', + * meta: true, // Cmd on Mac, Ctrl on Windows + * handler: () => copyToClipboard(), + * description: 'Copy color', + * }, + * { + * key: 'k', + * meta: true, + * handler: () => openCommandPalette(), + * description: 'Open command palette', + * }, + * ]); + * ``` + */ +export function useKeyboard(shortcuts: KeyboardShortcut[]) { + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + for (const shortcut of shortcuts) { + const keyMatches = event.key.toLowerCase() === shortcut.key.toLowerCase(); + const ctrlMatches = shortcut.ctrl ? event.ctrlKey : !event.ctrlKey; + const shiftMatches = shortcut.shift ? event.shiftKey : !event.shiftKey; + const altMatches = shortcut.alt ? event.altKey : !event.altKey; + const metaMatches = shortcut.meta ? event.metaKey : !event.metaKey; + + // On Windows/Linux, treat meta as ctrl for convenience + const modifierMatches = shortcut.meta + ? event.metaKey || (event.ctrlKey && !navigator.platform.includes('Mac')) + : metaMatches; + + if ( + keyMatches && + ctrlMatches && + shiftMatches && + altMatches && + modifierMatches + ) { + event.preventDefault(); + shortcut.handler(event); + break; + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [shortcuts]); +} + +/** + * Hook to register a single keyboard shortcut (convenience wrapper) + */ +export function useKeyboardShortcut( + key: string, + handler: (event: KeyboardEvent) => void, + modifiers?: { + ctrl?: boolean; + shift?: boolean; + alt?: boolean; + meta?: boolean; + } +) { + useKeyboard([ + { + key, + ...modifiers, + handler, + }, + ]); +} diff --git a/lib/stores/historyStore.ts b/lib/stores/historyStore.ts new file mode 100644 index 0000000..e6fb3c5 --- /dev/null +++ b/lib/stores/historyStore.ts @@ -0,0 +1,68 @@ +import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; + +export interface ColorHistoryEntry { + color: string; + timestamp: number; +} + +interface ColorHistoryState { + history: ColorHistoryEntry[]; + addColor: (color: string) => void; + removeColor: (color: string) => void; + clearHistory: () => void; + getRecent: (limit?: number) => ColorHistoryEntry[]; +} + +/** + * Color history store with localStorage persistence + * + * Tracks up to 50 most recent colors with timestamps + * Automatically removes duplicates (keeps most recent) + * Persists across browser sessions + */ +export const useColorHistory = create()( + persist( + (set, get) => ({ + history: [], + + addColor: (color) => { + const normalizedColor = color.toLowerCase(); + set((state) => { + // Remove existing entry if present + const filtered = state.history.filter( + (entry) => entry.color.toLowerCase() !== normalizedColor + ); + + // Add new entry at the beginning + const newHistory = [ + { color: normalizedColor, timestamp: Date.now() }, + ...filtered, + ].slice(0, 50); // Keep only 50 most recent + + return { history: newHistory }; + }); + }, + + removeColor: (color) => { + const normalizedColor = color.toLowerCase(); + set((state) => ({ + history: state.history.filter( + (entry) => entry.color.toLowerCase() !== normalizedColor + ), + })); + }, + + clearHistory: () => set({ history: [] }), + + getRecent: (limit = 10) => { + const { history } = get(); + return history.slice(0, limit); + }, + }), + { + name: 'pastel-color-history', + storage: createJSONStorage(() => localStorage), + } + ) +);