Skip to content

Commit

Permalink
feat: 컬러 설정 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
kangju2000 committed Sep 24, 2024
1 parent 6585690 commit f1eb95d
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 65 deletions.
88 changes: 46 additions & 42 deletions src/content/components/Trigger.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AnimatePresence, motion } from 'framer-motion'
import { X } from 'lucide-react'
import { useState } from 'react'
import { useMemo, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'

import { SettingsContent } from './setting'
Expand All @@ -9,6 +9,46 @@ import { TaskContent } from '@/content/components/task'
import { useStorageStore } from '@/storage/useStorageStore'
import { cn } from '@/utils/cn'

const iconVariants = {
closed: {
opacity: 0,
scale: 0,
pathLength: 0,
transition: { duration: 0.2 },
},
open: {
opacity: 1,
scale: 1,
pathLength: 1,
transition: {
opacity: { delay: 0.2, duration: 0.2 },
scale: { delay: 0.2, duration: 0.2 },
pathLength: { delay: 0.2, duration: 0.3 },
},
},
}

const modalVariants = {
hidden: {
opacity: 0,
scale: 0.9,
y: 20,
transition: { duration: 0.2, ease: 'easeInOut' },
},
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: { duration: 0.3, ease: 'easeOut' },
},
exit: {
opacity: 0,
scale: 0.9,
y: 20,
transition: { duration: 0.2, ease: 'easeInOut' },
},
}

export function Trigger() {
const [isOpen, setIsOpen] = useState(false)
const [activeTab, setActiveTab] = useState<'tasks' | 'settings'>('tasks')
Expand All @@ -23,54 +63,18 @@ export function Trigger() {
return null
}

const iconVariants = {
closed: {
opacity: 0,
scale: 0,
pathLength: 0,
transition: { duration: 0.2 },
},
open: {
opacity: 1,
scale: 1,
pathLength: 1,
transition: {
opacity: { delay: 0.2, duration: 0.2 },
scale: { delay: 0.2, duration: 0.2 },
pathLength: { delay: 0.2, duration: 0.3 },
},
},
}

const modalVariants = {
hidden: {
opacity: 0,
scale: 0.9,
y: 20,
transition: { duration: 0.2, ease: 'easeInOut' },
},
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: { duration: 0.3, ease: 'easeOut' },
},
exit: {
opacity: 0,
scale: 0.9,
y: 20,
transition: { duration: 0.2, ease: 'easeInOut' },
},
}

return (
<>
<div
onClick={() => setIsOpen(prev => !prev)}
className={cn(
'd-mask d-mask-squircle fixed bottom-25px right-25px h-56px w-56px cursor-pointer bg-cover bg-center bg-no-repeat shadow-lg transition-all duration-300 ease-in-out hover:shadow-xl',
)}
style={{ backgroundImage: `url(${settings.triggerImage})` }}
style={
settings.trigger.type === 'color'
? { background: settings.trigger.color }
: { backgroundImage: `url(${settings.trigger.image})` }
}
>
<AnimatePresence>
{isOpen && (
Expand Down
117 changes: 117 additions & 0 deletions src/content/components/setting/ColorPickerModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { motion } from 'framer-motion'
import { useState, useCallback, useEffect } from 'react'

import { cn } from '@/utils/cn'

type ColorPickerModalProps = {
onComplete: (value: string) => void
onClose: () => void
}

const colorOptions = [
{ value: '#F44336', label: '빨강' },
{ value: '#E91E63', label: '분홍' },
{ value: '#9C27B0', label: '보라' },
{ value: '#673AB7', label: '진보라' },
{ value: '#3F51B5', label: '남색' },
{ value: '#2196F3', label: '파랑' },
{ value: '#03A9F4', label: '하늘색' },
{ value: '#00BCD4', label: '청록' },
{ value: '#009688', label: '틸' },
{ value: '#4CAF50', label: '초록' },
{ value: '#8BC34A', label: '연두' },
{ value: '#CDDC39', label: '라임' },
{ value: '#FFEB3B', label: '노랑' },
{ value: '#FFC107', label: '황색' },
{ value: '#FF9800', label: '주황' },
{ value: '#FF5722', label: '심홍' },
{ value: '#795548', label: '갈색' },
{ value: '#9E9E9E', label: '회색' },
{ value: '#607D8B', label: '청회색' },
{ value: 'linear-gradient(135deg, #FF6B6B, #4ECDC4)', label: '빨강-청록 그라데이션' },
{ value: 'linear-gradient(135deg, #A770EF, #CF8BF3, #FDB99B)', label: '보라-분홍-주황 그라데이션' },
{ value: 'linear-gradient(135deg, #667eea, #764ba2)', label: '파랑-보라 그라데이션' },
{ value: 'linear-gradient(135deg, #11998e, #38ef7d)', label: '청록-연두 그라데이션' },
{ value: 'linear-gradient(135deg, #FC466B, #3F5EFB)', label: '분홍-파랑 그라데이션' },
{ value: 'linear-gradient(135deg, #FDBB2D, #22C1C3)', label: '황색-청록 그라데이션' },
{ value: 'linear-gradient(135deg, #F761A1, #8C1BAB)', label: '분홍-보라 그라데이션' },
{ value: 'linear-gradient(135deg, #43CBFF, #9708CC)', label: '하늘-보라 그라데이션' },
{ value: 'linear-gradient(135deg, #5433FF, #20BDFF, #A5FECB)', label: '보라-파랑-연두 그라데이션' },
{ value: 'linear-gradient(135deg, #FFD26F, #3677FF)', label: '황색-파랑 그라데이션' },
]

export function ColorPickerModal({ onComplete, onClose }: ColorPickerModalProps) {
const [selectedValue, setSelectedValue] = useState('transparent')

const handleComplete = useCallback(() => {
onComplete(selectedValue)
}, [selectedValue, onComplete])

const handleColorSelect = (value: string) => {
setSelectedValue(value)
}

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose()
} else if (event.key === 'Enter') {
handleComplete()
}
}

window.addEventListener('keydown', handleKeyDown)
return () => {
window.removeEventListener('keydown', handleKeyDown)
}
}, [handleComplete, onClose])

return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"
>
<div className="flex h-400px w-300px flex-col rounded-12px bg-white p-12px">
<h3 className="mb-8px text-center text-16px font-bold">색상 선택</h3>
<div className="relative flex-1">
<div className="absolute inset-x-0 top-0 z-10 h-16px bg-gradient-to-b from-white to-transparent"></div>
<div className="absolute inset-x-0 bottom-0 z-10 h-16px bg-gradient-to-t from-white to-transparent"></div>
<div className="absolute inset-0 overflow-y-auto overflow-x-hidden">
<div className="flex justify-center pt-12px">
<div className="grid grid-cols-4 gap-3 p-2">
{colorOptions.map(color => (
<button
key={color.value}
className={`h-48px w-48px rounded-full ${
selectedValue === color.value ? 'ring-2 ring-blue-500' : ''
}`}
style={{ background: color.value }}
onClick={() => handleColorSelect(color.value)}
aria-label={color.label}
/>
))}
</div>
</div>
</div>
</div>
<div className="mt-8px flex items-center justify-between">
<button onClick={onClose} className="rounded bg-gray-200 px-12px py-6px transition-colors hover:bg-gray-300">
취소
</button>
<div
className={cn('d-mask d-mask-squircle relative h-48px w-48px overflow-hidden', {})}
style={{ background: selectedValue }}
/>
<button
onClick={handleComplete}
className="rounded bg-blue-500 px-12px py-6px text-white transition-colors hover:bg-blue-600"
>
완료
</button>
</div>
</div>
</motion.div>
)
}
10 changes: 8 additions & 2 deletions src/content/components/setting/ImageCropModal.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { motion } from 'framer-motion'
import React, { useState, useCallback, useEffect } from 'react'
import Cropper from 'react-easy-crop'

Expand Down Expand Up @@ -46,7 +47,12 @@ export function ImageCropModal({ image, onComplete, onClose }: ImageCropModalPro
}, [])

return (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"
>
<div className="flex h-400px w-300px flex-col gap-12px rounded-12px bg-white p-12px">
<div className="relative h-full w-full overflow-hidden rounded-12px">
<div className="absolute inset-0 bg-white" />
Expand Down Expand Up @@ -82,6 +88,6 @@ export function ImageCropModal({ image, onComplete, onClose }: ImageCropModalPro
</button>
</div>
</div>
</div>
</motion.div>
)
}
70 changes: 51 additions & 19 deletions src/content/components/setting/SettingsContent.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Camera } from 'lucide-react'
import { AnimatePresence } from 'framer-motion'
import { Camera, Palette } from 'lucide-react'
import { useCallback, useRef, useState } from 'react'
import { useDropzone } from 'react-dropzone'

import { ColorPickerModal } from './ColorPickerModal'
import { ImageCropModal } from './ImageCropModal'
import { SettingItem } from './SettingItem'
import packageJson from '../../../../package.json'
Expand All @@ -23,6 +25,7 @@ export function SettingsContent() {
const { settings, updateSettings } = useStorageStore()
const [image, setImage] = useState<string | null>(null)
const [isCropModalOpen, setIsCropModalOpen] = useState(false)
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false)

const inputRef = useRef<HTMLInputElement>(null)

Expand All @@ -43,7 +46,7 @@ export function SettingsContent() {

const handleCropComplete = useCallback(
async (croppedImage: string) => {
updateSettings({ triggerImage: croppedImage })
updateSettings({ trigger: { type: 'image', image: croppedImage } })
setIsCropModalOpen(false)
},
[updateSettings],
Expand All @@ -54,6 +57,15 @@ export function SettingsContent() {
updateSettings({ refreshInterval: newRefreshInterval })
}

const renderTriggerPreview = () => {
switch (settings.trigger.type) {
case 'image':
return <img src={settings.trigger.image} alt="버튼 이미지" className="h-full w-full object-cover" />
case 'color':
return <div className="h-full w-full" style={{ background: settings.trigger.color }} />
}
}

return (
<div className="relative flex flex-1 flex-col overflow-y-auto bg-gray-50">
<div className="mb-12px mt-4px bg-white bg-opacity-50 px-16px py-12px">
Expand All @@ -71,20 +83,27 @@ export function SettingsContent() {
)}
>
<div className="relative h-120px w-120px overflow-hidden rounded-full">
<img src={settings.triggerImage} alt="버튼 이미지" className="h-full w-full object-cover" />
{renderTriggerPreview()}
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-25">
<div
className="flex h-36px w-36px cursor-pointer items-center justify-center rounded-full bg-white bg-opacity-75 transition-all duration-200 hover:bg-opacity-100"
onClick={() => inputRef.current?.click()}
>
<Camera size={20} className="text-gray-700" />
<div className="flex gap-2">
<button
className="flex h-36px w-36px cursor-pointer items-center justify-center rounded-full bg-white bg-opacity-75 transition-all duration-200 hover:bg-opacity-100"
onClick={() => inputRef.current?.click()}
>
<Camera size={20} className="text-gray-700" />
</button>
<button
className="flex h-36px w-36px cursor-pointer items-center justify-center rounded-full bg-white bg-opacity-75 transition-all duration-200 hover:bg-opacity-100"
onClick={() => setIsColorPickerOpen(true)}
>
<Palette size={20} className="text-gray-700" />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<p className="mt-8px text-center text-12px text-gray-400">클릭하거나 이미지를 끌어다 놓아 변경하세요</p>
</div>

<div className="rounded-lg bg-white p-16px shadow-sm">
Expand All @@ -106,16 +125,29 @@ export function SettingsContent() {

<input {...getInputProps()} ref={inputRef} className="hidden" />

{isCropModalOpen && image && (
<ImageCropModal
image={image}
onComplete={handleCropComplete}
onClose={() => {
setIsCropModalOpen(false)
setImage(null)
}}
/>
)}
<AnimatePresence>
{isCropModalOpen && image && (
<ImageCropModal
image={image}
onComplete={handleCropComplete}
onClose={() => {
setIsCropModalOpen(false)
setImage(null)
}}
/>
)}

{isColorPickerOpen && (
<ColorPickerModal
onComplete={value => {
updateSettings({ trigger: { type: 'color', color: value } })
setIsColorPickerOpen(false)
}}
onClose={() => setIsColorPickerOpen(false)}
/>
)}
</AnimatePresence>

<div className="absolute bottom-8px left-16px">
<span className="text-12px text-gray-500">버전 {version}</span>
</div>
Expand Down
5 changes: 4 additions & 1 deletion src/storage/useStorageStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ const initialStorageData: StorageData = {
filterOptions: { status: 'ongoing', courseId: '-1' },
settings: {
refreshInterval: 1000 * 60 * 20, // 20 minutes
triggerImage: chrome.runtime.getURL('/assets/Lee-Gil-ya.webp'),
trigger: {
type: 'image',
image: chrome.runtime.getURL('/assets/Lee-Gil-ya.webp'),
},
},
}

Expand Down
2 changes: 1 addition & 1 deletion src/types/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type StorageData = {
filterOptions: FilterOptions
settings: {
refreshInterval: number
triggerImage: string
trigger: { type: 'image'; image: string } | { type: 'color'; color: string }
}
}

Expand Down

0 comments on commit f1eb95d

Please sign in to comment.