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

Merge branch 'support/apply-nextjs-2' into feat/use-apiv3-for-post-login

Yohei-Shiina 3 лет назад
Родитель
Сommit
49d258d754
40 измененных файлов с 502 добавлено и 259 удалено
  1. 1 1
      packages/app/package.json
  2. 3 0
      packages/app/public/static/locales/en_US/admin.json
  3. 3 0
      packages/app/public/static/locales/ja_JP/admin.json
  4. 3 0
      packages/app/public/static/locales/zh_CN/admin.json
  5. 4 3
      packages/app/src/client/util/smooth-scroll.ts
  6. 2 2
      packages/app/src/components/Admin/App/AppSetting.jsx
  7. 3 4
      packages/app/src/components/BookmarkButtons.module.scss
  8. 2 2
      packages/app/src/components/BookmarkButtons.tsx
  9. 1 1
      packages/app/src/components/ContentLinkButtons.module.scss
  10. 47 44
      packages/app/src/components/ContentLinkButtons.tsx
  11. 39 34
      packages/app/src/components/Fab.tsx
  12. 1 5
      packages/app/src/components/Icons/CreatePageIcon.tsx
  13. 1 6
      packages/app/src/components/Icons/ReturnTopIcon.tsx
  14. 3 3
      packages/app/src/components/Layout/BasicLayout.tsx
  15. 50 0
      packages/app/src/components/Layout/ShareLinkLayout.tsx
  16. 4 4
      packages/app/src/components/LikeButtons.module.scss
  17. 2 2
      packages/app/src/components/LikeButtons.tsx
  18. 0 1
      packages/app/src/components/Me/BasicInfoSettings.tsx
  19. 1 0
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  20. 4 5
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  21. 1 1
      packages/app/src/components/Navbar/GrowiSubNavigation.tsx
  22. 3 2
      packages/app/src/components/Page.tsx
  23. 7 11
      packages/app/src/components/Page/DisplaySwitcher.tsx
  24. 0 52
      packages/app/src/components/Page/ShareLinkAlert.jsx
  25. 52 0
      packages/app/src/components/Page/ShareLinkAlert.tsx
  26. 5 9
      packages/app/src/components/ShareLink/ShareLinkForm.tsx
  27. 22 0
      packages/app/src/components/User/SeenUserInfo.module.scss
  28. 5 1
      packages/app/src/components/User/SeenUserInfo.tsx
  29. 4 9
      packages/app/src/components/User/UserInfo.tsx
  30. 11 0
      packages/app/src/interfaces/share-link.ts
  31. 2 1
      packages/app/src/pages/admin/[[...path]].page.tsx
  32. 2 1
      packages/app/src/pages/me/[[...path]].page.tsx
  33. 169 0
      packages/app/src/pages/share/[[...path]].page.tsx
  34. 5 2
      packages/app/src/pages/tags.page.tsx
  35. 6 4
      packages/app/src/pages/utils/commons.ts
  36. 1 1
      packages/app/src/server/routes/index.js
  37. 0 17
      packages/app/src/styles/atoms/_buttons.scss
  38. 1 1
      packages/app/src/styles/theme/_apply-colors.scss
  39. 1 0
      packages/core/src/interfaces/user.ts
  40. 31 30
      yarn.lock

+ 1 - 1
packages/app/package.json

@@ -130,7 +130,7 @@
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "next": "^12.2.5",
-    "next-i18next": "^11.0.0",
+    "next-i18next": "^11.3.0",
     "next-superjson": "^0.0.4",
     "next-themes": "^0.2.0",
     "nocache": "^3.0.1",

+ 3 - 0
packages/app/public/static/locales/en_US/admin.json

@@ -1,4 +1,7 @@
 {
+  "meta": {
+    "display_name": "English"
+  },
   "wiki_management_home_page": "Wiki Management Home Page",
   "app_settings": "App Settings",
   "security_settings": {

+ 3 - 0
packages/app/public/static/locales/ja_JP/admin.json

@@ -1,4 +1,7 @@
 {
+  "meta": {
+    "display_name": "日本語"
+  },
   "Update": "更新",
   "Delete": "削除",
   "User": "ユーザー",

+ 3 - 0
packages/app/public/static/locales/zh_CN/admin.json

@@ -1,4 +1,7 @@
 {
+  "meta": {
+    "display_name": "简体中文"
+  },
   "Update": "更新",
   "Delete": "删除",
   "User": "用户",

+ 4 - 3
packages/app/src/client/util/smooth-scroll.ts

@@ -1,10 +1,11 @@
 const WIKI_HEADER_LINK = 120;
 
-export const smoothScrollIntoView = (element: HTMLElement, offsetTop = 0, scrollElement: HTMLElement | Window = window): void => {
-  const targetElement = element || window.document.body;
+export const smoothScrollIntoView = (
+    element: HTMLElement = window.document.body, offsetTop = 0, scrollElement: HTMLElement | Window = window,
+): void => {
 
   // get the distance to the target element top
-  const rectTop = targetElement.getBoundingClientRect().top;
+  const rectTop = element.getBoundingClientRect().top;
 
   const top = window.pageYOffset + rectTop - offsetTop;
 

+ 2 - 2
packages/app/src/components/Admin/App/AppSetting.jsx

@@ -79,8 +79,8 @@ const AppSetting = (props) => {
         <div className="col-md-6 py-2">
           {
             i18nConfig.locales.map((locale) => {
-              const fixedT = i18n.getFixedT(locale);
-              i18n.loadLanguages(i18nConfig.locales);
+              if (i18n == null) { return }
+              const fixedT = i18n.getFixedT(locale, 'admin');
 
               return (
                 <div key={locale} className="custom-control custom-radio custom-control-inline">

+ 3 - 4
packages/app/src/components/BookmarkButtons.module.scss

@@ -1,12 +1,11 @@
 @use '~/styles/bootstrap/init' as bs;
 
-.btn-bookmark {
-  :global {
+.btn-group-bookmark :global {
+  .btn-bookmark {
     box-shadow: none !important;
-  }
 
-  &:global {
     @include bs.button-outline-variant(rgba(bs.$secondary, 50%), bs.$orange, rgba(lighten(bs.$orange, 20%), 0.5), rgba(lighten(bs.$orange, 20%), 0.5));
+
     &:not(:disabled):not(.disabled):active,
     &:not(:disabled):not(.disabled).active {
       color: bs.$orange;

+ 2 - 2
packages/app/src/components/BookmarkButtons.tsx

@@ -52,12 +52,12 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
   }, [isGuestUser, isBookmarked]);
 
   return (
-    <div className="btn-group" role="group" aria-label="Bookmark buttons">
+    <div className={`btn-group btn-group-bookmark ${styles['btn-group-bookmark']}`} role="group" aria-label="Bookmark buttons">
       <button
         type="button"
         id="bookmark-button"
         onClick={handleClick}
-        className={`shadow-none btn btn-bookmark ${styles['btn-bookmark']} border-0
+        className={`shadow-none btn btn-bookmark border-0
           ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
         <i className={`fa ${isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>

+ 1 - 1
packages/app/src/styles/_toc.scss → packages/app/src/components/ContentLinkButtons.module.scss

@@ -1,4 +1,4 @@
-.grw-icon-container-recently-created {
+.grw-icon-container-recently-created :global {
   svg {
     width: 14px;
     height: 14px;

+ 47 - 44
packages/app/src/components/ContentLinkButtons.tsx

@@ -1,57 +1,62 @@
-import React, { useCallback, useMemo } from 'react';
+import React, { useCallback } from 'react';
 
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
 import { usePageUser } from '~/stores/context';
 
+import styles from './ContentLinkButtons.module.scss';
 
 const WIKI_HEADER_LINK = 120;
 
+const BookMarkLinkButton = React.memo(() => {
 
-const ContentLinkButtons = (): JSX.Element => {
+  const BookMarkLinkButtonClickHandler = useCallback(() => {
+    const getBookMarkListHeaderDom = document.getElementById('bookmarks-list');
+    if (getBookMarkListHeaderDom == null) { return }
+    smoothScrollIntoView(getBookMarkListHeaderDom, WIKI_HEADER_LINK);
+  }, []);
+
+  return (
+    <button
+      type="button"
+      className="btn btn-outline-secondary btn-sm px-2"
+      onClick={BookMarkLinkButtonClickHandler}
+    >
+      <i className="fa fa-fw fa-bookmark-o"></i>
+      <span>Bookmarks</span>
+    </button>
+  );
+});
+
+BookMarkLinkButton.displayName = 'BookMarkLinkButton';
+
+const RecentlyCreatedLinkButton = React.memo(() => {
+
+  const RecentlyCreatedListButtonClickHandler = useCallback(() => {
+    const getRecentlyCreatedListHeaderDom = document.getElementById('recently-created-list');
+    if (getRecentlyCreatedListHeaderDom == null) { return }
+    smoothScrollIntoView(getRecentlyCreatedListHeaderDom, WIKI_HEADER_LINK);
+  }, []);
+
+  return (
+    <button
+      type="button"
+      className="btn btn-outline-secondary btn-sm px-3"
+      onClick={RecentlyCreatedListButtonClickHandler}
+    >
+      <i className={`${styles['grw-icon-container-recently-created']} grw-icon-container-recently-created mr-2`}><RecentlyCreatedIcon /></i>
+      <span>Recently Created</span>
+    </button>
+  );
+});
+
+RecentlyCreatedLinkButton.displayName = 'RecentlyCreatedLinkButton';
+
+export const ContentLinkButtons = (): JSX.Element => {
 
   const { data: pageUser } = usePageUser();
 
-  // get element for smoothScroll
-  const getBookMarkListHeaderDom = useMemo(() => { return document.getElementById('bookmarks-list') }, []);
-  const getRecentlyCreatedListHeaderDom = useMemo(() => { return document.getElementById('recently-created-list') }, []);
-
-
-  const BookMarkLinkButton = useCallback((): JSX.Element => {
-    if (getBookMarkListHeaderDom == null) {
-      return <></>;
-    }
-
-    return (
-      <button
-        type="button"
-        className="btn btn-outline-secondary btn-sm px-2"
-        onClick={() => smoothScrollIntoView(getBookMarkListHeaderDom, WIKI_HEADER_LINK)}
-      >
-        <i className="fa fa-fw fa-bookmark-o"></i>
-        <span>Bookmarks</span>
-      </button>
-    );
-  }, [getBookMarkListHeaderDom]);
-
-  const RecentlyCreatedLinkButton = useCallback(() => {
-    if (getRecentlyCreatedListHeaderDom == null) {
-      return <></>;
-    }
-
-    return (
-      <button
-        type="button"
-        className="btn btn-outline-secondary btn-sm px-3"
-        onClick={() => smoothScrollIntoView(getRecentlyCreatedListHeaderDom, WIKI_HEADER_LINK)}
-      >
-        <i className="grw-icon-container-recently-created mr-2"><RecentlyCreatedIcon /></i>
-        <span>Recently Created</span>
-      </button>
-    );
-  }, [getRecentlyCreatedListHeaderDom]);
-
-  if (pageUser == null) {
+  if (pageUser == null || pageUser.status === 4) {
     return <></>;
   }
 
@@ -63,5 +68,3 @@ const ContentLinkButtons = (): JSX.Element => {
   );
 
 };
-
-export default ContentLinkButtons;

+ 39 - 34
packages/app/src/components/Fab.jsx → packages/app/src/components/Fab.tsx

@@ -10,18 +10,18 @@ import { useCurrentPagePath, useCurrentUser } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import loggerFactory from '~/utils/logger';
 
-import CreatePageIcon from './Icons/CreatePageIcon';
-import ReturnTopIcon from './Icons/ReturnTopIcon';
+import { CreatePageIcon } from './Icons/CreatePageIcon';
+import { ReturnTopIcon } from './Icons/ReturnTopIcon';
 
 import styles from './Fab.module.scss';
 
-const logger = loggerFactory('growi:cli:Fab');
+// const logger = loggerFactory('growi:cli:Fab');
 
-const Fab = () => {
-  const { data: currentUser } = useCurrentUser();
+export const Fab = (): JSX.Element => {
 
-  const { open: openCreateModal } = usePageCreateModal();
+  const { data: currentUser } = useCurrentUser();
   const { data: currentPath = '' } = useCurrentPagePath();
+  const { open: openCreateModal } = usePageCreateModal();
 
   const [animateClasses, setAnimateClasses] = useState('invisible');
   const [buttonClasses, setButtonClasses] = useState('');
@@ -30,32 +30,39 @@ const Fab = () => {
   const createBtnRef = useRef(null);
   useRipple(createBtnRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
 
-  const stickyChangeHandler = useCallback((event) => {
-    logger.debug('StickyEvents.CHANGE detected');
-
-    const newAnimateClasses = event.detail.isSticky ? 'animated fadeInUp faster' : 'animated fadeOut faster';
-    const newButtonClasses = event.detail.isSticky ? '' : 'disabled grw-pointer-events-none';
-
-    setAnimateClasses(newAnimateClasses);
-    setButtonClasses(newButtonClasses);
-  }, []);
-
-  // setup effect by sticky event
-  useEffect(() => {
-    // sticky
-    // See: https://github.com/ryanwalters/sticky-events
-    const stickyEvents = new StickyEvents({ stickySelector: '#grw-fav-sticky-trigger' });
-    const { stickySelector } = stickyEvents;
-    const elem = document.querySelector(stickySelector);
-    elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-
-    // return clean up handler
-    return () => {
-      elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-    };
-  }, [stickyChangeHandler]);
+  /*
+  * Comment out to prevent err >>> TypeError: Cannot read properties of null (reading 'bottom')
+  */
+  // const stickyChangeHandler = useCallback((event) => {
+  //   logger.debug('StickyEvents.CHANGE detected');
+
+  //   const newAnimateClasses = event.detail.isSticky ? 'animated fadeInUp faster' : 'animated fadeOut faster';
+  //   const newButtonClasses = event.detail.isSticky ? '' : 'disabled grw-pointer-events-none';
+
+  //   setAnimateClasses(newAnimateClasses);
+  //   setButtonClasses(newButtonClasses);
+  // }, []);
+
+  // // setup effect by sticky event
+  // useEffect(() => {
+  //   // sticky
+  //   // See: https://github.com/ryanwalters/sticky-events
+  //   const stickyEvents = new StickyEvents({ stickySelector: '#grw-fav-sticky-trigger' });
+  //   const { stickySelector } = stickyEvents;
+  //   const elem = document.querySelector(stickySelector);
+  //   elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
+
+  //   // return clean up handler
+  //   return () => {
+  //     elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
+  //   };
+  // }, [stickyChangeHandler]);
+
+  if (currentPath == null) {
+    return <></>;
+  }
 
-  function renderPageCreateButton() {
+  const renderPageCreateButton = () => {
     return (
       <>
         <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: '2.3rem', right: '4rem' }}>
@@ -70,7 +77,7 @@ const Fab = () => {
         </div>
       </>
     );
-  }
+  };
 
   return (
     <div className={`${styles['grw-fab']} grw-fab d-none d-md-block d-edit-none`} data-testid="grw-fab">
@@ -88,5 +95,3 @@ const Fab = () => {
   );
 
 };
-
-export default Fab;

+ 1 - 5
packages/app/src/components/Icons/CreatePageIcon.jsx → packages/app/src/components/Icons/CreatePageIcon.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-const CreatePageIcon = () => (
+export const CreatePageIcon = (): JSX.Element => (
   <svg
     xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 27 30"
@@ -19,8 +19,4 @@ const CreatePageIcon = () => (
     />
     <rect fillOpacity="0" width="27" height="27" />
   </svg>
-
 );
-
-
-export default CreatePageIcon;

+ 1 - 6
packages/app/src/components/Icons/ReturnTopIcon.jsx → packages/app/src/components/Icons/ReturnTopIcon.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-const ReturnTopIcon = () => (
+export const ReturnTopIcon = (): JSX.Element => (
   <svg
     xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 23 23"
@@ -11,10 +11,5 @@ const ReturnTopIcon = () => (
     />
     <path d="M22.35,4.61H.65a.65.65,0,0,1,0-1.3h21.7a.65.65,0,1,1,0,1.3Z" />
     <rect fillOpacity="0" width="23" height="23" />
-
   </svg>
-
 );
-
-
-export default ReturnTopIcon;

+ 3 - 3
packages/app/src/components/Layout/BasicLayout.tsx

@@ -7,7 +7,7 @@ import Sidebar from '../Sidebar';
 
 import { RawLayout } from './RawLayout';
 
-// const HotkeysManager = dynamic(() => import('../client/js/components/Hotkeys/HotkeysManager'), { ssr: false });
+const HotkeysManager = dynamic(() => import('../Hotkeys/HotkeysManager'), { ssr: false });
 // const PageCreateModal = dynamic(() => import('../client/js/components/PageCreateModal'), { ssr: false });
 const GrowiNavbarBottom = dynamic(() => import('../Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
 const ShortcutsModal = dynamic(() => import('../ShortcutsModal'), { ssr: false });
@@ -20,7 +20,7 @@ const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false
 const PagePresentationModal = dynamic(() => import('../PagePresentationModal'), { ssr: false });
 const PageAccessoriesModal = dynamic(() => import('../PageAccessoriesModal'), { ssr: false });
 // Fab
-const Fab = dynamic(() => import('../Fab'), { ssr: false });
+const Fab = dynamic(() => import('../Fab').then(mod => mod.Fab), { ssr: false });
 
 
 type Props = {
@@ -58,7 +58,7 @@ export const BasicLayout = ({
       <PageRenameModal />
       <PagePresentationModal />
       <PageAccessoriesModal />
-      {/* <HotkeysManager /> */}
+      <HotkeysManager />
 
       <Fab />
 

+ 50 - 0
packages/app/src/components/Layout/ShareLinkLayout.tsx

@@ -0,0 +1,50 @@
+import React, { ReactNode } from 'react';
+
+import dynamic from 'next/dynamic';
+
+import { GrowiNavbar } from '../Navbar/GrowiNavbar';
+
+import { RawLayout } from './RawLayout';
+
+const PageCreateModal = dynamic(() => import('../PageCreateModal'), { ssr: false });
+const GrowiNavbarBottom = dynamic(() => import('../Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
+const ShortcutsModal = dynamic(() => import('../ShortcutsModal'), { ssr: false });
+const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
+
+// Fab
+const Fab = dynamic(() => import('../Fab').then(mod => mod.Fab), { ssr: false });
+
+
+type Props = {
+  title: string
+  className?: string,
+  expandContainer?: boolean,
+  children?: ReactNode
+}
+
+export const ShareLinkLayout = ({
+  children, title, className, expandContainer,
+}: Props): JSX.Element => {
+
+  const myClassName = `${className ?? ''} ${expandContainer ? 'growi-layout-fluid' : ''}`;
+
+  return (
+    <RawLayout title={title} className={myClassName}>
+      <GrowiNavbar />
+
+      <div className="page-wrapper d-flex d-print-block">
+        <div className="flex-fill mw-0">
+          {children}
+        </div>
+      </div>
+
+      <GrowiNavbarBottom />
+
+      <Fab />
+
+      <ShortcutsModal />
+      <PageCreateModal />
+      <SystemVersion showShortcutsButton />
+    </RawLayout>
+  );
+};

+ 4 - 4
packages/app/src/components/LikeButtons.module.scss

@@ -1,11 +1,11 @@
 @use '~/styles/bootstrap/init' as bs;
 
-.btn-like {
-  :global {
+.btn-group-like :global {
+  .btn-like {
     box-shadow: none !important;
-  }
-  &:global {
+
     @include bs.button-outline-variant(rgba(bs.$secondary, 50%), lighten(bs.$red, 15%), rgba(lighten(bs.$red, 10%), 0.15), rgba(lighten(bs.$red, 10%), 0.5));
+
     &:not(:disabled):not(.disabled):active,
     &:not(:disabled):not(.disabled).active {
       color: lighten(bs.$red, 15%);

+ 2 - 2
packages/app/src/components/LikeButtons.tsx

@@ -45,12 +45,12 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
   }, [isGuestUser, isLiked]);
 
   return (
-    <div className="btn-group" role="group" aria-label="Like buttons">
+    <div className={`btn-group btn-group-like ${styles['btn-group-like']}`} role="group" aria-label="Like buttons">
       <button
         type="button"
         id="like-button"
         onClick={onLikeClicked}
-        className={`shadow-none btn btn-like ${styles['btn-like']} border-0
+        className={`shadow-none btn btn-like border-0
             ${isLiked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
         <i className={`fa ${isLiked ? 'fa-heart' : 'fa-heart-o'}`}></i>

+ 0 - 1
packages/app/src/components/Me/BasicInfoSettings.tsx

@@ -109,7 +109,6 @@ export const BasicInfoSettings = (): JSX.Element => {
             i18nConfig.locales.map((locale) => {
               if (i18n == null) { return }
               const fixedT = i18n.getFixedT(locale);
-              i18n.loadLanguages(i18nConfig.locales);
 
               return (
                 <div key={locale} className="custom-control custom-radio custom-control-inline">

+ 1 - 0
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -179,6 +179,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { data: currentUser } = useCurrentUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isSharedUser } = useIsSharedUser();
+  const { data: isNotFound } = useIsNotFound();
   const { data: shareLinkId } = useShareLinkId();
 
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();

+ 4 - 5
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -144,18 +144,17 @@ export const GrowiNavbar = (): JSX.Element => {
         {appTitle}
       </div>
 
-
       {/* Navbar Right  */}
       <ul className="navbar-nav ml-auto">
         <NavbarRight />
         <Confidential confidential={confidential} />
       </ul>
 
-      { isSearchServiceConfigured && !isDeviceSmallerThanMd && !isSearchPage && (
-        <div className="grw-global-search-container position-absolute">
+      <div className="grw-global-search-container position-absolute">
+        { isSearchServiceConfigured && !isDeviceSmallerThanMd && !isSearchPage && (
           <GlobalSearch />
-        </div>
-      ) }
+        ) }
+      </div>
     </nav>
   );
 

+ 1 - 1
packages/app/src/components/Navbar/GrowiSubNavigation.tsx

@@ -86,7 +86,7 @@ export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element
           <PagePathNav pageId={pageId} pagePath={path} isSingleLineMode={isEditorMode} isCompactMode={isCompactMode} />
         </div>
       </div>
-      {/* Right side. isNotFound for avoid flicker when called ForbiddenPage.tsx */}
+      {/* Right side. */}
       <div className="d-flex">
         <Controls />
         {/* Page Authors */}

+ 3 - 2
packages/app/src/components/Page.tsx

@@ -10,7 +10,7 @@ import { HtmlElementNode } from 'rehype-toc';
 
 // import { getOptionsToSave } from '~/client/util/editor';
 import {
-  useIsGuestUser, useCurrentPageTocNode,
+  useIsGuestUser, useCurrentPageTocNode, useShareLinkId,
 } from '~/stores/context';
 import {
   useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
@@ -200,7 +200,8 @@ export const Page = (props) => {
     tocRef.current = toc;
   }, []);
 
-  const { data: currentPage } = useSWRxCurrentPage();
+  const { data: shareLinkId } = useShareLinkId();
+  const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
   const { data: editorMode } = useEditorMode();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isMobile } = useIsMobile();

+ 7 - 11
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -6,7 +6,7 @@ import dynamic from 'next/dynamic';
 
 // import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import {
-  useCurrentPagePath, useIsSharedUser, useIsEditable, usePageUser, useShareLinkId, useIsNotFound,
+  useCurrentPagePath, useIsSharedUser, useIsEditable, useShareLinkId, useIsNotFound,
 } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
@@ -17,22 +17,19 @@ import CustomTabContent from '../CustomNavigation/CustomTabContent';
 import PageListIcon from '../Icons/PageListIcon';
 import { Page } from '../Page';
 import TableOfContents from '../TableOfContents';
-import { UserInfoProps } from '../User/UserInfo';
-
 
 import styles from './DisplaySwitcher.module.scss';
 
-
-const { isTopPage, isUsersTopPage } = pagePathUtils;
+const { isTopPage, isUsersHomePage } = pagePathUtils;
 
 
 const PageEditor = dynamic(() => import('../PageEditor'), { ssr: false });
 const PageEditorByHackmd = dynamic(() => import('../PageEditorByHackmd').then(mod => mod.PageEditorByHackmd), { ssr: false });
 const EditorNavbarBottom = dynamic(() => import('../PageEditor/EditorNavbarBottom'), { ssr: false });
 const HashChanged = dynamic(() => import('../EventListeneres/HashChanged'), { ssr: false });
-const ContentLinkButtons = dynamic(() => import('../ContentLinkButtons'), { ssr: false });
+const ContentLinkButtons = dynamic(() => import('../ContentLinkButtons').then(mod => mod.ContentLinkButtons), { ssr: false });
 const NotFoundPage = dynamic(() => import('../NotFoundPage'), { ssr: false });
-const UserInfo = dynamic<UserInfoProps>(() => import('../User/UserInfo').then(mod => mod.UserInfo), { ssr: false });
+const UserInfo = dynamic(() => import('../User/UserInfo').then(mod => mod.UserInfo), { ssr: false });
 
 
 const PageView = React.memo((): JSX.Element => {
@@ -41,19 +38,18 @@ const PageView = React.memo((): JSX.Element => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
-  const { data: pageUser } = usePageUser();
   const { data: isNotFound } = useIsNotFound();
   const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
   const { open: openDescendantPageListModal } = useDescendantsPageListModal();
 
   const isTopPagePath = isTopPage(currentPagePath ?? '');
-  const isUsersTopPagePath = isUsersTopPage(currentPagePath ?? '');
+  const isUsersHomePagePath = isUsersHomePage(currentPagePath ?? '');
 
   return (
     <div className="d-flex flex-column flex-lg-row">
 
       <div className="flex-grow-1 flex-basis-0 mw-0">
-        { pageUser != null && isUsersTopPagePath && <UserInfo pageUser={pageUser} />}
+        { isUsersHomePagePath && <UserInfo /> }
         { !isNotFound && <Page /> }
         { isNotFound && <NotFoundPage /> }
       </div>
@@ -98,7 +94,7 @@ const PageView = React.memo((): JSX.Element => {
 
             <div className="d-none d-lg-block">
               <TableOfContents />
-              <ContentLinkButtons />
+              { isUsersHomePagePath && <ContentLinkButtons /> }
             </div>
 
           </div>

+ 0 - 52
packages/app/src/components/Page/ShareLinkAlert.jsx

@@ -1,52 +0,0 @@
-import React from 'react';
-
-import { useTranslation } from 'next-i18next';
-
-const ShareLinkAlert = () => {
-  const { t } = useTranslation();
-
-  const shareContent = document.getElementById('is-shared-page');
-  const expiredAt = shareContent.getAttribute('data-share-link-expired-at');
-  const createdAt = shareContent.getAttribute('data-share-link-created-at');
-
-  function generateRatio() {
-    const wholeTime = new Date(expiredAt).getTime() - new Date(createdAt).getTime();
-    const remainingTime = new Date(expiredAt).getTime() - new Date().getTime();
-    return remainingTime / wholeTime;
-  }
-
-  let ratio = 1;
-
-  if (expiredAt !== '') {
-    ratio = generateRatio();
-  }
-
-  function specifyColor() {
-    let color;
-    if (ratio >= 0.75) {
-      color = 'success';
-    }
-    else if (ratio < 0.75 && ratio >= 0.5) {
-      color = 'info';
-    }
-    else if (ratio < 0.5 && ratio >= 0.25) {
-      color = 'warning';
-    }
-    else {
-      color = 'danger';
-    }
-    return color;
-  }
-
-  return (
-    <p className={`alert alert-${specifyColor()} py-3 px-4 d-edit-none`}>
-      <i className="icon-fw icon-link"></i>
-      {(expiredAt === '' ? <span>{t('page_page.notice.no_deadline')}</span>
-      // eslint-disable-next-line react/no-danger
-        : <span dangerouslySetInnerHTML={{ __html: t('page_page.notice.expiration', { expiredAt }) }} />
-      )}
-    </p>
-  );
-};
-
-export default ShareLinkAlert;

+ 52 - 0
packages/app/src/components/Page/ShareLinkAlert.tsx

@@ -0,0 +1,52 @@
+import React, { FC } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+const generateRatio = (expiredAt: Date, createdAt: Date): number => {
+  const wholeTime = new Date(expiredAt).getTime() - new Date(createdAt).getTime();
+  const remainingTime = new Date(expiredAt).getTime() - new Date().getTime();
+  return remainingTime / wholeTime;
+};
+
+const getAlertColor = (ratio: number): string => {
+  let color: string;
+
+  if (ratio >= 0.75) {
+    color = 'success';
+  }
+  else if (ratio < 0.75 && ratio >= 0.5) {
+    color = 'info';
+  }
+  else if (ratio < 0.5 && ratio >= 0.25) {
+    color = 'warning';
+  }
+  else {
+    color = 'danger';
+  }
+  return color;
+};
+
+type Props = {
+  createdAt: Date,
+  expiredAt?: Date,
+}
+
+const ShareLinkAlert: FC<Props> = (props: Props) => {
+  const { t } = useTranslation();
+  const { expiredAt, createdAt } = props;
+
+  const ratio = expiredAt != null ? generateRatio(expiredAt, createdAt) : 1;
+  const alertColor = getAlertColor(ratio);
+
+  return (
+    <p className={`alert alert-${alertColor} my-3 px-4 d-edit-none`}>
+      <i className="icon-fw icon-link"></i>
+      {(expiredAt === null ? <span>{t('page_page.notice.no_deadline')}</span>
+      // eslint-disable-next-line react/no-danger
+        : <span dangerouslySetInnerHTML={{ __html: t('page_page.notice.expiration', { expiredAt }) }} />
+      )}
+    </p>
+  );
+};
+
+export default ShareLinkAlert;

+ 5 - 9
packages/app/src/components/ShareLink/ShareLinkForm.tsx

@@ -1,7 +1,9 @@
 import React, { FC, useState, useCallback } from 'react';
 
 import { isInteger } from 'core-js/fn/number';
-import { format, parse } from 'date-fns';
+import {
+  format, parse, addDays, set,
+} from 'date-fns';
 import { useTranslation } from 'next-i18next';
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
@@ -56,8 +58,6 @@ export const ShareLinkForm: FC<Props> = (props: Props) => {
   }, []);
 
   const generateExpired = useCallback(() => {
-    let expiredAt;
-
     if (expirationType === ExpirationType.UNLIMITED) {
       return null;
     }
@@ -66,16 +66,12 @@ export const ShareLinkForm: FC<Props> = (props: Props) => {
       if (!isInteger(Number(numberOfDays))) {
         throw new Error(t('share_links.Invalid_Number_of_Date'));
       }
-      const date = new Date();
-      date.setDate(date.getDate() + Number(numberOfDays));
-      expiredAt = date;
+      return addDays(new Date(), numberOfDays);
     }
 
     if (expirationType === ExpirationType.CUSTOM) {
-      expiredAt = parse(`${customExpirationDate}T${customExpirationTime}`, "yyyy-MM-dd'T'HH:mm", new Date());
+      return set(customExpirationDate, { hours: customExpirationTime.getHours(), minutes: customExpirationTime.getMinutes() });
     }
-
-    return expiredAt;
   }, [t, customExpirationTime, customExpirationDate, expirationType, numberOfDays]);
 
   const closeForm = useCallback(() => {

+ 22 - 0
packages/app/src/components/User/SeenUserInfo.module.scss

@@ -0,0 +1,22 @@
+@use '~/styles/bootstrap/init' as bs;
+@use '~/styles/mixins';
+
+.grw-seen-user-info :global {
+  .btn.btn-seen-user {
+    $color-seen-user: #549c79;
+
+    @include bs.button-outline-variant($color-seen-user, $color-seen-user, rgba(lighten($color-seen-user, 10%), 0.15), rgba(lighten($color-seen-user, 10%), 0.5));
+    @include mixins.button-outline-svg-icon-variant($color-seen-user, $color-seen-user);
+
+    &:not(:disabled):not(.disabled):active,
+    &:not(:disabled):not(.disabled).active {
+      color: $color-seen-user;
+      svg {
+        fill: $color-seen-user;
+      }
+    }
+    &:not(:disabled):not(.disabled):not(:hover) {
+      background-color: transparent;
+    }
+  }
+}

+ 5 - 1
packages/app/src/components/User/SeenUserInfo.tsx

@@ -8,6 +8,10 @@ import { IUser } from '~/interfaces/user';
 
 import UserPictureList from './UserPictureList';
 
+
+import styles from './SeenUserInfo.module.scss';
+
+
 interface Props {
   seenUsers: IUser[],
   sumOfSeenUsers?: number,
@@ -23,7 +27,7 @@ const SeenUserInfo: FC<Props> = (props: Props) => {
   const togglePopover = () => setIsPopoverOpen(!isPopoverOpen);
 
   return (
-    <div className="grw-seen-user-info">
+    <div className={`grw-seen-user-info ${styles['grw-seen-user-info']}`}>
       <button type="button" id="btn-seen-user" className="shadow-none btn btn-seen-user border-0">
         <span className="mr-1 footstamp-icon">
           <FootstampIcon />

+ 4 - 9
packages/app/src/components/User/UserInfo.tsx

@@ -2,20 +2,15 @@ import React from 'react';
 
 import { UserPicture } from '@growi/ui';
 
-import { IUserHasId } from '~/interfaces/user';
+import { usePageUser } from '~/stores/context';
 
 import styles from './UserInfo.module.scss';
 
-export type UserInfoProps = {
-  pageUser: IUserHasId,
-}
+export const UserInfo = (): JSX.Element => {
 
-export const UserInfo = (props: UserInfoProps): JSX.Element => {
+  const { data: pageUser } = usePageUser();
 
-  const { pageUser } = props;
-
-  // Do not display when the user does not exist
-  if (pageUser == null) {
+  if (pageUser == null || pageUser.status === 4) {
     return <></>;
   }
 

+ 11 - 0
packages/app/src/interfaces/share-link.ts

@@ -1,4 +1,15 @@
+import { IPageHasId, HasObjectId } from '@growi/core';
+
 // Todo: specify more detailed Type
 export type IResShareLinkList = {
   shareLinksResult: any[],
 };
+
+export type IShareLink = {
+  relatedPage: IPageHasId,
+  createdAt: Date,
+  expiredAt?: Date,
+  description: string,
+};
+
+export type IShareLinkHasId = IShareLink & HasObjectId;

+ 2 - 1
packages/app/src/pages/admin/[[...path]].page.tsx

@@ -307,7 +307,8 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
  * @param namespacesRequired
  */
 async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+  // preload all languages because of language lists in user setting
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired, true);
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 

+ 2 - 1
packages/app/src/pages/me/[[...path]].page.tsx

@@ -169,7 +169,8 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 //  * @param namespacesRequired
 //  */
 async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+  // preload all languages because of language lists in user setting
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired, true);
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 

+ 169 - 0
packages/app/src/pages/share/[[...path]].page.tsx

@@ -0,0 +1,169 @@
+import React from 'react';
+
+import { IUser, IUserHasId } from '@growi/core';
+import {
+  NextPage, GetServerSideProps, GetServerSidePropsContext,
+} from 'next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+import dynamic from 'next/dynamic';
+
+import { ShareLinkLayout } from '~/components/Layout/ShareLinkLayout';
+import GrowiContextualSubNavigation from '~/components/Navbar/GrowiContextualSubNavigation';
+import { Page } from '~/components/Page';
+import { CrowiRequest } from '~/interfaces/crowi-request';
+import { RendererConfig } from '~/interfaces/services/renderer';
+import { IShareLinkHasId } from '~/interfaces/share-link';
+import {
+  useCurrentUser, useCurrentPagePath, useCurrentPathname, useCurrentPageId, useRendererConfig,
+  useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault,
+} from '~/stores/context';
+
+import {
+  CommonProps, getServerSideCommonProps, useCustomTitle, getNextI18NextConfig,
+} from '../utils/commons';
+
+const ShareLinkAlert = dynamic(() => import('~/components/Page/ShareLinkAlert'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/ForbiddenPage'), { ssr: false });
+
+type Props = CommonProps & {
+  shareLink?: IShareLinkHasId,
+  isExpired: boolean,
+  currentUser: IUser,
+  disableLinkSharing: boolean,
+  isSearchServiceConfigured: boolean,
+  isSearchServiceReachable: boolean,
+  isSearchScopeChildrenAsDefault: boolean,
+  rendererConfig: RendererConfig,
+};
+
+const SharedPage: NextPage<Props> = (props: Props) => {
+  useShareLinkId(props.shareLink?._id);
+  useCurrentPageId(props.shareLink?.relatedPage._id);
+  useCurrentPagePath(props.shareLink?.relatedPage.path);
+  useCurrentUser(props.currentUser);
+  useCurrentPathname(props.currentPathname);
+  useRendererConfig(props.rendererConfig);
+  useIsSearchServiceConfigured(props.isSearchServiceConfigured);
+  useIsSearchServiceReachable(props.isSearchServiceReachable);
+  useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
+
+  const isNotFound = props.shareLink == null || props.shareLink.relatedPage == null || props.shareLink.relatedPage.isEmpty;
+  const isShowSharedPage = !props.disableLinkSharing && !isNotFound && !props.isExpired;
+
+  return (
+    <ShareLinkLayout title={useCustomTitle(props, 'GROWI')} expandContainer={props.isContainerFluid}>
+      <div className="h-100 d-flex flex-column justify-content-between">
+        <header className="py-0 position-relative">
+          {isShowSharedPage && <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />}
+        </header>
+
+        <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
+
+        <div className="flex-grow-1">
+          <div id="content-main" className="content-main grw-container-convertible">
+            { props.disableLinkSharing && (
+              <div className="mt-4">
+                <ForbiddenPage isLinkSharingDisabled={props.disableLinkSharing} />
+              </div>
+            )}
+
+            { (isNotFound && !props.disableLinkSharing) && (
+              <div className="container-lg">
+                <h2 className="text-muted mt-4">
+                  <i className="icon-ban" aria-hidden="true" />
+                  <span> Page is not found</span>
+                </h2>
+              </div>
+            )}
+
+            { (props.isExpired && !props.disableLinkSharing) && (
+              <div className="container-lg">
+                <h2 className="text-muted mt-4">
+                  <i className="icon-ban" aria-hidden="true" />
+                  <span> Page is expired</span>
+                </h2>
+              </div>
+            )}
+
+            {(isShowSharedPage && props.shareLink != null) && (
+              <>
+                <ShareLinkAlert expiredAt={props.shareLink.expiredAt} createdAt={props.shareLink.createdAt} />
+                <Page />
+              </>
+            )}
+          </div>
+        </div>
+      </div>
+    </ShareLinkLayout>
+  );
+};
+
+function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const { crowi } = req;
+
+  props.disableLinkSharing = crowi.configManager.getConfig('crowi', 'security:disableLinkSharing');
+
+  props.isSearchServiceConfigured = crowi.searchService.isConfigured;
+  props.isSearchServiceReachable = crowi.searchService.isReachable;
+  props.isSearchScopeChildrenAsDefault = crowi.configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
+
+  props.rendererConfig = {
+    isEnabledLinebreaks: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
+    isEnabledLinebreaksInComments: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+    adminPreferredIndentSize: crowi.configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
+    isIndentSizeForced: crowi.configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
+
+    plantumlUri: process.env.PLANTUML_URI ?? null,
+    blockdiagUri: process.env.BLOCKDIAG_URI ?? null,
+
+    // XSS Options
+    isEnabledXssPrevention: crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
+    attrWhiteList: crowi.xssService.getAttrWhiteList(),
+    tagWhiteList: crowi.xssService.getTagWhiteList(),
+    highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
+  };
+}
+
+async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+  props._nextI18Next = nextI18NextConfig._nextI18Next;
+}
+
+export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+  const req = context.req as CrowiRequest<IUserHasId & any>;
+  const { user, crowi } = req;
+  const result = await getServerSideCommonProps(context);
+
+  if (!('props' in result)) {
+    throw new Error('invalid getSSP result');
+  }
+  const props: Props = result.props as Props;
+
+  if (user != null) {
+    props.currentUser = user.toObject();
+  }
+
+  const { linkId } = req.params;
+  try {
+    const ShareLinkModel = crowi.model('ShareLink');
+    const shareLink = await ShareLinkModel.findOne({ _id: linkId }).populate('relatedPage');
+    if (shareLink != null) {
+      props.isExpired = shareLink.isExpired();
+      props.shareLink = shareLink.toObject();
+    }
+  }
+  catch (err) {
+    //
+  }
+
+  injectServerConfigurations(context, props);
+  // await injectUserUISettings(context, props);
+  await injectNextI18NextConfigurations(context, props);
+
+  return {
+    props,
+  };
+};
+
+export default SharedPage;

+ 5 - 2
packages/app/src/pages/tags.page.tsx

@@ -5,10 +5,9 @@ import {
 } from '@growi/core';
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
 import Head from 'next/head';
 
-import TagCloudBox from '~/components/TagCloudBox';
-import TagList from '~/components/TagList';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { IDataTagCount } from '~/interfaces/tag';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
@@ -36,6 +35,9 @@ type Props = CommonProps & {
   userUISettings?: IUserUISettings
 };
 
+const TagList = dynamic(() => import('~/components/TagList'), { ssr: false });
+const TagCloudBox = dynamic(() => import('~/components/TagCloudBox'), { ssr: false });
+
 const TagPage: NextPage<CommonProps> = (props: Props) => {
   const [activePage, setActivePage] = useState<number>(1);
   const [offset, setOffset] = useState<number>(0);
@@ -85,6 +87,7 @@ const TagPage: NextPage<CommonProps> = (props: Props) => {
               </div>
             )
           }
+          <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
         </div>
       </BasicLayout>
     </>

+ 6 - 4
packages/app/src/pages/utils/commons.ts

@@ -1,4 +1,4 @@
-import { DevidedPagePath, Lang } from '@growi/core';
+import { DevidedPagePath, Lang, AllLang } from '@growi/core';
 import { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { SSRConfig, UserConfig } from 'next-i18next';
 
@@ -61,8 +61,10 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
 export const getNextI18NextConfig = async(
     // 'serverSideTranslations' method should be given from Next.js Page
     //  because importing it in this file causes https://github.com/isaachinman/next-i18next/issues/1545
-    serverSideTranslations: (initialLocale: string, namespacesRequired?: string[] | undefined, configOverride?: UserConfig | null) => Promise<SSRConfig>,
-    context: GetServerSidePropsContext, namespacesRequired?: string[] | undefined,
+    serverSideTranslations: (
+      initialLocale: string, namespacesRequired?: string[] | undefined, configOverride?: UserConfig | null, extraLocales?: string[] | false
+    ) => Promise<SSRConfig>,
+    context: GetServerSidePropsContext, namespacesRequired?: string[] | undefined, preloadAllLang = false,
 ): Promise<SSRConfig> => {
 
   const req: CrowiRequest = context.req as CrowiRequest;
@@ -74,7 +76,7 @@ export const getNextI18NextConfig = async(
     ?? configManager.getConfig('crowi', 'app:globalLang') as Lang
     ?? Lang.en_US;
 
-  return serverSideTranslations(locale, namespacesRequired ?? ['translation'], nextI18NextConfig);
+  return serverSideTranslations(locale, namespacesRequired ?? ['translation'], nextI18NextConfig, preloadAllLang ? AllLang : false);
 };
 
 /**

+ 1 - 1
packages/app/src/server/routes/index.js

@@ -243,7 +243,7 @@ module.exports = function(crowi, app) {
     .use(userActivation.tokenErrorHandlerMiddeware));
   app.post('/user-activation/register', applicationInstalled, csrfProtection, userActivation.registerRules(), userActivation.validateRegisterForm, userActivation.registerAction(crowi));
 
-  app.get('/share/:linkId', page.showSharedPage);
+  app.get('/share/:linkId', next.delegateToNext);
 
   app.use('/ogp', express.Router().get('/:pageId([0-9a-z]{0,})', loginRequired, ogp.pageIdRequired, ogp.ogpValidator, ogp.renderOgp));
 

+ 0 - 17
packages/app/src/styles/atoms/_buttons.scss

@@ -1,23 +1,6 @@
 @use '../bootstrap/init' as bs;
 @use '../mixins';
 
-.btn.btn-seen-user {
-  $color-seen-user: #549c79;
-
-  @include bs.button-outline-variant($color-seen-user, $color-seen-user, rgba(lighten($color-seen-user, 10%), 0.15), rgba(lighten($color-seen-user, 10%), 0.5));
-  @include mixins.button-outline-svg-icon-variant($color-seen-user, $color-seen-user);
-  &:not(:disabled):not(.disabled):active,
-  &:not(:disabled):not(.disabled).active {
-    color: $color-seen-user;
-    svg {
-      fill: $color-seen-user;
-    }
-  }
-  &:not(:disabled):not(.disabled):not(:hover) {
-    background-color: transparent;
-  }
-}
-
 .btn-copy,
 .btn-edit {
   &:not(:hover):not(:active) {

+ 1 - 1
packages/app/src/styles/theme/_apply-colors.scss

@@ -169,7 +169,7 @@ ul.pagination {
 
 .grw-navbar {
   background: $bgcolor-navbar;
-  .nav-item > .nav-link {
+  .nav-item .nav-link {
     color: $color-link-nabvar;
   }
 

+ 1 - 0
packages/core/src/interfaces/user.ts

@@ -20,6 +20,7 @@ export type IUser = {
   createdAt: Date,
   lastLoginAt?: Date,
   introduction: string,
+  status: number,
 }
 
 export type IUserGroupRelation = {

+ 31 - 30
yarn.lock

@@ -1441,13 +1441,6 @@
   dependencies:
     regenerator-runtime "^0.13.4"
 
-"@babel/runtime@^7.13.17", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2":
-  version "7.18.6"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580"
-  integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==
-  dependencies:
-    regenerator-runtime "^0.13.4"
-
 "@babel/runtime@^7.13.8", "@babel/runtime@^7.8.7":
   version "7.17.7"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.7.tgz#a5f3328dc41ff39d803f311cfe17703418cf9825"
@@ -1455,6 +1448,13 @@
   dependencies:
     regenerator-runtime "^0.13.4"
 
+"@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580"
+  integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
 "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2":
   version "7.16.7"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa"
@@ -1462,6 +1462,13 @@
   dependencies:
     regenerator-runtime "^0.13.4"
 
+"@babel/runtime@^7.18.6":
+  version "7.19.0"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
+  integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
 "@babel/runtime@^7.6.3":
   version "7.7.7"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.7.tgz#194769ca8d6d7790ec23605af9ee3e42a0aa79cf"
@@ -11582,7 +11589,7 @@ hogan.js@3.0.2:
     mkdirp "0.3.0"
     nopt "1.0.10"
 
-hoist-non-react-statics@^3.2.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
+hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
   integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -11617,11 +11624,6 @@ html-escaper@^2.0.0:
   resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.0.tgz#71e87f931de3fe09e56661ab9a29aadec707b491"
   integrity sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==
 
-html-escaper@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
-  integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
-
 html-parse-stringify@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
@@ -11807,10 +11809,10 @@ i18next-localstorage-backend@^3.1.3:
   dependencies:
     "@babel/runtime" "^7.14.6"
 
-i18next@^21.6.14:
-  version "21.8.11"
-  resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.8.11.tgz#197aac04be51b7083999a2cb63deb831cf1ba4c8"
-  integrity sha512-+s8N6kQShwNK+Ua/+VsS/Sji24NUJJLBk9QIucygj1f97f4hPNDWmLP9fQCI4d5+XLfXJ3JctX4g+zJla967Vw==
+i18next@^21.8.13:
+  version "21.9.2"
+  resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.9.2.tgz#3f7c5594393eb27117c1db4c38f5ec766e68de0e"
+  integrity sha512-00fVrLQOwy45nm3OtC9l1WiLK3nJlIYSljgCt0qzTaAy65aciMdRy9GsuW+a2AtKtdg9/njUGfRH30LRupV7ZQ==
   dependencies:
     "@babel/runtime" "^7.17.2"
 
@@ -16108,18 +16110,18 @@ nested-error-stacks@^2.0.0:
   resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61"
   integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==
 
-next-i18next@^11.0.0:
-  version "11.0.0"
-  resolved "https://registry.yarnpkg.com/next-i18next/-/next-i18next-11.0.0.tgz#2857d13c58a5ed976fe57c44286f1520b07f7c96"
-  integrity sha512-phxbQiZGSJTTBE2FI4+BnqFZl88AI2V+6MrEQnT9aPFAXq/fATQ/F0pOUM3J7kU4nEeCfn3hjISq+ygGHlEz0g==
+next-i18next@^11.3.0:
+  version "11.3.0"
+  resolved "https://registry.yarnpkg.com/next-i18next/-/next-i18next-11.3.0.tgz#bfce51d8df07fb5cd61097423eeb7d744e09ae25"
+  integrity sha512-xl0oIRtiVrk9ZaWBRUbNk/prva4Htdu59o9rFWzd9ax/KemaDVuTTuBZTQMkmXohUQk/MJ7w1rV/mICL6TzyGw==
   dependencies:
-    "@babel/runtime" "^7.13.17"
+    "@babel/runtime" "^7.18.6"
     "@types/hoist-non-react-statics" "^3.3.1"
     core-js "^3"
-    hoist-non-react-statics "^3.2.0"
-    i18next "^21.6.14"
+    hoist-non-react-statics "^3.3.2"
+    i18next "^21.8.13"
     i18next-fs-backend "^1.1.4"
-    react-i18next "^11.16.2"
+    react-i18next "^11.18.0"
 
 next-superjson@^0.0.4:
   version "0.0.4"
@@ -18450,13 +18452,12 @@ react-hotkeys@^2.0.0:
   dependencies:
     prop-types "^15.6.1"
 
-react-i18next@^11.16.2:
-  version "11.17.3"
-  resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.17.3.tgz#eff742f162f1a413056fb510e8b830d42c8c020d"
-  integrity sha512-rIrLl5cLDoHdXFWdjKurRpatA3MPC9j3yTZidv0GmJEea5+XGXl42p7NupA1dmghoLGOXllShNUobgPYtgEcRA==
+react-i18next@^11.18.0:
+  version "11.18.6"
+  resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.18.6.tgz#e159c2960c718c1314f1e8fcaa282d1c8b167887"
+  integrity sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==
   dependencies:
     "@babel/runtime" "^7.14.5"
-    html-escaper "^2.0.2"
     html-parse-stringify "^3.0.1"
 
 react-image-crop@^8.3.0: