diff --git a/.dockerignore b/.dockerignore index 1168d108..0ec9738d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,8 @@ node_modules out dist +.husky +.next .idea .history .vscode diff --git a/.drone.yml b/.drone.yml index 0d9eefa7..695a9504 100644 --- a/.drone.yml +++ b/.drone.yml @@ -11,10 +11,10 @@ steps: image: plugins/docker when: branch: - - master + - never environment: - NEXT_PUBLIC_API_HOST: https://pig.vault48.org/ - NEXT_PUBLIC_REMOTE_CURRENT: https://pig.vault48.org/static/ + NEXT_PUBLIC_API_HOST: https://vault48.org/api/ + NEXT_PUBLIC_REMOTE_CURRENT: https://vault48.org/static/ NEXT_PUBLIC_PUBLIC_HOST: https://vault48.org/ NEXT_PUBLIC_BOT_USERNAME: vault48bot settings: diff --git a/.env.local b/.env.local index 7ce09d99..cf359933 100644 --- a/.env.local +++ b/.env.local @@ -2,6 +2,6 @@ # NEXT_PUBLIC_REMOTE_CURRENT=https://pig.staging.vault48.org/static/ # NEXT_PUBLIC_API_HOST=http://localhost:7777/ # NEXT_PUBLIC_REMOTE_CURRENT=http://localhost:7777/static/ -NEXT_PUBLIC_API_HOST=https://pig.vault48.org/ -NEXT_PUBLIC_REMOTE_CURRENT=https://pig.vault48.org/static/ +NEXT_PUBLIC_API_HOST=https://vault48.org/api/ +NEXT_PUBLIC_REMOTE_CURRENT=https://vault48.org/static/ NEXT_PUBLIC_BOT_USERNAME=vault48testbot \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 46b6af84..e822b399 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,7 @@ module.exports = { extends: ['plugin:react/recommended', 'plugin:@next/next/recommended'], rules: { + 'prettier/prettier': 'error', 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks 'react-hooks/exhaustive-deps': 'warn', // Checks effect dependencies 'react/prop-types': 0, @@ -9,13 +10,21 @@ module.exports = { '@next/next/no-img-element': 0, 'unused-imports/no-unused-imports': 'warn', // 'no-unused-vars': 'warn', - 'quotes': [2, 'single', { 'avoidEscape': true }], + quotes: [2, 'single', { avoidEscape: true }], 'import/order': [ 'error', { alphabetize: { order: 'asc' }, 'newlines-between': 'always', - groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'unknown'], + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + 'unknown', + ], pathGroups: [ { pattern: 'react', @@ -34,18 +43,17 @@ module.exports = { paths: [ { name: 'ramda', - message: - 'import from \'~/utils/ramda\' instead', + message: "import from '~/utils/ramda' instead", }, ], }, - ] + ], }, parserOptions: { ecmaVersion: 7, sourceType: 'module', }, - plugins: ['import', 'react-hooks', 'unused-imports'], + plugins: ['import', 'react-hooks', 'unused-imports', 'prettier'], parser: '@typescript-eslint/parser', settings: { react: { diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 00000000..1167a419 --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -0,0 +1,46 @@ +name: Build & Publish + +on: + push: + branches: [master] + +jobs: + push_to_registry: + name: Build & Publish + runs-on: ubuntu-22.04 + permissions: + packages: write + contents: read + attestations: write + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Registry Login + uses: docker/login-action@v3 + with: + registry: git.vault48.org + username: ${{ secrets.username }} + password: ${{ secrets.password }} + + - name: Extract docker metadata + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: git.vault48.org/${{ env.GITHUB_REPOSITORY }} + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/nextjs-standalone/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + NEXT_PUBLIC_API_HOST=https://vault48.org/api/ + NEXT_PUBLIC_REMOTE_CURRENT=https://vault48.org/static/ + NEXT_PUBLIC_PUBLIC_HOST=https://vault48.org/ + NEXT_PUBLIC_BOT_USERNAME=vault48bot \ No newline at end of file diff --git a/docker/nextjs-standalone/Dockerfile b/docker/nextjs-standalone/Dockerfile new file mode 100644 index 00000000..c6f52021 --- /dev/null +++ b/docker/nextjs-standalone/Dockerfile @@ -0,0 +1,51 @@ +# As written here: +# https://dev.to/leduc1901/reduce-docker-image-size-for-your-nextjs-app-5911 + +# Base ─────────────────────────────────────────────────────────────────────── +FROM node:14-alpine as base + +WORKDIR /opt/app + +ENV PATH /opt/app/node_modules/.bin:$PATH + +# Build ────────────────────────────────────────────────────────────────────── +FROM base as builder + +ARG NEXT_PUBLIC_API_HOST +ARG NEXT_PUBLIC_REMOTE_CURRENT +ARG NEXT_PUBLIC_PUBLIC_HOST +ARG NEXT_PUBLIC_BOT_USERNAME + +ENV NEXT_PUBLIC_API_HOST $NEXT_PUBLIC_API_HOST +ENV NEXT_PUBLIC_REMOTE_CURRENT $NEXT_PUBLIC_REMOTE_CURRENT +ENV NEXT_PUBLIC_PUBLIC_HOST $NEXT_PUBLIC_PUBLIC_HOST +ENV NEXT_PUBLIC_BOT_USERNAME $NEXT_PUBLIC_BOT_USERNAME + +# ENV NEXT_PUBLIC_API_HOST https://vault48.org/api/ +# ENV NEXT_PUBLIC_REMOTE_CURRENT https://vault48.org/static/ +# ENV NEXT_PUBLIC_PUBLIC_HOST https://vault48.org/ +# ENV NEXT_PUBLIC_BOT_USERNAME vault48bot + +COPY package.json . +COPY yarn.lock . + +RUN true \ + && yarn install --frozen-lockfile\ + && true + +COPY . /opt/app + +# pkg packs nodejs with given script, so we don't need it in next section +RUN yarn next build + +FROM node:14-alpine as runner + +WORKDIR /opt/app + +COPY --from=builder /opt/app/public ./public +COPY --from=builder /opt/app/.next/standalone . +COPY --from=builder /opt/app/.next/static ./.next/static + +EXPOSE 3000 + +ENTRYPOINT ["node", "server.js"] \ No newline at end of file diff --git a/next.config.js b/next.config.js index 8fb923d7..c95c6aa1 100644 --- a/next.config.js +++ b/next.config.js @@ -2,44 +2,49 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }); -const withTM = require('next-transpile-modules')(['ramda', '@v9v/ts-react-telegram-login']); +const withTM = require('next-transpile-modules')([ + 'ramda', + '@v9v/ts-react-telegram-login', +]); module.exports = withBundleAnalyzer( withTM({ + output: 'standalone', /** rewrite old-style node paths */ async rewrites() { return [ { - source: '/post:id', + // everything except 'post' is for backwards compatibility here + source: '/(post|photo|blog|song|video|cell):id', destination: '/node/:id', }, { source: '/~:username', destination: '/profile/:username', - } + }, ]; }, /** don't try to optimize fonts */ optimizeFonts: false, images: { - remotePatterns: [ - { - protocol: 'https', - hostname: '*.vault48.org', - pathname: '/**', - }, - { - protocol: 'https', - hostname: '*.ytimg.com', - pathname: '/**', - }, - { - protocol: 'http', - hostname: 'localhost', - pathname: '/**', - }, - ], - }, - }) + remotePatterns: [ + { + protocol: 'https', + hostname: 'vault48.org', + pathname: '/static/**', + }, + { + protocol: 'https', + hostname: '*.ytimg.com', + pathname: '/**', + }, + { + protocol: 'http', + hostname: 'localhost', + pathname: '/**', + }, + ], + }, + }), ); diff --git a/package.json b/package.json index 10e5589c..43e726e8 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "mobx-persist-store": "^1.0.4", "mobx-react-lite": "^3.2.3", "next": "^12.3.0", - "photoswipe": "^4.1.3", + "photoswipe": "^5.4.4", "raleway-cyrillic": "^4.0.2", "ramda": "^0.26.1", "react": "^17.0.2", @@ -36,12 +36,13 @@ "react-lazyload": "^3.2.0", "react-masonry-css": "^1.0.16", "react-popper": "^2.2.3", + "react-resize-detector": "^12.0.2", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", "react-sticky-box": "^1.0.2", "sass": "^1.49.0", "sharp": "^0.32.6", - "swiper": "^11.0.3", + "swiper": "^11.2.2", "swr": "^1.0.1", "throttle-debounce": "^2.1.0", "typescript": "^4.0.5", @@ -92,13 +93,14 @@ "@typescript-eslint/parser": "^5.10.1", "eslint": "^7.32.0", "eslint-plugin-import": "^2.25.4", + "eslint-plugin-prettier": "^5.2.3", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-unused-imports": "^3.0.0", "husky": "^7.0.4", "lint-staged": "^12.1.6", "next-transpile-modules": "^9.0.0", - "prettier": "^2.7.1" + "prettier": "^3.0.0" }, "lint-staged": { "./**/*.{js,jsx,ts,tsx}": [ diff --git a/public/images/sansivieria.svg b/public/images/sansivieria.svg new file mode 100644 index 00000000..ff60a0da --- /dev/null +++ b/public/images/sansivieria.svg @@ -0,0 +1,752 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/common/Avatar/index.tsx b/src/components/common/Avatar/index.tsx index dc35d469..946c63f9 100644 --- a/src/components/common/Avatar/index.tsx +++ b/src/components/common/Avatar/index.tsx @@ -14,7 +14,7 @@ interface Props extends DivProps { username?: string; size?: number; hasUpdates?: boolean; - preset?: typeof imagePresets[keyof typeof imagePresets]; + preset?: (typeof imagePresets)[keyof typeof imagePresets]; } const Avatar = forwardRef( diff --git a/src/components/common/Columns/index.tsx b/src/components/common/Columns/index.tsx index 6901c609..e491e77c 100644 --- a/src/components/common/Columns/index.tsx +++ b/src/components/common/Columns/index.tsx @@ -31,7 +31,7 @@ const Columns: FC = ({ if (!childs) return; - const timeout = setTimeout(() => setColumns([...childs]), 150); + const timeout = setTimeout(() => setColumns([...childs.values()]), 150); return () => clearTimeout(timeout); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/components/common/NodeHorizontalCard/index.tsx b/src/components/common/NodeHorizontalCard/index.tsx index 3c0847be..309110ea 100644 --- a/src/components/common/NodeHorizontalCard/index.tsx +++ b/src/components/common/NodeHorizontalCard/index.tsx @@ -8,6 +8,8 @@ import { URLS } from '~/constants/urls'; import { INode } from '~/types'; import { getPrettyDate } from '~/utils/dom'; +import { getNewCommentAnchor } from '../../../constants/dom/links'; + import styles from './styles.module.scss'; interface Props { @@ -16,32 +18,34 @@ interface Props { onClick?: MouseEventHandler; } -const NodeHorizontalCard: FC = ({ node, hasNew, onClick }) => { - return ( - = ({ node, hasNew, onClick }) => ( + +
-
- -
+ +
-
-
{node.title || '...'}
+
+
{node.title || '...'}
-
- {getPrettyDate(node.created_at)} -
+
+ {getPrettyDate(node.created_at)}
- - ); -}; +
+ +); export { NodeHorizontalCard }; diff --git a/src/components/common/NodeHorizontalCard/styles.module.scss b/src/components/common/NodeHorizontalCard/styles.module.scss index 32007320..6eb626c4 100644 --- a/src/components/common/NodeHorizontalCard/styles.module.scss +++ b/src/components/common/NodeHorizontalCard/styles.module.scss @@ -27,8 +27,8 @@ &.new { &::after { content: ' '; - width: 12px; - height: 12px; + width: 8px; + height: 8px; border-radius: 100%; background: $color_danger; box-shadow: $content_bg 0 0 0 5px; diff --git a/src/components/common/SubTitle/index.tsx b/src/components/common/SubTitle/index.tsx index ebfd8961..e24510ea 100644 --- a/src/components/common/SubTitle/index.tsx +++ b/src/components/common/SubTitle/index.tsx @@ -13,9 +13,11 @@ interface Props extends DivProps { const SubTitle: FC = ({ isLoading, children, ...rest }) => (
- - {children} - + + + {children} + +
); diff --git a/src/components/common/SubTitle/styles.module.scss b/src/components/common/SubTitle/styles.module.scss index 98651785..c3a36b26 100644 --- a/src/components/common/SubTitle/styles.module.scss +++ b/src/components/common/SubTitle/styles.module.scss @@ -1,7 +1,25 @@ -@import "src/styles/variables.scss"; +@import 'src/styles/variables.scss'; .title { font: $font_12_semibold; text-transform: uppercase; - opacity: 0.3; + display: flex; + flex-direction: row; + align-items: center; + gap: $gap / 2; + color: var(--gray_75); + + a { + text-decoration: none; + color: inherit; + } + + &::after { + content: ' '; + display: flex; + height: 2px; + background-color: var(--gray_90); + flex: 1; + border-radius: 2px; + } } diff --git a/src/components/input/InputText/styles.module.scss b/src/components/input/InputText/styles.module.scss index 1422f8fc..740fca00 100644 --- a/src/components/input/InputText/styles.module.scss +++ b/src/components/input/InputText/styles.module.scss @@ -25,7 +25,7 @@ background: none; padding: 0 $gap 0 $gap; font: $font_14_semibold; - border-radius: $radius; + border-radius: $input_radius; } } diff --git a/src/components/input/InputWrapper/styles.module.scss b/src/components/input/InputWrapper/styles.module.scss index b200c3a2..5c588408 100644 --- a/src/components/input/InputWrapper/styles.module.scss +++ b/src/components/input/InputWrapper/styles.module.scss @@ -5,7 +5,7 @@ background: $input_bg_color; min-height: $input_height; - border-radius: $radius; + border-radius: $input_radius; position: relative; color: $input_text_color; font: $input_font; diff --git a/src/components/node/NodeImageSwiperBlock/index.tsx b/src/components/node/NodeImageSwiperBlock/index.tsx index 9e5d8082..c3c778d3 100644 --- a/src/components/node/NodeImageSwiperBlock/index.tsx +++ b/src/components/node/NodeImageSwiperBlock/index.tsx @@ -57,7 +57,9 @@ const NodeImageSwiperBlock: FC = observer(({ node }) => { useEffect(() => { controlledSwiper?.slideTo(0, 0); - return () => controlledSwiper?.slideTo(0, 0); + return () => { + controlledSwiper?.slideTo(0, 0); + }; }, [controlledSwiper, images, node.id]); useEffect(() => { diff --git a/src/components/node/NodeRelated/styles.module.scss b/src/components/node/NodeRelated/styles.module.scss index 1d4de34d..7a26a31a 100644 --- a/src/components/node/NodeRelated/styles.module.scss +++ b/src/components/node/NodeRelated/styles.module.scss @@ -29,11 +29,6 @@ .title { padding-left: 5px; - - a { - text-decoration: none; - color: inherit; - } } .text { diff --git a/src/components/notifications/NotificationComment/index.tsx b/src/components/notifications/NotificationComment/index.tsx index 194a60bb..e707a27f 100644 --- a/src/components/notifications/NotificationComment/index.tsx +++ b/src/components/notifications/NotificationComment/index.tsx @@ -9,6 +9,8 @@ import { Square } from '~/components/common/Square'; import { NotificationItem } from '~/types/notifications'; import { formatText, getURLFromString } from '~/utils/dom'; +import { getCommentAnchor } from '../../../constants/dom/links'; + import styles from './styles.module.scss'; interface NotificationCommentProps { @@ -17,7 +19,10 @@ interface NotificationCommentProps { } const NotificationComment: FC = ({ item, isNew }) => ( - +
- `${CONFIG.apiHost}oauth/${provider}/redirect`, + `${CONFIG.apiHost}oauth/${provider}/redirect/`, ME: '/auth', UPDATE_PHOTO: '/auth/photo', UPDATE_COVER: '/auth/photo', - PROFILE: (username: string) => `/users/${username}/profile`, + PROFILE: (username: string) => `/users/${username}`, MESSAGES: (username: string) => `/users/${username}/messages`, MESSAGE_SEND: (username: string) => `/users/${username}/messages`, MESSAGE_DELETE: (username: string, id: number) => diff --git a/src/constants/comment.ts b/src/constants/comment.ts index cd3e5f2f..a61d0339 100644 --- a/src/constants/comment.ts +++ b/src/constants/comment.ts @@ -20,7 +20,7 @@ export const COMMENT_BLOCK_DETECTORS = [ ]; export type ICommentBlock = { - type: typeof COMMENT_BLOCK_TYPES[keyof typeof COMMENT_BLOCK_TYPES]; + type: (typeof COMMENT_BLOCK_TYPES)[keyof typeof COMMENT_BLOCK_TYPES]; content: string; }; diff --git a/src/constants/dom/index.ts b/src/constants/dom/index.ts index be18c3a4..3d9462a8 100644 --- a/src/constants/dom/index.ts +++ b/src/constants/dom/index.ts @@ -5,3 +5,5 @@ export const isTablet = () => { return window.innerWidth < 599; }; + +export const headerHeight = 64; // px diff --git a/src/constants/dom/links.ts b/src/constants/dom/links.ts new file mode 100644 index 00000000..52933eae --- /dev/null +++ b/src/constants/dom/links.ts @@ -0,0 +1,15 @@ +export const NEW_COMMENT_ANCHOR_NAME = 'new-comment'; +export const COMMENT_ANCHOR_PREFIX = 'comment'; + +export const getCommentId = (id: number) => + [COMMENT_ANCHOR_PREFIX, id].join('-'); + +export const getNewCommentAnchor = (url: string) => + [url, NEW_COMMENT_ANCHOR_NAME].join('#'); + +export const getCommentAnchor = (url: string, commentId: number) => + [url, getCommentId(commentId)].join('#'); + +export const isCommentAnchor = (hash: string | undefined) => + hash?.startsWith(COMMENT_ANCHOR_PREFIX) || + hash?.startsWith(NEW_COMMENT_ANCHOR_NAME); diff --git a/src/constants/modal/index.ts b/src/constants/modal/index.ts index 820f9962..f72114fd 100644 --- a/src/constants/modal/index.ts +++ b/src/constants/modal/index.ts @@ -1,3 +1,5 @@ +import { lazy } from 'react'; + import { LoginDialog } from '~/containers/auth/LoginDialog'; import { LoginSocialRegisterDialog } from '~/containers/auth/LoginSocialRegisterDialog'; import { RestorePasswordDialog } from '~/containers/auth/RestorePasswordDialog'; @@ -6,9 +8,14 @@ import { TelegramAttachDialog } from '~/containers/auth/TelegramAttachDialog'; import { EditorCreateDialog } from '~/containers/dialogs/EditorCreateDialog'; import { EditorEditDialog } from '~/containers/dialogs/EditorEditDialog'; import { LoadingDialog } from '~/containers/dialogs/LoadingDialog'; -import { PhotoSwipe } from '~/containers/dialogs/PhotoSwipe'; import { TestDialog } from '~/containers/dialogs/TestDialog'; +const PhotoSwipe = lazy(() => + import('~/containers/dialogs/PhotoSwipe').then((it) => ({ + default: it.PhotoSwipe, + })), +); + export enum Dialog { Login = 'Login', Register = 'Register', diff --git a/src/constants/sidebar/index.ts b/src/constants/sidebar/index.ts index 587b493e..abd3390a 100644 --- a/src/constants/sidebar/index.ts +++ b/src/constants/sidebar/index.ts @@ -1,4 +1,3 @@ - export enum SidebarName { Settings = 'settings', Tag = 'tag', diff --git a/src/constants/themes/index.ts b/src/constants/themes/index.ts index 3956b523..b32d2266 100644 --- a/src/constants/themes/index.ts +++ b/src/constants/themes/index.ts @@ -1,6 +1,7 @@ export enum Theme { Default = 'Default', Horizon = 'Horizon', + Sansevieria = 'Sansevieria', } interface ThemeColors { @@ -17,7 +18,7 @@ export const themeColors: Record = { 'linear-gradient(165deg, #ff7549 -50%, #ff3344 150%)', 'linear-gradient(170deg, #582cd0, #592071)', ], - background: 'url(\'/images/noise_top.png\') 0% 0% #23201f', + background: "url('/images/noise_top.png') 0% 0% #23201f", }, [Theme.Horizon]: { name: 'Веспера', @@ -28,4 +29,13 @@ export const themeColors: Record = { ], background: 'url("/images/horizon_bg.svg") 50% 50% / cover rgb(28, 30, 38)', }, + [Theme.Sansevieria]: { + name: 'Сансевирия', + colors: [ + 'linear-gradient(165deg, #f4e7aa -50%, #a23500 150%)', + 'linear-gradient(165deg, #ff7e56 -50%, #280003 150%)', + 'linear-gradient(170deg, #476695, #22252d)', + ], + background: '#1f2625', + }, }; diff --git a/src/constants/urls.ts b/src/constants/urls.ts index 50da82fe..9bed6e30 100644 --- a/src/constants/urls.ts +++ b/src/constants/urls.ts @@ -37,7 +37,7 @@ export const imagePresets = { flow_horizontal: 'flow_horizontal', } as const; -export type ImagePreset = typeof imagePresets[keyof typeof imagePresets]; +export type ImagePreset = (typeof imagePresets)[keyof typeof imagePresets]; export const imageSrcSets: Partial> = { [imagePresets[1600]]: 1600, @@ -49,7 +49,7 @@ export const imageSrcSets: Partial> = { export const flowDisplayToPreset: Record< FlowDisplayVariant, - typeof imagePresets[keyof typeof imagePresets] + (typeof imagePresets)[keyof typeof imagePresets] > = { single: 'flow_square', quadro: 'flow_square', diff --git a/src/containers/boris/BorisSuperpowers/ssr.tsx b/src/containers/boris/BorisSuperpowers/ssr.tsx index c8104207..5779c5c2 100644 --- a/src/containers/boris/BorisSuperpowers/ssr.tsx +++ b/src/containers/boris/BorisSuperpowers/ssr.tsx @@ -3,10 +3,12 @@ import dynamic from 'next/dynamic'; import type { BorisSuperpowersProps } from './index'; export const BorisSuperPowersSSR = dynamic( - () => import('~/containers/boris/BorisSuperpowers/index') - .then(it => it.BorisSuperpowers), + () => + import('~/containers/boris/BorisSuperpowers/index').then( + (it) => it.BorisSuperpowers, + ), { ssr: false, loading: () =>
, - } + }, ); diff --git a/src/containers/dialogs/EditorCreateDialog/index.tsx b/src/containers/dialogs/EditorCreateDialog/index.tsx index 50b16dd4..5abd35aa 100644 --- a/src/containers/dialogs/EditorCreateDialog/index.tsx +++ b/src/containers/dialogs/EditorCreateDialog/index.tsx @@ -8,7 +8,7 @@ import { DialogComponentProps } from '~/types/modal'; import { values } from '~/utils/ramda'; export interface EditorCreateDialogProps extends DialogComponentProps { - type: typeof NODE_TYPES[keyof typeof NODE_TYPES]; + type: (typeof NODE_TYPES)[keyof typeof NODE_TYPES]; isInLab: boolean; } diff --git a/src/containers/dialogs/EditorDialog/constants/index.ts b/src/containers/dialogs/EditorDialog/constants/index.ts index dd9d0bc0..ce392b50 100644 --- a/src/containers/dialogs/EditorDialog/constants/index.ts +++ b/src/containers/dialogs/EditorDialog/constants/index.ts @@ -11,7 +11,7 @@ import { TextEditor } from '../components/TextEditor'; import { VideoEditor } from '../components/VideoEditor'; export const NODE_EDITORS: Record< - typeof NODE_TYPES[keyof typeof NODE_TYPES], + (typeof NODE_TYPES)[keyof typeof NODE_TYPES], FC > = { [NODE_TYPES.IMAGE]: ImageEditor, @@ -22,7 +22,7 @@ export const NODE_EDITORS: Record< }; export const NODE_EDITOR_DATA: Record< - typeof NODE_TYPES[keyof typeof NODE_TYPES], + (typeof NODE_TYPES)[keyof typeof NODE_TYPES], Partial > = { [NODE_TYPES.TEXT]: { diff --git a/src/containers/dialogs/Modal/index.tsx b/src/containers/dialogs/Modal/index.tsx index 7bda9a70..090a7a6a 100644 --- a/src/containers/dialogs/Modal/index.tsx +++ b/src/containers/dialogs/Modal/index.tsx @@ -1,7 +1,8 @@ -import { FC, createElement } from 'react'; +import { FC, createElement, Suspense } from 'react'; import { observer } from 'mobx-react-lite'; +import { LoaderCircle } from '~/components/common/LoaderCircle'; import { ModalWrapper } from '~/components/common/ModalWrapper'; import { DIALOG_CONTENT } from '~/constants/modal'; import { useModalStore } from '~/store/modal/useModalStore'; @@ -18,10 +19,12 @@ const Modal: FC = observer(() => { return ( - {createElement(DIALOG_CONTENT[current!]! as any, { - onRequestClose: hide, - ...props, - })} + }> + {createElement(DIALOG_CONTENT[current!]! as any, { + onRequestClose: hide, + ...props, + })} + ); }); diff --git a/src/containers/dialogs/PhotoSwipe/index.tsx b/src/containers/dialogs/PhotoSwipe/index.tsx index 3a285696..0872b2cc 100644 --- a/src/containers/dialogs/PhotoSwipe/index.tsx +++ b/src/containers/dialogs/PhotoSwipe/index.tsx @@ -1,10 +1,12 @@ -import { useEffect, useRef, VFC } from 'react'; +import { useEffect, useRef } from 'react'; + +import 'photoswipe/style.css'; -import classNames from 'classnames'; import { observer } from 'mobx-react-lite'; -import PhotoSwipeUI_Default from 'photoswipe/dist/photoswipe-ui-default.js'; -import PhotoSwipeJs from 'photoswipe/dist/photoswipe.js'; +import PSWP from 'photoswipe'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { Icon } from '~/components/common/Icon'; import { imagePresets } from '~/constants/urls'; import { useWindowSize } from '~/hooks/dom/useWindowSize'; import { useModal } from '~/hooks/modal/useModal'; @@ -13,125 +15,55 @@ import { DialogComponentProps } from '~/types/modal'; import { getURL } from '~/utils/dom'; import styles from './styles.module.scss'; - -export interface PhotoSwipeProps extends DialogComponentProps { +export interface Props extends DialogComponentProps { items: IFile[]; index: number; } -const PhotoSwipe: VFC = observer(({ index, items }) => { - let ref = useRef(null); +const arrowNextSVG = renderToStaticMarkup(); +const arrowPrevSVG = renderToStaticMarkup(); +const closeSVG = renderToStaticMarkup(); + +const padding = { top: 10, left: 10, right: 10, bottom: 10 } as const; + +const PhotoSwipe = observer(({ index, items }: Props) => { const { hideModal } = useModal(); const { isTablet } = useWindowSize(); + const pswp = useRef(new PSWP()); useEffect(() => { - new Promise(async (resolve) => { - const images = await Promise.all( - items.map( - (file) => - new Promise((resolve) => { - const src = getURL( - file, - isTablet ? imagePresets[900] : imagePresets[1600], - ); + const dataSource = items.map((file) => ({ + src: getURL(file, imagePresets[1600]), + width: file.metadata?.width, + height: file.metadata?.height, + })); - if (file.metadata?.width && file.metadata.height) { - resolve({ - src, - w: file.metadata.width, - h: file.metadata.height, - }); + pswp.current.options = { + ...pswp.current.options, + dataSource, + index: index || 0, + closeOnVerticalDrag: true, + padding, + mainClass: styles.wrap, + zoom: false, + counter: false, + bgOpacity: 0.1, + arrowNextSVG, + arrowPrevSVG, + closeSVG, + }; - return; - } + pswp.current.on('closingAnimationEnd', hideModal); + pswp.current.init(); - const img = new Image(); - - img.onload = () => { - resolve({ - src, - h: img.naturalHeight, - w: img.naturalWidth, - }); - }; - - img.onerror = () => { - resolve({}); - }; - - img.src = getURL(file, imagePresets[1600]); - }), - ), - ); - - resolve(images); - }).then((images) => { - const ps = new PhotoSwipeJs(ref.current, PhotoSwipeUI_Default, images, { - index: index || 0, - closeOnScroll: false, - history: false, - }); - - ps.init(); - ps.listen('destroy', hideModal); - ps.listen('close', hideModal); - }); + return () => { + pswp.current?.off('close', hideModal); + // eslint-disable-next-line react-hooks/exhaustive-deps + pswp.current?.destroy(); + }; }, [hideModal, items, index, isTablet]); - return ( -