diff --git a/index.html b/index.html index 5b45a52..a2dccdd 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + Vi.No diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..c8f46c2 Binary files /dev/null and b/public/favicon.ico differ diff --git a/src/apis/user.ts b/src/apis/user.ts index 3c06989..626280a 100644 --- a/src/apis/user.ts +++ b/src/apis/user.ts @@ -15,7 +15,7 @@ import { FindEmailRequest, FindPasswordResponse, FindPasswordRequest, - CreateVideoAlarmRequest + CreateVideoAlarmRequest, } from '@/models/user'; import { AlarmResponse, @@ -93,11 +93,8 @@ export const createVideoAlarmAPI = ( PREFIX + `/videoAlarm/${videoId}/${status}`, data, ); -} +}; -export const findPasswordAPI = (data : FindPasswordRequest) => { - return axios.post( - PREFIX + '/findPassword', - data - ); -} +export const findPasswordAPI = (data: FindPasswordRequest) => { + return axios.post(PREFIX + '/findPassword', data); +}; diff --git a/src/apis/videos.ts b/src/apis/videos.ts index b3175f8..e309cb1 100644 --- a/src/apis/videos.ts +++ b/src/apis/videos.ts @@ -1,5 +1,7 @@ import { APIBaseResponse, APIResponse } from '@/models/config/axios'; +import { ModelingFinalData } from '@/models/modeling'; import { + CreateVideoResponse, IVideo, UpdateVideoCategoryRequest, UpdateVideoRequest, @@ -13,8 +15,11 @@ import { IVideoProps } from 'types/videos'; const PREFIX = '/videos'; -export const createVideoAPI = (data: IVideo) => { - return axios.post>(PREFIX + `/new-video`, data); +export const createVideoAPI = (data: ModelingFinalData) => { + return axios.post>( + PREFIX + `/new-video`, + data, + ); }; export const getVideoAPI = ( @@ -24,6 +29,10 @@ export const getVideoAPI = ( return axios.get>(PREFIX + `/${videoId}/${versionId}`); }; +export const getDummyVideoAPI = (videoId: string | number) => { + return axios.get>(PREFIX + `/dummyVideos/${videoId}/get`); +}; + export const deleteVideos = async (videos: number[] | undefined) => { const response = await axiosInstance.delete('/videos/selectDelete', { data: { videos }, @@ -76,6 +85,17 @@ export const getUnReadDummyVideosAPI = () => { return axios.get>('/videos/dummyVideos/unRead'); }; +export const getAllDummyVideosAPI = () => { + return axios.get>('/videos/dummyVideos'); +}; + +export const getUnReadDummyVideos = async (): Promise< + APIResponse> +> => { + const response = await axiosInstance.get('/videos/dummyVideos/unRead'); + return response.data; +}; + export const getAllDummyVideos = async (): Promise< APIResponse> > => { diff --git a/src/assets/icons/dark-logo.svg b/src/assets/icons/dark-logo.svg new file mode 100644 index 0000000..5bbbe4e --- /dev/null +++ b/src/assets/icons/dark-logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/assets/icons/light-logo.svg b/src/assets/icons/light-logo.svg new file mode 100644 index 0000000..0bf9383 --- /dev/null +++ b/src/assets/icons/light-logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/assets/spinner.gif b/src/assets/spinner.gif new file mode 100644 index 0000000..6b2ca4b Binary files /dev/null and b/src/assets/spinner.gif differ diff --git a/src/components/Home/InsightVideos.tsx b/src/components/Home/InsightVideos.tsx index ec9a34a..f295aec 100644 --- a/src/components/Home/InsightVideos.tsx +++ b/src/components/Home/InsightVideos.tsx @@ -55,7 +55,7 @@ const InsightVideos: React.FC = ({ }, [userToken]); return ( - +

이런 인사이트는 어때요?

@@ -66,7 +66,7 @@ const InsightVideos: React.FC = ({ {dummyVideos.map((video) => ( { return ( - +
최근 읽은 영상 @@ -37,8 +37,15 @@ const RecentVideos = ({ videos, searchRef }: IRecentVideosProp) => { 처음 방문하셨나요?
아직 정리해본 영상이 없어요!
- searchRef?.current?.focus()}> -

영상 정리해보기

+ window.scrollTo({ top: 0, behavior: 'smooth' })} + > +

searchRef.current?.focus()} + > + 영상 정리해보기 +

)} diff --git a/src/components/Home/SearchYoutube.tsx b/src/components/Home/SearchYoutube.tsx index 779cb9b..43fa985 100644 --- a/src/components/Home/SearchYoutube.tsx +++ b/src/components/Home/SearchYoutube.tsx @@ -1,8 +1,5 @@ import React, { useState, FormEvent } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; - -import { createVideoAPI } from '@/apis/videos'; +import { useRecoilState, useSetRecoilState } from 'recoil'; import VideoIcon from '@/assets/icons/video.svg?react'; import WarningIcon from '@/assets/icons/warning.svg?react'; @@ -16,31 +13,30 @@ import { } from '@/styles/HomepageStyle'; import { recommendationModalState } from '@/stores/modal'; + import { modelingDataState, modelingProgressState, modelingStatusState, videoLinkState, } from '@/stores/model-controller'; -import { userTokenState } from '@/stores/user'; import { validateYoutubeLink } from '@/utils/validation'; import ProgressBar from './ProgressBar'; +import useCreateVideo from '@/hooks/useCreateVideo'; type Props = { searchRef: React.RefObject; }; const SearchYoutube = ({ searchRef }: Props) => { - const navigate = useNavigate(); - - const userToken = useRecoilValue(userTokenState); const setIsOpenModal = useSetRecoilState(recommendationModalState); + const setModelingData = useSetRecoilState(modelingDataState); const setVideoLink = useSetRecoilState(videoLinkState); const setProgress = useSetRecoilState(modelingProgressState); const [status, setStatus] = useRecoilState(modelingStatusState); - const [modelingData, setModelingData] = useRecoilState(modelingDataState); + const { createVideo } = useCreateVideo(); const [inputLink, setInputLink] = useState(''); @@ -96,27 +92,7 @@ const SearchYoutube = ({ searchRef }: Props) => { setVideoLink(null); setStatus('NONE'); setProgress(0); - }; - - const handleClickCreateVideoButton = async () => { - if (!modelingData) return; - - if (userToken) { - try { - const { video_id } = (await createVideoAPI(modelingData)).data.result; - - navigate(`/summary/${video_id}`); - setModelingData(null); - } catch (e) { - console.error(e); - } - } else { - navigate('/summary/guest'); - } - - setVideoLink(null); - setStatus('NONE'); - setProgress(0); + setModelingData(null); }; return ( @@ -124,7 +100,17 @@ const SearchYoutube = ({ searchRef }: Props) => {
-

{getTitle()}

+

+ {getTitle()} +

{(status === 'ERROR' || (!isValidate && inputLink !== '')) && ( @@ -156,12 +142,12 @@ const SearchYoutube = ({ searchRef }: Props) => {
@@ -172,7 +158,7 @@ const SearchYoutube = ({ searchRef }: Props) => { color: theme.color.gray500, backgroundColor: theme.color.green400, }} - onClick={handleClickCreateVideoButton} + onClick={createVideo} > 영상 읽기 diff --git a/src/components/PhoneCheck.tsx b/src/components/PhoneCheck.tsx index 0857d31..66e6879 100644 --- a/src/components/PhoneCheck.tsx +++ b/src/components/PhoneCheck.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import { sendSMSAPI, checkSMSAPI, sendSMSFindAPI } from '@/apis/sms'; import Container from '@/styles/PhoneCheck'; +import { AxiosError } from "axios"; interface PhoneCheckProps { @@ -100,7 +101,9 @@ const PhoneCheck : React.FC = ({setCheck, tel, setTel, type}) = setToken(data.result.token); } } catch(e){ - setPhoneCertify(true); + const err = e as AxiosError; + if(err.response?.status === 400) + setPhoneCertify(true); } } diff --git a/src/components/SearchPage/SearchResultBox.tsx b/src/components/SearchPage/SearchResultBox.tsx index 4013054..1bbf1b2 100644 --- a/src/components/SearchPage/SearchResultBox.tsx +++ b/src/components/SearchPage/SearchResultBox.tsx @@ -6,48 +6,77 @@ import { userInfoState } from '@/stores/user'; import { useRecoilValue } from 'recoil'; interface SearchResultProp { - video : IVideo; - tags : string[]; + video: IVideo; + tags: string[]; } -const SearchResultBox : React.FC= ({video, tags}) => { - const nav = useNavigate(); - const userName = useRecoilValue(userInfoState); - const date = video.created_at.toString().split('T')[0].split('-'); - const handleImg = (event : React.SyntheticEvent) => { - const target = event.target as HTMLImageElement; - target.style.display = 'none'; - } - const handleOnclick = () => { - nav(`/summary/${video.video_id}?insight=${userName?.name === video.user}`); - } - - return ( - -
-
- {video.user} - - - {`${date[0]}년 ${date[1]}월 ${date[2]}일`} - -
-
-
-
-
-
-
- {video.tag.map((item, index) => - {item.name} - )} -
-
-
- handleImg(event)}> -
-
- ); -} +const SearchResultBox: React.FC = ({ video, tags }) => { + const nav = useNavigate(); + const userName = useRecoilValue(userInfoState); + const date = video.created_at.toString().split('T')[0].split('-'); + const handleImg = (event: React.SyntheticEvent) => { + const target = event.target as HTMLImageElement; + target.style.display = 'none'; + }; + const handleOnclick = () => { + nav(`/summary/${video.video_id}?insight=${userName?.name !== video.user}`); + }; + + return ( + +
+
+ + {video.user} + + + + {`${date[0]}년 ${date[1]}월 ${date[2]}일`} + +
+
+
+
+
+
+
+ {video.tag.map((item, index) => ( + + # {item.name} + + ))} +
+
+
+ handleImg(event)}> +
+
+ ); +}; -export default SearchResultBox; \ No newline at end of file +export default SearchResultBox; diff --git a/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategorySelectBox.tsx b/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategorySelectBox.tsx index cd9acfe..f6c7079 100644 --- a/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategorySelectBox.tsx +++ b/src/components/SummaryPage/SummaryDetailBox/CategorySelectBox/CategorySelectBox.tsx @@ -12,18 +12,16 @@ import { userTokenState } from '@/stores/user'; import { CategoryDropdown } from './CategoryDropdown'; type Props = { + size?: 'SMALL' | 'LARGE'; disabled?: boolean; selectedCategoryId?: number; - startSelect?: boolean; - setStartSelect?: React.Dispatch>; onSelect: (categoryId: number, categoryName?: string) => void; }; const CategorySelectBox = ({ + size, disabled, selectedCategoryId, - startSelect, - setStartSelect, onSelect, }: Props) => { const userToken = useRecoilValue(userTokenState); @@ -59,17 +57,11 @@ const CategorySelectBox = ({ const handleSelect = (categoryId: number) => { setSelectedId(categoryId); - setStartSelect && setStartSelect(true); setIsOpen(false); }; const handleClick = () => { - if ( - !selectedCategory || - selectedId === selectedCategoryId || - disabled || - !startSelect - ) + if (!selectedCategory || selectedId === selectedCategoryId || disabled) return; onSelect(selectedCategory.categoryId, selectedCategory.name); @@ -92,7 +84,9 @@ const CategorySelectBox = ({ {userToken ? selectedCategory ? selectedCategory.name - : '어떤 카테고리에 넣을까요?' + : size === 'SMALL' + ? '카테고리 선택' + : '어떤 카테고리에 넣을까요?' : '로그인하고 요약한 영상을 아카이빙해요!'} @@ -105,9 +99,9 @@ const CategorySelectBox = ({
diff --git a/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx b/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx index 0c9e9ed..5e74e74 100644 --- a/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx +++ b/src/components/SummaryPage/SummaryDetailBox/SummaryDetailBox.tsx @@ -89,6 +89,14 @@ const SummaryDetailBox = ({ onRefresh }: Props) => { } }; + useEffect(() => { + return () => { + setSummaryVideoTime(0); + setPlaySubHeadingId(-1); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { if (player.current) return; @@ -98,10 +106,6 @@ const SummaryDetailBox = ({ onRefresh }: Props) => { window.onmessage = handleMessage; - return () => { - setSummaryVideoTime(0); - setPlaySubHeadingId(-1); - }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [summaryVideo]); diff --git a/src/components/SummaryPage/SummaryScriptBox/ScriptViewer/ScriptViewer.tsx b/src/components/SummaryPage/SummaryScriptBox/ScriptViewer/ScriptViewer.tsx index ecbbefe..7ccd19b 100644 --- a/src/components/SummaryPage/SummaryScriptBox/ScriptViewer/ScriptViewer.tsx +++ b/src/components/SummaryPage/SummaryScriptBox/ScriptViewer/ScriptViewer.tsx @@ -67,7 +67,7 @@ const ScriptViewer = ({ keyword }: Props) => { return (
{scriptList.map((script) => ( -
+
{ -
+
{isEditingView ? : }
diff --git a/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx b/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx index 9b04cec..c6d608c 100644 --- a/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx +++ b/src/components/SummaryPage/SummaryScriptBox/ToolBox/Indicator.tsx @@ -1,22 +1,83 @@ -// 임시 타입 -interface Item { - id: number; -} - -type Props = { - list: Item[]; - focusId: number; - onChange: (focusId: number) => void; -}; +import { useEffect, useState } from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import { IVideo } from '@/models/video'; + +import { + summaryPlaySubHeadingIdState, + summaryVideoState, +} from '@/stores/summary'; + +const Indicator = () => { + const summaryVideo = useRecoilValue(summaryVideoState) as IVideo; + const [playSubHeadingId, setPlaySubHeadingId] = useRecoilState( + summaryPlaySubHeadingIdState, + ); + + const [focusId, setFocusId] = useState(-1); + + useEffect(() => { + const handleScroll = () => { + const { top: containerTop } = container.getBoundingClientRect(); + const list = Array.from(document.querySelectorAll('.script-box')) + .map((el) => el.getBoundingClientRect().top - containerTop) + .filter((top) => top < 100); + // window.innerHeight * 0.3 + + (document.querySelector('.tools') as HTMLDivElement).style.boxShadow = + `0 4px 40px 0 rgba(0, 0, 0, ${container.scrollTop > 0 ? 0.05 : 0})`; + + if (list.length) { + const { id } = summaryVideo.subHeading[list.length - 1]; + + setFocusId(id); + } + }; + + if (summaryVideo.subHeading.length) { + setFocusId(summaryVideo.subHeading[0].id); + } + + const container = document.querySelector('#script-box') as HTMLDivElement; + container.addEventListener('scroll', handleScroll); + + return () => { + container.removeEventListener('scroll', handleScroll); + }; + }, [summaryVideo]); + + useEffect(() => { + if (playSubHeadingId < 0) return; + + const findIdx = summaryVideo.subHeading.findIndex( + (s) => s.id === playSubHeadingId, + ); + + if (findIdx > -1) { + const container = document.querySelector('#script-box') as HTMLDivElement; + const element = document.querySelectorAll('.script-box')[findIdx]; + + if (element) { + const { top: containerTop } = container.getBoundingClientRect(); + const { top } = element.getBoundingClientRect(); + + container.scrollTo({ + top: container.scrollTop + top - containerTop - 20, + behavior: 'smooth', + }); + } + } + + setFocusId(playSubHeadingId); + }, [playSubHeadingId, summaryVideo]); -const Indicator = ({ list, focusId, onChange }: Props) => { return (
- {list.map((item) => ( + {summaryVideo.subHeading.map((item) => (
onChange(item.id)} + onClick={() => setPlaySubHeadingId(item.id)} /> ))}
diff --git a/src/components/SummaryPage/SummaryScriptBox/ToolBox/ToolBox.tsx b/src/components/SummaryPage/SummaryScriptBox/ToolBox/ToolBox.tsx index f219031..1056b81 100644 --- a/src/components/SummaryPage/SummaryScriptBox/ToolBox/ToolBox.tsx +++ b/src/components/SummaryPage/SummaryScriptBox/ToolBox/ToolBox.tsx @@ -5,6 +5,8 @@ import { getVideoAPI, updateVideoAPI } from '@/apis/videos'; import ModifyIcon from '@/assets/icons/modify.svg?react'; +import useCreateToast from '@/hooks/useCreateToast'; + import { IVideo } from '@/models/video'; import { @@ -17,7 +19,6 @@ import { import Indicator from './Indicator'; import { SearchKeyword } from './SearchKeyword'; import { ChangeKeyword } from './ChangeKeyword'; -import useCreateToast from '@/hooks/useCreateToast'; type Props = { onRefresh: () => void; @@ -33,10 +34,11 @@ const ToolBox = ({ onRefresh, onChangeKeyword }: Props) => { const [isEditingView, setIsEditingView] = useRecoilState( summaryIsEditingViewState, ); - const { createToast } = useCreateToast(); const [originalSummary, setOriginalSummary] = useState(null); + const { createToast } = useCreateToast(); + const handleClickModifyIcon = () => { setPlaySubHeadingId(-1); setIsEditingView(true); @@ -125,11 +127,7 @@ const ToolBox = ({ onRefresh, onChangeKeyword }: Props) => { ) : ( <> - {}} - /> +
diff --git a/src/components/category/Card.tsx b/src/components/category/Card.tsx index ea7e021..06bbccd 100644 --- a/src/components/category/Card.tsx +++ b/src/components/category/Card.tsx @@ -1,13 +1,14 @@ -import React, { useState } from 'react'; +import React from 'react'; import { useRecoilValue } from 'recoil'; import { IVideoProps } from 'types/videos'; import { CategorySelectBox } from '@/components/SummaryPage/SummaryDetailBox/CategorySelectBox'; -import { categoryState } from '@/stores/category'; +import { userTokenState } from '@/stores/user'; import * as CardStyles from '@/styles/category/Card.style'; -import Chip from '../common/chip/Chip'; + +import Chip from '@/components/common/chip/Chip'; interface ICardProps { mode: 'default' | 'category' | 'recommend'; @@ -28,15 +29,9 @@ const Card: React.FC = ({ setCheckedVideos, onFileClick, }) => { - const category = useRecoilValue(categoryState); - - const [selectedCategoryId, setSelectedCategoryId] = useState( - category.length ? category[0].categoryId : -1, - ); - const [startSelect, setStartSelect] = useState(false); + const userToken = useRecoilValue(userTokenState); const onFileClickWithProps = (categoryId: number, categoryName?: string) => { - setSelectedCategoryId(categoryId); onFileClick && onFileClick(video.video_id, categoryId, categoryName); }; @@ -48,7 +43,7 @@ const Card: React.FC = ({ } }; return ( - +
{mode === 'category' && ( @@ -65,9 +60,7 @@ const Card: React.FC = ({
{video.title} {video.description} @@ -77,12 +70,11 @@ const Card: React.FC = ({ ))} - {mode === 'recommend' && ( + {mode === 'recommend' && userToken && ( diff --git a/src/components/common/ModelController/ModelController.tsx b/src/components/common/ModelController/ModelController.tsx index 90de4ea..6dab468 100644 --- a/src/components/common/ModelController/ModelController.tsx +++ b/src/components/common/ModelController/ModelController.tsx @@ -1,11 +1,5 @@ import { useEffect, useRef } from 'react'; -import { useRecoilState, useSetRecoilState } from 'recoil'; - -import { - modelingProcess1, - modelingProcess2, - modelingProcess3, -} from '@/apis/video'; +import { useRecoilState, useSetRecoilState, useRecoilValue } from 'recoil'; import { modelingDataState, @@ -13,10 +7,18 @@ import { modelingStatusState, videoLinkState, } from '@/stores/model-controller'; +import { + modelingProcess1, + modelingProcess2, + modelingProcess3, +} from '@/apis/video'; +import { userTokenState } from '@/stores/user'; + import { createVideoAlarmAPI } from '@/apis/user'; const ModelController = () => { const interval = useRef(); + const userToken = useRecoilValue(userTokenState); const [videoLink, setVideoLink] = useRecoilState(videoLinkState); const setModelingStatus = useSetRecoilState(modelingStatusState); const setModelingProgress = useSetRecoilState(modelingProgressState); @@ -32,11 +34,13 @@ const ModelController = () => { const handleError = async () => { try { - await createVideoAlarmAPI(0, 'fail', { - title: '앗, 영상 변환 중 오류가 생겼어요', - content: '어떤 문제인지 확인해보세요!', - is_confirm: false, - }); + if (userToken) { + await createVideoAlarmAPI(0, 'fail', { + title: '앗, 영상 변환 중 오류가 생겼어요', + content: '어떤 문제인지 확인해보세요!', + is_confirm: false, + }); + } } catch (e) { console.error(e); } @@ -45,6 +49,8 @@ const ModelController = () => { clearInterval(interval.current); } + clearInterval(interval.current); + setModelingStatus('ERROR'); setVideoLink(null); }; @@ -53,6 +59,8 @@ const ModelController = () => { if (!videoLink) return; const callProcess1API = async () => { + setModelingProgress(0); + try { const { videoId } = (await modelingProcess1(videoLink)).data.result; @@ -83,11 +91,14 @@ const ModelController = () => { try { const { finalData } = (await modelingProcess3({ videoId })).data.result; - if (interval.current) { - clearInterval(interval.current); - } + clearInterval(interval.current); - setModelingData(finalData); + setModelingData({ + ...finalData, + youtube_id: videoId, + created_at: new Date().toString(), + updated_at: new Date().toString(), + }); setModelingProgress(100); setModelingStatus('COMPLETE'); } catch (e) { @@ -98,7 +109,6 @@ const ModelController = () => { }; setModelingStatus('CONTINUE'); - setModelingProgress(Math.ceil(Math.random() * 5)); callProcess1API(); startInterval(); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 00d149e..b828841 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -1,5 +1,5 @@ import { Outlet, useLocation } from 'react-router-dom'; -import { useRecoilValue } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { isSideBarOpenState } from '@/stores/ui'; @@ -7,13 +7,17 @@ import Footer from './footer/Footer'; import Header from './header'; import SideBar from './sideBar'; import NicknameModal from '@/components/NicknameModal'; +import ErrorModal from '../modals/ErrorModal'; + import { useMemo, useEffect } from 'react'; import { userInfoState } from '@/stores/user'; +import { errorModalState } from '@/stores/modal'; const Layout = () => { const { pathname } = useLocation(); const isSideBarOpen = useRecoilValue(isSideBarOpenState); const userInfo = useRecoilValue(userInfoState); + const isErrorModalOpen = useRecoilValue(errorModalState) const isShowFooter = useMemo( () => pathname === '/' || /^(\/category)/g.test(pathname), @@ -38,6 +42,7 @@ const Layout = () => { {isShowFooter &&
} {userInfo && userInfo.nick_name === '' && } + {isErrorModalOpen && } ); }; diff --git a/src/components/layout/header/alarm/AlarmItem.tsx b/src/components/layout/header/alarm/AlarmItem.tsx index d026ac1..83b66dd 100644 --- a/src/components/layout/header/alarm/AlarmItem.tsx +++ b/src/components/layout/header/alarm/AlarmItem.tsx @@ -11,12 +11,20 @@ import ErrorImage from '@/assets/Error.png'; import { Container } from '@/styles/layout/header/alarm/AlarmItem.style'; import { diffTime } from '@/utils/date'; +import { useRecoilValue } from 'recoil'; +import { + modelingProgressState, + modelingStatusState, +} from '@/stores/model-controller'; +import theme from '@/styles/theme'; +import { confirmSelectAlarmAPI } from '@/apis/user'; type Props = { alarm: IAlarm; selectIdList: number[]; onUpdateSelectIdList: (list: number[]) => void; onClose: () => void; + onRefresh: () => void; }; const AlarmItem = ({ @@ -24,8 +32,11 @@ const AlarmItem = ({ selectIdList, onUpdateSelectIdList, onClose, + onRefresh, }: Props) => { const navigate = useNavigate(); + const status = useRecoilValue(modelingStatusState); + const progress = useRecoilValue(modelingProgressState); const isSelected = selectIdList.indexOf(alarm.alarm_id) > -1; const type = () => { @@ -58,11 +69,26 @@ const AlarmItem = ({ return `${second}초`; }; - const handleClick = () => { + const handleClick = async () => { + if (!alarm.is_confirm) { + try { + await confirmSelectAlarmAPI({ alarms: [alarm.alarm_id] }); + onRefresh(); + } catch (e) { + console.error(e); + } + } if (alarm.type === 'notice') { navigate('/guide'); - onClose(); } + if ( + alarm.type === 'video' && + alarm.state === 'success' && + alarm.alarm_id !== 999 + ) { + navigate(`/summary/${alarm.video_id}`); + } + onClose(); }; const handleClickRemoveButton: React.MouseEventHandler = ( @@ -110,6 +136,23 @@ const AlarmItem = ({ {alarm.content}
+ {status !== 'NONE' && alarm.alarm_id === 999 && ( +
+
+
+
+ + + {status === 'ERROR' ? '변환 중 오류' : `${progress}%`} + +
+ )} ); }; diff --git a/src/components/layout/header/alarm/AlarmList.tsx b/src/components/layout/header/alarm/AlarmList.tsx index 72336a5..41b5a53 100644 --- a/src/components/layout/header/alarm/AlarmList.tsx +++ b/src/components/layout/header/alarm/AlarmList.tsx @@ -75,6 +75,7 @@ const AlarmList = ({ alarmList, onRefresh, onClose }: Props) => { selectIdList={selectIdList} onUpdateSelectIdList={setSelectIdList} onClose={onClose} + onRefresh={onRefresh} /> ))}
diff --git a/src/components/layout/header/alarm/index.tsx b/src/components/layout/header/alarm/index.tsx index 365177c..b60c51d 100644 --- a/src/components/layout/header/alarm/index.tsx +++ b/src/components/layout/header/alarm/index.tsx @@ -1,20 +1,18 @@ import { useEffect, useState } from 'react'; -import { useRecoilValue } from 'recoil'; - -import { getAlarmAPI } from '@/apis/user'; +import { useRecoilState, useRecoilValue } from 'recoil'; import NotifyOffIcon from '@/assets/icons/notify-off.svg?react'; import NotifyOnIcon from '@/assets/icons/notify-on.svg?react'; import useOutsideClick from '@/hooks/useOutsideClick'; -import { IAlarm } from '@/models/alarm'; - import { modelingStatusState } from '@/stores/model-controller'; import * as HeaderStyle from '@/styles/layout/header'; import AlarmList from './AlarmList'; +import { userAlarmState } from '@/stores/user'; +import useGetAlarm from '@/hooks/useGetAlarm'; type Props = { isDark: boolean; @@ -23,26 +21,39 @@ type Props = { const Alarm = ({ isDark }: Props) => { const status = useRecoilValue(modelingStatusState); const [isOpen, setIsOpen] = useState(false); - const [alarmList, setAlarmList] = useState([]); + const [alarmList, setAlarmList] = useRecoilState(userAlarmState); + const { getAlarm } = useGetAlarm(); const [alarmRef] = useOutsideClick(() => setIsOpen(false)); const hasNotReadAlarm = alarmList.find((item) => !item.is_confirm); - const callAPI = async () => { - const { alarms } = (await getAlarmAPI()).data.result; - - setAlarmList(alarms); - }; - useEffect(() => { - callAPI(); + getAlarm(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { - if (status === 'ERROR') { - callAPI(); + if (status === 'ERROR' || status === 'COMPLETE') { + getAlarm(); + } + + if (status === 'CONTINUE') { + setAlarmList([ + { + state: 'success', + type: 'video', + alarm_id: 999, + title: '열심히 영상을 변환하는 중이에요!', + content: '잠시후 멋진 글을 만날 수 있어요:)', + is_confirm: 0, + created_at: new Date().toString(), + updated_at: new Date().toString(), + }, + ...alarmList, + ]); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [status]); return ( @@ -61,7 +72,7 @@ const Alarm = ({ isDark }: Props) => { {isOpen && ( setIsOpen(false)} /> )} diff --git a/src/components/layout/header/index.tsx b/src/components/layout/header/index.tsx index d2bf1f4..44daa8d 100644 --- a/src/components/layout/header/index.tsx +++ b/src/components/layout/header/index.tsx @@ -5,8 +5,8 @@ import { useRecoilState, useRecoilValue } from 'recoil'; import CloseIcon from '@/assets/icons/close.svg?react'; import MenuIcon from '@/assets/icons/menu.svg?react'; import SearchIcon from '@/assets/icons/search-light.svg?react'; -import DarkLogoImage from '@/assets/logo-dark.png'; -import LightLogoImage from '@/assets/logo-light.png'; +import DarkLogoIcon from '@/assets/icons/dark-logo.svg?react'; +import LightLogoIcon from '@/assets/icons/light-logo.svg?react'; import * as HeaderStyle from '@/styles/layout/header'; @@ -71,10 +71,7 @@ const Header = () => { - Logo + {isDarkSection ? : } diff --git a/src/components/layout/sideBar/AddCategory.tsx b/src/components/layout/sideBar/AddCategory.tsx index 210aa73..909c334 100644 --- a/src/components/layout/sideBar/AddCategory.tsx +++ b/src/components/layout/sideBar/AddCategory.tsx @@ -1,18 +1,22 @@ import * as AddCategoryStyle from '@/styles/layout/sideBar/AddCategory.style'; import PlusSvg from '@/assets/icons/plus.svg?react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { topCategoryModalState } from '@/stores/modal'; +import { addCategoryModalState } from '@/stores/modal'; import { userTokenState } from '@/stores/user'; import { useState } from 'react'; import NoticeModal from '@/components/modals/NoticeModal'; const AddCategory = () => { const isUser = useRecoilValue(userTokenState); - const setTopCategoryModal = useSetRecoilState(topCategoryModalState); + const setIsAddCategoryModalOpen = useSetRecoilState(addCategoryModalState); const [isNoticeModalOpen, setIsNoticeModalOpen] = useState(false); const openAddModal = (e: React.MouseEvent) => { - setTopCategoryModal(true); + setIsAddCategoryModalOpen({ + location: 'top', + isOpen: true, + categoryId: -1, + }); e.stopPropagation(); }; diff --git a/src/components/layout/sideBar/ConvertVideo.tsx b/src/components/layout/sideBar/ConvertVideo.tsx index aa5de8d..aee3b06 100644 --- a/src/components/layout/sideBar/ConvertVideo.tsx +++ b/src/components/layout/sideBar/ConvertVideo.tsx @@ -1,8 +1,6 @@ import { useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; - -import { createVideoAPI } from '@/apis/videos'; +import { useLocation } from 'react-router-dom'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import VideoSvg from '@/assets/icons/video.svg?react'; import DownSvg from '@/assets/icons/down.svg?react'; @@ -13,27 +11,23 @@ import { CommonTitle } from '@/styles/layout/sideBar/UserMode.style'; import { recommendationModalState } from '@/stores/modal'; import { - modelingDataState, modelingProgressState, modelingStatusState, videoLinkState, } from '@/stores/model-controller'; -import { userTokenState } from '@/stores/user'; import theme from '@/styles/theme'; import { validateYoutubeLink } from '@/utils/validation'; +import useCreateVideo from '@/hooks/useCreateVideo'; const ConvertVideo = () => { const { pathname } = useLocation(); - const navigate = useNavigate(); - - const userToken = useRecoilValue(userTokenState); const setIsOpenModal = useSetRecoilState(recommendationModalState); const setVideoLink = useSetRecoilState(videoLinkState); - const [status, setStatus] = useRecoilState(modelingStatusState); - const [progress, setProgress] = useRecoilState(modelingProgressState); - const [modelingData, setModelingData] = useRecoilState(modelingDataState); + const status = useRecoilValue(modelingStatusState); + const progress = useRecoilValue(modelingProgressState); + const { createVideo } = useCreateVideo(); const [isOpen, setIsOpen] = useState(false); const [isFocus, setIsFocus] = useState(false); @@ -41,27 +35,6 @@ const ConvertVideo = () => { const isValidate = validateYoutubeLink(url); - const handleClickCreateVideoButton = async () => { - if (!modelingData) return; - - if (userToken) { - try { - const { video_id } = (await createVideoAPI(modelingData)).data.result; - - navigate(`/summary/${video_id}`); - setModelingData(null); - } catch (e) { - console.error(e); - } - } else { - navigate('/summary/guest'); - } - - setVideoLink(null); - setStatus('NONE'); - setProgress(0); - }; - const handleClickStartConvertButton: React.MouseEventHandler< HTMLButtonElement > = (e) => { @@ -112,7 +85,7 @@ const ConvertVideo = () => { color: theme.color.gray500, backgroundColor: theme.color.green400, }} - onClick={handleClickCreateVideoButton} + onClick={createVideo} > start diff --git a/src/components/layout/sideBar/SubCategory.tsx b/src/components/layout/sideBar/SubCategory.tsx index dfcca2c..c248f90 100644 --- a/src/components/layout/sideBar/SubCategory.tsx +++ b/src/components/layout/sideBar/SubCategory.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import * as SubCategoryStyles from '@/styles/layout/sideBar/SubCategory.style'; import Option from './Option'; import handleDrag from '@/utils/handleDrag'; -import { ISubFolderProps } from 'types/category'; +import { IEditProps, ISubFolderProps } from 'types/category'; import EditCategoryName from '@/components/category/EditCategoryName'; import useCreateToast from '@/hooks/useCreateToast'; @@ -13,10 +13,8 @@ interface ISubCategoryProps { subId: number; subFolder: ISubFolderProps; grabedCategory: React.MutableRefObject; - isEditing: { activated: boolean; categoryId: number }; - setIsEditing: React.Dispatch< - React.SetStateAction<{ activated: boolean; categoryId: number }> - >; + isEditing: IEditProps; + setIsEditing: React.Dispatch>; setIsDeleteModalOpen: React.Dispatch>; putCategoryFolder: () => void; setCategoryId: React.Dispatch>; diff --git a/src/components/layout/sideBar/TopCategory.tsx b/src/components/layout/sideBar/TopCategory.tsx index d717eba..9493cfe 100644 --- a/src/components/layout/sideBar/TopCategory.tsx +++ b/src/components/layout/sideBar/TopCategory.tsx @@ -7,7 +7,12 @@ import useOutsideClick from '@/hooks/useOutsideClick'; import SubCategory from './SubCategory'; import Option from './Option'; import handleDrag from '@/utils/handleDrag'; -import { IFolderProps, ISubFolderProps } from 'types/category'; +import { + IAddCategoryModalProps, + IEditProps, + IFolderProps, + ISubFolderProps, +} from 'types/category'; import EditCategoryName from '@/components/category/EditCategoryName'; interface ITopCategoryProps { @@ -17,11 +22,11 @@ interface ITopCategoryProps { grabedCategory: React.MutableRefObject; dropedCategory: React.MutableRefObject; category: IFolderProps; - isEditing: { activated: boolean; categoryId: number }; - setIsEditing: React.Dispatch< - React.SetStateAction<{ activated: boolean; categoryId: number }> + isEditing: IEditProps; + setIsEditing: React.Dispatch>; + setIsAddCategoryModalOpen: React.Dispatch< + React.SetStateAction >; - setIsSubCategoryModalOpen: React.Dispatch>; setIsDeleteModalOpen: React.Dispatch>; setCategoryId: React.Dispatch>; putCategoryFolder: () => void; @@ -36,7 +41,7 @@ const TopCategory = ({ category, isEditing, setIsEditing, - setIsSubCategoryModalOpen, + setIsAddCategoryModalOpen, setIsDeleteModalOpen, putCategoryFolder, setCategoryId, @@ -53,7 +58,12 @@ const TopCategory = ({ const handleOptionClick = (e: React.MouseEvent, option: string) => { e.stopPropagation(); if (option === '추가') { - setIsSubCategoryModalOpen(true); + console.log(category.categoryId); + setIsAddCategoryModalOpen({ + location: 'sub', + isOpen: true, + categoryId: category.categoryId, + }); } else if (option === '수정') { setIsEditing({ activated: true, categoryId: category.categoryId }); setBeforeEdit(edit); @@ -101,7 +111,6 @@ const TopCategory = ({ return ( <> { - const isTopCategoryModalOpen = useRecoilValue(topCategoryModalState); + const [isAddCategoryModalOpen, setIsAddCategoryModalOpen] = useRecoilState( + addCategoryModalState, + ); const [isSuccessAddCategoryModalOpen, setIsSuccessAddCategoryModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -24,7 +26,6 @@ const UserMode = () => { const [categoryId, setCategoryId] = useState(null); const [to, setTo] = useState(''); const categories = useRecoilValue(categoryState); - const [isSubCategoryModalOpen, setIsSubCategoryModalOpen] = useState(false); const grabedCategory = useRef(undefined); const dropedCategory = useRef(undefined); const { createToast } = useCreateToast(); @@ -96,7 +97,7 @@ const UserMode = () => { category={category} isEditing={isEditing} setIsEditing={setIsEditing} - setIsSubCategoryModalOpen={setIsSubCategoryModalOpen} + setIsAddCategoryModalOpen={setIsAddCategoryModalOpen} setIsDeleteModalOpen={setIsDeleteModalOpen} putCategoryFolder={putCategoryFolder} setCategoryId={setCategoryId} @@ -104,14 +105,11 @@ const UserMode = () => { /> ))} - {(isTopCategoryModalOpen || isSubCategoryModalOpen) && ( + {isAddCategoryModalOpen.isOpen && ( )} diff --git a/src/components/loadingSpinner/loadingSpinner.tsx b/src/components/loadingSpinner/loadingSpinner.tsx new file mode 100644 index 0000000..2d2f60e --- /dev/null +++ b/src/components/loadingSpinner/loadingSpinner.tsx @@ -0,0 +1,41 @@ +import { useEffect } from 'react'; +import Spinner from '@/assets/spinner.gif'; +import Container from '@/styles/loadingSpinner'; + +interface LoadingSpinnerProps { + isCroll : boolean; + setLoading : (value : boolean) => void; + time : number; +} + +const LoadingSpinner : React.FC = ({isCroll, setLoading, time}) => { + useEffect(() => { + if(isCroll){ + const timer = setTimeout(() => { + setLoading(false); + }, time); + return () => clearTimeout(timer); + } + }, []); + + const randMsg = ['지금 이 서비스는 한 달 만에 개발된 서비스랍니다!', + '바꾸기 버튼으로 한 번에 원하는 단어를 바꿀 수 있어요!', '해시태그는 한번에 3개까지 섞어 검색할 수 있어요!', + '진행바에 마우스를 올리면 몇 %까지 진행됐는지 알 수 있어요!', '카테고리는 당겨서 이동시킬 수 있어요!']; + const randomElement = randMsg[Math.floor(Math.random() * randMsg.length)]; + + return ( + +
+
+ spinner-img +
+ 그거 아셨나요? + {randomElement} +
+
+
+
+ ); +} + +export default LoadingSpinner; \ No newline at end of file diff --git a/src/components/modals/AddCategoryModal.tsx b/src/components/modals/AddCategoryModal.tsx index fccc42f..78f8c6d 100644 --- a/src/components/modals/AddCategoryModal.tsx +++ b/src/components/modals/AddCategoryModal.tsx @@ -1,6 +1,6 @@ import useOutsideClick from '@/hooks/useOutsideClick'; -import { topCategoryModalState } from '@/stores/modal'; -import { useSetRecoilState } from 'recoil'; +import { addCategoryModalState } from '@/stores/modal'; +import { useRecoilState } from 'recoil'; import OpenFileSvg from '@/assets/icons/open-file.svg?react'; import CloseSvg from '@/assets/icons/close.svg?react'; import * as AddTopCategoryModalStyles from '@/styles/modals/AddCategoryModal.style'; @@ -17,22 +17,18 @@ import useUpdateCategories from '@/hooks/useUpdateCategories'; import useCreateToast from '@/hooks/useCreateToast'; interface IAddTopCategoryModalProps extends ICommonModalProps { - isTopCategoryModalOpen: boolean; - setIsSubCategoryModalOpen: React.Dispatch>; - topCategoryId: number; setTo: React.Dispatch>; } const AddCategoryModal = ({ - isTopCategoryModalOpen, - setIsSubCategoryModalOpen, categoryName, setCategoryName, setIsSuccessAddCategoryModalOpen, - topCategoryId, setTo, }: IAddTopCategoryModalProps) => { - const setIsTopCategoryModalOpen = useSetRecoilState(topCategoryModalState); + const [isAddCategoryModalOpen, setIsAddCategoryModalOpen] = useRecoilState( + addCategoryModalState, + ); const { createToast } = useCreateToast(); const { updateCategories } = useUpdateCategories(); const { editText } = handleEdit(); @@ -42,13 +38,14 @@ const AddCategoryModal = ({ const categoryNameRegex = /^[a-zA-Z0-9가-힣\s]*$/; const checkCategoryNameRegex = categoryNameRegex.test(categoryName); const addEnabled = categoryName.length > 0 && checkCategoryNameRegex; + const isTopAdd = isAddCategoryModalOpen.location === 'top'; - const onCloseModal = () => { - isTopCategoryModalOpen - ? setIsTopCategoryModalOpen(false) - : setIsSubCategoryModalOpen(false); - setCategoryName(''); - }; + const onCloseModal = () => + setIsAddCategoryModalOpen({ + location: '', + isOpen: false, + categoryId: -1, + }); const [topCategoryModalRef] = useOutsideClick(onCloseModal); @@ -60,13 +57,13 @@ const AddCategoryModal = ({ createToast(`'기타' 이름은 사용하실 수 없어요`); return; } - const response = isTopCategoryModalOpen + const response = isTopAdd ? await postTopCategroy(categoryName) - : await postSubCategroy(categoryName, topCategoryId); + : await postSubCategroy(categoryName, isAddCategoryModalOpen.categoryId); if (response.isSuccess) { updateCategories(); setTo( - isTopCategoryModalOpen + isTopAdd ? `${response.result.categoryId}` : `${response.result.topCategoryId}/${response.result.categoryId}`, ); @@ -83,7 +80,7 @@ const AddCategoryModal = ({ - {isTopCategoryModalOpen ? '상위' : '하위'} 카테고리 추가 + {isTopAdd ? '상위' : '하위'} 카테고리 추가 만들고 싶은 카테고리의 이름을 작성해주세요 diff --git a/src/components/modals/ErrorModal.tsx b/src/components/modals/ErrorModal.tsx new file mode 100644 index 0000000..90182ab --- /dev/null +++ b/src/components/modals/ErrorModal.tsx @@ -0,0 +1,39 @@ +import { useSetRecoilState } from 'recoil'; +import { errorModalState } from '@/stores/modal'; +import useOutsideClick from '@/hooks/useOutsideClick'; + +import CloseIcon from '@/assets/icons/close.svg?react'; +import errorImg from '@/assets/Error.png'; +import { ErrorModalContainer } from '@/styles/modals/ErrorModal.style'; + +const ErrorModal = () => { + const setIsOpenModal = useSetRecoilState(errorModalState); + const [modalRef] = useOutsideClick(() => + setIsOpenModal(false), + ); + + return ( + +
+
+
setIsOpenModal(false)}> + +
+ +
+
+ errorImg +

영상 업로드 중 오류

+
+

업로드 중 알 수 없는 오류가 발생했어요
다시 시도해주세요

+
+
+ +
+
+ ); +}; + +export default ErrorModal; \ No newline at end of file diff --git a/src/components/modals/RecommendationModal.tsx b/src/components/modals/RecommendationModal.tsx index 209253a..21c32d5 100644 --- a/src/components/modals/RecommendationModal.tsx +++ b/src/components/modals/RecommendationModal.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; +import { useSetRecoilState, useRecoilValue } from 'recoil'; -import { getUnReadDummyVideosAPI } from '@/apis/videos'; +import { getAllDummyVideosAPI, getUnReadDummyVideosAPI } from '@/apis/videos'; import CloseIcon from '@/assets/icons/close.svg?react'; import TransformationIcon from '@/assets/icons/transformation.svg?react'; @@ -12,10 +12,12 @@ import useOutsideClick from '@/hooks/useOutsideClick'; import { IVideo } from '@/models/video'; import { recommendationModalState } from '@/stores/modal'; +import { userTokenState } from '@/stores/user'; import { RecommendationModalContainer } from '@/styles/modals/RecommendationModal.style'; const RecommendationModal = () => { + const userToken = useRecoilValue(userTokenState); const setIsOpenModal = useSetRecoilState(recommendationModalState); const [dummyVideo, setDummyVideo] = useState(); @@ -25,10 +27,17 @@ const RecommendationModal = () => { useEffect(() => { const callAPI = async () => { + let videos: IVideo[] = []; + try { - const { videos } = (await getUnReadDummyVideosAPI()).data.result; const random = Math.round(Math.random() * (videos.length - 1)); + if (userToken) { + videos = (await getUnReadDummyVideosAPI()).data.result.videos; + } else { + videos = (await getAllDummyVideosAPI()).data.result.videos; + } + setDummyVideo(videos[random]); } catch (e) { console.error(e); @@ -36,7 +45,7 @@ const RecommendationModal = () => { }; callAPI(); - }, [setDummyVideo]); + }, [userToken, setDummyVideo]); return ( diff --git a/src/hooks/useCreateVideo.ts b/src/hooks/useCreateVideo.ts new file mode 100644 index 0000000..75e6a6e --- /dev/null +++ b/src/hooks/useCreateVideo.ts @@ -0,0 +1,57 @@ +import { createVideoAlarmAPI } from '@/apis/user'; +import { createVideoAPI } from '@/apis/videos'; +import { errorModalState } from '@/stores/modal'; +import { + modelingDataState, + modelingProgressState, + modelingStatusState, + videoLinkState, +} from '@/stores/model-controller'; +import { userTokenState } from '@/stores/user'; +import { useNavigate } from 'react-router-dom'; +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import useGetAlarm from './useGetAlarm'; + +const useCreateVideo = () => { + const [modelingData, setModelingData] = useRecoilState(modelingDataState); + const userToken = useRecoilValue(userTokenState); + const setIsOpenErrorModal = useSetRecoilState(errorModalState); + const setVideoLink = useSetRecoilState(videoLinkState); + const setStatus = useSetRecoilState(modelingStatusState); + const setProgress = useSetRecoilState(modelingProgressState); + const { getAlarm } = useGetAlarm(); + const navigate = useNavigate(); + + const createVideo = async () => { + if (!modelingData) return; + + if (userToken) { + try { + const { video_id } = (await createVideoAPI(modelingData)).data.result; + + await createVideoAlarmAPI(video_id, 'success', { + title: `[${modelingData.title}]`, + content: + '영상이 모두 변환되었어요!\n이제 정리 된 영상을 확인하러 가볼까요?', + is_confirm: false, + }); + getAlarm(); + navigate(`/summary/${video_id}`); + setModelingData(null); + } catch (e) { + console.error(e); + setIsOpenErrorModal(true); + } + } else { + navigate('/summary/guest'); + } + + setVideoLink(null); + setStatus('NONE'); + setProgress(0); + }; + + return { createVideo }; +}; + +export default useCreateVideo; diff --git a/src/hooks/useGetAlarm.ts b/src/hooks/useGetAlarm.ts new file mode 100644 index 0000000..abca080 --- /dev/null +++ b/src/hooks/useGetAlarm.ts @@ -0,0 +1,16 @@ +import { getAlarmAPI } from '@/apis/user'; +import { userAlarmState } from '@/stores/user'; +import { useSetRecoilState } from 'recoil'; + +const useGetAlarm = () => { + const setAlarmList = useSetRecoilState(userAlarmState); + const getAlarm = async () => { + const { alarms } = (await getAlarmAPI()).data.result; + + setAlarmList(alarms); + }; + + return { getAlarm }; +}; + +export default useGetAlarm; diff --git a/src/models/modeling.ts b/src/models/modeling.ts index 8c281b4..c8b16e3 100644 --- a/src/models/modeling.ts +++ b/src/models/modeling.ts @@ -1,5 +1,3 @@ -import { IVideo } from './video'; - export type ModelingStatus = | 'NONE' | 'CONTINUE' @@ -17,7 +15,36 @@ export interface ModelingProcessRequest { videoId: string; } +export interface ModelingSubHeading { + content: string; + end_time: number; + name: string; + start_time: number; +} + +export interface ModelingSummary { + content: string; +} + +export interface ModelingTag { + name: string; +} + +export interface ModelingFinalData { + description: string; + link: string; + subheading: ModelingSubHeading[]; + summary: ModelingSummary[]; + tag: ModelingTag[]; + title: string; + youtube_created_at: string; + youtube_id: string; + + created_at: string; + updated_at: string; +} + export interface ModelingResponse { - finalData: IVideo; + finalData: ModelingFinalData; message: string; } diff --git a/src/models/video.ts b/src/models/video.ts index c7e2074..ebc0b31 100644 --- a/src/models/video.ts +++ b/src/models/video.ts @@ -53,3 +53,7 @@ export interface UpdateVideoRequest { export interface UpdateVideoCategoryRequest { video_id: (string | number)[]; } + +export interface CreateVideoResponse { + video_id: number; +} diff --git a/src/pages/FindEmailPage.tsx b/src/pages/FindEmailPage.tsx index 66ab9f4..8d29e34 100644 --- a/src/pages/FindEmailPage.tsx +++ b/src/pages/FindEmailPage.tsx @@ -1,18 +1,22 @@ -import React, { useState} from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; import * as FindEmailPageStyles from '@/styles/find/FindEmailPageStyle'; import { Link } from 'react-router-dom'; -import smallLogo from "../assets/logo.png"; -import theme from '@/styles/theme'; + +import { findEmailAPI } from '@/apis/user'; + +import LogoIcon from '@/assets/icons/dark-logo.svg?react'; + import NotFindUserModal from '@/components/modals/NotFindUserModal'; import PhoneCheck from '@/components/PhoneCheck'; -import { findEmailAPI } from '@/apis/user'; import FindEmail from '@/components/FindEmail'; import ImageSlider from '@/components/ImageSlider'; import { useNavigate } from 'react-router-dom'; +import theme from '@/styles/theme'; + const FindEmailPage = () => { - const [name, setName] = useState(""); + const [name, setName] = useState(''); const [tel, setTel] = useState(''); const [email, setEmail] = useState(''); const [isModal, setIsModal] = useState(false); @@ -21,82 +25,108 @@ const FindEmailPage = () => { const onChangeName = (e: React.ChangeEvent) => { setName(e.target.value); - } + }; const findBtnHandler = async () => { try { - const {data} = await findEmailAPI({ + const { data } = await findEmailAPI({ name: name, phone_number: tel, }); - if(data.success){ + if (data.success) { setIsFind(true); setEmail(data.email); } } catch (error) { - setIsModal(true); - } -} + setIsModal(true); + } + }; -const navigate = useNavigate(); + const navigate = useNavigate(); -const tohome = () => { - navigate('/'); -} + const tohome = () => { + navigate('/'); + }; -if(isFind){ + if (isFind) { + return ( + + + + ); + } return ( - - - ); -} -return ( - - - - - - 로고 이미지 -

이메일 찾기

-

이메일이 기억나지 않으시나요?

-
- - - 이름 - - - - - - - 찾아보기 - - - + + + + + +

이메일 찾기

+

이메일이 기억나지 않으시나요?

+
+ + + 이름 + + + + + + + 찾아보기 + + + 계정이 기억나시나요? 로그인 - - - - 아직 계정이 없으신가요? + + + + 아직 계정이 없으신가요? 이메일로 회원가입 - - -
-
- {isModal && } -
+ + + + + {isModal && } + ); }; @@ -104,8 +134,8 @@ export default FindEmailPage; const StyledLink = styled(Link)` color: ${({ theme }) => theme.color.gray500}; - ${ theme.typography.Body3 }; + ${theme.typography.Body3}; text-align: center; text-decoration: none; - margin : 0px 0px 0px 10px; + margin: 0px 0px 0px 10px; `; diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 021054d..7feffd7 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -35,6 +35,7 @@ const HomePage: React.FC = () => { const [recentVideos, setRecentVideos] = useState([]); const [dummyVideos, setDummyVideos] = useState([]); const { createToast } = useCreateToast(); + const searchRef = useRef(null); const onFileClick = async ( videoId: number, @@ -50,7 +51,6 @@ const HomePage: React.FC = () => { ); } }; - const searchRef = useRef(null); useEffect(() => { userToken && @@ -74,25 +74,23 @@ const HomePage: React.FC = () => { - {userToken ? ( -
- - -
- ) : ( -
- - -
- )} +
+ + +
{isOpenModal && } diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx index 88d60ea..ac399ca 100644 --- a/src/pages/SearchPage.tsx +++ b/src/pages/SearchPage.tsx @@ -5,99 +5,140 @@ import TagInput from '@/components/SearchPage/SearchComponent'; import { tagAPI } from '@/apis/search'; const SearchPage = () => { - const [tags, setTags] = useState([]); - const [input, setInput] = useState(''); - const [searchType, setSearchType] = useState(true); // True : keyword | False : hashTag - const [userHashTag, setUserHashTag] = useState([]); - const [selectedHashtags, setSelectedHashtags] = useState([]); - - useEffect(() => { - const handleTagAPI = async () => { - try { - const {data} = (await tagAPI()); - const extraData = data.result.map((item) => { - return item.name; - }); - const shuffleData = sortShuffle(extraData).slice(0,10); - setUserHashTag(shuffleData); + const [tags, setTags] = useState([]); + const [input, setInput] = useState(''); + const [searchType, setSearchType] = useState(true); // True : keyword | False : hashTag + const [userHashTag, setUserHashTag] = useState([]); + const [selectedHashtags, setSelectedHashtags] = useState([]); - } catch(e) { - setUserHashTag([]); - } - } - handleTagAPI(); - }, []); - - - const handleHashtagBox = (value : string) => { - const isSelected = selectedHashtags.includes(value); - setSelectedHashtags(prev => - isSelected ? prev.filter(idx => idx !== value) : [...prev, value] - ); - isSelected ? setTags(tags.filter((prev) => prev !== '#'+value)) : setTags([...tags, `#${value}`]); - setSearchType(false); // 박스를 클릭했을 때도 type 변경 - } - - const sortShuffle = (arr : string[]) => { - return arr.sort(() => Math.random() - 0.5); + useEffect(() => { + const handleTagAPI = async () => { + try { + const { data } = await tagAPI(); + const extraData = data.result.map((item) => { + return item.name; + }); + const shuffleData = sortShuffle(extraData).slice(0, 10); + setUserHashTag(shuffleData); + } catch (e) { + setUserHashTag([]); } - const firstHalf = userHashTag.slice(0, 5); - const secondHalf = userHashTag.slice(5); + }; + handleTagAPI(); + }, []); - return ( - -
-
-
-
- 찾고 싶은 키워드가 있나요? - 찾고자 하는 키워드를 검색하면 관련 영상을 찾아드릴게요 -
+ const handleHashtagBox = (value: string) => { + const isSelected = selectedHashtags.includes(value); + setSelectedHashtags((prev) => + isSelected ? prev.filter((idx) => idx !== value) : [...prev, value], + ); + isSelected + ? setTags(tags.filter((prev) => prev !== '#' + value)) + : setTags([...tags, `#${value}`]); + setSearchType(false); // 박스를 클릭했을 때도 type 변경 + }; -
-
-
- -
-
-
- -
- {(input.length === 0 && tags.length === 0) ? ( - - <> - 1. 키워드 검색 : 키워드를 검색하면 해당 키워드가 언급 된 영상들을 찾아드려요! -
- 2. 해시태그 검색 : #을 함께 검색하면 해당 해시태그가 있는 영상들을 찾아드려요! - -
- ) - : '' - } -
+ const sortShuffle = (arr: string[]) => { + return arr.sort(() => Math.random() - 0.5); + }; + const firstHalf = userHashTag.slice(0, 5); + const secondHalf = userHashTag.slice(5); -
-
- { - firstHalf.map((value : string, idx : number) => { - return( handleHashtagBox(value)} - className={selectedHashtags.includes(value) ? 'toggle' : ''}>{'#' + value}) - }) - } -
-
- { - secondHalf.map((value : string, idx : number) => { - return( handleHashtagBox(value)} - className={selectedHashtags.includes(value) ? 'toggle' : ''}>{'#' + value}) - }) - } -
+ return ( + +
+
+
+
+ + 찾고 싶은 키워드가 있나요? + + + 찾고자 하는 키워드를 검색하면 관련 영상을 찾아드릴게요 + +
+ +
+
+
+
+
- -); +
+ {input.length === 0 && tags.length === 0 ? ( + + <> + 1. 키워드 검색 : 키워드를 검색하면 해당 키워드가 언급 된 + 영상들을 찾아드려요! +
+ 2. 해시태그 검색 : #을 함께 검색하면 해당 해시태그가 있는 + 영상들을 찾아드려요! + +
+ ) : ( + '' + )} +
+ +
+
+ {firstHalf.map((value: string, idx: number) => { + return ( + handleHashtagBox(value)} + className={selectedHashtags.includes(value) ? 'toggle' : ''} + > + {'#' + value} + + ); + })} +
+
+ {secondHalf.map((value: string, idx: number) => { + return ( + handleHashtagBox(value)} + className={selectedHashtags.includes(value) ? 'toggle' : ''} + > + {'#' + value} + + ); + })} +
+
+
+
+ ); }; export default SearchPage; diff --git a/src/pages/SearchResultPage.tsx b/src/pages/SearchResultPage.tsx index c6dd4d9..4817318 100644 --- a/src/pages/SearchResultPage.tsx +++ b/src/pages/SearchResultPage.tsx @@ -1,9 +1,12 @@ -import { escapeHTML } from '@/utils/string'; import { useState, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import Styled from '@/styles/SearchResult'; + import TagInput from '@/components/SearchPage/SearchComponent'; import SearchNotFound from '@/components/SearchPage/SearchNotFound'; +import LoadingSpinner from '@/components/loadingSpinner/loadingSpinner'; +import { escapeHTML } from '@/utils/string'; + import SearchIcon from '@/assets/icons/search.svg?react'; import { IVideo } from '@/models/search'; import { searchAPI } from '@/apis/search'; @@ -15,13 +18,13 @@ const SearchResult = () => { const [input, setInput] = useState(''); const [searchType, setSearchType] = useState(true); // True : keyword | False : hashTag const [loading, setLoading] = useState(false); + const [isCroll, setIsCroll] = useState(false); const [errormsg, setErrormsg] = useState(''); const [data, setData] = useState([]); const location = useLocation(); useEffect(() => { const searchParams = new URLSearchParams(location.search); - setLoading(true); switch (searchParams.get('type')) { @@ -32,33 +35,28 @@ const SearchResult = () => { setInput(inputValues); handleSearchAPI(inputValues, keywordtype, ' '); - if (data.length === 0) { - setErrormsg(inputValues); - } break; case 'hashtag': const tagValues = searchParams.get('value') as string; const tagtype = searchParams.get('type') as string; - + setTags(tagValues.replace(/\s+/g, '').split('&')); setSearchType(false); handleSearchAPI(tagValues, tagtype, '&'); - if (data.length === 0) { - setErrormsg(tagValues.replace(/&/g, ' ')); - } break; default: // 기타 에러 } }, [location.search]); - + const handleSearchAPI = async ( inputValues: string, type: string, splittype: string, ) => { try { + setIsCroll(true); const keywords = inputValues.split(splittype); const requests = keywords.map((value) => { if (type === 'hashtag') { @@ -68,20 +66,31 @@ const SearchResult = () => { return searchData.then((value) => value.data.result); }); const responses = await Promise.all(requests); + let responseArr = [] as IVideo[]; responses.forEach((response) => { const ivideos = response.videos as IVideo[]; - dataDuplicateHandler(ivideos, inputValues); + ivideos.forEach((val) => { + responseArr.push(val); + }) }); + if(responseArr.length === 0){ + setData([]); + if(type === 'hashtag') + setErrormsg(inputValues.replace(/\&/g, ' ')); + else { + setErrormsg(inputValues); + } + } else { + dataDuplicateHandler(responseArr, inputValues); + } } catch (error) { - } finally { - setLoading(false); - } + } }; const formatContent = (content: string, keyword: string) => { let result = escapeHTML(content); const keywordArr = keyword.split(' '); - + keywordArr.forEach((keyword) => { if (keyword.trim() !== '') { result = result @@ -89,17 +98,17 @@ const SearchResult = () => { .join(`${escapeHTML(keyword)}`); } }); - + result = result.replace(/\n/g, '
'); - + return result; }; const dataDuplicateHandler = (videos: IVideo[], check: string) => { - const uniqueData = videos.filter((v, index, arr) => - arr.findIndex(t => t.video_id === v.video_id) === index + const uniqueData = videos.filter( + (v, index, arr) => + arr.findIndex((t) => t.video_id === v.video_id) === index, ); - const mappingData = uniqueData.map((video) => { return { ...video, @@ -108,12 +117,12 @@ const SearchResult = () => { content: formatContent(video.content, check), }; }); - setData([...data, ...mappingData]); + setData([...mappingData]); }; if (loading) { return ( -
스켈레톤 페이지
+ ); } return ( @@ -157,7 +166,7 @@ const SearchResult = () => { ) : ( data.map((item, index) => ( - + )) )}
diff --git a/src/pages/SignInPage.tsx b/src/pages/SignInPage.tsx index ca494a2..d902dc9 100644 --- a/src/pages/SignInPage.tsx +++ b/src/pages/SignInPage.tsx @@ -9,7 +9,7 @@ import * as SignInPageStyles from '@/styles/signin/SigninpageStyle'; import { loginAPI } from '@/apis/user'; import CloseIcon from '@/assets/icons/close.svg?react'; -import smallLogo from '@/assets/logo-dark.png'; +import LogoIcon from '@/assets/icons/dark-logo.svg?react'; import lineImg from '@/assets/line_img.png'; import errorImg from '@/assets/Error.png'; import signupImg from '@/assets/before-login.png'; @@ -70,11 +70,11 @@ const SignInPage: React.FC = () => { } } catch (error) { if (error instanceof AxiosError) { - const { message } = error.response?.data as APIBaseResponse; + const { code } = error.response?.data as APIBaseResponse; - if (message.indexOf('비밀번호') > -1) { + if (code === 'WRONG_PASSWORD') { setIsOpenErrorModal(true); - } else if (message.indexOf('이메일') > -1) { + } else if (code === 'NOT_FOUND_EMAIL') { setIsOpenSignUpModal(true); } } @@ -89,7 +89,7 @@ const SignInPage: React.FC = () => { const handleOnClick = () => { handleClickLoginButton(); - } + }; const redirect_uri = `${location.origin}/social-account`; //Redirect URI const KAKAO_KEY = '77ddf1baeb87f4a9752ed437db43cd96'; //kakao REST API KEY @@ -104,12 +104,12 @@ const SignInPage: React.FC = () => { const tohome = () => { navigate('/'); - } + }; return ( - + 로그인 { - + { > OR - + diff --git a/src/pages/SignUpPage.tsx b/src/pages/SignUpPage.tsx index 9c9d21e..a64a49b 100644 --- a/src/pages/SignUpPage.tsx +++ b/src/pages/SignUpPage.tsx @@ -1,18 +1,21 @@ +import { AxiosError } from 'axios'; import React, { useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; -import * as SignupPageStyles from '@/styles/signup/SignuppageStyle'; -import { useNavigate } from 'react-router-dom'; -import logo from '../assets/logo.png'; + +import { checkEmailAPI, joinAPI } from '@/apis/user'; + +import LogoIcon from '@/assets/icons/dark-logo.svg?react'; import errorImg from '@/assets/Error.png'; import CloseIcon from '@/assets/icons/close.svg?react'; -import { AxiosError } from 'axios'; -import { checkEmailAPI, joinAPI } from '@/apis/user'; -import { BlurBackground } from '@/styles/modals/common.style'; import Calendar from '@/components/Calendar'; import ImageSlider from '@/components/ImageSlider'; import PhoneCheck from '@/components/PhoneCheck'; -import { Link } from 'react-router-dom'; + +import { BlurBackground } from '@/styles/modals/common.style'; +import * as SignupPageStyles from '@/styles/signup/SignuppageStyle'; + import useCreateToast from '@/hooks/useCreateToast'; const SignUp = () => { @@ -34,9 +37,7 @@ const SignUp = () => { const [passwordMessage, setPasswordMessage] = useState( '*8자 이상으로 입력 *대문자 사용 *숫자 사용 *특수문자 사용', ); - const [passwordcheckMessage, setPasswordCheckMessage] = useState( - '', - ); + const [passwordcheckMessage, setPasswordCheckMessage] = useState(''); const [mismatchError, setMismatchError] = useState(false); const [isEmailSuccess, setIsEmailSuccess] = useState(false); @@ -120,7 +121,7 @@ const SignUp = () => { const tohome = () => { navigate('/'); - } + }; const onApply = () => { if ( @@ -170,7 +171,7 @@ const SignUp = () => { const handleOnClick = () => { onApply(); - } + }; return ( @@ -180,7 +181,7 @@ const SignUp = () => { - 로고 이미지 +

회원가입

새로운 계정을 생성하고 나만의 영상 아카이빙을 시작해요

@@ -338,10 +339,7 @@ const SignUp = () => { isPassword && passwordCheck && !mismatchError ? ( - + 가입하기 ) : ( diff --git a/src/pages/SummaryPage.tsx b/src/pages/SummaryPage.tsx index d59d6b8..34d7141 100644 --- a/src/pages/SummaryPage.tsx +++ b/src/pages/SummaryPage.tsx @@ -1,22 +1,28 @@ -import { useCallback, useEffect } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { useRecoilState } from 'recoil'; +import { useEffect } from 'react'; +import { useNavigate, useParams, useLocation } from 'react-router-dom'; +import { useRecoilState, useRecoilValue } from 'recoil'; -import { getVideoAPI } from '@/apis/videos'; +import { createVideoAPI, getDummyVideoAPI, getVideoAPI } from '@/apis/videos'; import { SummaryDetailBox } from '@/components/SummaryPage'; import { SummaryScriptBox } from '@/components/SummaryPage'; +import { modelingDataState } from '@/stores/model-controller'; import { summaryVideoState } from '@/stores/summary'; +import { userTokenState } from '@/stores/user'; import { Container } from '@/styles/SummaryPage'; const SummaryPage = () => { const navigate = useNavigate(); const { videoId } = useParams(); + const { search } = useLocation(); + + const userToken = useRecoilValue(userTokenState); + const modelingData = useRecoilValue(modelingDataState); const [summaryVideo, setSummaryVideo] = useRecoilState(summaryVideoState); - const callAPI = useCallback(async () => { + const callVideoAPI = async () => { if (!videoId) return; try { @@ -24,6 +30,7 @@ const SummaryPage = () => { if (!isSuccess) { navigate('/'); + return; } setSummaryVideo(result); @@ -31,22 +38,92 @@ const SummaryPage = () => { console.error(e); navigate('/'); } - }, [videoId, navigate, setSummaryVideo]); + }; + + const callDummyAPI = async () => { + if (!videoId) return; + + try { + const { isSuccess, result } = (await getDummyVideoAPI(videoId)).data; + + if (!isSuccess) { + navigate('/'); + return; + } + + if (userToken) { + const { video_id } = ( + await createVideoAPI({ + subheading: result.subHeading, + ...result, + }) + ).data.result; + + navigate(`/summary/${video_id}`); + } else { + setSummaryVideo(result); + } + } catch (e) { + console.error(e); + navigate('/'); + } + }; + + const setGuestSummaryVideo = () => { + if (!modelingData) { + navigate('/'); + return; + } + + const { subheading, tag, summary, ...others } = modelingData; + + setSummaryVideo({ + subHeading: subheading.map((item, id) => { + return { id, ...item }; + }), + tag: tag.map((item, id) => { + return { id, ...item }; + }), + summary: summary.map((item, id) => { + return { id, ...item }; + }), + video_id: 0, + image: '', + ...others, + }); + }; useEffect(() => { - callAPI(); + const searchParam = new URLSearchParams(search); + const isInsight = searchParam.get('insight') === 'true'; + + if (userToken) { + if (isInsight) { + callDummyAPI(); + } else { + callVideoAPI(); + } + } else { + if (videoId === 'guest') { + setGuestSummaryVideo(); + } else { + callDummyAPI(); + } + } return () => { setSummaryVideo(null); + // setModelingData(null); }; - }, [callAPI, setSummaryVideo]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [search]); return ( {summaryVideo && ( <> - - + + )} diff --git a/src/stores/modal.ts b/src/stores/modal.ts index ad21ac9..5570113 100644 --- a/src/stores/modal.ts +++ b/src/stores/modal.ts @@ -1,8 +1,8 @@ import { atom } from 'recoil'; -export const topCategoryModalState = atom({ - key: 'topCategoryModal', - default: false, +export const addCategoryModalState = atom({ + key: 'addCategoryModal', + default: { location: '', isOpen: false, categoryId: -1 }, }); export const summaryTransformModalState = atom({ @@ -14,3 +14,8 @@ export const recommendationModalState = atom({ key: 'recommendation-modal', default: false, }); + +export const errorModalState = atom({ + key: 'error-modal', + default: false, +}); diff --git a/src/stores/model-controller.ts b/src/stores/model-controller.ts index 4022a82..1989b8a 100644 --- a/src/stores/model-controller.ts +++ b/src/stores/model-controller.ts @@ -1,7 +1,6 @@ import { atom } from 'recoil'; -import { IVideo } from '@/models/video'; -import { ModelingStatus } from '@/models/modeling'; +import { ModelingFinalData, ModelingStatus } from '@/models/modeling'; import localStorageEffect from './effects/localStorageEffect'; @@ -20,7 +19,7 @@ export const modelingStatusState = atom({ default: 'NONE', }); -export const modelingDataState = atom({ +export const modelingDataState = atom({ key: 'modeling-data', default: null, effects_UNSTABLE: [localStorageEffect], diff --git a/src/stores/user.ts b/src/stores/user.ts index b9a6f60..6e26e83 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -3,6 +3,7 @@ import { atom } from 'recoil'; import { MyInfoResponse } from '@/models/user'; import localStorageEffect from './effects/localStorageEffect'; +import { IAlarm } from '@/models/alarm'; export const userInfoState = atom({ key: 'user-info', @@ -14,3 +15,8 @@ export const userTokenState = atom({ default: null, effects_UNSTABLE: [localStorageEffect], }); + +export const userAlarmState = atom({ + key: 'user-alarm', + default: [], +}); diff --git a/src/styles/HomepageStyle.ts b/src/styles/HomepageStyle.ts index 65971a5..218ecb3 100644 --- a/src/styles/HomepageStyle.ts +++ b/src/styles/HomepageStyle.ts @@ -5,7 +5,9 @@ export const HomePageContainer = styled.div` background-color: ${theme.color.white}; min-height: 100vh; width: 100%; + background-color: ${theme.color.gray500}; `; + export const SearchContainer = styled.div` display: flex; flex-direction: column; @@ -13,7 +15,6 @@ export const SearchContainer = styled.div` justify-content: center; width: 100%; padding-bottom: 100px; - background-color: ${theme.color.gray500}; `; export const SearchForm = styled.form` @@ -116,15 +117,12 @@ export const SearchButton = styled.button` } `; -export const RecentVideosContainer = styled.div<{ length: number }>` +export const RecentVideosContainer = styled.div` background-color: ${theme.color.white}; width: 100%; display: flex; justify-content: center; - border-radius: 50px 50px 0px 0px; position: relative; - bottom: 50px; - padding: ${(props) => (props.length ? '100px' : '0')} 0 110px; .container { width: 910px; @@ -220,15 +218,12 @@ export const VideoButton = styled.button` } `; -export const InsightVideosContainer = styled.div<{ user: string | null }>` +export const InsightVideosContainer = styled.div` display: flex; justify-content: center; background-color: ${theme.color.white}; width: 100%; - border-radius: 50px 50px 0 0; - padding: ${(props) => (props.user ? '0' : '100px')} 0 110px; position: relative; - bottom: 50px; .insight-container { display: flex; diff --git a/src/styles/SearchResult.ts b/src/styles/SearchResult.ts index 07379d2..10befac 100644 --- a/src/styles/SearchResult.ts +++ b/src/styles/SearchResult.ts @@ -1,305 +1,300 @@ -import styled from "styled-components"; -import theme from "./theme"; +import styled from 'styled-components'; +import theme from './theme'; const Container = styled.div` - display : flex; - flex : 1 1 auto; - flex-direction : column; - - & mark { - color : ${(props) => props.theme.color.green600} !important; - background : transparent; - } - - & div.inputContainer { - display: flex; - flex-direction: column; - align-items: center; - padding: 40px 0px 40px; - gap: 40px; - - background: ${(props) => props.theme.color.white}; + display: flex; + flex: 1 1 auto; + flex-direction: column; + + & mark { + color: ${(props) => props.theme.color.green600} !important; + background: transparent; + } + + & div.inputContainer { + display: flex; + flex-direction: column; + align-items: center; + padding: 40px 0px 40px; + gap: 40px; + + background: ${(props) => props.theme.color.white}; + } + & div.inputwrap { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0px; + gap: 20px; + + background: ${(props) => props.theme.color.gray100}; + border-radius: 12px; + white-space: nowrap; + overflow: hidden; + } + + & div.inputwrap:hover { + box-shadow: 1px 1px 20px ${(props) => props.theme.color.gray100}; + } + + & div.input-inner { + display: flex; + justify-content: space-between; + white-space: nowrap; + } + + & div.input { + display: flex; + gap: 10px; + } + + & input::placeholder { + ${(props) => props.theme.typography.Subheader2}; + + color: ${(props) => props.theme.color.gray300}; + } + + & button.search-btn { + ${(props) => props.theme.typography.Body1}; + + color: ${(props) => props.theme.color.white}; + + background: ${(props) => props.theme.color.gray500}; + + border-radius: 8px; + order: 1; + border: 0; + &:hover { + background-color: ${theme.color.green500}; + color: ${theme.color.gray500}; } - & div.inputwrap { - display : flex; - flex-direction : column; - justify-content: center; - align-items: center; - padding: 0px; - gap: 20px; - - background: ${(props) => props.theme.color.gray100}; - border-radius: 12px; - white-space: nowrap; - overflow: hidden; + } + + & button:disabled { + background: ${(props) => props.theme.color.gray300}; + } + + & div.result { + display: flex; + min-height: 800px; + overflow: scroll; + align-items: center; + flex-direction: column; + gap: 10px; + } + + & div.filter { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + padding: 0px; + gap: 12px; + & span { + ${(props) => props.theme.typography.Body3}; + color: ${(props) => props.theme.color.gray300}; } - - & div.inputwrap:hover { - box-shadow: 1px 1px 20px ${(props) => props.theme.color.gray100}; - } - - & div.input-inner { - display : flex; - justify-content : space-between; - white-space: nowrap; - } - - & div.input { - display : flex; - gap : 10px; - } - - & input::placeholder { - ${(props) => props.theme.typography.Subheader2}; - - color: ${(props) => props.theme.color.gray300}; - } - - & button.search-btn { - - ${(props) => props.theme.typography.Body1}; - - color: ${(props) => props.theme.color.white}; - - - background : ${(props) => props.theme.color.gray500}; - - border-radius: 8px; - order : 1; - border : 0; - &:hover { - background-color : ${theme.color.green500}; - color : ${theme.color.gray500}; - } - } - - & button:disabled { - background: ${(props) => props.theme.color.gray300}; - } - - & div.result { - display: flex; - min-height : 800px; - overflow : scroll; - align-items : center; - flex-direction: column; - gap : 10px; - } - - & div.filter { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: flex-start; - padding: 0px; - gap: 12px; - & span { - ${(props) => props.theme.typography.Body3}; - color : ${(props) => props.theme.color.gray300}; - } - } - & div.content { - display : flex; - flex-direction : column; - padding : 10px 0 10px 0; - gap : 20px; - } + } + & div.content { + display: flex; + flex-direction: column; + padding: 10px 0 10px 0; + gap: 20px; + } `; const VideoCard = styled.div` - display: flex; - flex-direction: row; - justify-content: center; - align-items: flex-start; - padding: 0px; - - background : ${(props) => props.theme.color.white} - flex: none; - order: 1; - flex-grow: 0; - - box-shadow: 0px 4px 40px rgba(0, 0, 0, 0.05); - border-radius: 16px; - - & div.main { - display: flex; - flex-direction: column; - align-items: flex-start; - padding: 24px; - gap: 24px; - - background : transparent; - - flex: none; - order: 0; - flex-grow: 1; - } - - & div.user { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - padding: 0px 0px 0px 24px; - gap: 8px; - - flex: none; - order: 0; - flex-grow: 0; - } - - & span.userName { - - ${(props) => props.theme.typography.Caption1}; - color: ${(props) => props.theme.color.gray300}; - flex: none; - order: 0; - flex-grow: 0; - } - - & span.contour { - border: 1px solid ${(props) => props.theme.color.gray300}; - - flex: none; - order: 1; - flex-grow: 0; - } - - & span.userDate { - ${(props) => props.theme.typography.Caption1}; - - color: ${(props) => props.theme.color.gray300}; - - flex: none; - order: 2; - flex-grow: 0; - } - - & div.content { - display: flex; - flex-direction: column; - align-items: flex-start; - padding: 0px; - gap: 8px; - - flex: none; - order: 1; - align-self: stretch; - flex-grow: 0; - } - - & div.title { - ${(props) => props.theme.typography.Subheader3}; - - color: ${(props) => props.theme.color.gray500}; - - flex: none; - order: 0; - align-self: stretch; - flex-grow: 0; - } - - & div.subtitle { - ${(props) => props.theme.typography.Body3}; - - color: ${(props) => props.theme.color.gray400}; - - flex: none; - order: 1; - align-self: stretch; - flex-grow: 0; - } - - & div.subcontent { - ${(props) => props.theme.typography.Body3}; - - color: ${(props) => props.theme.color.gray300}; - overflow : hidden; - text-overflow : ellipsis; - flex: none; - order: 2; - align-self: stretch; - flex-grow: 0; - } - - & div.hashtag { - display: flex; - flex-direction: row; - align-items: flex-start; - padding: 0px; - gap: 8px; - order : 2; - } - - & div.imgBox { - padding: 0; - width : 213px; - height : 254px; - border-radius : 0px 16px 16px 0px; - overflow:hidden; - margin:0 auto; - } - & img { - width : 100%; - height : 100%; - object-fit: cover; - } -` + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; + padding: 0px; + + background: ${(props) => props.theme.color.white}; + flex: none; + order: 1; + flex-grow: 0; + + box-shadow: 0px 4px 40px rgba(0, 0, 0, 0.05); + border-radius: 16px; + cursor: pointer; + + & div.main { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 24px; + gap: 24px; + + background: transparent; + + flex: none; + order: 0; + flex-grow: 1; + } + + & div.user { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 0px 0px 0px 24px; + gap: 8px; + + flex: none; + order: 0; + flex-grow: 0; + } + + & span.userName { + ${(props) => props.theme.typography.Caption1}; + color: ${(props) => props.theme.color.gray300}; + flex: none; + order: 0; + flex-grow: 0; + } + + & span.contour { + border: 1px solid ${(props) => props.theme.color.gray300}; + + flex: none; + order: 1; + flex-grow: 0; + } + + & span.userDate { + ${(props) => props.theme.typography.Caption1}; + + color: ${(props) => props.theme.color.gray300}; + + flex: none; + order: 2; + flex-grow: 0; + } + + & div.content { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0px; + gap: 8px; + + flex: none; + order: 1; + align-self: stretch; + flex-grow: 0; + } + + & div.title { + ${(props) => props.theme.typography.Subheader3}; + + color: ${(props) => props.theme.color.gray500}; + + flex: none; + order: 0; + align-self: stretch; + flex-grow: 0; + } + + & div.subtitle { + ${(props) => props.theme.typography.Body3}; + + color: ${(props) => props.theme.color.gray400}; + + flex: none; + order: 1; + align-self: stretch; + flex-grow: 0; + } + + & div.subcontent { + ${(props) => props.theme.typography.Body3}; + + color: ${(props) => props.theme.color.gray300}; + overflow: hidden; + text-overflow: ellipsis; + flex: none; + order: 2; + align-self: stretch; + flex-grow: 0; + } + + & div.hashtag { + display: flex; + flex-direction: row; + align-items: flex-start; + padding: 0px; + gap: 8px; + order: 2; + } + + & div.imgBox { + padding: 0; + width: 213px; + height: 254px; + border-radius: 0px 16px 16px 0px; + overflow: hidden; + margin: 0 auto; + } + & img { + width: 100%; + height: 100%; + object-fit: cover; + } +`; const hashtagBox = styled.div` - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - padding: 6px 10px; - gap: 10px; - - height: 31px; - - background: ${(props) => props.theme.color.gray100}; - color : ${(props) => props.theme.color.gray400}; - border-radius: 8px; - - flex: none; - order: 2; - flex-grow: 0; - - ${(props) => props.theme.typography.Caption1}; - ` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 6px 10px; + gap: 10px; -const SearchNotFoundContainer = styled.div` - width : 500px; - height : 337px; - display : flex; - flex-direction : column; - justify-content: center; - align-items: center; - gap : 40px; - - & div.text { - ${(props) => props.theme.typography.Header3}; - display : flex; - flex-direction : column; - justify-content: center; - align-items: center; - } - & span.user { - color : ${(props) => props.theme.color.gray300}; - text-align : center; - } - - & button { - width : 235px; - height : 56px; - background : ${(props) => props.theme.color.gray500}; - color : ${(props) => props.theme.color.white}; - border : none; - border-radius : 100px; - padding: 12px 32px; - gap: 10px; - ${(props) => props.theme.typography.Subheader2}; - } - ` -export default {Container, VideoCard, hashtagBox, SearchNotFoundContainer}; + height: 31px; + + background: ${(props) => props.theme.color.gray100}; + color: ${(props) => props.theme.color.gray400}; + border-radius: 8px; + flex: none; + order: 2; + flex-grow: 0; + ${(props) => props.theme.typography.Caption1}; +`; +const SearchNotFoundContainer = styled.div` + width: 500px; + height: 337px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 40px; + + & div.text { + ${(props) => props.theme.typography.Header3}; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + & span.user { + color: ${(props) => props.theme.color.gray300}; + text-align: center; + } + + & button { + width: 235px; + height: 56px; + background: ${(props) => props.theme.color.gray500}; + color: ${(props) => props.theme.color.white}; + border: none; + border-radius: 100px; + padding: 12px 32px; + gap: 10px; + ${(props) => props.theme.typography.Subheader2}; + } +`; +export default { Container, VideoCard, hashtagBox, SearchNotFoundContainer }; diff --git a/src/styles/SummaryPage.ts b/src/styles/SummaryPage.ts index b93ee7c..ce5213b 100644 --- a/src/styles/SummaryPage.ts +++ b/src/styles/SummaryPage.ts @@ -249,17 +249,18 @@ export const ScriptBox = styled.div` position: relative; display: flex; flex-direction: column; - gap: 40px; min-width: 555px; max-width: 865px; box-shadow: 0 4px 40px 0 rgba(0, 0, 0, 0.05); & div.tools { - padding: 20px 100px 0 60px; + z-index: 1; + padding: 20px 100px 20px 60px; display: flex; align-items: center; justify-content: space-between; width: 100%; + transition: box-shadow 0.1s; & button.edit-button { padding: 8px 20px; @@ -325,7 +326,7 @@ export const ScriptBox = styled.div` } & div.script-container { - padding: 0 100px 20px 60px; + padding: 20px 100px 20px 60px; display: flex; flex-direction: column; gap: 60px; diff --git a/src/styles/category/Card.style.ts b/src/styles/category/Card.style.ts index abfae83..39802a5 100644 --- a/src/styles/category/Card.style.ts +++ b/src/styles/category/Card.style.ts @@ -47,6 +47,10 @@ export const DropdownWrap = styled.div` fill: ${(props) => props.theme.color.gray400}; } } + + &.changed { + background-color: ${theme.color.green400}; + } } `; @@ -98,7 +102,7 @@ export const CardContainer = styled.div` row-gap: 40px; `; -export const Wrap = styled.div<{ mode: string }>` +export const Wrap = styled.div<{ token: string | null; mode: string }>` display: flex; flex-direction: column; width: 290px; @@ -122,6 +126,7 @@ export const Wrap = styled.div<{ mode: string }>` box-shadow: 0px 0px 10px 5px rgba(0, 0, 0, 0.1); ${(props) => props.mode === 'recommend' && + props.token && css` height: 424px; `} diff --git a/src/styles/layout/header/alarm/AlarmItem.style.ts b/src/styles/layout/header/alarm/AlarmItem.style.ts index 6b9496d..7b31746 100644 --- a/src/styles/layout/header/alarm/AlarmItem.style.ts +++ b/src/styles/layout/header/alarm/AlarmItem.style.ts @@ -1,6 +1,8 @@ +import theme from '@/styles/theme'; import styled from 'styled-components'; export const Container = styled.div` + cursor: pointer !important; padding: 28px 0; display: flex; flex-direction: column; @@ -119,7 +121,37 @@ export const Container = styled.div` & h1 { font-weight: bold; + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + -webkit-line-clamp: 2; } } } + + & div.progress-wrap { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; + } + + & div.progress-bar { + width: 100%; + height: 8px; + border-radius: 100px; + background-color: ${(props) => props.theme.color.gray100}; + overflow: hidden; + + & > div { + height: 100%; + transition: 1s; + transition-delay: 0.5s; + } + } + + & span.progress-text { + ${theme.typography.Caption3} + color: ${theme.color.gray400}; + } `; diff --git a/src/styles/loadingSpinner.ts b/src/styles/loadingSpinner.ts new file mode 100644 index 0000000..75c0f0f --- /dev/null +++ b/src/styles/loadingSpinner.ts @@ -0,0 +1,44 @@ +import styled from "styled-components"; +import theme from "./theme"; + + +const Container = styled.div` + width : 100%; + height : 100vh; + + display : flex; + align-items : center; + justify-content: center; + + & div.wrap { + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + } + & div.tip { + display : flex; + gap : 10px; + flex-direction : column; + } + & div.center-box { + display : flex; + flex-direction : column; + gap : 30px; + justify-content: center; + align-items: center; + } + & span.header { + text-align : center; + ${theme.typography.Subheader3}; + color : ${theme.color.gray500}; + } + & span.body { + text-align : center; + ${theme.typography.Body4}; + color : ${theme.color.gray400}; + } +`; + + +export default Container; \ No newline at end of file diff --git a/src/styles/modals/ErrorModal.style.ts b/src/styles/modals/ErrorModal.style.ts new file mode 100644 index 0000000..26f2543 --- /dev/null +++ b/src/styles/modals/ErrorModal.style.ts @@ -0,0 +1,85 @@ +import styled from 'styled-components'; +import theme from '../theme'; + +export const ErrorModalContainer = styled.div` + position: fixed; + z-index: 100; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + + .container { + width: 700px; + height: 384px; + background-color: ${theme.color.white}; + padding: 40px 50px; + display: flex; + flex-direction: column; + justify-content: space-between; + } + + .wrapper { + width: 600px; + height: 198px; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + } + + .close-btn { + align-self: flex-end; + cursor: pointer; + } + + .main { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + } + + .modal-main { + width: 245px; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + } + + .modal-main h2 { + color: ${theme.color.gray500}; + font-style: ${theme.typography.Header6}; + font-size: 24px; + line-height: 1.6em; + margin-top: 12px; + margin-bottom: 12px; + } + + .main h4 { + color: ${theme.color.gray300}; + font-style: ${theme.typography.Body1}; + font-size: 16px; + line-height: 1.6em; + } + + .restart-btn { + width: 600px; + height: 58px; + font-style: ${theme.typography.Body1}; + font-size: 16px; + padding: 16px 24px; + color: ${theme.color.white}; + background-color: ${theme.color.gray500}; + border: none; + border-radius: 12px; + cursor: pointer; + } +`; \ No newline at end of file diff --git a/src/styles/signup/SignuppageStyle.ts b/src/styles/signup/SignuppageStyle.ts index d1ba235..7177635 100644 --- a/src/styles/signup/SignuppageStyle.ts +++ b/src/styles/signup/SignuppageStyle.ts @@ -12,7 +12,7 @@ export const Wrapper = styled.div` `; export const LogoSection = styled.div` - img{ + img { display: flex; width: auto; height: 840px; @@ -26,8 +26,9 @@ export const MainSection = styled.div` justify-content: center; width: 580px; height: 896px; - margin-top: 128px; + margin-top: 95px; `; + export const InputSection = styled.div` display: flex; flex-direction: column; @@ -38,13 +39,13 @@ export const InputSection = styled.div` &::-webkit-scrollbar { width: 8px; } - &::-webkit-scrollbar-thumb{ + &::-webkit-scrollbar-thumb { height: 200px; - background-color:#f3f3f3; + background-color: #f3f3f3; border-radius: 100px; } - &::-webkit-scrollbar-track{ - background-color:#ffffff; + &::-webkit-scrollbar-track { + background-color: #ffffff; } `; @@ -102,8 +103,8 @@ export const TwoLabel = styled.label` `; export const ThreeLabel = styled.label` - display : flex; - flex-direction : column; + display: flex; + flex-direction: column; margin-bottom: 8px; `; @@ -118,12 +119,12 @@ export const PhoneInputBox = styled.input` flex: 1 0 0; font-size: 16px; font-style: normal; - color: var(--Main, #1E1E1E); + color: var(--Main, #1e1e1e); font-family: Pretendard; font-weight: 500; line-height: 160%; border-radius: 12px; - border: 1.5px solid var(--gray-200, #e8e8e8) ; + border: 1.5px solid var(--gray-200, #e8e8e8); margin-top: 8px; outline: none; &:hover { @@ -188,7 +189,7 @@ export const EmailInputBox = styled.input` flex: 1 0 0; font-size: 16px; font-style: normal; - color: var(--Main, #1E1E1E); + color: var(--Main, #1e1e1e); font-family: Pretendard; font-weight: 500; line-height: 160%; @@ -236,14 +237,14 @@ export const BirthInputBox = styled.input` flex: 1 0 0; font-size: 16px; font-style: normal; - color: var(--Main, #1E1E1E); + color: var(--Main, #1e1e1e); font-family: Pretendard; font-weight: 500; line-height: 160%; margin-right: 8px; border-radius: 12px; border: 1.5px solid #e8e8e8; - color: var(--Main, #1E1E1E); + color: var(--Main, #1e1e1e); background: #fff; &::placeholder { color: #bbb; @@ -257,7 +258,6 @@ export const BirthInputBox = styled.input` } `; - export const SexSelectBox = styled.div` display: flex; flex-direction: row; @@ -281,7 +281,8 @@ export const SexButton = styled.button<{ selected: boolean }>` line-height: 160%; margin-right: 10px; cursor: pointer; - ${(props) => props.selected && + ${(props) => + props.selected && ` background: #1e1e1e; color: #fff; @@ -299,7 +300,7 @@ export const Error = styled.p` `; export const Avail = styled.p` - color: #3681FE; + color: #3681fe; font-size: 14px; font-weight: 500; margin-top: 8px; @@ -335,7 +336,7 @@ export const Button = styled.button` export const SucButton = styled.button` border-radius: 12px; - background: #1E1E1E; + background: #1e1e1e; color: #fff; display: flex; width: 494px; @@ -353,8 +354,8 @@ export const SucButton = styled.button` export const DupSucButton = styled.button` border-radius: 12px; - background: #E9FF3F; - color: #1E1E1E; + background: #e9ff3f; + color: #1e1e1e; display: flex; margin-top: 8px; margin-left: 8px; @@ -370,8 +371,8 @@ export const DupSucButton = styled.button` border: none; cursor: pointer; &:disabled { - background-color : ${theme.color.gray100}; - color : ${theme.color.gray300}; + background-color: ${theme.color.gray100}; + color: ${theme.color.gray300}; cursor: default; } `; @@ -432,15 +433,14 @@ export const RetryButton = styled.button((props) => ({ })); export const PwDiv = styled.div` - font-size: 14px; - margin-top: 8px; - color:#3681FE; - font-weight: 500; - line-height: 160%; - padding-left: 16px; + font-size: 14px; + margin-top: 8px; + color: #3681fe; + font-weight: 500; + line-height: 160%; + padding-left: 16px; `; - export const TextTotalComponent = styled.div` display: flex; flex-direction: row; @@ -448,118 +448,118 @@ export const TextTotalComponent = styled.div` `; export const TextDiv = styled.div` - font-size: 14px; - color: #BBB; - font-weight: 500; - line-height: 160%; + font-size: 14px; + color: #bbb; + font-weight: 500; + line-height: 160%; `; export const UserButton = styled.button` - width : 160px; - height : 56px; - color : #1E1E1E; - background-color : ${theme.color.green400}; - border : none; - border-radius : 12px; + width: 160px; + height: 56px; + color: #1e1e1e; + background-color: ${theme.color.green400}; + border: none; + border-radius: 12px; margin-top: 8px; ${theme.typography.Body1}; &:disabled { - background-color : ${theme.color.gray100}; - color : ${theme.color.gray300}; + background-color: ${theme.color.gray100}; + color: ${theme.color.gray300}; } -` +`; export const UserDiv = styled.div` - display : flex; - flex-direction : row; - gap : 10px; -` + display: flex; + flex-direction: row; + gap: 10px; +`; export const SendMsg = styled.div` - margin-left : 16px; - margin-top : 8px; - ${theme.typography.Body3}; - color : ${theme.color.red}; -` + margin-left: 16px; + margin-top: 8px; + ${theme.typography.Body3}; + color: ${theme.color.red}; +`; export const CustomButton = styled.button` - width : 54px; - height : 54px; - display : flex; - align-items: center; - justify-content: center; - background : #1E1E1E; - border : none; - border-radius : 12px; -` -export const CalendarContainer = styled.div` + width: 54px; + height: 54px; + display: flex; + align-items: center; + justify-content: center; + background: #1e1e1e; + border: none; + border-radius: 12px; +`; +export const CalendarContainer = styled.div` .custom-inputSelected { - background : #BBBBBB !important; + background: #bbbbbb !important; } .react-datepicker { & select { - border : none; - color : #1E1E1E; - font-weight: bold; + border: none; + color: #1e1e1e; + font-weight: bold; } & button { - border : none; - border-radius : 8px; + border: none; + border-radius: 8px; } .react-datepicker__month-container { - .react-datepicker__header { - background-color: white; - border: none; - } - .react-datepicker__day-name { - margin: 0px 7px 0px 7px; - } - .react-datepicker__month { - .react-datepicker__day { - margin: 5px 7px 5px 7px; - &:hover { - border-radius: 18px; - background-color: #FBFFCC - } - } - .react-datepicker__day--today, - .react-datepicker__day--keyboard-selected { - border-radius: 18px; - background-color: #E9FF3F; - font-weight: 400; - } - .react-datepicker__day--selected, - .react-datepicker__day--in-range, - .react-datepicker__day--in-selecting-range { - border-radius: 18px; - background-color: #E9FF3F; - color: black; - } + .react-datepicker__header { + background-color: white; + border: none; + } + .react-datepicker__day-name { + margin: 0px 7px 0px 7px; + } + .react-datepicker__month { + .react-datepicker__day { + margin: 5px 7px 5px 7px; + &:hover { + border-radius: 18px; + background-color: #fbffcc; } + } + .react-datepicker__day--today, + .react-datepicker__day--keyboard-selected { + border-radius: 18px; + background-color: #e9ff3f; + font-weight: 400; + } + .react-datepicker__day--selected, + .react-datepicker__day--in-range, + .react-datepicker__day--in-selecting-range { + border-radius: 18px; + background-color: #e9ff3f; + color: black; + } } + } } - .react-datepicker__aria-live, - .react-datepicker__time-list-item—disabled, - .react-datepicker-time__header { - display: none; - } + .react-datepicker__aria-live, + .react-datepicker__time-list-item—disabled, + .react-datepicker-time__header { + display: none; + } - .react-datepicker__time-container { - overflow-y: scroll; - height: 100px; - cursor: pointer; - } - .react-datepicker__input-container > input, - .react-datepicker__time-container { - width: 80px; - background-color: #f9f9f9; - outline: none; - text-align: center; - overflow-x: hidden; - } - .react-datepicker__time-list-item—selected { - background-color: #fff2b4 !important; - color: black !important; - } -` \ No newline at end of file + .react-datepicker__time-container { + overflow-y: scroll; + height: 100px; + cursor: pointer; + } + .react-datepicker__input-container > input, + .react-datepicker__time-container { + width: 80px; + background-color: #f9f9f9; + outline: none; + text-align: center; + overflow-x: hidden; + } + .react-datepicker__time-list-item—selected { + background-color: #fff2b4 !important; + color: black !important; + } +`; diff --git a/src/utils/date.ts b/src/utils/date.ts index d7d55fb..828e2d6 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -43,7 +43,7 @@ export const getDate = (dateString?: string) => { export const formatTime = (time: number) => { const hour = Math.floor(time / 60 / 60); - const minute = Math.floor(time / 60); + const minute = Math.floor(time / 60) - hour * 60; const second = Math.floor(time % 60); if (hour > 0) { diff --git a/types/category.ts b/types/category.ts index ac6a12b..4dbe752 100644 --- a/types/category.ts +++ b/types/category.ts @@ -20,3 +20,14 @@ export interface ITagProps { tag_id: number; name: string; } + +export interface IEditProps { + activated: boolean; + categoryId: number; +} + +export interface IAddCategoryModalProps { + location: string; + isOpen: boolean; + categoryId: number; +}