Skip to content

Commit

Permalink
Merge pull request stakwork#2298 from MahtabBukhari/color_picker_moda…
Browse files Browse the repository at this point in the history
…l_part_2

 Color picker popover part 2  &&  [Icons] Add icon column and icon to post request
  • Loading branch information
Rassl authored Oct 7, 2024
2 parents e95e173 + d9c9e30 commit 1c8421b
Show file tree
Hide file tree
Showing 12 changed files with 644 additions and 24 deletions.
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

0 comments on commit 1c8421b

Please sign in to comment.