From 28747a6c8feda9b6c2d9418345e31270a5870011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sat, 28 Feb 2026 09:57:06 +0100 Subject: [PATCH] refactor: extract ColorManipulation component and pass icon/summary to AppPage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename ColorPage → ColorManipulation (no AppPage wrapper inside) - Move AppPage + title/description/icon to color/page.tsx, consistent with other tools - AppPage now accepts icon prop directly; removes internal usePathname lookup and 'use client' - All tool pages pass tool.summary as description and tool.icon as icon Co-Authored-By: Claude Sonnet 4.6 --- app/(app)/ascii/page.tsx | 3 +- app/(app)/color/page.tsx | 15 +- app/(app)/favicon/page.tsx | 3 +- app/(app)/media/page.tsx | 3 +- app/(app)/qrcode/page.tsx | 3 +- app/(app)/units/page.tsx | 3 +- components/color/ColorManipulation.tsx | 378 ++++++++++++++++++++++++ components/color/ColorPage.tsx | 391 ------------------------- components/layout/AppPage.tsx | 12 +- lib/tools.tsx | 12 +- 10 files changed, 408 insertions(+), 415 deletions(-) create mode 100644 components/color/ColorManipulation.tsx delete mode 100644 components/color/ColorPage.tsx diff --git a/app/(app)/ascii/page.tsx b/app/(app)/ascii/page.tsx index 987f0ce..c84c98b 100644 --- a/app/(app)/ascii/page.tsx +++ b/app/(app)/ascii/page.tsx @@ -11,7 +11,8 @@ export default function ASCIIPage() { return ( diff --git a/app/(app)/color/page.tsx b/app/(app)/color/page.tsx index 12bae77..f45cf78 100644 --- a/app/(app)/color/page.tsx +++ b/app/(app)/color/page.tsx @@ -1,11 +1,20 @@ import type { Metadata } from 'next'; +import { ColorManipulation } from '@/components/color/ColorManipulation'; +import { AppPage } from '@/components/layout/AppPage'; import { getToolByHref } from '@/lib/tools'; -import { ColorPage } from '@/components/color/ColorPage'; const tool = getToolByHref('/color')!; export const metadata: Metadata = { title: tool.title }; -export default function Page() { - return ; +export default function ColorPage() { + return ( + + + + ); } diff --git a/app/(app)/favicon/page.tsx b/app/(app)/favicon/page.tsx index b5ecafe..af7eab9 100644 --- a/app/(app)/favicon/page.tsx +++ b/app/(app)/favicon/page.tsx @@ -11,7 +11,8 @@ export default function FaviconPage() { return ( diff --git a/app/(app)/media/page.tsx b/app/(app)/media/page.tsx index fab46b9..b06c6fe 100644 --- a/app/(app)/media/page.tsx +++ b/app/(app)/media/page.tsx @@ -11,7 +11,8 @@ export default function MediaPage() { return ( diff --git a/app/(app)/qrcode/page.tsx b/app/(app)/qrcode/page.tsx index 7a9c6f5..0bf446d 100644 --- a/app/(app)/qrcode/page.tsx +++ b/app/(app)/qrcode/page.tsx @@ -11,7 +11,8 @@ export default function QRCodePage() { return ( diff --git a/app/(app)/units/page.tsx b/app/(app)/units/page.tsx index 6016759..196d669 100644 --- a/app/(app)/units/page.tsx +++ b/app/(app)/units/page.tsx @@ -11,7 +11,8 @@ export default function UnitsPage() { return ( diff --git a/components/color/ColorManipulation.tsx b/components/color/ColorManipulation.tsx new file mode 100644 index 0000000..88adf23 --- /dev/null +++ b/components/color/ColorManipulation.tsx @@ -0,0 +1,378 @@ +'use client'; + +import { useState, useEffect, Suspense } from 'react'; +import { useSearchParams, useRouter } from 'next/navigation'; +import { ColorPicker } from '@/components/color/ColorPicker'; +import { ColorInfo } from '@/components/color/ColorInfo'; +import { ManipulationPanel } from '@/components/color/ManipulationPanel'; +import { PaletteGrid } from '@/components/color/PaletteGrid'; +import { ExportMenu } from '@/components/color/ExportMenu'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { useColorInfo, useGeneratePalette, useGenerateGradient } from '@/lib/color/api/queries'; +import { Loader2, Share2, Palette, Plus, X, Layers } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { toast } from 'sonner'; + +type HarmonyType = + | 'monochromatic' + | 'analogous' + | 'complementary' + | 'triadic' + | 'tetradic'; + +function ColorManipulationContent() { + const searchParams = useSearchParams(); + const router = useRouter(); + const [color, setColor] = useState(() => { + const urlColor = searchParams.get('color'); + return urlColor ? `#${urlColor.replace('#', '')}` : '#ff0099'; + }); + + // Harmony state + const [harmonyType, setHarmonyType] = useState('complementary'); + const [palette, setPalette] = useState([]); + const paletteMutation = useGeneratePalette(); + + // Gradient state + const [stops, setStops] = useState(['#ff0099', '#0099ff']); + const [gradientCount, setGradientCount] = useState(10); + const [gradientResult, setGradientResult] = useState([]); + const gradientMutation = useGenerateGradient(); + + const { data, isLoading, isError, error } = useColorInfo({ + colors: [color], + }); + + const colorInfo = data?.colors[0]; + + // Update URL when color changes + useEffect(() => { + const hex = color.replace('#', ''); + if (hex.length === 6 || hex.length === 3) { + router.push(`/color?color=${hex}`, { scroll: false }); + } + }, [color, router]); + + // Sync first gradient stop with active color + useEffect(() => { + const newStops = [...stops]; + newStops[0] = color; + setStops(newStops); + }, [color]); + + const handleShare = () => { + const url = `${window.location.origin}/color?color=${color.replace('#', '')}`; + navigator.clipboard.writeText(url); + toast.success('Link copied to clipboard!'); + }; + + const generateHarmony = async () => { + try { + const result = await paletteMutation.mutateAsync({ + base: color, + scheme: harmonyType, + }); + + const colors = [result.palette.primary, ...result.palette.secondary]; + setPalette(colors); + toast.success(`Generated ${harmonyType} harmony palette`); + } catch (error) { + toast.error('Failed to generate harmony palette'); + console.error(error); + } + }; + + const generateGradient = async () => { + try { + const result = await gradientMutation.mutateAsync({ + stops, + count: gradientCount, + }); + setGradientResult(result.gradient); + toast.success(`Generated ${result.gradient.length} colors`); + } catch (error) { + toast.error('Failed to generate gradient'); + } + }; + + const addStop = () => { + setStops([...stops, '#000000']); + }; + + const removeStop = (index: number) => { + if (index === 0) return; + if (stops.length > 2) { + setStops(stops.filter((_, i) => i !== index)); + } + }; + + const updateStop = (index: number, colorValue: string) => { + const newStops = [...stops]; + newStops[index] = colorValue; + setStops(newStops); + if (index === 0) setColor(colorValue); + }; + + const harmonyDescriptions: Record = { + monochromatic: 'Single color with variations', + analogous: 'Colors adjacent on the color wheel (±30°)', + complementary: 'Colors opposite on the color wheel (180°)', + triadic: 'Three colors evenly spaced on the color wheel (120°)', + tetradic: 'Four colors evenly spaced on the color wheel (90°)', + }; + + return ( +
+ {/* Row 1: Workspace */} +
+ {/* Main Workspace: Color Picker and Information */} +
+ + + Color Picker + + + +
+
+ +
+ +
+ {isLoading && ( +
+ +
+ )} + + {isError && ( +
+

Error loading color information

+

{error?.message || 'Unknown error'}

+
+ )} + + {colorInfo && } +
+
+
+
+
+ + {/* Sidebar: Color Manipulation */} +
+ + + Adjustments + + + + + +
+
+ + {/* Row 2: Harmony Generator */} +
+ {/* Harmony Controls */} +
+ + + Harmony + + + + +

+ {harmonyDescriptions[harmonyType]} +

+ + +
+
+
+ + {/* Harmony Results */} +
+ + + + Palette {palette.length > 0 && ({palette.length})} + + + + {palette.length > 0 ? ( +
+ +
+ +
+
+ ) : ( +
+ +

Generate a harmony palette from the current color

+
+ )} +
+
+
+
+ + {/* Row 3: Gradient Generator */} +
+ {/* Gradient Controls */} +
+ + + Gradient + + +
+ + {stops.map((stop, index) => ( +
+ updateStop(index, e.target.value)} + className="w-9 h-9 p-1 shrink-0 cursor-pointer" + /> + updateStop(index, e.target.value)} + className="font-mono text-xs flex-1" + /> + {index !== 0 && stops.length > 2 && ( + + )} +
+ ))} + +
+ +
+ + setGradientCount(parseInt(e.target.value))} + /> +
+ + +
+
+
+ + {/* Gradient Results */} +
+ + + + Gradient {gradientResult.length > 0 && ({gradientResult.length})} + + + + {gradientResult.length > 0 ? ( +
+
+ +
+ +
+
+ ) : ( +
+ +

Add color stops and generate a smooth gradient

+
+ )} + + +
+
+
+ ); +} + +export function ColorManipulation() { + return ( + + +
+ }> + + + ); +} diff --git a/components/color/ColorPage.tsx b/components/color/ColorPage.tsx deleted file mode 100644 index 21d4691..0000000 --- a/components/color/ColorPage.tsx +++ /dev/null @@ -1,391 +0,0 @@ -'use client'; - -import { useState, useEffect, Suspense } from 'react'; -import { useSearchParams, useRouter } from 'next/navigation'; -import { ColorPicker } from '@/components/color/ColorPicker'; -import { ColorInfo } from '@/components/color/ColorInfo'; -import { ManipulationPanel } from '@/components/color/ManipulationPanel'; -import { PaletteGrid } from '@/components/color/PaletteGrid'; -import { ExportMenu } from '@/components/color/ExportMenu'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { AppPage } from '@/components/layout/AppPage'; -import { useColorInfo, useGeneratePalette, useGenerateGradient } from '@/lib/color/api/queries'; -import { Loader2, Share2, Palette, Plus, X, Layers } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { toast } from 'sonner'; -import { getToolByHref } from '@/lib/tools'; - -const tool = getToolByHref('/color')!; - -type HarmonyType = - | 'monochromatic' - | 'analogous' - | 'complementary' - | 'triadic' - | 'tetradic'; - -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'; - }); - - // Harmony state - const [harmonyType, setHarmonyType] = useState('complementary'); - const [palette, setPalette] = useState([]); - const paletteMutation = useGeneratePalette(); - - // Gradient state - const [stops, setStops] = useState(['#ff0099', '#0099ff']); - const [gradientCount, setGradientCount] = useState(10); - const [gradientResult, setGradientResult] = useState([]); - const gradientMutation = useGenerateGradient(); - - const { data, isLoading, isError, error } = useColorInfo({ - colors: [color], - }); - - const colorInfo = data?.colors[0]; - - // Update URL when color changes - useEffect(() => { - const hex = color.replace('#', ''); - if (hex.length === 6 || hex.length === 3) { - router.push(`/color?color=${hex}`, { scroll: false }); - } - }, [color, router]); - - // Sync first gradient stop with active color - useEffect(() => { - const newStops = [...stops]; - newStops[0] = color; - setStops(newStops); - }, [color]); - - // Share color via URL - const handleShare = () => { - const url = `${window.location.origin}/color?color=${color.replace('#', '')}`; - navigator.clipboard.writeText(url); - toast.success('Link copied to clipboard!'); - }; - - const generateHarmony = async () => { - try { - const result = await paletteMutation.mutateAsync({ - base: color, - scheme: harmonyType, - }); - - const colors = [result.palette.primary, ...result.palette.secondary]; - setPalette(colors); - toast.success(`Generated ${harmonyType} harmony palette`); - } catch (error) { - toast.error('Failed to generate harmony palette'); - console.error(error); - } - }; - - const generateGradient = async () => { - try { - const result = await gradientMutation.mutateAsync({ - stops, - count: gradientCount, - }); - setGradientResult(result.gradient); - toast.success(`Generated ${result.gradient.length} colors`); - } catch (error) { - toast.error('Failed to generate gradient'); - } - }; - - const addStop = () => { - setStops([...stops, '#000000']); - }; - - const removeStop = (index: number) => { - if (index === 0) return; // Prevent deleting the first stop (synchronized with picker) - if (stops.length > 2) { - setStops(stops.filter((_, i) => i !== index)); - } - }; - - const updateStop = (index: number, colorValue: string) => { - const newStops = [...stops]; - newStops[index] = colorValue; - setStops(newStops); - if (index === 0) setColor(colorValue); - }; - - const harmonyDescriptions: Record = { - monochromatic: 'Single color with variations', - analogous: 'Colors adjacent on the color wheel (±30°)', - complementary: 'Colors opposite on the color wheel (180°)', - triadic: 'Three colors evenly spaced on the color wheel (120°)', - tetradic: 'Four colors evenly spaced on the color wheel (90°)', - }; - - return ( - -
- {/* Row 1: Workspace */} -
- {/* Main Workspace: Color Picker and Information */} -
- - - Color Picker - - - -
-
- -
- -
- {isLoading && ( -
- -
- )} - - {isError && ( -
-

Error loading color information

-

{error?.message || 'Unknown error'}

-
- )} - - {colorInfo && } -
-
-
-
-
- - {/* Sidebar: Color Manipulation */} -
- - - Adjustments - - - - - -
-
- - {/* Row 2: Harmony Generator */} -
- {/* Harmony Controls */} -
- - - Harmony - - - - -

- {harmonyDescriptions[harmonyType]} -

- - -
-
-
- - {/* Harmony Results */} -
- - - - Palette {palette.length > 0 && ({palette.length})} - - - - {palette.length > 0 ? ( -
- -
- -
-
- ) : ( -
- -

Generate a harmony palette from the current color

-
- )} -
-
-
-
- - {/* Row 3: Gradient Generator */} -
- {/* Gradient Controls */} -
- - - Gradient - - -
- - {stops.map((stop, index) => ( -
- updateStop(index, e.target.value)} - className="w-9 h-9 p-1 shrink-0 cursor-pointer" - /> - updateStop(index, e.target.value)} - className="font-mono text-xs flex-1" - /> - {index !== 0 && stops.length > 2 && ( - - )} -
- ))} - -
- -
- - setGradientCount(parseInt(e.target.value))} - /> -
- - -
-
-
- - {/* Gradient Results */} -
- - - - Gradient {gradientResult.length > 0 && ({gradientResult.length})} - - - - {gradientResult.length > 0 ? ( -
-
- -
- -
-
- ) : ( -
- -

Add color stops and generate a smooth gradient

-
- )} - - -
-
-
- - ); -} - -export function ColorPage() { - return ( - -
- -
-
- }> - - - ); -} diff --git a/components/layout/AppPage.tsx b/components/layout/AppPage.tsx index 9439133..5f1ef60 100644 --- a/components/layout/AppPage.tsx +++ b/components/layout/AppPage.tsx @@ -1,23 +1,15 @@ -'use client'; - import * as React from 'react'; -import { usePathname } from 'next/navigation'; import { cn } from '@/lib/utils'; -import { getToolByHref } from '@/lib/tools'; interface AppPageProps { title: string; description?: string; + icon?: React.ElementType; children: React.ReactNode; className?: string; } -export function AppPage({ title, description, children, className }: AppPageProps) { - const pathname = usePathname(); - const firstSegment = pathname.split('/').filter(Boolean)[0]; - const tool = getToolByHref(`/${firstSegment ?? ''}`); - const Icon = tool?.icon; - +export function AppPage({ title, description, icon: Icon, children, className }: AppPageProps) { return (
diff --git a/lib/tools.tsx b/lib/tools.tsx index a5a190e..c24450f 100644 --- a/lib/tools.tsx +++ b/lib/tools.tsx @@ -25,7 +25,7 @@ export const tools: Tool[] = [ title: 'Color Manipulation', navTitle: 'Color Manipulation', href: '/color', - description: 'Interactive color manipulation and analysis tool', + description: 'Interactive color manipulation and analysis tool.', summary: 'Modern color manipulation toolkit with palette generation, accessibility testing, and format conversion. Supports hex, RGB, HSL, Lab, and more.', icon: ColorIcon, @@ -36,7 +36,7 @@ export const tools: Tool[] = [ title: 'Units Converter', navTitle: 'Units Converter', href: '/units', - description: 'Smart unit converter with 187 units across 23 categories', + description: 'Smart unit converter with 187 units across 23 categories.', summary: 'Smart unit converter with 187 units across 23 categories. Real-time bidirectional conversion with fuzzy search.', icon: UnitsIcon, @@ -47,7 +47,7 @@ export const tools: Tool[] = [ title: 'ASCII Art Generator', navTitle: 'ASCII Art', href: '/ascii', - description: 'ASCII Art Text Generator with 373 Fonts', + description: 'ASCII Art Text Generator with 373 Fonts.', summary: 'ASCII art text generator with 373 fonts. Create stunning text banners, terminal art, and retro designs with live preview and multiple export formats.', icon: ASCIIIcon, @@ -58,7 +58,7 @@ export const tools: Tool[] = [ title: 'Media Converter', navTitle: 'Media Converter', href: '/media', - description: 'Professional browser-based media conversion for video, audio, and images', + description: 'Browser-based media conversion for video, audio, and images.', summary: 'Modern browser-based file converter powered by WebAssembly. Convert videos, images, and audio locally without server uploads. Privacy-first with no file size limits.', icon: MediaIcon, @@ -69,7 +69,7 @@ export const tools: Tool[] = [ title: 'Favicon Generator', navTitle: 'Favicon Generator', href: '/favicon', - description: 'Create a complete set of icons for your website including PWA manifest and HTML code.', + description: 'Create a complete set of icons for your website.', summary: 'Generate a complete set of favicons for your website. Includes PWA manifest and HTML embed code. All processing happens locally in your browser.', icon: FaviconIcon, @@ -80,7 +80,7 @@ export const tools: Tool[] = [ title: 'QR Code Generator', navTitle: 'QR Code Generator', href: '/qrcode', - description: 'Generate QR codes with custom colors, error correction, and multi-format export.', + description: 'Generate QR codes with custom colors and error correction.', summary: 'Generate QR codes with live preview, customizable colors, error correction levels, and export as PNG or SVG. All processing happens locally in your browser.', icon: QRCodeIcon,