refactor: extract ColorManipulation component and pass icon/summary to AppPage

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 09:57:06 +01:00
parent 82649f6674
commit 28747a6c8f
10 changed files with 408 additions and 415 deletions

View File

@@ -11,7 +11,8 @@ export default function ASCIIPage() {
return ( return (
<AppPage <AppPage
title={tool.title} title={tool.title}
description={tool.description} description={tool.summary}
icon={tool.icon}
> >
<ASCIIConverter /> <ASCIIConverter />
</AppPage> </AppPage>

View File

@@ -1,11 +1,20 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { ColorManipulation } from '@/components/color/ColorManipulation';
import { AppPage } from '@/components/layout/AppPage';
import { getToolByHref } from '@/lib/tools'; import { getToolByHref } from '@/lib/tools';
import { ColorPage } from '@/components/color/ColorPage';
const tool = getToolByHref('/color')!; const tool = getToolByHref('/color')!;
export const metadata: Metadata = { title: tool.title }; export const metadata: Metadata = { title: tool.title };
export default function Page() { export default function ColorPage() {
return <ColorPage />; return (
<AppPage
title={tool.title}
description={tool.summary}
icon={tool.icon}
>
<ColorManipulation />
</AppPage>
);
} }

View File

@@ -11,7 +11,8 @@ export default function FaviconPage() {
return ( return (
<AppPage <AppPage
title={tool.title} title={tool.title}
description={tool.description} description={tool.summary}
icon={tool.icon}
> >
<FaviconGenerator /> <FaviconGenerator />
</AppPage> </AppPage>

View File

@@ -11,7 +11,8 @@ export default function MediaPage() {
return ( return (
<AppPage <AppPage
title={tool.title} title={tool.title}
description={tool.description} description={tool.summary}
icon={tool.icon}
> >
<FileConverter /> <FileConverter />
</AppPage> </AppPage>

View File

@@ -11,7 +11,8 @@ export default function QRCodePage() {
return ( return (
<AppPage <AppPage
title={tool.title} title={tool.title}
description={tool.description} description={tool.summary}
icon={tool.icon}
> >
<QRCodeGenerator /> <QRCodeGenerator />
</AppPage> </AppPage>

View File

@@ -11,7 +11,8 @@ export default function UnitsPage() {
return ( return (
<AppPage <AppPage
title={tool.title} title={tool.title}
description={tool.description} description={tool.summary}
icon={tool.icon}
> >
<MainConverter /> <MainConverter />
</AppPage> </AppPage>

View File

@@ -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<HarmonyType>('complementary');
const [palette, setPalette] = useState<string[]>([]);
const paletteMutation = useGeneratePalette();
// Gradient state
const [stops, setStops] = useState<string[]>(['#ff0099', '#0099ff']);
const [gradientCount, setGradientCount] = useState(10);
const [gradientResult, setGradientResult] = useState<string[]>([]);
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<HarmonyType, string> = {
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 (
<div className="space-y-6">
{/* Row 1: Workspace */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch">
{/* Main Workspace: Color Picker and Information */}
<div className="lg:col-span-2">
<Card className="h-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle>Color Picker</CardTitle>
<Button onClick={handleShare} variant="outline" size="xs">
<Share2 className="h-3 w-3 mr-1" />
Share
</Button>
</CardHeader>
<CardContent>
<div className="flex flex-col md:flex-row gap-8">
<div className="flex-shrink-0 mx-auto md:mx-0">
<ColorPicker color={color} onChange={setColor} />
</div>
<div className="flex-1 min-w-0">
{isLoading && (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
)}
{isError && (
<div className="p-3 bg-destructive/10 text-destructive rounded-lg text-sm">
<p className="font-medium">Error loading color information</p>
<p className="mt-1">{error?.message || 'Unknown error'}</p>
</div>
)}
{colorInfo && <ColorInfo info={colorInfo} />}
</div>
</div>
</CardContent>
</Card>
</div>
{/* Sidebar: Color Manipulation */}
<div className="lg:col-span-1">
<Card className="h-full">
<CardHeader>
<CardTitle>Adjustments</CardTitle>
</CardHeader>
<CardContent>
<ManipulationPanel color={color} onColorChange={setColor} />
</CardContent>
</Card>
</div>
</div>
{/* Row 2: Harmony Generator */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch">
{/* Harmony Controls */}
<div className="lg:col-span-1">
<Card className="h-full">
<CardHeader>
<CardTitle>Harmony</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Select
value={harmonyType}
onValueChange={(value) => setHarmonyType(value as HarmonyType)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select harmony" />
</SelectTrigger>
<SelectContent>
<SelectItem value="monochromatic">Monochromatic</SelectItem>
<SelectItem value="analogous">Analogous</SelectItem>
<SelectItem value="complementary">Complementary</SelectItem>
<SelectItem value="triadic">Triadic</SelectItem>
<SelectItem value="tetradic">Tetradic (Square)</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{harmonyDescriptions[harmonyType]}
</p>
<Button
onClick={generateHarmony}
disabled={paletteMutation.isPending}
className="w-full"
>
{paletteMutation.isPending ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
Generating...
</>
) : (
'Generate'
)}
</Button>
</CardContent>
</Card>
</div>
{/* Harmony Results */}
<div className="lg:col-span-2">
<Card className="h-full">
<CardHeader>
<CardTitle>
Palette {palette.length > 0 && <span className="text-muted-foreground font-normal text-sm ml-1">({palette.length})</span>}
</CardTitle>
</CardHeader>
<CardContent>
{palette.length > 0 ? (
<div className="space-y-5">
<PaletteGrid colors={palette} onColorClick={setColor} />
<div className="pt-3 border-t">
<ExportMenu colors={palette} />
</div>
</div>
) : (
<div className="py-8 text-center text-muted-foreground text-xs">
<Palette className="h-8 w-8 mx-auto mb-2 opacity-20" />
<p>Generate a harmony palette from the current color</p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
{/* Row 3: Gradient Generator */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch">
{/* Gradient Controls */}
<div className="lg:col-span-1">
<Card className="h-full">
<CardHeader>
<CardTitle>Gradient</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label className="text-xs">Color Stops</Label>
{stops.map((stop, index) => (
<div key={index} className="flex items-center gap-2">
<Input
type="color"
value={stop}
onChange={(e) => updateStop(index, e.target.value)}
className="w-9 h-9 p-1 shrink-0 cursor-pointer"
/>
<Input
type="text"
value={stop}
onChange={(e) => updateStop(index, e.target.value)}
className="font-mono text-xs flex-1"
/>
{index !== 0 && stops.length > 2 && (
<Button
variant="ghost"
size="icon-xs"
onClick={() => removeStop(index)}
>
<X className="h-3.5 w-3.5" />
</Button>
)}
</div>
))}
<Button onClick={addStop} variant="outline" className="w-full">
<Plus className="h-3.5 w-3.5 mr-1.5" />
Add Stop
</Button>
</div>
<div className="space-y-2">
<Label className="text-xs">Steps</Label>
<Input
type="number"
min={2}
max={100}
value={gradientCount}
onChange={(e) => setGradientCount(parseInt(e.target.value))}
/>
</div>
<Button
onClick={generateGradient}
disabled={gradientMutation.isPending}
className="w-full"
>
{gradientMutation.isPending ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
Generating...
</>
) : (
'Generate'
)}
</Button>
</CardContent>
</Card>
</div>
{/* Gradient Results */}
<div className="lg:col-span-2">
<Card className="h-full">
<CardHeader>
<CardTitle>
Gradient {gradientResult.length > 0 && <span className="text-muted-foreground font-normal text-sm ml-1">({gradientResult.length})</span>}
</CardTitle>
</CardHeader>
<CardContent>
{gradientResult.length > 0 ? (
<div className="space-y-5">
<div
className="h-16 w-full rounded-lg border"
style={{
background: `linear-gradient(to right, ${gradientResult.join(', ')})`,
}}
/>
<PaletteGrid colors={gradientResult} onColorClick={setColor} />
<div className="pt-3 border-t">
<ExportMenu colors={gradientResult} />
</div>
</div>
) : (
<div className="py-8 text-center text-muted-foreground text-xs">
<Layers className="h-8 w-8 mx-auto mb-2 opacity-20" />
<p>Add color stops and generate a smooth gradient</p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
</div>
);
}
export function ColorManipulation() {
return (
<Suspense fallback={
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
}>
<ColorManipulationContent />
</Suspense>
);
}

View File

@@ -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<HarmonyType>('complementary');
const [palette, setPalette] = useState<string[]>([]);
const paletteMutation = useGeneratePalette();
// Gradient state
const [stops, setStops] = useState<string[]>(['#ff0099', '#0099ff']);
const [gradientCount, setGradientCount] = useState(10);
const [gradientResult, setGradientResult] = useState<string[]>([]);
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<HarmonyType, string> = {
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 (
<AppPage
title={tool.title}
description={tool.description}
>
<div className="space-y-6">
{/* Row 1: Workspace */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch">
{/* Main Workspace: Color Picker and Information */}
<div className="lg:col-span-2">
<Card className="h-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle>Color Picker</CardTitle>
<Button onClick={handleShare} variant="outline" size="xs">
<Share2 className="h-3 w-3 mr-1" />
Share
</Button>
</CardHeader>
<CardContent>
<div className="flex flex-col md:flex-row gap-8">
<div className="flex-shrink-0 mx-auto md:mx-0">
<ColorPicker color={color} onChange={setColor} />
</div>
<div className="flex-1 min-w-0">
{isLoading && (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
)}
{isError && (
<div className="p-3 bg-destructive/10 text-destructive rounded-lg text-sm">
<p className="font-medium">Error loading color information</p>
<p className="mt-1">{error?.message || 'Unknown error'}</p>
</div>
)}
{colorInfo && <ColorInfo info={colorInfo} />}
</div>
</div>
</CardContent>
</Card>
</div>
{/* Sidebar: Color Manipulation */}
<div className="lg:col-span-1">
<Card className="h-full">
<CardHeader>
<CardTitle>Adjustments</CardTitle>
</CardHeader>
<CardContent>
<ManipulationPanel color={color} onColorChange={setColor} />
</CardContent>
</Card>
</div>
</div>
{/* Row 2: Harmony Generator */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch">
{/* Harmony Controls */}
<div className="lg:col-span-1">
<Card className="h-full">
<CardHeader>
<CardTitle>Harmony</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Select
value={harmonyType}
onValueChange={(value) => setHarmonyType(value as HarmonyType)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select harmony" />
</SelectTrigger>
<SelectContent>
<SelectItem value="monochromatic">Monochromatic</SelectItem>
<SelectItem value="analogous">Analogous</SelectItem>
<SelectItem value="complementary">Complementary</SelectItem>
<SelectItem value="triadic">Triadic</SelectItem>
<SelectItem value="tetradic">Tetradic (Square)</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{harmonyDescriptions[harmonyType]}
</p>
<Button
onClick={generateHarmony}
disabled={paletteMutation.isPending}
className="w-full"
>
{paletteMutation.isPending ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
Generating...
</>
) : (
'Generate'
)}
</Button>
</CardContent>
</Card>
</div>
{/* Harmony Results */}
<div className="lg:col-span-2">
<Card className="h-full">
<CardHeader>
<CardTitle>
Palette {palette.length > 0 && <span className="text-muted-foreground font-normal text-sm ml-1">({palette.length})</span>}
</CardTitle>
</CardHeader>
<CardContent>
{palette.length > 0 ? (
<div className="space-y-5">
<PaletteGrid colors={palette} onColorClick={setColor} />
<div className="pt-3 border-t">
<ExportMenu colors={palette} />
</div>
</div>
) : (
<div className="py-8 text-center text-muted-foreground text-xs">
<Palette className="h-8 w-8 mx-auto mb-2 opacity-20" />
<p>Generate a harmony palette from the current color</p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
{/* Row 3: Gradient Generator */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch">
{/* Gradient Controls */}
<div className="lg:col-span-1">
<Card className="h-full">
<CardHeader>
<CardTitle>Gradient</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label className="text-xs">Color Stops</Label>
{stops.map((stop, index) => (
<div key={index} className="flex items-center gap-2">
<Input
type="color"
value={stop}
onChange={(e) => updateStop(index, e.target.value)}
className="w-9 h-9 p-1 shrink-0 cursor-pointer"
/>
<Input
type="text"
value={stop}
onChange={(e) => updateStop(index, e.target.value)}
className="font-mono text-xs flex-1"
/>
{index !== 0 && stops.length > 2 && (
<Button
variant="ghost"
size="icon-xs"
onClick={() => removeStop(index)}
>
<X className="h-3.5 w-3.5" />
</Button>
)}
</div>
))}
<Button onClick={addStop} variant="outline" className="w-full">
<Plus className="h-3.5 w-3.5 mr-1.5" />
Add Stop
</Button>
</div>
<div className="space-y-2">
<Label className="text-xs">Steps</Label>
<Input
type="number"
min={2}
max={100}
value={gradientCount}
onChange={(e) => setGradientCount(parseInt(e.target.value))}
/>
</div>
<Button
onClick={generateGradient}
disabled={gradientMutation.isPending}
className="w-full"
>
{gradientMutation.isPending ? (
<>
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
Generating...
</>
) : (
'Generate'
)}
</Button>
</CardContent>
</Card>
</div>
{/* Gradient Results */}
<div className="lg:col-span-2">
<Card className="h-full">
<CardHeader>
<CardTitle>
Gradient {gradientResult.length > 0 && <span className="text-muted-foreground font-normal text-sm ml-1">({gradientResult.length})</span>}
</CardTitle>
</CardHeader>
<CardContent>
{gradientResult.length > 0 ? (
<div className="space-y-5">
<div
className="h-16 w-full rounded-lg border"
style={{
background: `linear-gradient(to right, ${gradientResult.join(', ')})`,
}}
/>
<PaletteGrid colors={gradientResult} onColorClick={setColor} />
<div className="pt-3 border-t">
<ExportMenu colors={gradientResult} />
</div>
</div>
) : (
<div className="py-8 text-center text-muted-foreground text-xs">
<Layers className="h-8 w-8 mx-auto mb-2 opacity-20" />
<p>Add color stops and generate a smooth gradient</p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
</div>
</AppPage>
);
}
export function ColorPage() {
return (
<Suspense fallback={
<div className="min-h-screen py-12">
<div className="max-w-7xl mx-auto px-8 flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
</div>
}>
<PlaygroundContent />
</Suspense>
);
}

View File

@@ -1,23 +1,15 @@
'use client';
import * as React from 'react'; import * as React from 'react';
import { usePathname } from 'next/navigation';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { getToolByHref } from '@/lib/tools';
interface AppPageProps { interface AppPageProps {
title: string; title: string;
description?: string; description?: string;
icon?: React.ElementType;
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
} }
export function AppPage({ title, description, children, className }: AppPageProps) { export function AppPage({ title, description, icon: Icon, children, className }: AppPageProps) {
const pathname = usePathname();
const firstSegment = pathname.split('/').filter(Boolean)[0];
const tool = getToolByHref(`/${firstSegment ?? ''}`);
const Icon = tool?.icon;
return ( return (
<div className={cn("min-h-screen py-8", className)}> <div className={cn("min-h-screen py-8", className)}>
<div className="max-w-7xl mx-auto px-8 space-y-6 animate-fade-in"> <div className="max-w-7xl mx-auto px-8 space-y-6 animate-fade-in">

View File

@@ -25,7 +25,7 @@ export const tools: Tool[] = [
title: 'Color Manipulation', title: 'Color Manipulation',
navTitle: 'Color Manipulation', navTitle: 'Color Manipulation',
href: '/color', href: '/color',
description: 'Interactive color manipulation and analysis tool', description: 'Interactive color manipulation and analysis tool.',
summary: summary:
'Modern color manipulation toolkit with palette generation, accessibility testing, and format conversion. Supports hex, RGB, HSL, Lab, and more.', 'Modern color manipulation toolkit with palette generation, accessibility testing, and format conversion. Supports hex, RGB, HSL, Lab, and more.',
icon: ColorIcon, icon: ColorIcon,
@@ -36,7 +36,7 @@ export const tools: Tool[] = [
title: 'Units Converter', title: 'Units Converter',
navTitle: 'Units Converter', navTitle: 'Units Converter',
href: '/units', href: '/units',
description: 'Smart unit converter with 187 units across 23 categories', description: 'Smart unit converter with 187 units across 23 categories.',
summary: summary:
'Smart unit converter with 187 units across 23 categories. Real-time bidirectional conversion with fuzzy search.', 'Smart unit converter with 187 units across 23 categories. Real-time bidirectional conversion with fuzzy search.',
icon: UnitsIcon, icon: UnitsIcon,
@@ -47,7 +47,7 @@ export const tools: Tool[] = [
title: 'ASCII Art Generator', title: 'ASCII Art Generator',
navTitle: 'ASCII Art', navTitle: 'ASCII Art',
href: '/ascii', href: '/ascii',
description: 'ASCII Art Text Generator with 373 Fonts', description: 'ASCII Art Text Generator with 373 Fonts.',
summary: summary:
'ASCII art text generator with 373 fonts. Create stunning text banners, terminal art, and retro designs with live preview and multiple export formats.', '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, icon: ASCIIIcon,
@@ -58,7 +58,7 @@ export const tools: Tool[] = [
title: 'Media Converter', title: 'Media Converter',
navTitle: 'Media Converter', navTitle: 'Media Converter',
href: '/media', href: '/media',
description: 'Professional browser-based media conversion for video, audio, and images', description: 'Browser-based media conversion for video, audio, and images.',
summary: 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.', '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, icon: MediaIcon,
@@ -69,7 +69,7 @@ export const tools: Tool[] = [
title: 'Favicon Generator', title: 'Favicon Generator',
navTitle: 'Favicon Generator', navTitle: 'Favicon Generator',
href: '/favicon', 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: summary:
'Generate a complete set of favicons for your website. Includes PWA manifest and HTML embed code. All processing happens locally in your browser.', '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, icon: FaviconIcon,
@@ -80,7 +80,7 @@ export const tools: Tool[] = [
title: 'QR Code Generator', title: 'QR Code Generator',
navTitle: 'QR Code Generator', navTitle: 'QR Code Generator',
href: '/qrcode', 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: 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.', '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, icon: QRCodeIcon,