Replace hex color codes with friendly color names for customers
- Add colorNames utility with 80+ standard color names - Map hex codes to human-readable names (Black, Navy, Red, Teal, etc) - Display color names in product page (with hex tooltip for reference) - Show selected color name below color swatches - Update shopping cart to display color names instead of hex codes - Admin color picker shows both name and hex code for clarity Improves UX for customers who don't understand hex color codes. Keeps hex codes for internal data and system tooltips. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
47bc87692d
commit
26ac37b1df
@@ -3,6 +3,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useCart } from '@/lib/cartStore';
|
import { useCart } from '@/lib/cartStore';
|
||||||
|
import { getColorName } from '@/lib/colorNames';
|
||||||
import Price from '@/components/Price';
|
import Price from '@/components/Price';
|
||||||
|
|
||||||
export default function CartPage() {
|
export default function CartPage() {
|
||||||
@@ -70,8 +71,9 @@ export default function CartPage() {
|
|||||||
<span
|
<span
|
||||||
className="h-3.5 w-3.5 rounded-full border border-line"
|
className="h-3.5 w-3.5 rounded-full border border-line"
|
||||||
style={{ backgroundColor: item.color }}
|
style={{ backgroundColor: item.color }}
|
||||||
|
title={item.color}
|
||||||
/>
|
/>
|
||||||
{item.color}
|
<span>{getColorName(item.color)}</span>
|
||||||
</span>
|
</span>
|
||||||
{item.size && <span>Size {item.size}</span>}
|
{item.size && <span>Size {item.size}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { getColorName } from '@/lib/colorNames';
|
||||||
|
|
||||||
function frontFieldName(hex: string) {
|
function frontFieldName(hex: string) {
|
||||||
return `colorPhoto__${hex.replace('#', '')}`;
|
return `colorPhoto__${hex.replace('#', '')}`;
|
||||||
@@ -203,6 +204,7 @@ export default function ColorPicker({
|
|||||||
<div key={`${r.filename}-${i}`} className="flex items-center gap-2 text-xs">
|
<div key={`${r.filename}-${i}`} className="flex items-center gap-2 text-xs">
|
||||||
<div className="h-4 w-4 shrink-0 rounded-full border border-line" style={{ backgroundColor: r.hex }} />
|
<div className="h-4 w-4 shrink-0 rounded-full border border-line" style={{ backgroundColor: r.hex }} />
|
||||||
<span className="font-mono text-muted">{r.hex}</span>
|
<span className="font-mono text-muted">{r.hex}</span>
|
||||||
|
<span className="text-muted">({getColorName(r.hex)})</span>
|
||||||
<span className="truncate text-muted">{r.filename}</span>
|
<span className="truncate text-muted">{r.filename}</span>
|
||||||
<span className="tag-label shrink-0 text-muted">{r.variant}</span>
|
<span className="tag-label shrink-0 text-muted">{r.variant}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,7 +240,10 @@ export default function ColorPicker({
|
|||||||
onChange={(e) => setPicked(e.target.value)}
|
onChange={(e) => setPicked(e.target.value)}
|
||||||
className="h-9 w-9 cursor-pointer border border-line bg-paper p-0"
|
className="h-9 w-9 cursor-pointer border border-line bg-paper p-0"
|
||||||
/>
|
/>
|
||||||
<span className="font-mono text-xs text-muted">{picked}</span>
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<span className="text-xs font-medium">{getColorName(picked)}</span>
|
||||||
|
<span className="font-mono text-xs text-muted">{picked}</span>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={addColor}
|
onClick={addColor}
|
||||||
@@ -266,7 +271,10 @@ export default function ColorPicker({
|
|||||||
<div key={hex} className="border border-line p-2">
|
<div key={hex} className="border border-line p-2">
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
<div className="h-6 w-6 shrink-0 rounded-full border border-line" style={{ backgroundColor: hex }} />
|
<div className="h-6 w-6 shrink-0 rounded-full border border-line" style={{ backgroundColor: hex }} />
|
||||||
<span className="font-mono text-xs text-muted">{hex}</span>
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<span className="font-medium text-xs">{getColorName(hex)}</span>
|
||||||
|
<span className="font-mono text-xs text-muted">{hex}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2 sm:grid-cols-2">
|
<div className="grid gap-2 sm:grid-cols-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { v4 as uuid } from 'uuid';
|
|||||||
import ProductMockup from './ProductMockup';
|
import ProductMockup from './ProductMockup';
|
||||||
import Price from './Price';
|
import Price from './Price';
|
||||||
import { useCart } from '@/lib/cartStore';
|
import { useCart } from '@/lib/cartStore';
|
||||||
|
import { getColorName } from '@/lib/colorNames';
|
||||||
import type { ProductDTO } from '@/lib/types';
|
import type { ProductDTO } from '@/lib/types';
|
||||||
|
|
||||||
export default function StandardProductView({ product }: { product: ProductDTO }) {
|
export default function StandardProductView({ product }: { product: ProductDTO }) {
|
||||||
@@ -104,18 +105,26 @@ export default function StandardProductView({ product }: { product: ProductDTO }
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="tag-label mb-3">Color</p>
|
<p className="tag-label mb-3">Color</p>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="space-y-2">
|
||||||
{product.colors.map((c) => (
|
<div className="flex flex-wrap gap-3">
|
||||||
<button
|
{product.colors.map((c) => (
|
||||||
key={c}
|
<button
|
||||||
onClick={() => setColor(c)}
|
key={c}
|
||||||
aria-label={`Choose color ${c}`}
|
onClick={() => setColor(c)}
|
||||||
className={`h-9 w-9 rounded-full border transition-transform ${
|
aria-label={`Choose ${getColorName(c)} color`}
|
||||||
color === c ? 'scale-110 border-ink ring-2 ring-ink ring-offset-2 ring-offset-paper' : 'border-line'
|
className={`h-9 w-9 rounded-full border transition-transform ${
|
||||||
}`}
|
color === c ? 'scale-110 border-ink ring-2 ring-ink ring-offset-2 ring-offset-paper' : 'border-line'
|
||||||
style={{ backgroundColor: c }}
|
}`}
|
||||||
/>
|
style={{ backgroundColor: c }}
|
||||||
))}
|
title={getColorName(c)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{color && (
|
||||||
|
<p className="text-sm text-muted font-medium">
|
||||||
|
Selected: <span className="text-ink">{getColorName(color)}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{product.imageUrl && !product.photoRecolorable && Object.keys(product.colorPhotos).length === 0 && (
|
{product.imageUrl && !product.photoRecolorable && Object.keys(product.colorPhotos).length === 0 && (
|
||||||
<p className="mt-2 text-xs text-muted">
|
<p className="mt-2 text-xs text-muted">
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* Convert hex color codes to friendly color names
|
||||||
|
* Customers see names, admins/data uses hex codes internally
|
||||||
|
*/
|
||||||
|
|
||||||
|
const COLOR_NAMES: Record<string, string> = {
|
||||||
|
// Neutrals
|
||||||
|
'#000000': 'Black',
|
||||||
|
'#1a1a1a': 'Charcoal',
|
||||||
|
'#333333': 'Dark Gray',
|
||||||
|
'#4d4d4d': 'Gray',
|
||||||
|
'#666666': 'Medium Gray',
|
||||||
|
'#808080': 'Light Gray',
|
||||||
|
'#999999': 'Silver',
|
||||||
|
'#b3b3b3': 'Pale Gray',
|
||||||
|
'#cccccc': 'Very Light Gray',
|
||||||
|
'#e6e6e6': 'Off-White',
|
||||||
|
'#ffffff': 'White',
|
||||||
|
|
||||||
|
// Reds & Pinks
|
||||||
|
'#cc0000': 'Red',
|
||||||
|
'#ff0000': 'Bright Red',
|
||||||
|
'#ff3333': 'Light Red',
|
||||||
|
'#ff6666': 'Coral',
|
||||||
|
'#ff9999': 'Salmon',
|
||||||
|
'#ffcccc': 'Pink',
|
||||||
|
'#ff69b4': 'Hot Pink',
|
||||||
|
'#ff1493': 'Deep Pink',
|
||||||
|
'#c71585': 'Crimson',
|
||||||
|
'#8b0000': 'Dark Red',
|
||||||
|
|
||||||
|
// Oranges & Browns
|
||||||
|
'#ff6600': 'Orange',
|
||||||
|
'#ff8c00': 'Dark Orange',
|
||||||
|
'#ffaa00': 'Burnt Orange',
|
||||||
|
'#ff9900': 'Apricot',
|
||||||
|
'#ffbb99': 'Peach',
|
||||||
|
'#cc6600': 'Rust',
|
||||||
|
'#996633': 'Brown',
|
||||||
|
'#8b4513': 'Saddle Brown',
|
||||||
|
'#a0522d': 'Sienna',
|
||||||
|
'#d2691e': 'Chocolate',
|
||||||
|
|
||||||
|
// Yellows & Golds
|
||||||
|
'#ffff00': 'Yellow',
|
||||||
|
'#ffff99': 'Light Yellow',
|
||||||
|
'#ffcc00': 'Gold',
|
||||||
|
'#ffd700': 'Bright Gold',
|
||||||
|
'#f0e68c': 'Khaki',
|
||||||
|
'#f4a460': 'Sandy Brown',
|
||||||
|
'#daa520': 'Goldenrod',
|
||||||
|
|
||||||
|
// Greens
|
||||||
|
'#00aa00': 'Green',
|
||||||
|
'#00cc00': 'Lime',
|
||||||
|
'#66ff66': 'Light Green',
|
||||||
|
'#99ff99': 'Pale Green',
|
||||||
|
'#33cc33': 'Kelly Green',
|
||||||
|
'#228b22': 'Forest Green',
|
||||||
|
'#006600': 'Dark Green',
|
||||||
|
'#355c3d': 'Hunter Green',
|
||||||
|
'#2f4f2f': 'Dark Forest Green',
|
||||||
|
'#3cb371': 'Medium Sea Green',
|
||||||
|
'#20b2aa': 'Light Sea Green',
|
||||||
|
'#008080': 'Teal',
|
||||||
|
'#006666': 'Dark Teal',
|
||||||
|
|
||||||
|
// Blues
|
||||||
|
'#0066ff': 'Blue',
|
||||||
|
'#0099cc': 'Sky Blue',
|
||||||
|
'#00ccff': 'Cyan',
|
||||||
|
'#3399ff': 'Light Blue',
|
||||||
|
'#6699ff': 'Periwinkle',
|
||||||
|
'#9999ff': 'Lavender Blue',
|
||||||
|
'#1e90ff': 'Dodger Blue',
|
||||||
|
'#4169e1': 'Royal Blue',
|
||||||
|
'#00008b': 'Dark Blue',
|
||||||
|
'#000080': 'Navy',
|
||||||
|
'#191970': 'Midnight Blue',
|
||||||
|
|
||||||
|
// Purples & Violets
|
||||||
|
'#9933ff': 'Purple',
|
||||||
|
'#cc00ff': 'Magenta',
|
||||||
|
'#ff00ff': 'Fuchsia',
|
||||||
|
'#ee82ee': 'Violet',
|
||||||
|
'#9370db': 'Medium Purple',
|
||||||
|
'#8b008b': 'Dark Magenta',
|
||||||
|
'#4b0082': 'Indigo',
|
||||||
|
'#663399': 'Rebecca Purple',
|
||||||
|
|
||||||
|
// Common craft colors
|
||||||
|
'#17181c': 'Graphite',
|
||||||
|
'#1ea7e0': 'Turquoise',
|
||||||
|
'#fbe9e7': 'Blush',
|
||||||
|
'#f5f5f5': 'Smoke',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the friendly name for a hex color
|
||||||
|
* Falls back to "Custom" if hex code is not in the standard palette
|
||||||
|
*/
|
||||||
|
export function getColorName(hex: string): string {
|
||||||
|
const normalized = hex.toLowerCase();
|
||||||
|
return COLOR_NAMES[normalized] || 'Custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the hex code from a color name
|
||||||
|
* Used for reverse lookup if needed
|
||||||
|
*/
|
||||||
|
export function getColorHex(name: string): string | null {
|
||||||
|
for (const [hex, colorName] of Object.entries(COLOR_NAMES)) {
|
||||||
|
if (colorName.toLowerCase() === name.toLowerCase()) {
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a hex code is in our standard palette
|
||||||
|
*/
|
||||||
|
export function isStandardColor(hex: string): boolean {
|
||||||
|
return hex.toLowerCase() in COLOR_NAMES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available standard color names
|
||||||
|
*/
|
||||||
|
export function getAllColorNames(): string[] {
|
||||||
|
return Object.values(COLOR_NAMES).sort();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user