-
-
- H
- {hsl.h}°
-
-
handleHslChange("h", parseInt(e.target.value))}
- className="w-full"
- />
-
-
-
-
S
-
{hsl.s}%
+ {/* Right Column: Sliders */}
+
+
+ {/* RGB Section */}
+
+
+
+
+
+ RGB Channels
+
+
+ {[
+ { label: 'R', channel: 'r', val: rgb.r, max: 255, gradient: `linear-gradient(to right, rgb(0,${rgb.g},${rgb.b}), rgb(255,${rgb.g},${rgb.b}))` },
+ { label: 'G', channel: 'g', val: rgb.g, max: 255, gradient: `linear-gradient(to right, rgb(${rgb.r},0,${rgb.b}), rgb(${rgb.r},255,${rgb.b}))` },
+ { label: 'B', channel: 'b', val: rgb.b, max: 255, gradient: `linear-gradient(to right, rgb(${rgb.r},${rgb.g},0), rgb(${rgb.r},${rgb.g},255))` }
+ ].map((item) => (
+
+
+ {item.label}
+ {item.val}
+
+
handleRgbChange(item.channel as any, parseInt(e.target.value))}
+ className="w-full h-2 rounded-full appearance-none cursor-pointer hover:opacity-90 transition-opacity"
+ style={{ background: item.gradient }}
+ />
-
handleHslChange("s", parseInt(e.target.value))}
- className="w-full"
- />
-
-
-
- L
- {hsl.l}%
+ ))}
+
+
+
+ {/* HSL Section */}
+
+
+ HSL
+ Model Control
+
+
+ {[
+ { label: 'Hue', channel: 'h', val: hsl.h, max: 360, unit: '°', bg: 'linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)' },
+ { label: 'Saturation', channel: 's', val: hsl.s, max: 100, unit: '%', bg: 'linear-gradient(to right, gray, disabled)' }, // Custom bg for S needed
+ { label: 'Lightness', channel: 'l', val: hsl.l, max: 100, unit: '%', bg: 'linear-gradient(to right, black, white)' }
+ ].map((item) => (
+
+
+ {item.label}
+ {item.val}{item.unit}
+
+
handleHslChange(item.channel as any, parseInt(e.target.value))}
+ className="w-full h-2 rounded-full appearance-none cursor-pointer bg-muted"
+ style={item.channel === 'h' ? { background: item.bg } : undefined}
+ />
-
handleHslChange("l", parseInt(e.target.value))}
- className="w-full"
- />
-
+ ))}
+
diff --git a/components/color-picker/popover-picker.tsx b/components/color-picker/popover-picker.tsx
new file mode 100644
index 0000000..abe2df2
--- /dev/null
+++ b/components/color-picker/popover-picker.tsx
@@ -0,0 +1,117 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
+import { Label } from "@/components/ui/label"
+import { Input } from "@/components/ui/input"
+import { hexToRgb, rgbToHex, rgbToHsl, hslToRgb } from "@/lib/color-utils"
+
+interface PopoverPickerProps {
+ color: string
+ onChange: (color: string) => void
+ trigger?: React.ReactNode
+}
+
+export function PopoverPicker({ color, onChange, trigger }: PopoverPickerProps) {
+ const [hexValue, setHexValue] = useState(color)
+ const rgb = hexToRgb(color) || { r: 0, g: 0, b: 0 }
+ const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)
+
+ useEffect(() => {
+ setHexValue(color)
+ }, [color])
+
+ const handleHexChange = (value: string) => {
+ setHexValue(value)
+ if (/^#[0-9A-F]{6}$/i.test(value)) {
+ onChange(value)
+ }
+ }
+
+ const handleRgbChange = (channel: "r" | "g" | "b", value: number) => {
+ const newRgb = { ...rgb, [channel]: Math.max(0, Math.min(255, value)) }
+ onChange(rgbToHex(newRgb.r, newRgb.g, newRgb.b))
+ }
+
+ const handleHslChange = (channel: "h" | "s" | "l", value: number) => {
+ let newHsl = { ...hsl, [channel]: value }
+ if (channel === "h") newHsl.h = ((value % 360) + 360) % 360
+ if (channel === "s") newHsl.s = Math.max(0, Math.min(100, value))
+ if (channel === "l") newHsl.l = Math.max(0, Math.min(100, value))
+
+ const newRgb = hslToRgb(newHsl.h, newHsl.s, newHsl.l)
+ onChange(rgbToHex(newRgb.r, newRgb.g, newRgb.b))
+ }
+
+ return (
+
+
+ {trigger}
+
+
+
+ {/* RGB Sliders */}
+
+
RGB Channels
+ {[
+ { label: 'R', channel: 'r', val: rgb.r, gradient: `linear-gradient(to right, rgb(0,${rgb.g},${rgb.b}), rgb(255,${rgb.g},${rgb.b}))` },
+ { label: 'G', channel: 'g', val: rgb.g, gradient: `linear-gradient(to right, rgb(${rgb.r},0,${rgb.b}), rgb(${rgb.r},255,${rgb.b}))` },
+ { label: 'B', channel: 'b', val: rgb.b, gradient: `linear-gradient(to right, rgb(${rgb.r},${rgb.g},0), rgb(${rgb.r},${rgb.g},255))` }
+ ].map((item) => (
+
+
+ {item.label}
+ {item.val}
+
+
handleRgbChange(item.channel as any, parseInt(e.target.value))}
+ className="w-full h-2 rounded-full appearance-none cursor-pointer hover:opacity-90 transition-opacity focus:outline-none focus:ring-2 focus:ring-primary/20"
+ style={{ background: item.gradient }}
+ />
+
+ ))}
+
+
+
+
+ {/* HSL Sliders */}
+
+
HSL Overlay
+ {[
+ { label: 'Hue', channel: 'h', val: hsl.h, max: 360, bg: 'linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)' },
+ ].map((item) => (
+
+
+ {item.label}
+ {item.val}°
+
+
handleHslChange(item.channel as any, parseInt(e.target.value))}
+ className="w-full h-2 rounded-full appearance-none cursor-pointer hover:opacity-90 transition-opacity focus:outline-none focus:ring-2 focus:ring-primary/20"
+ style={{ background: item.bg }}
+ />
+
+ ))}
+
+
+
+ Hex Code
+ handleHexChange(e.target.value)}
+ className="h-9 font-mono bg-white/5 border-white/10 focus:border-primary/50"
+ />
+
+
+
+
+ )
+}
diff --git a/components/home/hero.tsx b/components/home/hero.tsx
index bccdd5a..602ea9c 100644
--- a/components/home/hero.tsx
+++ b/components/home/hero.tsx
@@ -129,14 +129,13 @@ const Hero = () => {
transition={{ duration: 1.2, delay: 0.7, ease: [0.19, 1, 0.22, 1] }}
className="absolute bottom-[-15%] md:bottom-[-2%] left-1/2 -translate-x-1/2 md:left-[-28%] md:translate-x-0 w-[260px] h-[170px] md:w-[480px] md:h-[320px] bg-white rounded-[24px] md:rounded-[40px] overflow-hidden flex border-none z-20"
>
- {currentColors.map((color, idx) => (
+ {Array.from({ length: 5 }).map((_, idx) => (
))}
diff --git a/components/home/instant-color-picker.tsx b/components/home/instant-color-picker.tsx
index 4024043..325ae2a 100644
--- a/components/home/instant-color-picker.tsx
+++ b/components/home/instant-color-picker.tsx
@@ -1,14 +1,13 @@
"use client"
-import { useState, useRef, useEffect } from "react"
+import { useState, useRef, useEffect, useCallback, memo } from "react"
import Image from "next/image"
import Link from "next/link"
-import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
-import { Upload, Copy, Maximize2, Minus, Plus, Download, Save, ChevronRight, CheckCircle2, X } from "lucide-react"
+import { Upload, Copy, Maximize2, Minus, Plus, Download, Save, CheckCircle2, X } from "lucide-react"
import { hexToRgb, rgbToHex, rgbToHsl, extractColorsFromImage, generateTints } from "@/lib/color-utils"
import { toast } from "sonner"
-import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"
+import { Dialog, DialogContent } from "@/components/ui/dialog"
import { ExportPaletteDialog } from "@/components/home/export-palette-dialog"
import { useUser, useClerk } from "@clerk/nextjs"
@@ -19,44 +18,14 @@ export function InstantColorPicker() {
const [image, setImage] = useState
("/image/demo.jpg");
const [extractedColors, setExtractedColors] = useState([]);
const [copiedColor, setCopiedColor] = useState(null);
- const [showMagnifier, setShowMagnifier] = useState(false);
- const [cursorPosition, setCursorPosition] = useState({ x: 0, y: 0 });
const [isFullScreen, setIsFullScreen] = useState(false);
const [isExportOpen, setIsExportOpen] = useState(false);
- const canvasRef = useRef(null);
- const imageRef = useRef(null);
const fileInputRef = useRef(null);
const rgb = hexToRgb(selectedColor) || { r: 0, g: 0, b: 0 };
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
- // Initialize: Load default image to canvas and extract colors
- useEffect(() => {
- if (image) {
- const img = new window.Image();
- img.crossOrigin = "Anonymous"; // In case of external images, though local is fine
- img.onload = () => {
- const canvas = canvasRef.current;
- if (!canvas) return;
- const ctx = canvas.getContext("2d");
- if (!ctx) return;
-
- canvas.width = img.width;
- canvas.height = img.height;
- ctx.drawImage(img, 0, 0);
-
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
- const colors = extractColorsFromImage(imageData, 10);
- setExtractedColors(colors);
- if (colors.length > 0) {
- setSelectedColor(colors[0]);
- }
- };
- img.src = image;
- }
- }, [image]);
-
const handleImageUpload = (e: React.ChangeEvent) => {
const file = e.target.files?.[0];
if (file) {
@@ -69,24 +38,6 @@ export function InstantColorPicker() {
}
};
- const handleImageClick = (e: React.MouseEvent) => {
- const canvas = canvasRef.current;
- const ctx = canvas?.getContext("2d");
- const img = imageRef.current;
- if (!canvas || !ctx || !img) return;
-
- const rect = img.getBoundingClientRect();
- const scaleX = canvas.width / rect.width;
- const scaleY = canvas.height / rect.height;
- const x = (e.clientX - rect.left) * scaleX;
- const y = (e.clientY - rect.top) * scaleY;
-
- const pixel = ctx.getImageData(x, y, 1, 1).data;
- const hex = rgbToHex(pixel[0], pixel[1], pixel[2]);
- setSelectedColor(hex);
- // toast.success(`Color ${hex} selected`) // Optional: remove toast for cleaner UI
- };
-
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
setCopiedColor(text);
@@ -94,6 +45,17 @@ export function InstantColorPicker() {
setTimeout(() => setCopiedColor(null), 2000);
};
+ const handleColorsExtracted = useCallback((colors: string[]) => {
+ setExtractedColors(colors);
+ if (colors.length > 0) {
+ setSelectedColor(colors[0]);
+ }
+ }, []);
+
+ const handleColorSelect = useCallback((color: string) => {
+ setSelectedColor(color);
+ }, []);
+
return (
{/* Top Tabs */}
@@ -112,7 +74,6 @@ export function InstantColorPicker() {
{/* Main Card */}
- {/* Resize Icon (Visual only based on screenshot) */}
{/* Resize Icon */}
setIsFullScreen(true)}
@@ -133,50 +94,11 @@ export function InstantColorPicker() {
Image
{/* Image Preview Area */}
- setShowMagnifier(true)}
- onMouseLeave={() => setShowMagnifier(false)}
- onMouseMove={(e) => {
- const rect = e.currentTarget.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
- setCursorPosition({ x, y });
- }}
- >
-
- {image ? (
- <>
-
- {/*
- Wait, CSS background approach is hard because of `object-contain`.
- Alternative: Use a canvas-based magnifier.
- I'll add a separate small canvas for the magnifier.
- */}
-
- >
- ) : (
-
-
- No image uploaded
-
- )}
-
+
{/* Color Palette Section */}
@@ -348,6 +270,113 @@ export function InstantColorPicker() {
);
}
+// ----------------------------------------------------------------------
+// SUB-COMPONENTS
+// ----------------------------------------------------------------------
+
+interface InteractiveImageAreaProps {
+ image: string | null;
+ onColorsExtracted: (colors: string[]) => void;
+ onColorSelect: (color: string) => void;
+}
+
+const InteractiveImageArea = memo(function InteractiveImageArea({
+ image,
+ onColorsExtracted,
+ onColorSelect
+}: InteractiveImageAreaProps) {
+ const [showMagnifier, setShowMagnifier] = useState(false);
+ const [cursorPosition, setCursorPosition] = useState({ x: 0, y: 0 });
+ const canvasRef = useRef
(null);
+ const imageRef = useRef(null);
+
+ // Load image to canvas and extract colors
+ useEffect(() => {
+ if (image) {
+ const img = new window.Image();
+ img.crossOrigin = "Anonymous";
+ img.onload = () => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = canvas.getContext("2d");
+ if (!ctx) return;
+
+ canvas.width = img.width;
+ canvas.height = img.height;
+ ctx.drawImage(img, 0, 0);
+
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const colors = extractColorsFromImage(imageData, 10);
+
+ onColorsExtracted(colors);
+ };
+ img.src = image;
+ }
+ }, [image, onColorsExtracted]);
+
+ const handleImageClick = (e: React.MouseEvent) => {
+ const canvas = canvasRef.current;
+ const ctx = canvas?.getContext("2d");
+ const img = imageRef.current;
+ if (!canvas || !ctx || !img) return;
+
+ const rect = img.getBoundingClientRect();
+ const scaleX = canvas.width / rect.width;
+ const scaleY = canvas.height / rect.height;
+ const x = (e.clientX - rect.left) * scaleX;
+ const y = (e.clientY - rect.top) * scaleY;
+
+ // Safety: ensure coordinates are within bounds
+ if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) return;
+
+ const pixel = ctx.getImageData(x, y, 1, 1).data;
+ const hex = rgbToHex(pixel[0], pixel[1], pixel[2]);
+ onColorSelect(hex);
+ };
+
+ return (
+ setShowMagnifier(true)}
+ onMouseLeave={() => setShowMagnifier(false)}
+ onMouseMove={(e) => {
+ const rect = e.currentTarget.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+ setCursorPosition({ x, y });
+ }}
+ >
+
+ {image ? (
+ <>
+
+
+ >
+ ) : (
+
+
+ No image uploaded
+
+ )}
+
+ );
+});
+
function CanvasMagnifier({ show, x, y, sourceCanvas, parentRef }: {
show: boolean;
x: number;
@@ -371,61 +400,49 @@ function CanvasMagnifier({ show, x, y, sourceCanvas, parentRef }: {
const imgElement = parentRef.current;
const rect = imgElement.getBoundingClientRect();
- // x, y passed are relative to the container element
- // we need to find x, y relative to the image element to map to canvas source coords
-
- // This relies on the container being the offsetParent or handling the calculation upstream.
- // In the parent component: x = e.clientX - rect.left (container rect).
-
- // For object-contain, the image might be smaller than container or offset.
- // Let's assume for simpler "Pick from Image" that the image is somewhat centered or fills?
- // But if there's black bars (bg-[#1a1a1a]), we need to know exactly where the image is.
- // BoundingClientRect of the IMAGE element gives the actual rendered image dimensions/pos.
-
+ // The logic below relies on correct containment hierarchy
const containerRect = imgElement.parentElement?.getBoundingClientRect();
if (!containerRect) return;
// Calculate cursor pos relative to the IMAGE ELEMENT
- // We know x,y are relative to CONTAINER.
- // We need cursor relative to IMAGE.
+ // x,y are relative to the Container (passed by props)
const cursorScreenX = containerRect.left + x;
const cursorScreenY = containerRect.top + y;
const cursorRelX = cursorScreenX - rect.left;
const cursorRelY = cursorScreenY - rect.top;
- // Verify if cursor is within the image (handles black bars)
if (cursorRelX < 0 || cursorRelY < 0 || cursorRelX > rect.width || cursorRelY > rect.height) {
- // Should we hide? or just show edge?
- // If we are strictly checking source colors, we should probably just clamp or hide.
return;
}
- // Map to source canvas coordinates
const scaleX = sourceCanvas.width / rect.width;
const scaleY = sourceCanvas.height / rect.height;
const sourceX = cursorRelX * scaleX;
const sourceY = cursorRelY * scaleY;
- // Draw the zoomed region
- // We want the cursor to be center of magnifier
const zoomWidth = SIZE / ZOOM_LEVEL;
const zoomHeight = SIZE / ZOOM_LEVEL;
- ctx.drawImage(
- sourceCanvas,
- sourceX - zoomWidth / 2,
- sourceY - zoomHeight / 2,
- zoomWidth,
- zoomHeight,
- 0,
- 0,
- SIZE,
- SIZE
- );
-
- // Optional: Crosshair
+ // Safely draw
+ try {
+ ctx.drawImage(
+ sourceCanvas,
+ sourceX - zoomWidth / 2,
+ sourceY - zoomHeight / 2,
+ zoomWidth,
+ zoomHeight,
+ 0,
+ 0,
+ SIZE,
+ SIZE
+ );
+ } catch (e) {
+ // Ignore potential index size errors if out of bounds briefly
+ }
+
+ // Crosshair
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.beginPath();
ctx.moveTo(SIZE / 2, 0); ctx.lineTo(SIZE / 2, SIZE);
@@ -445,7 +462,7 @@ function CanvasMagnifier({ show, x, y, sourceCanvas, parentRef }: {
style={{
left: x - SIZE / 2,
top: y - SIZE / 2,
- backgroundColor: 'black', // fallback
+ backgroundColor: 'black',
}}
/>
);
diff --git a/components/home/testimonials.tsx b/components/home/testimonials.tsx
new file mode 100644
index 0000000..53f86f7
--- /dev/null
+++ b/components/home/testimonials.tsx
@@ -0,0 +1,93 @@
+"use client";
+
+import { motion } from "framer-motion";
+import { Quote } from "lucide-react";
+import Image from "next/image";
+
+const TESTIMONIALS = [
+ {
+ content: "This tool completely transformed my design workflow. The color extraction is insanely accurate and the palettes are just beautiful.",
+ author: "Elen Jenkins",
+ role: "Product Designer @ Figma",
+ avatar: "/Avater/kari-rasmussen.jpg"
+ },
+ {
+ content: "I used to struggle with color theory, but ColorKit makes theming my apps effortless. The AI suggestions are better than what I could come up with.",
+ author: "David Chen",
+ role: "Frontend Developer",
+ avatar: "/Avater/jonathan-kelly.jpg"
+ },
+ {
+ content: "Simply the most aesthetic and functional color tool on the internet. It feels premium, works fast, and the export options are a lifesaver.",
+ author: "Elena Rodriguez",
+ role: "Digital Artist",
+ avatar: "/Avater/sally-mason.jpg"
+ }
+];
+
+export function Testimonials() {
+ return (
+
+ {/* Decorative Background Elements */}
+
+
+
+
+
+
+ Loved by Designers
+
+
+ Join thousands of creators who trust ColorKit for their daily creative needs.
+
+
+
+
+ {TESTIMONIALS.map((testimonial, i) => (
+
+
+
+
+ "{testimonial.content}"
+
+
+
+
+
+
+
+
{testimonial.author}
+ {testimonial.role}
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/next.config.js b/next.config.js
index b321514..8ca0b49 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,9 +1,5 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
- output: 'export',
- eslint: {
- ignoreDuringBuilds: true,
- },
images: { unoptimized: true },
};
diff --git a/public/Avater/jonathan-kelly.jpg b/public/Avater/jonathan-kelly.jpg
new file mode 100644
index 0000000..e39f366
Binary files /dev/null and b/public/Avater/jonathan-kelly.jpg differ
diff --git a/public/Avater/kari-rasmussen.jpg b/public/Avater/kari-rasmussen.jpg
new file mode 100644
index 0000000..7471fc6
Binary files /dev/null and b/public/Avater/kari-rasmussen.jpg differ
diff --git a/public/Avater/sally-mason.jpg b/public/Avater/sally-mason.jpg
new file mode 100644
index 0000000..b688bf3
Binary files /dev/null and b/public/Avater/sally-mason.jpg differ