Просмотр исходного кода

use hook instead HOC and remove any type

RoyLiu 4 лет назад
Родитель
Сommit
8f747b3795
40 измененных файлов с 568 добавлено и 632 удалено
  1. 5 16
      components/AnnotationList/index.tsx
  2. 14 4
      components/AnnotationListHead/index.tsx
  3. 29 37
      components/DeleteDialog/index.tsx
  4. 98 106
      components/FreeTextOption/index.tsx
  5. 35 33
      components/Head.tsx
  6. 43 51
      components/HighlightOption/index.tsx
  7. 68 76
      components/InkOption/index.tsx
  8. 4 4
      components/Navbar/data.ts
  9. 0 1
      components/Navbar/index.tsx
  10. 4 15
      components/Pagination/index.tsx
  11. 4 15
      components/Search/index.tsx
  12. 57 65
      components/ShapeOption/index.tsx
  13. 4 15
      components/Toolbar/index.tsx
  14. 1 1
      components/Typography/index.tsx
  15. 1 0
      components/Viewer/styled.ts
  16. 1 0
      components/Watermark/styled.ts
  17. 89 97
      components/WatermarkOption/index.tsx
  18. 4 1
      containers/AnnotationListHead.tsx
  19. 4 2
      containers/AutoSave.tsx
  20. 4 9
      containers/CreateForm.tsx
  21. 3 2
      containers/FreehandTools.tsx
  22. 4 9
      containers/MarkupTools.tsx
  23. 11 3
      containers/Navbar.tsx
  24. 7 2
      containers/PdfViewer.tsx
  25. 4 9
      containers/Sidebar.tsx
  26. 14 16
      containers/WatermarkTool.tsx
  27. 3 3
      helpers/annotation.ts
  28. 4 4
      helpers/dom.ts
  29. 5 5
      helpers/position.ts
  30. 1 1
      helpers/time.ts
  31. 1 0
      helpers/utility.ts
  32. 12 11
      helpers/watermark.ts
  33. 8 10
      hooks/useAutoSave.ts
  34. 3 3
      hooks/useCursorPosition.ts
  35. 1 0
      package.json
  36. 4 0
      public/locales/en/toast.json
  37. 4 0
      public/locales/zh-tw/toast.json
  38. 1 1
      store/index.tsx
  39. 4 5
      store/initialPdfState.ts
  40. 5 0
      yarn.lock

+ 5 - 16
components/AnnotationList/index.tsx

@@ -1,8 +1,7 @@
 import React, {
   useEffect, useState, useRef, useCallback,
 } from 'react';
-import { WithTranslation } from 'next-i18next';
-import { withTranslation } from '../../i18n';
+import { useTranslation } from '../../i18n';
 
 import Typography from '../Typography';
 import Item from '../AnnotationItem';
@@ -15,11 +14,7 @@ import { getAnnotationText } from '../../helpers/annotation';
 
 import { Body } from '../../global/sidebarStyled';
 
-type i18nProps = {
-  t: (key: string) => string;
-};
-
-type OwnerProps = {
+type Props = {
   isActive?: boolean;
   annotations: AnnotationType[];
   viewport: ViewportType;
@@ -27,16 +22,14 @@ type OwnerProps = {
   pdf: any;
 };
 
-type Props = i18nProps & OwnerProps;
-
 const AnnotationList: React.FC<Props> = ({
-  t,
   isActive = false,
   annotations,
   viewport,
   scale,
   pdf,
 }: Props) => {
+  const { t } = useTranslation('sidebar');
   const [renderQueue, setQueue] = useState<AnnotationType[]>([]);
   const containerRef = useRef<HTMLDivElement>(null);
   const innerRef = useRef<HTMLDivElement>(null);
@@ -80,7 +73,7 @@ const AnnotationList: React.FC<Props> = ({
     if (isActive) {
       setQueue(annotations.slice(0, 10));
     }
-  }, [isActive]);
+  }, [isActive, annotations]);
 
   return (
     <Body ref={containerRef}>
@@ -120,8 +113,4 @@ const AnnotationList: React.FC<Props> = ({
   );
 };
 
-const translator = withTranslation('sidebar');
-
-type TransProps = WithTranslation & OwnerProps;
-
-export default translator<TransProps>(AnnotationList);
+export default AnnotationList;

+ 14 - 4
components/AnnotationListHead/index.tsx

@@ -14,16 +14,20 @@ import {
 type Props = {
   close: () => void;
   addAnnots: (annotations: AnnotationType[]) => void;
+  hasAnnots: boolean;
 };
 
 const index: React.FC<Props> = ({
   close,
   addAnnots,
+  hasAnnots,
 }: Props) => {
   const handleExport = (): void => {
-    const parsed = queryString.parse(window.location.search);
-    const uri = `/api/v1/output.xfdf?f=${parsed.token}`;
-    downloadFileWithUri('output.xfdf', uri);
+    if (hasAnnots) {
+      const parsed = queryString.parse(window.location.search);
+      const uri = `/api/v1/output.xfdf?f=${parsed.token}`;
+      downloadFileWithUri('output.xfdf', uri);
+    }
   };
 
   const handleImport = (): void => {
@@ -43,7 +47,13 @@ const index: React.FC<Props> = ({
         <Icon glyph="sort" />
       </IconWrapper>
       <IconWrapper onClick={handleExport}>
-        <Icon glyph="annotation-export" />
+        <Icon
+          glyph="annotation-export"
+          style={{
+            opacity: hasAnnots ? 1 : 0.5,
+            cursor: hasAnnots ? 'pointer' : 'default',
+          }}
+        />
       </IconWrapper>
       <IconWrapper onClick={handleImport}>
         <Icon glyph="import" />

+ 29 - 37
components/DeleteDialog/index.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
-import { WithTranslation } from 'next-i18next';
-import { withTranslation } from '../../i18n';
+import { useTranslation } from '../../i18n';
 
 import Dialog from '../Dialog';
 import Button from '../Button';
@@ -9,48 +8,41 @@ import {
   TextWrapper, BtnWrapper,
 } from './styled';
 
-type i18nProps = {
-  t: (key: string) => string;
-}
-
-type OwnerProps = {
+type Props = {
   open: boolean;
   onCancel: (e: any) => void;
   onDelete: (e: any) => void;
 };
 
-type Props = i18nProps & OwnerProps
-
 const index: React.FC<Props> = ({
-  t,
   open,
   onCancel,
   onDelete,
-}: Props) => (
-  <Dialog open={open}>
-    <TextWrapper>
-      {t('deleteAnnotationAlert')}
-    </TextWrapper>
-    <BtnWrapper>
-      <Button
-        appearance="default-hollow"
-        onClick={onCancel}
-      >
-        {t('cancel')}
-      </Button>
-      <Button
-        appearance="primary"
-        onClick={onDelete}
-        style={{ marginLeft: '16px' }}
-      >
-        {t('continue')}
-      </Button>
-    </BtnWrapper>
-  </Dialog>
-);
-
-const translator = withTranslation('dialog');
-
-type TransProps = WithTranslation & OwnerProps;
+}: Props) => {
+  const { t } = useTranslation('dialog');
+
+  return (
+    <Dialog open={open}>
+      <TextWrapper>
+        {t('deleteAnnotationAlert')}
+      </TextWrapper>
+      <BtnWrapper>
+        <Button
+          appearance="default-hollow"
+          onClick={onCancel}
+        >
+          {t('cancel')}
+        </Button>
+        <Button
+          appearance="primary"
+          onClick={onDelete}
+          style={{ marginLeft: '16px' }}
+        >
+          {t('continue')}
+        </Button>
+      </BtnWrapper>
+    </Dialog>
+  )
+};
 
-export default translator<TransProps>(index);
+export default index;

+ 98 - 106
components/FreeTextOption/index.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
-import { WithTranslation } from 'next-i18next';
-import { withTranslation } from '../../i18n';
+import { useTranslation } from '../../i18n';
 
 import Icon from '../Icon';
 import Typography from '../Typography';
@@ -20,14 +19,7 @@ import {
   styleOptions,
 } from './data';
 
-type i18nProps = {
-  t: (key: string) => string;
-}
-
-type Props = i18nProps & OptionPropsType;
-
-const TextOption: React.SFC<Props> = ({
-  t,
+const TextOption: React.SFC<OptionPropsType> = ({
   fontName,
   fontSize,
   align,
@@ -37,102 +29,102 @@ const TextOption: React.SFC<Props> = ({
   setDataState = (): void => {
     // do nothing
   },
-}: Props) => (
-  <>
-    <Wrapper>
-      <Typography
-        variant="subtitle"
-        style={{ marginTop: '8px' }}
-        align="left"
-      >
-        {t('font')}
-      </Typography>
-      <Group>
-        <SelectBox
-          style={{ width: '100px' }}
-          defaultValue={fontName}
-          options={fontOptions}
-          onChange={(option: SelectOptionType): void => {
-            setDataState({ fontName: option.child });
-          }}
+}: OptionPropsType) => {
+  const { t } = useTranslation('sidebar');
+
+  return (
+    <>
+      <Wrapper>
+        <Typography
+          variant="subtitle"
+          style={{ marginTop: '8px' }}
+          align="left"
+        >
+          {t('font')}
+        </Typography>
+        <Group>
+          <SelectBox
+            style={{ width: '100px' }}
+            defaultValue={fontName}
+            options={fontOptions}
+            onChange={(option: SelectOptionType): void => {
+              setDataState({ fontName: option.child });
+            }}
+          />
+          {
+            styleOptions.map(ele => (
+              <Item
+                key={ele.key}
+                size="small"
+                selected={fontStyle === ele.child}
+                onClick={(): void => {
+                  setDataState({ fontStyle: ele.child === fontStyle ? '' : ele.child });
+                }}
+              >
+                <Icon glyph={ele.key} />
+              </Item>
+            ))
+          }
+        </Group>
+      </Wrapper>
+      <Wrapper width="40%">
+        <Typography
+          variant="subtitle"
+          style={{ marginTop: '8px' }}
+          align="left"
+        >
+          {t('size')}
+        </Typography>
+        <Group>
+          <SelectBox
+            defaultValue={fontSize}
+            options={sizeOptions}
+            onChange={(option: SelectOptionType): void => {
+              setDataState({ fontSize: option.child });
+            }}
+          />
+        </Group>
+      </Wrapper>
+      <Wrapper width="60%">
+        <Typography
+          variant="subtitle"
+          style={{ marginTop: '8px' }}
+          align="left"
+        >
+          {t('align')}
+        </Typography>
+        <Group>
+          {
+            alignOptions.map(ele => (
+              <Item
+                key={ele.key}
+                size="small"
+                selected={align === ele.child}
+                onClick={(): void => { setDataState({ align: ele.child }); }}
+              >
+                <Icon glyph={ele.key} />
+              </Item>
+            ))
+          }
+        </Group>
+      </Wrapper>
+      <Wrapper>
+        <ColorSelector
+          title={t('color')}
+          selectedColor={color}
+          onClick={(selected: string): void => { setDataState({ color: selected }); }}
         />
-        {
-          styleOptions.map(ele => (
-            <Item
-              key={ele.key}
-              size="small"
-              selected={fontStyle === ele.child}
-              onClick={(): void => {
-                setDataState({ fontStyle: ele.child === fontStyle ? '' : ele.child });
-              }}
-            >
-              <Icon glyph={ele.key} />
-            </Item>
-          ))
-        }
-      </Group>
-    </Wrapper>
-    <Wrapper width="40%">
-      <Typography
-        variant="subtitle"
-        style={{ marginTop: '8px' }}
-        align="left"
-      >
-        {t('size')}
-      </Typography>
-      <Group>
-        <SelectBox
-          defaultValue={fontSize}
-          options={sizeOptions}
-          onChange={(option: SelectOptionType): void => {
-            setDataState({ fontSize: option.child });
-          }}
+      </Wrapper>
+      <Wrapper>
+        <SliderWithTitle
+          title={t('opacity')}
+          value={opacity}
+          tips={`${opacity}%`}
+          onSlide={(val: number): void => { setDataState({ opacity: val }); }}
         />
-      </Group>
-    </Wrapper>
-    <Wrapper width="60%">
-      <Typography
-        variant="subtitle"
-        style={{ marginTop: '8px' }}
-        align="left"
-      >
-        {t('align')}
-      </Typography>
-      <Group>
-        {
-          alignOptions.map(ele => (
-            <Item
-              key={ele.key}
-              size="small"
-              selected={align === ele.child}
-              onClick={(): void => { setDataState({ align: ele.child }); }}
-            >
-              <Icon glyph={ele.key} />
-            </Item>
-          ))
-        }
-      </Group>
-    </Wrapper>
-    <Wrapper>
-      <ColorSelector
-        title={t('color')}
-        selectedColor={color}
-        onClick={(selected: string): void => { setDataState({ color: selected }); }}
-      />
-    </Wrapper>
-    <Wrapper>
-      <SliderWithTitle
-        title={t('opacity')}
-        value={opacity}
-        tips={`${opacity}%`}
-        onSlide={(val: number): void => { setDataState({ opacity: val }); }}
-      />
-    </Wrapper>
-  </>
-);
-
-const translator = withTranslation('sidebar');
-
-type TransProps = WithTranslation & OptionPropsType;
+      </Wrapper>
+    </>
+  );
+};
 
-export default translator<TransProps>(TextOption);
+export default TextOption;

+ 35 - 33
components/Head.tsx

@@ -1,51 +1,53 @@
 import React from 'react';
 import NextHead from 'next/head';
-import { withTranslation } from '../i18n';
+import { useTranslation } from '../i18n';
 
 const defaultOGURL = '';
 const defaultOGImage = '';
 
 type Props = {
-  t: (key: string) => string;
   title?: string;
   description?: string;
   url?: string;
   ogImage?: string;
 };
 
-const Head: React.StatelessComponent<Props> = ({
-  t,
+const Head: React.SFC<Props> = ({
   title = 'title',
   description = 'description',
   url = '',
   ogImage = '',
-}: Props) => (
-  <NextHead>
-    <meta charSet="UTF-8" />
-    <title>{t(title)}</title>
-    <meta
-      name="description"
-      content={t(description)}
-    />
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
-    <link rel="icon" sizes="192x192" href="/static/touch-icon.png" />
-    <link rel="apple-touch-icon" href="/static/touch-icon.png" />
-    <link rel="mask-icon" href="/static/favicon-mask.svg" color="#49B882" />
-    <link rel="icon" href="/static/favicon.ico" />
-    <meta property="og:url" content={url || defaultOGURL} />
-    <meta property="og:title" content={title || ''} />
-    <meta
-      property="og:description"
-      content={t(description)}
-    />
-    <meta name="twitter:site" content={url || defaultOGURL} />
-    <meta name="twitter:card" content="summary_large_image" />
-    <meta name="twitter:image" content={ogImage || defaultOGImage} />
-    <meta property="og:image" content={ogImage || defaultOGImage} />
-    <meta property="og:image:width" content="1200" />
-    <meta property="og:image:height" content="630" />
-    <meta httpEquiv="X-UA-Compatible" content="IE=EmulateIE11,chrome=1" />
-  </NextHead>
-);
+}: Props) => {
+  const { t } = useTranslation('meta');
 
-export default withTranslation('meta')(Head);
+  return (
+    <NextHead>
+      <meta charSet="UTF-8" />
+      <title>{t(title)}</title>
+      <meta
+        name="description"
+        content={t(description)}
+      />
+      <meta name="viewport" content="width=device-width, initial-scale=1" />
+      <link rel="icon" sizes="192x192" href="/static/touch-icon.png" />
+      <link rel="apple-touch-icon" href="/static/touch-icon.png" />
+      <link rel="mask-icon" href="/static/favicon-mask.svg" color="#49B882" />
+      <link rel="icon" href="/static/favicon.ico" />
+      <meta property="og:url" content={url || defaultOGURL} />
+      <meta property="og:title" content={title || ''} />
+      <meta
+        property="og:description"
+        content={t(description)}
+      />
+      <meta name="twitter:site" content={url || defaultOGURL} />
+      <meta name="twitter:card" content="summary_large_image" />
+      <meta name="twitter:image" content={ogImage || defaultOGImage} />
+      <meta property="og:image" content={ogImage || defaultOGImage} />
+      <meta property="og:image:width" content="1200" />
+      <meta property="og:image:height" content="630" />
+      <meta httpEquiv="X-UA-Compatible" content="IE=EmulateIE11,chrome=1" />
+    </NextHead>
+  );
+};
+
+export default Head;

+ 43 - 51
components/HighlightOption/index.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
-import { WithTranslation } from 'next-i18next';
-import { withTranslation } from '../../i18n';
+import { useTranslation } from '../../i18n';
 
 import Icon from '../Icon';
 import Typography from '../Typography';
@@ -11,58 +10,51 @@ import { OptionPropsType } from '../../constants/type';
 import { Group, Item, Wrapper } from '../../global/toolStyled';
 import data from './data';
 
-type i18nProps = {
-  t: (key: string) => string;
-}
-
-type Props = OptionPropsType & i18nProps;
-
-const HighlightOption: React.SFC<Props> = ({
-  t,
+const HighlightOption: React.SFC<OptionPropsType> = ({
   type,
   color,
   opacity,
   setDataState = (): void => {
     // do nothing
   },
-}: Props) => (
-  <>
-    <Wrapper>
-      <Typography variant="subtitle" style={{ marginTop: '8px' }} align="left">
-        {t('style')}
-      </Typography>
-      <Group>
-        {data.lineType.map(ele => (
-          <Item
-            key={ele.key}
-            selected={type === ele.key}
-            onClick={(): void => { setDataState({ type: ele.key }); }}
-          >
-            <Icon glyph={ele.icon} />
-          </Item>
-        ))}
-      </Group>
-    </Wrapper>
-    <Wrapper>
-      <ColorSelector
-        title={t('color')}
-        selectedColor={color}
-        onClick={(selected: string): void => { setDataState({ color: selected }); }}
-      />
-    </Wrapper>
-    <Wrapper>
-      <SliderWithTitle
-        title={t('opacity')}
-        value={opacity}
-        tips={`${opacity}%`}
-        onSlide={(val: number): void => { setDataState({ opacity: val }); }}
-      />
-    </Wrapper>
-  </>
-);
-
-const translator = withTranslation('sidebar');
-
-type TransProps = WithTranslation & OptionPropsType;
-
-export default translator<TransProps>(HighlightOption);
+}: OptionPropsType) => {
+  const { t } = useTranslation('sidebar');
+
+  return (
+    <>
+      <Wrapper>
+        <Typography variant="subtitle" style={{ marginTop: '8px' }} align="left">
+          {t('style')}
+        </Typography>
+        <Group>
+          {data.lineType.map(ele => (
+            <Item
+              key={ele.key}
+              selected={type === ele.key}
+              onClick={(): void => { setDataState({ type: ele.key }); }}
+            >
+              <Icon glyph={ele.icon} />
+            </Item>
+          ))}
+        </Group>
+      </Wrapper>
+      <Wrapper>
+        <ColorSelector
+          title={t('color')}
+          selectedColor={color}
+          onClick={(selected: string): void => { setDataState({ color: selected }); }}
+        />
+      </Wrapper>
+      <Wrapper>
+        <SliderWithTitle
+          title={t('opacity')}
+          value={opacity}
+          tips={`${opacity}%`}
+          onSlide={(val: number): void => { setDataState({ opacity: val }); }}
+        />
+      </Wrapper>
+    </>
+  );
+};
+
+export default HighlightOption;

+ 68 - 76
components/InkOption/index.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
-import { WithTranslation } from 'next-i18next';
-import { withTranslation } from '../../i18n';
+import { useTranslation } from '../../i18n';
 
 import { OptionPropsType } from '../../constants/type';
 import Icon from '../Icon';
@@ -11,14 +10,7 @@ import SliderWithTitle from '../SliderWithTitle';
 
 import { Group, Item, Wrapper } from '../../global/toolStyled';
 
-type i18nProps = {
-  t: (key: string) => string;
-}
-
-type Props = i18nProps & OptionPropsType;
-
-const FreehandOption: React.SFC<Props> = ({
-  t,
+const FreehandOption: React.SFC<OptionPropsType> = ({
   type,
   color,
   opacity,
@@ -26,71 +18,71 @@ const FreehandOption: React.SFC<Props> = ({
   setDataState = (): void => {
     // do nothing
   },
-}: Props) => (
-  <>
-    <Wrapper>
-      <Typography variant="subtitle" style={{ marginTop: '4px' }} align="left">
-        {t('style')}
-      </Typography>
-      <Group>
-        <Item
-          size="small"
-          selected={type === 'pen'}
-          onClick={(): void => { setDataState({ type: 'pen' }); }}
-        >
-          <Icon glyph="freehand" />
-        </Item>
-        <Item size="small">
-          <Icon glyph="eraser" />
-        </Item>
-        <Item size="small">
-          <Icon glyph="redo" />
-        </Item>
-        <Item size="small">
-          <Icon glyph="undo" />
-        </Item>
-      </Group>
-    </Wrapper>
-    <Wrapper>
-      <ColorSelector
-        title={t('color')}
-        selectedColor={color}
-        onClick={(selected: string): void => { setDataState({ color: selected }); }}
-      />
-    </Wrapper>
-    <Wrapper>
-      <SliderWithTitle
-        title={t('opacity')}
-        value={opacity}
-        tips={`${opacity}%`}
-        onSlide={(val: number): void => { setDataState({ opacity: val }); }}
-      />
-    </Wrapper>
-    <Wrapper>
-      <SliderWithTitle
-        title={t('brushSize')}
-        value={width}
-        tips={`${width} pt`}
-        onSlide={(val: number): void => { setDataState({ width: val }); }}
-        maximum={40}
-      />
-    </Wrapper>
-    <Wrapper>
-      <Group style={{ marginTop: '20px', paddingRight: '40px' }}>
-        <Button
-          appearance="danger-hollow"
-          shouldFitContainer
-        >
-          <Icon glyph="clear" style={{ marginRight: '5px' }} />
-          {t('clear')}
-        </Button>
-      </Group>
-    </Wrapper>
-  </>
-);
-
-const translator = withTranslation('sidebar');
+}: OptionPropsType) => {
+  const { t } = useTranslation('sidebar');
 
-type TransProps = WithTranslation & OptionPropsType;
+  return (
+    <>
+      <Wrapper>
+        <Typography variant="subtitle" style={{ marginTop: '4px' }} align="left">
+          {t('style')}
+        </Typography>
+        <Group>
+          <Item
+            size="small"
+            selected={type === 'pen'}
+            onClick={(): void => { setDataState({ type: 'pen' }); }}
+          >
+            <Icon glyph="freehand" />
+          </Item>
+          <Item size="small">
+            <Icon glyph="eraser" />
+          </Item>
+          <Item size="small">
+            <Icon glyph="redo" />
+          </Item>
+          <Item size="small">
+            <Icon glyph="undo" />
+          </Item>
+        </Group>
+      </Wrapper>
+      <Wrapper>
+        <ColorSelector
+          title={t('color')}
+          selectedColor={color}
+          onClick={(selected: string): void => { setDataState({ color: selected }); }}
+        />
+      </Wrapper>
+      <Wrapper>
+        <SliderWithTitle
+          title={t('opacity')}
+          value={opacity}
+          tips={`${opacity}%`}
+          onSlide={(val: number): void => { setDataState({ opacity: val }); }}
+        />
+      </Wrapper>
+      <Wrapper>
+        <SliderWithTitle
+          title={t('brushSize')}
+          value={width}
+          tips={`${width} pt`}
+          onSlide={(val: number): void => { setDataState({ width: val }); }}
+          maximum={40}
+        />
+      </Wrapper>
+      <Wrapper>
+        <Group style={{ marginTop: '20px', paddingRight: '40px' }}>
+          <Button
+            appearance="danger-hollow"
+            shouldFitContainer
+          >
+            <Icon glyph="clear" style={{ marginRight: '5px' }} />
+            {t('clear')}
+          </Button>
+        </Group>
+      </Wrapper>
+    </>
+  );
+};
 
-export default translator<TransProps>(FreehandOption);
+export default FreehandOption;

+ 4 - 4
components/Navbar/data.ts

@@ -12,10 +12,10 @@ export default {
       key: 'nav-thumbnails',
       content: 'thumbnails',
     },
-    {
-      key: 'nav-print',
-      content: 'print',
-    },
+    // {
+    //   key: 'nav-print',
+    //   content: 'print',
+    // },
     {
       key: 'nav-export',
       content: 'export',

+ 0 - 1
components/Navbar/index.tsx

@@ -24,7 +24,6 @@ const Navbar: React.FC<Props> = ({
 }: Props) => (
   <Container isHidden={displayMode === 'full'}>
     <Typography variant="title">{fileName}</Typography>
-    {/* <Icon glyph="edit" /> */}
     <Separator />
     {
       data.btnGroup.map(ele => (

+ 4 - 15
components/Pagination/index.tsx

@@ -1,31 +1,24 @@
 import React, { useEffect, useState } from 'react';
-import { WithTranslation } from 'next-i18next';
-import { withTranslation } from '../../i18n';
+import { useTranslation } from '../../i18n';
 
 import {
   Container, Text, Input, ArrowButton,
 } from './styled';
 
-type I18nProps = {
-  t: (key: string) => string;
-};
-
-type OwnerProps = {
+type Props = {
   currentPage: number;
   totalPage: number;
   onChange: (page: number) => void;
 };
 
-type Props = OwnerProps & I18nProps;
-
 const Pagination: React.FC<Props> = ({
-  t,
   currentPage = 1,
   totalPage = 1,
   onChange = (): void => {
     // do nothing
   },
 }: Props) => {
+  const { t } = useTranslation('toolbar');
   const [inputValue, setInputValue] = useState(currentPage);
 
   const handleRightClick = (): void => {
@@ -82,8 +75,4 @@ const Pagination: React.FC<Props> = ({
   );
 };
 
-const translator = withTranslation('toolbar');
-
-type TransProps = WithTranslation & OwnerProps;
-
-export default translator<TransProps>(Pagination);
+export default Pagination;

+ 4 - 15
components/Search/index.tsx

@@ -1,6 +1,5 @@
 import React, { useRef, useEffect, MutableRefObject } from 'react';
-import { WithTranslation } from 'next-i18next';
-import { withTranslation } from '../../i18n';
+import { useTranslation } from '../../i18n';
 
 import Button from '../Button';
 import Portal from '../Portal';
@@ -9,11 +8,7 @@ import {
   Wrapper, InputWrapper, Input, ResultInfo, ArrowButton,
 } from './styled';
 
-type i18nProps = {
-  t: (key: string) => string;
-}
-
-type OwnerProps = {
+type Props = {
   matchesTotal: number;
   matchIndex: number;
   onEnter: (val: string) => void;
@@ -23,10 +18,7 @@ type OwnerProps = {
   close: () => void;
 };
 
-type Props = i18nProps & OwnerProps;
-
 const Search: React.FC<Props> = ({
-  t,
   matchesTotal = 0,
   matchIndex = 1,
   onPrev,
@@ -35,6 +27,7 @@ const Search: React.FC<Props> = ({
   isActive = false,
   close,
 }: Props) => {
+  const { t } = useTranslation('toolbar');
   const inputRef = useRef(null) as MutableRefObject<any>;
 
   const handleKeyDown = (e: React.KeyboardEvent): void => {
@@ -68,8 +61,4 @@ const Search: React.FC<Props> = ({
   );
 };
 
-const translator = withTranslation('toolbar');
-
-type TransProps = WithTranslation & OwnerProps;
-
-export default translator<TransProps>(Search);
+export default Search;

+ 57 - 65
components/ShapeOption/index.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
-import { WithTranslation } from 'next-i18next';
-import { withTranslation } from '../../i18n';
+import { useTranslation } from '../../i18n';
 
 import { OptionPropsType } from '../../constants/type';
 import Typography from '../Typography';
@@ -16,14 +15,7 @@ import {
   shapeOptions, typeOptions,
 } from './data';
 
-type i18nProps = {
-  t: (key: string) => string;
-}
-
-type Props = i18nProps & OptionPropsType;
-
-const ShapeOption: React.SFC<Props> = ({
-  t,
+const ShapeOption: React.SFC<OptionPropsType> = ({
   shape,
   color,
   opacity,
@@ -31,60 +23,60 @@ const ShapeOption: React.SFC<Props> = ({
   setDataState = (): void => {
     // do nothing
   },
-}: Props) => (
-  <>
-    <Wrapper>
-      <Typography
-        variant="subtitle"
-        style={{ marginTop: '8px' }}
-        align="left"
-      >
-        {t('style')}
-      </Typography>
-      <SelectBox
-        options={shapeOptions}
-        style={{ marginRight: '10px' }}
-        onChange={(item: Record<string, any>): void => { setDataState({ shape: item.key }); }}
-      />
-      {
-        shape !== 'line' && shape !== 'arrow' && (
-          <SelectBox
-            options={typeOptions}
-            onChange={(item: Record<string, any>): void => { setDataState({ type: item.key }); }}
-          />
-        )
-      }
-    </Wrapper>
-    <Wrapper>
-      <ColorSelector
-        title={t('color')}
-        mode="shape"
-        selectedColor={color}
-        onClick={(selected: string): void => { setDataState({ color: selected }); }}
-      />
-    </Wrapper>
-    <Wrapper>
-      <SliderWithTitle
-        title={t('opacity')}
-        value={opacity}
-        tips={`${opacity}%`}
-        onSlide={(val: number): void => { setDataState({ opacity: val }); }}
-      />
-    </Wrapper>
-    <Wrapper>
-      <SliderWithTitle
-        title={t('width')}
-        value={width}
-        tips={`${width}pt`}
-        onSlide={(val: number): void => { setDataState({ width: val }); }}
-        maximum={40}
-      />
-    </Wrapper>
-  </>
-);
-
-const translator = withTranslation('sidebar');
+}: OptionPropsType) => {
+  const { t } = useTranslation('sidebar');
 
-type TransProps = WithTranslation & OptionPropsType;
+  return (
+    <>
+      <Wrapper>
+        <Typography
+          variant="subtitle"
+          style={{ marginTop: '8px' }}
+          align="left"
+        >
+          {t('style')}
+        </Typography>
+        <SelectBox
+          options={shapeOptions}
+          style={{ marginRight: '10px' }}
+          onChange={(item: Record<string, any>): void => { setDataState({ shape: item.key }); }}
+        />
+        {
+          shape !== 'line' && shape !== 'arrow' && (
+            <SelectBox
+              options={typeOptions}
+              onChange={(item: Record<string, any>): void => { setDataState({ type: item.key }); }}
+            />
+          )
+        }
+      </Wrapper>
+      <Wrapper>
+        <ColorSelector
+          title={t('color')}
+          mode="shape"
+          selectedColor={color}
+          onClick={(selected: string): void => { setDataState({ color: selected }); }}
+        />
+      </Wrapper>
+      <Wrapper>
+        <SliderWithTitle
+          title={t('opacity')}
+          value={opacity}
+          tips={`${opacity}%`}
+          onSlide={(val: number): void => { setDataState({ opacity: val }); }}
+        />
+      </Wrapper>
+      <Wrapper>
+        <SliderWithTitle
+          title={t('width')}
+          value={width}
+          tips={`${width}pt`}
+          onSlide={(val: number): void => { setDataState({ width: val }); }}
+          maximum={40}
+        />
+      </Wrapper>
+    </>
+  );
+};
 
-export default translator<TransProps>(ShapeOption);
+export default ShapeOption;

+ 4 - 15
components/Toolbar/index.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
-import { WithTranslation } from 'next-i18next';
-import { withTranslation } from '../../i18n';
+import { useTranslation } from '../../i18n';
 
 import Icon from '../Icon';
 import Pagination from '../Pagination';
@@ -12,11 +11,7 @@ import { scaleCheck } from '../../helpers/utility';
 import { Container, ToggleButton } from './styled';
 import dataset from './data';
 
-type I18nProps = {
-  t: (key: string) => string;
-};
-
-type OwnerProps = {
+type Props = {
   totalPage: number;
   currentPage: number;
   setCurrentPage: (num: number) => void;
@@ -30,10 +25,7 @@ type OwnerProps = {
   handleHandClick: () => void;
 };
 
-type Props = I18nProps & OwnerProps;
-
 const Toolbar: React.FC<Props> = ({
-  t,
   totalPage,
   currentPage,
   setCurrentPage,
@@ -46,6 +38,7 @@ const Toolbar: React.FC<Props> = ({
   toggleDisplayMode,
   handleHandClick,
 }: Props) => {
+  const { t } = useTranslation('toolbar');
   const data = dataset(t);
 
   const handleClockwiseRotation = (): void => {
@@ -110,8 +103,4 @@ const Toolbar: React.FC<Props> = ({
   );
 };
 
-const translator = withTranslation('toolbar');
-
-type TransProps = WithTranslation & OwnerProps;
-
-export default translator<TransProps>(Toolbar);
+export default Toolbar;

+ 1 - 1
components/Typography/index.tsx

@@ -5,7 +5,7 @@ import { Title, Subtitle, Body } from './styled';
 type Props = {
   variant?: 'title' | 'subtitle' | 'body';
   light?: boolean;
-  children: string | number;
+  children: React.ReactNode;
   style?: {};
   align?: 'left' | 'center' | 'right';
 };

+ 1 - 0
components/Viewer/styled.ts

@@ -16,4 +16,5 @@ export const Wrapper = styled('div')<{width: number}>`
   display: inline-flex;
   flex-direction: column;
   width: ${props => props.width}px;
+  margin: 0 10px;
 `;

+ 1 - 0
components/Watermark/styled.ts

@@ -1,6 +1,7 @@
 import styled from 'styled-components';
 
 export const TextWrapper = styled.span`
+  white-space: pre;
 `;
 
 export const Img = styled.img``;

+ 89 - 97
components/WatermarkOption/index.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
-import { WithTranslation } from 'next-i18next';
-import { withTranslation } from '../../i18n';
+import { useTranslation } from '../../i18n';
 
 import { WatermarkType, SelectOptionType } from '../../constants/type';
 import Button from '../Button';
@@ -17,11 +16,7 @@ import {
   Container, Head, Body, Footer, IconWrapper,
 } from '../../global/sidebarStyled';
 
-type i18nProps = {
-  t: (key: string) => string;
-};
-
-type OwnerProps = WatermarkType & {
+type Props = WatermarkType & {
   onClick: () => void;
   isActive: boolean;
   onSave: () => void;
@@ -29,10 +24,7 @@ type OwnerProps = WatermarkType & {
   setDataState: (arg: Record<string, any>) => void;
 };
 
-type Props = i18nProps & OwnerProps;
-
-const WatermarkOption: React.FC<Props> = ({
-  t,
+const WatermarkOption: React.SFC<Props> = ({
   onClick,
   type,
   opacity = 0,
@@ -45,91 +37,91 @@ const WatermarkOption: React.FC<Props> = ({
   setDataState = (): void => {
     // do nothing
   },
-}: Props) => (
-  <Container>
-    <Head>
-      <IconWrapper>
-        <Icon glyph="left-back" onClick={onClick} />
-      </IconWrapper>
-      <Typography light>
-        {t('watermark')}
-      </Typography>
-    </Head>
-    <Body>
-      <Typography variant="subtitle" align="left">
-        {t('type')}
-      </Typography>
-      <Tabs
-        options={[
-          {
-            key: 'text',
-            content: t('text'),
-            child: (
-              <TextBox
-                t={t}
-                value={text}
-                onChange={(value: string): void => { setDataState({ text: value }); }}
-              />
-            ),
-          },
-        ]}
-        onChange={(option: SelectOptionType): void => { setDataState({ type: option.key }); }}
-      />
-      {
-        type === 'text' ? (
-          <>
-            <Divider orientation="horizontal" style={{ margin: '20px 0' }} />
-            <ColorSelect
-              title={t('color')}
-              mode="watermark"
-              selectedColor={textcolor}
-              onClick={(val: string): void => { setDataState({ textcolor: val }); }}
-            />
-          </>
-        ) : null
-      }
-      <Box mt="20">
-        <SliderWithTitle
-          title={t('opacity')}
-          value={opacity * 100}
-          tips={`${(opacity * 100).toFixed(0)}%`}
-          onSlide={(val: number): void => { setDataState({ opacity: val * 0.01 }); }}
-        />
-      </Box>
-      <Box mt="20">
-        <SliderWithTitle
-          title={t('rotate')}
-          value={rotation}
-          maximum={360}
-          tips={`${rotation}°`}
-          onSlide={(val: number): void => { setDataState({ rotation: val }); }}
-        />
-      </Box>
-      <Box mt="20">
-        <SliderWithTitle
-          title={t('scale')}
-          value={scale * 100}
-          tips={`${Math.round(scale * 100)}%`}
-          minimum={50}
-          maximum={250}
-          onSlide={(val: number): void => { setDataState({ scale: val / 100 }); }}
-        />
-      </Box>
-      <Divider orientation="horizontal" style={{ margin: '20px 0' }} />
-    </Body>
-    <Footer>
-      <Button appearance="primary" onClick={onSave}>
-        {t('save')}
-      </Button>
-      <Button appearance="danger-link" onClick={onDelete}>
-        {t('removeWatermark')}
-      </Button>
-    </Footer>
-  </Container>
-);
-
-const translator = withTranslation('sidebar');
+}: Props) => {
+  const { t } = useTranslation('sidebar');
 
-type TransProps = WithTranslation & OwnerProps;
+  return (
+    <Container>
+      <Head>
+        <IconWrapper>
+          <Icon glyph="left-back" onClick={onClick} />
+        </IconWrapper>
+        <Typography light>
+          {t('watermark')}
+        </Typography>
+      </Head>
+      <Body>
+        <Typography variant="subtitle" align="left">
+          {t('type')}
+        </Typography>
+        <Tabs
+          options={[
+            {
+              key: 'text',
+              content: t('text'),
+              child: (
+                <TextBox
+                  t={t}
+                  value={text}
+                  onChange={(value: string): void => { setDataState({ text: value }); }}
+                />
+              ),
+            },
+          ]}
+          onChange={(option: SelectOptionType): void => { setDataState({ type: option.key }); }}
+        />
+        {
+          type === 'text' ? (
+            <>
+              <Divider orientation="horizontal" style={{ margin: '20px 0' }} />
+              <ColorSelect
+                title={t('color')}
+                mode="watermark"
+                selectedColor={textcolor}
+                onClick={(val: string): void => { setDataState({ textcolor: val }); }}
+              />
+            </>
+          ) : null
+        }
+        <Box mt="20">
+          <SliderWithTitle
+            title={t('opacity')}
+            value={opacity * 100}
+            tips={`${(opacity * 100).toFixed(0)}%`}
+            onSlide={(val: number): void => { setDataState({ opacity: val * 0.01 }); }}
+          />
+        </Box>
+        <Box mt="20">
+          <SliderWithTitle
+            title={t('rotate')}
+            value={rotation}
+            maximum={360}
+            tips={`${rotation}°`}
+            onSlide={(val: number): void => { setDataState({ rotation: val }); }}
+          />
+        </Box>
+        <Box mt="20">
+          <SliderWithTitle
+            title={t('scale')}
+            value={scale * 100}
+            tips={`${Math.round(scale * 100)}%`}
+            minimum={50}
+            maximum={250}
+            onSlide={(val: number): void => { setDataState({ scale: val / 100 }); }}
+          />
+        </Box>
+        <Divider orientation="horizontal" style={{ margin: '20px 0' }} />
+      </Body>
+      <Footer>
+        <Button appearance="primary" onClick={onSave}>
+          {t('save')}
+        </Button>
+        <Button appearance="danger-link" onClick={onDelete}>
+          {t('removeWatermark')}
+        </Button>
+      </Footer>
+    </Container>
+  );
+};
 
-export default translator<TransProps>(WatermarkOption);
+export default WatermarkOption;

+ 4 - 1
containers/AnnotationListHead.tsx

@@ -5,7 +5,9 @@ import useStore from '../store';
 import useActions from '../actions';
 
 const AnnotationListHead: React.FC = () => {
-  const [, dispatch] = useStore();
+  const [{
+    annotations,
+  }, dispatch] = useStore();
   const {
     setNavbar,
     addAnnots,
@@ -15,6 +17,7 @@ const AnnotationListHead: React.FC = () => {
     <Head
       close={(): void => { setNavbar(''); }}
       addAnnots={addAnnots}
+      hasAnnots={!!annotations.length}
     />
   );
 };

+ 4 - 2
containers/AutoSave.tsx

@@ -1,16 +1,18 @@
 import React, { useEffect } from 'react';
 import { useSnackbar } from 'notistack';
+import { useTranslation } from '../i18n';
 
 import useAutoSave from '../hooks/useAutoSave';
 import Icon from '../components/Icon';
 
 const index: React.FC = () => {
+  const { t } = useTranslation('toast');
   const [isSaved, isSaving] = useAutoSave();
   const { enqueueSnackbar, closeSnackbar } = useSnackbar();
 
   useEffect(() => {
     if (isSaving) {
-      enqueueSnackbar('File saving', {
+      enqueueSnackbar(t('saving'), {
         variant: 'info',
         action: key => (
           <Icon glyph="close" onClick={(): void => { closeSnackbar(key); }} />
@@ -18,7 +20,7 @@ const index: React.FC = () => {
       });
     }
     if (!isSaving && isSaved) {
-      enqueueSnackbar('File is saved', {
+      enqueueSnackbar(t('saved'), {
         variant: 'success',
         action: key => (
           <Icon glyph="close" onClick={(): void => { closeSnackbar(key); }} />

+ 4 - 9
containers/CreateForm.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { withTranslation } from '../i18n';
+import { useTranslation } from '../i18n';
 
 import Button from '../components/Button';
 import Icon from '../components/Icon';
@@ -9,13 +9,8 @@ import useActions from '../actions';
 import useStore from '../store';
 import { BtnWrapper } from '../global/toolStyled';
 
-type Props = {
-  t: (key: string) => string;
-}
-
-const CreateForm: React.FC<Props> = ({
-  t,
-}: Props) => {
+const CreateForm: React.FC = () => {
+  const { t } = useTranslation('sidebar');
   const [{ sidebarState }, dispatch] = useStore();
   const { setSidebar } = useActions(dispatch);
 
@@ -59,4 +54,4 @@ const CreateForm: React.FC<Props> = ({
   );
 };
 
-export default withTranslation('sidebar')(CreateForm);
+export default CreateForm;

+ 3 - 2
containers/FreehandTools.tsx

@@ -87,7 +87,7 @@ const FreehandTools: React.FC<Props> = ({
       && cursorPosition.x && cursorPosition.y
     ) {
       const type = annotations[index].obj_type;
-      const { position } = annotations[index].obj_attr;
+      const position = annotations[index].obj_attr.position as PointType[][];
       const coordinates = parsePositionForBackend(
         type, { x: cursorPosition.x, y: cursorPosition.y }, viewport.height, scale,
       ) as PointType;
@@ -98,7 +98,8 @@ const FreehandTools: React.FC<Props> = ({
         coordinates.x !== lastPosition.x
         && coordinates.y !== lastPosition.y
       ) {
-        annotations[index].obj_attr.position[0].push(coordinates);
+        position[0].push(coordinates);
+        annotations[index].obj_attr.position = position;
         updateAnnots([...annotations]);
       }
     }

+ 4 - 9
containers/MarkupTools.tsx

@@ -1,5 +1,5 @@
 import React, { useEffect } from 'react';
-import { withTranslation } from '../i18n';
+import { useTranslation } from '../i18n';
 
 import Button from '../components/Button';
 import ExpansionPanel from '../components/ExpansionPanel';
@@ -13,13 +13,8 @@ import ShapeTools from './ShapeTools';
 import useActions from '../actions';
 import useStore from '../store';
 
-type Props = {
-  t: (key: string) => string;
-}
-
-const MarkupTools: React.FC<Props> = ({
-  t,
-}: Props) => {
+const MarkupTools: React.FC = () => {
+  const { t } = useTranslation('sidebar');
   const [{ sidebarState, markupToolState }, dispatch] = useStore();
   const { setSidebar, setMarkupTool } = useActions(dispatch);
 
@@ -88,4 +83,4 @@ const MarkupTools: React.FC<Props> = ({
   );
 };
 
-export default withTranslation('sidebar')(MarkupTools);
+export default MarkupTools;

+ 11 - 3
containers/Navbar.tsx

@@ -15,6 +15,8 @@ const Navbar: React.FC = () => {
     navbarState,
     displayMode,
     info,
+    annotations,
+    totalPage,
   }, dispatch] = useStore();
   const { setNavbar } = useActions(dispatch);
 
@@ -22,9 +24,15 @@ const Navbar: React.FC = () => {
     switch (state) {
       case 'export': {
         const parsed = queryString.parse(window.location.search);
-        const fileName = atob(parsed.token as string);
-        const path = `/api/v1/output.pdf?f=${parsed.token}`;
-        downloadFileWithUri(fileName, path);
+        const fileName = atob(info.token as string);
+        const outputPath = `/api/v1/output.pdf?f=${info.token}&user_id=${parsed.watermark}&pages=0-${totalPage}`;
+        const originalPath = `/api/v1/original.pdf?transaction_id=${info.id}`;
+
+        if (annotations.length) {
+          downloadFileWithUri(fileName, outputPath);
+        } else {
+          downloadFileWithUri(fileName, originalPath);
+        }
         break;
       }
       case navbarState:

+ 7 - 2
containers/PdfViewer.tsx

@@ -56,8 +56,13 @@ const PdfViewer: React.FC = () => {
       .then((xfdf) => {
         const annotations = parseAnnotationFromXml(xfdf);
         const watermark = parseWatermarkFromXml(xfdf);
-        addAnnots([...annotations, watermark]);
-        updateWatermark(watermark.obj_attr);
+
+        if (watermark.obj_attr.type) {
+          addAnnots([...annotations, watermark]);
+          updateWatermark(watermark.obj_attr);
+        } else {
+          addAnnots(annotations);
+        }
       })
       .catch((error) => {
         console.log(error);

+ 4 - 9
containers/Sidebar.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { withTranslation } from '../i18n';
+import { useTranslation } from '../i18n';
 
 // import Button from '../components/Button';
 import Typography from '../components/Typography';
@@ -14,13 +14,8 @@ import useStore from '../store';
 // import { BtnWrapper } from '../global/toolStyled';
 import { SidebarWrapper } from '../global/otherStyled';
 
-type Props = {
-  t: (key: string) => string;
-}
-
-const Sidebar: React.FC<Props> = ({
-  t,
-}: Props) => {
+const Sidebar: React.FC = () => {
+  const { t } = useTranslation('sidebar');
   const [{ displayMode }] = useStore();
   // const { setSidebar } = useActions(dispatch);
 
@@ -50,4 +45,4 @@ const Sidebar: React.FC<Props> = ({
   );
 };
 
-export default withTranslation('sidebar')(Sidebar);
+export default Sidebar;

+ 14 - 16
containers/WatermarkTool.tsx

@@ -1,25 +1,20 @@
 import React, { useState, useEffect } from 'react';
 import queryString from 'query-string';
-import { withTranslation } from '../i18n';
+import dayjs from 'dayjs';
+import { useTranslation } from '../i18n';
 
 import { WatermarkType, AnnotationType } from '../constants/type';
 import Icon from '../components/Icon';
 import Button from '../components/Button';
 import Drawer from '../components/Drawer';
 import WatermarkOption from '../components/WatermarkOption';
-
 import useActions from '../actions';
 import useStore from '../store';
 
 import { BtnWrapper } from '../global/toolStyled';
 
-type Props = {
-  t: (key: string) => string;
-};
-
-const WatermarkTool: React.FC<Props> = ({
-  t,
-}: Props) => {
+const WatermarkTool: React.FC = () => {
+  const { t } = useTranslation('sidebar');
   const [isActive, setActive] = useState(false);
   const [{
     totalPage,
@@ -51,7 +46,10 @@ const WatermarkTool: React.FC<Props> = ({
   };
 
   const handleSave = (): void => {
-    const { type, imagepath, ...rest } = watermark;
+    const { type, imagepath = '', ...rest } = watermark;
+    const imagePathMatch = imagepath.match(/image\/(.*);base64/) || [];
+    const imageTypeMatch = imagepath.match(/base64,(.*)/) || [];
+
     const watermarkData = {
       obj_type: 'watermark',
       obj_attr: {
@@ -59,10 +57,11 @@ const WatermarkTool: React.FC<Props> = ({
         type,
         pages: `0-${totalPage}`,
         opacity: watermark.opacity,
-        original_image_name: imagepath ? `temp.${imagepath.match(/image\/(.*);base64/)[1]}` : '',
-        image_data: type === 'image' ? imagepath.match(/base64,(.*)/)[1] : '',
+        original_image_name: imagePathMatch && `temp.${imagePathMatch[1]}`,
+        image_data: imageTypeMatch && imageTypeMatch[1],
       },
     };
+
     removeWatermarkInAnnots();
     addAnnots([watermarkData], true);
   };
@@ -80,8 +79,9 @@ const WatermarkTool: React.FC<Props> = ({
     const parsed = queryString.parse(window.location.search);
 
     if (parsed.watermark) {
+      const datetime = dayjs().format('YYYY-MM-DD HH:mm:ss');
       setDataState({
-        text: parsed.watermark as string,
+        text: `${parsed.watermark} @ ${datetime}`,
       });
     }
   }, []);
@@ -108,6 +108,4 @@ const WatermarkTool: React.FC<Props> = ({
   );
 };
 
-const translator = withTranslation('sidebar');
-
-export default translator(WatermarkTool);
+export default WatermarkTool;

+ 3 - 3
helpers/annotation.ts

@@ -8,7 +8,7 @@ import {
 import { getPdfPage, renderTextLayer } from './pdf';
 import { getPosition, parsePositionForBackend } from './position';
 import { normalizeRound, floatToHex } from './utility';
-import { xmlParser, getElementsByTagname } from './dom';
+import { xmlParser, getElementsByTagName } from './dom';
 
 type GetFontAttributeFunc = (type: string, element: Record<string, any>) => Record<string, any>;
 
@@ -44,8 +44,8 @@ export const parseAnnotationFromXml = (xmlString: string): AnnotationType[] => {
 
   const xmlDoc = xmlParser(xmlString);
   const elements = xmlDoc.firstElementChild || xmlDoc.firstChild;
-  const element = getElementsByTagname(elements, 'annots') || [];
-  const annotations: any[] = Array.prototype.slice.call(element);
+  const element = getElementsByTagName(elements as ChildNode, 'annots') || [];
+  const annotations: Element[] = Array.prototype.slice.call(element);
   const filterAnnots = annotations.reduce((acc: any[], cur: any) => {
     const type = ANNOTATION_TYPE[cur.tagName];
 

+ 4 - 4
helpers/dom.ts

@@ -2,16 +2,16 @@ export const canUseDOM = (): boolean => (
   !!(typeof window !== 'undefined' && window.document)
 );
 
-export const xmlParser = (xmlString: string): any => {
+export const xmlParser = (xmlString: string): Document => {
   const parser = new window.DOMParser();
   const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
   return xmlDoc;
 };
 
-export const getElementsByTagname = (elements: any, tagname: string): NodeList => {
-  const array = [...elements.childNodes];
+export const getElementsByTagName = (elements: ChildNode, tagname: string): NodeList => {
+  const arr = Array.prototype.slice.call(elements.childNodes);
   let element: any = null;
-  array.forEach((ele) => {
+  arr.forEach((ele) => {
     if (ele.tagName === tagname) {
       element = ele.childNodes;
     }

+ 5 - 5
helpers/position.ts

@@ -13,13 +13,13 @@ export const rectCalc = (
   height: (position.top - position.bottom) * scale,
 });
 
-export const getPosition = (type: string, element: Record<string, any>): any => {
+export const getPosition = (type: string, element: Record<string, any>): AnnotationPositionType => {
   switch (type) {
     case 'Ink': {
-      const gestures: any[] = [];
-      let nodes: any = element.childNodes[1].childNodes;
-      nodes = Array.prototype.slice.call(nodes);
-      nodes.forEach((ele: HTMLElement) => {
+      const gestures: PointType[][] = [];
+      const nodes: HTMLCollection = element.childNodes[1].childNodes;
+      const nodeList = Array.prototype.slice.call(nodes);
+      nodeList.forEach((ele: HTMLElement) => {
         if (ele.tagName === 'gesture') {
           const points: PointType[] = [];
           const gestureArray = (ele.innerHTML || ele.textContent || '').split(';');

+ 1 - 1
helpers/time.ts

@@ -1,4 +1,4 @@
-export const delay = (ms: number): Promise<any> => (
+export const delay = (ms: number): Promise<number> => (
   new Promise((resolve) => {
     setTimeout(() => { resolve(); }, ms);
   })

+ 1 - 0
helpers/utility.ts

@@ -102,6 +102,7 @@ export const downloadFileWithUri = (name: string, uri: string): void => {
   const ele = document.createElement('a');
   ele.download = name;
   ele.href = uri;
+  ele.target = '_blank';
   document.body.appendChild(ele);
   ele.click();
   document.body.removeChild(ele);

+ 12 - 11
helpers/watermark.ts

@@ -2,7 +2,7 @@ import {
   WatermarkType,
 } from '../constants/type';
 import { floatToHex } from './utility';
-import { xmlParser, getElementsByTagname } from './dom';
+import { xmlParser, getElementsByTagName } from './dom';
 
 type WatermarkAnnotType = {
   obj_type: string;
@@ -15,36 +15,37 @@ export const parseWatermarkFromXml = (xmlString: string): WatermarkAnnotType =>
   const xmlDoc = xmlParser(xmlString);
   const elements = xmlDoc.firstElementChild || xmlDoc.firstChild;
   const data: WatermarkType = {};
-  const element = getElementsByTagname(elements, 'watermarks');
+  const element = getElementsByTagName(elements as ChildNode, 'watermarks');
   if (element[1]) {
-    const array: any[] = Array.prototype.slice.call(element[1].childNodes);
+    const array: Element[] = Array.prototype.slice.call(element[1].childNodes);
     array.forEach((ele, index) => {
+      const attr: any = ele.attributes;
       switch (ele.tagName) {
         case 'Font':
           data.type = 'text';
-          data.text = array[index + 1].textContent.trim();
+          data.text = (array[index + 1].textContent as string).trim();
           break;
         case 'Opacity': {
-          data.opacity = ele.attributes.value.value;
+          data.opacity = attr.value.value;
           break;
         }
         case 'Rotation': {
-          data.rotation = parseFloat(ele.attributes.value.value);
+          data.rotation = parseFloat(attr.value.value);
           break;
         }
         case 'Scale': {
-          data.scale = parseFloat(ele.attributes.value.value);
+          data.scale = parseFloat(attr.value.value);
           break;
         }
         case 'Color': {
-          const r = ele.attributes.r.value;
-          const g = ele.attributes.g.value;
-          const b = ele.attributes.b.value;
+          const r = attr.r.value;
+          const g = attr.g.value;
+          const b = attr.b.value;
           data.textcolor = floatToHex(r, g, b);
           break;
         }
         case 'PageRange': {
-          data.pages = array[index + 1].textContent.trim();
+          data.pages = (array[index + 1].textContent as string).trim();
           break;
         }
         default:

+ 8 - 10
hooks/useAutoSave.ts

@@ -1,7 +1,5 @@
-import {
-  useState, useRef, useEffect,
-} from 'react';
-import { interval } from 'rxjs';
+import { useState, useEffect } from 'react';
+import { Subscription, interval } from 'rxjs';
 import { take } from 'rxjs/operators';
 
 import useStore from '../store';
@@ -11,11 +9,11 @@ const useAutoSave = (): [boolean, boolean] => {
   const [{ info, annotations, isInit }] = useStore();
   const [isSaved, setSaved] = useState(false);
   const [isSaving, setSaving] = useState(false);
-  const subscription = useRef<any>(null);
+  let subscription: Subscription | null = null;
 
   useEffect(() => {
     if (info.id && isInit) {
-      if (subscription.current) subscription.current.unsubscribe();
+      if (subscription) subscription.unsubscribe();
 
       setSaved(false);
       const observable = interval(1000);
@@ -25,10 +23,10 @@ const useAutoSave = (): [boolean, boolean] => {
         append_objects: annotations,
       };
 
-      subscription.current = observable.pipe(
-        take(6),
+      subscription = observable.pipe(
+        take(3),
       ).subscribe((x: number) => {
-        if (x === 5) {
+        if (x === 2) {
           setSaving(true);
           saveFile(info.token, data).then(() => {
             setSaving(false);
@@ -39,7 +37,7 @@ const useAutoSave = (): [boolean, boolean] => {
     }
 
     return (): void => {
-      if (subscription.current) subscription.current.unsubscribe();
+      if (subscription) subscription.unsubscribe();
     };
   }, [info, annotations, isInit]);
 

+ 3 - 3
hooks/useCursorPosition.ts

@@ -1,7 +1,7 @@
 import {
   useState, useRef, useCallback, useEffect,
 } from 'react';
-import { fromEvent } from 'rxjs';
+import { fromEvent, Subscription } from 'rxjs';
 import {
   throttleTime,
 } from 'rxjs/operators';
@@ -85,8 +85,8 @@ const useCursorPosition: UseCursorPositionType = (time = defaultTime) => {
   }, [element]);
 
   useEffect(() => {
-    let mouseSubscription: any = null;
-    let touchSubscription: any = null;
+    let mouseSubscription: Subscription | null = null;
+    let touchSubscription: Subscription | null = null;
 
     const onEnter = (): void => {
       entered.current = true;

+ 1 - 0
package.json

@@ -18,6 +18,7 @@
     "@material-ui/core": "^4.6.1",
     "babel-plugin-inline-react-svg": "^1.1.0",
     "core-js": "3.6.4",
+    "dayjs": "^1.8.23",
     "express": "^4.17.1",
     "immer": "^5.0.0",
     "isomorphic-unfetch": "^3.0.0",

+ 4 - 0
public/locales/en/toast.json

@@ -0,0 +1,4 @@
+{
+  "saving": "Saving...",
+  "saved": "Saved"
+}

+ 4 - 0
public/locales/zh-tw/toast.json

@@ -0,0 +1,4 @@
+{
+  "saving": "儲存中...",
+  "saved": "儲存成功"
+}

+ 1 - 1
store/index.tsx

@@ -37,4 +37,4 @@ export const StoreProvider = ({
   );
 };
 
-export default (): any => useContext(StateContext);
+export default (): IContextProps => useContext(StateContext);

+ 4 - 5
store/initialPdfState.ts

@@ -4,7 +4,6 @@ import {
   AnnotationType,
   WatermarkType,
 } from '../constants/type';
-import { color as theme } from '../constants/style';
 
 export type StateType = {
   totalPage: number;
@@ -34,11 +33,11 @@ export default {
   isInit: false,
   watermark: {
     type: 'text',
-    textcolor: theme.gray,
-    opacity: 0.5,
+    textcolor: '#c0c0c0',
+    opacity: 0.3,
     text: '',
-    scale: 1,
-    rotation: 0,
+    scale: 2,
+    rotation: 45,
     vertalign: 'center',
     horizalign: 'center',
     xoffset: 0,

+ 5 - 0
yarn.lock

@@ -3356,6 +3356,11 @@ data-urls@^1.0.0:
     whatwg-mimetype "^2.2.0"
     whatwg-url "^7.0.0"
 
+dayjs@^1.8.23:
+  version "1.8.23"
+  resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.23.tgz#07b5a8e759c4d75ae07bdd0ad6977f851c01e510"
+  integrity sha512-NmYHMFONftoZbeOhVz6jfiXI4zSiPN6NoVWJgC0aZQfYVwzy/ZpESPHuCcI0B8BUMpSJQ08zenHDbofOLKq8hQ==
+
 debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"