From a88a4a7904f80410e3a71827abf3d063ee092562 Mon Sep 17 00:00:00 2001 From: Ivoryeee <105477246+Ivoryeee@users.noreply.github.com> Date: Sun, 29 Dec 2024 22:14:47 +0900 Subject: [PATCH] =?UTF-8?q?[Style]=20=EB=AA=A8=EB=A6=BD=EC=84=B8=ED=8A=B8?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=8D=BC=EB=B8=94=EB=A6=AC?= =?UTF-8?q?=EC=8B=B1=20(#226)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [style] 사이드바 모립세트 뷰 추가 (#220) * [style] 모립세트 경로 추가 (#220) * [style] tab 스타일 변경사항 반영 (#220) * [style] 모립세트 페이지 퍼블리싱 (#220) * [ Style ] 모립세트 컴포넌트 구현 (#225) * refactor: SideBoxTemporary 컴포넌트 삭제 * feat: MoribSetCOntainer 컴포넌트 구현 * style: 허용할 사이트 영역 홀,짝수번째 배경화면 다르게 수정 * feat: 취소 버튼 온클릭 핸들러 추가 * refactor: MoribSetContainer가 열린 상태에서 다른 요소 클릭 가능하게 수정 * feat: 배열 별도의 상수 파일로 분리 및 기능 구현 * refactor: 아이콘 url도 ALLOW_SITE_LIST에서 관리 * codereview: 대원 코드리뷰 반영 * refactor: 모립세트 워딩 허용서비스 세트로 수정 * codereview: 대원, 상아 코드리뷰 반영 * feat: 툴팁 생성 * refactor: 타이머페이지 툴팁 삭제 * refacotr: 툴팁 isAllowedServiceVisible이 true일 때만 화면에 등장하도록 수정 * codereview: BoxAllowedService에서 PopoverAllowedService로 네이밍 변경 * [ Style ] 설정 뷰 내의 경고 모달 뷰 구현 (#227) * feat: alert모달 내 버튼 공통 컴포넌트 생성 (#222) * feat: ModalContentsAlert 생성 (#222) * feat: input창 에러일 때 추가 * fix: 필요없는 코드 삭제 (#222) * refactor: 약간의 로직분리 .. (#222) * code review: user email prop 추가, 모달 컴포넌트 하나의 객체로 export 하도록 수정 * style: 스타일 누락된 것 추가 * refactor: userEmail props 추가 누락된 부분 수정 * [ Style ] 온보딩 뷰 구현 (#228) * refactor: home/ not found 페이지에 쓰이는 사이드바 컴포넌트 리팩토링 (#224) - 세팅모달, 기존 SidebarHome에서 쓰이는 컴포넌트 구조 배럴파일 방식으로 변경 - HomePage, NotFoundePage 두 곳에서 쓰이므로 shared로 위치 이동 - Sidebar에서 쓰이는 svg는 Sidebar 폴더 아래로 이동 * refactor: 홈페이지 구조 absolute를 사용해서 변경 (#224) * feat: 온보딩 페이지 라우팅 (#224) * refactor: 설정 모달 위치 변경 (#224) * refactor: 사이드바 위치 변경 (#224) * refactor: 레이아웃 생성 및 적용 (#224) * refactor: 폰트 적용 방식 자동완성 될 수 있도록 변경 (#224) * feat: 사용하는 아이콘 다운로드 및 export 설정 (#224) * feat: StartStep, FieldStep 구현 (#224) * feat: ServiceStep 구현 및 export (#224) * chore: 상수 분리 및 컴포넌트 디자인 일부 수정 (#224) * feat: useFunnel 훅 생성 및 온보딩 페이지에 적용 (#224) * feat: Step 컴포넌트 컨벤션에 맞게 네이밍 변경 (#224) * feat: 온보딩 페이지 퍼블리싱 (#224) * feat: input 상태 ServiceStep 컴포넌트에 연결 (#224) * style: StepField 컴포넌트 화면 너비에 따라 패딩값 일부 수정 (#224) * feat: ServiceStep 버튼에 disabled 속성 추가 및 엔터 눌렀을 때 입력되게 끔 로직 추가 (#224) * feat: svg 아이콘 폴더 shared로 변경, assets 아래 페이지별 폴더를 만들어 관리 (#224) * feat: 바뀐 svg들의 위치에 맞게 import 경로 변경 및 BoxAllowedService 버튼에 홈으로 이동하는 로직 추가 (#224) * style: 반응형 고려하여 홈 페이지 컴포넌트 일부 스타일 수정 (#224) - 미완성입니다요 * refactor: truncate 활용해서 스타일 재적용 (#224) * refactor: StepField.tsx p태그에서 h2태그로 변경 (#224) * chore: 브라우저 리스트 업데이트하면서 일부 패키지 버전업 (#224) * refactor: StepService.tsx 인풋 상태 input -> inputURL로 수정 (#224) * [Refactor] home 페이지 폴더구조 변경 (#231) * feat: home 페이지 폴더구조 변경 (#230) * feat: 공통적으로 쓰이는 BoxTodo, ButtonTogoToggle -> shared로 이동 (#230) * feat: shared로 이동한 컴포넌트 경로 변경 (#230) * refactor: ModalContents 폴더 구조 변경 (#230) * feat: import 문 구분을 위해 폴더 구조 배럴파일 구조로 변경 (#230) * feat: commit 누락된 부분 저장 (#230) * feat: 홈페이지 import 문 수정 (#230) * refactor: atoms 컴포넌트 용도에 맞게 폴더 이동 및 네이밍 컨벤션에 맞게 변경 (#230) * refactor: 바뀐 컴포넌트 경로에 맞게 import문 수정 (#230) * refactor: 홈에서만 쓰이는 hook 폴더 이동 및 훅을 사용하는 컴포넌트의 import문 변경 (#230) * refactor: ButtonSVG 사용 불필요하여 삭제 (#230) * refactor: 상아 피드백 반영해서 UI 분류 페이지 제거 모든 컴포넌트 depth 1로 고정 (#230) * refactor: hook import 경로 수정 (#230) * refactor: 로그인 페이지 구조 변경 및 불필요한 templates 레거시 코드 삭제 (#230) (#232) * [Refactor] 타이머 페이지 폴더 구조 변경 (#233) * refactor: timer 컴포넌트/훅 컨벤션에 맞게 정리 및 불필요 컴포넌트/훅 삭제 (#230) * refactor: 불필요한 templates 컴포넌트 삭제 (#230) * Merge branch 'develop' of github.com:morib-in/Morib-Client into refactor/#230/timer-folder-structure * [Refactor] 온보딩 페이지 폴더구조 변경 (#234) * refactor: 온보딩 페이지 컴포넌트 폴더 삭제 및 컴포넌트명으로 폴더 생성하여 적용 (#230) * refactor: 상아 피드백 반영해서 컴포넌트 depth 1로 고정 (#230) * [style] 컬러팔레트 네이밍 추가 및 색상 공통 컴포넌트 생성 (#220) * [refactor] 색상 네이밍에 맞춰 컬러 팔레트 수정 (#220) * [feat] url 중복 유효성 검사 로직 추가 (#220) * [refactor] 드롭다운 클릭 시 이벤트 전파 방지 (#220) * [style] 카테고리 내 허용서비스 컴포넌트 분리 (#220) * [code review] 코드리뷰 반영 (#220) * [style] moribSet -> allowedService 네이밍 변경 (#220) * [chore] 리베이스 충돌 해결 (#220) * [style] sideBar 삭제 후 Layout 적용 (#220) * [code review] 코드리뷰 반영 (#220) * [style] 사이드바 홈, 허용서비스뷰 추가 (#220) * [style] 드롭다운 이벤트 전파 방지 적용 (#220) --------- Co-authored-by: 김건휘 <66071954+KIMGEONHWI@users.noreply.github.com> Co-authored-by: Hanseo Kim <108131226+seueooo@users.noreply.github.com> Co-authored-by: suwonthugger <127329855+suwonthugger@users.noreply.github.com> --- src/pages/MoribSetPage/AllowedServicePage.tsx | 134 ++++++++++++++++++ .../components/Box/BoxAllowedServiceItem.tsx | 71 ++++++++++ .../components/Box/BoxEachUrlInfo.tsx | 28 ++++ .../components/Box/BoxMakeAllowedService.tsx | 101 +++++++++++++ .../components/Box/BoxRecommendService.tsx | 83 +++++++++++ .../components/Box/BoxUrlList.tsx | 29 ++++ .../Button/ButtonAddAllowedService.tsx | 15 ++ .../components/CategoryAllowedService.tsx | 46 ++++++ .../MoribSetPage/components/ColorPallete.tsx | 22 +++ .../Input/InputAllowedServiceUrl.tsx | 85 +++++++++++ .../components/TableAllowedService.tsx | 45 ++++++ src/pages/MoribSetPage/types/index.ts | 13 ++ src/router/Router.tsx | 11 ++ src/router/routesConfig.ts | 4 + src/shared/assets/svgs/moribSet.svg | 3 + src/shared/assets/svgs/plus.svg | 3 + src/shared/components/ButtonCategoryTab.tsx | 4 +- src/shared/components/CircleColorIcon.tsx | 11 ++ src/shared/components/Dropdown.tsx | 10 +- src/shared/constants/colorPallete.ts | 14 ++ src/shared/constants/recommendSites.ts | 15 ++ src/shared/constants/tabSelections.ts | 4 +- .../layout/components/Sidebar/Sidebar.tsx | 15 +- src/shared/utils/isUrlValid/index.ts | 2 +- tailwind.config.js | 12 ++ 25 files changed, 771 insertions(+), 9 deletions(-) create mode 100644 src/pages/MoribSetPage/AllowedServicePage.tsx create mode 100644 src/pages/MoribSetPage/components/Box/BoxAllowedServiceItem.tsx create mode 100644 src/pages/MoribSetPage/components/Box/BoxEachUrlInfo.tsx create mode 100644 src/pages/MoribSetPage/components/Box/BoxMakeAllowedService.tsx create mode 100644 src/pages/MoribSetPage/components/Box/BoxRecommendService.tsx create mode 100644 src/pages/MoribSetPage/components/Box/BoxUrlList.tsx create mode 100644 src/pages/MoribSetPage/components/Button/ButtonAddAllowedService.tsx create mode 100644 src/pages/MoribSetPage/components/CategoryAllowedService.tsx create mode 100644 src/pages/MoribSetPage/components/ColorPallete.tsx create mode 100644 src/pages/MoribSetPage/components/Input/InputAllowedServiceUrl.tsx create mode 100644 src/pages/MoribSetPage/components/TableAllowedService.tsx create mode 100644 src/pages/MoribSetPage/types/index.ts create mode 100644 src/shared/assets/svgs/moribSet.svg create mode 100644 src/shared/assets/svgs/plus.svg create mode 100644 src/shared/components/CircleColorIcon.tsx create mode 100644 src/shared/constants/colorPallete.ts create mode 100644 src/shared/constants/recommendSites.ts diff --git a/src/pages/MoribSetPage/AllowedServicePage.tsx b/src/pages/MoribSetPage/AllowedServicePage.tsx new file mode 100644 index 00000000..3d9f980a --- /dev/null +++ b/src/pages/MoribSetPage/AllowedServicePage.tsx @@ -0,0 +1,134 @@ +import { useEffect, useRef, useState } from 'react'; + +import ModalWrapper, { ModalWrapperRef } from '@/shared/components/ModalWrapper'; + +import useClickOutside from '@/shared/hooks/useClickOutside'; + +import BellIcon from '@/shared/assets/svgs/bell.svg?react'; +import FriendSettingIcon from '@/shared/assets/svgs/friend_setting.svg?react'; + +import ModalContentsFriends from '@/pages/HomePage/ModalContentsFriends/ModalContentsFriends'; + +import BoxMakeAllowedService from './components/Box/BoxMakeAllowedService'; +import BoxRecommendService from './components/Box/BoxRecommendService'; +import CategoryAllowedService from './components/CategoryAllowedService'; +import { AllowedService, UrlInfo } from './types'; + +const AllowedServicePage = () => { + const friendsModalRef = useRef(null); + const friendModalContentRef = useRef(null); + + const [allowedServices, setAllowedServices] = useState([ + { + id: Date.now(), + allowedServiceName: '', + selectedColor: 'bg-gray-bg-07', + urlList: [], + }, + ]); + const [activeAllowedServiceId, setActiveAllowedServiceId] = useState(allowedServices[0].id); + + const handleAddAllowedService = () => { + const newSet: AllowedService = { + id: Date.now(), + allowedServiceName: '', + selectedColor: 'bg-gray-bg-07', + urlList: [], + }; + setAllowedServices((prev) => [...prev, newSet]); + setActiveAllowedServiceId(newSet.id); + }; + + const handleUpdateAllowedService = (id: number, key: T, value: AllowedService[T]) => { + setAllowedServices((prev) => + prev.map((set) => + set.id === id + ? { + ...set, + [key]: value, + } + : set, + ), + ); + }; + + const handleDeleteAllowedService = (id: number) => { + const updatedAllowedServices = allowedServices.filter((allowedService) => allowedService.id !== id); + if (updatedAllowedServices.length === 0) { + return; + } + setAllowedServices(updatedAllowedServices); + }; + + const handleAddUrlToAllowedService = (urlInfo: UrlInfo) => { + setAllowedServices((prev) => + prev.map((set) => + set.id === activeAllowedServiceId + ? { + ...set, + urlList: [...set.urlList, urlInfo], + } + : set, + ), + ); + }; + + useEffect(() => { + if (allowedServices.length > 0 && !allowedServices.find((set) => set.id === activeAllowedServiceId)) { + setActiveAllowedServiceId(allowedServices[allowedServices.length - 1].id); + } + }, [allowedServices, activeAllowedServiceId]); + + const activeAllowedService = allowedServices.find((set) => set.id === activeAllowedServiceId); + + const handleOpenFriendsModal = () => { + friendsModalRef.current?.open(); + }; + const handleCloseFriendsModal = () => { + friendsModalRef.current?.close(); + }; + + useClickOutside(friendModalContentRef, handleCloseFriendsModal); + + return ( +
+
+ + +
+ +
+ + +
+ {activeAllowedService && ( + handleUpdateAllowedService(activeAllowedServiceId, key, value)} + /> + )} + +
+ +
+
+
+ + + + +
+ ); +}; + +export default AllowedServicePage; diff --git a/src/pages/MoribSetPage/components/Box/BoxAllowedServiceItem.tsx b/src/pages/MoribSetPage/components/Box/BoxAllowedServiceItem.tsx new file mode 100644 index 00000000..eba4c063 --- /dev/null +++ b/src/pages/MoribSetPage/components/Box/BoxAllowedServiceItem.tsx @@ -0,0 +1,71 @@ +import Dropdown from '@/shared/components/Dropdown'; + +import IconMeatBall from '@/shared/assets/svgs/todo_meatball_default.svg?react'; + +import { AllowedService } from './../../types'; + +interface BoxAllowedServiceItemProps { + allowedService: AllowedService; + activeAllowedServiceId: number | null; + setActiveAllowedServiceId: (id: number) => void; + deleteAllowedService: (id: number) => void; + maxIconsToShow: number; +} + +const BoxAllowedServiceItem = ({ + allowedService, + activeAllowedServiceId, + setActiveAllowedServiceId, + deleteAllowedService, + maxIconsToShow, +}: BoxAllowedServiceItemProps) => { + const additionalIconsCount = + allowedService.urlList.length > maxIconsToShow ? allowedService.urlList.length - maxIconsToShow : 0; + + return ( +
setActiveAllowedServiceId(allowedService.id)} + > +
+
+
+

+ {allowedService.allowedServiceName || '모립세트 이름을 입력해주세요.'} +

+
+ + + + + + + { + deleteAllowedService(allowedService.id); + }} + /> + + +
+ +
+ {allowedService.urlList.slice(0, maxIconsToShow).map(({ faviconUrl }, url) => ( + favicon + ))} + {additionalIconsCount > 0 && ( +
+ +{additionalIconsCount} +
+ )} +
+
+ ); +}; + +export default BoxAllowedServiceItem; diff --git a/src/pages/MoribSetPage/components/Box/BoxEachUrlInfo.tsx b/src/pages/MoribSetPage/components/Box/BoxEachUrlInfo.tsx new file mode 100644 index 00000000..baed0d10 --- /dev/null +++ b/src/pages/MoribSetPage/components/Box/BoxEachUrlInfo.tsx @@ -0,0 +1,28 @@ +import { ReactNode } from 'react'; + +import { UrlInfo } from './../../types'; + +interface BoxEachUrlInfoProps { + urlInfo: UrlInfo; + children: ReactNode; +} + +const BoxEachUrlInfo = ({ urlInfo, children }: BoxEachUrlInfoProps) => { + return ( + <> +
+ favicon +

{urlInfo.siteName}

+
+
+

{urlInfo.page}

+
+
+

{urlInfo.url}

+
+
{children}
+ + ); +}; + +export default BoxEachUrlInfo; diff --git a/src/pages/MoribSetPage/components/Box/BoxMakeAllowedService.tsx b/src/pages/MoribSetPage/components/Box/BoxMakeAllowedService.tsx new file mode 100644 index 00000000..84fe3068 --- /dev/null +++ b/src/pages/MoribSetPage/components/Box/BoxMakeAllowedService.tsx @@ -0,0 +1,101 @@ +import { ChangeEvent, useState } from 'react'; + +import ArrowSVGBtn from '@/shared/components/Button/ButtonArrowSVG/ButtonArrowSVG'; +import CategoryTabSelect from '@/shared/components/CategoryTabSelect'; + +import { Direction } from '@/shared/types/global'; + +import { CATEGORY_MODALTABS } from '@/shared/constants/tabSelections'; + +import { AllowedService } from './../../types'; +import ColorPallete from './../ColorPallete'; +import InputAllowedServiceUrl from './../Input/InputAllowedServiceUrl'; +import TableAllowedService from './../TableAllowedService'; +import BoxUrlList from './BoxUrlList'; + +interface BoxMakeAllowedServiceProps { + allowedService: AllowedService; + updateAllowedService: (key: T, value: AllowedService[T]) => void; +} + +const BoxMakeAllowedService = ({ allowedService, updateAllowedService }: BoxMakeAllowedServiceProps) => { + const { allowedServiceName, selectedColor, urlList } = allowedService; + const [isPaletteOpen, setIsPaletteOpen] = useState(false); + const [selectedTabId, setSelectedTabId] = useState(CATEGORY_MODALTABS[0].id); + + const handleColorPalleteOpen = () => { + setIsPaletteOpen((prev) => !prev); + }; + + const handleColorSelect = (color: string) => { + updateAllowedService('selectedColor', color); + setIsPaletteOpen(false); + }; + + const handleMoribNameChange = (e: ChangeEvent) => { + updateAllowedService('allowedServiceName', e.target.value); + }; + + const handleTabChange = (tab: number) => { + setSelectedTabId(tab); + }; + const handleDeleteUrlInfo = (urlToDelete: string) => { + const updatedUrlList = urlList.filter((urlInfo) => urlInfo.url !== urlToDelete); + updateAllowedService('urlList', updatedUrlList); + }; + + const addUrl = (url: string) => { + try { + const domain = new URL(url).hostname; + const faviconUrl = `https://www.google.com/s2/favicons?domain=${domain}`; + + updateAllowedService('urlList', [ + ...urlList, + { + siteName: domain.replace(/^www\./, '').replace(/\.[a-z]{2,}$/, ''), + page: domain, //api로 받은 값 + url, + faviconUrl, + }, + ]); + } catch (error) { + console.error('Invalid URL:', error); + } + }; + + return ( +
+
+
+
+ +
+ +
+ + + +
+ +
+ + {selectedTabId === CATEGORY_MODALTABS[0].id && ( + <> +
+ +
+ + + + + )} +
+ ); +}; + +export default BoxMakeAllowedService; diff --git a/src/pages/MoribSetPage/components/Box/BoxRecommendService.tsx b/src/pages/MoribSetPage/components/Box/BoxRecommendService.tsx new file mode 100644 index 00000000..94ed7668 --- /dev/null +++ b/src/pages/MoribSetPage/components/Box/BoxRecommendService.tsx @@ -0,0 +1,83 @@ +import { useRef, useState } from 'react'; + +import ArrowSVGBtn from '@/shared/components/Button/ButtonArrowSVG/ButtonArrowSVG'; + +import { Direction } from '@/shared/types/global'; + +import { recommendServices } from '@/shared/constants/recommendSites'; + +import { UrlInfo } from './../../types'; + +interface BoxRecommendServiceProps { + addUrlToAllowedService: (url: UrlInfo) => void; +} + +const BoxRecommendService = ({ addUrlToAllowedService }: BoxRecommendServiceProps) => { + const [availableServices, setAvailableServices] = useState(recommendServices); + + const handleServiceClick = (service: { serviceName: string; url: string }) => { + addUrlToAllowedService({ + siteName: service.serviceName.replace(/^www\./, '').replace(/\.[a-z]{2,}$/, ''), + page: service.serviceName, + url: service.url, + faviconUrl: `https://www.google.com/s2/favicons?domain=${service.url}`, + }); + setAvailableServices((prevServices) => prevServices.filter((item) => item.url !== service.url)); + }; + + const carouselContainerRef = useRef(null); + + const scrollCarousel = (direction: 'left' | 'right') => { + if (carouselContainerRef.current) { + const scrollAmount = 1264; + if (direction === 'left') { + carouselContainerRef.current.scrollLeft -= scrollAmount; + } else { + carouselContainerRef.current.scrollLeft += scrollAmount; + } + } + }; + + return ( +
+
+

추천 서비스

+
+ scrollCarousel('left')} + /> + scrollCarousel('right')} + /> +
+
+ +
+ {availableServices.map((service, index) => ( +
handleServiceClick(service)} + > + favicon +

{service.serviceName}

+
+ ))} +
+
+
+ ); +}; + +export default BoxRecommendService; diff --git a/src/pages/MoribSetPage/components/Box/BoxUrlList.tsx b/src/pages/MoribSetPage/components/Box/BoxUrlList.tsx new file mode 100644 index 00000000..ae5a550c --- /dev/null +++ b/src/pages/MoribSetPage/components/Box/BoxUrlList.tsx @@ -0,0 +1,29 @@ +import MinusBtn from '@/shared/assets/svgs/minus_btn.svg?react'; + +import { UrlInfo } from './../../types'; +import BoxEachUrlInfo from './BoxEachUrlInfo'; + +interface BoxUrlListProps { + urlList: UrlInfo[]; + onDelete: (url: string) => void; +} + +const BoxUrlList = ({ urlList, onDelete }: BoxUrlListProps) => { + return ( + <> + {urlList.map((urlInfo) => ( +
+ +
+ +
+
+
+ ))} + + ); +}; + +export default BoxUrlList; diff --git a/src/pages/MoribSetPage/components/Button/ButtonAddAllowedService.tsx b/src/pages/MoribSetPage/components/Button/ButtonAddAllowedService.tsx new file mode 100644 index 00000000..73629153 --- /dev/null +++ b/src/pages/MoribSetPage/components/Button/ButtonAddAllowedService.tsx @@ -0,0 +1,15 @@ +const ButtonAddAllowedService = ({ disabled = false, ...props }) => { + const defaultStyle = + 'absolute right-[1.2rem] body-semibold-16 flex items-center justify-center rounded-[5px] py-[1.2rem] px-[2.2rem]'; + const buttonStyle = disabled + ? ' bg-gray-bg-05 text-gray-04' + : ' bg-main-gra-01 text-gray-01 active:bg-main-gra-press'; + + return ( + + ); +}; + +export default ButtonAddAllowedService; diff --git a/src/pages/MoribSetPage/components/CategoryAllowedService.tsx b/src/pages/MoribSetPage/components/CategoryAllowedService.tsx new file mode 100644 index 00000000..e1c3d7af --- /dev/null +++ b/src/pages/MoribSetPage/components/CategoryAllowedService.tsx @@ -0,0 +1,46 @@ +import IconPlus from '@/shared/assets/svgs/plus.svg?react'; + +import { AllowedService } from './../types'; +import BoxAllowedServiceItem from './Box/BoxAllowedServiceItem'; + +interface CategoryAllowedServiceProps { + allowedServices: AllowedService[]; + activeAllowedServiceId: number | null; + setActiveAllowedServiceId: (id: number) => void; + addAllowedService: () => void; + deleteAllowedService: (id: number) => void; +} + +const CategoryAllowedService = ({ + allowedServices, + activeAllowedServiceId, + setActiveAllowedServiceId, + addAllowedService, + deleteAllowedService, +}: CategoryAllowedServiceProps) => { + const maxIconsToShow = 5; + + return ( +
+
+

내 모립세트

+ +
+ +
+ {allowedServices.map((allowedService) => ( + + ))} +
+
+ ); +}; + +export default CategoryAllowedService; diff --git a/src/pages/MoribSetPage/components/ColorPallete.tsx b/src/pages/MoribSetPage/components/ColorPallete.tsx new file mode 100644 index 00000000..d2393a51 --- /dev/null +++ b/src/pages/MoribSetPage/components/ColorPallete.tsx @@ -0,0 +1,22 @@ +import CircleColorIcon from '@/shared/components/CircleColorIcon'; + +import { colors } from '@/shared/constants/colorPallete'; + +interface ColorPaletteProps { + isOpen: boolean; + onSelectColor: (color: string) => void; +} + +const ColorPalette = ({ isOpen, onSelectColor }: ColorPaletteProps) => { + if (!isOpen) return null; + + return ( +
+ {colors.map((color) => ( + onSelectColor(color)} size={'h-[3rem] w-[3rem]'} /> + ))} +
+ ); +}; + +export default ColorPalette; diff --git a/src/pages/MoribSetPage/components/Input/InputAllowedServiceUrl.tsx b/src/pages/MoribSetPage/components/Input/InputAllowedServiceUrl.tsx new file mode 100644 index 00000000..c428b762 --- /dev/null +++ b/src/pages/MoribSetPage/components/Input/InputAllowedServiceUrl.tsx @@ -0,0 +1,85 @@ +import { FormEvent, useRef, useState } from 'react'; + +import { isUrlValid } from '@/shared/utils/isUrlValid/index'; + +import InputClearButton from '@/shared/assets/svgs/btn_inputClear.svg?react'; +import IconInputError from '@/shared/assets/svgs/error_input.svg?react'; + +import ButtonAddAllowedService from '@/pages/MoribSetPage/components/Button/ButtonAddAllowedService'; + +import { UrlInfo } from './../../types'; + +interface InputAllowedServiceUrlProps { + urlList: UrlInfo[]; + addUrl: (url: string) => void; +} + +const InputAllowedServiceUrl = ({ urlList, addUrl }: InputAllowedServiceUrlProps) => { + const inputRef = useRef(null); + const [showClearBtn, setShowClearBtn] = useState(false); + const [errorMessage, setErrorMessage] = useState({ error: '' }); + + const handleInputChange = () => { + if (inputRef.current) { + setShowClearBtn(inputRef.current.value.length > 0); + setErrorMessage({ error: '' }); + } + }; + + const handleClearBtn = () => { + if (inputRef.current) { + inputRef.current.value = ''; + setShowClearBtn(false); + setErrorMessage({ error: '' }); + } + }; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + const url = inputRef.current ? inputRef.current.value : ''; + if (!isUrlValid(url)) { + setErrorMessage({ error: '알맞은 형식의 주소를 입력해주세요.' }); + return; + } + if (Array.isArray(urlList) && urlList.some((item) => item.url === url)) { + setErrorMessage({ error: '이미 존재하는 주소입니다.' }); + return; + } + addUrl(url); + if (inputRef.current) { + inputRef.current.value = ''; + setShowClearBtn(false); + } + }; + + const errorStyle = 'border-error-02 border-[1px]'; + + return ( +
+
+ 0 ? errorStyle : `border-[1px] border-gray-bg-02`} placeholder-text-gray-04 w-full rounded-[8px] bg-gray-bg-02 px-[2rem] py-[2rem] text-white subhead-med-18 focus:outline-none`} + /> + {showClearBtn && ( + + )} + 0} /> +
+ + {errorMessage.error && ( +
+ +

{errorMessage.error}

+
+ )} +
+ ); +}; + +export default InputAllowedServiceUrl; diff --git a/src/pages/MoribSetPage/components/TableAllowedService.tsx b/src/pages/MoribSetPage/components/TableAllowedService.tsx new file mode 100644 index 00000000..85f875a9 --- /dev/null +++ b/src/pages/MoribSetPage/components/TableAllowedService.tsx @@ -0,0 +1,45 @@ +import { ReactNode } from 'react'; + +import { UrlInfo } from './../types'; + +interface TableAllowedServiceProps { + children: ReactNode; + urlList: UrlInfo[]; +} + +const TableAllowedService = ({ children, urlList }: TableAllowedServiceProps) => { + const tableNum = 9; + + const showYScroll = urlList.length > tableNum; + const defaultTableStyle = `h-[46rem] w-full overflow-y overflow-x-hidden rounded-[8px] mt-[1rem]`; + + const optionalScrollStyle = showYScroll ? 'overflow-y-auto' : 'overflow-y-hidden'; + const theadStyle = + ' detail-semibold-14 flex h-[4.6rem] items-center border-b-[0.1rem] border-gray-bg-04 text-gray-04'; + + const renderEmptyRows = (count: number) => { + return Array.from({ length: count }).map((_, index) => ( +
+ )); + }; + const emptyRowCount = Math.max(tableNum - urlList.length, 0); + + return ( +
+
+
+
사이트 이름
+
페이지
+
주소
+
+ +
+ {children} + {renderEmptyRows(emptyRowCount)} +
+
+
+ ); +}; + +export default TableAllowedService; diff --git a/src/pages/MoribSetPage/types/index.ts b/src/pages/MoribSetPage/types/index.ts new file mode 100644 index 00000000..53011253 --- /dev/null +++ b/src/pages/MoribSetPage/types/index.ts @@ -0,0 +1,13 @@ +export interface AllowedService { + id: number; + allowedServiceName: string; + selectedColor: string; + urlList: UrlInfo[]; +} + +export interface UrlInfo { + siteName: string; + page: string; + url: string; + faviconUrl: string; +} diff --git a/src/router/Router.tsx b/src/router/Router.tsx index 0c5ee65d..89756c20 100644 --- a/src/router/Router.tsx +++ b/src/router/Router.tsx @@ -13,6 +13,7 @@ import { ROUTES_CONFIG } from './routesConfig'; const LoginPage = lazy(() => import('@/pages/LoginPage/LoginPage')); const HomePage = lazy(() => import('@/pages/HomePage/HomePage')); const TimerPage = lazy(() => import('@/pages/TimerPage/TimerPage')); +const AllowedServicePage = lazy(() => import('@/pages/MoribSetPage/AllowedServicePage')); const ProtectedRoute = () => { //Todo: 개발이 진행되면 실제 토큰 상태를 받아서 login page로 이동 시킴 @@ -76,6 +77,16 @@ const router: Router = createBrowserRouter([ ), }, + { + path: ROUTES_CONFIG.allowedService.path, + element: ( + Loading...
}> + + + + + ), + }, ], }, diff --git a/src/router/routesConfig.ts b/src/router/routesConfig.ts index df91dfa0..df653f61 100644 --- a/src/router/routesConfig.ts +++ b/src/router/routesConfig.ts @@ -11,6 +11,10 @@ export const ROUTES_CONFIG = { title: 'Onboarding', path: '/onboarding', }, + allowedService: { + title: 'AllowedService', + path: '/allowedService', + }, redirect: { title: 'Redirect', path: '/redirect', diff --git a/src/shared/assets/svgs/moribSet.svg b/src/shared/assets/svgs/moribSet.svg new file mode 100644 index 00000000..da2b05bf --- /dev/null +++ b/src/shared/assets/svgs/moribSet.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/assets/svgs/plus.svg b/src/shared/assets/svgs/plus.svg new file mode 100644 index 00000000..49c4ac59 --- /dev/null +++ b/src/shared/assets/svgs/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/components/ButtonCategoryTab.tsx b/src/shared/components/ButtonCategoryTab.tsx index 03e1efc8..fad0b596 100644 --- a/src/shared/components/ButtonCategoryTab.tsx +++ b/src/shared/components/ButtonCategoryTab.tsx @@ -7,8 +7,8 @@ interface TabBtnProps extends ButtonHTMLAttributes { } const CategoryTabBtn = ({ children, activeTab, tabId, onClick }: TabBtnProps) => { - const notSelectedStyle = 'text-gray-03 bg-gray-bg-04 subhead-bold-22 px-[4px] py-[10px] mr-[1.7rem]'; - const SelectedStyle = 'bg-gray-bg-04 text-white subhead-bold-22 px-[4px] py-[10px] mr-[1.7rem]'; + const notSelectedStyle = 'text-gray-03 subhead-bold-22 p-[1rem]'; + const SelectedStyle = 'text-white subhead-bold-22 p-[1rem] border-b-[2px] border-text-white'; const tabBtnStyle = activeTab === tabId ? SelectedStyle : notSelectedStyle; return ( diff --git a/src/shared/components/CircleColorIcon.tsx b/src/shared/components/CircleColorIcon.tsx new file mode 100644 index 00000000..c6603ae9 --- /dev/null +++ b/src/shared/components/CircleColorIcon.tsx @@ -0,0 +1,11 @@ +interface CircleColorIconProps { + color: string; + size?: string; + onClick?: () => void; +} + +const CircleColorIcon = ({ color, size, onClick }: CircleColorIconProps) => { + return ); @@ -100,7 +106,7 @@ const DropdownItem = ({ label, textColor = 'default', ...props }: DropdownItemPr {...props} className={`flex bg-gray-bg-02 px-[1.6rem] py-[0.4rem] hover:bg-gray-bg-03 active:bg-gray-bg-04`} > -

{label}

+

{label}

); diff --git a/src/shared/constants/colorPallete.ts b/src/shared/constants/colorPallete.ts new file mode 100644 index 00000000..406b5a94 --- /dev/null +++ b/src/shared/constants/colorPallete.ts @@ -0,0 +1,14 @@ +export const colors = [ + 'bg-color-palette-red', + 'bg-color-palette-yellow1', + 'bg-color-palette-yellow2', + 'bg-color-palette-green1', + 'bg-color-palette-green2', + 'bg-color-palette-green3', + 'bg-mint-01', + 'bg-color-palette-blue1', + 'bg-color-palette-blue2', + 'bg-color-palette-purple1', + 'bg-color-palette-pink', + 'bg-gray-bg-07', +]; diff --git a/src/shared/constants/recommendSites.ts b/src/shared/constants/recommendSites.ts new file mode 100644 index 00000000..23993a03 --- /dev/null +++ b/src/shared/constants/recommendSites.ts @@ -0,0 +1,15 @@ +/* api 테스트용 */ +export const recommendServices = [ + { serviceName: 'naver', url: 'https://www.naver.com' }, + { serviceName: 'tailwind', url: 'https://tailwindcss.com' }, + { serviceName: 'github', url: 'https://github.com' }, + { serviceName: 'google', url: 'https://www.google.com' }, + { serviceName: 'youtube', url: 'https://www.youtube.com' }, + { serviceName: 'notion', url: 'https://www.notion.so' }, + { serviceName: 'naver', url: 'https://www.naver.com/1332' }, + { serviceName: 'tailwind', url: 'https://tailwindcss.com/Q4' }, + { serviceName: 'github', url: 'https://github.com/ADFADS' }, + { serviceName: 'google', url: 'https://www.google.com/ADFDAFS' }, + { serviceName: 'youtube', url: 'https://www.youtube.com/ADFASF' }, + { serviceName: 'notion', url: 'https://www.notion.so/ASDF' }, +]; diff --git a/src/shared/constants/tabSelections.ts b/src/shared/constants/tabSelections.ts index 767da920..70d667fd 100644 --- a/src/shared/constants/tabSelections.ts +++ b/src/shared/constants/tabSelections.ts @@ -1,4 +1,4 @@ export const CATEGORY_MODALTABS = [ - { id: 1, name: '기존 모립 세트' }, - { id: 2, name: '현재 열린 탭' }, + { id: 1, name: '웹사이트' }, + { id: 2, name: '앱' }, ]; diff --git a/src/shared/layout/components/Sidebar/Sidebar.tsx b/src/shared/layout/components/Sidebar/Sidebar.tsx index 64b32749..925df376 100644 --- a/src/shared/layout/components/Sidebar/Sidebar.tsx +++ b/src/shared/layout/components/Sidebar/Sidebar.tsx @@ -1,26 +1,37 @@ import { useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; import ModalWrapper, { ModalWrapperRef } from '@/shared/components/ModalWrapper'; +import { ROUTES_CONFIG } from '@/router/routesConfig'; + import { FolderIcon, GearIcon, LogoIcon } from './assets/svgs'; import { ModalContentsSetting } from './components/ModalContents'; const Sidebar = () => { const modalRef = useRef(null); + const navigate = useNavigate(); + const openSettings = () => { modalRef.current?.open(); }; + const navigateHome = () => { + navigate(ROUTES_CONFIG.home.path); + }; + const navigateMoribSet = () => { + navigate(ROUTES_CONFIG.allowedService.path); + }; return ( <>