diff --git a/package.json b/package.json index cdc6a35..fc68f6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-thumbnail-generator", - "version": "2.3.1", + "version": "2.4.0", "description": "react-thumbnail-generator", "main": "dist/index.js", "module": "dist/index.js", diff --git a/src/components/Accordion/index.tsx b/src/components/Accordion/index.tsx new file mode 100644 index 0000000..b29a63f --- /dev/null +++ b/src/components/Accordion/index.tsx @@ -0,0 +1,37 @@ +import React, { useState } from 'react'; +import * as S from './styled'; +import { arrowBottom, arrowTop } from '../../assets/icons'; +import TGIcon from '../../components/TGIcon'; + +interface AccordionProps { + title: string; + children: React.ReactNode; +} + +const Accordion = ({ title, children }: AccordionProps) => { + const [isOpenPanel, setIsOpenPanel] = useState(false); + + const handleToggle = () => { + setIsOpenPanel(!isOpenPanel); + }; + + return ( + + +

{title}

+ + {isOpenPanel ? ( + + ) : ( + + )} + +
+ + {children} + +
+ ); +}; + +export default Accordion; diff --git a/src/components/Accordion/styled.tsx b/src/components/Accordion/styled.tsx new file mode 100644 index 0000000..5b90e5a --- /dev/null +++ b/src/components/Accordion/styled.tsx @@ -0,0 +1,49 @@ +import styled from 'styled-components'; + +export const AccordionWrapper = styled.div` + margin: 0 auto; + width: 100%; +`; + +export const AccordionTopContainer = styled.div` + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: 0.3s; + padding: 0 20px; + height: 39px; + + & > p { + cursor: pointer; + background-color: transparent; + outline: none; + border: none; + text-align: start; + font-size: 1rem; + color: #111; + margin: 0; + } + + &:hover { + & > p { + color: #3264b5; + } + } +`; + +export const AccordionPanelContainer = styled.div` + background-color: #fff; + overflow: hidden; + transition: max-height 0.3s linear; + max-height: 0; + + & > p { + padding: 20px 40px; + font-size: 1.15rem; + } + + &.active { + max-height: 200px; + } +`; diff --git a/src/components/TGCanvas.tsx b/src/components/Canvas/index.tsx similarity index 84% rename from src/components/TGCanvas.tsx rename to src/components/Canvas/index.tsx index e132ece..41193c5 100644 --- a/src/components/TGCanvas.tsx +++ b/src/components/Canvas/index.tsx @@ -1,20 +1,17 @@ import React, { useEffect } from 'react'; import { Color } from 'react-color-palette'; -import { CanvasState } from '../types/canvas'; -import { TGCanvasWrapper } from './TG.styled'; +import { CanvasState } from '../../types/canvas'; +import * as S from './styled'; -interface TGCanvasProps { +interface CanvasProps { canvasState: CanvasState; bgColor: Color; fontColor: Color; strokeColor: Color; } -const TGCanvas = React.forwardRef( - ( - { canvasState, bgColor, fontColor, strokeColor }: TGCanvasProps, - ref: any - ) => { +const Canvas = React.forwardRef( + ({ canvasState, bgColor, fontColor, strokeColor }: CanvasProps, ref: any) => { const { value, canvasWidth, @@ -84,7 +81,7 @@ const TGCanvas = React.forwardRef( if (ctx) { ctx.save(); - if (isBlur) ctx.filter = 'blur(4px)'; // (*) + if (isBlur) ctx.filter = 'blur(5px)'; // (*) if (selectedImage) { ctx.drawImage(selectedImage, 0, 0); } else { @@ -98,13 +95,13 @@ const TGCanvas = React.forwardRef( }, [bgColor, fontColor, strokeColor, canvasState]); return ( - + - + ); } ); -TGCanvas.displayName = 'Search'; +Canvas.displayName = 'Search'; -export default TGCanvas; +export default Canvas; diff --git a/src/components/Canvas/styled.tsx b/src/components/Canvas/styled.tsx new file mode 100644 index 0000000..5409dbf --- /dev/null +++ b/src/components/Canvas/styled.tsx @@ -0,0 +1,7 @@ +import styled from 'styled-components'; + +export const CanvasWrapper = styled.div` + display: flex; + justify-content: center; + width: 100%; +`; diff --git a/src/components/TGColorPicker.tsx b/src/components/ColorPicker/index.tsx similarity index 72% rename from src/components/TGColorPicker.tsx rename to src/components/ColorPicker/index.tsx index 7735428..143275f 100644 --- a/src/components/TGColorPicker.tsx +++ b/src/components/ColorPicker/index.tsx @@ -1,13 +1,18 @@ import React, { useEffect, useRef, useState } from 'react'; -import { TGColorPickerWrapper, TGIConButton } from './TG.styled'; -import { Color, ColorPicker } from 'react-color-palette'; +import { ColorPickerWrapper } from './styled'; +import { Color, ColorPicker as PaletteColorPicker } from 'react-color-palette'; +import { IconButton } from '../Icon/styled'; -interface TGColorPickerProps { +interface ColorPickerPickerProps { children: React.ReactNode; color: Color; setColor: (color: Color) => void; } -const TGColorPicker = ({ children, color, setColor }: TGColorPickerProps) => { +const ColorPickerPicker = ({ + children, + color, + setColor, +}: ColorPickerPickerProps) => { const [isOpenColorPicker, setIsOpenColorPicker] = useState(false); const colorRef = useRef(null); @@ -34,16 +39,16 @@ const TGColorPicker = ({ children, color, setColor }: TGColorPickerProps) => { }, [handleCloseColorPicker]); return ( - - + {children} - + {isOpenColorPicker && (
- { />
)} -
+ ); }; -export default TGColorPicker; +export default ColorPickerPicker; diff --git a/src/components/ColorPicker/styled.tsx b/src/components/ColorPicker/styled.tsx new file mode 100644 index 0000000..5594d54 --- /dev/null +++ b/src/components/ColorPicker/styled.tsx @@ -0,0 +1,143 @@ +import styled from 'styled-components'; + +export const ColorPickerWrapper = styled.div` + position: relative; + + & > div { + position: absolute; + left: 50%; + bottom: 40px; + transform: translateX(-50%); + } + + .rcp-light { + --rcp-background: #ffffff; + --rcp-input-text: #111111; + --rcp-input-border: rgba(0, 0, 0, 0.1); + --rcp-input-label: #717171; + } + .rcp-dark { + --rcp-background: #181818; + --rcp-input-text: #f3f3f3; + --rcp-input-border: rgba(255, 255, 255, 0.1); + --rcp-input-label: #999999; + } + .rcp { + display: flex; + flex-direction: column; + align-items: center; + background-color: var(--rcp-background); + border-radius: 10px; + } + .rcp-body { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; + width: 100%; + box-sizing: border-box; + padding: 20px; + } + .rcp-saturation { + position: relative; + width: 100%; + background-image: linear-gradient(transparent, black), + linear-gradient(to right, white, transparent); + border-radius: 10px 10px 0 0; + user-select: none; + } + .rcp-saturation-cursor { + position: absolute; + width: 20px; + height: 20px; + border: 2px solid #ffffff; + border-radius: 50%; + box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.15); + box-sizing: border-box; + transform: translate(-10px, -10px); + } + .rcp-hue { + position: relative; + width: 100%; + height: 12px; + background-image: linear-gradient( + to right, + rgb(255, 0, 0), + rgb(255, 255, 0), + rgb(0, 255, 0), + rgb(0, 255, 255), + rgb(0, 0, 255), + rgb(255, 0, 255), + rgb(255, 0, 0) + ); + border-radius: 10px; + user-select: none; + } + .rcp-hue-cursor { + position: absolute; + width: 20px; + height: 20px; + border: 2px solid #ffffff; + border-radius: 50%; + box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 0px 0.5px; + box-sizing: border-box; + transform: translate(-10px, -4px); + } + .rcp-alpha { + position: relative; + width: 100%; + height: 12px; + border-radius: 10px; + user-select: none; + } + .rcp-alpha-cursor { + position: absolute; + width: 20px; + height: 20px; + border: 2px solid #ffffff; + border-radius: 50%; + box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 0px 0.5px; + box-sizing: border-box; + transform: translate(-10px, -4px); + } + .rcp-fields { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 10px; + width: 100%; + } + .rcp-fields-element { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + width: 100%; + } + .hex-element { + grid-row: 1; + } + .hex-element:nth-child(3n) { + grid-column: 1 / -1; + } + .rcp-fields-element-input { + width: 100%; + font-size: 14px; + font-weight: 600; + color: var(--rcp-input-text); + text-align: center; + background: none; + border: 2px solid; + border-color: var(--rcp-input-border); + border-radius: 5px; + box-sizing: border-box; + outline: none; + padding: 10px; + } + .rcp-fields-element-label { + font-size: 14px; + font-weight: 600; + color: var(--rcp-input-label); + text-transform: uppercase; + } +`; diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx new file mode 100644 index 0000000..1859a06 --- /dev/null +++ b/src/components/Icon/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +interface IconProps { + src: string; + width: number; + height: number; +} + +const Icon = ({ src, width, height }: IconProps) => { + return ; +}; + +export default Icon; diff --git a/src/components/Icon/styled.tsx b/src/components/Icon/styled.tsx new file mode 100644 index 0000000..1d9da53 --- /dev/null +++ b/src/components/Icon/styled.tsx @@ -0,0 +1,26 @@ +import styled from 'styled-components'; + +export const IconButton = styled.button<{ + isOpenColorPicker?: boolean; + isBorder?: boolean; +}>` + padding: 4px 5px; + background: #fff; + border-radius: 5px; + display: flex; + align-items: center; + cursor: pointer; + + ${({ isBorder, isOpenColorPicker }) => { + if (!isBorder) return `border: none;`; + return ` + border: ${ + isOpenColorPicker ? '1px solid #0e1b30;' : '1px solid #cccccc;' + }; + `; + }} + + &:hover { + border: ${({ isBorder }) => (isBorder ? `1px solid #0e1b30` : 'none')}; + } +`; diff --git a/src/components/TG.styled.ts b/src/components/TG.styled.ts index 2f2978a..3a8aca6 100644 --- a/src/components/TG.styled.ts +++ b/src/components/TG.styled.ts @@ -27,7 +27,7 @@ export const TGBodyWrapper = styled.section<{ position: fixed; display: flex; justify-content: center; - min-width: 700px; + min-width: 600px; border-radius: 7px; box-shadow: 1px 1px 10px #cccccc; z-index: 9999; @@ -38,12 +38,6 @@ export const TGBodyWrapper = styled.section<{ ${({ modalPosition }) => getModalPosition(modalPosition)} `; -export const TGCanvasWrapper = styled.div` - display: flex; - justify-content: center; - width: 100%; -`; - export const TGLimitSizeText = styled.div` font-size: 0.85rem; margin-top: 5px; @@ -78,11 +72,16 @@ export const TGHeaderWrapper = styled.div` } a { - color: #111; + color: #111111; padding: 0; margin: 0; font-size: 0.875rem; font-weight: bold; + text-decoration: none; + + &:hover { + color: #3264b5; + } } button { @@ -142,148 +141,6 @@ export const TGButtonWrapper = styled.div` } `; -export const TGColorPickerWrapper = styled.div` - position: relative; - - & > div { - position: absolute; - left: 50%; - bottom: 40px; - transform: translateX(-50%); - } - - .rcp-light { - --rcp-background: #ffffff; - --rcp-input-text: #111111; - --rcp-input-border: rgba(0, 0, 0, 0.1); - --rcp-input-label: #717171; - } - .rcp-dark { - --rcp-background: #181818; - --rcp-input-text: #f3f3f3; - --rcp-input-border: rgba(255, 255, 255, 0.1); - --rcp-input-label: #999999; - } - .rcp { - display: flex; - flex-direction: column; - align-items: center; - background-color: var(--rcp-background); - border-radius: 10px; - } - .rcp-body { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 20px; - width: 100%; - box-sizing: border-box; - padding: 20px; - } - .rcp-saturation { - position: relative; - width: 100%; - background-image: linear-gradient(transparent, black), - linear-gradient(to right, white, transparent); - border-radius: 10px 10px 0 0; - user-select: none; - } - .rcp-saturation-cursor { - position: absolute; - width: 20px; - height: 20px; - border: 2px solid #ffffff; - border-radius: 50%; - box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.15); - box-sizing: border-box; - transform: translate(-10px, -10px); - } - .rcp-hue { - position: relative; - width: 100%; - height: 12px; - background-image: linear-gradient( - to right, - rgb(255, 0, 0), - rgb(255, 255, 0), - rgb(0, 255, 0), - rgb(0, 255, 255), - rgb(0, 0, 255), - rgb(255, 0, 255), - rgb(255, 0, 0) - ); - border-radius: 10px; - user-select: none; - } - .rcp-hue-cursor { - position: absolute; - width: 20px; - height: 20px; - border: 2px solid #ffffff; - border-radius: 50%; - box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 0px 0.5px; - box-sizing: border-box; - transform: translate(-10px, -4px); - } - .rcp-alpha { - position: relative; - width: 100%; - height: 12px; - border-radius: 10px; - user-select: none; - } - .rcp-alpha-cursor { - position: absolute; - width: 20px; - height: 20px; - border: 2px solid #ffffff; - border-radius: 50%; - box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 0px 0.5px; - box-sizing: border-box; - transform: translate(-10px, -4px); - } - .rcp-fields { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 10px; - width: 100%; - } - .rcp-fields-element { - display: flex; - flex-direction: column; - align-items: center; - gap: 5px; - width: 100%; - } - .hex-element { - grid-row: 1; - } - .hex-element:nth-child(3n) { - grid-column: 1 / -1; - } - .rcp-fields-element-input { - width: 100%; - font-size: 14px; - font-weight: 600; - color: var(--rcp-input-text); - text-align: center; - background: none; - border: 2px solid; - border-color: var(--rcp-input-border); - border-radius: 5px; - box-sizing: border-box; - outline: none; - padding: 10px; - } - .rcp-fields-element-label { - font-size: 14px; - font-weight: 600; - color: var(--rcp-input-label); - text-transform: uppercase; - } -`; - export const TGControllerWrapper = styled.div` display: flex; justify-content: center; @@ -302,35 +159,11 @@ export const TGControllerWrapper = styled.div` } `; -export const TGIConButton = styled.button<{ - isOpenColorPicker?: boolean; - isBorder?: boolean; -}>` - padding: 4px 5px; - background: #fff; - border-radius: 5px; - display: flex; - align-items: center; - cursor: pointer; - - ${({ isBorder, isOpenColorPicker }) => { - if (!isBorder) return `border: none;`; - return ` - border: ${ - isOpenColorPicker ? '1px solid #0e1b30;' : '1px solid #cccccc;' - }; - `; - }} - - &:hover { - border: ${({ isBorder }) => (isBorder ? `1px solid #0e1b30` : 'none')}; - } -`; - // TG Select export const SelectWrapper = styled.div` position: relative; min-width: 150px; + max-width: 150px; label { font-size: 0.7rem; diff --git a/src/components/TG.tsx b/src/components/TG.tsx index d72c708..b3589c4 100644 --- a/src/components/TG.tsx +++ b/src/components/TG.tsx @@ -1,11 +1,9 @@ import React, { ChangeEvent, useMemo, useRef, useState } from 'react'; -import TGCanvas from './TGCanvas'; import TGHeader from './TGHeader'; -import TGColorPicker from './TGColorPicker'; import TGSelect from './TGSelect'; import TGSelectItem from './TGSelectItem'; import TGInputText from './TGInputText'; -import TGIcon from './TGIcon'; +import Icon from './Icon'; import TGInputFile from './TGInputFile'; import Divider from './Divider'; import { @@ -19,6 +17,10 @@ import { fill, font, stroke, blur } from '../assets/icons'; import { Color, useColor } from 'react-color-palette'; import * as S from './TG.styled'; import { downloadCanvas, getValidMessage } from '../utils/common'; +import Accordion from './Accordion'; +import Canvas from './Canvas'; +import ColorPicker from './ColorPicker'; +import { IconButton } from './Icon/styled'; interface TGProps { additionalFontFamily?: string[]; @@ -37,7 +39,7 @@ const TG = ({ fontSize: '30px', fontStrokeType: 'None', fontFamily: 'Arial', - canvasWidth: '700', + canvasWidth: '600', canvasHeight: '400', imageType: 'png', xAxis: '0', @@ -148,7 +150,7 @@ const TG = ({ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - {fontFamilyOptions.map((item) => ( - - {item} - - ))} - - - {fontSizes.map((item) => ( - - {item} - - ))} - - - {strokeTypes.map((item) => ( - - {item} - - ))} - - - - + + + + + + + + + + + + {fontFamilyOptions.map((item) => ( + + {item} + + ))} + + + {fontSizes.map((item) => ( + + {item} + + ))} + + + {strokeTypes.map((item) => ( + + {item} + + ))} + + + + + + + + + + + + + + {imageTypes.map((item) => ( diff --git a/src/components/TGHeader.tsx b/src/components/TGHeader.tsx index d02d650..36c8de3 100644 --- a/src/components/TGHeader.tsx +++ b/src/components/TGHeader.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { TGHeaderWrapper, TGIConButton, TGLimitSizeText } from './TG.styled'; +import { TGHeaderWrapper, TGLimitSizeText } from './TG.styled'; import TGIcon from './TGIcon'; import { close } from '../assets/icons'; +import { IconButton } from './Icon/styled'; interface TGHeaderProps { onToggle: () => void; @@ -18,9 +19,9 @@ const TGHeader = ({ onToggle }: TGHeaderProps) => { rel="noreferrer"> Simple Thumbnail Generator - + - + Limit Width: {`${LimitWidthSize}px`} diff --git a/src/components/TGInputText.tsx b/src/components/TGInputText.tsx index 16711a2..ce85a02 100644 --- a/src/components/TGInputText.tsx +++ b/src/components/TGInputText.tsx @@ -9,7 +9,7 @@ const TGInputText = ({ name, label, value, - width = 100, + width = 124, maxLength = 5, disabled = false, onChange,