diff --git a/base/components/Hidden.tsx b/base/components/Hidden.tsx new file mode 100644 index 0000000..3457e86 --- /dev/null +++ b/base/components/Hidden.tsx @@ -0,0 +1,33 @@ +import { CSSProperties, ReactNode } from 'react'; +import styled from 'styled-components'; + +export interface HiddenProps { + isHidden: boolean; + isUseVisibility?: boolean; + style?: CSSProperties; + children?: ReactNode; +} +export function Hidden({ + isHidden, + isUseVisibility = false, + style, + children, +}: HiddenProps) { + return ( + + {children} + + ); +} + +const HiddenContainer = styled.div<{ + isHidden: boolean; + isUseVisibility: boolean; +}>` + ${({ isUseVisibility, isHidden }) => + isHidden && (isUseVisibility ? 'visiblity : hidden' : 'display : none')} +`; diff --git a/base/components/ModalBase.tsx b/base/components/ModalBase.tsx new file mode 100644 index 0000000..dbb5221 --- /dev/null +++ b/base/components/ModalBase.tsx @@ -0,0 +1,47 @@ +import { ReactElement, ReactNode, cloneElement } from 'react'; +import { createPortal } from 'react-dom'; +import styled from 'styled-components'; + +import { Hidden } from '@Base/components/Hidden'; + +export interface ModalBaseProps { + backdropComponent?: ReactElement; + children?: ReactNode; + isOpen: boolean; + onClose?: () => void; +} + +export function ModalBase({ + backdropComponent = , + children, + isOpen, + onClose, +}: ModalBaseProps) { + return createPortal( + + + {cloneElement(backdropComponent, { onClick: onClose })} + {children} + + , + document.body + ); +} + +export const Container = styled.div` + position: relative; + height: 100%; + max-height: 100dvh; + overflow: hidden; +`; + +export const DefaultBackdrop = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #27272a; + opacity: 0.5; + z-index: 10; +`; diff --git a/base/hooks/useModal.tsx b/base/hooks/useModal.tsx new file mode 100644 index 0000000..fb34cc6 --- /dev/null +++ b/base/hooks/useModal.tsx @@ -0,0 +1,28 @@ +import { useCallback, useState } from 'react'; + +import { isTrue } from '@Base/utils/check'; + +export interface UseModalProps { + defaultMessage?: string; + defaultIsOpen?: boolean; +} + +export function useModal({ + defaultIsOpen = false, + defaultMessage = '', +}: UseModalProps) { + const [isOpen, setIsOpen] = useState(defaultIsOpen); + const [message, setMessage] = useState(defaultMessage); + + const open = useCallback((message?: string) => { + if (isTrue(message)) setMessage(message); + + setIsOpen(true); + }, []); + + const close = useCallback(() => { + setIsOpen(false); + }, []); + + return { isOpen, message, open, close }; +} diff --git a/src/hooks/useScrollTop.ts b/base/hooks/useScrollTop.tsx similarity index 100% rename from src/hooks/useScrollTop.ts rename to base/hooks/useScrollTop.tsx diff --git a/base/hooks/useTrigger.tsx b/base/hooks/useTrigger.tsx new file mode 100644 index 0000000..6cd2a4d --- /dev/null +++ b/base/hooks/useTrigger.tsx @@ -0,0 +1,31 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { isFalse } from '@Base/utils/check'; + +export interface UseTriggerProps { + triggerFn?: () => boolean; + onTrigger?: (reset: () => void) => void; +} + +export function useTrigger({ triggerFn, onTrigger }: UseTriggerProps) { + const [isTriggered, setIsTriggered] = useState(false); + + const reset = useCallback(() => { + setIsTriggered(false); + }, []); + + const trigger = useCallback(() => { + setIsTriggered(true); + }, []); + + useEffect(() => { + if (isFalse(triggerFn)) return; + + if (triggerFn()) { + setIsTriggered(true); + onTrigger?.(reset); + } + }, [onTrigger, reset, triggerFn]); + + return { isTriggered, reset, trigger }; +} diff --git a/base/utils/arrExtension.ts b/base/utils/arrExtension.ts new file mode 100644 index 0000000..1cc576b --- /dev/null +++ b/base/utils/arrExtension.ts @@ -0,0 +1,3 @@ +export function createConsecutiveNumbers(length: number, startNum = 0) { + return new Array(length).fill(0).map((_, idx) => startNum + idx); +} diff --git a/base/utils/check.ts b/base/utils/check.ts new file mode 100644 index 0000000..bfd1941 --- /dev/null +++ b/base/utils/check.ts @@ -0,0 +1,22 @@ +/** + * checking object if is null + undefined or not + * if object is 0, this functions consider as true + */ +export function isFalse(obj: T | undefined | null): obj is undefined | null { + if (obj === undefined || obj === null) return true; + + return false; +} + +/** + * checking object if is null + undefined or not + * if object is 0, this functions consider as true + */ +export function isTrue(obj: T | undefined | null): obj is T { + return !isFalse(obj); +} + +export function isEmpty>(obj: T) { + if (obj.length === 0) return true; + return false; +} diff --git a/public/translations/en.json b/public/translations/en.json index 2cc6654..cf8be34 100644 --- a/public/translations/en.json +++ b/public/translations/en.json @@ -1,4 +1,5 @@ { + "confirm": "Confirm", "landingTitle_1": "Oppa", "titleHighlight": " Colors", "landingTitle_2": " ", @@ -16,9 +17,13 @@ "modalGuidance_2": " ", "confirmButton": "Next", "statusContent": " Stage", - "explanation_1": "Choose the color looks best on you", - "explanation_2": "The color looks good on you gives you", - "explanation_3": "more even toned, healthy looking skin", + "explanation_1": "Choose a color that looks best on you.", + "explanation_2": "The color that looks good on you gives you", + "explanation_3": "even tone and healthy looking skin.", + "colorChoiceGuideTitle": "Tips for choosing the right color", + "colorChoiceGuideExplanation_1": "\"You\" are the one that needs to be focused on, not the color.", + "colorChoiceGuideExplanation_2": "With a mismatch? ๐Ÿ™\nโŒ You look too hot / too cold.\nโŒ Faces and colors look separate.", + "colorChoiceGuideExplanation_3": "With a match? ๐Ÿ˜Š\nโญ•๏ธ Facial contours come alive.\nโญ•๏ธ Your nose and jawline look sharp.", "bonusStatus": "Last Stage", "errorMsg": "We have encountered an error", "resultTitle": "Your Seasonal Color is", @@ -209,6 +214,7 @@ "alertNoImg": "No file chosen", "alertSuccessCopy": "Copied Successfully! โœจ", "alertFailCopy": "Failed to copy...๐Ÿฅฒ", - "alertMacOS": "Chrome browser on Mac is not working. Open in another browser ๐Ÿฅฐ", - "alertKakao": "It is not working on kakaoTalk InAppBrowser.. Open in another browser ๐Ÿฅฐ" + "alertMacOS": "Chrome browser on Mac is not working. Open in another browser. ๐Ÿฅฐ", + "alertKakao": "It is not working on kakaoTalk InAppBrowser. Open in another browser. ๐Ÿฅฐ", + "alertNotSupportedBrowser": "It is not working on this browser. Open in another browser. ๐Ÿฅฐ" } diff --git a/public/translations/ko.json b/public/translations/ko.json index 514bfae..9863d8a 100644 --- a/public/translations/ko.json +++ b/public/translations/ko.json @@ -1,4 +1,5 @@ { + "confirm": "ํ™•์ธ", "landingTitle_1": "์˜ค๋น ! ", "titleHighlight": "ํ†ค", "landingTitle_2": " ๋งŽ์•„?", @@ -19,6 +20,10 @@ "explanation_1": "์–ผ๊ตด๊ณผ ์ž˜ ์–ด์šธ๋ฆฌ๋Š” ์ƒ‰์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”.", "explanation_2": "์–ผ๊ตด๊ณผ ์ƒ‰์ด ํ•˜๋‚˜๋กœ ์ด์–ด์ง„ ๊ฒƒ์ฒ˜๋Ÿผ ์กฐํ™”๋กœ์›Œ ๋ณด์ด๊ณ ,", "explanation_3": "ํ”ผ๋ถ€์ƒ‰์ด ๊ท ์ผํ•˜๊ณ  ๋ง‘์•„ ๋ณด์ด๋Š” ์ƒ‰์ด ์ž˜ ์–ด์šธ๋ฆฌ๋Š” ์ƒ‰์ž…๋‹ˆ๋‹ค.", + "colorChoiceGuideTitle": "์ž˜ ์–ด์šธ๋ฆฌ๋Š” ์ƒ‰ ๊ณ ๋ฅด๋Š” ๐ŸฏTIP", + "colorChoiceGuideExplanation_1": "์ƒ‰์ด ๋ˆˆ์— ๋“ค์–ด์˜ค๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ โ€˜๋‚ดโ€™๊ฐ€ ๋‹๋ณด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค!", + "colorChoiceGuideExplanation_2": "์•ˆ ์–ด์šธ๋ฆฌ๋Š” ์ƒ‰์€? ๐Ÿ™\nโŒ ๋„ˆ๋ฌด ๋”์›Œ ๋ณด์—ฌ์š”\nโŒ ์–ผ๊ตด๊ณผ ์ƒ‰์ด ๋ถ„๋ฆฌ๋˜์–ด ๋ณด์—ฌ์š”", + "colorChoiceGuideExplanation_3": "์ž˜ ์–ด์šธ๋ฆฌ๋Š” ์ƒ‰์€? ๐Ÿ˜Š\nโญ•๏ธ ์–ผ๊ตด ์œค๊ณฝ์ด ์‚ด์•„๋‚˜์š”\nโญ•๏ธ ์ฝง๋Œ€์™€ ํ„ฑ ๋ผ์ธ์ด ๋‚ ๋ ตํ•ด ๋ณด์—ฌ์š”", "bonusStatus": "๋งˆ์ง€๋ง‰ ๋‹จ๊ณ„", "errorMsg": "์˜ˆ๊ธฐ์น˜ ๋ชปํ•œ ์ƒํ™ฉ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", "resultTitle": "๋‹น์‹ ์˜ ํผ์Šค๋„ ์ปฌ๋Ÿฌ๋Š”", @@ -210,5 +215,6 @@ "alertSuccessCopy": "๋งํฌ ๋ณต์‚ฌ ์„ฑ๊ณต! โœจ", "alertFailCopy": "๋งํฌ ๋ณต์‚ฌ์— ์‹คํŒจํ–ˆ์–ด์š”...๐Ÿฅฒ", "alertMacOS": "macOS ํ™˜๊ฒฝ์˜ ํฌ๋กฌ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹คํ–‰ํ•ด ์ฃผ์„ธ์š”. ๐Ÿฅฐ", - "alertKakao": "์นด์นด์˜ค ์ธ์•ฑ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹คํ–‰ํ•ด ์ฃผ์„ธ์š”. ๐Ÿฅฐ" + "alertKakao": "์นด์นด์˜ค ์ธ์•ฑ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹คํ–‰ํ•ด ์ฃผ์„ธ์š”. ๐Ÿฅฐ", + "alertNotSupportedBrowser": "ํ˜„์žฌ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹คํ–‰ํ•ด ์ฃผ์„ธ์š”. ๐Ÿฅฐ" } diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..f190d99 --- /dev/null +++ b/robots.txt @@ -0,0 +1 @@ +Sitemap: https://omct.web.app/sitemap.xml \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..4ff2b97 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,87 @@ + + + + + + + https://omct.web.app/ + 2023-11-05T00:28:50+00:00 + 1.00 + + + https://omct.web.app/all-types-view + 2023-11-05T00:28:50+00:00 + 0.80 + + + https://omct.web.app/result?colorType=springbright + 2023-11-05T00:28:50+00:00 + 0.80 + + + https://omct.web.app/result?colorType=springwarm + 2023-11-05T00:28:50+00:00 + 0.80 + + + https://omct.web.app/result?colorType=springlight + 2023-11-05T00:28:50+00:00 + 0.80 + + + https://omct.web.app/result?colorType=summerlight + 2023-11-05T00:28:50+00:00 + 0.80 + + + https://omct.web.app/result?colorType=summercool + 2023-11-05T00:28:50+00:00 + 0.80 + + + https://omct.web.app/result?colorType=summermute + 2023-11-05T00:28:50+00:00 + 0.80 + + + https://omct.web.app/result?colorType=autumnmute + 2023-11-05T00:28:50+00:00 + 0.80 + + + https://omct.web.app/result?colorType=autumnwarm + 2023-11-05T00:28:50+00:00 + 0.80 + + + https://omct.web.app/result?colorType=autumndeep + 2023-11-05T00:28:50+00:00 + 0.80 + + + https://omct.web.app/result?colorType=winterdeep + 2023-11-05T00:28:50+00:00 + 0.80 + + + https://omct.web.app/result?colorType=wintercool + 2023-11-05T00:28:50+00:00 + 0.80 + + + https://omct.web.app/result?colorType=winterbright + 2023-11-05T00:28:50+00:00 + 0.80 + + + https://omct.web.app/image-upload + 2023-11-05T00:28:50+00:00 + 0.20 + + + + \ No newline at end of file diff --git a/src/components/AdSense/index.tsx b/src/components/AdSense/index.tsx new file mode 100644 index 0000000..a869b55 --- /dev/null +++ b/src/components/AdSense/index.tsx @@ -0,0 +1,28 @@ +import { useEffect } from 'react'; + +declare global { + interface Window { + adsbygoogle: unknown[]; + } +} + +export interface AdSenseProps { + 'data-ad-slot': string; +} + +export function AdSense(props: AdSenseProps) { + useEffect(() => { + (window.adsbygoogle = window.adsbygoogle || []).push({}); + }, []); + + return ( + + ); +} diff --git a/src/components/AlertModal/AlertModal.tsx b/src/components/AlertModal/AlertModal.tsx deleted file mode 100644 index e777c7b..0000000 --- a/src/components/AlertModal/AlertModal.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import styled from 'styled-components'; -import { ModalContainer, ModalBackground, Button } from '@Styles/theme'; -import { FormattedMessage } from 'react-intl'; - -interface AlertModalProps { - alertModal: string; - setAlertModal: React.Dispatch>; -} -function AlertModal({ alertModal, setAlertModal }: AlertModalProps) { - const [isModalOpen, setIsModalOpen] = useState(false); - - useEffect(() => { - alertModal ? setIsModalOpen(true) : setIsModalOpen(false); - }, [alertModal]); - - return ( - - - -

- -

- -
-
- ); -} - -export default AlertModal; - -const Modal = styled.div` - position: fixed; - top: 80%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 100; - width: 370px; - border-radius: 20px; - background-color: #e4e4e7; - border: 2px solid #27272a; - color: #27272a; - padding: 40px 20px; - display: flex; - flex-direction: column; - align-items: center; - gap: 50px; - font-size: 24px; - font-weight: 500; - h1 { - line-height: 1.2; - } - button { - font-size: 20px; - } -`; diff --git a/src/components/AlertModal/index.tsx b/src/components/AlertModal/index.tsx new file mode 100644 index 0000000..446be83 --- /dev/null +++ b/src/components/AlertModal/index.tsx @@ -0,0 +1,42 @@ +import { FormattedMessage } from 'react-intl'; +import { ModalBackground, Button } from '@Styles/theme'; +import * as S from './style'; +import { ModalBase } from '@Base/components/ModalBase'; + +interface AlertModalProps { + isOpen: boolean; + title?: string; + textSize?: 'sm' | 'md'; + handleClose: () => void; +} + +const AlertModal = ({ + isOpen, + title, + children, + textSize = 'md', + handleClose, +}: AlertModalProps & React.PropsWithChildren) => { + return ( + } + onClose={handleClose} + > + + {title && ( + + + + )} + {children} + + + + + ); +}; + +export default AlertModal; diff --git a/src/components/AlertModal/style.ts b/src/components/AlertModal/style.ts new file mode 100644 index 0000000..5661489 --- /dev/null +++ b/src/components/AlertModal/style.ts @@ -0,0 +1,37 @@ +import styled from 'styled-components'; + +export const Modal = styled.div` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 100; + + display: flex; + flex-direction: column; + gap: 16px; + + width: calc(100% - 64px); + max-width: calc(var(--viewport-max-width) - 64px); + padding: 24px 16px; + border-radius: 20px; + box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), + 0 8px 10px -6px rgb(0 0 0 / 0.1); + background-color: ${({ theme }) => theme.gray[50]}; + color: ${({ theme }) => theme.gray[800]}; + + white-space: break-spaces; + + button { + font-size: 1.25rem; + } +`; + +export const Title = styled.h6``; + +export const Message = styled.div<{ $textSize: 'sm' | 'md' }>` + margin-bottom: 0.5rem; + font-size: ${({ $textSize }) => $textSize === 'sm' && '0.875rem'}; + line-height: 1.4; + text-align: initial; +`; diff --git a/src/pages/_app.page.tsx b/src/pages/_app.page.tsx index 242af44..ef41e43 100644 --- a/src/pages/_app.page.tsx +++ b/src/pages/_app.page.tsx @@ -9,6 +9,7 @@ import GlobalStyle from '@Styles/GlobalStyle'; import theme from '@Styles/theme'; import '@fortawesome/fontawesome-svg-core/styles.css'; import { config } from '@fortawesome/fontawesome-svg-core'; + config.autoAddCss = false; const App = ({ Component, pageProps }: AppProps) => { @@ -17,8 +18,8 @@ const App = ({ Component, pageProps }: AppProps) => { return ( <> - ์˜ค๋น  ํ†ค ๋งŽ์•„? ํผ์Šค๋„ ์ปฌ๋Ÿฌ ์ž๊ฐ€์ง„๋‹จ + ์˜ค๋น  ํ†ค ๋งŽ์•„? ํผ์Šค๋„ ์ปฌ๋Ÿฌ ์ž๊ฐ€์ง„๋‹จ ํ…Œ์ŠคํŠธ diff --git a/src/pages/_document.page.tsx b/src/pages/_document.page.tsx index e4973d3..a88578b 100644 --- a/src/pages/_document.page.tsx +++ b/src/pages/_document.page.tsx @@ -9,7 +9,7 @@ import Document, { } from 'next/document'; import { ServerStyleSheet } from 'styled-components'; -export default class MyDcoument extends Document { +export default class MyDocument extends Document { static async getInitialProps( ctx: DocumentContext ): Promise { @@ -42,11 +42,35 @@ export default class MyDcoument extends Document { return ( + + + + + {/* og tag */} - + - + + {/* Google Search Console */} { +const AllTypesView = ({ intl }: WrappedComponentProps) => { const [selectedIndex, setSelectedIndex] = useState( undefined ); @@ -40,14 +44,17 @@ const AllTypesView = () => { ({ - title: name.replace(' ', ''), - color: - hoveredIndex === index || selectedIndex === index - ? color[index].textColor - : theme.gray[50], - value: 1, - }))} + data={color.map(({ type }, index) => { + const message = intl.formatMessage({ id: `${type}.name` }); + return { + title: message, + color: + hoveredIndex === index || selectedIndex === index + ? color[index].textColor + : theme.gray[50], + value: 1, + }; + })} label={({ dataEntry }) => dataEntry.title} labelStyle={(index) => ({ ...defaultLabelStyle, @@ -116,4 +123,4 @@ const AllTypesView = () => { ); }; -export default AllTypesView; +export default injectIntl(AllTypesView); diff --git a/src/pages/all-types-view/style.ts b/src/pages/all-types-view/style.ts index 9d72d6b..f652f58 100644 --- a/src/pages/all-types-view/style.ts +++ b/src/pages/all-types-view/style.ts @@ -14,7 +14,7 @@ type TagStyleProps = { export const Wrapper = styled.div` ${flexCustom('column', 'center', 'flex-start')} width: 100%; - max-width: 400px; + max-width: var(--viewport-max-width); height: 100%; padding: 32px; margin: 0 auto; @@ -37,7 +37,7 @@ export const Title = styled.h1` `; export const PieChart = styled(_PieChart)` - max-height: calc(400px - 32px * 2); + max-height: calc(var(--viewport-max-width) - 32px * 2); margin: 32px 0; `; diff --git a/src/pages/choice-color/Guidance/index.tsx b/src/pages/choice-color/Guidance/index.tsx index 40912a9..2187398 100644 --- a/src/pages/choice-color/Guidance/index.tsx +++ b/src/pages/choice-color/Guidance/index.tsx @@ -1,16 +1,49 @@ +import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons'; +import AlertModal from '@Components/AlertModal'; + import * as S from './style'; function Guidance() { + const [isOpenGuideModal, setIsOpenGuideModal] = useState(false); + + const handleOpenGuideModal = () => { + setIsOpenGuideModal(true); + }; + return ( - - -

+ + {' '} + + {isOpenGuideModal && ( + setIsOpenGuideModal(false)} + > + + {[ + 'colorChoiceGuideExplanation_1', + 'colorChoiceGuideExplanation_2', + 'colorChoiceGuideExplanation_3', + ].map((messageId) => ( +

+ +

+ ))} + + + )} +
-

-
+
+ ); } diff --git a/src/pages/choice-color/Guidance/style.ts b/src/pages/choice-color/Guidance/style.ts index cb7a289..0d36142 100644 --- a/src/pages/choice-color/Guidance/style.ts +++ b/src/pages/choice-color/Guidance/style.ts @@ -1,14 +1,20 @@ +import { flexCustom } from '@Styles/theme'; import styled from 'styled-components'; -export const Explanation = styled.div` +export const Guidance = styled.div` margin-bottom: 20px; font-size: 16px; line-height: 1.6em; text-align: center; +`; + +export const Explanation = styled.p` + margin-top: 8px; + color: ${({ theme }) => theme.gray[400]}; + font-size: 14px; +`; - p { - margin-top: 8px; - color: ${({ theme }) => theme.gray[400]}; - font-size: 14px; - } +export const ColorChoiceGuideWrapper = styled.div` + ${flexCustom('column', 'flex-start', 'flex-start')} + row-gap: 0.75rem; `; diff --git a/src/pages/choice-color/index.page.tsx b/src/pages/choice-color/index.page.tsx index 50a4166..55ee944 100644 --- a/src/pages/choice-color/index.page.tsx +++ b/src/pages/choice-color/index.page.tsx @@ -1,18 +1,15 @@ -import { useState, useMemo, useEffect } from 'react'; -import { useRouter } from 'next/router'; -import { useRecoilValue } from 'recoil'; -import { CropImage } from '@Recoil/app'; +import { useState, useMemo } from 'react'; import useSelectBonusColorTypes from '@Hooks/useSelectBonusColorTypes'; import choiceColorData from '@Data/choiceColorData'; import BasicStage from './BasicStage'; import BonusStage from './BonusStage'; import * as S from './style'; +import { AdSense } from '@Components/AdSense'; +import useCropImg from '@Hooks/useCropImg'; function ChoiceColor() { const [selectedTypes, setSelectedTypes] = useState([]); - const router = useRouter(); - const stageNum = selectedTypes.length; const MAX_STAGE_NUM = choiceColorData.length; @@ -26,15 +23,7 @@ function ChoiceColor() { MAX_STAGE_NUM ); - const userImg = useRecoilValue(CropImage); - - // HJ TODO: fallback component ํ•„์š” ? - useEffect(() => { - if (!router) return; - if (!userImg) { - router.push('/no-image'); - } - }, [router, userImg]); + const userImg = useCropImg(); const onBasicClick = (type: ColorType) => { setSelectedTypes((prev) => [...prev, type]); @@ -53,6 +42,7 @@ function ChoiceColor() { ) : ( )} + ); } diff --git a/src/pages/choice-color/style.ts b/src/pages/choice-color/style.ts index 3d2bdac..37c66c6 100644 --- a/src/pages/choice-color/style.ts +++ b/src/pages/choice-color/style.ts @@ -7,6 +7,6 @@ export const Wrapper = styled.div` gap: 10px; margin: 0 auto; padding: 20px; - max-width: 400px; + max-width: var(--viewport-max-width); height: 100%; `; diff --git a/src/pages/image-upload/FaceDetection/index.tsx b/src/pages/image-upload/FaceDetection/index.tsx index 6cc3335..f4932a0 100644 --- a/src/pages/image-upload/FaceDetection/index.tsx +++ b/src/pages/image-upload/FaceDetection/index.tsx @@ -11,14 +11,14 @@ import * as S from './style'; interface FaceDetectionProps { imageFile: File; - setIsModalOpen: React.Dispatch>; - setAlertModal: React.Dispatch>; + setAlertMessage: React.Dispatch>; + handleClose: () => void; } function FaceDetection({ imageFile, - setIsModalOpen, - setAlertModal, + setAlertMessage, + handleClose, }: FaceDetectionProps) { const [image, setImage] = useState(''); const [scale, setScale] = useState(1); @@ -30,8 +30,8 @@ function FaceDetection({ const file = imageFile; if (!file.type.startsWith('image/')) { - setIsModalOpen(false); - setAlertModal('alertSelectImg'); + handleClose(); + setAlertMessage('alertSelectImg'); return; } @@ -47,7 +47,7 @@ function FaceDetection({ throw Error('์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); }; - }, [imageFile]); + }, [handleClose, imageFile, setAlertMessage]); const OnChange = (event: React.ChangeEvent) => { const { name, value } = event.target; @@ -76,8 +76,8 @@ function FaceDetection({ errorMsg = 'alertNoImg'; } - setAlertModal(errorMsg); - setIsModalOpen(false); + setAlertMessage(errorMsg); + handleClose(); }; return ( diff --git a/src/pages/image-upload/index.page.tsx b/src/pages/image-upload/index.page.tsx index 755e2a9..f4933cf 100644 --- a/src/pages/image-upload/index.page.tsx +++ b/src/pages/image-upload/index.page.tsx @@ -1,23 +1,30 @@ import { useRef, useState } from 'react'; import Link from 'next/link'; -import { useRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { FormattedMessage } from 'react-intl'; + import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faUserPlus } from '@fortawesome/free-solid-svg-icons'; import { faFaceSmile } from '@fortawesome/free-regular-svg-icons'; import { CropImage } from '@Recoil/app'; import ROUTE_PATH from '@Constant/routePath'; -import AlertModal from '@Components/AlertModal/AlertModal'; +import AlertModal from '@Components/AlertModal'; import theme, { Modal, ModalBackground, ModalContainer } from '@Styles/theme'; -import FaceDetection from './FaceDetection'; +import { useModal } from '@Base/hooks/useModal'; +import FaceDetection from './FaceDetection'; import * as S from './style'; function ImageUploadPage() { const [imageFile, setImageFile] = useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); - const [alertModal, setAlertModal] = useState(''); - const imagePreviewURL = useRecoilState(CropImage)[0]; + + const { + isOpen: isOpenImageUploadModal, + open: openImageUploadModal, + close: closeImageUploadModal, + } = useModal({}); + const [alertMessage, setAlertMessage] = useState(''); + const imagePreviewURL = useRecoilValue(CropImage); const inputRef: React.RefObject = useRef(null); @@ -28,31 +35,36 @@ function ImageUploadPage() { const selectImage = (event: React.ChangeEvent) => { if (event.target.files) { setImageFile(event.target.files[0]); - setIsModalOpen(true); + openImageUploadModal(); return; } - setAlertModal('alertRetry'); + setAlertMessage('alertRetry'); }; return ( - - {isModalOpen && imageFile ? ( + + {isOpenImageUploadModal && imageFile ? ( <> ) : null} - {alertModal && ( - + {alertMessage && ( + setAlertMessage('')} + > + + )} - + {imagePreviewURL ? ( <> @@ -105,7 +117,8 @@ function ImageUploadPage() { - + {/* */} + diff --git a/src/pages/image-upload/style.ts b/src/pages/image-upload/style.ts index c30065d..4efff79 100644 --- a/src/pages/image-upload/style.ts +++ b/src/pages/image-upload/style.ts @@ -9,8 +9,8 @@ export const CroppedImageBox = styled(Image)` z-index: 1; `; -export const FlexContainer = styled.div<{ isModalOpen: boolean }>` - display: ${({ isModalOpen }) => (isModalOpen ? 'hidden' : 'block')}; +export const FlexContainer = styled.div<{ isOpen: boolean }>` + display: ${({ isOpen }) => (isOpen ? 'hidden' : 'block')}; height: 100dvh; ${flexCustom('column', 'center', 'center')} padding: 40px 20px; diff --git a/src/pages/result/index.logic.ts b/src/pages/result/index.logic.ts index 9e58266..48eeba3 100644 --- a/src/pages/result/index.logic.ts +++ b/src/pages/result/index.logic.ts @@ -22,7 +22,7 @@ export function useLateColorType() { return colorType; } - return null; + return 'springbright'; }, [router.isReady, router.query]); const status: LateStatus = useMemo(() => { diff --git a/src/pages/result/index.page.tsx b/src/pages/result/index.page.tsx index 3977124..b23fc54 100644 --- a/src/pages/result/index.page.tsx +++ b/src/pages/result/index.page.tsx @@ -1,13 +1,17 @@ -import { useRef, useState } from 'react'; +import { useRef } from 'react'; +import Head from 'next/head'; +import { FormattedMessage } from 'react-intl'; -import resultColorData from '@Data/resultColorData'; -import ColorChipSpinner from '@Components/ColorChipSpinner'; +import { createConsecutiveNumbers } from '@Base/utils/arrExtension'; +import useScrollTop from '@Base/hooks/useScrollTop'; +import resultColorData from '@Data/resultColorData'; import RestartButton from '@Pages/result/RestartButton'; +import ColorChipSpinner from '@Components/ColorChipSpinner'; import LoadingIndicator from '@Components/LoadingIndicator'; -import useScrollTop from '@Hooks/useScrollTop'; +import Tag from '@Components/Tag'; +import { AdSense } from '@Components/AdSense'; -import * as S from './style'; import ShareSubPage from './share.subPage'; import PaletteSubPage from './palette.subPage'; import { @@ -15,24 +19,17 @@ import { useNavigateByColorType, useLateColorType, } from './index.logic'; - -import { FormattedMessage } from 'react-intl'; -import Tag from '@Components/Tag'; -import AlertModal from '@Components/AlertModal/AlertModal'; +import * as S from './style'; // HJ TODO: ๋กœ์ง๊ณผ ๋ Œ๋”๋ง ๊ด€์‹ฌ ๋ถ„๋ฆฌ function ResultPage(): JSX.Element { const resultContainerRef = useRef(null); - // hooks useScrollTop(); useClearPageTheme(); const { data: colorType, status, error } = useLateColorType(); - const onClickAnotherResult = useNavigateByColorType(); - - // alert modal - const [alertModal, setAlertModal] = useState(''); + const navigateByColorType = useNavigateByColorType(); // conditional rendering if (status === 'loading') { @@ -69,6 +66,13 @@ function ResultPage(): JSX.Element { return ( + + + + @@ -81,7 +85,7 @@ function ResultPage(): JSX.Element { - {[0, 1, 2, 3, 4].map((index, number) => ( + {createConsecutiveNumbers(5).map((index, number) => (
  • @@ -117,7 +121,7 @@ function ResultPage(): JSX.Element { ({ title, type, name, textColor, bestColors }) => ( onClickAnotherResult(type)} + onClick={() => navigateByColorType(type)} > @@ -137,14 +141,11 @@ function ResultPage(): JSX.Element { ) )}
    - {alertModal && ( - - )} +
    ); } diff --git a/src/pages/result/palette.subPage.tsx b/src/pages/result/palette.subPage.tsx index 59a5d98..a38b789 100644 --- a/src/pages/result/palette.subPage.tsx +++ b/src/pages/result/palette.subPage.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react'; +import { useRef } from 'react'; import * as S from './style'; import Palette from '@Components/Palette'; @@ -9,6 +9,7 @@ import ColorTransition, { import { useChangeTheme } from './palette.logic'; import useCropImg from '@Hooks/useCropImg'; import { FormattedMessage } from 'react-intl'; +import { useTrigger } from '@Base/hooks/useTrigger'; interface PaletteSubPageProps { colors: Color[]; @@ -16,13 +17,13 @@ interface PaletteSubPageProps { function PaletteSubPage({ colors }: PaletteSubPageProps) { const transitionRef = useRef(null); - const [isBeforeClick, setIsBeforeClick] = useState(false); + const { isTriggered: isBeforeClick, trigger } = useTrigger({}); + const changeTheme = useChangeTheme(); const cropImg = useCropImg(); - const changeTheme = useChangeTheme(); const onClickPalette = (color: string) => { - setIsBeforeClick(true); + !isBeforeClick && trigger(); transitionRef.current?.play(color); changeTheme(color); }; diff --git a/src/pages/result/share.subPage.tsx b/src/pages/result/share.subPage.tsx index 9fd0d11..2ca767e 100644 --- a/src/pages/result/share.subPage.tsx +++ b/src/pages/result/share.subPage.tsx @@ -6,10 +6,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faDownload, faLink, faShare } from '@fortawesome/free-solid-svg-icons'; import kakaoIcon from 'public/images/icon/kakaoIcon.png'; +import { isEmpty, isFalse } from '@Base/utils/check'; +import AlertModal from '@Components/AlertModal'; +import { useModal } from '@Base/hooks/useModal'; import ROUTE_PATH from '@Constant/routePath'; import useKakaoShare from '@Hooks/useKakaoShare'; import { copyUrl } from '@Utils/clipboard'; -import { webShare } from '@Utils/share'; +import { canWebShare, webShare } from '@Utils/share'; import { isChrome, isOSX } from '@Utils/userAgent'; import RestartButton from '@Pages/result/RestartButton'; import { captureAndDownload, checkIfKakaoAndAlert } from './share.logic'; @@ -18,25 +21,28 @@ import * as S from './style'; interface MenuSubPageProps { resultContainerRef: React.RefObject; colorType: string; - setAlertModal: React.Dispatch>; } -function ShareSubPage({ - resultContainerRef, - colorType, - setAlertModal, -}: MenuSubPageProps) { +function ShareSubPage({ resultContainerRef, colorType }: MenuSubPageProps) { + // alert modal + const { + isOpen: isOpenAlertModal, + message: alertModalMessage, + open: openAlertModal, + close: closeAlertModal, + } = useModal({ defaultMessage: '' }); + const { isLoading, kakaoShare } = useKakaoShare(); const kakaoAlertMsg = checkIfKakaoAndAlert(); const onClickCapture = async () => { if (kakaoAlertMsg) { - setAlertModal(kakaoAlertMsg); + openAlertModal(kakaoAlertMsg); return; } const wrapper = resultContainerRef.current; - if (!wrapper) return; + if (isFalse(wrapper)) return; const imgName = `${colorType}-result.png`; captureAndDownload(wrapper, imgName); @@ -44,16 +50,16 @@ function ShareSubPage({ const onClickLinkCopy = async () => { if (kakaoAlertMsg) { - setAlertModal(kakaoAlertMsg); + openAlertModal(kakaoAlertMsg); return; } const copyAlertMsg = await copyUrl(location.href); - setAlertModal(copyAlertMsg); + openAlertModal(copyAlertMsg); }; const onClickKakaoShare = () => { if (isLoading) { - setAlertModal('alertRetry'); + openAlertModal('alertRetry'); } else { kakaoShare(); } @@ -61,12 +67,17 @@ function ShareSubPage({ const onClickShare = async () => { if (kakaoAlertMsg) { - setAlertModal(kakaoAlertMsg); + openAlertModal(kakaoAlertMsg); return; } if (isChrome() && isOSX()) { - setAlertModal('alertMacOS'); + openAlertModal('alertMacOS'); + return; + } + + if (!canWebShare) { + openAlertModal('alertNotSupportedBrowser'); return; } @@ -126,6 +137,11 @@ function ShareSubPage({ + {isOpenAlertModal && !isEmpty(alertModalMessage) && ( + + + + )} ); } diff --git a/src/pages/result/style.ts b/src/pages/result/style.ts index 0b7bdfe..db5fada 100644 --- a/src/pages/result/style.ts +++ b/src/pages/result/style.ts @@ -10,7 +10,7 @@ interface ColorItemStyleProps { export const Wrapper = styled.div` ${flexCustom('column', 'inherit', 'flex-start')} margin: 0 auto; - max-width: 400px; + max-width: var(--viewport-max-width); `; export const ResultContainer = styled.div` @@ -21,7 +21,7 @@ export const ResultContainer = styled.div` export const LoadingWrapper = styled.div` ${flexCustom('column', 'center', 'center')} row-gap: 40px; - max-width: 400px; + max-width: var(--viewport-max-width); margin: 0 auto; padding: 48px 32px 30px 36px; `; diff --git a/src/pages/style.ts b/src/pages/style.ts index fd01eca..b27527e 100644 --- a/src/pages/style.ts +++ b/src/pages/style.ts @@ -6,7 +6,7 @@ export const LandingWrap = styled.div` ${flexCustom('column')} margin: 0 auto; padding: 48px 0 36px; - max-width: 400px; + max-width: var(--viewport-max-width); height: 100%; background-color: ${({ theme }) => theme.gray[100]}; `; diff --git a/src/styles/GlobalStyle.ts b/src/styles/GlobalStyle.ts index a7d185a..6df5649 100644 --- a/src/styles/GlobalStyle.ts +++ b/src/styles/GlobalStyle.ts @@ -23,7 +23,8 @@ const GlobalStyle = createGlobalStyle` ${ResetStyle} :root { - --font-jalnan: ${jalnan.style.fontFamily} + --font-jalnan: ${jalnan.style.fontFamily}; + --viewport-max-width: 400px; } * { diff --git a/src/styles/theme.ts b/src/styles/theme.ts index b85ee26..2f3baf8 100644 --- a/src/styles/theme.ts +++ b/src/styles/theme.ts @@ -1,3 +1,4 @@ +import { CSSProperties } from 'react'; import styled, { css } from 'styled-components'; const theme = { @@ -20,9 +21,9 @@ const theme = { export default theme; export const flexCustom = ( - flexDirection = 'initial', - alignItems = 'center', - justifyContent = 'center' + flexDirection: CSSProperties['flexDirection'] = 'initial', + alignItems: CSSProperties['alignItems'] = 'center', + justifyContent: CSSProperties['justifyContent'] = 'center' ) => css` display: flex; flex-direction: ${flexDirection}; @@ -33,6 +34,7 @@ export const flexCustom = ( export const Button = styled.button` padding: 16px 0; width: 320px; + max-width: 100%; border-radius: 20px; background-color: ${theme.gray[800]}; @@ -62,14 +64,14 @@ export const BorderedButton = styled(Button)` `; type ModalContainerProps = { - isModalOpen: boolean; + isOpen: boolean; }; export const ModalContainer = styled.div` position: relative; height: 100%; - max-height: ${({ isModalOpen }) => (isModalOpen ? '100dvh' : 'none')}; - overflow: ${({ isModalOpen }) => (isModalOpen ? 'hidden' : 'auto')}; + max-height: ${({ isOpen }) => (isOpen ? '100dvh' : 'none')}; + overflow: ${({ isOpen }) => (isOpen ? 'hidden' : 'auto')}; `; export const ModalBackground = styled.div` @@ -78,7 +80,7 @@ export const ModalBackground = styled.div` left: 0; width: 100%; height: 100%; - background-color: ${({ theme }) => theme.gray[500]}; + background-color: ${({ theme }) => theme.gray[800]}; opacity: 0.5; z-index: 10; `; diff --git a/src/utils/flattenMessages.ts b/src/utils/flattenMessages.ts index b971d2e..90c1c19 100644 --- a/src/utils/flattenMessages.ts +++ b/src/utils/flattenMessages.ts @@ -1,20 +1,24 @@ -// ํƒ€์ž… ์ˆ˜์ •ํ•˜๊ธฐ -const flattenMessages = (nestedMessages: any, prefix = '') => { - if (nestedMessages === null) { - return {}; - } +interface NestedMessages { + [key: string]: string | string[] | NestedMessages; +} + +const flattenMessages = (nestedMessages: NestedMessages, prefix = '') => { return Object.keys(nestedMessages).reduce((messages, key) => { const value = nestedMessages[key]; const prefixedKey = prefix ? `${prefix}.${key}` : key; if (typeof value === 'string') { Object.assign(messages, { [prefixedKey]: value }); + } else if (Array.isArray(value)) { + value.forEach((msg, index) => { + Object.assign(messages, { [`${prefixedKey}.${index}`]: msg }); + }); } else { Object.assign(messages, flattenMessages(value, prefixedKey)); } return messages; - }, {}); + }, {} as Record); }; export default flattenMessages; diff --git a/tsconfig.json b/tsconfig.json index 06cb1d3..082423c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,9 @@ "@Recoil/*": ["./src/recoil/*"], "@Styles/*": ["./src/styles/*"], "@Utils/*": ["./src/utils/*"], - "@Translations/*": ["./src/translations/*"] + "@Translations/*": ["./src/translations/*"], + // @Base + "@Base/*": ["./base/*"] }, "allowJs": true, "jsx": "preserve", @@ -40,7 +42,9 @@ "src/**/*.ts", "src/**/*.tsx", "@types/**/*d.ts", - ".next/types/**/*.ts" + ".next/types/**/*.ts", + "base/**/*.ts", + "base/**/*.tsx" ], "exclude": ["node_modules", "src/libs"] }