@ -1,37 +0,0 @@ |
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700&display=swap'); |
||||
|
||||
html, body { |
||||
scroll-behavior: auto; |
||||
-webkit-overflow-scrolling: touch; |
||||
font-size: 16px; |
||||
height: 100%; |
||||
} |
||||
|
||||
body { |
||||
padding: 0; |
||||
margin: 0; |
||||
} |
||||
|
||||
button { |
||||
background: none; |
||||
border: none; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
a { |
||||
text-decoration: none; |
||||
} |
||||
|
||||
* { |
||||
font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; |
||||
-webkit-font-smoothing: antialiased; |
||||
-moz-osx-font-smoothing: auto; |
||||
font-weight: 400; |
||||
-webkit-tap-highlight-color: rgba(0,0,0,0); |
||||
font-size: 1rem; |
||||
color: #333; |
||||
} |
||||
|
||||
#root { |
||||
height : 100%; |
||||
} |
@ -0,0 +1,9 @@ |
||||
html, body { |
||||
padding: 0; |
||||
margin: 0; |
||||
} |
||||
|
||||
html { |
||||
width: 100vw; |
||||
height: 100vh; |
||||
} |
Before Width: | Height: | Size: 475 B |
Before Width: | Height: | Size: 981 B |
Before Width: | Height: | Size: 1.8 KiB |
@ -1,22 +1,17 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="utf-8" /> |
||||
<title>React-Epub-Viewer Demo</title> |
||||
<meta |
||||
name="description" |
||||
content="React-Epub-Viewer Demo Page" |
||||
/> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
||||
<meta name="theme-color" content="#000000" /> |
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> |
||||
<link rel="icon" sizes="24x24" href="%PUBLIC_URL%/favicons/favicon_24x24.png" /> |
||||
<link rel="icon" sizes="48x48" href="%PUBLIC_URL%/favicons/favicon_48x48.png" /> |
||||
<link rel="icon" sizes="96x96" href="%PUBLIC_URL%/favicons/favicon_96x96.png" /> |
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicons/favicon_96x96.png" /> |
||||
<link rel="stylesheet" type="text/css" href="%PUBLIC_URL%/css/global.css"> |
||||
</head> |
||||
<body> |
||||
<div id="root"></div> |
||||
</body> |
||||
</html> |
||||
<head> |
||||
<meta charset="UTF-8"> |
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
<title>React Epub Viewer</title> |
||||
<meta |
||||
name="description" |
||||
content="React Epub Viewer powered by Epub.js" |
||||
/> |
||||
<link rel="stylesheet" type="text/css" href="%PUBLIC_URL%/css/index.css"> |
||||
</head> |
||||
<body> |
||||
<div id="root"></div> |
||||
</body> |
||||
</html> |
@ -1,15 +0,0 @@ |
||||
{ |
||||
"short_name": "React-Epub-Viewer", |
||||
"name": "React-Epub-Viewer Demo", |
||||
"icons": [ |
||||
{ |
||||
"src": "favicons/favicon_96x96.png", |
||||
"sizes": "96x96 48x48 24x24", |
||||
"type": "image/x-icon" |
||||
} |
||||
], |
||||
"start_url": ".", |
||||
"display": "standalone", |
||||
"theme_color": "#000000", |
||||
"background_color": "#ffffff" |
||||
} |
@ -1,3 +0,0 @@ |
||||
# https://www.robotstxt.org/robotstxt.html |
||||
User-agent: * |
||||
Disallow: |
@ -1,90 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import * as styles from 'lib/styles/styles' |
||||
import media from 'lib/styles/media' |
||||
import palette from 'lib/styles/palette' |
||||
import animations from 'lib/styles/animations' |
||||
// types
|
||||
import SnackbarType from 'types/snackbar' |
||||
|
||||
const Snackbar = ({ onClick, text, type }: Props)=> { |
||||
return ( |
||||
<Container onClick={onClick} type={type}> |
||||
<Content> |
||||
<Message>{text}</Message> |
||||
</Content> |
||||
</Container> |
||||
); |
||||
} |
||||
|
||||
const Container = styled.div<{ type: SnackbarType }>` |
||||
position: relative; |
||||
width: 320px; |
||||
height: auto; |
||||
max-height: 800px; |
||||
background-color: ${({ type }) => { |
||||
if (type === 'SUCCESS') {
|
||||
return palette.green4; |
||||
} else if (type === 'WARNING') { |
||||
return palette.orange4; |
||||
} else if (type === 'ERROR') { |
||||
return palette.red3; |
||||
} else if (type === 'INFO') { |
||||
return palette.blue4; |
||||
} else { |
||||
return palette.gray4; |
||||
} |
||||
} |
||||
}; |
||||
box-shadow: ${styles.boxShadow.regular}; |
||||
border-radius: 4px; |
||||
animation: ${animations.fadeInBottom} .3s cubic-bezier(0.25,0.1,0.25,1), |
||||
${animations.fadeOutBottom} .5s cubic-bezier(0.25,0.1,0.25,1) 3.6s; |
||||
cursor: pointer; |
||||
transition: .2s ${styles.transition}; |
||||
${styles.noselect}; |
||||
|
||||
&:active { |
||||
transform: scale(.97, .97); |
||||
} |
||||
|
||||
${media.small} { |
||||
border-radius: 0px; |
||||
width: 100%; |
||||
min-height: 1.8rem; |
||||
} |
||||
`;
|
||||
|
||||
const Content = styled.div` |
||||
position: relative; |
||||
display: flex; |
||||
width: 100%; |
||||
height: auto; |
||||
min-height: 2rem; |
||||
box-sizing: border-box; |
||||
padding: 12px 20px; |
||||
|
||||
${media.small} { |
||||
min-height: 1.6rem; |
||||
line-height: 1.6rem; |
||||
} |
||||
`;
|
||||
|
||||
const Message = styled.div` |
||||
position: relative; |
||||
flex-grow: 1; |
||||
color: white; |
||||
font-size: 14px; |
||||
white-space: pre-wrap; |
||||
word-break: keep-all; |
||||
vertical-align: top; |
||||
`;
|
||||
|
||||
|
||||
interface Props { |
||||
onClick(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void; |
||||
text: string; |
||||
type: SnackbarType; |
||||
} |
||||
|
||||
export default Snackbar |
@ -1,15 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import * as styles from 'lib/styles/styles' |
||||
|
||||
const ViewerWrapper = styled.div` |
||||
position: relative; |
||||
width: 100vw; |
||||
height: 100vh; |
||||
overflow-x: hidden; |
||||
display: flex; |
||||
flex-direction: column; |
||||
${styles.scrollbar(0)}; |
||||
`;
|
||||
|
||||
export default ViewerWrapper |
@ -1,72 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import * as styles from 'lib/styles/styles' |
||||
import palette from 'lib/styles/palette' |
||||
|
||||
const ColorItem = ({ name, color, onClick }: Props) => { |
||||
const onClickItem = (e: any) => { |
||||
onClick(); |
||||
e.target.blur(); |
||||
}; |
||||
|
||||
return ( |
||||
<Wrapper onClick={onClickItem}> |
||||
<ColorWrapper> |
||||
<Color color={color} /> |
||||
</ColorWrapper> |
||||
<Message>{name}</Message> |
||||
</Wrapper> |
||||
); |
||||
} |
||||
|
||||
const Wrapper = styled.button` |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
width: 100%; |
||||
height: 32px; |
||||
border-radius: 6px; |
||||
cursor: pointer; |
||||
background-color: rgba(0,0,0,0); |
||||
transition: .1s ${styles.transition}; |
||||
outline: none; |
||||
|
||||
&:hover, &:focus { |
||||
background-color: ${palette.blue3}; |
||||
|
||||
& > span { |
||||
color: #fff; |
||||
} |
||||
} |
||||
`;
|
||||
|
||||
const ColorWrapper = styled.div` |
||||
width: 16px; |
||||
height: 16px; |
||||
border-radius: 16px; |
||||
background-color: #fff; |
||||
margin-right: 16px; |
||||
`;
|
||||
|
||||
const Color = styled.div<{ color: string }>` |
||||
width: 16px; |
||||
height: 16px; |
||||
border-radius: 16px; |
||||
background-color: ${({ color }) => color}; |
||||
opacity: .4; |
||||
`;
|
||||
|
||||
const Message = styled.span` |
||||
width: 48px; |
||||
font-size: 14px; |
||||
color: #ddd; |
||||
line-height: 20px; |
||||
`;
|
||||
|
||||
interface Props { |
||||
name: string; |
||||
color: string; |
||||
onClick: () => void; |
||||
} |
||||
|
||||
export default ColorItem |
@ -1,34 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import * as styles from 'lib/styles/styles' |
||||
import palette from 'lib/styles/palette' |
||||
|
||||
const Item = ({ text, onClick }: Props) => { |
||||
return <Container onClick={onClick}>{text}</Container>; |
||||
} |
||||
|
||||
const Container = styled.button` |
||||
width: 100%; |
||||
height: 32px; |
||||
line-height: 32px; |
||||
text-align: center; |
||||
font-size: 14px; |
||||
color: #ccc; |
||||
transition: .1s ${styles.transition}; |
||||
background-color: rgba(0,0,0,0); |
||||
border-radius: 6px; |
||||
cursor: pointer; |
||||
outline: none; |
||||
|
||||
&:focus, &:hover { |
||||
color: #fff; |
||||
background-color: ${palette.red3}; |
||||
} |
||||
`;
|
||||
|
||||
interface Props { |
||||
text: string; |
||||
onClick: (e?: any) => void; |
||||
} |
||||
|
||||
export default Item |
@ -1,65 +0,0 @@ |
||||
import styled, { css } from 'styled-components' |
||||
// lib
|
||||
import { contextmenuWidth } from 'lib/styles/viewerLayout' |
||||
import * as styles from 'lib/styles/styles' |
||||
import zIndex from 'lib/styles/zIndex' |
||||
|
||||
const Wrapper = styled.div<Props>` |
||||
position: absolute; |
||||
left: ${({ x }) => window.innerWidth < x + contextmenuWidth |
||||
? `${window.innerWidth - contextmenuWidth}px` |
||||
: `${x}px` |
||||
}; |
||||
top: ${({ y }) => window.innerHeight < y + 40 |
||||
? `${window.innerHeight - 40}px` |
||||
: `${y}px` |
||||
}; |
||||
width: ${({ width }) => width + 'px'}; |
||||
height: ${({ height }) => height + 'px'}; |
||||
box-sizing: border-box; |
||||
padding: ${({ width }) => width > 0 |
||||
? `4px` |
||||
: `` |
||||
}; |
||||
border-radius: 8px; |
||||
background-color: rgba(0,0,0,.8); |
||||
box-shadow: ${styles.boxShadow.regular}; |
||||
z-index: ${zIndex.menu}; |
||||
|
||||
& > div:first-child { |
||||
height: 100%; |
||||
overflow-y: auto; |
||||
${styles.scrollbar(0)}; |
||||
} |
||||
|
||||
&::before { |
||||
display: ${({ width }) => width > 0 |
||||
? `black` |
||||
: `none` |
||||
}; |
||||
content: ""; |
||||
position: absolute; |
||||
left: 80px; |
||||
${({ isReverse }) => isReverse |
||||
? css`bottom: -16px;` |
||||
: css`top: -16px;` |
||||
} |
||||
transform: translateX(-8px); |
||||
border: 8px solid rgba(0,0,0,0); |
||||
${({ isReverse }) => isReverse |
||||
? css`border-top: 8px solid rgba(0,0,0,.8);` |
||||
: css`border-bottom: 8px solid rgba(0,0,0,.8);` |
||||
} |
||||
z-index: 1; |
||||
} |
||||
`;
|
||||
|
||||
interface Props { |
||||
x: number; |
||||
y: number; |
||||
width: number; |
||||
height: number; |
||||
isReverse: boolean; |
||||
} |
||||
|
||||
export default Wrapper |
@ -1,28 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import palette from 'lib/styles/palette' |
||||
|
||||
const Item = ({ text }: Props) => { |
||||
return ( |
||||
<Container> |
||||
<span>{text}</span> |
||||
</Container> |
||||
); |
||||
} |
||||
|
||||
const Container = styled.div` |
||||
height: 100%; |
||||
flex-basis: 100%; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
& > span { |
||||
color: ${palette.gray4}; |
||||
} |
||||
`;
|
||||
|
||||
interface Props { |
||||
text: string; |
||||
} |
||||
|
||||
export default Item; |
@ -1,56 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// icons
|
||||
import { PrevIcon, NextIcon } from 'lib/svg' |
||||
// lib
|
||||
import * as styles from 'lib/styles/styles' |
||||
|
||||
const MoveBtn = ({ type, onClick }: Props) => { |
||||
const Icon = type === "PREV" |
||||
? PrevIcon |
||||
: NextIcon; |
||||
|
||||
const msg = type === "PREV" |
||||
? "이전 페이지" |
||||
: "다음 페이지"; |
||||
|
||||
return ( |
||||
<Container onClick={onClick} title={msg}> |
||||
<Content> |
||||
<Icon /> |
||||
</Content> |
||||
</Container> |
||||
); |
||||
} |
||||
|
||||
const Container = styled.button` |
||||
min-width: 60px; |
||||
height: 100%; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
cursor: pointer; |
||||
outline: none; |
||||
|
||||
&:focus, &:hover { |
||||
filter: invert(40%) sepia(85%) saturate(1256%) hue-rotate(210deg) brightness(113%) contrast(101%); |
||||
} |
||||
`;
|
||||
|
||||
const Content = styled.div` |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
transition: .1s ${styles.transition}; |
||||
|
||||
& > svg { |
||||
width: 18px; |
||||
height: 18px; |
||||
} |
||||
`;
|
||||
|
||||
interface Props { |
||||
type: "PREV" | "NEXT" |
||||
onClick: () => void; |
||||
} |
||||
|
||||
export default MoveBtn; |
@ -1,17 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import palette from 'lib/styles/palette' |
||||
|
||||
const Wrapper = styled.div` |
||||
position: fixed; |
||||
left: 0; |
||||
bottom: 0; |
||||
width: 100%; |
||||
height: 60px; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
background-color: ${palette.white}; |
||||
`;
|
||||
|
||||
export default Wrapper |
@ -1,34 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import * as styles from 'lib/styles/styles' |
||||
import palette from 'lib/styles/palette' |
||||
|
||||
const ControlBtn = ({ message, onClick }: Props) => { |
||||
return <Btn onClick={() => onClick()}>{message}</Btn>; |
||||
} |
||||
|
||||
const Btn = styled.button` |
||||
height: 100%; |
||||
padding: 0 16px; |
||||
cursor: pointer; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
transition: .1s ${styles.transition}; |
||||
outline: none; |
||||
|
||||
&:focus, &:hover { |
||||
color: ${palette.blue3}; |
||||
} |
||||
|
||||
&:first-child { |
||||
margin-left: 8px; |
||||
} |
||||
`;
|
||||
|
||||
interface Props { |
||||
message: string; |
||||
onClick: (value?: boolean) => void; |
||||
} |
||||
|
||||
export default ControlBtn |
@ -1,132 +0,0 @@ |
||||
import styled, { css } from 'styled-components' |
||||
// icons
|
||||
import {
|
||||
ScrollHorizontalIcon, |
||||
ScrollVerticalIcon, |
||||
BookCloseIcon, |
||||
BookOpenIcon |
||||
} from 'lib/svg' |
||||
// lib
|
||||
import * as styles from 'lib/styles/styles' |
||||
import palette from 'lib/styles/palette' |
||||
|
||||
const ControlIconBtn = ({ type, alt, active, isSelected, onClick }: Props) => { |
||||
let HeaderIcon = null; |
||||
switch (type) { |
||||
case "ScrollVertical": |
||||
HeaderIcon = ScrollVerticalIcon; |
||||
break; |
||||
case "ScrollHorizontal": |
||||
HeaderIcon = ScrollHorizontalIcon; |
||||
break; |
||||
case "BookOpen": |
||||
HeaderIcon = BookOpenIcon; |
||||
break; |
||||
case "BookClose": |
||||
HeaderIcon = BookCloseIcon; |
||||
break; |
||||
} |
||||
|
||||
const onClickBtn = () => { |
||||
if (active) onClick(); |
||||
} |
||||
|
||||
return ( |
||||
<Btn
|
||||
onClick={onClickBtn}
|
||||
isSelected={isSelected} |
||||
active={active} |
||||
title={alt} |
||||
> |
||||
<Icon> |
||||
<HeaderIcon /> |
||||
</Icon> |
||||
</Btn> |
||||
); |
||||
} |
||||
|
||||
const Btn = styled.button<{ isSelected: boolean, active: boolean }>` |
||||
position: relative; |
||||
height: 100%; |
||||
padding: 0 12px; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
z-index: 0; |
||||
transition: .3s ${styles.transition}; |
||||
opacity: ${({active}) => active |
||||
? '1' |
||||
: '.3' |
||||
}; |
||||
cursor: ${({active}) => active |
||||
? 'pointer' |
||||
: 'default' |
||||
}; |
||||
outline: none; |
||||
|
||||
&::before { |
||||
content: ''; |
||||
position: absolute; |
||||
left: 0; |
||||
right: 0; |
||||
top: 0; |
||||
bottom: 0; |
||||
margin: auto; |
||||
z-index: -1; |
||||
border-radius: 30px; |
||||
width: ${({isSelected}) => isSelected |
||||
? '32px' |
||||
: '8px' |
||||
}; |
||||
height: ${({isSelected}) => isSelected |
||||
? '32px' |
||||
: '8px' |
||||
}; |
||||
opacity: ${({active, isSelected}) => active && isSelected |
||||
? '.7' |
||||
: '0' |
||||
}; |
||||
transition: .1s ${styles.transition}; |
||||
background-color: ${palette.blue1}; |
||||
} |
||||
|
||||
&:focus, &:hover { |
||||
${({active}) => active |
||||
? css` |
||||
&:before { |
||||
width: 32px; |
||||
height: 32px; |
||||
opacity: 1; |
||||
} |
||||
` |
||||
: "" |
||||
}; |
||||
|
||||
} |
||||
`;
|
||||
|
||||
const Icon = styled.div` |
||||
width: 16px; |
||||
height: 16px; |
||||
|
||||
& > div, & > svg { |
||||
width: 16px; |
||||
height: 16px; |
||||
} |
||||
`;
|
||||
|
||||
type HeaderIcon =
|
||||
"ScrollHorizontal" | |
||||
"ScrollVertical" | |
||||
"BookOpen" | |
||||
"BookClose"; |
||||
|
||||
interface Props { |
||||
type: HeaderIcon; |
||||
alt: string; |
||||
active: boolean; |
||||
isSelected: boolean; |
||||
onClick: () => void; |
||||
} |
||||
|
||||
export default ControlIconBtn |
@ -1,34 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import palette from 'lib/styles/palette' |
||||
import zIndex from 'lib/styles/zIndex' |
||||
|
||||
const Layout = styled.div` |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
justify-content: center; |
||||
width: 100%; |
||||
height: 54px; |
||||
background-color: ${palette.white}; |
||||
z-index: ${zIndex.header}; |
||||
`;
|
||||
|
||||
export const AutoLayout = styled.div` |
||||
width: 100%; |
||||
flex-grow: 1; |
||||
display: flex; |
||||
justify-content: space-between; |
||||
border-bottom: 2px solid ${palette.gray1}; |
||||
|
||||
& > div { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: flex-start; |
||||
} |
||||
& > div:last-child { |
||||
margin-right: 8px; |
||||
} |
||||
`;
|
||||
|
||||
export default Layout |
@ -1,20 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import palette from 'lib/styles/palette' |
||||
|
||||
const VerticalLine = ({ height=16 }: Props) => { |
||||
return <Bar height={height} /> |
||||
} |
||||
|
||||
const Bar = styled.div<{height: number}>` |
||||
width: 1px; |
||||
height: ${({height}) => height+'px'}; |
||||
margin: 0 8px; |
||||
background-color: ${palette.gray2}; |
||||
`;
|
||||
|
||||
interface Props { |
||||
height?: number |
||||
} |
||||
|
||||
export default VerticalLine |
@ -1,16 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import palette from 'lib/styles/palette' |
||||
import zIndex from 'lib/styles/zIndex' |
||||
|
||||
const Wrapper = styled.div` |
||||
position: sticky; |
||||
width: 100%; |
||||
height: auto; |
||||
left: 0; |
||||
top: 0; |
||||
background-color: ${palette.white}; |
||||
z-index: ${zIndex.header}; |
||||
`;
|
||||
|
||||
export default Wrapper |
@ -1,11 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
|
||||
const Layout = styled.div` |
||||
flex-grow: 1; |
||||
display: flex; |
||||
flex-direction: column; |
||||
box-sizing: border-box; |
||||
padding: 24px 0; |
||||
`;
|
||||
|
||||
export default Layout |
@ -1,34 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import * as styles from 'lib/styles/styles' |
||||
import palette from 'lib/styles/palette' |
||||
|
||||
const SortedBtn = ({ text, isSelected, onClick }: Props) => {
|
||||
return <Btn isSelected={isSelected} onClick={onClick}>{text}</Btn>; |
||||
} |
||||
|
||||
const Btn = styled.button<{isSelected: boolean}>` |
||||
height: 20px; |
||||
line-height: 20px; |
||||
font-size: 14px; |
||||
cursor: pointer; |
||||
text-align: center; |
||||
margin-left: 16px; |
||||
color: ${({isSelected}) => isSelected
|
||||
? palette.blue3 |
||||
: palette.gray4 |
||||
}; |
||||
transition: .2s ${styles.transition}; |
||||
|
||||
&:hover { |
||||
color: ${palette.blue3}; |
||||
} |
||||
`;
|
||||
|
||||
interface Props { |
||||
text: string; |
||||
isSelected: boolean; |
||||
onClick: () => void; |
||||
} |
||||
|
||||
export default SortedBtn |
@ -1,10 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
|
||||
const SortedBtnWrapper = styled.div` |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: flex-end; |
||||
padding: 0 24px 24px 24px; |
||||
`;
|
||||
|
||||
export default SortedBtnWrapper |
@ -1,21 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import palette from 'lib/styles/palette' |
||||
|
||||
const PageNum = ({ num }: Props) => { |
||||
return <Content>{num}p</Content> |
||||
} |
||||
|
||||
const Content = styled.span` |
||||
min-width: 32px; |
||||
margin-right: 16px; |
||||
font-size: 14px; |
||||
font-weight: 500; |
||||
color: ${palette.gray4}; |
||||
`;
|
||||
|
||||
interface Props { |
||||
num: number; |
||||
} |
||||
|
||||
export default PageNum |
@ -1,24 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
|
||||
const Post = styled.div<{ color: string }>` |
||||
position: relative; |
||||
font-size: 14px; |
||||
margin-top: 16px; |
||||
padding: 8px; |
||||
box-sizing: border-box; |
||||
|
||||
&::before { |
||||
content: ''; |
||||
position: absolute; |
||||
width: 100%; |
||||
height: 100%; |
||||
left: 0; |
||||
top: 0; |
||||
background-color: ${({ color }) => color}; |
||||
opacity: .3; |
||||
border-radius: 8px; |
||||
mix-blend-mode: multiply; |
||||
} |
||||
`;
|
||||
|
||||
export default Post |
@ -1,9 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
|
||||
const Title = styled.div` |
||||
font-size: 14px; |
||||
font-weight: 600; |
||||
color: inherit; |
||||
`;
|
||||
|
||||
export default Title |
@ -1,32 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import palette from 'lib/styles/palette' |
||||
import * as styles from 'lib/styles/styles' |
||||
|
||||
const Wrapper = styled.button` |
||||
padding: 16px 24px; |
||||
display: flex; |
||||
align-items: flex-start; |
||||
justify-content: flex-start; |
||||
background-color: ${palette.gray0}; |
||||
transition: .1s ${styles.transition}; |
||||
cursor: pointer; |
||||
outline: none; |
||||
text-align: left; |
||||
border-bottom: 1px solid ${palette.gray1}; |
||||
|
||||
&:nth-child(2) { |
||||
border-top: 1px solid ${palette.gray1}; |
||||
} |
||||
|
||||
& > div { |
||||
color: inherit; |
||||
flex-grow: 1; |
||||
} |
||||
|
||||
&:hover > *, &:focus > * { |
||||
color: ${palette.blue3}; |
||||
} |
||||
`;
|
||||
|
||||
export default Wrapper |
@ -1,50 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
|
||||
const BookInfo = ({src="", title ,publisher, author}: Props) => { |
||||
return ( |
||||
<Container> |
||||
<BookImg src={src} alt={title} /> |
||||
<BookContent> |
||||
<Title>{title}</Title> |
||||
<Info>{publisher}</Info> |
||||
<Info>{author}</Info> |
||||
</BookContent> |
||||
</Container> |
||||
); |
||||
} |
||||
|
||||
const Container = styled.div` |
||||
display: flex; |
||||
padding: 24px; |
||||
`;
|
||||
|
||||
const BookImg = styled.img` |
||||
margin-right: 12px; |
||||
width: 44%; |
||||
min-width: 120px; |
||||
border-radius: 4px; |
||||
background: #eee; |
||||
`;
|
||||
|
||||
const BookContent = styled.div` |
||||
flex-grow: 1; |
||||
`;
|
||||
|
||||
const Title = styled.div` |
||||
font-weight: 500; |
||||
margin-bottom: 4px; |
||||
`;
|
||||
|
||||
const Info = styled.div` |
||||
font-size: 14px; |
||||
margin-bottom: 4px; |
||||
`;
|
||||
|
||||
interface Props { |
||||
src: string; |
||||
title: string; |
||||
publisher: string; |
||||
author: string; |
||||
} |
||||
|
||||
export default BookInfo |
@ -1,43 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import * as styles from 'lib/styles/styles' |
||||
import palette from 'lib/styles/palette' |
||||
|
||||
const NavItem = ({ message, onClick }: Props) => { |
||||
return ( |
||||
<Content onClick={onClick}> |
||||
<span>{message}</span> |
||||
</Content> |
||||
); |
||||
} |
||||
|
||||
const Content = styled.button` |
||||
width: 100%; |
||||
height: 48px; |
||||
box-sizing: border-box; |
||||
padding: 12px 24px; |
||||
display: flex; |
||||
align-items: center; |
||||
cursor: pointer; |
||||
background-color: ${palette.gray0}; |
||||
outline: none; |
||||
|
||||
& > span { |
||||
transition: .2s ${styles.transition}; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
&:focus, &:hover { |
||||
& > span { |
||||
color: ${palette.blue3}; |
||||
margin-left: 12px; |
||||
} |
||||
} |
||||
`;
|
||||
|
||||
interface Props { |
||||
message: string; |
||||
onClick: () => void; |
||||
} |
||||
|
||||
export default NavItem |
@ -1,79 +0,0 @@ |
||||
import { useState, useEffect, useRef, useCallback } from 'react' |
||||
import styled from 'styled-components' |
||||
// components
|
||||
import OptionWrapper from 'components/option/OptionWrapper' |
||||
import OptionTitle from 'components/option/OptionTitle' |
||||
import DropdownValue from 'components/option/DropdownValue' |
||||
import DropdownItemWrapper from 'components/option/DropdownItemWrapper' |
||||
import DropdownItem from 'components/option/DropdownItem' |
||||
// types
|
||||
import { BookFontFamily } from 'types/book' |
||||
|
||||
const Dropdown = ({ |
||||
title, |
||||
defaultValue, |
||||
valueList, |
||||
onSelect |
||||
}: Props) => { |
||||
const ref = useRef<HTMLDivElement | null>(null); |
||||
const [visible, setVisible] = useState<boolean>(false); |
||||
const value = defaultValue === "Origin" ? "Original" : defaultValue; |
||||
|
||||
/** Toggle dropdown */ |
||||
const onToggle = useCallback(() => setVisible(!visible), [visible]); |
||||
|
||||
/** Close dropdown */ |
||||
const onClose = useCallback((e: any) => { |
||||
if (!ref || !ref.current) return; |
||||
if (![...e.path].includes(ref.current)) { |
||||
onToggle(); |
||||
} |
||||
}, [ref, onToggle]); |
||||
|
||||
const Items = valueList.map(font =>
|
||||
<DropdownItem key={font} value={font} onClick={() => { |
||||
onSelect(font); |
||||
onToggle(); |
||||
}} /> |
||||
); |
||||
|
||||
/** Register dropdown close event */ |
||||
useEffect(() => { |
||||
if (visible) { |
||||
document.addEventListener('click', onClose); |
||||
} else { |
||||
document.removeEventListener('click', onClose); |
||||
} |
||||
return () => { |
||||
document.removeEventListener('click', onClose); |
||||
} |
||||
}, [visible, onClose]); |
||||
|
||||
return ( |
||||
<OptionWrapper> |
||||
<OptionTitle>{title}</OptionTitle> |
||||
|
||||
<DropdownWrapper ref={ref}> |
||||
<DropdownValue value={value}
|
||||
isDropdown={visible}
|
||||
onClick={onToggle} /> |
||||
<DropdownItemWrapper show={visible}> |
||||
{Items} |
||||
</DropdownItemWrapper> |
||||
</DropdownWrapper> |
||||
</OptionWrapper> |
||||
); |
||||
} |
||||
|
||||
const DropdownWrapper = styled.div` |
||||
position: relative; |
||||
`;
|
||||
|
||||
interface Props { |
||||
title: string; |
||||
defaultValue: BookFontFamily; |
||||
valueList: BookFontFamily[]; |
||||
onSelect: (font: BookFontFamily) => void; |
||||
} |
||||
|
||||
export default Dropdown |
@ -1,33 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import * as styles from 'lib/styles/styles' |
||||
|
||||
const DropdownItem = ({ value, onClick }: Props) => { |
||||
const text = value === "Origin" ? "Original" : value; |
||||
|
||||
return <Container onClick={onClick}>{text}</Container>; |
||||
} |
||||
|
||||
const Container = styled.button` |
||||
width: 100%; |
||||
height: 40px; |
||||
box-sizing: border-box; |
||||
padding: 0 16px; |
||||
transition: .1s ${styles.transition}; |
||||
font-size: 14px; |
||||
line-height: 40px; |
||||
cursor: pointer; |
||||
text-align: left; |
||||
outline: none; |
||||
|
||||
&:focus, &:hover { |
||||
background-color: rgba(0,0,0,.05); |
||||
} |
||||
`;
|
||||
|
||||
interface Props { |
||||
value: string; |
||||
onClick: () => void; |
||||
} |
||||
|
||||
export default DropdownItem |
@ -1,48 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import * as styles from 'lib/styles/styles' |
||||
import palette from 'lib/styles/palette' |
||||
|
||||
const DropdownItemWrapper = ({ show, children }: Props) => { |
||||
return <Container show={show}>{children}</Container> |
||||
} |
||||
|
||||
const Container = styled.div<{show: boolean}>` |
||||
position: absolute; |
||||
top: 100%; |
||||
left: 0; |
||||
width: 100%; |
||||
max-height: 200px; |
||||
overflow-y: auto; |
||||
box-sizing: border-box; |
||||
padding: 0 0 16px 0; |
||||
background-color: ${palette.white}; |
||||
border: 2px solid ${palette.blue3}; |
||||
z-index: 3; |
||||
border-radius: ${({show}) => show |
||||
? '0 0 20px 20px' |
||||
: '20px' |
||||
}; |
||||
transition: .4s ${styles.transition}; |
||||
opacity: ${({show}) => show |
||||
? '1' |
||||
: '0' |
||||
}; |
||||
transform-origin: top; |
||||
transform: ${({show}) => show |
||||
? 'translateY(-2px) scaleY(1);' |
||||
: 'translateY(-40px) scaleY(0);' |
||||
}; |
||||
${styles.scrollbar(6)} |
||||
${({show}) => show |
||||
? '' |
||||
: styles.noselect |
||||
}; |
||||
`;
|
||||
|
||||
interface Props { |
||||
show: boolean; |
||||
children: React.ReactElement[]; |
||||
} |
||||
|
||||
export default DropdownItemWrapper |
@ -1,84 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import * as styles from 'lib/styles/styles' |
||||
import palette from 'lib/styles/palette' |
||||
// icons
|
||||
import { DownIcon } from 'lib/svg' |
||||
|
||||
const DropdownValue = ({ value, isDropdown, onClick }: Props) => { |
||||
return ( |
||||
<Container
|
||||
onClick={onClick} |
||||
isDropdown={isDropdown} |
||||
title="Select font" |
||||
> |
||||
<Content>{value}</Content> |
||||
<Icon isDropdown={isDropdown}> |
||||
<DownIcon /> |
||||
</Icon> |
||||
</Container> |
||||
); |
||||
} |
||||
|
||||
const Container = styled.button<{isDropdown: boolean}>` |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
position: relative; |
||||
width: 100%; |
||||
height: 40px; |
||||
background-color: ${palette.white}; |
||||
border: ${({isDropdown}) => isDropdown |
||||
? `2px solid ${palette.blue3}` |
||||
: `2px solid ${palette.gray1}` |
||||
}; |
||||
border-radius: ${({isDropdown}) => isDropdown |
||||
? "20px 20px 0 0" |
||||
: "20px" |
||||
}; |
||||
box-sizing: border-box; |
||||
padding: 0px 16px; |
||||
cursor: pointer; |
||||
z-index: 4; |
||||
transition: .1s ${styles.transition}; |
||||
text-align: left; |
||||
outline: none; |
||||
|
||||
&:hover, &:focus { |
||||
border: 2px solid ${palette.blue3}; |
||||
|
||||
& > div { |
||||
opacity: 1; |
||||
filter: invert(40%) sepia(85%) saturate(1256%) hue-rotate(210deg) brightness(113%) contrast(101%); |
||||
} |
||||
} |
||||
`;
|
||||
|
||||
const Content = styled.span` |
||||
flex-grow: 1; |
||||
margin-right: 16px; |
||||
font-size: 14px; |
||||
`;
|
||||
|
||||
const Icon = styled.div<{isDropdown: boolean}>` |
||||
display: flex; |
||||
width: 12px; |
||||
height: 12px; |
||||
opacity: .3; |
||||
|
||||
& > svg { |
||||
transform-origin: center; |
||||
transform: ${({isDropdown}) => isDropdown |
||||
? 'rotate(180deg)' |
||||
: 'rotate(0deg)' |
||||
}; |
||||
} |
||||
`;
|
||||
|
||||
interface Props { |
||||
value: string; |
||||
isDropdown: boolean; |
||||
onClick: (e: any) => void; |
||||
} |
||||
|
||||
export default DropdownValue |
@ -1,8 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
|
||||
const Wrapper = styled.div` |
||||
box-sizing: border-box; |
||||
padding: 24px; |
||||
`;
|
||||
|
||||
export default Wrapper |
@ -1,9 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
|
||||
const OptionTitle = styled.div` |
||||
font-size: 14px; |
||||
font-weight: 500; |
||||
margin-bottom: 16px; |
||||
`;
|
||||
|
||||
export default OptionTitle |
@ -1,18 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import palette from 'lib/styles/palette' |
||||
|
||||
const OptionValue = styled.span<{ active: boolean }>` |
||||
display: inline-block; |
||||
font-size: 14px; |
||||
color: ${props => props.active |
||||
? palette.blue3 |
||||
: palette.gray4 |
||||
}; |
||||
margin-right: 8px; |
||||
width: 40px; |
||||
text-align: right; |
||||
line-height: 20px; |
||||
`;
|
||||
|
||||
export default OptionValue |
@ -1,7 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
|
||||
const OptionWrapper = styled.div` |
||||
margin-bottom: 24px; |
||||
`;
|
||||
|
||||
export default OptionWrapper |
@ -1,56 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// components
|
||||
import OptionWrapper from 'components/option/OptionWrapper' |
||||
import OptionTitle from 'components/option/OptionTitle' |
||||
import OptionValue from 'components/option/OptionValue' |
||||
import SliderValue from 'components/option/SliderValue' |
||||
|
||||
const Slider = ({ |
||||
active, |
||||
title, |
||||
minValue, |
||||
maxValue, |
||||
defaultValue, |
||||
step, |
||||
onChange |
||||
}: Props) => { |
||||
const onChangeValue = (e: any) => { |
||||
if (!active) return; |
||||
onChange(e); |
||||
} |
||||
|
||||
|
||||
return ( |
||||
<OptionWrapper> |
||||
<OptionTitle>{title}</OptionTitle> |
||||
|
||||
<SliderWrapper> |
||||
<OptionValue active={active}>{defaultValue}</OptionValue> |
||||
<SliderValue active={active} |
||||
minValue={minValue} |
||||
maxValue={maxValue} |
||||
defaultValue={defaultValue} |
||||
step={step} |
||||
onChange={onChangeValue} /> |
||||
</SliderWrapper> |
||||
</OptionWrapper> |
||||
); |
||||
} |
||||
|
||||
const SliderWrapper = styled.div` |
||||
display: flex; |
||||
align-items: center; |
||||
height: 20px; |
||||
`;
|
||||
|
||||
interface Props { |
||||
active: boolean; |
||||
title: string; |
||||
minValue: number; |
||||
maxValue: number; |
||||
defaultValue: number; |
||||
step: number; |
||||
onChange: (e: any) => void; |
||||
} |
||||
|
||||
export default Slider |
@ -1,136 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import palette from 'lib/styles/palette' |
||||
import * as styles from 'lib/styles/styles' |
||||
|
||||
const SliderValue = ({
|
||||
active,
|
||||
minValue,
|
||||
maxValue,
|
||||
defaultValue,
|
||||
step,
|
||||
onChange
|
||||
}: Props) => { |
||||
const percentage = Math.round((defaultValue - minValue) / (maxValue - minValue) * 100); |
||||
|
||||
return (<> |
||||
<Icon>-</Icon> |
||||
<SliderWrapper> |
||||
<SliderBackground /> |
||||
<SliderBar
|
||||
active={active} |
||||
percentage={percentage} |
||||
/> |
||||
<Slider |
||||
type="range"
|
||||
min={minValue} |
||||
max={maxValue} |
||||
defaultValue={defaultValue} |
||||
step={step} |
||||
onChange={onChange} |
||||
active={active} |
||||
disabled={!active} /> |
||||
</SliderWrapper> |
||||
<Icon>+</Icon> |
||||
</>); |
||||
} |
||||
|
||||
const SliderWrapper = styled.div` |
||||
flex-grow: 1; |
||||
display: flex; |
||||
position: relative; |
||||
width: auto; |
||||
`;
|
||||
|
||||
const SliderBackground = styled.div` |
||||
position: absolute; |
||||
width: 100%; |
||||
height: 6px; |
||||
background-color: #eee; |
||||
border-radius: 6px; |
||||
z-index: 0; |
||||
`;
|
||||
|
||||
const SliderBar = styled.div<{ percentage: number, active: boolean }>` |
||||
position: absolute; |
||||
top: 50%; |
||||
left: 0; |
||||
transform: translateY(-50%); |
||||
width: ${({ percentage }) => percentage + '%'}; |
||||
height: 6px; |
||||
border-radius: 6px 0 0 6px; |
||||
background-color: ${props => props.active |
||||
? palette.blue4 |
||||
: palette.gray2 |
||||
}; |
||||
z-index: 1; |
||||
`;
|
||||
|
||||
const Slider = styled.input<{ active: boolean }>` |
||||
appearance: none; |
||||
width: 100%; |
||||
height: 6px; |
||||
background-color: transparent; |
||||
border-radius: 6px; |
||||
margin: 0; |
||||
z-index: 2; |
||||
outline: none; |
||||
|
||||
&::-webkit-slider-thumb { |
||||
appearance: none; |
||||
width: 18px; |
||||
height: 18px; |
||||
border-radius: 18px; |
||||
box-shadow: ${styles.boxShadow.regular}; |
||||
background-color: white; |
||||
transition: .2s ${styles.transition}; |
||||
transition-property: box-shadow, background-color; |
||||
border: ${props => props.active |
||||
? `2px solid ${palette.blue3}` |
||||
: `2px solid ${palette.gray4}` |
||||
}; |
||||
cursor: ${props => props.active |
||||
? 'pointer' |
||||
: '' |
||||
};; |
||||
|
||||
&:hover { |
||||
background-color: ${props => props.active |
||||
? palette.blue1 |
||||
: '' |
||||
}; |
||||
} |
||||
|
||||
&:active { |
||||
box-shadow: none; |
||||
background-color: ${props => props.active |
||||
? palette.blue3 |
||||
: '' |
||||
}; |
||||
} |
||||
} |
||||
|
||||
&:focus::-webkit-slider-thumb { |
||||
box-shadow: none; |
||||
background-color: ${palette.blue3}; |
||||
} |
||||
`;
|
||||
|
||||
const Icon = styled.span` |
||||
font-size: 12px; |
||||
font-weight: 500; |
||||
color: ${palette.gray4}; |
||||
margin: 0 8px; |
||||
line-height: 20px; |
||||
`;
|
||||
|
||||
interface Props { |
||||
active: boolean; |
||||
minValue: number; |
||||
maxValue: number; |
||||
defaultValue: number; |
||||
step: number; |
||||
onChange: (e: any) => void; |
||||
} |
||||
|
||||
export default SliderValue |
@ -1,40 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import * as styles from 'lib/styles/styles' |
||||
// icons
|
||||
import { CloseIcon } from 'lib/svg' |
||||
|
||||
const CloseBtn = ({ onClick }: Props) => { |
||||
return ( |
||||
<Btn onClick={onClick}> |
||||
<Icon /> |
||||
</Btn> |
||||
); |
||||
} |
||||
|
||||
const Btn = styled.button` |
||||
position: relative; |
||||
height: 100%; |
||||
padding: 0 24px; |
||||
cursor: pointer; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
transition: .1s ${styles.transition}; |
||||
outline: none; |
||||
|
||||
&:focus, &:hover { |
||||
filter: invert(40%) sepia(85%) saturate(1256%) hue-rotate(210deg) brightness(113%) contrast(101%); |
||||
} |
||||
`;
|
||||
|
||||
const Icon = styled(CloseIcon)` |
||||
width: 18px; |
||||
height: 18px; |
||||
`;
|
||||
|
||||
interface Props { |
||||
onClick: () => void; |
||||
} |
||||
|
||||
export default CloseBtn; |
@ -1,23 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
// lib
|
||||
import palette from 'lib/styles/palette' |
||||
|
||||
const MenuEmpty = ({ text }: Props) => { |
||||
return <Content>{text}</Content>; |
||||
} |
||||
|
||||
const Content = styled.div` |
||||
flex-grow: 1; |
||||
height: auto; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
color: ${palette.blue2}; |
||||
font-size: 14px; |
||||
`;
|
||||
|
||||
interface Props { |
||||
text: string; |
||||
} |
||||
|
||||
export default MenuEmpty |
@ -1,65 +0,0 @@ |
||||
import React from 'react' |
||||
import styled from 'styled-components' |
||||
// components
|
||||
import CloseBtn from 'components/sideMenu/CloseBtn' |
||||
// lib
|
||||
import * as styles from 'lib/styles/styles' |
||||
import zIndex from 'lib/styles/zIndex' |
||||
import palette from 'lib/styles/palette' |
||||
|
||||
const Wrapper = ({ title, show, onClose, children }: Props, ref: any) => { |
||||
return ( |
||||
<Container show={show} ref={ref}> |
||||
<Header> |
||||
<span>{title}</span> |
||||
<CloseBtn onClick={onClose} /> |
||||
</Header> |
||||
{children} |
||||
</Container> |
||||
) |
||||
} |
||||
|
||||
const Container = styled.div` |
||||
display: flex; |
||||
flex-direction: column; |
||||
position: fixed; |
||||
width: 340px; |
||||
height: 100vh; |
||||
top: 0; |
||||
right: 0; |
||||
z-index: ${zIndex.menu}; |
||||
box-shadow: -4px 0 8px 0 rgba(0,0,0,.16); |
||||
background-color: ${palette.white}; |
||||
border-radius: 16px 0 0 16px; |
||||
transform: ${({ show }: { show: boolean }) => show
|
||||
? `translateX(0px) scale(1)` |
||||
: `translateX(420px) scale(.8)` |
||||
}; |
||||
transition: .3s ${styles.transition}; |
||||
overflow-y: auto; |
||||
${styles.scrollbar()}; |
||||
`;
|
||||
|
||||
const Header = styled.div` |
||||
width: 100%; |
||||
height: 64px; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
padding-top: 30px; |
||||
|
||||
& > span { |
||||
padding-left: 24px; |
||||
font-size: 21px; |
||||
font-weight: 600; |
||||
} |
||||
`;
|
||||
|
||||
interface Props { |
||||
title: string; |
||||
show: boolean; |
||||
onClose: () => void; |
||||
children?: React.ReactNode; |
||||
} |
||||
|
||||
export default React.forwardRef(Wrapper) |
@ -1,25 +0,0 @@ |
||||
// components
|
||||
import Wrapper from 'components/footer/Wrapper' |
||||
import Item from 'components/footer/Item' |
||||
import MoveBtn from 'components/footer/MoveBtn' |
||||
|
||||
|
||||
const Footer = ({ title, nowPage, totalPage, onPageMove }: Props) => { |
||||
return ( |
||||
<Wrapper> |
||||
<MoveBtn type="PREV" onClick={() => onPageMove("PREV")} /> |
||||
<Item text={title} /> |
||||
<Item text={`${nowPage} / ${totalPage}`} /> |
||||
<MoveBtn type="NEXT" onClick={() => onPageMove("NEXT")} /> |
||||
</Wrapper> |
||||
); |
||||
} |
||||
|
||||
interface Props { |
||||
title: string; |
||||
nowPage: number; |
||||
totalPage: number; |
||||
onPageMove: (type: "PREV" | "NEXT") => void; |
||||
} |
||||
|
||||
export default Footer |
@ -1,125 +0,0 @@ |
||||
import { useState } from 'react' |
||||
// components
|
||||
import Wrapper from 'components/header/Wrapper' |
||||
import Layout, { AutoLayout } from 'components/header/Layout' |
||||
import ControlBtn from 'components/header/ControlBtn' |
||||
import ControlIconBtn from 'components/header/ControlIconBtn' |
||||
import VerticalLine from 'components/header/VerticalLine' |
||||
// types
|
||||
import { BookOption } from 'types/book' |
||||
|
||||
|
||||
const Header = ({
|
||||
title,
|
||||
bookOption,
|
||||
onNavToggle,
|
||||
onOptionToggle,
|
||||
onLearningToggle, |
||||
onBookOptionChange |
||||
}: Props) => { |
||||
const [isScrollHorizontal, setIsScrollHorizontal] = useState<boolean>(true); |
||||
const [viewType, setViewType] = useState<ViewType>({ |
||||
active: true, |
||||
spread: true |
||||
}); |
||||
|
||||
|
||||
/** |
||||
* Select view direction |
||||
* @param type Direction |
||||
*/ |
||||
const onClickDirection = (type: "Horizontal" | "Vertical") => { |
||||
if (type === "Horizontal") { |
||||
setIsScrollHorizontal(true); |
||||
setViewType({ ...viewType, active: true }); |
||||
onBookOptionChange({ |
||||
...bookOption, |
||||
flow: "paginated" |
||||
}); |
||||
} else { |
||||
setIsScrollHorizontal(false); |
||||
setViewType({ ...viewType, active: false }); |
||||
onBookOptionChange({ |
||||
...bookOption, |
||||
flow: "scrolled-doc" |
||||
}); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Select isSpread |
||||
* @param isSpread Whether spread view
|
||||
*/ |
||||
const onClickViewType = (isSpread: boolean) => { |
||||
if (isSpread) { |
||||
setViewType({ ...viewType, spread: true }); |
||||
onBookOptionChange({ |
||||
...bookOption, |
||||
spread: "auto" |
||||
}); |
||||
} else { |
||||
setViewType({ ...viewType, spread: false }); |
||||
onBookOptionChange({ |
||||
...bookOption, |
||||
spread: "none" |
||||
}); |
||||
} |
||||
} |
||||
|
||||
|
||||
return ( |
||||
<Wrapper> |
||||
<Layout> |
||||
<AutoLayout> |
||||
<div> |
||||
<ControlBtn message="Contents" onClick={onNavToggle} /> |
||||
<ControlBtn message="Setting" onClick={onOptionToggle} /> |
||||
<ControlBtn message="Highlights" onClick={onLearningToggle} /> |
||||
</div> |
||||
|
||||
<div> |
||||
<ControlIconBtn type="ScrollHorizontal" |
||||
alt="Horizontal View" |
||||
active={true} |
||||
isSelected={isScrollHorizontal} |
||||
onClick={() => onClickDirection("Horizontal")} /> |
||||
<ControlIconBtn type="ScrollVertical"
|
||||
alt="Vertical View" |
||||
active={true} |
||||
isSelected={!isScrollHorizontal} |
||||
onClick={() => onClickDirection("Vertical")} /> |
||||
|
||||
<VerticalLine /> |
||||
|
||||
<ControlIconBtn type="BookOpen"
|
||||
alt="Two Page View" |
||||
active={viewType.active} |
||||
isSelected={viewType.spread} |
||||
onClick={() => onClickViewType(true)} /> |
||||
<ControlIconBtn type="BookClose"
|
||||
alt="One Page View" |
||||
active={viewType.active} |
||||
isSelected={!viewType.spread} |
||||
onClick={() => onClickViewType(false)} /> |
||||
</div> |
||||
</AutoLayout> |
||||
</Layout> |
||||
</Wrapper> |
||||
); |
||||
} |
||||
|
||||
interface Props { |
||||
title: string; |
||||
bookOption: BookOption; |
||||
onNavToggle: (value?: boolean) => void; |
||||
onOptionToggle: (value?: boolean) => void; |
||||
onLearningToggle: (value?: boolean) => void; |
||||
onBookOptionChange: (bookOption: BookOption) => void; |
||||
} |
||||
|
||||
type ViewType = { |
||||
active: boolean, |
||||
spread: boolean |
||||
} |
||||
|
||||
export default Header |
@ -1,223 +0,0 @@ |
||||
import { useState, useRef } from 'react' |
||||
import { useSelector, useDispatch } from 'react-redux' |
||||
import { Provider } from 'react-redux' |
||||
// containers
|
||||
import Viewer from 'modules/reactViewer/ReactViewer' |
||||
import Header from 'containers/Header' |
||||
import Footer from 'containers/Footer' |
||||
import Nav from 'containers/menu/Nav' |
||||
import Option from 'containers/menu/Option' |
||||
import Learning from 'containers/menu/Note' |
||||
import ContextMenu from 'containers/commons/ContextMenu' |
||||
import Snackbar from 'containers/commons/Snackbar' |
||||
// components
|
||||
import ViewerWrapper from 'components/commons/ViewerWrapper' |
||||
import LoadingView from 'LoadingView' |
||||
// slices
|
||||
import store from 'slices' |
||||
import { updateBook, updateCurrentPage, updateToc } from 'slices/book' |
||||
// hooks
|
||||
import useMenu from 'lib/hooks/useMenu' |
||||
import useHighlight from 'lib/hooks/useHighlight' |
||||
// styles
|
||||
import 'lib/styles/readerStyle.css' |
||||
import viewerLayout from 'lib/styles/viewerLayout' |
||||
// types
|
||||
import { RootState } from 'slices' |
||||
import { ViewerRef } from 'types' |
||||
import Book, { BookStyle, BookOption } from 'types/book' |
||||
import Page from 'types/page' |
||||
import Toc from 'types/toc' |
||||
|
||||
|
||||
const Reader = ({ url, loadingView }: Props) => { |
||||
const dispatch = useDispatch(); |
||||
const book = useSelector<RootState, Book>(state => state.book.book);
|
||||
const currentLocation = useSelector<RootState, Page>(state => state.book.currentLocation); |
||||
|
||||
const viewerRef = useRef<ViewerRef | null>(null); |
||||
const navRef = useRef<HTMLDivElement | null>(null); |
||||
const optionRef = useRef<HTMLDivElement | null>(null); |
||||
const learningRef = useRef<HTMLDivElement | null>(null); |
||||
|
||||
const [isContextMenu, setIsContextMenu] = useState<boolean>(false); |
||||
|
||||
const [bookStyle, setBookStyle] = useState<BookStyle>({ |
||||
fontFamily: 'Origin', |
||||
fontSize: 18, |
||||
lineHeight: 1.4, |
||||
marginHorizontal: 15, |
||||
marginVertical: 5 |
||||
}); |
||||
|
||||
const [bookOption, setBookOption] = useState<BookOption>({ |
||||
flow: "paginated", |
||||
resizeOnOrientationChange: true, |
||||
spread: "auto" |
||||
}); |
||||
|
||||
const [navControl, onNavToggle] = useMenu(navRef, 300.); |
||||
const [optionControl, onOptionToggle, emitEvent] = useMenu(optionRef, 300); |
||||
const [learningControl, onLearningToggle] = useMenu(learningRef, 300); |
||||
const {
|
||||
selection,
|
||||
onSelection,
|
||||
onClickHighlight, |
||||
onAddHighlight, |
||||
onRemoveHighlight, |
||||
onUpdateHighlight |
||||
} = useHighlight(viewerRef, setIsContextMenu, bookStyle, bookOption.flow); |
||||
|
||||
|
||||
|
||||
/** |
||||
* Change Epub book information |
||||
* @param book Epub Book Info |
||||
*/ |
||||
const onBookInfoChange = (book: Book) => dispatch(updateBook(book)); |
||||
|
||||
/** |
||||
* Change Epub location |
||||
* @param loc epubCFI or href |
||||
*/ |
||||
const onLocationChange = (loc: string) => { |
||||
if (!viewerRef.current) return; |
||||
viewerRef.current.setLocation(loc); |
||||
}
|
||||
|
||||
/** |
||||
* Move page |
||||
* @param type Direction |
||||
*/ |
||||
const onPageMove = (type: "PREV" | "NEXT") => { |
||||
const node = viewerRef.current; |
||||
if (!node || !node.prevPage || !node.nextPage) return; |
||||
|
||||
type === "PREV" && node.prevPage(); |
||||
type === "NEXT" && node.nextPage(); |
||||
}; |
||||
|
||||
/** |
||||
* Set toc |
||||
* @param toc Table of Epub contents |
||||
*/ |
||||
const onTocChange = (toc: Toc[]) => dispatch(updateToc(toc)); |
||||
|
||||
/** |
||||
* Set Epub viewer styles |
||||
* @param bokkStyle_ viewer style |
||||
*/ |
||||
const onBookStyleChange = (bookStyle_: BookStyle) => setBookStyle(bookStyle_); |
||||
|
||||
/** |
||||
* Set Epub viewer options |
||||
* @param bookOption_ viewer option |
||||
*/ |
||||
const onBookOptionChange = (bookOption_: BookOption) => setBookOption(bookOption_); |
||||
|
||||
/** |
||||
* Change current page |
||||
* @param page Epub page |
||||
*/ |
||||
const onPageChange = (page: Page) => dispatch(updateCurrentPage(page)); |
||||
|
||||
/** |
||||
* ContextMenu on
|
||||
* @param cfiRange CfiRange |
||||
*/ |
||||
const onContextMenu = (cfiRange: string) => { |
||||
const result = onSelection(cfiRange); |
||||
setIsContextMenu(result); |
||||
} |
||||
|
||||
/** ContextMenu off */ |
||||
const onContextmMenuRemove = () => setIsContextMenu(false); |
||||
|
||||
|
||||
|
||||
return (<> |
||||
<ViewerWrapper> |
||||
<Header
|
||||
title={book.title} |
||||
bookOption={bookOption} |
||||
onNavToggle={onNavToggle} |
||||
onOptionToggle={onOptionToggle} |
||||
onLearningToggle={onLearningToggle} |
||||
onBookOptionChange={onBookOptionChange} |
||||
/> |
||||
|
||||
<Viewer
|
||||
url={url} |
||||
viewerLayout={viewerLayout} |
||||
viewerStyle={bookStyle} |
||||
viewerOption={bookOption} |
||||
onBookInfoChange={onBookInfoChange} |
||||
onPageChange={onPageChange} |
||||
onTocChange={onTocChange} |
||||
onSelection={onContextMenu} |
||||
loadingView={loadingView || <LoadingView />} |
||||
ref={viewerRef} |
||||
/> |
||||
|
||||
<Footer
|
||||
title={currentLocation.chapterName} |
||||
nowPage={currentLocation.currentPage} |
||||
totalPage={currentLocation.totalPage} |
||||
onPageMove={onPageMove} |
||||
/> |
||||
</ViewerWrapper> |
||||
|
||||
<Nav |
||||
control={navControl} |
||||
onToggle={onNavToggle} |
||||
onLocation={onLocationChange} |
||||
ref={navRef} |
||||
/> |
||||
|
||||
<Option
|
||||
control={optionControl} |
||||
bookStyle={bookStyle} |
||||
bookFlow={bookOption.flow} |
||||
onToggle={onOptionToggle}
|
||||
emitEvent={emitEvent} |
||||
onBookStyleChange={onBookStyleChange} |
||||
ref={optionRef} |
||||
/> |
||||
|
||||
<Learning
|
||||
control={learningControl} |
||||
onToggle={onLearningToggle} |
||||
onClickHighlight={onClickHighlight} |
||||
emitEvent={emitEvent} |
||||
viewerRef={viewerRef} |
||||
ref={learningRef} |
||||
/> |
||||
|
||||
<ContextMenu |
||||
active={isContextMenu} |
||||
viewerRef={viewerRef} |
||||
selection={selection} |
||||
onAddHighlight={onAddHighlight} |
||||
onRemoveHighlight={onRemoveHighlight} |
||||
onUpdateHighlight={onUpdateHighlight} |
||||
onContextmMenuRemove={onContextmMenuRemove} |
||||
/> |
||||
|
||||
<Snackbar /> |
||||
</>); |
||||
} |
||||
|
||||
const ReaderWrapper = ({ url, loadingView }: Props) => { |
||||
return ( |
||||
<Provider store={store}> |
||||
<Reader url={url} loadingView={loadingView} /> |
||||
</Provider> |
||||
); |
||||
} |
||||
|
||||
interface Props { |
||||
url: string; |
||||
loadingView?: React.ReactNode; |
||||
} |
||||
|
||||
export default ReaderWrapper |
@ -1,207 +0,0 @@ |
||||
import { useState, useEffect, useCallback, useRef } from 'react' |
||||
import { useSelector } from 'react-redux' |
||||
// components
|
||||
import Wrapper from 'components/contextMenu/Wrapper' |
||||
import ColorItem from 'components/contextMenu/ColorItem' |
||||
import Item from 'components/contextMenu/Item' |
||||
// utils
|
||||
import { getParagraphCfi } from 'lib/utils/commonUtil' |
||||
// slices
|
||||
import { contextmenuWidth } from 'lib/styles/viewerLayout' |
||||
// types
|
||||
import { RootState } from 'slices' |
||||
import Highlight, { Color } from 'types/highlight' |
||||
import Selection from 'types/selection' |
||||
|
||||
|
||||
const ContextMenu = ({
|
||||
active, |
||||
viewerRef,
|
||||
selection,
|
||||
onAddHighlight, |
||||
onRemoveHighlight, |
||||
onUpdateHighlight,
|
||||
onContextmMenuRemove |
||||
}: Props) => { |
||||
const highlights = useSelector<RootState, Highlight[]>(state => state.book.highlights); |
||||
const colorList = useSelector<RootState, Color[]>(state => state.book.colorList); |
||||
|
||||
const menuRef = useRef<HTMLDivElement | null>(null); |
||||
|
||||
const [highlight, setHighlight] = useState<Highlight | null>(null); |
||||
|
||||
const [display, setDisplay] = useState<boolean>(false); |
||||
const [isEraseBtn, setIsEraseBtn] = useState<boolean>(false); |
||||
const [isReverse, setIsReverse] = useState<boolean>(false); |
||||
|
||||
const [height, setHeight] = useState<number>(0); |
||||
const [y, setY] = useState<number>(selection.y); |
||||
|
||||
const ColorList = colorList.map( |
||||
color => <ColorItem key={color.code} |
||||
name={color.name} |
||||
color={color.code} |
||||
onClick={selection.update |
||||
? () => onUpdateHighlight(highlight, color.code) |
||||
: () => onAddHighlight(color.code) |
||||
} /> |
||||
); |
||||
|
||||
|
||||
/** Remove highlight */ |
||||
const onRemoveHighlight_ = useCallback(() => { |
||||
if (!highlight) return; |
||||
|
||||
onRemoveHighlight(highlight.key, highlight.cfiRange); |
||||
onContextmMenuRemove(); |
||||
|
||||
setIsEraseBtn(false); |
||||
}, [highlight, onRemoveHighlight, onContextmMenuRemove]); |
||||
|
||||
/** |
||||
* Remove contextmenu |
||||
* @param e Mouse Event |
||||
* @param e.path |
||||
*/ |
||||
const onRemove = useCallback(({ path }: any) => { |
||||
if (!menuRef.current) return; |
||||
|
||||
if ([...path].includes(menuRef.current)) return; |
||||
onContextmMenuRemove(); |
||||
}, [menuRef, onContextmMenuRemove]); |
||||
|
||||
/** |
||||
* Arrow event |
||||
* @param e Keyboard Event |
||||
* @param e.key |
||||
*/ |
||||
const onKeyPress = useCallback(({ key }: any) => { |
||||
key && key === "ArrowLeft" && onContextmMenuRemove(); |
||||
key && key === "ArrowRight" && onContextmMenuRemove(); |
||||
}, [onContextmMenuRemove]); |
||||
|
||||
|
||||
/** Check whether the menu button is visible */ |
||||
useEffect(() => { |
||||
if (!active) setIsEraseBtn(false); |
||||
|
||||
const paragraphCfi = getParagraphCfi(selection.cfiRange); |
||||
if (!paragraphCfi) return; |
||||
|
||||
const filtered = highlights.filter(highlight => highlight.key === paragraphCfi + selection.cfiRange); |
||||
|
||||
if (!filtered.length) return; |
||||
const highlight_ = filtered[0]; |
||||
setHighlight(highlight_); |
||||
|
||||
if (selection.update) { |
||||
setIsEraseBtn(true); |
||||
} else { |
||||
setIsEraseBtn(false); |
||||
} |
||||
}, [ |
||||
active, |
||||
highlights,
|
||||
selection.cfiRange, |
||||
selection.update |
||||
]); |
||||
|
||||
/** Register contextmenu events */ |
||||
useEffect(() => { |
||||
if (!viewerRef.current) return; |
||||
|
||||
const iframe = document.querySelector('iframe'); |
||||
const node = iframe && iframe.contentWindow && iframe.contentWindow.document; |
||||
const scrolledTarget = viewerRef.current.querySelector('div'); |
||||
|
||||
if (active) { |
||||
setDisplay(true); |
||||
scrolledTarget && scrolledTarget.addEventListener('scroll', onContextmMenuRemove); |
||||
node && node.addEventListener('mousedown', onRemove); |
||||
node && node.addEventListener('keyup', onKeyPress); |
||||
document.addEventListener('mousedown', onRemove); |
||||
document.addEventListener('keyup', onKeyPress); |
||||
} else { |
||||
setDisplay(false); |
||||
scrolledTarget && scrolledTarget.removeEventListener('scroll', onContextmMenuRemove); |
||||
node && node.removeEventListener('mousedown', onRemove); |
||||
node && node.removeEventListener('keyup', onKeyPress); |
||||
document.removeEventListener('mousedown', onRemove); |
||||
document.removeEventListener('keyup', onKeyPress); |
||||
}; |
||||
|
||||
return () => { |
||||
node && node.removeEventListener('mousedown', onContextmMenuRemove); |
||||
node && node.removeEventListener('keyup', onKeyPress); |
||||
document.removeEventListener('keyup', onKeyPress); |
||||
} |
||||
}, [ |
||||
viewerRef,
|
||||
active, |
||||
onRemove, |
||||
onContextmMenuRemove,
|
||||
onKeyPress |
||||
]); |
||||
|
||||
/** Set modified contextmenu height & whether menu is reverse */ |
||||
useEffect(() => { |
||||
const menuPadding = 8; |
||||
const itemHeight = 32; |
||||
let itemCnt = ColorList.length; |
||||
|
||||
if (isEraseBtn) itemCnt += 1; |
||||
|
||||
const defaultHeight = itemCnt * itemHeight + menuPadding; |
||||
let y_ = selection.y; |
||||
const { innerHeight } = window; |
||||
|
||||
if (selection.y + defaultHeight > innerHeight) { |
||||
y_ = selection.y - selection.height - (defaultHeight); |
||||
if (y_ < 0) { |
||||
setHeight(defaultHeight + y_ - 8); |
||||
y_ = 8; |
||||
} else { |
||||
setHeight(defaultHeight); |
||||
} |
||||
setIsReverse(true); |
||||
} else { |
||||
setHeight(defaultHeight); |
||||
setIsReverse(false); |
||||
} |
||||
|
||||
setY(y_); |
||||
}, [ |
||||
selection.y,
|
||||
selection.height,
|
||||
ColorList,
|
||||
isEraseBtn,
|
||||
setHeight |
||||
]); |
||||
|
||||
|
||||
return (<> |
||||
{display && <Wrapper x={selection.x}
|
||||
y={y}
|
||||
width={contextmenuWidth} |
||||
height={height} |
||||
isReverse={isReverse} |
||||
ref={menuRef}> |
||||
<div> |
||||
{ColorList} |
||||
{isEraseBtn && <Item text="Remove" onClick={onRemoveHighlight_} />} |
||||
</div> |
||||
</Wrapper>} |
||||
</>); |
||||
} |
||||
|
||||
interface Props { |
||||
active: boolean; |
||||
viewerRef: any; |
||||
selection: Selection; |
||||
onAddHighlight: (color: string) => void; |
||||
onRemoveHighlight: (key: string, cfiRange: string) => void; |
||||
onUpdateHighlight: (highlight: Highlight | null, color: string) => void; |
||||
onContextmMenuRemove: () => void; |
||||
} |
||||
|
||||
export default ContextMenu |
@ -1,56 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
import { useSelector, useDispatch } from 'react-redux' |
||||
// components
|
||||
import Snackbar from 'components/commons/Snackbar' |
||||
// lib
|
||||
import zIndex from 'lib/styles/zIndex' |
||||
import media from 'lib/styles/media' |
||||
// slices
|
||||
import { deleteSnackbar } from 'slices/snackbar' |
||||
// types
|
||||
import { RootState } from 'slices' |
||||
import { SnackbarState } from 'slices/snackbar' |
||||
|
||||
|
||||
const SnackbarWrapper = ()=> { |
||||
const dispatch = useDispatch(); |
||||
const snackbar = useSelector<RootState, SnackbarState>(state => state.snackbar); |
||||
|
||||
|
||||
const onClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => { |
||||
if (e) { |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
} |
||||
dispatch(deleteSnackbar()); |
||||
}; |
||||
|
||||
|
||||
return ( |
||||
<Container> |
||||
{snackbar.text !== '' && <Snackbar onClick={onClick}
|
||||
text={snackbar.text}
|
||||
type={snackbar.type} />} |
||||
</Container> |
||||
); |
||||
} |
||||
|
||||
const Container = styled.div` |
||||
position: fixed; |
||||
top: 40px; |
||||
right: 30px; |
||||
width: 320px; |
||||
height: auto; |
||||
z-index: ${zIndex.snackbar}; |
||||
|
||||
${media.small} { |
||||
width: 100%; |
||||
top: initial; |
||||
bottom: 0; |
||||
left: 0; |
||||
right: 0; |
||||
margin: auto; |
||||
} |
||||
`;
|
||||
|
||||
export default SnackbarWrapper |
@ -1,52 +0,0 @@ |
||||
import React from 'react' |
||||
import { useSelector } from 'react-redux' |
||||
// components
|
||||
import Wrapper from 'components/sideMenu/Wrapper' |
||||
import BookInfo from 'components/nav/BookInfo' |
||||
import NavItem from 'components/nav/NavItem' |
||||
// types
|
||||
import { RootState } from 'slices' |
||||
import Book from 'types/book' |
||||
import Toc from 'types/toc' |
||||
import { MenuControl } from 'lib/hooks/useMenu' |
||||
|
||||
const Nav = ({ control, onToggle, onLocation }: Props, ref: any) => { |
||||
const book = useSelector<RootState, Book>(state => state.book.book); |
||||
const bookToc = useSelector<RootState, Toc[]>(state => state.book.toc); |
||||
|
||||
/** Click nav item */ |
||||
const onClickItem = (loc: string) => { |
||||
onLocation(loc); |
||||
onToggle(); |
||||
} |
||||
|
||||
const Tocs = bookToc.map((t, idx) =>
|
||||
<NavItem key={idx} |
||||
message={t.label} |
||||
onClick={() => onClickItem(t.href)} /> |
||||
); |
||||
|
||||
|
||||
return (<> |
||||
{control.display && <Wrapper title="Contents" |
||||
show={control.open} |
||||
onClose={onToggle} |
||||
ref={ref}> |
||||
<BookInfo
|
||||
src={book.coverURL} |
||||
title={book.title} |
||||
publisher={book.publisher} |
||||
author={book.author}
|
||||
/> |
||||
{Tocs} |
||||
</Wrapper>} |
||||
</>); |
||||
} |
||||
|
||||
interface Props { |
||||
control: MenuControl; |
||||
onToggle: () => void; |
||||
onLocation: (loc: string) => void; |
||||
} |
||||
|
||||
export default React.forwardRef(Nav) |
@ -1,100 +0,0 @@ |
||||
import React, { useState, useEffect } from 'react' |
||||
import { useSelector } from 'react-redux' |
||||
// containers
|
||||
import Highlight from 'containers/menu/commons/Highlight' |
||||
// components
|
||||
import Wrapper from 'components/sideMenu/Wrapper' |
||||
import LearningLayout from 'components/learning/Layout' |
||||
import SortedBtnWrapper from 'components/learning/SortedBtnWrapper' |
||||
import SortedBtn from 'components/learning/SortedBtn' |
||||
import MenuEmpty from 'components/sideMenu/MenuEmpty' |
||||
// types
|
||||
import { RootState } from 'slices' |
||||
import HighlightType from 'types/highlight' |
||||
import { MenuControl } from 'lib/hooks/useMenu' |
||||
|
||||
|
||||
const Note = ({
|
||||
control,
|
||||
onToggle,
|
||||
onClickHighlight,
|
||||
emitEvent, |
||||
viewerRef
|
||||
}: Props, ref: any) => { |
||||
const highlights = useSelector<RootState, HighlightType[]>(state => state.book.highlights); |
||||
|
||||
const [sorted, setSorted] = useState<Sorted>("CART"); |
||||
const [highlightList, setHighlightList] = useState<any[]>([]); |
||||
|
||||
/** |
||||
* Select sort type |
||||
* @param type Viewed or Created |
||||
*/ |
||||
const onSortedClick = (type: Sorted) => setSorted(type); |
||||
|
||||
|
||||
/** Set highlight contents */ |
||||
useEffect(() => { |
||||
const sortedKey = sorted === "LOOK"
|
||||
? "accessTime"
|
||||
: "createTime"; |
||||
|
||||
const highlightList = highlights.slice().sort(function (a, b) { |
||||
return new Date(b[sortedKey]).getTime() - new Date(a[sortedKey]).getTime(); |
||||
}); |
||||
const Items = highlightList.map(h => <Highlight key={h.key}
|
||||
highlight={h} |
||||
onClick={onClickHighlight}
|
||||
emitEvent={emitEvent} |
||||
viewerRef={viewerRef} /> |
||||
); |
||||
setHighlightList(Items);
|
||||
}, [ |
||||
viewerRef, |
||||
sorted,
|
||||
highlights, |
||||
onClickHighlight, |
||||
emitEvent, |
||||
setHighlightList |
||||
]); |
||||
|
||||
|
||||
return (<> |
||||
{control.display && <Wrapper title="Highlight" |
||||
show={control.open} |
||||
onClose={onToggle} |
||||
ref={ref} > |
||||
<LearningLayout> |
||||
<SortedBtnWrapper> |
||||
<SortedBtn
|
||||
text="viewed"
|
||||
isSelected={sorted === "LOOK"}
|
||||
onClick={() => onSortedClick("LOOK")} |
||||
/> |
||||
<SortedBtn
|
||||
text="created"
|
||||
isSelected={sorted === "CART"}
|
||||
onClick={() => onSortedClick("CART")} |
||||
/> |
||||
</SortedBtnWrapper> |
||||
|
||||
{highlightList.length > 0 |
||||
? highlightList |
||||
: <MenuEmpty text="Empty highlights!" /> |
||||
} |
||||
</LearningLayout> |
||||
</Wrapper>} |
||||
</>); |
||||
} |
||||
|
||||
type Sorted = "LOOK" | "CART" |
||||
|
||||
interface Props { |
||||
control: MenuControl; |
||||
onToggle: () => void; |
||||
onClickHighlight: (highlightNode: any) => void; |
||||
emitEvent: () => void; |
||||
viewerRef: any; |
||||
} |
||||
|
||||
export default React.forwardRef(Note) |
@ -1,137 +0,0 @@ |
||||
import React from 'react' |
||||
import { useState, useEffect } from 'react' |
||||
// components
|
||||
import Wrapper from 'components/sideMenu/Wrapper' |
||||
import OptionLayout from 'components/option/Layout' |
||||
import OptionDropdown from 'components/option/Dropdown' |
||||
import OptionSlider from 'components/option/Slider' |
||||
// types
|
||||
import { BookStyle, BookFontFamily, BookFlow } from 'types/book' |
||||
import { MenuControl } from 'lib/hooks/useMenu' |
||||
|
||||
const Option = ({
|
||||
control,
|
||||
bookStyle, |
||||
bookFlow, |
||||
onToggle,
|
||||
emitEvent, |
||||
onBookStyleChange |
||||
}: Props, ref: any) => { |
||||
const [fontFamily, setFontFamily] = useState<BookFontFamily>(bookStyle.fontFamily); |
||||
const [fontSize, setFontSize] = useState<number>(bookStyle.fontSize); |
||||
const [lineHeight, setLineHeight] = useState<number>(bookStyle.lineHeight); |
||||
const [marginHorizontal, setMarginHorizontal] = useState<number>(bookStyle.marginHorizontal); |
||||
const [marginVertical, setMarginVertical] = useState<number>(bookStyle.marginVertical); |
||||
|
||||
|
||||
/** Change font family */ |
||||
const onSelectFontFamily = (font: BookFontFamily) => setFontFamily(font); |
||||
|
||||
/** Change font style and layout */ |
||||
const onChangeSlider = (type: SliderType, e: any) => { |
||||
if (!e || !e.target) return; |
||||
switch (type) { |
||||
case "FontSize": |
||||
setFontSize(e.target.value); |
||||
break; |
||||
case "LineHeight": |
||||
setLineHeight(e.target.value); |
||||
break; |
||||
case "MarginHorizontal": |
||||
setMarginHorizontal(e.target.value); |
||||
break; |
||||
case "MarginVertical": |
||||
setMarginVertical(e.target.value); |
||||
break; |
||||
default: |
||||
break; |
||||
} |
||||
} |
||||
|
||||
|
||||
/* Save userdata */ |
||||
// TODO Fix the infinite re-rendering issue, when inlcude `onBookStyleChange` to dependencies array.
|
||||
/* eslint-disable */ |
||||
useEffect(() => { |
||||
const timer = window.setTimeout(() => {
|
||||
onBookStyleChange({ |
||||
fontFamily, |
||||
fontSize, |
||||
lineHeight, |
||||
marginHorizontal, |
||||
marginVertical |
||||
}); |
||||
}, 250); |
||||
|
||||
return () => window.clearTimeout(timer); |
||||
}, [ |
||||
fontFamily,
|
||||
fontSize,
|
||||
lineHeight,
|
||||
marginHorizontal,
|
||||
marginVertical |
||||
]); |
||||
/* eslint-enable */ |
||||
|
||||
/** Re-register close event, when after set */ |
||||
useEffect(() => emitEvent(), [bookStyle, emitEvent]); |
||||
|
||||
|
||||
return (<> |
||||
{control.display && <Wrapper title="Setting" |
||||
show={control.open} |
||||
onClose={onToggle} |
||||
ref={ref}> |
||||
<OptionLayout> |
||||
<OptionDropdown title="Font" |
||||
defaultValue={fontFamily} |
||||
valueList={["Origin", "Roboto"]} |
||||
onSelect={onSelectFontFamily} /> |
||||
<OptionSlider active={true} |
||||
title="Size" |
||||
minValue={8} |
||||
maxValue={36} |
||||
defaultValue={fontSize} |
||||
step={1} |
||||
onChange={(e) => onChangeSlider("FontSize", e)} /> |
||||
<OptionSlider active={true} |
||||
title="Line height" |
||||
minValue={1} |
||||
maxValue={3} |
||||
defaultValue={lineHeight} |
||||
step={0.1} |
||||
onChange={(e) => onChangeSlider("LineHeight", e)} /> |
||||
<OptionSlider active={true} |
||||
title="Horizontal margin" |
||||
minValue={0} |
||||
maxValue={100} |
||||
defaultValue={marginHorizontal} |
||||
step={1} |
||||
onChange={(e) => onChangeSlider("MarginHorizontal", e)} /> |
||||
<OptionSlider active={bookFlow === "paginated"} |
||||
title="Vertical margin" |
||||
minValue={0} |
||||
maxValue={100} |
||||
defaultValue={marginVertical} |
||||
step={1} |
||||
onChange={(e) => onChangeSlider("MarginVertical", e)} /> |
||||
</OptionLayout> |
||||
</Wrapper>} |
||||
</>); |
||||
} |
||||
|
||||
interface Props { |
||||
control: MenuControl; |
||||
bookStyle: BookStyle; |
||||
bookFlow: BookFlow; |
||||
onToggle: () => void; |
||||
emitEvent: () => void; |
||||
onBookStyleChange: (bookStyle: BookStyle) => void; |
||||
} |
||||
|
||||
type SliderType = "FontSize"
|
||||
| "LineHeight"
|
||||
| "MarginHorizontal"
|
||||
| "MarginVertical"; |
||||
|
||||
export default React.forwardRef(Option) |
@ -1,71 +0,0 @@ |
||||
import { useDispatch } from 'react-redux' |
||||
// components
|
||||
import Wrapper from 'components/learning/highlight/Wrapper' |
||||
import Title from 'components/learning/highlight/Title' |
||||
import Post from 'components/learning/highlight/Post' |
||||
import PageNum from 'components/learning/highlight/PageNum' |
||||
// slices
|
||||
import { updateHighlight } from 'slices/book' |
||||
// utils
|
||||
import { cfiRangeSpliter } from 'lib/utils/commonUtil' |
||||
// types
|
||||
import HighlightType from 'types/highlight' |
||||
|
||||
const Highlight = ({
|
||||
highlight,
|
||||
onClick,
|
||||
emitEvent,
|
||||
viewerRef |
||||
}: Props) => { |
||||
const dispatch = useDispatch(); |
||||
|
||||
|
||||
/** 하이라이트 클릭 */ |
||||
const onClickHighlight = () => { |
||||
if (!viewerRef.current) return; |
||||
|
||||
const now = new Date().toISOString(); |
||||
dispatch(updateHighlight({ |
||||
...highlight, |
||||
accessTime: now |
||||
})); |
||||
|
||||
const splitCfi = cfiRangeSpliter(highlight.cfiRange); |
||||
if (!splitCfi) return; |
||||
|
||||
const { startCfi } = splitCfi; |
||||
|
||||
viewerRef.current.setLocation(startCfi); |
||||
|
||||
emitEvent(); |
||||
|
||||
const svgContainer = viewerRef.current.querySelector("svg"); |
||||
if (!svgContainer) return; |
||||
|
||||
const targetSvg = svgContainer.querySelector(`g[data-epubcfi="${highlight.cfiRange}"]`); |
||||
if (!targetSvg) return; |
||||
|
||||
onClick(targetSvg.childNodes[0]); |
||||
} |
||||
|
||||
return ( |
||||
<Wrapper onClick={onClickHighlight}> |
||||
<PageNum num={highlight.pageNum} /> |
||||
|
||||
<div> |
||||
<Title>{highlight.chpaterName}</Title> |
||||
|
||||
<Post color={highlight.color}>{highlight.content}</Post> |
||||
</div> |
||||
</Wrapper> |
||||
); |
||||
} |
||||
|
||||
interface Props { |
||||
highlight: HighlightType; |
||||
onClick: (highlightNode: any) => void; |
||||
emitEvent: () => void; |
||||
viewerRef: any; |
||||
} |
||||
|
||||
export default Highlight |
@ -1,6 +1,20 @@ |
||||
import { useRef } from 'react' |
||||
import ReactDOM from 'react-dom'; |
||||
import Reader from 'containers/Reader' |
||||
import ReactEpubViewer from 'modules/reactViewer/ReactViewer' |
||||
import { ViewerRef } from 'types' |
||||
|
||||
const EPUB_URL = "files/Alices Adventures in Wonderland.epub"; |
||||
|
||||
ReactDOM.render(<Reader url={EPUB_URL} />, document.getElementById('root')); |
||||
const App = () => { |
||||
const EPUB_URL = "files/Alices Adventures in Wonderland.epub"; |
||||
const ref = useRef<ViewerRef>(null); |
||||
|
||||
return ( |
||||
<ReactEpubViewer
|
||||
url={EPUB_URL} |
||||
ref={ref} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root')); |
@ -1,261 +0,0 @@ |
||||
import React, { useState, useEffect, useCallback } from 'react' |
||||
import { useSelector, useDispatch } from 'react-redux' |
||||
// slices
|
||||
import { pushHighlight, popHighlight, updateHighlight } from 'slices/book' |
||||
import { newSnackbar } from 'slices/snackbar' |
||||
// utils
|
||||
import {
|
||||
getParagraphCfi, |
||||
clashCfiRange,
|
||||
getSelectionPosition, |
||||
compareCfi, |
||||
cfiRangeSpliter, |
||||
getNodefromCfi |
||||
} from 'lib/utils/commonUtil' |
||||
// styles
|
||||
import viewerLayout, { contextmenuWidth } from 'lib/styles/viewerLayout' |
||||
// types
|
||||
import { RootState } from 'slices' |
||||
import { BookStyle, BookFlow } from 'types/book' |
||||
import Page from 'types/page' |
||||
import Selection from 'types/selection' |
||||
import Highlight from 'types/highlight' |
||||
|
||||
|
||||
/** |
||||
* Epub Reader의 Highlight 관련 hook |
||||
* - [selection]: 현재 selection의 정보 |
||||
* - [onSelection]: selection 적용 함수 (Iframe에 selection 이벤트로 등록해야 합니다.) |
||||
* - [onClickHighlight]: highlight 클릭 함수 |
||||
* - [onAddHighlight] highlight 생성 함수 |
||||
* - [onRemoveHighlight]: highlight 제거 함수 |
||||
* - [onUpdateHighlight]: highlight 갱신 함수 |
||||
* |
||||
* @param viewerRef Viewer에 등록된 가지고 있는 useRef |
||||
* @param setIsContextMenu Contextmenu를 컨트롤 할 수 있는 useState 함수 |
||||
*
|
||||
* @example |
||||
* const [isContextMenu, setIsContextMenu] = useState<boolean>(false); |
||||
*
|
||||
* const {
|
||||
* selection,
|
||||
* onSelection,
|
||||
* onClickHighlight, |
||||
* onRemoveHighlight |
||||
* } = useSelection(viewerRef, setIsContextMenu); |
||||
*/ |
||||
const useHighlight = ( |
||||
viewerRef: any,
|
||||
setIsContextMenu: React.Dispatch<React.SetStateAction<boolean>>, |
||||
bookStyle: BookStyle, |
||||
bookFlow: BookFlow |
||||
) => { |
||||
const dispatch = useDispatch(); |
||||
const currentLocation = useSelector<RootState, Page>(state => state.book.currentLocation); |
||||
const highlights = useSelector<RootState, Highlight[]>(state => state.book.highlights); |
||||
|
||||
const [selection, setSelection] = useState<Selection>({ |
||||
update: false, |
||||
x: 0, |
||||
y: 0, |
||||
height: 0, |
||||
cfiRange: '', |
||||
content: '' |
||||
}); |
||||
|
||||
|
||||
/** |
||||
* Selection 발동 이벤트 |
||||
* @param cfiRange CfiRange |
||||
* @return Whether event is success |
||||
*/ |
||||
const onSelection = useCallback((cfiRange: string): boolean => { |
||||
if (!viewerRef.current) return false; |
||||
|
||||
const iframe = viewerRef.current.querySelector('iframe'); |
||||
if (!iframe) return false; |
||||
|
||||
const iframeWin = iframe.contentWindow; |
||||
if (!iframeWin) return false; |
||||
|
||||
const filtered = highlights.filter( |
||||
highlight => clashCfiRange(highlight.cfiRange, cfiRange) |
||||
); |
||||
|
||||
if (filtered.length > 0) { |
||||
dispatch(newSnackbar({ |
||||
text: "There are already registered highlights in your selection.",
|
||||
type: "WARNING"
|
||||
})); |
||||
|
||||
iframeWin.getSelection().removeAllRanges(); |
||||
return false; |
||||
} |
||||
|
||||
const position = getSelectionPosition( |
||||
viewerRef.current, |
||||
bookStyle, |
||||
bookFlow, |
||||
viewerLayout.MIN_VIEWER_WIDTH, |
||||
viewerLayout.MIN_VIEWER_HEIGHT, |
||||
viewerLayout.VIEWER_HEADER_HEIGHT, |
||||
contextmenuWidth |
||||
); |
||||
if (!position) return false; |
||||
|
||||
const { x, y, height } = position; |
||||
|
||||
const content = iframeWin.getSelection().toString().trim(); |
||||
|
||||
if (content.length === 0) return false; |
||||
|
||||
setSelection({
|
||||
update: false, |
||||
x,
|
||||
y, |
||||
height, |
||||
cfiRange, |
||||
content |
||||
}); |
||||
|
||||
return true; |
||||
}, [ |
||||
dispatch,
|
||||
viewerRef, |
||||
highlights,
|
||||
bookFlow,
|
||||
bookStyle, |
||||
setSelection |
||||
]); |
||||
|
||||
/** Highlight 클릭 이벤트 */ |
||||
const onClickHighlight = useCallback((highlightNode: HTMLElement) => { |
||||
const targetNode = highlightNode.parentElement; |
||||
if (!targetNode) return; |
||||
|
||||
const cfiRange = targetNode.dataset.epubcfi; |
||||
if (!cfiRange) return; |
||||
|
||||
const { x, y, width, height } = targetNode.getBoundingClientRect(); |
||||
|
||||
setSelection({
|
||||
update: true, |
||||
x: x + width / 2 - contextmenuWidth / 2,
|
||||
y: y + height, |
||||
height, |
||||
cfiRange, |
||||
content: "" |
||||
}); |
||||
}, [setSelection]); |
||||
|
||||
/** 하이라이트 생성 함수 */ |
||||
const onAddHighlight = useCallback((color: string) => { |
||||
const paragraphCfi = getParagraphCfi(selection.cfiRange); |
||||
if (!paragraphCfi) return; |
||||
|
||||
const highlight: Highlight = { |
||||
key: paragraphCfi + selection.cfiRange, |
||||
accessTime: new Date().toISOString(), |
||||
createTime: new Date().toISOString(), |
||||
color, |
||||
paragraphCfi, |
||||
cfiRange: selection.cfiRange, |
||||
chpaterName: currentLocation.chapterName, |
||||
pageNum: currentLocation.currentPage, |
||||
content: selection.content |
||||
}; |
||||
|
||||
dispatch(pushHighlight(highlight)); |
||||
|
||||
setSelection({ ...selection, update: true }); |
||||
}, [dispatch, selection, currentLocation]); |
||||
|
||||
/** |
||||
* 하이라이트 수정 |
||||
* @param color 색상 Hex 코드 |
||||
*/ |
||||
const onUpdateHighlight = useCallback((highlight: Highlight | null, color: string) => { |
||||
if (!highlight) return; |
||||
|
||||
dispatch(updateHighlight({ |
||||
...highlight, |
||||
color |
||||
})); |
||||
}, [dispatch]); |
||||
|
||||
/** |
||||
* 하이라이트 제거 함수
|
||||
* @param key 북마크 식별값 |
||||
*/ |
||||
const onRemoveHighlight = useCallback((key: string, cfiRange: string) => { |
||||
if (!viewerRef.current || !key) return; |
||||
|
||||
dispatch(popHighlight(key)); |
||||
|
||||
dispatch(newSnackbar({ |
||||
text: "The highlight removed successful!",
|
||||
type: "INFO"
|
||||
})); |
||||
|
||||
viewerRef.current.offHighlight(cfiRange); |
||||
}, [dispatch, viewerRef]); |
||||
|
||||
|
||||
/** |
||||
* 하이라이트 여부 확인 |
||||
* - 페이지 변경에 따른 하이라이트 확인 및 적용 |
||||
*/ |
||||
useEffect(() => { |
||||
if (!viewerRef.current) return; |
||||
|
||||
const iframe = viewerRef.current.querySelector('iframe'); |
||||
if (!iframe) return; |
||||
|
||||
const iframeWin = iframe.contentWindow; |
||||
if (!iframeWin) return; |
||||
|
||||
highlights.forEach(highlight => { |
||||
const cfiRange = cfiRangeSpliter(highlight.cfiRange); |
||||
if (!cfiRange) return; |
||||
|
||||
const { startCfi } = cfiRange; |
||||
|
||||
if (compareCfi(currentLocation.startCfi, startCfi) < 1 |
||||
&& compareCfi(currentLocation.endCfi, startCfi) > -1) { |
||||
const node = getNodefromCfi(highlight.paragraphCfi); |
||||
if (!node) return; |
||||
|
||||
viewerRef.current.onHighlight( |
||||
highlight.cfiRange, |
||||
(e: any) => { |
||||
onClickHighlight(e.target); |
||||
setIsContextMenu(true); |
||||
}, |
||||
highlight.color
|
||||
); |
||||
|
||||
iframeWin.getSelection().removeAllRanges(); |
||||
} |
||||
}); |
||||
}, [ |
||||
dispatch, |
||||
viewerRef,
|
||||
currentLocation,
|
||||
highlights,
|
||||
onRemoveHighlight, |
||||
onClickHighlight, |
||||
setIsContextMenu |
||||
]); |
||||
|
||||
|
||||
return {
|
||||
selection,
|
||||
onSelection,
|
||||
onClickHighlight, |
||||
onAddHighlight, |
||||
onRemoveHighlight, |
||||
onUpdateHighlight |
||||
}; |
||||
} |
||||
|
||||
export default useHighlight; |
@ -1,124 +0,0 @@ |
||||
import { useState, useEffect, useCallback } from 'react'; |
||||
|
||||
/** |
||||
* Epub Reader 메뉴를 컨트롤 하는 hook |
||||
* - control의 display를 Menu 생성/제거 플래그로 이용합니다. |
||||
* - control의 show를 Animaton 플래그로 이용합니다. |
||||
* - Menu 이외의 영역 클릭시 Close Event 발동 |
||||
* |
||||
* @param ref DOM을 가지고 있는 useRef |
||||
* @param delay 애니메이션 딜레이 시간(ms) |
||||
*
|
||||
* @example |
||||
* const Viewer = () => { |
||||
* const menuDelay = 300; |
||||
*
|
||||
* const ref = useRef<HTMLDivElement | null>(null); |
||||
* const [control, onToggle] = usemenu(ref, menuDelay); |
||||
*
|
||||
* return (<> |
||||
* <MenuOpenBtn onClick={onToggle} /> |
||||
* <Menu control={control} onToggle={onToggle} ref={ref} /> |
||||
* </>); |
||||
* } |
||||
*
|
||||
* const Menu = ({ control, onToggle }, ref) => { |
||||
* return (<> |
||||
* {control.display && <MenuWrapper show={control.show}
|
||||
* delay={menuDelay} |
||||
* ref={ref}> |
||||
* <CloseBtn onClick={onToggle} /> |
||||
* </MenuWrapper>} |
||||
* </>); |
||||
* } |
||||
*
|
||||
* const MenuWrapper = styled.div<{show: boolean; delay: boolean;}>` |
||||
* transform: ${({show}) => show |
||||
* ? "translateX(0px)" |
||||
* : "translateX(420px)" |
||||
* }; |
||||
* transition: ${({delay}) => `${delay}ms ease`}; |
||||
* `;
|
||||
*/ |
||||
function useMenu(ref: { current: any }, delay: number) { |
||||
const [eventSignal, setEventSignal] = useState<boolean>(true); |
||||
const [control, setControl] = useState<MenuControl>({ |
||||
display: false, |
||||
open: false |
||||
}); |
||||
|
||||
|
||||
/** Menu 토글 */ |
||||
const onToggle = useCallback(() => { |
||||
let event: any = null; |
||||
window.clearTimeout(event); |
||||
if (!control.display) { |
||||
setControl({ display: true, open: false }); |
||||
event = window.setTimeout(() => setControl({ display: true, open: true}), 0); |
||||
} else { |
||||
setControl({ display: true, open: false }); |
||||
event = window.setTimeout(() => setControl({ display: false, open: false }), delay - 50); |
||||
} |
||||
}, [control.display, delay]); |
||||
|
||||
/** |
||||
* 메뉴 닫기
|
||||
* - Event path에서 메뉴가 포함되어있지 않으면, 함수를 실행합니다. |
||||
*/ |
||||
const onClose = useCallback((e: any) => { |
||||
if (!ref || !ref.current) return; |
||||
if (!e.path.includes(ref.current)) { |
||||
onToggle(); |
||||
} |
||||
}, [ref, onToggle]); |
||||
|
||||
/** |
||||
* 이벤트 재등록 |
||||
* - '보기설정' 에서 rendition이 재 렌더링 될 때, 이벤트를 재등록하기 위한 함수 |
||||
* - Iframe 생성 시간 대기 [About 250ms] |
||||
*/ |
||||
const emitEvent = useCallback(() => { |
||||
window.setTimeout(() => setEventSignal(true), 300); |
||||
}, [setEventSignal]);
|
||||
|
||||
|
||||
/** |
||||
* 목차 닫기 이벤트 등록
|
||||
* - Epub이 iframe으로 등록되므로, iframe에도 Event를 등록시켜줍니다. |
||||
*/ |
||||
useEffect(() => { |
||||
if (!eventSignal && !control.display) return; |
||||
|
||||
const epubIframe = document.querySelector('iframe'); |
||||
|
||||
if (control.display) { |
||||
document.addEventListener('click', onClose); |
||||
if (epubIframe && epubIframe.contentWindow) { |
||||
epubIframe.contentWindow.document.addEventListener('click', onClose); |
||||
} |
||||
} else { |
||||
document.removeEventListener('click', onClose); |
||||
if (epubIframe && epubIframe.contentWindow) { |
||||
epubIframe.contentWindow.document.removeEventListener('click', onClose); |
||||
} |
||||
} |
||||
|
||||
setEventSignal(false); |
||||
|
||||
return () => { |
||||
document.removeEventListener('click', onClose); |
||||
if (epubIframe && epubIframe.contentWindow) { |
||||
epubIframe.contentWindow.document.removeEventListener('click', onClose); |
||||
} |
||||
} |
||||
}, [control.display, onClose, eventSignal]); |
||||
|
||||
return [control, onToggle, emitEvent] as const; |
||||
} |
||||
|
||||
export type MenuControl = { |
||||
display: boolean; |
||||
open: boolean; |
||||
} |
||||
|
||||
export default useMenu |
@ -1,62 +0,0 @@ |
||||
import { keyframes } from 'styled-components'; |
||||
|
||||
const fadeIn = keyframes` |
||||
0% { opacity: 0; } |
||||
100% { opacity: 1;} |
||||
`;
|
||||
const fadeOut = keyframes` |
||||
0% { opacity: 1; } |
||||
100% { opacity: 0; } |
||||
`;
|
||||
const fadeInTop = keyframes` |
||||
0% { opacity: 0; transform: translateY(-40px); } |
||||
100% { opacity: 1; transform: translateY(0); } |
||||
`;
|
||||
const fadeOutTop = keyframes` |
||||
0% { opacity: 1; transform: translateY(0); } |
||||
100% { opacity: 0; transform: translateY(-40px); } |
||||
`;
|
||||
const fadeInBottom = keyframes` |
||||
0% { opacity: 0; transform: translateY(40px); } |
||||
100% { opacity: 1; transform: translateY(0); } |
||||
`;
|
||||
const fadeOutBottom = keyframes` |
||||
0% { opacity: 1; transform: translateY(0); } |
||||
100% { opacity: 0; transform: translateY(40px); } |
||||
`;
|
||||
const fadeInLeft = keyframes` |
||||
0% { opacity: 0; transform: translateX(-40px); } |
||||
100% { opacity: 1; transform: translateX(0); } |
||||
`;
|
||||
const fadeOutLeft = keyframes` |
||||
0% { opacity: 1; transform: translateX(0); } |
||||
100% { opacity: 0; transform: translateX(-40px); } |
||||
`;
|
||||
const fadeInRight = keyframes` |
||||
0% { opacity: 0; transform: translateX(40px); } |
||||
100% { opacity: 1; transform: translateX(0); } |
||||
`;
|
||||
const fadeOutRight = keyframes` |
||||
0% { opacity: 1; transform: translateX(0); } |
||||
100% { opacity: 0; transform: translateX(40px); } |
||||
`;
|
||||
const spin = keyframes` |
||||
from { transform: rotate(0deg); } |
||||
to { transform: rotate(360deg); } |
||||
`;
|
||||
|
||||
const animations = { |
||||
fadeIn, |
||||
fadeOut, |
||||
fadeInTop, |
||||
fadeOutTop, |
||||
fadeInBottom, |
||||
fadeOutBottom, |
||||
fadeInLeft, |
||||
fadeOutLeft, |
||||
fadeInRight, |
||||
fadeOutRight, |
||||
spin |
||||
}; |
||||
|
||||
export default animations |
@ -1,31 +0,0 @@ |
||||
export const mediaQuery = (maxWidth: number): string => ` |
||||
@media (max-width: ${maxWidth}px) |
||||
`;
|
||||
|
||||
export const mediaValue = { |
||||
xlarge: 1600, |
||||
large: 1440, |
||||
medium: 1200, |
||||
small: 702, |
||||
xsmall: 350
|
||||
}; |
||||
|
||||
type Media = { |
||||
xlarge : string, |
||||
large : string, |
||||
medium : string, |
||||
small : string, |
||||
xsmall : string, |
||||
custom : (maxWidth: number) => string |
||||
} |
||||
|
||||
const media: Media = { |
||||
xlarge : mediaQuery(mediaValue.xlarge), |
||||
large : mediaQuery(mediaValue.large), |
||||
medium : mediaQuery(mediaValue.medium), |
||||
small : mediaQuery(mediaValue.small), |
||||
xsmall : mediaQuery(mediaValue.xsmall), |
||||
custom : mediaQuery, |
||||
}; |
||||
|
||||
export default media; |
@ -1,79 +0,0 @@ |
||||
/* |
||||
:::: Color Varient :::: |
||||
*/ |
||||
const palette = { |
||||
white: '#ffffff', |
||||
black: '#000000', |
||||
/* gray */ |
||||
gray0: '#fafafa', |
||||
gray1: '#e7e7e7', |
||||
gray2: '#c3c3c3', |
||||
gray3: '#9e9e9e', |
||||
gray4: '#868686', |
||||
gray5: '#656565', |
||||
gray6: '#505050', |
||||
gray7: '#3c3c3c', |
||||
gray8: '#282828', |
||||
/* red */ |
||||
red0: '#ffe8e8', |
||||
red1: '#ffd0d1', |
||||
red2: '#ff8a8b', |
||||
red3: '#ff4445', |
||||
red4: '#ff1517', |
||||
red5: '#bf1011', |
||||
red6: '#990d0e', |
||||
red7: '#73090a', |
||||
red8: '#4d0607', |
||||
/* orange */ |
||||
orange0: '#fff0ec', |
||||
orange1: '#ffe2d9', |
||||
orange2: '#ffb6a0', |
||||
orange3: '#ff8966', |
||||
orange4: '#ff6c40', |
||||
orange5: '#bf5130', |
||||
orange6: '#994126', |
||||
orange7: '#73311d', |
||||
orange8: '#4d2013', |
||||
/* yellow */ |
||||
yellow0: '#fffce6', |
||||
yellow1: '#fffacc', |
||||
yellow2: '#fff280', |
||||
yellow3: '#feea33', |
||||
yellow4: '#fee500', |
||||
yellow5: '#bfac00', |
||||
yellow6: '#988900', |
||||
yellow7: '#726700', |
||||
yellow8: '#4c4500', |
||||
/* green */ |
||||
green0: '#e6f7e6', |
||||
green1: '#cdefcc', |
||||
green2: '#83d880', |
||||
green3: '#39c133', |
||||
green4: '#07b100', |
||||
green5: '#058500', |
||||
green6: '#046a00', |
||||
green7: '#035000', |
||||
green8: '#023500', |
||||
/* blue */ |
||||
blue0: '#ebf1ff', |
||||
blue1: '#d7e3ff ', |
||||
blue2: '#9cb9ff ', |
||||
blue3: '#618eff ', |
||||
blue4: '#3972ff ', |
||||
blue5: '#2b56bf ', |
||||
blue6: '#224499 ', |
||||
blue7: '#1a3373 ', |
||||
blue8: '#11224d ', |
||||
/* purple */ |
||||
purple0: '#f2ebff', |
||||
purple1: '#d7e3ff', |
||||
purple2: '#be9cff', |
||||
purple3: '#9761ff', |
||||
purple4: '#7d39ff', |
||||
purple5: '#5e2bbf', |
||||
purple6: '#4b2299', |
||||
purple7: '#381a73', |
||||
purple8: '#26114d', |
||||
}; |
||||
|
||||
export default palette |
@ -1,24 +0,0 @@ |
||||
a { |
||||
text-decoration: none; |
||||
} |
||||
|
||||
input { |
||||
border: none; |
||||
background-color: rgba(0,0,0,0); |
||||
} |
||||
|
||||
::selection { |
||||
background-color: #d4e9ff; |
||||
} |
||||
|
||||
.epub-highlight { |
||||
fill-opacity: 0.3; |
||||
mix-blend-mode: multiply; |
||||
cursor: pointer; |
||||
pointer-events: fill; |
||||
transition: .1s ease-in-out; |
||||
} |
||||
|
||||
.epub-highlight:hover { |
||||
fill-opacity: 0.4; |
||||
} |
@ -1,35 +0,0 @@ |
||||
export const noselect = (): string => ` |
||||
-webkit-touch-callout: none; |
||||
-webkit-user-select: none; |
||||
-khtml-user-select: none; |
||||
-moz-user-select: none; |
||||
-ms-user-select: none; |
||||
user-select: none; |
||||
`;
|
||||
|
||||
export const transition = (): string =>` |
||||
cubic-bezier(0.25, 0.1, 0.25, 1); |
||||
`;
|
||||
|
||||
export const boxShadow = { |
||||
light: "0px 4px 16px rgba(0, 0, 0, 0.04)", |
||||
regular: "0px 8px 12px rgba(0, 0, 0, 0.2)", |
||||
bold: "0 8px 10px 1px rgba(0,0,0,0.14), 0 3px 8px 2px rgba(0,0,0,0.12), 0 5px 5px -3px rgba(0,0,0,0.2)" |
||||
}; |
||||
|
||||
export const scrollbar = (width=8) => ` |
||||
&::-webkit-scrollbar { |
||||
width: ${width}px; |
||||
background: transparent; |
||||
} |
||||
&::-webkit-scrollbar-thumb { |
||||
background: #ddd; |
||||
border-radius: 10px; |
||||
} |
||||
&::-webkit-scrollbar-thumb:hover { |
||||
background: #ccc; |
||||
} |
||||
&::-webkit-scrollbar-track { |
||||
background: transparent; |
||||
} |
||||
` |
@ -1,14 +0,0 @@ |
||||
import ViewerLayout from 'types/viewerLayout' |
||||
|
||||
/** Epub viewer layout size */ |
||||
const viewerLayout: ViewerLayout = { |
||||
MIN_VIEWER_WIDTH: 600,
|
||||
MIN_VIEWER_HEIGHT: 300, |
||||
VIEWER_HEADER_HEIGHT: 54, |
||||
VIEWER_FOOTER_HEIGHT: 60, |
||||
VIEWER_SIDEMENU_WIDTH: 0 |
||||
} |
||||
|
||||
export const contextmenuWidth = 160; |
||||
|
||||
export default viewerLayout |
@ -1,13 +0,0 @@ |
||||
export const zIndexSet = (layer: number): string => ` |
||||
${layer} |
||||
`;
|
||||
|
||||
const zIndex = { |
||||
header: zIndexSet(5), |
||||
menu: zIndexSet(10), |
||||
modal: zIndexSet(100), |
||||
snackbar: zIndexSet(400), |
||||
tooltip: zIndexSet(500) |
||||
}; |
||||
|
||||
export default zIndex; |
@ -1,36 +0,0 @@ |
||||
import * as React from "react"; |
||||
|
||||
function SvgBookClose(props) { |
||||
return ( |
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}> |
||||
<defs> |
||||
<style> |
||||
{ |
||||
".bookClose_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}" |
||||
} |
||||
</style> |
||||
</defs> |
||||
<g |
||||
id="bookClose_svg__\uB808\uC774\uC5B4_2" |
||||
data-name="\uB808\uC774\uC5B4 2" |
||||
> |
||||
<g |
||||
id="bookClose_svg__\uB808\uC774\uC5B4_1-2" |
||||
data-name="\uB808\uC774\uC5B4 1" |
||||
> |
||||
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} /> |
||||
<path |
||||
className="bookClose_svg__cls-3" |
||||
d="M37.12 3H10.88a3 3 0 00-.43 0A1.71 1.71 0 009.1 4.11a1.87 1.87 0 00-.1.59v29.18c0 1 0 .94 1.88 1.88l18 9a1.8 1.8 0 002.6-1.2 3 3 0 000-.31v-7.58h5.62A1.88 1.88 0 0039 33.8V4.87A1.88 1.88 0 0037.12 3z" |
||||
/> |
||||
<path |
||||
className="bookClose_svg__cls-3" |
||||
d="M31.5 35.67V14.84a2 2 0 00-1.1-1.78L10.45 3" |
||||
/> |
||||
</g> |
||||
</g> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
export default SvgBookClose; |
@ -1,32 +0,0 @@ |
||||
import * as React from "react"; |
||||
|
||||
function SvgBookOpen(props) { |
||||
return ( |
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}> |
||||
<defs> |
||||
<style> |
||||
{ |
||||
".bookOpen_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}" |
||||
} |
||||
</style> |
||||
</defs> |
||||
<g |
||||
id="bookOpen_svg__\uB808\uC774\uC5B4_2" |
||||
data-name="\uB808\uC774\uC5B4 2" |
||||
> |
||||
<g |
||||
id="bookOpen_svg__\uB808\uC774\uC5B4_1-2" |
||||
data-name="\uB808\uC774\uC5B4 1" |
||||
> |
||||
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} /> |
||||
<path |
||||
className="bookOpen_svg__cls-3" |
||||
d="M4 6.5a27.24 27.24 0 0120 1v34a27.24 27.24 0 00-20-1zM24 7.5a27.24 27.24 0 0120-1v34a27.24 27.24 0 00-20 1" |
||||
/> |
||||
</g> |
||||
</g> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
export default SvgBookOpen; |
@ -1,23 +0,0 @@ |
||||
import * as React from "react"; |
||||
|
||||
function SvgBookmark(props) { |
||||
return ( |
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}> |
||||
<g data-name="\uB808\uC774\uC5B4 2"> |
||||
<g data-name="\uB808\uC774\uC5B4 1"> |
||||
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} /> |
||||
<path |
||||
d="M10 5.07V44a1 1 0 001.71.73l10.88-11.29a1.94 1.94 0 012.82 0l10.88 11.25A1 1 0 0038 44V5.07A2 2 0 0036 3H12a2 2 0 00-2 2.07z" |
||||
stroke="#000" |
||||
strokeLinecap="round" |
||||
strokeLinejoin="round" |
||||
strokeWidth={2} |
||||
fill="none" |
||||
/> |
||||
</g> |
||||
</g> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
export default SvgBookmark; |
@ -1,22 +0,0 @@ |
||||
import * as React from "react"; |
||||
|
||||
function SvgBookmarker(props) { |
||||
return ( |
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}> |
||||
<g data-name="\uB808\uC774\uC5B4 2"> |
||||
<g data-name="\uB808\uC774\uC5B4 1"> |
||||
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} /> |
||||
<path |
||||
d="M42.93 12H4c-.93 0-1.39.92-.73 1.46l11.25 9.33a1.51 1.51 0 010 2.42L3.31 34.54c-.66.54-.2 1.46.69 1.46h38.93A1.92 1.92 0 0045 34.29V13.71A1.92 1.92 0 0042.93 12z" |
||||
stroke="#000" |
||||
strokeLinecap="round" |
||||
strokeLinejoin="round" |
||||
strokeWidth={2} |
||||
/> |
||||
</g> |
||||
</g> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
export default SvgBookmarker; |
@ -1,26 +0,0 @@ |
||||
import * as React from "react"; |
||||
|
||||
function SvgClose(props) { |
||||
return ( |
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}> |
||||
<defs> |
||||
<style> |
||||
{ |
||||
".close_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}" |
||||
} |
||||
</style> |
||||
</defs> |
||||
<g id="close_svg__\uB808\uC774\uC5B4_2" data-name="\uB808\uC774\uC5B4 2"> |
||||
<g |
||||
id="close_svg__\uB808\uC774\uC5B4_1-2" |
||||
data-name="\uB808\uC774\uC5B4 1" |
||||
> |
||||
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} /> |
||||
<path className="close_svg__cls-3" d="M4 4l40 40M4 44L44 4" /> |
||||
</g> |
||||
</g> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
export default SvgClose; |
@ -1,35 +0,0 @@ |
||||
import * as React from "react"; |
||||
|
||||
function SvgController(props) { |
||||
return ( |
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}> |
||||
<defs> |
||||
<style> |
||||
{ |
||||
".controller_svg__cls-3{fill:none;stroke:#000;stroke-miterlimit:10;stroke-width:2px}" |
||||
} |
||||
</style> |
||||
</defs> |
||||
<g |
||||
id="controller_svg__\uB808\uC774\uC5B4_2" |
||||
data-name="\uB808\uC774\uC5B4 2" |
||||
> |
||||
<g |
||||
id="controller_svg__\uB808\uC774\uC5B4_1-2" |
||||
data-name="\uB808\uC774\uC5B4 1" |
||||
> |
||||
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} /> |
||||
<circle className="controller_svg__cls-3" cx={39} cy={40} r={4} /> |
||||
<circle className="controller_svg__cls-3" cx={24} cy={24} r={4} /> |
||||
<path d="M20 24a4 4 0 01.55-2H4a2 2 0 000 4h16.55a4 4 0 01-.55-2zM44 22H27.45a3.91 3.91 0 010 4H44a2 2 0 000-4z" /> |
||||
<circle className="controller_svg__cls-3" cx={9} cy={8} r={4} /> |
||||
<path d="M44 6H12.45a3.91 3.91 0 010 4H44a2 2 0 000-4zM5 8a4 4 0 01.55-2H4a2 2 0 000 4h1.55A4 4 0 015 8z" /> |
||||
<circle className="controller_svg__cls-3" cx={39} cy={40} r={4} /> |
||||
<path d="M35 40a4 4 0 01.55-2H4a2 2 0 000 4h31.55a4 4 0 01-.55-2zM44 38h-1.55a3.91 3.91 0 010 4H44a2 2 0 000-4z" /> |
||||
</g> |
||||
</g> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
export default SvgController; |
@ -1,22 +0,0 @@ |
||||
import * as React from "react"; |
||||
|
||||
function SvgDown(props) { |
||||
return ( |
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}> |
||||
<g data-name="\uB808\uC774\uC5B4 2"> |
||||
<g data-name="\uB808\uC774\uC5B4 1"> |
||||
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} /> |
||||
<path |
||||
d="M8.33 15.82l14.32 20.46a1.63 1.63 0 002.7 0l14.32-20.46A1.78 1.78 0 0038.32 13H9.68a1.78 1.78 0 00-1.35 2.82z" |
||||
stroke="#000" |
||||
strokeLinecap="round" |
||||
strokeLinejoin="round" |
||||
strokeWidth={2} |
||||
/> |
||||
</g> |
||||
</g> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
export default SvgDown; |
@ -1,29 +0,0 @@ |
||||
import * as React from "react"; |
||||
|
||||
function SvgLeft(props) { |
||||
return ( |
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}> |
||||
<defs> |
||||
<style> |
||||
{ |
||||
".left_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}" |
||||
} |
||||
</style> |
||||
</defs> |
||||
<g id="left_svg__\uB808\uC774\uC5B4_2" data-name="\uB808\uC774\uC5B4 2"> |
||||
<g |
||||
id="left_svg__\uB808\uC774\uC5B4_1-2" |
||||
data-name="\uB808\uC774\uC5B4 1" |
||||
> |
||||
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} /> |
||||
<path |
||||
className="left_svg__cls-3" |
||||
d="M4 24L18.5 9.5M18.5 38.5L4 24M44 24H4" |
||||
/> |
||||
</g> |
||||
</g> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
export default SvgLeft; |
@ -1,18 +0,0 @@ |
||||
import * as React from "react"; |
||||
|
||||
function SvgMenu(props) { |
||||
return ( |
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}> |
||||
<g data-name="\uB808\uC774\uC5B4 2"> |
||||
<g data-name="\uB808\uC774\uC5B4 1"> |
||||
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} /> |
||||
<rect x={2} y={6} width={44} height={4} rx={2} /> |
||||
<rect x={2} y={22} width={44} height={4} rx={2} /> |
||||
<rect x={2} y={38} width={44} height={4} rx={2} /> |
||||
</g> |
||||
</g> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
export default SvgMenu; |
@ -1,26 +0,0 @@ |
||||
import * as React from "react"; |
||||
|
||||
function SvgNext(props) { |
||||
return ( |
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44" {...props}> |
||||
<defs> |
||||
<style> |
||||
{ |
||||
".next_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}" |
||||
} |
||||
</style> |
||||
</defs> |
||||
<g id="next_svg__\uB808\uC774\uC5B4_2" data-name="\uB808\uC774\uC5B4 2"> |
||||
<g |
||||
id="next_svg__\uB808\uC774\uC5B4_1-2" |
||||
data-name="\uB808\uC774\uC5B4 1" |
||||
> |
||||
<path fill="none" d="M0 0h44v44H0z" opacity={0.3} /> |
||||
<path className="next_svg__cls-3" d="M32 22L12 2M32 22L12 42" /> |
||||
</g> |
||||
</g> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
export default SvgNext; |
@ -1,26 +0,0 @@ |
||||
import * as React from "react"; |
||||
|
||||
function SvgPrev(props) { |
||||
return ( |
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44" {...props}> |
||||
<defs> |
||||
<style> |
||||
{ |
||||
".prev_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}" |
||||
} |
||||
</style> |
||||
</defs> |
||||
<g id="prev_svg__\uB808\uC774\uC5B4_2" data-name="\uB808\uC774\uC5B4 2"> |
||||
<g |
||||
id="prev_svg__\uB808\uC774\uC5B4_1-2" |
||||
data-name="\uB808\uC774\uC5B4 1" |
||||
> |
||||
<path fill="none" d="M0 0h44v44H0z" opacity={0.3} /> |
||||
<path className="prev_svg__cls-3" d="M12 22L32 2M12 22l20 20" /> |
||||
</g> |
||||
</g> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
export default SvgPrev; |
@ -1,38 +0,0 @@ |
||||
import * as React from "react"; |
||||
|
||||
function SvgScrollHorizontal(props) { |
||||
return ( |
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}> |
||||
<defs> |
||||
<style> |
||||
{ |
||||
".scrollHorizontal_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}" |
||||
} |
||||
</style> |
||||
</defs> |
||||
<g |
||||
id="scrollHorizontal_svg__\uB808\uC774\uC5B4_2" |
||||
data-name="\uB808\uC774\uC5B4 2" |
||||
> |
||||
<g |
||||
id="scrollHorizontal_svg__\uB808\uC774\uC5B4_1-2" |
||||
data-name="\uB808\uC774\uC5B4 1" |
||||
> |
||||
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} /> |
||||
<circle |
||||
className="scrollHorizontal_svg__cls-3" |
||||
cx={24} |
||||
cy={24} |
||||
r={5} |
||||
/> |
||||
<path |
||||
className="scrollHorizontal_svg__cls-3" |
||||
d="M3 24l12-12M3 24l12 12M45 24L33 12M45 24L33 36" |
||||
/> |
||||
</g> |
||||
</g> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
export default SvgScrollHorizontal; |
@ -1,33 +0,0 @@ |
||||
import * as React from "react"; |
||||
|
||||
function SvgScrollVertical(props) { |
||||
return ( |
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}> |
||||
<defs> |
||||
<style> |
||||
{ |
||||
".scrollVertical_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}" |
||||
} |
||||
</style> |
||||
</defs> |
||||
<g |
||||
id="scrollVertical_svg__\uB808\uC774\uC5B4_2" |
||||
data-name="\uB808\uC774\uC5B4 2" |
||||
> |
||||
<g |
||||
id="scrollVertical_svg__\uB808\uC774\uC5B4_1-2" |
||||
data-name="\uB808\uC774\uC5B4 1" |
||||
> |
||||
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} /> |
||||
<circle className="scrollVertical_svg__cls-3" cx={24} cy={24} r={5} /> |
||||
<path |
||||
className="scrollVertical_svg__cls-3" |
||||
d="M24 3l12 12M24 3L12 15M24 45l12-12M24 45L12 33" |
||||
/> |
||||
</g> |
||||
</g> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
export default SvgScrollVertical; |
@ -1,27 +0,0 @@ |
||||
import * as React from "react"; |
||||
|
||||
function SvgSearch(props) { |
||||
return ( |
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}> |
||||
<defs> |
||||
<style> |
||||
{ |
||||
".search_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}" |
||||
} |
||||
</style> |
||||
</defs> |
||||
<g id="search_svg__\uB808\uC774\uC5B4_2" data-name="\uB808\uC774\uC5B4 2"> |
||||
<g |
||||
id="search_svg__\uB808\uC774\uC5B4_1-2" |
||||
data-name="\uB808\uC774\uC5B4 1" |
||||
> |
||||
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} /> |
||||
<circle className="search_svg__cls-3" cx={17} cy={17} r={14} /> |
||||
<path className="search_svg__cls-3" d="M44 44L27 27" /> |
||||
</g> |
||||
</g> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
export default SvgSearch; |
@ -1,10 +0,0 @@ |
||||
import styled from 'styled-components' |
||||
|
||||
const TempIcon = styled.div` |
||||
width: 32px; |
||||
height: 32px; |
||||
background-color: #ccc; |
||||
border-radius: 32px; |
||||
`;
|
||||
|
||||
export default TempIcon |
Before Width: | Height: | Size: 691 B |
Before Width: | Height: | Size: 546 B |
Before Width: | Height: | Size: 542 B |
Before Width: | Height: | Size: 568 B |
Before Width: | Height: | Size: 484 B |
Before Width: | Height: | Size: 937 B |
Before Width: | Height: | Size: 516 B |
@ -1,31 +0,0 @@ |
||||
import MenuIcon from './Menu' |
||||
import ControllerIcon from './Controller'; |
||||
import PrevIcon from './Prev'; |
||||
import NextIcon from './Next'; |
||||
import BookCloseIcon from './BookClose'; |
||||
import BookOpenIcon from './BookOpen'; |
||||
import BookmarkIcon from './Bookmark'; |
||||
import BookmarkerIcon from './Bookmarker'; |
||||
import CloseIcon from './Close'; |
||||
import DownIcon from './Down'; |
||||
import LeftIcon from './Left'; |
||||
import ScrollHorizontalIcon from './ScrollHorizontal'; |
||||
import ScrollVerticalIcon from './ScrollVertical'; |
||||
import SearchIcon from './Search'; |
||||
|
||||
export {
|
||||
MenuIcon, |
||||
ControllerIcon, |
||||
PrevIcon, |
||||
NextIcon, |
||||
BookCloseIcon, |
||||
BookOpenIcon, |
||||
BookmarkIcon, |
||||
BookmarkerIcon, |
||||
CloseIcon, |
||||
DownIcon, |
||||
LeftIcon, |
||||
ScrollHorizontalIcon, |
||||
ScrollVerticalIcon, |
||||
SearchIcon |
||||
}; |
Before Width: | Height: | Size: 545 B |
Before Width: | Height: | Size: 441 B |
Before Width: | Height: | Size: 487 B |
Before Width: | Height: | Size: 487 B |
Before Width: | Height: | Size: 637 B |
Before Width: | Height: | Size: 637 B |
Before Width: | Height: | Size: 481 B |
@ -1,123 +0,0 @@ |
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; |
||||
// types
|
||||
import Book from 'types/book' |
||||
import Page from 'types/page' |
||||
import Toc from 'types/toc' |
||||
import Highlight, { Color } from 'types/highlight' |
||||
// lib
|
||||
import palette from 'lib/styles/palette' |
||||
|
||||
|
||||
/* |
||||
Initial State |
||||
*/ |
||||
|
||||
/** 초기 Book 상태 */ |
||||
const initialBook: Book = { |
||||
coverURL: '', |
||||
title: '', |
||||
description: '', |
||||
published_date: '', |
||||
modified_date: '', |
||||
author: '', |
||||
publisher: '', |
||||
language: '' |
||||
} |
||||
|
||||
/** 초기 CurrentPage 상태 */ |
||||
const initialCurrentLocation: Page = { |
||||
chapterName: "-", |
||||
currentPage: 0, |
||||
totalPage: 0, |
||||
startCfi: '', |
||||
endCfi: '', |
||||
base: '' |
||||
} |
||||
|
||||
/** 초기 색상표 상태 */ |
||||
const initialColorList: Color[] = [ |
||||
{ name: 'Red', code: palette.red4 }, |
||||
{ name: 'Orange', code: palette.orange4 }, |
||||
{ name: 'Yellow', code: palette.yellow4 }, |
||||
{ name: 'Green', code: palette.green4 }, |
||||
{ name: 'Blue', code: palette.blue4 }, |
||||
{ name: 'Purple', code: palette.purple4 } |
||||
] |
||||
|
||||
const initialState: BookState = { |
||||
book: initialBook, |
||||
currentLocation: initialCurrentLocation, |
||||
toc: [], |
||||
highlights: [], |
||||
colorList: initialColorList, |
||||
} |
||||
|
||||
|
||||
/* |
||||
Slice |
||||
*/ |
||||
const bookSlice = createSlice({ |
||||
name: 'book', |
||||
initialState, |
||||
reducers: { |
||||
/** Book 갱신 @dispatch */ |
||||
updateBook(state, action: PayloadAction<Book>) { |
||||
state.book = action.payload; |
||||
}, |
||||
/** CurrentPage 갱신 @dispatch */ |
||||
updateCurrentPage(state, action: PayloadAction<Page>) { |
||||
state.currentLocation = action.payload; |
||||
}, |
||||
/** Book 초기화 @dispatch */ |
||||
clearBook(state) { |
||||
state.book = initialBook; |
||||
}, |
||||
/** 목차 갱신 @dispatch */ |
||||
updateToc(state, action: PayloadAction<Toc[]>) { |
||||
state.toc = action.payload; |
||||
}, |
||||
/** 목차 초기화 @dispatch */ |
||||
clearToc(state) { |
||||
state.toc = []; |
||||
}, |
||||
/** 하이라이트 추가 */ |
||||
pushHighlight(state, action: PayloadAction<Highlight>) { |
||||
const check = state.highlights.filter(h => h.key === action.payload.key); |
||||
if (check.length > 0) return; |
||||
state.highlights.push(action.payload); |
||||
}, |
||||
/** 하이라이트 갱신 */ |
||||
updateHighlight(state, action: PayloadAction<Highlight>) { |
||||
const new_idx = action.payload.key; |
||||
const old_idx = state.highlights.map(h => h.key).indexOf(new_idx); |
||||
state.highlights.splice(old_idx, 1, action.payload); |
||||
}, |
||||
/** 하이라이트 삭제 */ |
||||
popHighlight(state, action: PayloadAction<string>) { |
||||
const idx = state.highlights.map(h => h.key).indexOf(action.payload); |
||||
state.highlights.splice(idx, 1); |
||||
} |
||||
} |
||||
}); |
||||
|
||||
|
||||
export interface BookState { |
||||
book: Book; |
||||
currentLocation: Page; |
||||
toc: Toc[]; |
||||
highlights: Highlight[]; |
||||
colorList: Color[]; |
||||
} |
||||
|
||||
export const {
|
||||
updateBook, |
||||
clearBook, |
||||
updateCurrentPage, |
||||
updateToc, |
||||
clearToc, |
||||
pushHighlight, |
||||
updateHighlight, |
||||
popHighlight |
||||
} = bookSlice.actions; |
||||
|
||||
export default bookSlice.reducer |
@ -1,24 +0,0 @@ |
||||
import { configureStore } from "@reduxjs/toolkit"; |
||||
import { combineReducers } from 'redux' |
||||
// reducers
|
||||
import book from 'slices/book' |
||||
import snackbar from 'slices/snackbar' |
||||
|
||||
/** |
||||
* 모든 리듀서가 결합된 Root 리듀서 |
||||
*/ |
||||
const reducer = combineReducers({ |
||||
book, |
||||
snackbar |
||||
}); |
||||
|
||||
export type RootState = ReturnType<typeof reducer> |
||||
|
||||
/** |
||||
* 공통 Store |
||||
*/ |
||||
const store = configureStore({ |
||||
reducer,
|
||||
}); |
||||
|
||||
export default store |
@ -1,74 +0,0 @@ |
||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit' |
||||
// types
|
||||
import Snackbar from 'types/snackbar' |
||||
|
||||
|
||||
const RefEvent: number[] = []; |
||||
|
||||
|
||||
/* |
||||
Thunks |
||||
*/ |
||||
export const newSnackbar = createAsyncThunk( |
||||
'snackbar/newSnackbar', |
||||
async ( |
||||
{ text, type }: SnackbarState,
|
||||
{ dispatch } |
||||
) => { |
||||
if(RefEvent.length !== 0) { |
||||
let event = RefEvent.shift(); |
||||
clearTimeout(event); |
||||
} |
||||
dispatch(SnackbarSlice.actions.deleteSnackbar()); |
||||
await setTimeout(function() { |
||||
dispatch(SnackbarSlice.actions.appendSnackbar({ text, type: type ? type: "INFO" })); |
||||
let event = window.setTimeout(function() { |
||||
dispatch(SnackbarSlice.actions.deleteSnackbar()); |
||||
}, 4000); |
||||
RefEvent.push(event); |
||||
}, 50); |
||||
} |
||||
); |
||||
|
||||
/* |
||||
Initial State |
||||
*/ |
||||
|
||||
const initialState: SnackbarState = { |
||||
text: "", |
||||
type: "INFO" |
||||
} |
||||
|
||||
|
||||
/* |
||||
Slice |
||||
*/ |
||||
const SnackbarSlice = createSlice({ |
||||
name: 'snackbar', |
||||
initialState, |
||||
reducers: { |
||||
/** 스낵바 갱신 @dispatch */ |
||||
appendSnackbar(state, action: PayloadAction<SnackbarState>) { |
||||
state.text = action.payload.text; |
||||
state.type = action.payload.type; |
||||
}, |
||||
/** 스낵바 초기화 @dispatch */ |
||||
deleteSnackbar(state) { |
||||
state.text = ""; |
||||
state.type = "INFO" |
||||
} |
||||
} |
||||
}); |
||||
|
||||
|
||||
export interface SnackbarState { |
||||
text: string; |
||||
type: Snackbar; |
||||
} |
||||
|
||||
export const { |
||||
appendSnackbar, |
||||
deleteSnackbar |
||||
} = SnackbarSlice.actions |
||||
|
||||
export default SnackbarSlice.reducer |