Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Color picker popover part 2 && [Icons] Add icon column and icon to post request #2298

Merged
merged 14 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"r3f-perf": "6.7.0",
"react": "^18.2.0",
"react-audio-player": "^0.17.0",
"react-color": "^2.19.3",
"react-dom": "^18.2.0",
"react-dropdown-select": "^4.9.3",
"react-hook-form": "^7.39.5",
Expand Down Expand Up @@ -185,6 +186,7 @@
"@types/lodash": "^4.14.182",
"@types/node": "^16.7.13",
"@types/react": "^18.2.15",
"@types/react-color": "^3.0.12",
"@types/react-dom": "^18.2.7",
"@types/react-input-mask": "3.0.2",
"@types/react-lottie": "^1.2.10",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
export const hexToHsl = (hex: string) => {
let r = 0
let g = 0
let b = 0

if (hex.length === 4) {
r = parseInt(hex[1] + hex[1], 16)
g = parseInt(hex[2] + hex[2], 16)
b = parseInt(hex[3] + hex[3], 16)
} else if (hex.length === 7) {
r = parseInt(hex[1] + hex[2], 16)
g = parseInt(hex[3] + hex[4], 16)
b = parseInt(hex[5] + hex[6], 16)
}

r /= 255
g /= 255
b /= 255

const max = Math.max(r, g, b)
const min = Math.min(r, g, b)

let h = 0
let s = 0
const l = (max + min) / 2

if (max !== min) {
const d = max - min

s = l > 0.5 ? d / (2 - max - min) : d / (max + min)

// eslint-disable-next-line default-case
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0)
break
case g:
h = (b - r) / d + 2
break
case b:
h = (r - g) / d + 4
break
}

h /= 6
}

return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100),
}
}

export const hslToHex = (h: number, s: number, l: number) => {
const saturation = s / 100

const lightness = l / 100

const k = (n: number) => (n + h / 30) % 12
const a = saturation * Math.min(lightness, 1 - lightness)

const f = (n: number) => Math.round(255 * (lightness - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1))))

const r = f(0)
const g = f(8)
const b = f(4)

// Construct the hex value without bitwise operations
const hex = `#${[r, g, b]
.map((val) => val.toString(16).padStart(2, '0'))
.join('')
.toUpperCase()}`

return hex
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React, { useRef, useEffect, useState, useCallback } from 'react'
import styled from 'styled-components'
import { hslToHex } from './ColorUtils'

interface SaturationPickerProps {
hue: number
onChange: (color: string) => void
}

const Container = styled.div`
position: relative;
`

const Pointer = styled.div<{ x: number; y: number }>`
position: absolute;
top: ${(props) => props.y - 7}px;
left: ${(props) => props.x - 7}px;
width: 8px;
height: 8px;
border-radius: 50%;
border: 2px solid white;
background-color: transparent;
box-shadow: inset 0 0 0 3px rgba(0, 0, 0, 0.3);
pointer-events: none;
transition: top 0.1s ease, left 0.1s ease;
`

const SaturationPicker: React.FC<SaturationPickerProps> = ({ hue, onChange }) => {
const canvasRef = useRef<HTMLCanvasElement | null>(null)
const [pointerPos, setPointerPos] = useState<{ x: number; y: number }>({ x: 80, y: 50 })
const [isDragging, setIsDragging] = useState(false)

useEffect(() => {
const canvas = canvasRef.current

if (canvas) {
const ctx = canvas.getContext('2d')

if (ctx) {
const { width, height } = canvas

// eslint-disable-next-line no-plusplus
for (let x = 0; x < width; x++) {
// eslint-disable-next-line no-plusplus
for (let y = 0; y < height; y++) {
const saturation = x / width
const brightness = 1 - y / height
const color = `hsl(${hue}, ${saturation * 100}%, ${brightness * 100}%)`

ctx.fillStyle = color
ctx.fillRect(x, y, 1, 1)
}
}
}
}
}, [hue])

const handleCanvasInteraction = useCallback(
(e: React.MouseEvent | MouseEvent) => {
const canvas = canvasRef.current

if (canvas) {
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top

const saturation = x / canvas.width
const brightness = 1 - y / canvas.height

const hexColor = hslToHex(hue, saturation * 100, brightness * 100)

setPointerPos({ x, y })
onChange(hexColor)
}
},
[hue, onChange],
)

const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
setIsDragging(true)
handleCanvasInteraction(e)
}

// eslint-disable-next-line react-hooks/exhaustive-deps
const handleMouseMove = (e: MouseEvent) => {
if (isDragging) {
handleCanvasInteraction(e)
}
}

const handleMouseUp = () => {
setIsDragging(false)
}

useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', handleMouseUp)
} else {
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
}

return () => {
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
}
}, [isDragging, handleMouseMove])

return (
<Container>
<canvas ref={canvasRef} height={162} onMouseDown={handleMouseDown} width={260} />
<Pointer x={pointerPos.x} y={pointerPos.y} />
</Container>
)
}

export default SaturationPicker
Original file line number Diff line number Diff line change
@@ -1,16 +1,94 @@
/* eslint-disable no-nested-ternary */
import styled from 'styled-components'
import { Flex } from '~/components/common/Flex'

import { HuePicker } from 'react-color'
import { colors } from '~/utils/colors'
import { useState } from 'react'
import SaturationPicker from './SaturationPicker'
import { hslToHex, hexToHsl } from './ColorUtils'
import { useAppStore } from '~/stores/useAppStore'
import { circleColors } from '../../Constants'

export const ColorPicker = () => {
const { selectedColor, setSelectedColor } = useAppStore((s) => s)
const [hexValue, setHexValue] = useState(selectedColor)
const [hueValue, setHueValue] = useState(315)
const [saturation, setSaturation] = useState(74)
const [brightness, setBrightness] = useState(59)

const handleColorChange = (hexColor: string) => {
setSelectedColor(hexColor)
setHexValue(hexColor)

const { h, s, l } = hexToHsl(hexColor)

setHueValue(h)
setSaturation(s)
setBrightness(l)
}

const handleHueChange = (color: { hsl: { h: number }; hex: string }) => {
setHueValue(color.hsl.h)

const newHex = hslToHex(color.hsl.h, saturation, brightness)

handleColorChange(newHex)
}

const handleSaturationChange = (hexColor: string) => {
setSelectedColor(hexColor)
setHexValue(hexColor)

const { h, s, l } = hexToHsl(hexColor)

setHueValue(h)
setSaturation(s)
setBrightness(l)
}

const handleHexInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newHex = e.target.value

setHexValue(newHex)

const { h, s, l } = hexToHsl(newHex)

setHueValue(h)
setSaturation(s)
setBrightness(l)
setSelectedColor(newHex)
}

return (
<Wrapper direction="column" justify="flex-end">
<TableWrapper align="center">
<PickerContainer>
<ColorPaletteWrapper>
<ColorPalette>
{circleColors.map((circleColor) => (
<ColorCircle key={circleColor} color={circleColor} onClick={() => handleColorChange(circleColor)} />
))}
</ColorPalette>
</ColorPaletteWrapper>

<SaturationPickerWrapper>
<SaturationPicker hue={hueValue} onChange={handleSaturationChange} />
</SaturationPickerWrapper>

<HuePickerWrapper>
<HuePicker color={selectedColor} onChange={handleHueChange} />
</HuePickerWrapper>

<HexaInputWrapper>
<LabelText>HEX</LabelText>

export const ColorPicker = () => (
<Wrapper direction="column" justify="flex-end">
<TableWrapper align="center" justify="center">
<p>this is color wrapper</p>
</TableWrapper>
</Wrapper>
)
<StyledInput onChange={handleHexInputChange} type="text" value={hexValue} />
</HexaInputWrapper>
</PickerContainer>
</TableWrapper>
</Wrapper>
)
}

const Wrapper = styled(Flex)`
flex: 1;
Expand Down Expand Up @@ -50,3 +128,81 @@ const TableWrapper = styled(Flex)`
flex: 1;
width: 100%;
`

const PickerContainer = styled.div`
padding: 0 20px;
width: 315px;
`

const ColorPalette = styled.div`
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
margin-bottom: 16px;
`

const ColorPaletteWrapper = styled.div`
margin-left: 10px;
margin-bottom: 6px;
`

const ColorCircle = styled.div<{ color: string }>`
width: 20px;
height: 20px;
border-radius: 50%;
margin: 4px;
background-color: ${(props) => props.color};
cursor: pointer;

&:hover {
box-shadow: 0 0 7px ${(props) => props.color};
}
`

const SaturationPickerWrapper = styled.div`
margin-bottom: 8px;
margin-left: 4px;
display: flex;
justify-content: center;
align-items: center;
`

const HuePickerWrapper = styled.div`
margin-bottom: 15px;
width: 224px;
margin-left: 10px;
display: flex;
justify-content: center;
align-items: center;
`

const StyledInput = styled.input`
font-family: 'Barlow';
padding: 2px 5px 2px 10px;
font-size: 13px;
background-color: ${colors.black};
color: ${colors.primaryText1};
border-radius: 6px;
width: 84px;
height: 28px;
border: none;

&:focus {
outline: none;
}
`

const LabelText = styled.p`
font-family: 'Barlow';
font-size: 13px;
font-weight: 400;
color: ${colors.primaryText1};
letter-spacing: 1px;
margin: 0 10px;
`

const HexaInputWrapper = styled.div`
display: flex;
align-items: center;
gap: 8px;
`
Loading
Loading