├── .babelrc ├── .browserlistrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── components └── Box │ ├── index.test.tsx │ └── index.tsx ├── custom.d.ts ├── hooks ├── useAxios.js ├── useBeforeLeave.js ├── useClick.js ├── useConfirm.js ├── useFadeIn.js ├── useFullscreen.js ├── useInput.js ├── useNetwork.js ├── useNotification.js ├── usePreventLeave.js ├── useScroll.js ├── useTabs.js └── useTitle.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.tsx └── index.tsx ├── shared └── const.ts ├── styles ├── global-style.ts ├── styled.d.ts └── theme.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel" 4 | ], 5 | "plugins": [ 6 | [ 7 | "babel-plugin-styled-components", 8 | { 9 | "fileName": true, 10 | "displayName": true, 11 | "pure": true 12 | } 13 | ] 14 | ] 15 | } -------------------------------------------------------------------------------- /.browserlistrc.js: -------------------------------------------------------------------------------- 1 | export const browserslist = ["defaults"]; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Config 2 | !config/default.json 3 | !config/development.json.sample 4 | config/*.json 5 | 6 | # Next 7 | .next 8 | out 9 | 10 | # Logs 11 | npm-debug.log* 12 | coverage 13 | 14 | # NPM 15 | node_modules/ 16 | 17 | # Transpiled code 18 | dist 19 | out 20 | .out 21 | 22 | # Dev tools 23 | .DS_Store 24 | .vscode 25 | .idea 26 | *.swp 27 | *.bak 28 | 29 | node_modules.nosync/ 30 | *.env.* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "prettier.semi": false, 4 | "prettier.trailingComma": "all", 5 | "prettier.singleQuote": true, 6 | "prettier.tslintIntegration": true, 7 | "prettier.tabWidth": 2, 8 | "prettier.printWidth": 120 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | next-styled 2 | -------------------------------------------------------------------------------- /components/Box/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import "@testing-library/jest-dom/extend-expect"; 4 | import Box from "./index"; 5 | 6 | it("renders the icon button", () => { 7 | render(); 8 | expect(screen.queryByText("box")); 9 | }); 10 | -------------------------------------------------------------------------------- /components/Box/index.tsx: -------------------------------------------------------------------------------- 1 | import { BoxProps } from "shared/const"; 2 | 3 | const Box = ({ name }: BoxProps) =>
{name}
; 4 | 5 | export default Box; 6 | -------------------------------------------------------------------------------- /custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: any; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /hooks/useAxios.js: -------------------------------------------------------------------------------- 1 | import defaultAxios from "axios"; 2 | 3 | const useAxios = (opts, axiosInstance = defaultAxios) => { 4 | const [state, setState] = useStste({ 5 | loading: true, 6 | error: null, 7 | data: null 8 | }); 9 | const [trigger, setTrigger] = useState(0); 10 | if (!opts.url) { 11 | return; 12 | } 13 | const refetch = () => { 14 | setState({ 15 | ...state, 16 | loading: true 17 | }) 18 | setTrigger(new Date.now()); 19 | } 20 | useEffect(() => { 21 | axiosInstance(opts) 22 | .then(data => { 23 | setState({ 24 | ...state, 25 | loading: false, 26 | data 27 | }); 28 | }) 29 | .catch(error => { 30 | setState({ ...state, loading: false, error }); 31 | }); 32 | }, [trigger]); 33 | return {...state, refetch} 34 | }; 35 | 36 | export default useAxios; 37 | -------------------------------------------------------------------------------- /hooks/useBeforeLeave.js: -------------------------------------------------------------------------------- 1 | export const useBeforeLeave = onBefore => { 2 | if (typeof onBefore !== "function") { 3 | return; 4 | } 5 | const handle = (event) => { 6 | const { clientY } = event; 7 | if(clientY <= 0) {onBefore()}; 8 | }; 9 | useEffect(() => { 10 | document.addEventListener("mouseleave", handle); 11 | return () => document.removeEventListener("mouseleave", handle); 12 | }, []); 13 | }; -------------------------------------------------------------------------------- /hooks/useClick.js: -------------------------------------------------------------------------------- 1 | export const useClick = onClick => { 2 | if (typeof onClick !== "function") { 3 | return; 4 | } 5 | const element = useRef(); 6 | useEffect(() => { 7 | if (element.current) { 8 | element.current.addEventListener("click", onClick); 9 | } 10 | return () => { 11 | if (element.current) { 12 | element.current.removeEventListener("click", onClick); 13 | } 14 | }; 15 | }, []); 16 | return element; 17 | }; 18 | -------------------------------------------------------------------------------- /hooks/useConfirm.js: -------------------------------------------------------------------------------- 1 | export const useConfirm = (message = "", onConfirm, onCancel) => { 2 | if (!onConfirm || typeof onConfirm !== "function") { 3 | return; 4 | } 5 | if (onCancel && typeof onCancel !== "function") { 6 | return; 7 | } 8 | const confirmAction = () => { 9 | if (confirm(message)) { 10 | onConfirm(); 11 | } else { 12 | onCancel(); 13 | } 14 | }; 15 | return confirmAction; 16 | }; 17 | -------------------------------------------------------------------------------- /hooks/useFadeIn.js: -------------------------------------------------------------------------------- 1 | export const useFadeIn = (duration = 1, delay = 0) => { 2 | if (typeof duration !== "number" || typeof delay !== "number") 3 | const element = useRef(); 4 | useEffect(() => { 5 | if (element.current) { 6 | const { current } = element; 7 | current.style.transition = `opacity ${duration}s ease-in-out ${delay}s`; 8 | current.style.opacity = 1; 9 | } 10 | }, []); 11 | return { ref: element, style: { opacity: 0 } }; 12 | }; 13 | -------------------------------------------------------------------------------- /hooks/useFullscreen.js: -------------------------------------------------------------------------------- 1 | export const useFullscreen = callback => { 2 | const element = useRef(); 3 | const runCb = isFull => { 4 | if (callback && typeof callback === "function") { 5 | callback(isFull); 6 | } 7 | }; 8 | const triggerFull = () => { 9 | if (element.current) { 10 | if (element.current.requestFullscreen) { 11 | element.current.requestFullscreen(); 12 | } else if (element.current.mozRequestFullScreen) { 13 | element.current.mozRequestFullScreen(); 14 | } else if (element.current.webkitRequestFullscreen) { 15 | element.current.webkitRequestFullscreen(); 16 | } else if (element.current.msRequestFullscreen) { 17 | element.current.msRequestFullscreen(); 18 | } 19 | runCb(true); 20 | } 21 | }; 22 | const exitFull = () => { 23 | if (document.exidocumenttFullscreen) { 24 | document.exitFullscreen(); 25 | } else if (document.mozCancelFullScreen) { 26 | document.mozCancelFullScreen(); 27 | } else if (document.webkitExitFullscreen) { 28 | document.webkitExitFullscreen(); 29 | } else if (document.msExitFullscreen) { 30 | document.msExitFullscreen(); 31 | } 32 | runCb(false); 33 | }; 34 | return { element, triggerFull, exitFull }; 35 | }; 36 | -------------------------------------------------------------------------------- /hooks/useInput.js: -------------------------------------------------------------------------------- 1 | export const useInput = (initialValue, validator) => { 2 | const [value, setValue] = useState(initialValue); 3 | const onChange = event => { 4 | const { 5 | target: { value } 6 | } = event; 7 | let willUpdate = true; 8 | if (typeof validator === "fuction") { 9 | willUpdate = validator(value); 10 | } 11 | if (willUpdate) { 12 | setValue(value); 13 | } 14 | }; 15 | return { value, onChange }; 16 | }; 17 | -------------------------------------------------------------------------------- /hooks/useNetwork.js: -------------------------------------------------------------------------------- 1 | export const useNetwork = onChange => { 2 | const [status, setStatus] = useState(navigator.onLine); 3 | const handleChange = () => { 4 | if (typeof onChange === "function") { 5 | onChange(navigator.onLine); 6 | } 7 | setStatus(navigator.onLine); 8 | }; 9 | useEffect(() => { 10 | window.addEventListener("online", handleChange); 11 | window.addEventListener("offline", handleChange); 12 | return () => { 13 | window.removeEventListener("online", handleChange); 14 | window.removeEventListener("offline", handleChange); 15 | }; 16 | }, []); 17 | return status; 18 | }; 19 | -------------------------------------------------------------------------------- /hooks/useNotification.js: -------------------------------------------------------------------------------- 1 | export const useNotification = (title, options) => { 2 | if (!("Notification" in window)) { 3 | return; 4 | } 5 | const fireNotif = () => { 6 | if (Notification.permission !== "granted") { 7 | Notification.requestPermission().then(permission => { 8 | if (permission === "granted") { 9 | new Notification(title, options); 10 | } else { 11 | return; 12 | } 13 | }); 14 | } else { 15 | new Notification(title, options); 16 | } 17 | }; 18 | return fireNotif; 19 | j; 20 | }; 21 | -------------------------------------------------------------------------------- /hooks/usePreventLeave.js: -------------------------------------------------------------------------------- 1 | export const usePreventLeave = () => { 2 | const listener = event => { 3 | event.preventDefault(); 4 | event.returnValue = ""; 5 | }; 6 | const enablePrevent = () => 7 | window.addEventListener("beforeunload", listener); 8 | const disablePrevent = () => 9 | window.removeEventListener("beforeunload", listener); 10 | return { enablePrevent, disablePrevent }; 11 | }; -------------------------------------------------------------------------------- /hooks/useScroll.js: -------------------------------------------------------------------------------- 1 | export const useScroll = () => { 2 | const [state, setState] = useState({ 3 | x: 0, 4 | y: 0 5 | }); 6 | const onScroll = () => { 7 | setState({ 8 | x: window.scrollX, 9 | y: window.scrollY 10 | }); 11 | }; 12 | useEffect(() => { 13 | window.addEventListener("scroll", onScroll); 14 | return () => window.removeEventListener("scroll", onScroll); 15 | }, []); 16 | return state; 17 | }; 18 | -------------------------------------------------------------------------------- /hooks/useTabs.js: -------------------------------------------------------------------------------- 1 | export const useTabs = (initialTab, allTabs) => { 2 | if (!allTabs || !Array.isArray(allTabs)) { 3 | return; 4 | } 5 | const [currentIndex, setCurrentIndex] = useState(initialTab); 6 | return { 7 | currentItem: allTabs[currentIndex], 8 | changeItem: setCurrentIndex 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /hooks/useTitle.js: -------------------------------------------------------------------------------- 1 | export const useTitle = initialTitle => { 2 | const [title, setTitle] = useState(initialTitle); 3 | const updateTitle = () => { 4 | const htmlTitle = document.querySelector("title"); 5 | htmlTitle.innerText = title; 6 | }; 7 | useEffect(updateTitle, [title]); 8 | return setTitle; 9 | }; 10 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withBundleAnalyzer = require("@next/bundle-analyzer")({ 2 | enabled: process.env.ANALYZE === "true", 3 | }); 4 | 5 | module.exports = withBundleAnalyzer({ 6 | target: "serverless", 7 | env: { 8 | BASE_URL: process.env.BASE_URL, 9 | }, 10 | 11 | webpack(conf) { 12 | conf.module.rules.push({ 13 | test: /\.svg$/, 14 | use: [ 15 | { 16 | loader: "@svgr/webpack", 17 | options: { 18 | svgoConfig: { 19 | plugins: [ 20 | { 21 | // Enable figma's wrong mask-type attribute work 22 | removeRasterImages: false, 23 | removeStyleElement: false, 24 | removeUnknownsAndDefaults: false, 25 | // Enable svgr's svg to fill the size 26 | removeViewBox: false, 27 | }, 28 | ], 29 | }, 30 | }, 31 | }, 32 | ], 33 | }); 34 | // 절대경로 35 | conf.resolve.modules.push(__dirname); 36 | return conf; 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "intern-2021-03", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://oss.navercorp.com/booking/intern-2021-03.git", 6 | "author": "Lee Jeong Min ", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "next", 10 | "debug": "NODE_OPTIONS='--inspect' next dev", 11 | "build": "next build", 12 | "start": "next start", 13 | "export": "next export", 14 | "type-check": "tsc", 15 | "eslint": "eslint .", 16 | "analyze": "ANALYZE=true next build", 17 | "test": "jest", 18 | "storybook": "start-storybook -p 6006", 19 | "build-storybook": "build-storybook" 20 | }, 21 | "dependencies": { 22 | "@next/bundle-analyzer": "^10.0.7", 23 | "axios": "^0.21.1", 24 | "babel-plugin-styled-components": "^1.12.0", 25 | "css-loader": "^5.0.2", 26 | "file-loader": "^6.2.0", 27 | "next": "^10.0.5", 28 | "query-string": "^7.0.0", 29 | "react": "^17.0.1", 30 | "react-dom": "^17.0.1", 31 | "react-ripples": "^2.2.1", 32 | "react-virtualized-auto-sizer": "^1.0.4", 33 | "react-window": "^1.8.6", 34 | "styled-components": "^5.2.1", 35 | "styled-reset": "^4.3.4" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.13.10", 39 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 40 | "@svgr/webpack": "^5.5.0", 41 | "@testing-library/jest-dom": "^5.11.9", 42 | "@testing-library/react": "^11.2.5", 43 | "@types/axios": "^0.14.0", 44 | "@types/node": "^14.14.22", 45 | "@types/react": "^17.0.0", 46 | "@types/react-dom": "^17.0.0", 47 | "@types/react-window": "^1.8.2", 48 | "@types/styled-components": "^5.1.7", 49 | "@typescript-eslint/eslint-plugin": "^4.14.1", 50 | "@typescript-eslint/eslint-plugin-tslint": "^4.14.1", 51 | "@typescript-eslint/parser": "^4.14.1", 52 | "babel-jest": "^26.6.3", 53 | "babel-loader": "^8.2.2", 54 | "eslint": "^7.18.0", 55 | "eslint-config-airbnb-typescript": "^12.3.1", 56 | "eslint-config-prettier": "^8.1.0", 57 | "eslint-plugin-import": "^2.22.1", 58 | "eslint-plugin-jsx-a11y": "^6.4.1", 59 | "eslint-plugin-react": "^7.22.0", 60 | "eslint-plugin-react-hooks": "^4.2.0", 61 | "jest": "^26.6.3", 62 | "prettier": "^2.2.1", 63 | "typescript": "^4.1.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from "next/app"; 2 | import Head from "next/head"; 3 | import { ThemeProvider } from "styled-components"; 4 | import { GlobalStyle } from "../styles/global-style"; 5 | import { theme } from "../styles/theme"; 6 | 7 | function MyApp({ Component, pageProps }: AppProps) { 8 | return ( 9 | <> 10 | 11 | 12 | boilerplate 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export default MyApp; 23 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { 2 | Html, 3 | Head, 4 | Main, 5 | NextScript, 6 | DocumentContext, 7 | } from "next/document"; 8 | import { ServerStyleSheet } from "styled-components"; 9 | 10 | class MyDocument extends Document { 11 | static async getInitialProps(ctx: DocumentContext) { 12 | const sheet = new ServerStyleSheet(); 13 | const originalRenderPage = ctx.renderPage; 14 | try { 15 | ctx.renderPage = () => 16 | originalRenderPage({ 17 | enhanceApp: (App) => (props) => 18 | sheet.collectStyles(), 19 | }); 20 | 21 | const initialProps = await Document.getInitialProps(ctx); 22 | return { 23 | ...initialProps, 24 | styles: ( 25 | <> 26 | {initialProps.styles} 27 | {sheet.getStyleElement()} 28 | 29 | ), 30 | }; 31 | } finally { 32 | sheet.seal(); 33 | } 34 | } 35 | 36 | render() { 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 54 | 58 | 59 | 60 |
61 | 62 | 63 | 64 | ); 65 | } 66 | } 67 | 68 | export default MyDocument; 69 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | const Index = () => { 2 | return
index
; 3 | }; 4 | 5 | export default Index; 6 | -------------------------------------------------------------------------------- /shared/const.ts: -------------------------------------------------------------------------------- 1 | export interface BoxProps { 2 | name: string; 3 | } 4 | -------------------------------------------------------------------------------- /styles/global-style.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | import { reset } from "styled-reset"; 3 | import { media } from "./theme"; 4 | 5 | export const GlobalStyle = createGlobalStyle` 6 | ${reset} 7 | :focus { 8 | outline: none; 9 | border: none; 10 | } 11 | ::-webkit-scrollbar { 12 | display: none; 13 | } 14 | html{ 15 | font-size: 11px; 16 | -webkit-text-size-adjust: none; 17 | font-family: -apple-system,BlinkMacSystemFont,helvetica,Apple SD Gothic Neo,sans-serif; 18 | font-display: fallback; 19 | ${media.tablet}{ 20 | font-size: 10px; 21 | } 22 | -ms-overflow-style: none; 23 | scrollbar-width: none; 24 | } 25 | button { 26 | background: none; 27 | padding: 0; 28 | border: none; 29 | cursor: pointer; 30 | &:disabled { 31 | cursor: default; 32 | fill: #f2f3f4; 33 | } 34 | } 35 | 36 | .pc-tablet-only { 37 | display: block; 38 | ${media.mobile} { 39 | display: none; 40 | } 41 | } 42 | .tablet-mobile-only{ 43 | display: none; 44 | ${media.tablet}{ 45 | display:block; 46 | } 47 | } 48 | .mobile-only { 49 | display: none; 50 | ${media.mobile} { 51 | display: block; 52 | } 53 | } 54 | `; 55 | -------------------------------------------------------------------------------- /styles/styled.d.ts: -------------------------------------------------------------------------------- 1 | import "styled-components"; 2 | 3 | declare module "styled-components" { 4 | export interface DefaultTheme { 5 | color: { 6 | purple: "#8661de"; 7 | blue: "#00bac7"; 8 | gray: "#f6f6f6"; 9 | green: "#07b495"; 10 | lightGreen: "#99ecdd"; 11 | darkGray: "#54595d"; 12 | }; 13 | boxShadow: { 14 | normal: "0 3px 8px 0 rgb(0 0 0 / 10%)"; 15 | purple: "0 3px 8px 0 #d6c9ff"; 16 | blue: "0 3px 8px 0 #b3e2e6"; 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /styles/theme.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme } from "styled-components"; 2 | 3 | export const theme: DefaultTheme = { 4 | color: { 5 | purple: "#8661de", 6 | blue: "#00bac7", 7 | gray: "#f6f6f6", 8 | green: "#07b495", 9 | lightGreen: "#99ecdd", 10 | darkGray: "#54595d", 11 | }, 12 | boxShadow: { 13 | normal: "0 3px 8px 0 rgb(0 0 0 / 10%)", 14 | purple: "0 3px 8px 0 #d6c9ff", 15 | blue: "0 3px 8px 0 #b3e2e6", 16 | }, 17 | }; 18 | 19 | const customMediaQuery = (maxWidth: number): string => 20 | `@media (max-width: ${maxWidth}px)`; 21 | 22 | export const media = { 23 | custom: customMediaQuery, 24 | pc: customMediaQuery(1440), 25 | tablet: customMediaQuery(768), 26 | mobile: customMediaQuery(576), 27 | }; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "baseUrl": ".", 21 | "rootDir": ".", 22 | }, 23 | "include": [ 24 | "next-env.d.ts", 25 | "**/*.ts", 26 | "**/*.tsx", 27 | "next.config.js", 28 | "custom.d.ts", 29 | "styles", 30 | "pages", 31 | "public", 32 | ], 33 | "exclude": [ 34 | "node_modules", 35 | ] 36 | } --------------------------------------------------------------------------------