diff --git a/package-lock.json b/package-lock.json index 69d7094..824ecdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,14 @@ "name": "ddang", "version": "0.0.0", "dependencies": { + "@types/react-textarea-autosize": "^8.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-helmet-async": "^2.0.5", "react-icons": "^5.3.0", "react-query": "^3.39.3", "react-router-dom": "^6.28.0", + "react-textarea-autosize": "^8.5.5", "styled-components": "^6.1.13", "styled-reset": "^4.5.2", "zustand": "^5.0.1" @@ -3776,6 +3778,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-textarea-autosize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/react-textarea-autosize/-/react-textarea-autosize-8.0.0.tgz", + "integrity": "sha512-KVqk+/+RMQB3ZDpk7ZTpYHauU3Ue+Y0f09POvGaEpaGb+izzbpoM47tkDGlbF37iT7JYZ8QFwLzqiOPYbQaztA==", + "deprecated": "This is a stub types definition. react-textarea-autosize provides its own type definitions, so you do not need this installed.", + "dependencies": { + "react-textarea-autosize": "*" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -8371,6 +8382,22 @@ "react-dom": ">=16.8" } }, + "node_modules/react-textarea-autosize": { + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.5.tgz", + "integrity": "sha512-CVA94zmfp8m4bSHtWwmANaBR8EPsKy2aZ7KwqhoS4Ftib87F9Kvi7XQhOixypPLMc6kVYgOXvKFuuzZDpHGRPg==", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -9982,6 +10009,43 @@ "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", "dev": true }, + "node_modules/use-composed-ref": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz", + "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz", + "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 1953079..a5143f3 100644 --- a/package.json +++ b/package.json @@ -13,19 +13,21 @@ "format:fix": "prettier --write --ignore-path .gitignore \"**/*.{ts,tsx}\"" }, "dependencies": { + "@types/react-textarea-autosize": "^8.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-helmet-async": "^2.0.5", "react-icons": "^5.3.0", "react-query": "^3.39.3", "react-router-dom": "^6.28.0", + "react-textarea-autosize": "^8.5.5", "styled-components": "^6.1.13", "styled-reset": "^4.5.2", "zustand": "^5.0.1" }, "devDependencies": { - "@types/node": "^22.9.0", "@eslint/js": "^9.13.0", + "@types/node": "^22.9.0", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", "@types/styled-components": "^5.1.34", @@ -44,12 +46,12 @@ "typescript": "^5.2.2", "typescript-eslint": "^8.11.0", "vite": "^5.2.10", - "vite-plugin-compression2": "^1.3.1", "vite-plugin-compression": "^0.5.1", + "vite-plugin-compression2": "^1.3.1", "vite-plugin-dts": "^4.3.0", "vite-plugin-mkcert": "^1.17.6", - "vite-plugin-svgr": "^4.3.0", "vite-plugin-pwa": "^0.20.5", + "vite-plugin-svgr": "^4.3.0", "vite-tsconfig-paths": "^5.1.2", "workbox-core": "^7.1.0", "workbox-precaching": "^7.1.0", diff --git a/src/components/Button/ActionButton.ts b/src/components/Button/ActionButton.ts new file mode 100644 index 0000000..7272161 --- /dev/null +++ b/src/components/Button/ActionButton.ts @@ -0,0 +1,52 @@ +import styled, { BrandColors, GrayscaleColors } from 'styled-components' + +type BgColorType = + | Extract + | Extract + +type ActionButtonProps = { + $bgColor?: BgColorType + $type?: 'roundedRect' | 'semiRoundedRect' | 'capsule' +} + +type ActionButtonStyles = { + padding: string + borderRadius: string + fontSize: string +} + +const ACTION_BUTTON_FONT_COLORS: Record = { + default: 'gc_4', + lighten_2: 'font_1', + font_1: 'gc_4', + gc_4: 'font_1', + gc_1: 'font_4', +} as const + +const ACTION_BUTTON_STYLES: Record = { + roundedRect: { + padding: '15.5px 24px', + borderRadius: '12px', + fontSize: '_14', + }, + semiRoundedRect: { + padding: '16.5px 24px', + borderRadius: '20px', + fontSize: '_15', + }, + capsule: { + padding: '18px 24px', + borderRadius: '100px', + fontSize: '_17', + }, +} + +export const ActionButton = styled.button` + width: 100%; + background-color: ${({ theme, $bgColor = 'default' }) => + theme.colors.grayscale[$bgColor] || theme.colors.brand[$bgColor]}; + color: ${({ theme, $bgColor = 'default' }) => theme.colors.grayscale[ACTION_BUTTON_FONT_COLORS[$bgColor]]}; + padding: ${({ $type = 'capsule' }) => ACTION_BUTTON_STYLES[$type]?.padding}; + border-radius: ${({ $type = 'capsule' }) => ACTION_BUTTON_STYLES[$type]?.borderRadius}; + font-size: ${({ theme, $type = 'capsule' }) => theme.typography[ACTION_BUTTON_STYLES[$type]?.fontSize]}; +` diff --git a/src/components/Input/TwoLineInput/index.tsx b/src/components/Input/TwoLineInput/index.tsx new file mode 100644 index 0000000..92e522c --- /dev/null +++ b/src/components/Input/TwoLineInput/index.tsx @@ -0,0 +1,27 @@ +import { TEXTAREA_VALIDATION } from '@constants/validations' +import { ChangeEvent, useState } from 'react' +import { TextareaAutosizeProps } from 'react-textarea-autosize' +import * as S from './styles' + +type TwoLineInputProps = TextareaAutosizeProps + +export default function TwoLineInput({ ...rest }: TwoLineInputProps) { + const [value, setValue] = useState('') + + const onChange = (e: ChangeEvent) => { + const currentValue = e.target.value + const lines = currentValue.split('\n') + + if (lines.length > 2) { + const limitedValue = lines.slice(0, 2).join('\n') + setValue(limitedValue) + return + } + + setValue(currentValue) + } + + return ( + + ) +} diff --git a/src/components/Input/TwoLineInput/styles.ts b/src/components/Input/TwoLineInput/styles.ts new file mode 100644 index 0000000..0c7a48c --- /dev/null +++ b/src/components/Input/TwoLineInput/styles.ts @@ -0,0 +1,16 @@ +import { styled } from 'styled-components' +import TextareaAutosize from 'react-textarea-autosize' + +export const TwoLineInput = styled(TextareaAutosize)` + width: 100%; + border: none; + text-align: center; + padding: 17px 32px; + border-radius: 12px; + font-size: ${({ theme }) => theme.typography._20}; + resize: none; // 수동 리사이즈 방지 + overflow: hidden; // 스크롤바 제거 + &:focus { + box-shadow: ${({ theme }) => `inset 0 0 0 1px ${theme.colors.grayscale.font_1}`}; + } +` diff --git a/src/components/Input/index.ts b/src/components/Input/index.ts new file mode 100644 index 0000000..22d4072 --- /dev/null +++ b/src/components/Input/index.ts @@ -0,0 +1,14 @@ +import { styled } from 'styled-components' + +export const Input = styled.input` + width: 100%; + border: none; + font-size: ${({ theme }) => theme.typography._20}; + text-align: center; + /* transition: 0.15s box-shadow; */ + padding: 17px 32px; + border-radius: 12px; + &:focus { + box-shadow: ${({ theme }) => `inset 0 0 0 1px ${theme.colors.grayscale.font_1}`}; + } +` diff --git a/src/components/Toggle/index.tsx b/src/components/Toggle/index.tsx new file mode 100644 index 0000000..695d33f --- /dev/null +++ b/src/components/Toggle/index.tsx @@ -0,0 +1,25 @@ +import { SettingsStoreKey, useSettingsStore } from '@stores/settingsStore' +import * as S from './styles' + +type ToggleProps = { + id: string + setting: SettingsStoreKey +} + +export default function Toggle({ id, setting }: ToggleProps) { + const value = useSettingsStore(state => state[setting]) + const setSetting = useSettingsStore(state => state.setSetting) + + const handleChange = (e: React.ChangeEvent) => { + setSetting(setting, e.target.checked) + } + + return ( + + + + + ) +} diff --git a/src/components/Toggle/styles.ts b/src/components/Toggle/styles.ts new file mode 100644 index 0000000..cc8dd9c --- /dev/null +++ b/src/components/Toggle/styles.ts @@ -0,0 +1,42 @@ +import { styled } from 'styled-components' + +export const Toggle = styled.div` + display: block; + width: fit-content; + border-radius: 100px; + position: relative; + overflow: hidden; + width: 40px; + height: 24px; + + & > label { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: 1; + cursor: pointer; + background-color: ${({ theme }) => theme.colors.grayscale.gc_1}; + transition: background-color 0.3s; + } + & > input:checked + label { + background-color: ${({ theme }) => theme.colors.grayscale.font_1}; + } +` + +export const Circle = styled.div` + position: absolute; + top: 50%; + left: 2px; + translate: 0 -50%; + width: 20px; + height: 20px; + border-radius: 50%; + transition: background-color 0.3s ease; + background-color: ${({ theme }) => theme.colors.grayscale.gc_4}; + input:checked + label > & { + translate: 16px -50%; + } + transition: translate 0.3s; +` diff --git a/src/components/ToggleArea/index.tsx b/src/components/ToggleArea/index.tsx new file mode 100644 index 0000000..ad6e303 --- /dev/null +++ b/src/components/ToggleArea/index.tsx @@ -0,0 +1,13 @@ +import ToggleBox from '@components/ToggleBox' +import * as S from './styles' + +export default function ToggleArea() { + return ( + + + + + + + ) +} diff --git a/src/components/ToggleArea/styles.ts b/src/components/ToggleArea/styles.ts new file mode 100644 index 0000000..685755a --- /dev/null +++ b/src/components/ToggleArea/styles.ts @@ -0,0 +1,3 @@ +import { styled } from 'styled-components' + +export const ToggleArea = styled.div`` diff --git a/src/components/ToggleBox/index.tsx b/src/components/ToggleBox/index.tsx new file mode 100644 index 0000000..6bda161 --- /dev/null +++ b/src/components/ToggleBox/index.tsx @@ -0,0 +1,33 @@ +import Toggle from '@components/Toggle' +import { Typo14, Typo15, Typo17 } from '@components/Typo' +import { SETTINGS_INFO } from '@constants/settingsInfo' +import { SettingsStoreKey } from '@stores/settingsStore' +import * as S from './styles' + +type ToggleBoxProps = { + type: 'sm' | 'md' | 'lg' + setting: SettingsStoreKey +} + +const Typo = { + sm: Typo14, + md: Typo15, + lg: Typo17, +} + +/** + *주의! 단독으로 사용할 경우, Wrapper로 감싸주셔야 border radius가 적용됩니다. + */ +export default function ToggleBox({ setting, type }: ToggleBoxProps) { + const SelectedTypo = Typo[type] + + return ( + + + {SETTINGS_INFO[setting].title} + {SETTINGS_INFO[setting].desc && {SETTINGS_INFO[setting].desc}} + + + + ) +} diff --git a/src/components/ToggleBox/styles.ts b/src/components/ToggleBox/styles.ts new file mode 100644 index 0000000..6e4fa5d --- /dev/null +++ b/src/components/ToggleBox/styles.ts @@ -0,0 +1,29 @@ +import { styled } from 'styled-components' + +type ToggleBoxProps = { + $type: 'sm' | 'md' | 'lg' +} + +const TOGGLE_BOX_PADDING = { + sm: '15.5px 16px 15.5px 20px', + md: '16.5px 16px 16.5px 20px', + lg: '18px 16px 18px 20px', +} +export const ToggleBox = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: ${({ $type }) => TOGGLE_BOX_PADDING[$type]}; + background-color: ${({ theme }) => theme.colors.grayscale.gc_4}; + + &:first-of-type { + border-top-left-radius: 16px; + border-top-right-radius: 16px; + } + &:last-of-type { + border-bottom-left-radius: 16px; + border-bottom-right-radius: 16px; + } +` + +export const MainArea = styled.div`` diff --git a/src/components/Typo/index.ts b/src/components/Typo/index.ts new file mode 100644 index 0000000..3346479 --- /dev/null +++ b/src/components/Typo/index.ts @@ -0,0 +1,39 @@ +import { FontWeight, GrayscaleColors, styled } from 'styled-components' + +type TypoProps = { + color?: keyof GrayscaleColors + weight?: FontWeight +} + +const Typo = styled.p` + color: ${({ color }) => (color ? color : 'inherit')}; + font-weight: ${({ weight }) => (weight ? weight : 400)}; +` + +export const Typo11 = styled(Typo)` + font-size: ${({ theme }) => theme.typography._11}; +` + +export const Typo13 = styled(Typo)` + font-size: ${({ theme }) => theme.typography._13}; +` + +export const Typo14 = styled(Typo)` + font-size: ${({ theme }) => theme.typography._14}; +` + +export const Typo15 = styled(Typo)` + font-size: ${({ theme }) => theme.typography._15}; +` + +export const Typo17 = styled(Typo)` + font-size: ${({ theme }) => theme.typography._17}; +` + +export const Typo20 = styled(Typo)` + font-size: ${({ theme }) => theme.typography._20}; +` + +export const Typo24 = styled(Typo)` + font-size: ${({ theme }) => theme.typography._24}; +` diff --git a/src/constants/settingsInfo.ts b/src/constants/settingsInfo.ts new file mode 100644 index 0000000..91bbd03 --- /dev/null +++ b/src/constants/settingsInfo.ts @@ -0,0 +1,28 @@ +import { SettingsStoreKey } from '@stores/settingsStore' + +export const SETTINGS_INFO: Record = { + allNotifications: { + title: '모든 알림', + desc: '', + }, + friendRequests: { + title: '친구 신청', + desc: '부연 설명이 들어가는 공간', + }, + familyWalkNotifications: { + title: '가족 산책 알림', + desc: '부연 설명이 들어가는 공간', + }, + myWalkNotifications: { + title: '내 산책 알림', + desc: '부연 설명이 들어가는 공간', + }, + messages: { + title: '메세지', + desc: '부연 설명이 들어가는 공간', + }, + gangbuntta: { + title: '강번따 허용 여부', + desc: '부연 설명이 들어가는 공간', + }, +} diff --git a/src/constants/validations.ts b/src/constants/validations.ts new file mode 100644 index 0000000..a874339 --- /dev/null +++ b/src/constants/validations.ts @@ -0,0 +1,5 @@ +export const INPUT_VALIDATION = {} + +export const TEXTAREA_VALIDATION = { + maxLength: 50, +} diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts new file mode 100644 index 0000000..118e064 --- /dev/null +++ b/src/stores/settingsStore.ts @@ -0,0 +1,28 @@ +import { create } from 'zustand' + +type SettingsStore = { + allNotifications: boolean + friendRequests: boolean + familyWalkNotifications: boolean + myWalkNotifications: boolean + messages: boolean + gangbuntta: boolean + setSetting: (key: keyof Omit, value: boolean) => void +} + +export const useSettingsStore = create(set => ({ + allNotifications: true, + friendRequests: true, + familyWalkNotifications: true, + myWalkNotifications: true, + messages: true, + gangbuntta: true, + + setSetting: (key, value) => + set(state => ({ + ...state, + [key]: value, + })), +})) + +export type SettingsStoreKey = keyof Omit diff --git a/src/styles/globalStyle.ts b/src/styles/globalStyle.ts index 2abf0d9..1cf45d7 100644 --- a/src/styles/globalStyle.ts +++ b/src/styles/globalStyle.ts @@ -46,7 +46,7 @@ const GlobalStyle = createGlobalStyle` line-height: 1.5; letter-spacing: -0.025em; color: ${({ theme }) => theme.colors.grayscale.font_1}; /* 기본 텍스트 색상 (Font_1) */ - background-color: ${({ theme }) => theme.colors.grayscale.gc_4}; /* 배경색 (GC_4) */ + background-color: ${({ theme }) => theme.colors.brand.lighten_3}; /* 배경색 (GC_4) */ } /* 버튼 스타일 */ @@ -61,6 +61,15 @@ const GlobalStyle = createGlobalStyle` font-family: inherit; /* 폰트 상속 */ outline: none; } + + input { + &::placeholder { + color: ${({ theme }) => theme.colors.grayscale.font_3}; + } + &:focus::placeholder { + color: transparent; + } + } ` export default GlobalStyle diff --git a/src/styles/styled.d.ts b/src/styles/styled.d.ts index 0a9728f..373e294 100644 --- a/src/styles/styled.d.ts +++ b/src/styles/styled.d.ts @@ -1,35 +1,44 @@ import 'styled-components' declare module 'styled-components' { - export interface DefaultTheme { - colors: { - brand: { - darken: string - default: string - lighten_1: string - lighten_2: string - lighten_3: string - sub: string - } - grayscale: { - font_1: string - font_2: string - font_3: string - font_4: string - gc_1: string - gc_2: string - gc_3: string - gc_4: string - } - } - typography: { - suitVariable24pt: string - suitVariable20pt: string - suitVariable17pt: string - suitVariable15pt: string - suitVariable14pt: string - suitVariable13pt: string - suitVariable11pt: string - } + export type DefaultTheme = { + colors: Colors + typography: Typography } + export type BrandColors = { + darken: string + default: string + lighten_1: string + lighten_2: string + lighten_3: string + sub: string + } + + export type GrayscaleColors = { + font_1: string + font_2: string + font_3: string + font_4: string + gc_1: string + gc_2: string + gc_3: string + gc_4: string + } + + export type Colors = { + brand: BrandColors + grayscale: GrayscaleColors + } + + export type Typography = { + _24: string + _20: string + _17: string + _15: string + _14: string + _13: string + _11: string + } + + export type FontWeight = '300' | '400' | '700' | '800' } diff --git a/src/styles/theme.ts b/src/styles/theme.ts index b8452ba..765b2f7 100644 --- a/src/styles/theme.ts +++ b/src/styles/theme.ts @@ -1,65 +1,58 @@ import { DefaultTheme } from 'styled-components' +const brand = { + darken: '#462008', + default: '#783D16', + lighten_1: '#ECB99A', + lighten_2: '#E8DCD4', + lighten_3: '#F4F0ED', + sub: '#6CA719', +} as const + +const grayscale = { + font_1: '#111111', // 기본 텍스트 색상 + font_2: '#505050', + font_3: '#767676', + font_4: '#999999', + gc_1: '#E5E5EC', // 배경 색상 + gc_2: '#F1F1F5', + gc_3: '#F7F7FB', + gc_4: '#FFFFFF', // 가장 밝은 배경 +} as const + +const grayscaleDark = { + font_1: '#FFFFFF', // 어두운 모드에서 기본 텍스트 색상 + font_2: '#E5E5EC', // 밝은 회색 톤의 텍스트 + font_3: '#999999', + font_4: '#767676', + gc_1: '#111111', // 어두운 배경 색상 + gc_2: '#505050', + gc_3: '#767676', + gc_4: '#999999', +} as const + +export const typography = { + _24: '24px', + _20: '20px', + _17: '17px', + _15: '15px', + _14: '14px', + _13: '13px', + _11: '11px', +} as const + export const lightTheme: DefaultTheme = { colors: { - brand: { - darken: '#462008', - default: '#783D16', - lighten_1: '#ECB99A', - lighten_2: '#E8DCD4', - lighten_3: '#F4F0ED', - sub: '#6CA719', - }, - grayscale: { - font_1: '#111111', // 기본 텍스트 색상 - font_2: '#505050', - font_3: '#767676', - font_4: '#999999', - gc_1: '#E5E5EC', // 배경 색상 - gc_2: '#F1F1F5', - gc_3: '#F7F7FB', - gc_4: '#FFFFFF', // 가장 밝은 배경 - }, - }, - typography: { - suitVariable24pt: '24px', - suitVariable20pt: '20px', - suitVariable17pt: '17px', - suitVariable15pt: '15px', - suitVariable14pt: '14px', - suitVariable13pt: '13px', - suitVariable11pt: '11px', + brand, + grayscale, }, + typography, } export const darkTheme: DefaultTheme = { colors: { - brand: { - darken: '#462008', - default: '#783D16', - lighten_1: '#ECB99A', - lighten_2: '#E8DCD4', - lighten_3: '#F4F0ED', - sub: '#6CA719', - }, - grayscale: { - font_1: '#FFFFFF', // 어두운 모드에서 기본 텍스트 색상 - font_2: '#E5E5EC', // 밝은 회색 톤의 텍스트 - font_3: '#999999', - font_4: '#767676', - gc_1: '#111111', // 어두운 배경 색상 - gc_2: '#505050', - gc_3: '#767676', - gc_4: '#999999', - }, - }, - typography: { - suitVariable24pt: '24px', - suitVariable20pt: '20px', - suitVariable17pt: '17px', - suitVariable15pt: '15px', - suitVariable14pt: '14px', - suitVariable13pt: '13px', - suitVariable11pt: '11px', + brand, + grayscale: grayscaleDark, }, + typography, }