From 5ef19f49c51e214f048fb3a9b496934d34a55a27 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Wed, 22 Jan 2025 14:36:26 +0700 Subject: [PATCH 01/29] change user profile page layout --- src/constants/api.ts | 2 +- .../profile/ProfilePageLeft/index.tsx | 44 ++++++++++++------- .../ProfilePageLeft/styles.module.scss | 18 +++----- src/layouts/ProfileLayout/index.tsx | 14 ++++-- src/layouts/ProfileLayout/styles.module.scss | 17 +++++-- 5 files changed, 61 insertions(+), 34 deletions(-) diff --git a/src/constants/api.ts b/src/constants/api.ts index b9a287f4..6fa89637 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -11,7 +11,7 @@ export const API = { 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/containers/profile/ProfilePageLeft/index.tsx b/src/containers/profile/ProfilePageLeft/index.tsx index 62734d49..983ca368 100644 --- a/src/containers/profile/ProfilePageLeft/index.tsx +++ b/src/containers/profile/ProfilePageLeft/index.tsx @@ -1,6 +1,7 @@ import { FC } from 'react'; import { Avatar } from '~/components/common/Avatar'; +import { Card } from '~/components/common/Card'; import { Placeholder } from '~/components/placeholders/Placeholder'; import { imagePresets } from '~/constants/urls'; import { IUser } from '~/types/auth'; @@ -11,28 +12,39 @@ interface Props { profile: IUser; isLoading: boolean; username: string; + description: string; } -const ProfilePageLeft: FC<Props> = ({ username, profile, isLoading }) => { +const ProfilePageLeft: FC<Props> = ({ + username, + profile, + description, + isLoading, +}) => { return ( - <div className={styles.wrap}> - <Avatar - username={username} - url={profile?.photo?.url} - className={styles.avatar} - preset={imagePresets['600']} - /> + <Card className={styles.wrap} elevation={0} seamless> + <Card seamless> + <Avatar + username={username} + url={profile?.photo?.url} + className={styles.avatar} + preset={imagePresets['600']} + /> + <div className={styles.region}> + <div className={styles.name}> + {isLoading ? <Placeholder /> : profile?.fullname} + </div> + + <div className={styles.username}> + {isLoading ? <Placeholder /> : `~${profile?.username}`} + </div> + </div> + </Card> <div className={styles.region}> - <div className={styles.name}> - {isLoading ? <Placeholder /> : profile?.fullname} - </div> - - <div className={styles.username}> - {isLoading ? <Placeholder /> : `~${profile?.username}`} - </div> + <div className={styles.description}>{description}</div> </div> - </div> + </Card> ); }; diff --git a/src/containers/profile/ProfilePageLeft/styles.module.scss b/src/containers/profile/ProfilePageLeft/styles.module.scss index b94b662c..fc3f0347 100644 --- a/src/containers/profile/ProfilePageLeft/styles.module.scss +++ b/src/containers/profile/ProfilePageLeft/styles.module.scss @@ -1,29 +1,26 @@ @import 'src/styles/variables'; .wrap { - @include outer_shadow; - @include blur; - - padding: $gap $gap $gap * 2; box-sizing: border-box; display: flex; align-items: stretch; justify-content: stretch; flex-direction: column; - height: 100%; border-radius: $radius; } +.top { + padding: 0; +} + .avatar { width: 100%; height: 0; padding-bottom: 100%; - margin-bottom: $gap * 2; } .region { - width: 100%; - text-align: center; + padding: $gap; } .name { @@ -44,8 +41,7 @@ .description { @include clamp(3, 21px * 3); - line-height: 21px; font: $font_14_regular; - margin-top: $gap * 3; - display: none; + line-height: 1.25em; + opacity: 0.5; } diff --git a/src/layouts/ProfileLayout/index.tsx b/src/layouts/ProfileLayout/index.tsx index fcdc8d66..48c0afdc 100644 --- a/src/layouts/ProfileLayout/index.tsx +++ b/src/layouts/ProfileLayout/index.tsx @@ -2,7 +2,10 @@ import { FC } from 'react'; import { observer } from 'mobx-react-lite'; +import { Card } from '~/components/common/Card'; import { Container } from '~/components/common/Container'; +import { Sticky } from '~/components/common/Sticky'; +import { FlowGrid } from '~/containers/flow/FlowGrid'; import { ProfilePageLeft } from '~/containers/profile/ProfilePageLeft'; import { useUser } from '~/hooks/auth/useUser'; import { useGetProfile } from '~/hooks/profile/useGetProfile'; @@ -24,16 +27,21 @@ const ProfileLayout: FC<Props> = observer(({ username }) => { <Container className={styles.wrap}> <div className={styles.grid}> <div className={styles.stamp}> - <div className={styles.row}> + <Sticky> <ProfilePageLeft + description={profile.description} profile={profile} username={username} isLoading={isLoading} /> - </div> + </Sticky> </div> - <div>here should be grid</div> + <Card className={styles.description}>{profile.description}</Card> + + <div className={styles.nodes}> + <FlowGrid nodes={nodes} user={user} onChangeCellView={() => {}} /> + </div> </div> </Container> ); diff --git a/src/layouts/ProfileLayout/styles.module.scss b/src/layouts/ProfileLayout/styles.module.scss index 5684cc64..5de4d336 100644 --- a/src/layouts/ProfileLayout/styles.module.scss +++ b/src/layouts/ProfileLayout/styles.module.scss @@ -8,7 +8,10 @@ } .grid { - @include flow_grid; + grid-template-columns: 250px 5fr; + display: grid; + column-gap: $gap; + row-gap: $gap; } .row { @@ -18,10 +21,18 @@ .description { font: $font_14_semibold; - text-align: center; - padding: $gap * 2 $gap; } .stamp { grid-row-end: span 2; } + +.nodes { + @include flow_grid(); +} + +.content { + display: flex; + flex-direction: column; + gap: $gap; +} From 0e4d2bf44d73050172a55bd3342d73729546ba00 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Fri, 24 Jan 2025 17:46:24 +0700 Subject: [PATCH 02/29] scroll to new comments from recent and notifications --- .../common/NodeHorizontalCard/index.tsx | 46 ++++++------- .../NotificationComment/index.tsx | 7 +- src/constants/dom/links.ts | 15 ++++ .../NodeComments/components/Comment/index.tsx | 29 ++++---- .../components/Comment/styles.module.scss | 6 ++ src/containers/node/NodeComments/index.tsx | 68 ++++++++++++++----- .../node/NodeComments/styles.module.scss | 6 ++ src/types/notifications/index.ts | 1 + 8 files changed, 124 insertions(+), 54 deletions(-) create mode 100644 src/constants/dom/links.ts diff --git a/src/components/common/NodeHorizontalCard/index.tsx b/src/components/common/NodeHorizontalCard/index.tsx index 3c0847be..d132ee90 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,30 @@ interface Props { onClick?: MouseEventHandler; } -const NodeHorizontalCard: FC<Props> = ({ node, hasNew, onClick }) => { - return ( - <Anchor - key={node.id} - className={styles.item} - href={URLS.NODE_URL(node.id)} - onClick={onClick} +const NodeHorizontalCard: FC<Props> = ({ node, hasNew, onClick }) => ( + <Anchor + key={node.id} + className={styles.item} + href={getNewCommentAnchor(URLS.NODE_URL(node.id))} + onClick={onClick} + > + <div + className={classNames(styles.thumb, { + [styles.new]: hasNew, + [styles.lab]: !node.is_promoted, + })} > - <div - className={classNames(styles.thumb, { - [styles.new]: hasNew, - [styles.lab]: !node.is_promoted, - })} - > - <NodeThumbnail item={node} /> - </div> + <NodeThumbnail item={node} /> + </div> - <div className={styles.info}> - <div className={styles.title}>{node.title || '...'}</div> + <div className={styles.info}> + <div className={styles.title}>{node.title || '...'}</div> - <div className={styles.comment}> - <span>{getPrettyDate(node.created_at)}</span> - </div> + <div className={styles.comment}> + <span>{getPrettyDate(node.created_at)}</span> </div> - </Anchor> - ); -}; + </div> + </Anchor> +); export { NodeHorizontalCard }; 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<NotificationCommentProps> = ({ item, isNew }) => ( - <Anchor href={item.url} className={styles.link}> + <Anchor + href={getCommentAnchor(item.url, item.itemId)} + className={styles.link} + > <div className={classNames(styles.message, { [styles.new]: isNew })}> <div className={styles.icon}> <Avatar 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/containers/node/NodeComments/components/Comment/index.tsx b/src/containers/node/NodeComments/components/Comment/index.tsx index 121d8051..1e3f2b60 100644 --- a/src/containers/node/NodeComments/components/Comment/index.tsx +++ b/src/containers/node/NodeComments/components/Comment/index.tsx @@ -8,6 +8,7 @@ import { CommentWrapper } from '~/containers/comments/CommentWrapper'; import { IComment, ICommentGroup, IFile } from '~/types'; import { CommendDeleted } from '../../../../../components/node/CommendDeleted'; +import { getCommentId } from '../../../../../constants/dom/links'; import { CommentContent } from './components/CommentContent'; import { CommentDistance } from './components/CommentDistance'; @@ -83,18 +84,22 @@ const Comment: FC<Props> = memo( ); return ( - <CommentContent - prefix={prefix} - saveComment={saveComment} - nodeId={nodeId} - comment={comment} - canEdit={!!canEdit} - canLike={!!canLike} - onLike={() => onLike(comment.id, !comment.liked)} - onDelete={(val: boolean) => onDelete(comment.id, val)} - onShowImageModal={onShowImageModal} - key={comment.id} - /> + <> + <a id={getCommentId(comment.id)} className={styles.anchor} /> + + <CommentContent + prefix={prefix} + saveComment={saveComment} + nodeId={nodeId} + comment={comment} + canEdit={!!canEdit} + canLike={!!canLike} + onLike={() => onLike(comment.id, !comment.liked)} + onDelete={(val: boolean) => onDelete(comment.id, val)} + onShowImageModal={onShowImageModal} + key={comment.id} + /> + </> ); })} </div> diff --git a/src/containers/node/NodeComments/components/Comment/styles.module.scss b/src/containers/node/NodeComments/components/Comment/styles.module.scss index 01c1e131..92cd93b4 100644 --- a/src/containers/node/NodeComments/components/Comment/styles.module.scss +++ b/src/containers/node/NodeComments/components/Comment/styles.module.scss @@ -15,3 +15,9 @@ .highlighted { box-shadow: $color_primary 0 0 0px 2px; } + +.anchor { + display: block; + position: relative; + top: -($header_height * 2); +} diff --git a/src/containers/node/NodeComments/index.tsx b/src/containers/node/NodeComments/index.tsx index acba75b0..a1d93732 100644 --- a/src/containers/node/NodeComments/index.tsx +++ b/src/containers/node/NodeComments/index.tsx @@ -1,9 +1,13 @@ -import { FC, useMemo } from 'react'; +import { FC, useEffect, useMemo } from 'react'; import { observer } from 'mobx-react-lite'; import { LoadMoreButton } from '~/components/input/LoadMoreButton'; import { ANNOUNCE_USER_ID, BORIS_NODE_ID } from '~/constants/boris/constants'; +import { + isCommentAnchor, + NEW_COMMENT_ANCHOR_NAME, +} from '~/constants/dom/links'; import { Comment } from '~/containers/node/NodeComments/components/Comment'; import { useGrouppedComments } from '~/hooks/node/useGrouppedComments'; import { ICommentGroup } from '~/types'; @@ -18,6 +22,11 @@ interface Props { order: 'ASC' | 'DESC'; } +const isFirstGroupWithNewCommentt = ( + group: ICommentGroup, + prevGroup: ICommentGroup | undefined, +) => group.hasNew && (!prevGroup || !prevGroup.hasNew); + const NodeComments: FC<Props> = observer(({ order }) => { const user = useUserContext(); const { node } = useNodeContext(); @@ -35,7 +44,7 @@ const NodeComments: FC<Props> = observer(({ order }) => { onSaveComment, } = useCommentContext(); - const groupped: ICommentGroup[] = useGrouppedComments( + const groupped = useGrouppedComments( comments, order, lastSeenCurrent ?? undefined, @@ -59,26 +68,49 @@ const NodeComments: FC<Props> = observer(({ order }) => { return null; } + useEffect(() => { + const anchor = location.hash?.replace('#', ''); + + if (!isLoading && isCommentAnchor(anchor)) { + setTimeout( + () => + document + .getElementById(anchor) + ?.scrollIntoView({ behavior: 'smooth' }), + 300, + ); + } + }, [isLoading]); + return ( <div className={styles.wrap}> {order === 'DESC' && more} - {groupped.map((group) => ( - <Comment - nodeId={node.id!} - key={group.ids.join()} - group={group} - highlighted={ - node.id === BORIS_NODE_ID && group.user.id === ANNOUNCE_USER_ID - } - onLike={onLike} - canLike={canLikeComment(group, user)} - canEdit={canEditComment(group, user)} - onDelete={onDeleteComment} - onShowImageModal={onShowImageModal} - isSame={group.user.id === user.id} - saveComment={onSaveComment} - /> + {groupped.map((group, index) => ( + <> + {isFirstGroupWithNewCommentt(group, groupped.at(index - 1)) && ( + <a + id={NEW_COMMENT_ANCHOR_NAME} + className={styles.newCommentAnchor} + /> + )} + + <Comment + nodeId={node.id!} + key={group.ids.join()} + group={group} + highlighted={ + node.id === BORIS_NODE_ID && group.user.id === ANNOUNCE_USER_ID + } + onLike={onLike} + canLike={canLikeComment(group, user)} + canEdit={canEditComment(group, user)} + onDelete={onDeleteComment} + onShowImageModal={onShowImageModal} + isSame={group.user.id === user.id} + saveComment={onSaveComment} + /> + </> ))} {order === 'ASC' && more} diff --git a/src/containers/node/NodeComments/styles.module.scss b/src/containers/node/NodeComments/styles.module.scss index 3cd52e5a..1d006bd7 100644 --- a/src/containers/node/NodeComments/styles.module.scss +++ b/src/containers/node/NodeComments/styles.module.scss @@ -13,3 +13,9 @@ .more { margin-bottom: $gap; } + +.newCommentAnchor { + position: relative; + top: -($header_height * 2); + display: block; +} \ No newline at end of file diff --git a/src/types/notifications/index.ts b/src/types/notifications/index.ts index 5a34538f..c23b3f90 100644 --- a/src/types/notifications/index.ts +++ b/src/types/notifications/index.ts @@ -2,6 +2,7 @@ import { ShallowUser } from '../auth'; export interface NotificationItem { id: number; + itemId: number; url: string; type: NotificationType; title: string; From ba0604ab9dacd8abba45b14ddfa53fb2105eba2d Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Fri, 24 Jan 2025 17:51:59 +0700 Subject: [PATCH 03/29] add eslint-plugin-prettier --- .eslintrc.js | 20 ++-- package.json | 3 +- src/components/common/Avatar/index.tsx | 2 +- src/constants/comment.ts | 2 +- src/constants/sidebar/index.ts | 1 - src/constants/themes/index.ts | 2 +- src/constants/urls.ts | 4 +- src/containers/boris/BorisSuperpowers/ssr.tsx | 8 +- .../dialogs/EditorCreateDialog/index.tsx | 2 +- .../dialogs/EditorDialog/constants/index.ts | 4 +- src/containers/main/Header/ssr.tsx | 11 ++- .../components/SubmitBar/ssr.ts | 5 +- .../sidebars/ProfileSidebar/index.tsx | 2 +- src/hooks/auth/useLastSeenBoris.ts | 2 +- src/hooks/auth/useLoginLogoutRestore.ts | 2 +- src/hooks/auth/useRestoreCode.ts | 5 +- src/hooks/auth/useRestorePasswordForm.ts | 16 ++- src/hooks/auth/useRestoreRequestForm.ts | 4 +- src/hooks/auth/useSessionCookie.ts | 2 +- src/hooks/auth/useSocialRegisterForm.ts | 8 +- src/hooks/auth/useUserActiveStatus.ts | 5 +- src/hooks/boris/useBorisStats.ts | 12 ++- src/hooks/color/useColorFromString.ts | 13 ++- src/hooks/color/useColorGradientFromString.ts | 2 +- src/hooks/data/usePersistedState.ts | 5 +- src/hooks/dom/useFocusEvent.ts | 9 +- src/hooks/dom/useFormatWrapper.ts | 17 ++-- src/hooks/dom/useInfiniteLoader.ts | 3 +- src/hooks/dom/useInputPasteUpload.ts | 4 +- src/hooks/dom/usePopperModifiers.ts | 8 +- src/hooks/dom/useScrollHeight.ts | 2 +- src/hooks/dom/useScrollToTop.ts | 2 +- src/hooks/dom/useScrollTop.ts | 4 +- src/hooks/flow/useFlowCellControls.ts | 7 +- src/hooks/flow/useFlowSetCellView.ts | 2 +- src/hooks/lab/useGetLabStats.ts | 23 +++-- src/hooks/messages/useMessages.ts | 2 +- src/hooks/modal/useModal.ts | 6 +- src/hooks/modal/useShowModal.ts | 2 +- src/hooks/navigation/useImageModal.ts | 2 +- src/hooks/navigation/useNavigation.ts | 2 +- src/hooks/node/useCreateNode.ts | 8 +- src/hooks/node/useGrouppedComments.ts | 6 +- src/hooks/node/useNodeActions.ts | 18 ++-- src/hooks/node/useNodeAudios.ts | 7 +- src/hooks/node/useNodeFormFormik.ts | 43 ++++---- src/hooks/node/useNodeImages.ts | 7 +- src/hooks/node/useUpdateNode.ts | 2 +- src/hooks/profile/useGetProfile.ts | 4 +- src/hooks/search/useSearch.ts | 24 +++-- src/hooks/tag/useTagAutocomplete.ts | 2 +- src/hooks/tag/useTagNodes.ts | 23 +++-- src/hooks/updates/useUpdates.ts | 2 +- src/reportWebVitals.js | 2 +- src/store/flow/FlowStore.ts | 6 +- src/store/metadata/MetadataStore.tsx | 12 ++- src/types/index.ts | 4 +- src/types/sidebar/index.ts | 15 ++- src/utils/color.ts | 41 ++++++-- src/utils/config/index.ts | 3 +- src/utils/fn.ts | 98 +++++++++++-------- src/utils/providers/ProfileProvider.tsx | 9 +- src/utils/splitText.ts | 10 +- src/utils/ssr/getPageTitle.ts | 2 +- src/utils/tag.ts | 10 +- src/utils/trans.ts | 5 +- src/utils/uploader.ts | 19 ++-- src/utils/validators.ts | 3 +- yarn.lock | 46 ++++++++- 69 files changed, 419 insertions(+), 249 deletions(-) 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/package.json b/package.json index 10e5589c..133af634 100644 --- a/package.json +++ b/package.json @@ -92,13 +92,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/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<HTMLDivElement, Props>( 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/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..ab72f641 100644 --- a/src/constants/themes/index.ts +++ b/src/constants/themes/index.ts @@ -17,7 +17,7 @@ export const themeColors: Record<Theme, ThemeColors> = { '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: 'Веспера', 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<Record<ImagePreset, number>> = { [imagePresets[1600]]: 1600, @@ -49,7 +49,7 @@ export const imageSrcSets: Partial<Record<ImagePreset, number>> = { 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<BorisSuperpowersProps>( - () => import('~/containers/boris/BorisSuperpowers/index') - .then(it => it.BorisSuperpowers), + () => + import('~/containers/boris/BorisSuperpowers/index').then( + (it) => it.BorisSuperpowers, + ), { ssr: false, loading: () => <div />, - } + }, ); 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<NodeEditorProps> > = { [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<INode> > = { [NODE_TYPES.TEXT]: { diff --git a/src/containers/main/Header/ssr.tsx b/src/containers/main/Header/ssr.tsx index 5b6e4b6a..1c698535 100644 --- a/src/containers/main/Header/ssr.tsx +++ b/src/containers/main/Header/ssr.tsx @@ -4,9 +4,12 @@ import type { HeaderProps } from '~/containers/main/Header/index'; import styles from './styles.module.scss'; -export const HeaderSSR = dynamic<HeaderProps>(() => import('./index').then(it => it.Header), { - ssr: false, - loading: () => <div className={styles.wrap} />, -}); +export const HeaderSSR = dynamic<HeaderProps>( + () => import('./index').then((it) => it.Header), + { + ssr: false, + loading: () => <div className={styles.wrap} />, + }, +); export const HeaderSSRPlaceholder = () => <div className={styles.wrap} />; diff --git a/src/containers/main/SubmitBarRouter/components/SubmitBar/ssr.ts b/src/containers/main/SubmitBarRouter/components/SubmitBar/ssr.ts index 50a28f1e..7304ab41 100644 --- a/src/containers/main/SubmitBarRouter/components/SubmitBar/ssr.ts +++ b/src/containers/main/SubmitBarRouter/components/SubmitBar/ssr.ts @@ -3,5 +3,6 @@ import dynamic from 'next/dynamic'; import type { SubmitBarProps } from './index'; export const SubmitBarSSR = dynamic<SubmitBarProps>( - () => import('./index').then(it => it.SubmitBar), - { ssr: false }); + () => import('./index').then((it) => it.SubmitBar), + { ssr: false }, +); diff --git a/src/containers/sidebars/ProfileSidebar/index.tsx b/src/containers/sidebars/ProfileSidebar/index.tsx index ff623d89..26bb13e5 100644 --- a/src/containers/sidebars/ProfileSidebar/index.tsx +++ b/src/containers/sidebars/ProfileSidebar/index.tsx @@ -14,7 +14,7 @@ import type { SidebarComponentProps } from '~/types/sidebar'; import { isNil } from '~/utils/ramda'; const tabs = ['profile', 'notifications', 'bookmarks'] as const; -type TabName = typeof tabs[number]; +type TabName = (typeof tabs)[number]; interface SettingsSidebarProps extends SidebarComponentProps<SidebarName.Settings> { diff --git a/src/hooks/auth/useLastSeenBoris.ts b/src/hooks/auth/useLastSeenBoris.ts index c8e08bbe..3cc5a5bb 100644 --- a/src/hooks/auth/useLastSeenBoris.ts +++ b/src/hooks/auth/useLastSeenBoris.ts @@ -10,7 +10,7 @@ export const useLastSeenBoris = () => { async (date: string) => { await update({ last_seen_boris: date }, false); }, - [update] + [update], ); return { setLastSeen, lastSeen }; diff --git a/src/hooks/auth/useLoginLogoutRestore.ts b/src/hooks/auth/useLoginLogoutRestore.ts index 4e438f16..76e74458 100644 --- a/src/hooks/auth/useLoginLogoutRestore.ts +++ b/src/hooks/auth/useLoginLogoutRestore.ts @@ -20,7 +20,7 @@ export const useLoginLogoutRestore = () => { showToastInfo(getRandomPhrase('WELCOME')); return result.user; }, - [auth] + [auth], ); return { logout, login }; diff --git a/src/hooks/auth/useRestoreCode.ts b/src/hooks/auth/useRestoreCode.ts index 8e957b58..74e3fa68 100644 --- a/src/hooks/auth/useRestoreCode.ts +++ b/src/hooks/auth/useRestoreCode.ts @@ -5,8 +5,9 @@ import { API } from '~/constants/api'; import { getErrorMessage } from '~/utils/errors/getErrorMessage'; export const useRestoreCode = (code: string) => { - const { data, isValidating, error } = useSWR(API.USER.REQUEST_CODE(code), () => - apiCheckRestoreCode({ code }) + const { data, isValidating, error } = useSWR( + API.USER.REQUEST_CODE(code), + () => apiCheckRestoreCode({ code }), ); const codeUser = data?.user; diff --git a/src/hooks/auth/useRestorePasswordForm.ts b/src/hooks/auth/useRestorePasswordForm.ts index 0373dfd0..0405993d 100644 --- a/src/hooks/auth/useRestorePasswordForm.ts +++ b/src/hooks/auth/useRestorePasswordForm.ts @@ -18,7 +18,7 @@ const validationSchema = object({ .test( 'sameAsPassword', 'Должен совпадать с паролем', - (val, ctx) => val === ctx.parent.newPassword + (val, ctx) => val === ctx.parent.newPassword, ), }); @@ -26,15 +26,21 @@ export type RestorePasswordData = Asserts<typeof validationSchema>; export const useRestorePasswordForm = ( code: string, - fetcher: (props: { code: string; password: string }) => Promise<{ token: string; user: IUser }>, - onSuccess: () => void + fetcher: (props: { + code: string; + password: string; + }) => Promise<{ token: string; user: IUser }>, + onSuccess: () => void, ) => { const auth = useAuthStore(); const onSubmit = useCallback<FormikConfig<RestorePasswordData>['onSubmit']>( async (values, { setErrors }) => { try { - const { token, user } = await fetcher({ password: values.newPassword, code }); + const { token, user } = await fetcher({ + password: values.newPassword, + code, + }); auth.setUser(user); auth.setToken(token); onSuccess(); @@ -47,7 +53,7 @@ export const useRestorePasswordForm = ( } } }, - [onSuccess, fetcher, code, auth] + [onSuccess, fetcher, code, auth], ); return useFormik<RestorePasswordData>({ diff --git a/src/hooks/auth/useRestoreRequestForm.ts b/src/hooks/auth/useRestoreRequestForm.ts index 6dc63a37..7386a9ac 100644 --- a/src/hooks/auth/useRestoreRequestForm.ts +++ b/src/hooks/auth/useRestoreRequestForm.ts @@ -15,7 +15,7 @@ type RestoreRequestData = Asserts<typeof validationSchema>; export const useRestoreRequestForm = ( fetcher: (field: string) => Promise<unknown>, - onSuccess: () => void + onSuccess: () => void, ) => { const onSubmit = useCallback<FormikConfig<RestoreRequestData>['onSubmit']>( async (values, { setErrors }) => { @@ -31,7 +31,7 @@ export const useRestoreRequestForm = ( } } }, - [fetcher, onSuccess] + [fetcher, onSuccess], ); return useFormik({ diff --git a/src/hooks/auth/useSessionCookie.ts b/src/hooks/auth/useSessionCookie.ts index 0d7ce68d..eeeab746 100644 --- a/src/hooks/auth/useSessionCookie.ts +++ b/src/hooks/auth/useSessionCookie.ts @@ -13,6 +13,6 @@ export const useSessionCookie = () => { autorun(() => { setCookie('session', auth.token, 30); }), - [auth] + [auth], ); }; diff --git a/src/hooks/auth/useSocialRegisterForm.ts b/src/hooks/auth/useSocialRegisterForm.ts index 3acad2d0..7ea8c26b 100644 --- a/src/hooks/auth/useSocialRegisterForm.ts +++ b/src/hooks/auth/useSocialRegisterForm.ts @@ -9,9 +9,7 @@ import { showErrorToast } from '~/utils/errors/showToast'; const validationSchema = object({ username: string().required(ERRORS.REQUIRED), - password: string() - .required(ERRORS.REQUIRED) - .min(6, ERRORS.PASSWORD_IS_SHORT), + password: string().required(ERRORS.REQUIRED).min(6, ERRORS.PASSWORD_IS_SHORT), }); type SocialRegisterData = Asserts<typeof validationSchema>; @@ -23,7 +21,7 @@ export const useSocialRegisterForm = ( username: string; password: string; }) => Promise<{ token: string }>, - onSuccess: (token: string) => void + onSuccess: (token: string) => void, ) => { const onSubmit = useCallback<FormikConfig<SocialRegisterData>['onSubmit']>( async (values, { setErrors }) => { @@ -43,7 +41,7 @@ export const useSocialRegisterForm = ( } } }, - [token, onSuccess, fetcher] + [token, onSuccess, fetcher], ); return useFormik<SocialRegisterData>({ diff --git a/src/hooks/auth/useUserActiveStatus.ts b/src/hooks/auth/useUserActiveStatus.ts index ba90caa9..ea364e88 100644 --- a/src/hooks/auth/useUserActiveStatus.ts +++ b/src/hooks/auth/useUserActiveStatus.ts @@ -7,7 +7,10 @@ const today = new Date(); export const useUserActiveStatus = (lastSeen?: string) => { try { const lastSeenDate = lastSeen ? parseISO(lastSeen) : undefined; - return lastSeenDate && differenceInDays(today, lastSeenDate) < INACTIVE_ACCOUNT_DAYS; + return ( + lastSeenDate && + differenceInDays(today, lastSeenDate) < INACTIVE_ACCOUNT_DAYS + ); } catch (e) { return false; } diff --git a/src/hooks/boris/useBorisStats.ts b/src/hooks/boris/useBorisStats.ts index 47d8a7b9..7713fe0b 100644 --- a/src/hooks/boris/useBorisStats.ts +++ b/src/hooks/boris/useBorisStats.ts @@ -6,12 +6,14 @@ import { initialBackendStats } from '~/constants/boris/constants'; import { BorisUsageStats } from '~/types/boris'; export const useBorisStats = () => { - const { data: backend = initialBackendStats, isValidating: isValidatingBackend } = useSWR( - API.BORIS.GET_BACKEND_STATS, - () => getBorisBackendStats() - ); + const { + data: backend = initialBackendStats, + isValidating: isValidatingBackend, + } = useSWR(API.BORIS.GET_BACKEND_STATS, () => getBorisBackendStats()); - const { data: issues = [] } = useSWR(API.BORIS.GITHUB_ISSUES, () => getGithubIssues()); + const { data: issues = [] } = useSWR(API.BORIS.GITHUB_ISSUES, () => + getGithubIssues(), + ); const stats: BorisUsageStats = { backend, diff --git a/src/hooks/color/useColorFromString.ts b/src/hooks/color/useColorFromString.ts index 10668fb8..dba30155 100644 --- a/src/hooks/color/useColorFromString.ts +++ b/src/hooks/color/useColorFromString.ts @@ -3,9 +3,16 @@ import { useMemo } from 'react'; import { normalizeBrightColor } from '~/utils/color'; import { stringToColour } from '~/utils/dom'; -export const useColorFromString = (val?: string, saturation = 3, lightness = 3) => { +export const useColorFromString = ( + val?: string, + saturation = 3, + lightness = 3, +) => { return useMemo( - () => (val && normalizeBrightColor(stringToColour(val), saturation, lightness)) || '', - [lightness, saturation, val] + () => + (val && + normalizeBrightColor(stringToColour(val), saturation, lightness)) || + '', + [lightness, saturation, val], ); }; diff --git a/src/hooks/color/useColorGradientFromString.ts b/src/hooks/color/useColorGradientFromString.ts index b94864f1..10e17cdc 100644 --- a/src/hooks/color/useColorGradientFromString.ts +++ b/src/hooks/color/useColorGradientFromString.ts @@ -6,7 +6,7 @@ export const useColorGradientFromString = ( val?: string, saturation = 3, lightness = 3, - angle = 155 + angle = 155, ) => useMemo(() => { if (!val) { diff --git a/src/hooks/data/usePersistedState.ts b/src/hooks/data/usePersistedState.ts index 80a48418..f0403824 100644 --- a/src/hooks/data/usePersistedState.ts +++ b/src/hooks/data/usePersistedState.ts @@ -1,6 +1,9 @@ import { useEffect, useMemo, useState } from 'react'; -export const usePersistedState = (key: string, initial: string): [string, (val: string) => any] => { +export const usePersistedState = ( + key: string, + initial: string, +): [string, (val: string) => any] => { const stored = useMemo(() => { try { return localStorage.getItem(`vault_${key}`) || initial; diff --git a/src/hooks/dom/useFocusEvent.ts b/src/hooks/dom/useFocusEvent.ts index baaf273a..3af9ab24 100644 --- a/src/hooks/dom/useFocusEvent.ts +++ b/src/hooks/dom/useFocusEvent.ts @@ -4,15 +4,18 @@ export const useFocusEvent = (initialState = false, delay = 0) => { const [focused, setFocused] = useState(initialState); const onFocus = useCallback( - event => { + (event) => { event.preventDefault(); event.stopPropagation(); setFocused(true); }, - [setFocused] + [setFocused], + ); + const onBlur = useCallback( + () => setTimeout(() => setFocused(false), delay), + [delay], ); - const onBlur = useCallback(() => setTimeout(() => setFocused(false), delay), [delay]); return { focused, onBlur, onFocus }; }; diff --git a/src/hooks/dom/useFormatWrapper.ts b/src/hooks/dom/useFormatWrapper.ts index 89c25b23..96b832a9 100644 --- a/src/hooks/dom/useFormatWrapper.ts +++ b/src/hooks/dom/useFormatWrapper.ts @@ -7,12 +7,13 @@ export const useFormatWrapper = (onChange: (val: string) => void) => { target: HTMLTextAreaElement, prefix = '', - suffix = '' - ) => event => { - event.preventDefault(); - wrapTextInsideInput(target, prefix, suffix, onChange); - }, - [onChange] + suffix = '', + ) => + (event) => { + event.preventDefault(); + wrapTextInsideInput(target, prefix, suffix, onChange); + }, + [onChange], ); }; @@ -21,7 +22,7 @@ export const wrapTextInsideInput = ( target: HTMLTextAreaElement, prefix: string, suffix: string, - onChange: (val: string) => void + onChange: (val: string) => void, ) => { if (!target) return; @@ -34,7 +35,7 @@ export const wrapTextInsideInput = ( onChange( target.value.substring(0, start) + replacement + - target.value.substring(end, target.value.length) + target.value.substring(end, target.value.length), ); target.focus(); diff --git a/src/hooks/dom/useInfiniteLoader.ts b/src/hooks/dom/useInfiniteLoader.ts index e6eb2608..32236c63 100644 --- a/src/hooks/dom/useInfiniteLoader.ts +++ b/src/hooks/dom/useInfiniteLoader.ts @@ -2,7 +2,8 @@ import { useCallback, useEffect } from 'react'; export const useInfiniteLoader = (loader: () => void, isLoading?: boolean) => { const onLoadMore = useCallback(() => { - const pos = window.scrollY + window.innerHeight - document.body.scrollHeight; + const pos = + window.scrollY + window.innerHeight - document.body.scrollHeight; if (isLoading || pos < -600) return; diff --git a/src/hooks/dom/useInputPasteUpload.ts b/src/hooks/dom/useInputPasteUpload.ts index 50ab42ba..b45ed986 100644 --- a/src/hooks/dom/useInputPasteUpload.ts +++ b/src/hooks/dom/useInputPasteUpload.ts @@ -5,13 +5,13 @@ import { getImageFromPaste } from '~/utils/uploader'; // useInputPasteUpload attaches event listener to input, that calls onUpload if user pasted any image export const useInputPasteUpload = (onUpload: (files: File[]) => void) => { return useCallback( - async event => { + async (event) => { const image = await getImageFromPaste(event); if (!image) return; onUpload([image]); }, - [onUpload] + [onUpload], ); }; diff --git a/src/hooks/dom/usePopperModifiers.ts b/src/hooks/dom/usePopperModifiers.ts index f057ccb0..4cccfa7e 100644 --- a/src/hooks/dom/usePopperModifiers.ts +++ b/src/hooks/dom/usePopperModifiers.ts @@ -17,7 +17,11 @@ const sameWidth = { }, }; -export const usePopperModifiers = (offsetX = 0, offsetY = 10, justify?: boolean): Modifier<any>[] => +export const usePopperModifiers = ( + offsetX = 0, + offsetY = 10, + justify?: boolean, +): Modifier<any>[] => useMemo( () => [ @@ -35,5 +39,5 @@ export const usePopperModifiers = (offsetX = 0, offsetY = 10, justify?: boolean) }, ...(justify ? [sameWidth] : []), ] as Modifier<any>[], - [offsetX, offsetY, justify] + [offsetX, offsetY, justify], ); diff --git a/src/hooks/dom/useScrollHeight.ts b/src/hooks/dom/useScrollHeight.ts index 48bf7f4d..b2eaae11 100644 --- a/src/hooks/dom/useScrollHeight.ts +++ b/src/hooks/dom/useScrollHeight.ts @@ -11,7 +11,7 @@ const getHeight = () => { body.offsetHeight, html.clientHeight, html.scrollHeight, - html.offsetHeight + html.offsetHeight, ); }; export const useScrollHeight = () => getHeight(); diff --git a/src/hooks/dom/useScrollToTop.ts b/src/hooks/dom/useScrollToTop.ts index f1c5c846..6127f83c 100644 --- a/src/hooks/dom/useScrollToTop.ts +++ b/src/hooks/dom/useScrollToTop.ts @@ -18,6 +18,6 @@ export const useScrollToTop = (deps?: any[]) => { }); }, // eslint-disable-next-line react-hooks/exhaustive-deps - deps && Array.isArray(deps) ? deps : [] + deps && Array.isArray(deps) ? deps : [], ); }; diff --git a/src/hooks/dom/useScrollTop.ts b/src/hooks/dom/useScrollTop.ts index d8e7e066..d8e33579 100644 --- a/src/hooks/dom/useScrollTop.ts +++ b/src/hooks/dom/useScrollTop.ts @@ -1,7 +1,9 @@ import { useEffect, useState } from 'react'; export const useScrollTop = () => { - const [top, setTop] = useState(typeof window !== 'undefined' ? window.scrollY : 0); + const [top, setTop] = useState( + typeof window !== 'undefined' ? window.scrollY : 0, + ); useEffect(() => { setTop(window.scrollY); diff --git a/src/hooks/flow/useFlowCellControls.ts b/src/hooks/flow/useFlowCellControls.ts index 6440ece4..3e749a4f 100644 --- a/src/hooks/flow/useFlowCellControls.ts +++ b/src/hooks/flow/useFlowCellControls.ts @@ -6,11 +6,12 @@ export const useFlowCellControls = ( id: INode['id'], description: string | undefined, flow: FlowDisplay, - onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void + onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void, ) => { const onChange = useCallback( - (value: Partial<FlowDisplay>) => onChangeCellView(id, { ...flow, ...value }), - [flow, id, onChangeCellView] + (value: Partial<FlowDisplay>) => + onChangeCellView(id, { ...flow, ...value }), + [flow, id, onChangeCellView], ); const hasDescription = !!description && description.length > 32; diff --git a/src/hooks/flow/useFlowSetCellView.ts b/src/hooks/flow/useFlowSetCellView.ts index 8f391dac..8c688137 100644 --- a/src/hooks/flow/useFlowSetCellView.ts +++ b/src/hooks/flow/useFlowSetCellView.ts @@ -17,6 +17,6 @@ export const useFlowSetCellView = () => { showErrorToast(error); } }, - [updateNode] + [updateNode], ); }; diff --git a/src/hooks/lab/useGetLabStats.ts b/src/hooks/lab/useGetLabStats.ts index 9a484a80..97853d49 100644 --- a/src/hooks/lab/useGetLabStats.ts +++ b/src/hooks/lab/useGetLabStats.ts @@ -21,15 +21,19 @@ export const useGetLabStats = () => { heroes: lab.heroes, tags: lab.tags, }, - onSuccess: data => { + onSuccess: (data) => { lab.setHeroes(data.heroes); lab.setTags(data.tags); }, refreshInterval, - } + }, ); - const { data: updatesData, isValidating: isValidatingUpdates, mutate: mutateUpdates } = useSWR( + const { + data: updatesData, + isValidating: isValidatingUpdates, + mutate: mutateUpdates, + } = useSWR( isUser ? API.LAB.UPDATES : null, async () => { const result = await getLabUpdates(); @@ -37,26 +41,27 @@ export const useGetLabStats = () => { }, { fallbackData: lab.updates, - onSuccess: data => { + onSuccess: (data) => { lab.setUpdates(data); }, refreshInterval, - } + }, ); const heroes = useMemo(() => stats?.heroes || [], [stats]); const tags = useMemo(() => stats?.tags || [], [stats]); const updates = useMemo(() => updatesData || [], [updatesData]); - const isLoading = (!stats || !updates) && (isValidatingStats || isValidatingUpdates); + const isLoading = + (!stats || !updates) && (isValidatingStats || isValidatingUpdates); const seenNode = useCallback( async (nodeId: number) => { await mutateUpdates( - updates.filter(it => it.id !== nodeId), - false + updates.filter((it) => it.id !== nodeId), + false, ); }, - [mutateUpdates, updates] + [mutateUpdates, updates], ); return { heroes, tags, updates, isLoading, seenNode }; diff --git a/src/hooks/messages/useMessages.ts b/src/hooks/messages/useMessages.ts index e7614fea..6349202d 100644 --- a/src/hooks/messages/useMessages.ts +++ b/src/hooks/messages/useMessages.ts @@ -11,7 +11,7 @@ const getKey = (username: string): string | null => { }; export const useMessages = (username: string) => { const { data, isValidating } = useSWR(getKey(username), async () => - apiGetUserMessages({ username }) + apiGetUserMessages({ username }), ); const messages: IMessage[] = useMemo(() => data?.messages || [], [data]); diff --git a/src/hooks/modal/useModal.ts b/src/hooks/modal/useModal.ts index de5ce0d2..66266935 100644 --- a/src/hooks/modal/useModal.ts +++ b/src/hooks/modal/useModal.ts @@ -5,7 +5,9 @@ import { useModalStore } from '~/store/modal/useModalStore'; import { DialogComponentProps } from '~/types/modal'; export type DialogContentProps = { - [K in keyof typeof DIALOG_CONTENT]: typeof DIALOG_CONTENT[K] extends (props: infer U) => any + [K in keyof typeof DIALOG_CONTENT]: (typeof DIALOG_CONTENT)[K] extends ( + props: infer U, + ) => any ? U extends DialogComponentProps ? keyof Omit<U, 'onRequestClose' | 'children'> extends never ? {} @@ -21,7 +23,7 @@ export const useModal = () => { <T extends Dialog>(dialog: T, props: DialogContentProps[T]) => { setCurrent(dialog, props); }, - [setCurrent] + [setCurrent], ); return { showModal, hideModal: hide, current, isOpened: !!current }; diff --git a/src/hooks/modal/useShowModal.ts b/src/hooks/modal/useShowModal.ts index 57452600..3ef0204a 100644 --- a/src/hooks/modal/useShowModal.ts +++ b/src/hooks/modal/useShowModal.ts @@ -10,6 +10,6 @@ export const useShowModal = <T extends Dialog>(dialog: T) => { (props: DialogContentProps[T]) => { modal.showModal(dialog, props); }, - [dialog, modal] + [dialog, modal], ); }; diff --git a/src/hooks/navigation/useImageModal.ts b/src/hooks/navigation/useImageModal.ts index ce3af6bd..68accfa9 100644 --- a/src/hooks/navigation/useImageModal.ts +++ b/src/hooks/navigation/useImageModal.ts @@ -11,6 +11,6 @@ export const useImageModal = () => { (images: IFile[], index: number) => { showModal({ items: images, index }); }, - [showModal] + [showModal], ); }; diff --git a/src/hooks/navigation/useNavigation.ts b/src/hooks/navigation/useNavigation.ts index 199801d8..b6f26c47 100644 --- a/src/hooks/navigation/useNavigation.ts +++ b/src/hooks/navigation/useNavigation.ts @@ -17,7 +17,7 @@ export const useNavigation = () => { craHistory.push(url); } }, - [craHistory, nextRouter] + [craHistory, nextRouter], ); return { push }; diff --git a/src/hooks/node/useCreateNode.ts b/src/hooks/node/useCreateNode.ts index 75b2a87a..6dfd10de 100644 --- a/src/hooks/node/useCreateNode.ts +++ b/src/hooks/node/useCreateNode.ts @@ -16,9 +16,13 @@ export const useCreateNode = () => { if (node.is_promoted) { flow.setNodes([result.node, ...flow.nodes]); } else { - await lab.unshift({ node: result.node, comment_count: 0, last_seen: node.created_at }); + await lab.unshift({ + node: result.node, + comment_count: 0, + last_seen: node.created_at, + }); } }, - [flow, lab] + [flow, lab], ); }; diff --git a/src/hooks/node/useGrouppedComments.ts b/src/hooks/node/useGrouppedComments.ts index dde9182e..22dfb3df 100644 --- a/src/hooks/node/useGrouppedComments.ts +++ b/src/hooks/node/useGrouppedComments.ts @@ -6,13 +6,13 @@ import { groupCommentsByUser } from '~/utils/fn'; export const useGrouppedComments = ( comments: IComment[], order: 'ASC' | 'DESC', - lastSeen?: string + lastSeen?: string, ) => useMemo( () => (order === 'DESC' ? [...comments].reverse() : comments).reduce( groupCommentsByUser(lastSeen), - [] + [], ), - [comments, lastSeen, order] + [comments, lastSeen, order], ); diff --git a/src/hooks/node/useNodeActions.ts b/src/hooks/node/useNodeActions.ts index 58377560..06bb88ae 100644 --- a/src/hooks/node/useNodeActions.ts +++ b/src/hooks/node/useNodeActions.ts @@ -6,7 +6,10 @@ import { useModal } from '~/hooks/modal/useModal'; import { INode } from '~/types'; import { showErrorToast } from '~/utils/errors/showToast'; -export const useNodeActions = (node: INode, update: (node: Partial<INode>) => Promise<unknown>) => { +export const useNodeActions = ( + node: INode, + update: (node: Partial<INode>) => Promise<unknown>, +) => { const { showModal } = useModal(); const onLike = useCallback(async () => { @@ -35,17 +38,20 @@ export const useNodeActions = (node: INode, update: (node: Partial<INode>) => Pr const onLock = useCallback(async () => { try { - const result = await apiLockNode({ id: node.id, is_locked: !node.deleted_at }); + const result = await apiLockNode({ + id: node.id, + is_locked: !node.deleted_at, + }); await update({ deleted_at: result.deleted_at }); } catch (error) { showErrorToast(error); } }, [node.deleted_at, node.id, update]); - const onEdit = useCallback(() => showModal(Dialog.EditNode, { nodeId: node.id! }), [ - node, - showModal, - ]); + const onEdit = useCallback( + () => showModal(Dialog.EditNode, { nodeId: node.id! }), + [node, showModal], + ); return { onLike, onStar, onLock, onEdit }; }; diff --git a/src/hooks/node/useNodeAudios.ts b/src/hooks/node/useNodeAudios.ts index 1ad28cf1..3149a171 100644 --- a/src/hooks/node/useNodeAudios.ts +++ b/src/hooks/node/useNodeAudios.ts @@ -4,7 +4,8 @@ import { UploadType } from '~/constants/uploads'; import { INode } from '~/types'; export const useNodeAudios = (node: INode) => { - return useMemo(() => node.files.filter(file => file && file.type === UploadType.Audio), [ - node.files, - ]); + return useMemo( + () => node.files.filter((file) => file && file.type === UploadType.Audio), + [node.files], + ); }; diff --git a/src/hooks/node/useNodeFormFormik.ts b/src/hooks/node/useNodeFormFormik.ts index 089f5ae2..cc723a4f 100644 --- a/src/hooks/node/useNodeFormFormik.ts +++ b/src/hooks/node/useNodeFormFormik.ts @@ -1,6 +1,11 @@ import { useCallback, useRef } from 'react'; -import { FormikConfig, FormikHelpers, useFormik, useFormikContext } from 'formik'; +import { + FormikConfig, + FormikHelpers, + useFormik, + useFormikContext, +} from 'formik'; import { object } from 'yup'; import { INode } from '~/types'; @@ -10,31 +15,31 @@ import { showErrorToast } from '~/utils/errors/showToast'; const validationSchema = object().shape({}); -const afterSubmit = ({ resetForm, setSubmitting, setErrors }: FormikHelpers<INode>) => ( - error?: unknown -) => { - setSubmitting(false); +const afterSubmit = + ({ resetForm, setSubmitting, setErrors }: FormikHelpers<INode>) => + (error?: unknown) => { + setSubmitting(false); - if (error) { - showErrorToast(error); - return; - } + if (error) { + showErrorToast(error); + return; + } - if (getValidationErrors(error)) { - setErrors(getValidationErrors(error)!); - return; - } + if (getValidationErrors(error)) { + setErrors(getValidationErrors(error)!); + return; + } - if (resetForm) { - resetForm(); - } -}; + if (resetForm) { + resetForm(); + } + }; export const useNodeFormFormik = ( values: INode, uploader: Uploader, stopEditing: () => void, - sendSaveRequest: (node: INode) => Promise<unknown> + sendSaveRequest: (node: INode) => Promise<unknown>, ) => { const { current: initialValues } = useRef(values); @@ -53,7 +58,7 @@ export const useNodeFormFormik = ( afterSubmit(helpers)(error); } }, - [sendSaveRequest, uploader.files] + [sendSaveRequest, uploader.files], ); return useFormik<INode>({ diff --git a/src/hooks/node/useNodeImages.ts b/src/hooks/node/useNodeImages.ts index e162be05..08cc1947 100644 --- a/src/hooks/node/useNodeImages.ts +++ b/src/hooks/node/useNodeImages.ts @@ -4,7 +4,8 @@ import { UploadType } from '~/constants/uploads'; import { INode } from '~/types'; export const useNodeImages = (node: INode) => { - return useMemo(() => node.files.filter(file => file && file.type === UploadType.Image), [ - node.files, - ]); + return useMemo( + () => node.files.filter((file) => file && file.type === UploadType.Image), + [node.files], + ); }; diff --git a/src/hooks/node/useUpdateNode.ts b/src/hooks/node/useUpdateNode.ts index 1a6ff356..cc1ad033 100644 --- a/src/hooks/node/useUpdateNode.ts +++ b/src/hooks/node/useUpdateNode.ts @@ -27,6 +27,6 @@ export const useUpdateNode = (id: number) => { await lab.updateNode(result.node.id!, result.node); } }, - [update, flow, lab] + [update, flow, lab], ); }; diff --git a/src/hooks/profile/useGetProfile.ts b/src/hooks/profile/useGetProfile.ts index 8d3eacbc..3e8c9308 100644 --- a/src/hooks/profile/useGetProfile.ts +++ b/src/hooks/profile/useGetProfile.ts @@ -20,7 +20,7 @@ export const useGetProfile = (username?: string) => { }, { refreshInterval: 60000, - } + }, ); const profile = data || EMPTY_USER; @@ -29,7 +29,7 @@ export const useGetProfile = (username?: string) => { async (user: Partial<IUser>) => { await mutate({ ...profile, ...user }); }, - [mutate, profile] + [mutate, profile], ); return { profile, isLoading: !data && isValidating, update }; diff --git a/src/hooks/search/useSearch.ts b/src/hooks/search/useSearch.ts index 4fa3b874..a38dd2f0 100644 --- a/src/hooks/search/useSearch.ts +++ b/src/hooks/search/useSearch.ts @@ -9,21 +9,19 @@ import { flatten } from '~/utils/ramda'; const RESULTS_COUNT = 20; -const getKey: (text: string) => SWRInfiniteKeyLoader = text => ( - pageIndex, - previousPageData: INode[] -) => { - if ((pageIndex > 0 && !previousPageData?.length) || !text) return null; +const getKey: (text: string) => SWRInfiniteKeyLoader = + (text) => (pageIndex, previousPageData: INode[]) => { + if ((pageIndex > 0 && !previousPageData?.length) || !text) return null; - const props: GetSearchResultsRequest = { - text, - skip: pageIndex * RESULTS_COUNT, - take: RESULTS_COUNT, + const props: GetSearchResultsRequest = { + text, + skip: pageIndex * RESULTS_COUNT, + take: RESULTS_COUNT, + }; + + return JSON.stringify(props); }; - return JSON.stringify(props); -}; - export const useSearch = () => { const [searchText, setSearchText] = useState(''); const [debouncedSearchText, setDebouncedSearchText] = useState(''); @@ -40,7 +38,7 @@ export const useSearch = () => { const result = await getSearchResults(props); return result.nodes; - } + }, ); const loadMore = useCallback(() => setSize(size + 1), [setSize, size]); diff --git a/src/hooks/tag/useTagAutocomplete.ts b/src/hooks/tag/useTagAutocomplete.ts index 8cbfb495..493690c8 100644 --- a/src/hooks/tag/useTagAutocomplete.ts +++ b/src/hooks/tag/useTagAutocomplete.ts @@ -26,5 +26,5 @@ export const useTagAutocomplete = ( }, ); - return useMemo(() => (search ? data ?? [] : []), [data, search]); + return useMemo(() => (search ? (data ?? []) : []), [data, search]); }; diff --git a/src/hooks/tag/useTagNodes.ts b/src/hooks/tag/useTagNodes.ts index 7df94f90..53d02b12 100644 --- a/src/hooks/tag/useTagNodes.ts +++ b/src/hooks/tag/useTagNodes.ts @@ -9,13 +9,11 @@ import { flatten, isNil } from '~/utils/ramda'; const PAGE_SIZE = 10; -const getKey: (tag: string) => SWRInfiniteKeyLoader = tag => ( - pageIndex, - previousPageData: INode[] -) => { - if (pageIndex > 0 && !previousPageData?.length) return null; - return `${API.TAG.NODES}?tag=${tag}&page=${pageIndex}`; -}; +const getKey: (tag: string) => SWRInfiniteKeyLoader = + (tag) => (pageIndex, previousPageData: INode[]) => { + if (pageIndex > 0 && !previousPageData?.length) return null; + return `${API.TAG.NODES}?tag=${tag}&page=${pageIndex}`; + }; const extractKey = (key: string) => { const re = new RegExp(`${API.TAG.NODES}\\?tag=[^&]+&page=(\\d+)`); @@ -39,7 +37,7 @@ export const useTagNodes = (tag: string) => { }); return result.nodes; - } + }, ); const nodes = useMemo(() => flatten(data || []), [data]); @@ -47,5 +45,12 @@ export const useTagNodes = (tag: string) => { const loadMore = useCallback(() => setSize(size + 1), [setSize, size]); - return { nodes, hasMore, loadMore, isLoading: !data && isValidating, mutate, data }; + return { + nodes, + hasMore, + loadMore, + isLoading: !data && isValidating, + mutate, + data, + }; }; diff --git a/src/hooks/updates/useUpdates.ts b/src/hooks/updates/useUpdates.ts index 91614788..9f1d4be0 100644 --- a/src/hooks/updates/useUpdates.ts +++ b/src/hooks/updates/useUpdates.ts @@ -9,7 +9,7 @@ export const useUpdates = () => { const { data } = useSWR( isUser ? API.USER.GET_UPDATES : null, () => apiAuthGetUpdates({ exclude_dialogs: 0, last: '' }), - { refreshInterval: 5 * 60 * 1000 } + { refreshInterval: 5 * 60 * 1000 }, ); const borisCommentedAt = data?.boris?.commented_at || ''; diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js index 5253d3ad..532f29b0 100644 --- a/src/reportWebVitals.js +++ b/src/reportWebVitals.js @@ -1,4 +1,4 @@ -const reportWebVitals = onPerfEntry => { +const reportWebVitals = (onPerfEntry) => { if (onPerfEntry && onPerfEntry instanceof Function) { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); diff --git a/src/store/flow/FlowStore.ts b/src/store/flow/FlowStore.ts index dab1c742..f7f689a9 100644 --- a/src/store/flow/FlowStore.ts +++ b/src/store/flow/FlowStore.ts @@ -24,11 +24,13 @@ export class FlowStore { /** removes node from updated after user seen it */ seenNode = (nodeId: number) => { - this.setUpdated(this.updated.filter(node => node.id !== nodeId)); + this.setUpdated(this.updated.filter((node) => node.id !== nodeId)); }; /** replaces node with value */ updateNode = (id: number, node: Partial<IFlowNode>) => { - this.setNodes(this.nodes.map(it => (it.id === id ? { ...it, ...node } : it))); + this.setNodes( + this.nodes.map((it) => (it.id === id ? { ...it, ...node } : it)), + ); }; } diff --git a/src/store/metadata/MetadataStore.tsx b/src/store/metadata/MetadataStore.tsx index 68a0b596..61105cd7 100644 --- a/src/store/metadata/MetadataStore.tsx +++ b/src/store/metadata/MetadataStore.tsx @@ -12,7 +12,9 @@ export class MetadataStore { pending: string[] = []; constructor( - protected apiMetadataLoader: (ids: string[]) => Promise<Record<string, EmbedMetadata>> + protected apiMetadataLoader: ( + ids: string[], + ) => Promise<Record<string, EmbedMetadata>>, ) { makeAutoObservable(this); } @@ -59,7 +61,7 @@ export class MetadataStore { try { const result = await this.apiMetadataLoader(items); - const fetchedIDs = values(result).map(it => it.address); + const fetchedIDs = values(result).map((it) => it.address); runInAction(() => { this.pushMetadataItems(result); @@ -72,7 +74,11 @@ export class MetadataStore { /** adds items to queue */ enqueue = (id: string) => { - if (this.queue.includes(id) || keys(this.metadata).includes(id) || this.pending.includes(id)) { + if ( + this.queue.includes(id) || + keys(this.metadata).includes(id) || + this.pending.includes(id) + ) { return; } diff --git a/src/types/index.ts b/src/types/index.ts index 04703abb..54ef359a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -153,13 +153,13 @@ export const NOTIFICATION_TYPES = { }; export type IMessageNotification = { - type: typeof NOTIFICATION_TYPES['message']; + type: (typeof NOTIFICATION_TYPES)['message']; content: Partial<IMessage>; created_at: string; }; export type ICommentNotification = { - type: typeof NOTIFICATION_TYPES['comment']; + type: (typeof NOTIFICATION_TYPES)['comment']; content: Partial<IComment>; created_at: string; }; diff --git a/src/types/sidebar/index.ts b/src/types/sidebar/index.ts index 4d11b90d..2e029c64 100644 --- a/src/types/sidebar/index.ts +++ b/src/types/sidebar/index.ts @@ -5,15 +5,14 @@ import type { SidebarComponents } from '~/constants/sidebar/components'; export type SidebarComponent = keyof SidebarComponents; // TODO: use it to store props for sidebar -export type SidebarProps< - T extends SidebarComponent -> = SidebarComponents[T] extends FunctionComponent<infer U> - ? U extends object - ? U extends SidebarComponentProps<T> - ? Omit<U, keyof SidebarComponentProps<T>> +export type SidebarProps<T extends SidebarComponent> = + SidebarComponents[T] extends FunctionComponent<infer U> + ? U extends object + ? U extends SidebarComponentProps<T> + ? Omit<U, keyof SidebarComponentProps<T>> + : U : U - : U - : {}; + : {}; export interface SidebarComponentProps<T extends SidebarComponent> { onRequestClose: () => void; openSidebar: (name: T, props: SidebarProps<T>) => void; diff --git a/src/utils/color.ts b/src/utils/color.ts index f37a7518..b6e67589 100644 --- a/src/utils/color.ts +++ b/src/utils/color.ts @@ -1,9 +1,19 @@ -import { adjustHue, darken, desaturate, parseToHsla, transparentize } from 'color2k'; +import { + adjustHue, + darken, + desaturate, + parseToHsla, + transparentize, +} from 'color2k'; import { DEFAULT_DOMINANT_COLOR } from '~/constants/node'; import { stringToColour } from '~/utils/dom'; -export const normalizeBrightColor = (color?: string, saturationExp = 3, lightnessExp = 3) => { +export const normalizeBrightColor = ( + color?: string, + saturationExp = 3, + lightnessExp = 3, +) => { if (!color) { return ''; } @@ -12,12 +22,23 @@ export const normalizeBrightColor = (color?: string, saturationExp = 3, lightnes const saturation = hsla[1]; const lightness = hsla[2]; - const desaturated = saturationExp > 1 ? desaturate(color, saturation ** saturationExp) : color; - return lightnessExp > 1 ? darken(desaturated, lightness ** lightnessExp) : desaturated; + const desaturated = + saturationExp > 1 ? desaturate(color, saturation ** saturationExp) : color; + return lightnessExp > 1 + ? darken(desaturated, lightness ** lightnessExp) + : desaturated; }; -export const generateColorTriplet = (val: string, saturation: number, lightness: number) => { - const color = normalizeBrightColor(stringToColour(val), saturation, lightness); +export const generateColorTriplet = ( + val: string, + saturation: number, + lightness: number, +) => { + const color = normalizeBrightColor( + stringToColour(val), + saturation, + lightness, + ); return [ color, @@ -31,9 +52,13 @@ export const generateGradientFromColor = ( saturation = 3, lightness = 3, angle = 155, - opacity = 1 + opacity = 1, ) => { - const [first, second, third] = generateColorTriplet(val, saturation, lightness).map(it => { + const [first, second, third] = generateColorTriplet( + val, + saturation, + lightness, + ).map((it) => { if (opacity > 1 || opacity < 0) { return it; } diff --git a/src/utils/config/index.ts b/src/utils/config/index.ts index 2e836055..82386766 100644 --- a/src/utils/config/index.ts +++ b/src/utils/config/index.ts @@ -6,5 +6,6 @@ export const CONFIG = { // image storage endpoint (sames as backend, but with /static usualy) remoteCurrent: process.env.NEXT_PUBLIC_REMOTE_CURRENT || '', // transitional prop, marks migration to nextjs - isNextEnvironment: !!process.env.NEXT_PUBLIC_REMOTE_CURRENT || typeof window === 'undefined', + isNextEnvironment: + !!process.env.NEXT_PUBLIC_REMOTE_CURRENT || typeof window === 'undefined', }; diff --git a/src/utils/fn.ts b/src/utils/fn.ts index 53b461be..482ea41c 100644 --- a/src/utils/fn.ts +++ b/src/utils/fn.ts @@ -3,11 +3,19 @@ import { differenceInDays, isAfter, isValid, parseISO } from 'date-fns'; import { IComment, ICommentGroup } from '~/types'; import { curry, insert, nth, path, remove } from '~/utils/ramda'; -export const moveArrItem = curry((at, to, list) => insert(to, nth(at, list), remove(at, 1, list))); +export const moveArrItem = curry((at, to, list) => + insert(to, nth(at, list), remove(at, 1, list)), +); export const objFromArray = (array: any[], key: string) => - array.reduce((obj, el) => (key && el[key] ? { ...obj, [el[key]]: el } : obj), {}); + array.reduce( + (obj, el) => (key && el[key] ? { ...obj, [el[key]]: el } : obj), + {}, + ); -const compareCommentDates = (commentDateValue?: string, lastSeenDateValue?: string) => { +const compareCommentDates = ( + commentDateValue?: string, + lastSeenDateValue?: string, +) => { if (!commentDateValue || !lastSeenDateValue) { return false; } @@ -37,45 +45,49 @@ const getCommentDistance = (firstDate?: string, secondDate?: string) => { } }; -export const groupCommentsByUser = (lastSeen?: string) => ( - grouppedComments: ICommentGroup[], - comment: IComment -): ICommentGroup[] => { - const last: ICommentGroup | undefined = path([grouppedComments.length - 1], grouppedComments); +export const groupCommentsByUser = + (lastSeen?: string) => + (grouppedComments: ICommentGroup[], comment: IComment): ICommentGroup[] => { + const last: ICommentGroup | undefined = path( + [grouppedComments.length - 1], + grouppedComments, + ); - if (!comment.user) { - return grouppedComments; - } + if (!comment.user) { + return grouppedComments; + } - return [ - ...(!last || path(['user', 'id'], last) !== path(['user', 'id'], comment) - ? [ - // add new group - ...grouppedComments, - { - user: comment.user, - comments: [comment], - distancesInDays: [0], - ids: [comment.id], - hasNew: compareCommentDates(comment.created_at, lastSeen), - }, - ] - : [ - // append to last group - ...grouppedComments.slice(0, grouppedComments.length - 1), - { - ...last, - distancesInDays: [ - ...last.distancesInDays, - getCommentDistance( - comment?.created_at, - last.comments[last.comments.length - 1]?.created_at - ), - ], - comments: [...last.comments, comment], - ids: [...last.ids, comment.id], - hasNew: last.hasNew || compareCommentDates(comment.created_at, lastSeen), - }, - ]), - ]; -}; + return [ + ...(!last || path(['user', 'id'], last) !== path(['user', 'id'], comment) + ? [ + // add new group + ...grouppedComments, + { + user: comment.user, + comments: [comment], + distancesInDays: [0], + ids: [comment.id], + hasNew: compareCommentDates(comment.created_at, lastSeen), + }, + ] + : [ + // append to last group + ...grouppedComments.slice(0, grouppedComments.length - 1), + { + ...last, + distancesInDays: [ + ...last.distancesInDays, + getCommentDistance( + comment?.created_at, + last.comments[last.comments.length - 1]?.created_at, + ), + ], + comments: [...last.comments, comment], + ids: [...last.ids, comment.id], + hasNew: + last.hasNew || + compareCommentDates(comment.created_at, lastSeen), + }, + ]), + ]; + }; diff --git a/src/utils/providers/ProfileProvider.tsx b/src/utils/providers/ProfileProvider.tsx index 4a7f0b8a..8e33d872 100644 --- a/src/utils/providers/ProfileProvider.tsx +++ b/src/utils/providers/ProfileProvider.tsx @@ -18,11 +18,16 @@ const ProfileContext = createContext<ProfileContextValue>({ isLoading: false, }); -export const ProfileProvider: FC<ProfileProviderProps> = ({ children, username }) => { +export const ProfileProvider: FC<ProfileProviderProps> = ({ + children, + username, +}) => { const { profile, isLoading } = useGetProfile(username); return ( - <ProfileContext.Provider value={{ profile, isLoading }}>{children}</ProfileContext.Provider> + <ProfileContext.Provider value={{ profile, isLoading }}> + {children} + </ProfileContext.Provider> ); }; diff --git a/src/utils/splitText.ts b/src/utils/splitText.ts index f7d1bb0a..3a841c15 100644 --- a/src/utils/splitText.ts +++ b/src/utils/splitText.ts @@ -2,10 +2,12 @@ import { flatten, isEmpty } from '~/utils/ramda'; export const splitTextByYoutube = (strings: string[]): string[] => flatten( - strings.map(str => - str.split(/(https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?[\w\-&=]+)/) - ) + strings.map((str) => + str.split( + /(https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?[\w\-&=]+)/, + ), + ), ); export const splitTextOmitEmpty = (strings: string[]): string[] => - strings.map(el => el.trim()).filter(el => !isEmpty(el)); + strings.map((el) => el.trim()).filter((el) => !isEmpty(el)); diff --git a/src/utils/ssr/getPageTitle.ts b/src/utils/ssr/getPageTitle.ts index d564ea98..e0a80215 100644 --- a/src/utils/ssr/getPageTitle.ts +++ b/src/utils/ssr/getPageTitle.ts @@ -1,4 +1,4 @@ /** just combines title elements to form title of the page */ export const getPageTitle = (...props: string[]): string => { - return ['Убежище', ...props].filter(it => it.trim()).join(' • '); + return ['Убежище', ...props].filter((it) => it.trim()).join(' • '); }; diff --git a/src/utils/tag.ts b/src/utils/tag.ts index 4bbf4d3f..b897ab24 100644 --- a/src/utils/tag.ts +++ b/src/utils/tag.ts @@ -3,11 +3,13 @@ import { ITag } from '~/types'; export const separateTags = (tags: Partial<ITag>[]): Partial<ITag>[][] => (tags || []).reduce( (obj, tag) => - tag?.title?.substr(0, 1) === '/' ? [[...obj[0], tag], obj[1]] : [obj[0], [...obj[1], tag]], - [[], []] as Partial<ITag>[][] + tag?.title?.substr(0, 1) === '/' + ? [[...obj[0], tag], obj[1]] + : [obj[0], [...obj[1], tag]], + [[], []] as Partial<ITag>[][], ); export const separateTagOptions = (options: string[]): string[][] => - separateTags(options.map((title): Partial<ITag> => ({ title }))).map(item => - item.filter(tag => tag.title).map(({ title }) => title!) + separateTags(options.map((title): Partial<ITag> => ({ title }))).map((item) => + item.filter((tag) => tag.title).map(({ title }) => title!), ); diff --git a/src/utils/trans.ts b/src/utils/trans.ts index 54445e43..cc8cab41 100644 --- a/src/utils/trans.ts +++ b/src/utils/trans.ts @@ -1,5 +1,6 @@ import { ERROR_LITERAL, ERRORS } from '~/constants/errors'; import { ValueOf } from '~/types'; -export const t = (string: ValueOf<typeof ERRORS>): ValueOf<typeof ERROR_LITERAL> => - ERROR_LITERAL[string] || string; +export const t = ( + string: ValueOf<typeof ERRORS>, +): ValueOf<typeof ERROR_LITERAL> => ERROR_LITERAL[string] || string; diff --git a/src/utils/uploader.ts b/src/utils/uploader.ts index 1abfcdc6..4e651c2e 100644 --- a/src/utils/uploader.ts +++ b/src/utils/uploader.ts @@ -2,10 +2,10 @@ import { FILE_MIMES, UploadType } from '~/constants/uploads'; import { isMimeOfImage } from '~/utils/validators'; /** if file is image, returns data-uri of thumbnail */ -export const uploadGetThumb = async file => { +export const uploadGetThumb = async (file) => { if (!file.type || !isMimeOfImage(file.type)) return ''; - return new Promise<string>(resolve => { + return new Promise<string>((resolve) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result?.toString() || ''); reader.readAsDataURL(file); @@ -15,14 +15,17 @@ export const uploadGetThumb = async file => { /** returns UploadType by file */ export const getFileType = (file: File): UploadType | undefined => ((file.type && - Object.keys(FILE_MIMES).find(mime => FILE_MIMES[mime].includes(file.type))) as UploadType) || - undefined; + Object.keys(FILE_MIMES).find((mime) => + FILE_MIMES[mime].includes(file.type), + )) as UploadType) || undefined; /** getImageFromPaste returns any images from paste event */ -export const getImageFromPaste = (event: ClipboardEvent): Promise<File | undefined> => { +export const getImageFromPaste = ( + event: ClipboardEvent, +): Promise<File | undefined> => { const items = event.clipboardData?.items; - return new Promise(resolve => { + return new Promise((resolve) => { for (let index in items) { const item = items[index]; @@ -31,7 +34,7 @@ export const getImageFromPaste = (event: ClipboardEvent): Promise<File | undefin const reader = new FileReader(); const type = item.type; - reader.onload = function(e) { + reader.onload = function (e) { if (!e.target?.result) { return; } @@ -40,7 +43,7 @@ export const getImageFromPaste = (event: ClipboardEvent): Promise<File | undefin new File([e.target?.result], 'paste.png', { type, lastModified: new Date().getTime(), - }) + }), ); }; diff --git a/src/utils/validators.ts b/src/utils/validators.ts index dcbb6144..a0a9426d 100644 --- a/src/utils/validators.ts +++ b/src/utils/validators.ts @@ -1,3 +1,4 @@ import { IMAGE_MIME_TYPES } from '~/constants/uploads'; -export const isMimeOfImage = (mime): boolean => !!mime && IMAGE_MIME_TYPES.indexOf(mime) >= 0; +export const isMimeOfImage = (mime): boolean => + !!mime && IMAGE_MIME_TYPES.indexOf(mime) >= 0; diff --git a/yarn.lock b/yarn.lock index 5ccef8ec..7a7ea181 100644 --- a/yarn.lock +++ b/yarn.lock @@ -221,6 +221,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@pkgr/core@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" + integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== + "@polka/url@^1.0.0-next.20": version "1.0.0-next.21" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" @@ -1206,6 +1211,14 @@ eslint-plugin-import@^2.25.4: resolve "^1.20.0" tsconfig-paths "^3.12.0" +eslint-plugin-prettier@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz#c4af01691a6fa9905207f0fbba0d7bea0902cce5" + integrity sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw== + dependencies: + prettier-linter-helpers "^1.0.0" + synckit "^0.9.1" + eslint-plugin-react-hooks@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" @@ -1394,6 +1407,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-diff@^1.1.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + fast-fifo@^1.1.0, fast-fifo@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" @@ -2452,10 +2470,17 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@^2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" - integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@^3.0.0: + version "3.4.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f" + integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ== pretty-format@^26.6.2: version "26.6.2" @@ -3087,6 +3112,14 @@ swr@^1.0.1: resolved "https://registry.yarnpkg.com/swr/-/swr-1.2.0.tgz#8649f6e9131ce94bbcf7ffd65c21334da3d1ec20" integrity sha512-C3IXeKOREn0jQ1ewXRENE7ED7jjGbFTakwB64eLACkCqkF/A0N2ckvpCTftcaSYi5yV36PzoehgVCOVRmtECcA== +synckit@^0.9.1: + version "0.9.2" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.2.tgz#a3a935eca7922d48b9e7d6c61822ee6c3ae4ec62" + integrity sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw== + dependencies: + "@pkgr/core" "^0.1.0" + tslib "^2.6.2" + table@^6.0.9: version "6.8.0" resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca" @@ -3221,6 +3254,11 @@ tslib@^2.0.3, tslib@^2.1.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +tslib@^2.6.2: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" From 032a24696305eb468377743dbfd0967e1820c8ee Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Fri, 24 Jan 2025 17:59:05 +0700 Subject: [PATCH 04/29] fix typecheck error --- src/containers/node/NodeComments/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containers/node/NodeComments/index.tsx b/src/containers/node/NodeComments/index.tsx index a1d93732..71c3a547 100644 --- a/src/containers/node/NodeComments/index.tsx +++ b/src/containers/node/NodeComments/index.tsx @@ -22,7 +22,7 @@ interface Props { order: 'ASC' | 'DESC'; } -const isFirstGroupWithNewCommentt = ( +const isFirstGroupWithNewComment = ( group: ICommentGroup, prevGroup: ICommentGroup | undefined, ) => group.hasNew && (!prevGroup || !prevGroup.hasNew); @@ -88,7 +88,7 @@ const NodeComments: FC<Props> = observer(({ order }) => { {groupped.map((group, index) => ( <> - {isFirstGroupWithNewCommentt(group, groupped.at(index - 1)) && ( + {isFirstGroupWithNewComment(group, groupped[index - 1]) && ( <a id={NEW_COMMENT_ANCHOR_NAME} className={styles.newCommentAnchor} From 71306d4c142c62c41b341dbe2f9d253f9f5a6df6 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Fri, 24 Jan 2025 18:22:13 +0700 Subject: [PATCH 05/29] only add new-comment hash to nodes with comments --- src/components/notifications/NotificationComment/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/notifications/NotificationComment/index.tsx b/src/components/notifications/NotificationComment/index.tsx index e707a27f..90acf0ae 100644 --- a/src/components/notifications/NotificationComment/index.tsx +++ b/src/components/notifications/NotificationComment/index.tsx @@ -20,7 +20,7 @@ interface NotificationCommentProps { const NotificationComment: FC<NotificationCommentProps> = ({ item, isNew }) => ( <Anchor - href={getCommentAnchor(item.url, item.itemId)} + href={isNew ? getCommentAnchor(item.url, item.itemId) : item.url} className={styles.link} > <div className={classNames(styles.message, { [styles.new]: isNew })}> From fd8907dd3a9228b4e113167f3285f6d51d47ba3a Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Sat, 25 Jan 2025 21:37:33 +0700 Subject: [PATCH 06/29] use container queries in flow --- .../components/FlowRecent/styles.module.scss | 8 +-- .../flow/FlowStamp/styles.module.scss | 2 +- src/layouts/FlowLayout/styles.module.scss | 57 ++++++++++++++----- src/styles/variables.scss | 20 +++++-- 4 files changed, 62 insertions(+), 25 deletions(-) diff --git a/src/containers/flow/FlowStamp/components/FlowRecent/styles.module.scss b/src/containers/flow/FlowStamp/components/FlowRecent/styles.module.scss index 3b9050d4..4925f482 100644 --- a/src/containers/flow/FlowStamp/components/FlowRecent/styles.module.scss +++ b/src/containers/flow/FlowStamp/components/FlowRecent/styles.module.scss @@ -1,11 +1,7 @@ -@import "src/styles/variables"; +@import 'src/styles/variables'; .recent { - @media (max-width: $flow_hide_recents) { + @container sizer (width < #{$flow_hide_recents}) { display: none; } } - -.updates { - -} diff --git a/src/containers/flow/FlowStamp/styles.module.scss b/src/containers/flow/FlowStamp/styles.module.scss index 7140b0f2..550094c3 100644 --- a/src/containers/flow/FlowStamp/styles.module.scss +++ b/src/containers/flow/FlowStamp/styles.module.scss @@ -108,7 +108,7 @@ } .whatsnew { - @media (max-width: $flow_hide_recents) { + @container sizer (width < #{$flow_hide_recents}) { display: none; } } diff --git a/src/layouts/FlowLayout/styles.module.scss b/src/layouts/FlowLayout/styles.module.scss index c3b0d2e0..a1039328 100644 --- a/src/layouts/FlowLayout/styles.module.scss +++ b/src/layouts/FlowLayout/styles.module.scss @@ -2,6 +2,19 @@ @import 'src/styles/variables'; +$target_flow_cell_width: 225px; + +/** Makes a breakpoint for target cell width **/ +@mixin breakpoint($columns) { + @container sizer (max-width: #{$target_flow_cell_width* $columns}) { + grid-template-columns: repeat(#{$columns}, 1fr); + grid-auto-rows: calc( + (100cqw - #{$columns - 1} * #{$gap} / 2) / #{$columns} + ); + @content; + } +} + .wrap { max-width: 2000px; padding: 0 40px 40px 40px; @@ -14,6 +27,7 @@ $cols: math.div($content_width, $cell); .container { @include container; margin-top: $page_top_offset; + container: sizer / size; @include tablet { padding: 0 $gap; @@ -22,9 +36,37 @@ $cols: math.div($content_width, $cell); } .grid { - grid-template-rows: 50vh; + width: 100%; + min-height: 200px; + display: grid; + gap: #{$gap}; + grid-template-columns: repeat(5, 1fr); + grid-auto-rows: calc((100cqw - 4 * #{$gap} / 2) / 5); + grid-template-rows: 40vh; + grid-auto-flow: row dense; - @include flow_grid; + @include breakpoint(5); + @include breakpoint(4); + @include breakpoint(3) { + grid-template-rows: calc(66cqw - #{$gap}) auto; + } + + @include breakpoint(2) { + grid-template-rows: calc(100cqw - #{$gap}) auto; + } + + @container sizer (width < #{$flow_hide_recents}) { + .stamp { + grid-column-start: 1; + grid-row-end: span 1; + } + + .login { + display: flex; + grid-column: 1 / 2; + grid-row-end: span 2; + } + } } .pad_last { @@ -52,11 +94,6 @@ $cols: math.div($content_width, $cell); justify-content: stretch; overflow: hidden; position: relative; - - @media (max-width: $flow_hide_recents) { - grid-column-start: 1; - grid-row-end: span 1; - } } .login { @@ -72,10 +109,4 @@ $cols: math.div($content_width, $cell); @include desktop { display: none; } - - @media (max-width: $flow_hide_recents) { - display: flex; - grid-column: 1 / 2; - grid-row-end: span 2; - } } diff --git a/src/styles/variables.scss b/src/styles/variables.scss index caf9a29f..14360c91 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -4,14 +4,19 @@ @import 'mixins'; @import 'animations'; +$gap: 8px; + $header_height: 64px; $cell: 250px; $fluid_cell: $cell; // smaller cell for fluid flow $cell_tablet: ($fluid_cell + 5) * 3 + 10; // flow breakpoint for tablet $cell_mobile: ($fluid_cell + 5) * 2 + 10; // flow breakpoint for mobile -$flow_hide_recents: $cell_tablet; // breakpoint, there recents will be hidden -$gap: 10px; +$target_flow_cell_width: 225px; +$flow_tablet_cell_count: 3; +$flow_hide_recents: $target_flow_cell_width * $flow_tablet_cell_count + + ($flow_tablet_cell_count - 1) * $gap; + $lab_gap: $gap * 2; $lab_gap_mobile: $gap * 2; $grid_line: 5px; @@ -33,11 +38,16 @@ $panel_size: 64px; $node_title_height: $panel_size; $upload_button_height: 52px; -$shadow_depth_1: transparentize(black, 0.8) 0 1px, inset $gray_90 0 1px; -$shadow_depth_2: transparentize(black, 0.8) 0 2px, inset $gray_90 0 1px; +$shadow_depth_1: + transparentize(black, 0.8) 0 1px, + inset $gray_90 0 1px; +$shadow_depth_2: + transparentize(black, 0.8) 0 2px, + inset $gray_90 0 1px; $comment_shadow: $shadow_depth_2; -$node_shadow: transparentize(black, 0.8) 0 2px, +$node_shadow: + transparentize(black, 0.8) 0 2px, transparentize(black, 0.8) 0 2px 4px; $tag_height: 26px; From 42f8f96e345ac81500e7c9f1ec719e08e92c854d Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Sun, 26 Jan 2025 19:01:42 +0700 Subject: [PATCH 07/29] only use new-comment tag if there's really new comment --- src/components/common/NodeHorizontalCard/index.tsx | 6 +++++- src/components/notifications/NotificationComment/index.tsx | 2 +- src/containers/node/NodeComments/index.tsx | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/common/NodeHorizontalCard/index.tsx b/src/components/common/NodeHorizontalCard/index.tsx index d132ee90..309110ea 100644 --- a/src/components/common/NodeHorizontalCard/index.tsx +++ b/src/components/common/NodeHorizontalCard/index.tsx @@ -22,7 +22,11 @@ const NodeHorizontalCard: FC<Props> = ({ node, hasNew, onClick }) => ( <Anchor key={node.id} className={styles.item} - href={getNewCommentAnchor(URLS.NODE_URL(node.id))} + href={ + hasNew + ? getNewCommentAnchor(URLS.NODE_URL(node.id)) + : URLS.NODE_URL(node.id) + } onClick={onClick} > <div diff --git a/src/components/notifications/NotificationComment/index.tsx b/src/components/notifications/NotificationComment/index.tsx index 90acf0ae..e707a27f 100644 --- a/src/components/notifications/NotificationComment/index.tsx +++ b/src/components/notifications/NotificationComment/index.tsx @@ -20,7 +20,7 @@ interface NotificationCommentProps { const NotificationComment: FC<NotificationCommentProps> = ({ item, isNew }) => ( <Anchor - href={isNew ? getCommentAnchor(item.url, item.itemId) : item.url} + href={getCommentAnchor(item.url, item.itemId)} className={styles.link} > <div className={classNames(styles.message, { [styles.new]: isNew })}> diff --git a/src/containers/node/NodeComments/index.tsx b/src/containers/node/NodeComments/index.tsx index 71c3a547..816f6d19 100644 --- a/src/containers/node/NodeComments/index.tsx +++ b/src/containers/node/NodeComments/index.tsx @@ -68,6 +68,7 @@ const NodeComments: FC<Props> = observer(({ order }) => { return null; } + /** Scrolls down to new comments or specific one from anchor */ useEffect(() => { const anchor = location.hash?.replace('#', ''); From 1d0ecc54a9d6442af2cbd1e253fb502238c4d695 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Mon, 27 Jan 2025 15:05:56 +0700 Subject: [PATCH 08/29] made first good profile layout --- src/layouts/FlowLayout/styles.module.scss | 32 +++---- src/layouts/ProfileLayout/index.tsx | 68 +++++++++++++-- src/layouts/ProfileLayout/styles.module.scss | 45 +++++----- src/styles/_mixins.scss | 92 +++++++++++--------- 4 files changed, 144 insertions(+), 93 deletions(-) diff --git a/src/layouts/FlowLayout/styles.module.scss b/src/layouts/FlowLayout/styles.module.scss index a1039328..4a9bec2f 100644 --- a/src/layouts/FlowLayout/styles.module.scss +++ b/src/layouts/FlowLayout/styles.module.scss @@ -4,17 +4,6 @@ $target_flow_cell_width: 225px; -/** Makes a breakpoint for target cell width **/ -@mixin breakpoint($columns) { - @container sizer (max-width: #{$target_flow_cell_width* $columns}) { - grid-template-columns: repeat(#{$columns}, 1fr); - grid-auto-rows: calc( - (100cqw - #{$columns - 1} * #{$gap} / 2) / #{$columns} - ); - @content; - } -} - .wrap { max-width: 2000px; padding: 0 40px 40px 40px; @@ -26,8 +15,9 @@ $cols: math.div($content_width, $cell); .container { @include container; + @include flow_container; + margin-top: $page_top_offset; - container: sizer / size; @include tablet { padding: 0 $gap; @@ -38,20 +28,18 @@ $cols: math.div($content_width, $cell); .grid { width: 100%; min-height: 200px; - display: grid; - gap: #{$gap}; - grid-template-columns: repeat(5, 1fr); - grid-auto-rows: calc((100cqw - 4 * #{$gap} / 2) / 5); - grid-template-rows: 40vh; - grid-auto-flow: row dense; - @include breakpoint(5); - @include breakpoint(4); - @include breakpoint(3) { + @include flow_grid() { + grid-template-rows: 40vh; + } + + @include flow_breakpoint(5); + @include flow_breakpoint(4); + @include flow_breakpoint(3) { grid-template-rows: calc(66cqw - #{$gap}) auto; } - @include breakpoint(2) { + @include flow_breakpoint(2) { grid-template-rows: calc(100cqw - #{$gap}) auto; } diff --git a/src/layouts/ProfileLayout/index.tsx b/src/layouts/ProfileLayout/index.tsx index 48c0afdc..e2f50377 100644 --- a/src/layouts/ProfileLayout/index.tsx +++ b/src/layouts/ProfileLayout/index.tsx @@ -26,7 +26,64 @@ const ProfileLayout: FC<Props> = observer(({ username }) => { return ( <Container className={styles.wrap}> <div className={styles.grid}> - <div className={styles.stamp}> + { + <Card className={styles.description}> + <p> + Inceptos cubilia velit faucibus mattis enim, massa conubia primis + torquent orci etiam? Pharetra arcu maecenas eget aptent auctor + massa habitant metus faucibus enim rhoncus. Laoreet fusce odio + litora primis senectus leo risus tristique semper augue tempor + arcu. Gravida sed cubilia malesuada hac proin parturient cubilia + habitant vulputate erat laoreet egestas. Condimentum. + </p> + <p> + Porta dui non eget varius pretium blandit fusce luctus sem + fermentum ac. At, porta iaculis primis! Mus aenean quam himenaeos + est vel interdum nostra sociosqu sodales sodales. Senectus + penatibus erat penatibus orci a suspendisse purus tristique + habitant rutrum ornare maecenas. Sapien vestibulum est ad + ridiculus viverra curae; suscipit penatibus lectus. A parturient + viverra morbi. Elit class primis laoreet, fusce integer pulvinar + facilisi. Dapibus scelerisque, leo mattis non primis dis. Sapien + lobortis mauris platea porttitor per class natoque maecenas fusce! + Est tellus sed leo! + </p> + <p> + Eros enim ac posuere vel mollis duis vivamus vivamus in est. + Elementum nostra himenaeos donec augue fermentum nascetur faucibus + dui lobortis. Hac per conubia a nunc primis. Tempus tempus erat + quam platea viverra nibh laoreet at aenean. Convallis habitasse, + luctus libero dis natoque suspendisse commodo hac? Natoque velit + pulvinar fusce posuere aliquam amet non. Dui phasellus netus + luctus. Potenti nostra tristique maecenas quisque egestas sociis! + A a sociosqu molestie sed blandit sapien sed pellentesque. Nisi + purus auctor aliquam tortor auctor faucibus. Quisque, ullamcorper + nisi tellus dignissim tempus. + </p> + <p> + Orci dis tincidunt porttitor amet ad hendrerit proin sollicitudin + mi. Amet sodales mi vivamus lacus sociosqu eleifend eros blandit + quisque mus dignissim imperdiet? Viverra suscipit metus eleifend + cras nibh nisl, fusce cum nascetur nibh. Sagittis cubilia + vulputate mauris lobortis! Rhoncus, ultrices magna ut condimentum. + Accumsan consequat penatibus vehicula varius nulla magna arcu leo + primis. Lacus pretium facilisis luctus quis sodales torquent + tempor? Nam scelerisque hendrerit diam ante cubilia volutpat. Nisi + curae; accumsan phasellus cursus orci tempus dolor ridiculus? + Taciti dis scelerisque sit. + </p> + <p> + Ligula odio aliquam donec platea? Ut; urna per praesent erat + conubia fermentum. Dis dapibus vulputate quisque odio cum et + vivamus ut. Risus accumsan cubilia ante nisi cum. Vulputate tempor + platea eget eleifend auctor rhoncus, vivamus vel ut? Nunc turpis + inceptos molestie molestie. Class libero eros volutpat placerat + quisque. Inceptos litora, felis. + </p> + </Card> + } + + <Card className={styles.left} seamless> <Sticky> <ProfilePageLeft description={profile.description} @@ -35,13 +92,10 @@ const ProfileLayout: FC<Props> = observer(({ username }) => { isLoading={isLoading} /> </Sticky> - </div> + </Card> - <Card className={styles.description}>{profile.description}</Card> - - <div className={styles.nodes}> - <FlowGrid nodes={nodes} user={user} onChangeCellView={() => {}} /> - </div> + <FlowGrid nodes={nodes} user={user} onChangeCellView={() => {}} /> + <FlowGrid nodes={nodes} user={user} onChangeCellView={() => {}} /> </div> </Container> ); diff --git a/src/layouts/ProfileLayout/styles.module.scss b/src/layouts/ProfileLayout/styles.module.scss index 5de4d336..8c8aac39 100644 --- a/src/layouts/ProfileLayout/styles.module.scss +++ b/src/layouts/ProfileLayout/styles.module.scss @@ -1,17 +1,29 @@ @import 'src/styles/variables'; .wrap { - display: grid; - grid-template-columns: auto; - grid-column-gap: $gap; + @include flow_container(); margin-top: $page_top_offset; } +.description { + grid-column: 2 / -3; + grid-row: 1 / 4; + height: 100%; + font: $font_14_medium; + overflow: auto; + height: 100%; +} + .grid { - grid-template-columns: 250px 5fr; - display: grid; - column-gap: $gap; - row-gap: $gap; + @include flow_grid(); + + @include flow_breakpoint(4); + @include flow_breakpoint(3); + @include flow_breakpoint(2) { + .left { + grid-column: 1 / -1; + } + } } .row { @@ -19,20 +31,7 @@ height: 100%; } -.description { - font: $font_14_semibold; -} - -.stamp { - grid-row-end: span 2; -} - -.nodes { - @include flow_grid(); -} - -.content { - display: flex; - flex-direction: column; - gap: $gap; +.left { + grid-row-start: 1; + grid-row-end: span 3; } diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index 98978bbf..e4cf1b60 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -1,19 +1,25 @@ @use 'sass:math'; @mixin outer_shadow() { - box-shadow: inset $gray_90 1px 1px, transparentize(black, 0.8) 1px 1px, + box-shadow: + inset $gray_90 1px 1px, + transparentize(black, 0.8) 1px 1px, transparentize(black, 0.6) 0 1px 5px; } // same as outer shadow, but higher @mixin dropdown_shadow { - box-shadow: inset $gray_90 1px 1px, transparentize(black, 0.8) 1px 1px, + box-shadow: + inset $gray_90 1px 1px, + transparentize(black, 0.8) 1px 1px, transparentize(black, 0.6) 5px 5px 10px; } @mixin row_shadow() { &:not(:last-child) { - box-shadow: $gray_90 0 1px, inset transparentize(black, 0.8) 0 -1px; + box-shadow: + $gray_90 0 1px, + inset transparentize(black, 0.8) 0 -1px; } &:only-child { @@ -22,7 +28,9 @@ } @mixin inner_shadow() { - box-shadow: inset $gray_90 -1px -1px, inset transparentize(black, 0.9) 1px 1px, + box-shadow: + inset $gray_90 -1px -1px, + inset transparentize(black, 0.9) 1px 1px, inset transparentize(black, 0.9) 0 0 10px; } @@ -36,7 +44,9 @@ } @mixin input_shadow() { - box-shadow: inset $gray_90 0 -1px, inset transparentize(black, 0.8) 0 1px; + box-shadow: + inset $gray_90 0 -1px, + inset transparentize(black, 0.8) 0 1px; } @mixin modal_mixin() { @@ -190,49 +200,49 @@ cursor: pointer; } +/** Creates container for container-query flow. + +Should wrap div with @flow_grid and @flow_breakpoint mixins +**/ +@mixin flow_container { + container: sizer / size; +} + +/** Setups flow grid. + +Should be wrapped with div that uses @include flow_container() for correct work + +Pass your custom rows here, like: + +@include flow_grid { + grid-template-rows: 220px; // will add 220px first row +} +**/ @mixin flow_grid { - width: 100%; - box-sizing: border-box; display: grid; - - grid-template-columns: repeat(auto-fit, minmax($cell - 5, 1fr)); - + gap: #{$gap}; + grid-template-columns: repeat(5, 1fr); + grid-auto-rows: calc((100cqw - 4 * #{$gap} / 2) / 5); grid-auto-flow: row dense; - grid-column-gap: $gap; - grid-row-gap: $gap; - grid-auto-rows: $cell; - // 4 cells - @media (max-width: ($cell * 5 + $gap * 4 + 55)) { - grid-auto-rows: calc(25vw - 30px); - } + @content; +} - // 3 cells - @media (max-width: ($cell * 4 + $gap * 3 + 55)) { - grid-auto-rows: calc(33vw - 30px); - } +/** Makes a breakpoint for target cell width, - // 2 cells - @media (max-width: ($cell * 3 + $gap * 2 + 55)) { - grid-auto-rows: calc(50vw - 40px); - } +Pass your rules for that breakpoint in @content: - // < 870px - @media (max-width: (($cell + 10) * 3)) { - grid-template-columns: repeat(auto-fill, minmax($fluid_cell - 20, 1fr)); - grid-template-rows: calc(50vw - 10px) $fluid_cell; - } - - // < 776px - @media (max-width: $cell_tablet) { - grid-template-rows: calc(66vw - 10px) auto calc(50vw - 40px); - } - - // < 520px - @media (max-width: $cell_mobile) { - grid-template-columns: repeat(auto-fill, minmax(calc(50vw - 20px), 1fr)); - grid-template-rows: calc(100vw - 10px) auto; - grid-auto-rows: calc(50vw - 10px); +@include flow_breakpoint(2) { // defines breakpoint for 2 cells + background: red; // will paint element red at 2 cells resolution; +} +**/ +@mixin flow_breakpoint($columns) { + @container sizer (max-width: #{$target_flow_cell_width* $columns}) { + grid-template-columns: repeat(#{$columns}, 1fr); + grid-auto-rows: calc( + (100cqw - #{$columns - 1} * #{$gap} / 2) / #{$columns} + ); + @content; } } From f0606a894a3291b3beb399ba4a14e9c973b04b24 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Mon, 27 Jan 2025 15:34:58 +0700 Subject: [PATCH 09/29] fix header stickyness --- src/styles/_mixins.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index e4cf1b60..ac2264b7 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -205,7 +205,7 @@ Should wrap div with @flow_grid and @flow_breakpoint mixins **/ @mixin flow_container { - container: sizer / size; + container: sizer / inline-size; } /** Setups flow grid. From 7924c2bdd95b26442ff455578bdb15c87b9b92aa Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Thu, 6 Feb 2025 22:53:11 +0700 Subject: [PATCH 10/29] fix flow hero height --- src/layouts/FlowLayout/styles.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/FlowLayout/styles.module.scss b/src/layouts/FlowLayout/styles.module.scss index 4a9bec2f..fbc432c3 100644 --- a/src/layouts/FlowLayout/styles.module.scss +++ b/src/layouts/FlowLayout/styles.module.scss @@ -30,7 +30,7 @@ $cols: math.div($content_width, $cell); min-height: 200px; @include flow_grid() { - grid-template-rows: 40vh; + grid-template-rows: min(50vh, 33cqw); } @include flow_breakpoint(5); From 69c61acc416a8b36ae5d210e59ef712627f1f941 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Fri, 7 Feb 2025 01:11:12 +0700 Subject: [PATCH 11/29] make better flow cell text --- .../components/FlowCellMenu/index.tsx | 3 +- .../components/FlowCellText/index.tsx | 29 +++++++++++++---- .../FlowCellText/styles.module.scss | 12 +++++-- .../FlowGrid/components/FlowCell/index.tsx | 3 +- .../components/FlowCell/styles.module.scss | 31 ++++++++++++------- src/styles/themes/_default.scss | 6 ++-- src/styles/themes/_horizon.scss | 6 ++-- 7 files changed, 60 insertions(+), 30 deletions(-) diff --git a/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellMenu/index.tsx b/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellMenu/index.tsx index b188e600..365a29a3 100644 --- a/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellMenu/index.tsx +++ b/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellMenu/index.tsx @@ -23,6 +23,7 @@ interface Props { const FlowCellMenu: FC<Props> = ({ onClose, + currentView, hasDescription, toggleViewDescription, descriptionEnabled, @@ -59,7 +60,7 @@ const FlowCellMenu: FC<Props> = ({ /> </div> - {hasDescription && ( + {hasDescription && currentView !== 'single' && ( <Group className={styles.description} horizontal diff --git a/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/index.tsx b/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/index.tsx index e49e2822..9fcd2e75 100644 --- a/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/index.tsx +++ b/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/index.tsx @@ -1,6 +1,7 @@ import { FC, ReactElement } from 'react'; import classNames from 'classnames'; +import { transparentize, darken, desaturate, getLuminance } from 'color2k'; import { Markdown } from '~/components/common/Markdown'; import { formatText } from '~/utils/dom'; @@ -11,13 +12,29 @@ import styles from './styles.module.scss'; interface Props extends DivProps { children: string; heading: string | ReactElement; + color?: string; } -const FlowCellText: FC<Props> = ({ children, heading, ...rest }) => ( - <div {...rest} className={classNames(styles.text, rest.className)}> - {heading && <div className={styles.heading}>{heading}</div>} - <Markdown className={styles.description}>{formatText(children)}</Markdown> - </div> -); +const FlowCellText: FC<Props> = ({ children, heading, color, ...rest }) => { + const colorIsBright = !!color && getLuminance(color) > 0.4; + + const textColor = colorIsBright + ? desaturate(darken(color, 0.5), 0.1) + : undefined; + + return ( + <div + {...rest} + className={classNames(styles.text, rest.className)} + style={{ + backgroundColor: color && transparentize(color, 0.5), + color: textColor, + }} + > + {heading && <div className={styles.heading}>{heading}</div>} + <Markdown className={styles.description}>{formatText(children)}</Markdown> + </div> + ); +}; export { FlowCellText }; diff --git a/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/styles.module.scss b/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/styles.module.scss index 7a421447..2624f594 100644 --- a/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/styles.module.scss +++ b/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/styles.module.scss @@ -1,10 +1,16 @@ -@import "src/styles/variables"; +@import 'src/styles/variables'; .text { - padding: $gap; + @include blur; + + padding: $gap / 2 $gap $gap $gap; line-height: 1.3em; } .heading { - margin-bottom: 0.4em; + margin-bottom: 0.25em; + + h4 { + // line-height: 1.1em; + } } diff --git a/src/containers/flow/FlowGrid/components/FlowCell/index.tsx b/src/containers/flow/FlowGrid/components/FlowCell/index.tsx index 25e9d158..ed305920 100644 --- a/src/containers/flow/FlowGrid/components/FlowCell/index.tsx +++ b/src/containers/flow/FlowGrid/components/FlowCell/index.tsx @@ -111,8 +111,9 @@ const FlowCell: FC<Props> = ({ <FlowCellText className={styles.text} heading={<h4 className={styles.title}>{title}</h4>} + color={color} > - {text!} + {text} </FlowCellText> )} diff --git a/src/containers/flow/FlowGrid/components/FlowCell/styles.module.scss b/src/containers/flow/FlowGrid/components/FlowCell/styles.module.scss index 511f7d4e..708196e0 100644 --- a/src/containers/flow/FlowGrid/components/FlowCell/styles.module.scss +++ b/src/containers/flow/FlowGrid/components/FlowCell/styles.module.scss @@ -33,18 +33,20 @@ .text { position: absolute; - bottom: 5px; - left: 5px; z-index: 1; overflow: hidden; border-radius: $radius; - max-height: calc(100% - 10px); - max-width: calc(100% - 10px); box-sizing: border-box; - font: $font_16_regular; + font: $font_14_medium; + line-height: 1.25em; + inset: 50% 0 0 0; + + &.bright { + color: var(--content_bg_lightest); + } @include tablet { - font: $font_14_regular; + font: $font_12_medium; left: 5px; bottom: 5px; } @@ -54,14 +56,19 @@ opacity: 0.5; } - .quadro &, .horizontal & { - max-width: calc(50% - 15px); + inset: 0 calc(50% + $gap / 2) 0 0; + border-radius: $radius 0 0 $radius; } - .quadro &, .vertical & { - max-height: calc(50% - 15px); + inset: calc(50% + $gap / 2) 0 0 0; + border-radius: 0 0 $radius $radius; + } + + .quadro & { + inset: calc(50% + $gap / 2) calc(50% + $gap / 2) 0 0; + border-radius: 0 $radius 0 $radius; } } @@ -76,11 +83,13 @@ .title { font: $font_cell_title; + line-height: 1.2em; text-transform: uppercase; word-break: break-word; + color: inherit; @include tablet { - font: $font_18_semibold; + font: $font_16_semibold; } } diff --git a/src/styles/themes/_default.scss b/src/styles/themes/_default.scss index dea7ea0e..ea630d2e 100644 --- a/src/styles/themes/_default.scss +++ b/src/styles/themes/_default.scss @@ -63,10 +63,8 @@ $_brown: #23201f; --content_bg_success: #{transparentize($_wisegreen, 0.7)}; --content_bg_info: #{transparentize($_blue, 0.5)}; --content_bg_danger: #{transparentize($_red, 0.5)}; - --content_bg_backdrop: url('/images/noise.png') #{transparentize( - $_brown, - 0.3 - )}; + --content_bg_backdrop: url('/images/noise.png') + #{transparentize($_brown, 0.3)}; --content_bg_hero: url('/images/noise.png') #{transparentize($_brown, 0.6)}; // white shades (move to --vars) diff --git a/src/styles/themes/_horizon.scss b/src/styles/themes/_horizon.scss index 4707b2f8..4d36e19b 100644 --- a/src/styles/themes/_horizon.scss +++ b/src/styles/themes/_horizon.scss @@ -85,10 +85,8 @@ $_ocean: #25b0bc; --gray_90: #{transparentize(white, 0.95)}; // page background - --page-background: 50% 50% / cover no-repeat url('/images/horizon_bg.svg') #{darken( - $_cold, - 4% - )} fixed; + --page-background: 50% 50% / cover no-repeat url('/images/horizon_bg.svg') + #{darken($_cold, 4%)} fixed; --page-background-top: linear-gradient( #{$_accent} -150%, #{transparentize($_ocean, 0.99)} 100px, From b257e9b5d9855d6e9302fcbf968b8e93adf1fce6 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Fri, 7 Feb 2025 01:39:58 +0700 Subject: [PATCH 12/29] scroll to cell on flow view change --- src/constants/dom/index.ts | 2 + .../FlowCellText/styles.module.scss | 4 -- .../FlowGrid/components/FlowCell/index.tsx | 40 ++++++++++++++----- .../components/FlowCell/styles.module.scss | 7 +--- src/containers/flow/FlowGrid/index.tsx | 2 +- src/utils/dom.ts | 12 ++++++ 6 files changed, 46 insertions(+), 21 deletions(-) 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/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/styles.module.scss b/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/styles.module.scss index 2624f594..0a2896ab 100644 --- a/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/styles.module.scss +++ b/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/styles.module.scss @@ -9,8 +9,4 @@ .heading { margin-bottom: 0.25em; - - h4 { - // line-height: 1.1em; - } } diff --git a/src/containers/flow/FlowGrid/components/FlowCell/index.tsx b/src/containers/flow/FlowGrid/components/FlowCell/index.tsx index ed305920..ffd837b9 100644 --- a/src/containers/flow/FlowGrid/components/FlowCell/index.tsx +++ b/src/containers/flow/FlowGrid/components/FlowCell/index.tsx @@ -1,4 +1,4 @@ -import { FC, useMemo } from 'react'; +import { FC, useCallback, useMemo } from 'react'; import classNames from 'classnames'; @@ -9,6 +9,8 @@ import { useWindowSize } from '~/hooks/dom/useWindowSize'; import { useFlowCellControls } from '~/hooks/flow/useFlowCellControls'; import { FlowDisplay, INode } from '~/types'; +import { isFullyVisible } from '../../../../../utils/dom'; + import { CellShade } from './components/CellShade'; import { FlowCellImage } from './components/FlowCellImage'; import { FlowCellMenu } from './components/FlowCellMenu'; @@ -25,7 +27,7 @@ interface Props { text?: string; flow: FlowDisplay; canEdit?: boolean; - onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void; + onChange: (id: INode['id'], flow: FlowDisplay) => void; } const FlowCell: FC<Props> = ({ @@ -37,7 +39,7 @@ const FlowCell: FC<Props> = ({ text, title, canEdit = false, - onChangeCellView, + onChange, }) => { const { isTablet } = useWindowSize(); @@ -45,6 +47,30 @@ const FlowCell: FC<Props> = ({ ((!!flow.display && flow.display !== 'single') || !image) && flow.show_description && !!text; + + const { + isActive: isMenuActive, + activate, + ref, + deactivate, + } = useClickOutsideFocus(); + + const onChangeWithScroll = useCallback<typeof onChange>( + (...args) => { + onChange(...args); + + setTimeout(() => { + if (!isFullyVisible(ref.current)) { + ref.current?.scrollIntoView({ + behavior: 'auto', + block: 'center', + }); + } + }, 0); + }, + [onChange, ref], + ); + const { hasDescription, setViewHorizontal, @@ -52,13 +78,7 @@ const FlowCell: FC<Props> = ({ setViewQuadro, setViewSingle, toggleViewDescription, - } = useFlowCellControls(id, text, flow, onChangeCellView); - const { - isActive: isMenuActive, - activate, - ref, - deactivate, - } = useClickOutsideFocus(); + } = useFlowCellControls(id, text, flow, onChangeWithScroll); const shadeSize = useMemo(() => { const min = isTablet ? 10 : 15; diff --git a/src/containers/flow/FlowGrid/components/FlowCell/styles.module.scss b/src/containers/flow/FlowGrid/components/FlowCell/styles.module.scss index 708196e0..6a0fb5ac 100644 --- a/src/containers/flow/FlowGrid/components/FlowCell/styles.module.scss +++ b/src/containers/flow/FlowGrid/components/FlowCell/styles.module.scss @@ -116,12 +116,7 @@ } .display_modal { - @include appear; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0; z-index: 11; } diff --git a/src/containers/flow/FlowGrid/index.tsx b/src/containers/flow/FlowGrid/index.tsx index 637a0d00..7bed5279 100644 --- a/src/containers/flow/FlowGrid/index.tsx +++ b/src/containers/flow/FlowGrid/index.tsx @@ -46,7 +46,7 @@ export const FlowGrid: FC<Props> = observer( text={node.description} title={node.title} canEdit={fetched && isUser && canEditNode(node, user)} - onChangeCellView={onChangeCellView} + onChange={onChangeCellView} /> </div> ))} diff --git a/src/utils/dom.ts b/src/utils/dom.ts index dd0b9d4d..473877d0 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -10,6 +10,7 @@ import { COMMENT_BLOCK_TYPES, ICommentBlock, } from '~/constants/comment'; +import { headerHeight } from '~/constants/dom'; import { IFile, ValueOf } from '~/types'; import { CONFIG } from '~/utils/config'; import { @@ -213,3 +214,14 @@ export const sizeOf = (bytes: number): string => { (bytes / Math.pow(1024, e)).toFixed(2) + ' ' + ' KMGTP'.charAt(e) + 'B' ); }; + +/** Tells if element is in view */ +export const isFullyVisible = (element?: HTMLElement): boolean => { + if (!element) { + return false; + } + + const rect = element.getBoundingClientRect(); + + return rect?.top > headerHeight && rect?.bottom < window.innerHeight; +}; From 24c66ccfdba48b17943fe611b614639a98b3be4c Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Fri, 7 Feb 2025 02:42:45 +0700 Subject: [PATCH 13/29] improve cell text appearance --- .../FlowCellText/styles.module.scss | 14 +++++- .../FlowGrid/components/FlowCell/index.tsx | 2 +- .../components/FlowCell/styles.module.scss | 43 ++++++++++++++----- .../flow/FlowGrid/styles.module.scss | 6 --- src/styles/_fonts.scss | 20 ++++++--- 5 files changed, 61 insertions(+), 24 deletions(-) diff --git a/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/styles.module.scss b/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/styles.module.scss index 0a2896ab..6fbd4a2f 100644 --- a/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/styles.module.scss +++ b/src/containers/flow/FlowGrid/components/FlowCell/components/FlowCellText/styles.module.scss @@ -3,8 +3,20 @@ .text { @include blur; - padding: $gap / 2 $gap $gap $gap; line-height: 1.3em; + display: flex; + flex-direction: column; + min-height: 0; +} + +.description { + mask-image: linear-gradient( + to bottom, + rgba(255, 255, 255, 1) 50%, + rgba(0, 0, 0, 0) 95% + ); + flex: 1; + overflow: hidden; } .heading { diff --git a/src/containers/flow/FlowGrid/components/FlowCell/index.tsx b/src/containers/flow/FlowGrid/components/FlowCell/index.tsx index ffd837b9..c83da0f1 100644 --- a/src/containers/flow/FlowGrid/components/FlowCell/index.tsx +++ b/src/containers/flow/FlowGrid/components/FlowCell/index.tsx @@ -145,7 +145,7 @@ const FlowCell: FC<Props> = ({ /> )} - {!!title && ( + {!!title && !withText && ( <CellShade color={color} className={styles.shade} diff --git a/src/containers/flow/FlowGrid/components/FlowCell/styles.module.scss b/src/containers/flow/FlowGrid/components/FlowCell/styles.module.scss index 6a0fb5ac..c2cc2e06 100644 --- a/src/containers/flow/FlowGrid/components/FlowCell/styles.module.scss +++ b/src/containers/flow/FlowGrid/components/FlowCell/styles.module.scss @@ -1,5 +1,7 @@ @import 'src/styles/variables'; +$compact_size: 200px; + .cell { @include inner_shadow; @@ -9,6 +11,7 @@ width: 100%; height: 100%; background: $content_bg; + container: cell / inline-size; } .thumb { @@ -37,18 +40,13 @@ overflow: hidden; border-radius: $radius; box-sizing: border-box; + inset: 50% 0 0 0; + padding: $gap $gap * 1.5 0 $gap * 1.5; font: $font_14_medium; line-height: 1.25em; - inset: 50% 0 0 0; - &.bright { - color: var(--content_bg_lightest); - } - - @include tablet { - font: $font_12_medium; - left: 5px; - bottom: 5px; + @container (max-width: $compact_size) { + padding: $gap / 2 $gap 0 $gap; } & :global(.grey) { @@ -56,6 +54,17 @@ opacity: 0.5; } + @container (max-width: #{$compact_size}) { + padding: $gap / 2 $gap 0 $gap; + } + + .horizontal &, + .quadro & { + @container (max-width: #{$compact_size * 2}) { + padding: $gap / 2 $gap 0 $gap; + } + } + .horizontal & { inset: 0 calc(50% + $gap / 2) 0 0; border-radius: $radius 0 0 $radius; @@ -70,6 +79,10 @@ inset: calc(50% + $gap / 2) calc(50% + $gap / 2) 0 0; border-radius: 0 $radius 0 $radius; } + + .title { + margin-bottom: 0.1em; + } } .title_wrapper { @@ -87,9 +100,17 @@ text-transform: uppercase; word-break: break-word; color: inherit; + margin-bottom: -0.125em; - @include tablet { - font: $font_16_semibold; + @container (max-width: #{$compact_size}) { + font: $font_cell_title_compact; + } + + .horizontal &, + .quadro & { + @container (max-width: #{$compact_size * 2}) { + font: $font_cell_title_compact; + } } } diff --git a/src/containers/flow/FlowGrid/styles.module.scss b/src/containers/flow/FlowGrid/styles.module.scss index ea6378f4..b3af5556 100644 --- a/src/containers/flow/FlowGrid/styles.module.scss +++ b/src/containers/flow/FlowGrid/styles.module.scss @@ -1,11 +1,5 @@ @import 'src/styles/variables'; -@mixin mobile { - @media (max-width: $cell * 2) { - @content; - } -} - .cell { &.horizontal, &.quadro { diff --git a/src/styles/_fonts.scss b/src/styles/_fonts.scss index a7086215..8f50e794 100644 --- a/src/styles/_fonts.scss +++ b/src/styles/_fonts.scss @@ -1,4 +1,3 @@ - $bold: 700; $semibold: 600; $regular: 400; @@ -6,10 +5,20 @@ $medium: 500; $light: 300; $extra_light: 200; - -$font: Montserrat, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, -'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', -'Noto Color Emoji'; +$font: + Montserrat, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + 'Helvetica Neue', + Arial, + 'Noto Sans', + sans-serif, + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji'; $font_48_semibold: $semibold 48px $font; $font_48_bold: $bold 48px $font; @@ -39,5 +48,6 @@ $font_8_regular: $regular 8px $font; $font_8_semibold: $semibold 8px $font; $font_cell_title: $font_24_semibold; +$font_cell_title_compact: $font_18_semibold; $font_hero_title: $bold 40px $font; $font_boris: $bold 72px $font; From 9e79cba7bffb1cf340c871c77613e63f56e9302a Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Fri, 7 Feb 2025 06:07:37 +0700 Subject: [PATCH 14/29] fix search appearance --- .../NodeHorizontalCard/styles.module.scss | 4 +- src/components/common/SubTitle/index.tsx | 8 +- .../common/SubTitle/styles.module.scss | 22 ++++- .../node/NodeRelated/styles.module.scss | 5 - .../FlowStamp/components/FlowRecent/index.tsx | 33 ------- .../components/FlowRecent/styles.module.scss | 7 -- src/containers/flow/FlowStamp/index.tsx | 99 +++++++++---------- .../flow/FlowStamp/styles.module.scss | 95 ++++-------------- 8 files changed, 94 insertions(+), 179 deletions(-) delete mode 100644 src/containers/flow/FlowStamp/components/FlowRecent/index.tsx delete mode 100644 src/containers/flow/FlowStamp/components/FlowRecent/styles.module.scss 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<Props> = ({ isLoading, children, ...rest }) => ( <div {...rest} className={classNames(styles.title, rest.className)}> - <Placeholder active={isLoading} loading> - {children} - </Placeholder> + <span className={styles.name}> + <Placeholder active={isLoading} loading> + {children} + </Placeholder> + </span> </div> ); 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/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/containers/flow/FlowStamp/components/FlowRecent/index.tsx b/src/containers/flow/FlowStamp/components/FlowRecent/index.tsx deleted file mode 100644 index f46249d0..00000000 --- a/src/containers/flow/FlowStamp/components/FlowRecent/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { FC } from 'react'; - -import { NodeHorizontalCard } from '~/components/common/NodeHorizontalCard'; -import { IFlowNode } from '~/types'; - -import styles from './styles.module.scss'; - -interface Props { - recent: IFlowNode[]; - updated: IFlowNode[]; -} - -const FlowRecent: FC<Props> = ({ recent, updated }) => { - return ( - <> - <div className={styles.updates}> - {updated && - updated.map((node) => ( - <NodeHorizontalCard node={node} key={node.id} hasNew /> - ))} - </div> - - <div className={styles.recent}> - {recent && - recent.map((node) => ( - <NodeHorizontalCard node={node} key={node.id} /> - ))} - </div> - </> - ); -}; - -export { FlowRecent }; diff --git a/src/containers/flow/FlowStamp/components/FlowRecent/styles.module.scss b/src/containers/flow/FlowStamp/components/FlowRecent/styles.module.scss deleted file mode 100644 index 4925f482..00000000 --- a/src/containers/flow/FlowStamp/components/FlowRecent/styles.module.scss +++ /dev/null @@ -1,7 +0,0 @@ -@import 'src/styles/variables'; - -.recent { - @container sizer (width < #{$flow_hide_recents}) { - display: none; - } -} diff --git a/src/containers/flow/FlowStamp/index.tsx b/src/containers/flow/FlowStamp/index.tsx index 3569fd7c..03f271a8 100644 --- a/src/containers/flow/FlowStamp/index.tsx +++ b/src/containers/flow/FlowStamp/index.tsx @@ -2,17 +2,15 @@ import { FC, FormEvent, useCallback, useMemo } from 'react'; import classNames from 'classnames'; -import { Group } from '~/components/common/Group'; +import { Card } from '~/components/common/Card'; import { Icon } from '~/components/common/Icon'; -import { Superpower } from '~/components/common/Superpower'; +import { NodeHorizontalCard } from '~/components/common/NodeHorizontalCard'; +import { SubTitle } from '~/components/common/SubTitle'; import { InputText } from '~/components/input/InputText'; -import { Toggle } from '~/components/input/Toggle'; -import { experimentalFeatures } from '~/constants/features'; import styles from '~/containers/flow/FlowStamp/styles.module.scss'; import { useFlowContext } from '~/utils/providers/FlowProvider'; import { useSearchContext } from '~/utils/providers/SearchProvider'; -import { FlowRecent } from './components/FlowRecent'; import { FlowSearchResults } from './components/FlowSearchResults'; interface Props { @@ -64,60 +62,55 @@ const FlowStamp: FC<Props> = ({ isFluid, onToggleLayout }) => { return ( <div className={styles.wrap}> - <form className={styles.search} onSubmit={onSearchSubmit}> - <InputText - title="Поиск" - value={searchText} - handler={setSearchText} - suffix={after} - onKeyUp={onKeyUp} - /> - </form> + <Card className={styles.search}> + <form onSubmit={onSearchSubmit}> + <InputText + title="Поиск" + value={searchText} + handler={setSearchText} + suffix={after} + onKeyUp={onKeyUp} + /> + </form> + </Card> {searchText ? ( - <div className={styles.search_results}> - <div className={styles.grid}> - <div className={styles.label}> - <span className={styles.label_text}>Результаты поиска</span> - <span className="line" /> - </div> - - <div className={styles.items}> - <FlowSearchResults - hasMore={searchHasMore} - isLoading={searchIsLoading} - results={searchResults} - onLoadMore={onSearchLoadMore} - /> - </div> - </div> - </div> - ) : ( - <div className={styles.grid}> - <div className={classNames(styles.label, styles.whatsnew)}> - <span className={styles.label_text}>Что нового?</span> - <span className="line" /> - </div> + <Card className={styles.grid}> + <SubTitle>Результаты поиска</SubTitle> <div className={styles.items}> - <FlowRecent updated={updates} recent={recent} /> + <FlowSearchResults + hasMore={searchHasMore} + isLoading={searchIsLoading} + results={searchResults} + onLoadMore={onSearchLoadMore} + /> </div> - </div> - )} + </Card> + ) : ( + <Card + className={classNames(styles.grid, { + [styles.noUpdates]: !updates.length, + })} + > + <SubTitle>Что нового?</SubTitle> - {experimentalFeatures.liquidFlow && ( - <Superpower> - <div className={styles.toggles}> - <Group - horizontal - onClick={onToggleLayout} - className={styles.fluid_toggle} - > - <Toggle value={isFluid} /> - <div className={styles.toggles__label}>Жидкое течение</div> - </Group> - </div> - </Superpower> + {updates.length > 0 && ( + <div className={classNames(styles.items, styles.updates)}> + {updates.map((node) => ( + <NodeHorizontalCard node={node} key={node.id} hasNew /> + ))} + </div> + )} + + {recent.length > 0 && ( + <div className={classNames(styles.items, styles.recent)}> + {recent.map((node) => ( + <NodeHorizontalCard node={node} key={node.id} /> + ))} + </div> + )} + </Card> )} </div> ); diff --git a/src/containers/flow/FlowStamp/styles.module.scss b/src/containers/flow/FlowStamp/styles.module.scss index 550094c3..8dd15e59 100644 --- a/src/containers/flow/FlowStamp/styles.module.scss +++ b/src/containers/flow/FlowStamp/styles.module.scss @@ -1,22 +1,24 @@ -@import '../../../styles/variables'; +@import '~/styles/variables'; .wrap { display: flex; flex-direction: column; width: 100%; - border-radius: $radius; + gap: $gap; +} + +.search { + background-color: var(--content_bg_lighter); } .grid { - @include outer_shadow(); display: flex; justify-content: stretch; flex-direction: column; flex: 1; - border-radius: $radius; - position: relative; - background: $content_bg; overflow: hidden; + gap: $gap; + padding: $gap; &::after { content: ''; @@ -33,49 +35,25 @@ display: none; } } + + &.noUpdates { + @container sizer (width < #{$flow_hide_recents}) { + display: none; + } + } +} + +.items.recent { + @container sizer (width < #{$flow_hide_recents}) { + display: none; + background-color: red; + } } .items { - padding: 0 $gap 0 $gap; flex: 1; display: flex; flex-direction: column; - overflow: hidden; -} - -.label { - display: flex; - flex-direction: row; - min-width: 0; - padding: $gap; - border-radius: $radius; - - @include title_with_line(); - - color: transparentize(white, $amount: 0.8); - - &_search { - color: white; - padding-left: $gap * 1.2; - } - - & > :global(.line) { - margin-right: $gap; - } -} - -.label_text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.search { - @include outer_shadow(); - - background: $content_bg_lighter; - padding: $gap; - border-radius: $radius; } .search_icon { @@ -89,34 +67,3 @@ stroke-width: 0.5; transition: opacity 0.25s; } - -.toggles { - & > div { - padding: $gap; - font: $font_14_semibold; - } - - &__label { - cursor: pointer; - } -} - -.fluid_toggle { - @include desktop { - display: none; - } -} - -.whatsnew { - @container sizer (width < #{$flow_hide_recents}) { - display: none; - } -} - -.search_results { - overflow: auto; - - @include tablet { - margin-top: $gap; - } -} From 6f2715a9ae71cc85a1e9b53dd4f73eb87d5f3d99 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Fri, 7 Feb 2025 07:25:59 +0700 Subject: [PATCH 15/29] put Vault last on page title --- src/utils/ssr/getPageTitle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/ssr/getPageTitle.ts b/src/utils/ssr/getPageTitle.ts index e0a80215..3f9a1030 100644 --- a/src/utils/ssr/getPageTitle.ts +++ b/src/utils/ssr/getPageTitle.ts @@ -1,4 +1,4 @@ /** just combines title elements to form title of the page */ export const getPageTitle = (...props: string[]): string => { - return ['Убежище', ...props].filter((it) => it.trim()).join(' • '); + return [...props, 'Убежище'].filter((it) => it.trim()).join(' • '); }; From 16689ae3a663545ab8ae470db4916e4a4fb45e38 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Mon, 10 Feb 2025 15:00:09 +0700 Subject: [PATCH 16/29] fix input radius --- src/components/input/InputText/styles.module.scss | 2 +- src/components/input/InputWrapper/styles.module.scss | 2 +- src/styles/variables.scss | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) 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/styles/variables.scss b/src/styles/variables.scss index 14360c91..64ab0eae 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -30,6 +30,7 @@ $cell_radius: $radius; $panel_radius: $radius; $dialog_radius: $radius * 2; $placeholder_bg: $gray_90; +$input_radius: #{$radius / 2}; $info_height: 24px; $limited_width: 940px; From 4eb605a398c916614bd60796baa4fe0800a0e610 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Wed, 12 Feb 2025 18:01:21 +0700 Subject: [PATCH 17/29] bump swiperjs, fix types --- package.json | 2 +- src/components/common/Columns/index.tsx | 2 +- src/components/node/NodeImageSwiperBlock/index.tsx | 4 +++- yarn.lock | 8 ++++---- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 133af634..abff71b0 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "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", 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<ColumnsProps> = ({ 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/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<Props> = 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/yarn.lock b/yarn.lock index 7a7ea181..a85702fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3102,10 +3102,10 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -swiper@^11.0.3: - version "11.0.3" - resolved "https://registry.yarnpkg.com/swiper/-/swiper-11.0.3.tgz#9c325154db2a4431f508b7e8e300621365eb4c3d" - integrity sha512-MyV9ooQsriAe2EibeamqewLjgCfSvl2xoyratl6S3ln5BXDL4BzlO6mxcbLMCzQL6Z60b/u0AS/nKrepL0+TAg== +swiper@^11.2.2: + version "11.2.2" + resolved "https://registry.yarnpkg.com/swiper/-/swiper-11.2.2.tgz#b49089fad99501e34cb1be916e1ae05379aace9a" + integrity sha512-FmAN6zACpVUbd/1prO9xQ9gKo9cc6RE2UKU/z4oXtS8fNyX4sdOW/HHT/e444WucLJs0jeMId6WjdWM2Lrs8zA== swr@^1.0.1: version "1.2.0" From bf1382af0b3fd9a364abfe35fbf27992d39cfbe8 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Wed, 12 Feb 2025 18:41:28 +0700 Subject: [PATCH 18/29] bump photoswipe --- package.json | 2 +- src/containers/dialogs/PhotoSwipe/index.tsx | 152 ++++++------------ .../dialogs/PhotoSwipe/styles.module.scss | 2 +- src/styles/_global.scss | 2 - yarn.lock | 8 +- 5 files changed, 58 insertions(+), 108 deletions(-) diff --git a/package.json b/package.json index abff71b0..ca720cff 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", diff --git a/src/containers/dialogs/PhotoSwipe/index.tsx b/src/containers/dialogs/PhotoSwipe/index.tsx index 3a285696..4c3c6fdf 100644 --- a/src/containers/dialogs/PhotoSwipe/index.tsx +++ b/src/containers/dialogs/PhotoSwipe/index.tsx @@ -1,9 +1,9 @@ -import { useEffect, useRef, VFC } from 'react'; +import { useEffect } from 'react'; -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 { SlideData } from 'photoswipe/dist/types/slide/slide'; + +import 'photoswipe/style.css'; import { imagePresets } from '~/constants/urls'; import { useWindowSize } from '~/hooks/dom/useWindowSize'; @@ -14,124 +14,76 @@ 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<PhotoSwipeProps> = observer(({ index, items }) => { - let ref = useRef<HTMLDivElement>(null); +const padding = { top: 10, left: 10, right: 10, bottom: 10 } as const; + +const PhotoSwipe = observer(({ index, items }: Props) => { const { hideModal } = useModal(); const { isTablet } = useWindowSize(); 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], - ); + Promise.all( + items.map( + (file): Promise<SlideData> => + new Promise((resolve) => { + const src = getURL( + file, + isTablet ? imagePresets[900] : imagePresets[1600], + ); - if (file.metadata?.width && file.metadata.height) { - resolve({ - src, - w: file.metadata.width, - h: file.metadata.height, - }); + if (file.metadata?.width && file.metadata.height) { + resolve({ + src, + width: file.metadata.width, + height: file.metadata.height, + }); - return; - } + return; + } - const img = new Image(); + const img = new Image(); - img.onload = () => { - resolve({ - src, - h: img.naturalHeight, - w: img.naturalWidth, - }); - }; + img.onload = () => { + resolve({ + src, + height: img.naturalHeight, + width: img.naturalWidth, + }); + }; - img.onerror = () => { - resolve({}); - }; + img.onerror = () => { + resolve({}); + }; - img.src = getURL(file, imagePresets[1600]); - }), - ), - ); + img.src = getURL(file, imagePresets[1600]); + }), + ), + ).then(async (images: SlideData[]) => { + const PSWP = await import('photoswipe').then((it) => it.default); - resolve(images); - }).then((images) => { - const ps = new PhotoSwipeJs(ref.current, PhotoSwipeUI_Default, images, { + const ps = new PSWP({ + dataSource: images, index: index || 0, - closeOnScroll: false, - history: false, + closeOnVerticalDrag: true, + padding, + mainClass: styles.wrap, + zoom: false, + counter: false, + bgOpacity: 0.1, }); + ps.on('destroy', hideModal); + ps.on('close', hideModal); + ps.init(); - ps.listen('destroy', hideModal); - ps.listen('close', hideModal); }); }, [hideModal, items, index, isTablet]); - return ( - <div - className="pswp" - tabIndex={-1} - role="dialog" - aria-hidden="true" - ref={ref} - > - <div className={classNames('pswp__bg', styles.bg)} /> - <div className={classNames('pswp__scroll-wrap', styles.wrap)}> - <div className="pswp__container"> - <div className="pswp__item" /> - <div className="pswp__item" /> - <div className="pswp__item" /> - </div> - - <div className="pswp__ui pswp__ui--hidden"> - <div className={classNames('pswp__top-bar', styles.bar)}> - <div className="pswp__counter" /> - <button - className="pswp__button pswp__button--close" - title="Close (Esc)" - /> - - <div className="pswp__preloader"> - <div className="pswp__preloader__icn"> - <div className="pswp__preloader__cut"> - <div className="pswp__preloader__donut" /> - </div> - </div> - </div> - </div> - - <div className="pswp__share-modal pswp__share-modal--hidden pswp__single-tap"> - <div className="pswp__share-tooltip" /> - </div> - - <button - className="pswp__button pswp__button--arrow--left" - title="Previous (arrow left)" - /> - - <button - className="pswp__button pswp__button--arrow--right" - title="Next (arrow right)" - /> - - <div className="pswp__caption"> - <div className="pswp__caption__center" /> - </div> - </div> - </div> - </div> - ); + return null; }); export { PhotoSwipe }; diff --git a/src/containers/dialogs/PhotoSwipe/styles.module.scss b/src/containers/dialogs/PhotoSwipe/styles.module.scss index 8a6d6c8b..e66eeb25 100644 --- a/src/containers/dialogs/PhotoSwipe/styles.module.scss +++ b/src/containers/dialogs/PhotoSwipe/styles.module.scss @@ -1,4 +1,4 @@ -@import "src/styles/variables"; +@import 'src/styles/variables'; .wrap { :global(.pswp__img) { diff --git a/src/styles/_global.scss b/src/styles/_global.scss index ab87de67..33f1f85a 100644 --- a/src/styles/_global.scss +++ b/src/styles/_global.scss @@ -2,8 +2,6 @@ @use './themes/horizon' as theme_horizon; @import 'src/styles/variables'; -@import 'photoswipe/dist/photoswipe'; -@import 'photoswipe/dist/default-skin/default-skin'; @import 'swiper/css'; @import 'swiper/css/effect-fade'; diff --git a/yarn.lock b/yarn.lock index a85702fa..51753816 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2423,10 +2423,10 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -photoswipe@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/photoswipe/-/photoswipe-4.1.3.tgz#59f49494eeb9ddab5888d03392926a19bc197550" - integrity sha512-89Z43IRUyw7ycTolo+AaiDn3W1EEIfox54hERmm9bI12IB9cvRfHSHez3XhAyU8XW2EAFrC+2sKMhh7SJwn0bA== +photoswipe@^5.4.4: + version "5.4.4" + resolved "https://registry.yarnpkg.com/photoswipe/-/photoswipe-5.4.4.tgz#e045dc036453493188d5c8665b0e8f1000ac4d6e" + integrity sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA== picocolors@^1.0.0: version "1.0.0" From 5e71294e7180f61b45e242a20280470d6b4f8730 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Wed, 12 Feb 2025 19:59:06 +0700 Subject: [PATCH 19/29] change photoswipe icons --- src/containers/dialogs/PhotoSwipe/index.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/containers/dialogs/PhotoSwipe/index.tsx b/src/containers/dialogs/PhotoSwipe/index.tsx index 4c3c6fdf..b5baccb7 100644 --- a/src/containers/dialogs/PhotoSwipe/index.tsx +++ b/src/containers/dialogs/PhotoSwipe/index.tsx @@ -1,10 +1,12 @@ import { useEffect } from 'react'; -import { observer } from 'mobx-react-lite'; -import { SlideData } from 'photoswipe/dist/types/slide/slide'; - import 'photoswipe/style.css'; +import { observer } from 'mobx-react-lite'; +import { SlideData } from 'photoswipe/dist/types/slide/slide'; +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,12 +15,15 @@ import { DialogComponentProps } from '~/types/modal'; import { getURL } from '~/utils/dom'; import styles from './styles.module.scss'; - export interface Props extends DialogComponentProps { items: IFile[]; index: number; } +const arrowNextSVG = renderToStaticMarkup(<Icon icon="right" size={40} />); +const arrowPrevSVG = renderToStaticMarkup(<Icon icon="left" size={40} />); +const closeSVG = renderToStaticMarkup(<Icon icon="close" size={32} />); + const padding = { top: 10, left: 10, right: 10, bottom: 10 } as const; const PhotoSwipe = observer(({ index, items }: Props) => { @@ -74,6 +79,9 @@ const PhotoSwipe = observer(({ index, items }: Props) => { zoom: false, counter: false, bgOpacity: 0.1, + arrowNextSVG, + arrowPrevSVG, + closeSVG, }); ps.on('destroy', hideModal); From 606700f5d2b9692b530ba7068ba2f585153b179d Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Thu, 13 Feb 2025 15:36:18 +0700 Subject: [PATCH 20/29] fix photoswipe --- src/constants/modal/index.ts | 9 ++- src/containers/dialogs/Modal/index.tsx | 13 ++-- src/containers/dialogs/PhotoSwipe/index.tsx | 85 +++++++-------------- 3 files changed, 44 insertions(+), 63 deletions(-) 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/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<Props> = observer(() => { return ( <ModalWrapper onOverlayClick={hide}> - {createElement(DIALOG_CONTENT[current!]! as any, { - onRequestClose: hide, - ...props, - })} + <Suspense fallback={<LoaderCircle />}> + {createElement(DIALOG_CONTENT[current!]! as any, { + onRequestClose: hide, + ...props, + })} + </Suspense> </ModalWrapper> ); }); diff --git a/src/containers/dialogs/PhotoSwipe/index.tsx b/src/containers/dialogs/PhotoSwipe/index.tsx index b5baccb7..f5d4098c 100644 --- a/src/containers/dialogs/PhotoSwipe/index.tsx +++ b/src/containers/dialogs/PhotoSwipe/index.tsx @@ -1,9 +1,9 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import 'photoswipe/style.css'; import { observer } from 'mobx-react-lite'; -import { SlideData } from 'photoswipe/dist/types/slide/slide'; +import PSWP from 'photoswipe'; import { renderToStaticMarkup } from 'react-dom/server'; import { Icon } from '~/components/common/Icon'; @@ -29,66 +29,37 @@ 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(() => { - Promise.all( - items.map( - (file): Promise<SlideData> => - 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, - width: file.metadata.width, - height: 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, - height: img.naturalHeight, - width: img.naturalWidth, - }); - }; - - img.onerror = () => { - resolve({}); - }; - - img.src = getURL(file, imagePresets[1600]); - }), - ), - ).then(async (images: SlideData[]) => { - const PSWP = await import('photoswipe').then((it) => it.default); - - const ps = new PSWP({ - dataSource: images, - index: index || 0, - closeOnVerticalDrag: true, - padding, - mainClass: styles.wrap, - zoom: false, - counter: false, - bgOpacity: 0.1, - arrowNextSVG, - arrowPrevSVG, - closeSVG, - }); - - ps.on('destroy', hideModal); - ps.on('close', hideModal); - - ps.init(); - }); + return () => { + pswp.current?.off('close', hideModal); + pswp.current?.destroy(); + }; }, [hideModal, items, index, isTablet]); return null; From 521f5ce43692040f75627f64f7c15e4c2ce0255e Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Mon, 17 Feb 2025 11:53:06 +0700 Subject: [PATCH 21/29] fix search results --- src/containers/flow/FlowStamp/index.tsx | 2 +- src/containers/flow/FlowStamp/styles.module.scss | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/containers/flow/FlowStamp/index.tsx b/src/containers/flow/FlowStamp/index.tsx index 03f271a8..d38e11bc 100644 --- a/src/containers/flow/FlowStamp/index.tsx +++ b/src/containers/flow/FlowStamp/index.tsx @@ -78,7 +78,7 @@ const FlowStamp: FC<Props> = ({ isFluid, onToggleLayout }) => { <Card className={styles.grid}> <SubTitle>Результаты поиска</SubTitle> - <div className={styles.items}> + <div className={classNames(styles.items, styles.scrollable)}> <FlowSearchResults hasMore={searchHasMore} isLoading={searchIsLoading} diff --git a/src/containers/flow/FlowStamp/styles.module.scss b/src/containers/flow/FlowStamp/styles.module.scss index 8dd15e59..43589473 100644 --- a/src/containers/flow/FlowStamp/styles.module.scss +++ b/src/containers/flow/FlowStamp/styles.module.scss @@ -54,6 +54,12 @@ flex: 1; display: flex; flex-direction: column; + + @container sizer (width >= #{$flow_hide_recents}) { + &.scrollable { + overflow: auto; + } + } } .search_icon { From 50560475463685a368a2ad630c8b7f7ce4ee3da6 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Mon, 17 Feb 2025 11:55:15 +0700 Subject: [PATCH 22/29] fix header apperance for guests --- src/containers/main/Header/index.tsx | 6 +++++- src/containers/main/Header/styles.module.scss | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/containers/main/Header/index.tsx b/src/containers/main/Header/index.tsx index 879d2b22..618b6c10 100644 --- a/src/containers/main/Header/index.tsx +++ b/src/containers/main/Header/index.tsx @@ -59,7 +59,11 @@ const Header: FC<HeaderProps> = observer(() => { className={classNames(styles.wrap, { [styles.is_scrolled]: isScrolled })} > <div className={styles.container}> - <div className={styles.logo_wrapper}> + <div + className={classNames(styles.logo_wrapper, { + [styles.guest]: !isUser, + })} + > <Logo /> </div> diff --git a/src/containers/main/Header/styles.module.scss b/src/containers/main/Header/styles.module.scss index 3d2b0401..9feb2309 100644 --- a/src/containers/main/Header/styles.module.scss +++ b/src/containers/main/Header/styles.module.scss @@ -106,7 +106,9 @@ transform: translate(50%, 0) scaleX(0); opacity: 0; border-radius: 3px; - transition: transform 0.5s, opacity 0.25s; + transition: + transform 0.5s, + opacity 0.25s; } &::after { @@ -159,7 +161,7 @@ } } -.logo_wrapper { +.logo_wrapper:not(.guest) { @include tablet { display: none; } From 06cf7050a995a646cd2211416ce2509be0f9dd6c Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Wed, 26 Feb 2025 16:48:54 +0700 Subject: [PATCH 23/29] play video in embed instead of new window on desktops --- package.json | 1 + .../components/CommentVideoFrame/index.tsx | 28 +++++++++ .../CommentVideoFrame/styles.module.scss | 8 +++ .../components/CommentEmbedBlock/index.tsx | 57 +++++++++++++---- .../CommentEmbedBlock/styles.module.scss | 49 ++++++++++++++- src/containers/node/NodeComments/index.tsx | 61 ++++++++++--------- src/utils/providers/VideoPlayerProvider.tsx | 17 ++++++ yarn.lock | 7 +++ 8 files changed, 184 insertions(+), 44 deletions(-) create mode 100644 src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/components/CommentVideoFrame/index.tsx create mode 100644 src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/components/CommentVideoFrame/styles.module.scss create mode 100644 src/utils/providers/VideoPlayerProvider.tsx diff --git a/package.json b/package.json index ca720cff..43e726e8 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "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", diff --git a/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/components/CommentVideoFrame/index.tsx b/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/components/CommentVideoFrame/index.tsx new file mode 100644 index 00000000..686e6dd1 --- /dev/null +++ b/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/components/CommentVideoFrame/index.tsx @@ -0,0 +1,28 @@ +import classNames from 'classnames'; +import { useResizeDetector } from 'react-resize-detector'; + +import styles from './styles.module.scss'; + +interface Props { + id: string; + title: string; + className?: string; +} + +export const CommentVideoFrame = ({ id, title, className }: Props) => { + const { ref, width = 0, height = 0 } = useResizeDetector(); + + return ( + <div className={classNames(styles.wrap, className)} ref={ref}> + <iframe + width={width} + height={height} + src={`https://www.youtube.com/embed/${id}?autoplay=1`} + allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" + frameBorder="0" + allowFullScreen + title={title} + /> + </div> + ); +}; diff --git a/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/components/CommentVideoFrame/styles.module.scss b/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/components/CommentVideoFrame/styles.module.scss new file mode 100644 index 00000000..26849291 --- /dev/null +++ b/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/components/CommentVideoFrame/styles.module.scss @@ -0,0 +1,8 @@ +@import '~/styles/variables'; + +.wrap { + width: 100%; + aspect-ratio: calc(16 / 9); + overflow: hidden; + border-radius: $radius; +} diff --git a/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/index.tsx b/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/index.tsx index 8305866d..542dea5a 100644 --- a/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/index.tsx +++ b/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/index.tsx @@ -1,15 +1,21 @@ -import { FC, memo, useMemo } from 'react'; +import { FC, memo, useCallback, useMemo } from 'react'; import { Icon } from '~/components/common/Icon'; import { ICommentBlockProps } from '~/constants/comment'; +import { useWindowSize } from '~/hooks/dom/useWindowSize'; import { useYoutubeMetadata } from '~/hooks/metadata/useYoutubeMetadata'; import { getYoutubeThumb } from '~/utils/dom'; +import { useVideoPlayer } from '~/utils/providers/VideoPlayerProvider'; +import { CommentVideoFrame } from './components/CommentVideoFrame'; import styles from './styles.module.scss'; type Props = ICommentBlockProps & {}; const CommentEmbedBlock: FC<Props> = memo(({ block }) => { + const { isTablet } = useWindowSize(); + const { url, setUrl } = useVideoPlayer(); + const id = useMemo(() => { const match = block.content.match( /https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?([\w\-=]+)/, @@ -18,7 +24,7 @@ const CommentEmbedBlock: FC<Props> = memo(({ block }) => { return (match && match[1]) || ''; }, [block.content]); - const url = useMemo(() => `https://youtube.com/watch?v=${id}`, [id]); + const address = `https://youtube.com/watch?v=${id}`; const preview = useMemo( () => getYoutubeThumb(block.content), @@ -28,21 +34,46 @@ const CommentEmbedBlock: FC<Props> = memo(({ block }) => { const metadata = useYoutubeMetadata(id); const title = metadata?.metadata?.title || ''; + const onClick = useCallback(() => { + if (isTablet) { + window.open(address, '_blank'); + return; + } + + setUrl(address); + }, [isTablet, setUrl, address]); + + const closeVideo = useCallback(() => setUrl(''), [setUrl]); + return ( <div className={styles.embed}> - <a href={url} target="_blank" rel="noreferrer" /> - - <div className={styles.preview}> - <div style={{ backgroundImage: `url("${preview}")` }}> - <div className={styles.backdrop}> - <div className={styles.play}> - <Icon icon="play" size={32} /> - </div> - - <div className={styles.title}>{title}</div> + {url === address ? ( + <div className={styles.video}> + <div className={styles.close} onClick={closeVideo}> + <Icon icon="close" /> + </div> + <div className={styles.animation}> + <CommentVideoFrame id={id} title={title} /> </div> </div> - </div> + ) : ( + <div + className={styles.preview} + role="button" + onClick={onClick} + tabIndex={-1} + > + <div style={{ backgroundImage: `url("${preview}")` }}> + <div className={styles.backdrop}> + <div className={styles.play}> + <Icon icon="play" size={32} /> + </div> + + <div className={styles.title}>{title}</div> + </div> + </div> + </div> + )} </div> ); }); diff --git a/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/styles.module.scss b/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/styles.module.scss index a8e0520b..5c033998 100644 --- a/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/styles.module.scss +++ b/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/styles.module.scss @@ -1,8 +1,8 @@ @import 'src/styles/variables'; .embed { - padding: 0 $gap; - height: $comment_height; + padding: 0 0; + min-height: $comment_height; width: 100%; box-sizing: border-box; background: 50% 50% no-repeat; @@ -69,6 +69,7 @@ justify-content: stretch; box-sizing: border-box; z-index: 2; + cursor: pointer; & > div { width: 100%; @@ -98,3 +99,47 @@ overflow: hidden; text-overflow: ellipsis; } + +@keyframes appear { + 0% { + grid-template-columns: 0fr; + opacity: 0; + } + + 50% { + grid-template-columns: 1fr; + } + + 100% { + opacity: 1; + } +} + +.video { + width: 100%; + position: relative; + padding: $gap / 2; +} + +.close { + display: flex; + align-items: center; + justify-content: center; + background: var(--color_danger); + width: 64px; + height: 24px; + position: absolute; + bottom: calc(100% - #{$gap / 2}); + right: 24px; + border-radius: $radius $radius 0 0; + z-index: 10; + cursor: pointer; +} + +.animation { + background-color: var(--content_bg_darker); + display: grid; + animation: appear 0.5s forwards; + width: 100%; + border-radius: $radius; +} diff --git a/src/containers/node/NodeComments/index.tsx b/src/containers/node/NodeComments/index.tsx index 816f6d19..fa39d493 100644 --- a/src/containers/node/NodeComments/index.tsx +++ b/src/containers/node/NodeComments/index.tsx @@ -15,6 +15,7 @@ import { useCommentContext } from '~/utils/context/CommentContextProvider'; import { useNodeContext } from '~/utils/context/NodeContextProvider'; import { useUserContext } from '~/utils/context/UserContextProvider'; import { canEditComment, canLikeComment } from '~/utils/node'; +import { VideoPlayerProvider } from '~/utils/providers/VideoPlayerProvider'; import styles from './styles.module.scss'; @@ -84,38 +85,40 @@ const NodeComments: FC<Props> = observer(({ order }) => { }, [isLoading]); return ( - <div className={styles.wrap}> - {order === 'DESC' && more} + <VideoPlayerProvider> + <div className={styles.wrap}> + {order === 'DESC' && more} - {groupped.map((group, index) => ( - <> - {isFirstGroupWithNewComment(group, groupped[index - 1]) && ( - <a - id={NEW_COMMENT_ANCHOR_NAME} - className={styles.newCommentAnchor} + {groupped.map((group, index) => ( + <> + {isFirstGroupWithNewComment(group, groupped[index - 1]) && ( + <a + id={NEW_COMMENT_ANCHOR_NAME} + className={styles.newCommentAnchor} + /> + )} + + <Comment + nodeId={node.id!} + key={group.ids.join()} + group={group} + highlighted={ + node.id === BORIS_NODE_ID && group.user.id === ANNOUNCE_USER_ID + } + onLike={onLike} + canLike={canLikeComment(group, user)} + canEdit={canEditComment(group, user)} + onDelete={onDeleteComment} + onShowImageModal={onShowImageModal} + isSame={group.user.id === user.id} + saveComment={onSaveComment} /> - )} + </> + ))} - <Comment - nodeId={node.id!} - key={group.ids.join()} - group={group} - highlighted={ - node.id === BORIS_NODE_ID && group.user.id === ANNOUNCE_USER_ID - } - onLike={onLike} - canLike={canLikeComment(group, user)} - canEdit={canEditComment(group, user)} - onDelete={onDeleteComment} - onShowImageModal={onShowImageModal} - isSame={group.user.id === user.id} - saveComment={onSaveComment} - /> - </> - ))} - - {order === 'ASC' && more} - </div> + {order === 'ASC' && more} + </div> + </VideoPlayerProvider> ); }); diff --git a/src/utils/providers/VideoPlayerProvider.tsx b/src/utils/providers/VideoPlayerProvider.tsx new file mode 100644 index 00000000..4f12345a --- /dev/null +++ b/src/utils/providers/VideoPlayerProvider.tsx @@ -0,0 +1,17 @@ +import { createContext, ReactNode, useContext, useState } from 'react'; + +const Context = createContext({ + url: '', + setUrl: (val: string) => {}, +}); + +/** Provides context for comment video playing */ +export const VideoPlayerProvider = ({ children }: { children: ReactNode }) => { + const [url, setUrl] = useState(''); + + return ( + <Context.Provider value={{ url, setUrl }}>{children}</Context.Provider> + ); +}; + +export const useVideoPlayer = () => useContext(Context); diff --git a/yarn.lock b/yarn.lock index 51753816..30bb1ef1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2626,6 +2626,13 @@ react-popper@^2.2.3: react-fast-compare "^3.0.1" warning "^4.0.2" +react-resize-detector@^12.0.2: + version "12.0.2" + resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-12.0.2.tgz#5e65f906a85835d246de57dbf608bf22ab333cad" + integrity sha512-aAI4WxWAysWLhA8wKDpsS+PnnxQ0lWCkTlk2t+2ijalWvoSa7vPxmcKRLURkH+PU84QE4KP4dO58oVP3ypWkKA== + dependencies: + lodash "^4.17.21" + react-router-dom@^5.1.2: version "5.3.0" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.0.tgz#da1bfb535a0e89a712a93b97dd76f47ad1f32363" From 1281a3c5958df0673a26f76d2e8f7f559eebfc14 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Mon, 24 Mar 2025 15:41:23 +0700 Subject: [PATCH 24/29] fix images pattern --- .drone.yml | 4 ++-- .env.local | 4 ++-- next.config.js | 48 ++++++++++++++++++++++++++---------------------- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/.drone.yml b/.drone.yml index 0d9eefa7..d8cf63c1 100644 --- a/.drone.yml +++ b/.drone.yml @@ -13,8 +13,8 @@ steps: branch: - master 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/next.config.js b/next.config.js index 8fb923d7..64e56747 100644 --- a/next.config.js +++ b/next.config.js @@ -2,7 +2,10 @@ 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({ @@ -10,36 +13,37 @@ module.exports = withBundleAnalyzer( 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: '/**', + }, + ], + }, + }), ); From f083b488ba6c07f0952aefc43cc6ab1068a9fa42 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Mon, 24 Mar 2025 15:47:11 +0700 Subject: [PATCH 25/29] add forgejo workflow --- .forgejo/workflows/build.yml | 46 ++++++++++++++++++++++++++++++++++++ src/pages/node/[id].tsx | 4 +++- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .forgejo/workflows/build.yml diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 00000000..25d067df --- /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/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/src/pages/node/[id].tsx b/src/pages/node/[id].tsx index 57a50fcd..26c829e6 100644 --- a/src/pages/node/[id].tsx +++ b/src/pages/node/[id].tsx @@ -49,7 +49,9 @@ export const getStaticPaths = async () => { .map((it) => it.id!.toString()); return { - paths: recentIDs.map((id) => ({ params: { id } })), + // this was generating too much garbage, so skip it + // paths: recentIDs.map((id) => ({ params: { id } })), + paths: [], fallback: true, }; }; From a676e98174d57615ac86370a97fe90ac251966b3 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Mon, 24 Mar 2025 17:42:00 +0700 Subject: [PATCH 26/29] add standalone build --- .dockerignore | 2 + .drone.yml | 2 +- .forgejo/workflows/build.yml | 2 +- docker/nextjs-standalone/Dockerfile | 51 +++++++++++++++++++++ next.config.js | 1 + src/containers/dialogs/PhotoSwipe/index.tsx | 1 + yarn.lock | 6 +-- 7 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 docker/nextjs-standalone/Dockerfile 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 d8cf63c1..695a9504 100644 --- a/.drone.yml +++ b/.drone.yml @@ -11,7 +11,7 @@ steps: image: plugins/docker when: branch: - - master + - never environment: NEXT_PUBLIC_API_HOST: https://vault48.org/api/ NEXT_PUBLIC_REMOTE_CURRENT: https://vault48.org/static/ diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 25d067df..1167a419 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -35,7 +35,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - file: ./docker/nextjs/Dockerfile + file: ./docker/nextjs-standalone/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} 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 64e56747..c95c6aa1 100644 --- a/next.config.js +++ b/next.config.js @@ -9,6 +9,7 @@ const withTM = require('next-transpile-modules')([ module.exports = withBundleAnalyzer( withTM({ + output: 'standalone', /** rewrite old-style node paths */ async rewrites() { return [ diff --git a/src/containers/dialogs/PhotoSwipe/index.tsx b/src/containers/dialogs/PhotoSwipe/index.tsx index f5d4098c..0872b2cc 100644 --- a/src/containers/dialogs/PhotoSwipe/index.tsx +++ b/src/containers/dialogs/PhotoSwipe/index.tsx @@ -58,6 +58,7 @@ const PhotoSwipe = observer(({ index, items }: Props) => { return () => { pswp.current?.off('close', hideModal); + // eslint-disable-next-line react-hooks/exhaustive-deps pswp.current?.destroy(); }; }, [hideModal, items, index, isTablet]); diff --git a/yarn.lock b/yarn.lock index 30bb1ef1..464da01d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -803,9 +803,9 @@ callsites@^3.0.0: integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== caniuse-lite@^1.0.30001332: - version "1.0.30001564" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001564.tgz" - integrity sha512-DqAOf+rhof+6GVx1y+xzbFPeOumfQnhYzVnZD6LAXijR77yPtm9mfOcqOnT3mpnJiZVT+kwLAFnRlZcIz+c6bg== + version "1.0.30001707" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz" + integrity sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw== chalk@^2.0.0: version "2.4.2" From 4d55906ae8b9f2879e390d75a2f550a5507b7ccb Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Wed, 2 Apr 2025 20:42:40 +0700 Subject: [PATCH 27/29] fix trailing slashes --- src/constants/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/api.ts b/src/constants/api.ts index 6fa89637..29c28e36 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -7,7 +7,7 @@ export const API = { USER: { LOGIN: '/auth', OAUTH_WINDOW: (provider: OAuthProvider) => - `${CONFIG.apiHost}oauth/${provider}/redirect`, + `${CONFIG.apiHost}oauth/${provider}/redirect/`, ME: '/auth', UPDATE_PHOTO: '/auth/photo', UPDATE_COVER: '/auth/photo', From 29c8bcd14578247878b9639e19679fe9313c29d9 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Fri, 4 Apr 2025 13:55:29 +0700 Subject: [PATCH 28/29] fix images in comment atachments was having wrong height --- .../components/CommentImageGrid/styles.module.scss | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentImageGrid/styles.module.scss b/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentImageGrid/styles.module.scss index de654b99..3a45aad6 100644 --- a/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentImageGrid/styles.module.scss +++ b/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentImageGrid/styles.module.scss @@ -7,7 +7,7 @@ &.multiple { // Desktop devices - @include flexbin(25vh, $flexbin-space); + @include flexbin(300px, $flexbin-space); // Tablet devices @media (max-width: $flexbin-tablet-max) { @@ -22,13 +22,16 @@ } .image { - max-height: 500px; + max-height: 300px; border-radius: $radius; max-width: 100%; .multiple & { - max-height: 250px; - max-inline-size: 250px; + // both of that were 250px, + // if you know why it should be like this, tell me + // it messes up with the flexbin above + max-height: 300px; + max-inline-size: 300px; } } From 2a0adb26e03d432a0228d56df7405a46f56b2152 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Sat, 5 Apr 2025 14:33:01 +0700 Subject: [PATCH 29/29] add sansevieria theme --- public/images/sansivieria.svg | 752 ++++++++++++++++++ src/constants/themes/index.ts | 10 + .../CommentEmbedBlock/styles.module.scss | 2 +- .../settings/ThemeSwitcher/index.tsx | 2 +- src/styles/_global.scss | 2 + src/styles/themes/_sanseviria.scss | 51 ++ src/utils/providers/ThemeProvider.tsx | 1 + 7 files changed, 818 insertions(+), 2 deletions(-) create mode 100644 public/images/sansivieria.svg create mode 100644 src/styles/themes/_sanseviria.scss 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 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="1920" + height="1080" + viewBox="0 0 508 285.75" + version="1.1" + id="svg1" + inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + sodipodi:docname="sansivieria.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview1" + pagecolor="#101315" + bordercolor="#2a2a2a" + borderopacity="1" + inkscape:showpageshadow="0" + inkscape:pageopacity="0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#101315" + inkscape:document-units="mm" + inkscape:zoom="0.5" + inkscape:cx="977" + inkscape:cy="444" + inkscape:window-width="1920" + inkscape:window-height="1011" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="layer1" /> + <defs + id="defs1"> + <linearGradient + id="linearGradient16" + inkscape:collect="always"> + <stop + style="stop-color:#222d2f;stop-opacity:1;" + offset="0" + id="stop17" /> + <stop + style="stop-color:#222d2f;stop-opacity:0;" + offset="1" + id="stop18" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient10" + cx="14.584812" + cy="82.411865" + fx="14.584812" + fy="82.411865" + r="6.6161571" + gradientTransform="matrix(16.722314,0.28277544,-0.23964041,14.171465,-209.55779,-1089.6093)" + gradientUnits="userSpaceOnUse" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient12" + cx="125.84482" + cy="74.220642" + fx="125.84482" + fy="74.220642" + r="37.123039" + gradientTransform="matrix(1.4233343,-0.04031753,0.06568704,2.3189569,-58.149762,-94.310274)" + gradientUnits="userSpaceOnUse" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient14" + cx="49.86562" + cy="41.432327" + fx="49.86562" + fy="41.432327" + r="11.167304" + gradientTransform="matrix(3.311949,0.13402602,-0.12963621,3.2034712,-109.91564,-96.433414)" + gradientUnits="userSpaceOnUse" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient16" + cx="106.77225" + cy="129.32372" + fx="106.77225" + fy="129.32372" + r="14.686029" + gradientTransform="matrix(1,0,0,2.4664414,0,-190.75799)" + gradientUnits="userSpaceOnUse" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient18" + cx="29.229187" + cy="220.45612" + fx="29.229187" + fy="220.45612" + r="17.13831" + gradientTransform="matrix(3.4889397,0.0218328,-0.03388246,5.4145064,-60.182826,-1005.8674)" + gradientUnits="userSpaceOnUse" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient19" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(16.535647,2.5076138,-2.1250983,14.013272,-43.219263,-1048.7787)" + cx="14.584812" + cy="82.411865" + fx="14.584812" + fy="82.411865" + r="6.6161571" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient21" + cx="67.744797" + cy="80.206696" + fx="67.744797" + fy="80.206696" + r="10.474073" + gradientTransform="matrix(1,0,0,6.1194546,0,-409.67547)" + gradientUnits="userSpaceOnUse" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient22" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(16.647228,-1.6079609,1.36268,14.107833,15.35168,-1041.1789)" + cx="14.584812" + cy="82.411865" + fx="14.584812" + fy="82.411865" + r="6.6161571" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient23" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(29.974473,5.5523576,-6.7662765,17.665097,379.82488,-1380.4222)" + cx="14.584812" + cy="82.411865" + fx="14.584812" + fy="82.411865" + r="6.6161571" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient24" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,6.1194546,394.75833,-326.5963)" + cx="67.744797" + cy="80.206696" + fx="67.744797" + fy="80.206696" + r="10.474073" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient25" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,2.4664414,252.4125,-155.83299)" + cx="106.77225" + cy="129.32372" + fx="106.77225" + fy="129.32372" + r="14.686029" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient26" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(14.749655,7.8843782,-6.6816823,12.499719,804.1388,-1075.9347)" + cx="14.584812" + cy="82.411865" + fx="14.584812" + fy="82.411865" + r="6.6161571" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient27" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(3.311949,0.13402602,-0.12963621,3.2034712,320.29686,104.12075)" + cx="49.86562" + cy="41.432327" + fx="49.86562" + fy="41.432327" + r="11.167304" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient28" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.4233343,-0.04031753,0.06568704,2.3189569,206.43357,95.660559)" + cx="125.84482" + cy="74.220642" + fx="125.84482" + fy="74.220642" + r="37.123039" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient29" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,2.4664414,335.49167,-275.95382)" + cx="106.77225" + cy="129.32372" + fx="106.77225" + fy="129.32372" + r="14.686029" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient30" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.98984555,0.14214704,-0.86986237,6.0573149,403.60754,-406.5492)" + cx="67.744797" + cy="80.206696" + fx="67.744797" + fy="80.206696" + r="10.474073" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient31" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.3259837,-0.51891557,0.84543937,2.1603491,-23.826148,-41.823374)" + cx="125.84482" + cy="74.220642" + fx="125.84482" + fy="74.220642" + r="37.123039" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient32" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.85314014,0.52168181,-1.2866976,2.1042201,266.4049,-230.63907)" + cx="106.77225" + cy="129.32372" + fx="106.77225" + fy="129.32372" + r="14.686029" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient33" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,6.1194546,201.6125,-463.65047)" + cx="67.744797" + cy="80.206696" + fx="67.744797" + fy="80.206696" + r="10.474073" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient34" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,6.1194546,363.5375,-277.3838)" + cx="67.744797" + cy="80.206696" + fx="67.744797" + fy="80.206696" + r="10.474073" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient35" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(3.311949,0.13402602,-0.12963621,3.2034712,67.88436,-113.89592)" + cx="49.86562" + cy="41.432327" + fx="49.86562" + fy="41.432327" + r="11.167304" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient36" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,6.1194546,93.6625,-251.9838)" + cx="67.744797" + cy="80.206696" + fx="67.744797" + fy="80.206696" + r="10.474073" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient37" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(14.527707,8.2862231,-7.0222289,12.311626,456.5098,-892.3642)" + cx="14.584812" + cy="82.411865" + fx="14.584812" + fy="82.411865" + r="6.6161571" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient38" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(16.679888,1.223556,-1.0369127,14.135511,161.35064,-1106.3396)" + cx="14.584812" + cy="82.411865" + fx="14.584812" + fy="82.411865" + r="6.6161571" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient39" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.8382036,0.44885537,-1.9792444,2.5359589,232.64281,-265.6639)" + cx="106.77225" + cy="129.32372" + fx="106.77225" + fy="129.32372" + r="14.686029" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient40" + cx="72.206665" + cy="181.65135" + fx="72.206665" + fy="181.65135" + r="35.266216" + gradientTransform="matrix(1.4063187,-1.2240507,0.2942198,0.33803077,-84.911033,213.79688)" + gradientUnits="userSpaceOnUse" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient41" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(2.3563545,2.5741948,-0.48133443,0.72808163,56.169463,-297.28826)" + cx="72.206665" + cy="181.65135" + fx="72.206665" + fy="181.65135" + r="35.266216" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient42" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(40.365604,0.28277544,-0.5784624,14.171465,-345.36595,-960.51812)" + cx="14.630581" + cy="83.018234" + fx="14.630581" + fy="83.018234" + r="6.6161571" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient16" + id="radialGradient43" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.1265465,-3.3029953,0.86977819,0.07260568,39.478992,494.56215)" + cx="72.206665" + cy="181.65135" + fx="72.206665" + fy="181.65135" + r="35.266216" /> + </defs> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + style="opacity:0.189"> + <path + style="opacity:1;fill:url(#radialGradient10);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke" + d="M 7.9686552,-11.362181 C 24.186436,4.9096141 20.925377,139.36464 10.35345,176.18591 30.87243,106.69149 15.979892,-6.6575459 15.979892,-6.6575459 Z" + id="path1" + sodipodi:nodetypes="cccc" /> + <path + style="opacity:1;fill:url(#radialGradient12);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke" + d="M 77.496457,-10.771584 C 98.675378,-4.1423852 147.30523,120.84028 151.74254,161.47094 143.99181,89.426312 86.700266,-9.505318 86.700266,-9.505318 Z" + id="path3" + sodipodi:nodetypes="cccc" /> + <path + style="opacity:1;fill:url(#radialGradient21);fill-opacity:1;stroke-width:0.278221;paint-order:markers fill stroke" + d="M 50.909713,144.11888 C 75.767669,75.5024 71.594125,15.927649 71.594125,15.927649 72.487753,19.967458 71.20699,94.882544 50.909713,144.11888 Z" + id="path4" + sodipodi:nodetypes="ccc" /> + <path + style="opacity:1;fill:url(#radialGradient16);fill-opacity:1;stroke-width:0.252673;paint-order:markers fill stroke" + d="M 112.47803,166.30448 C 109.99767,110.56088 83.105972,93.860658 83.105972,93.860658 84.614446,93.705287 111.63798,121.99159 112.47803,166.30448 Z" + id="path5" + sodipodi:nodetypes="ccc" /> + <path + style="opacity:1;fill:url(#radialGradient14);fill-opacity:1;stroke-width:0.236032;paint-order:markers fill stroke" + d="M 33.085655,76.535494 C 62.698491,36.591224 54.328663,4.9286614 54.328663,4.9286614 55.446727,5.6159474 57.511667,44.610258 33.085655,76.535494 Z" + id="path6" + sodipodi:nodetypes="ccc" /> + <path + style="opacity:1;fill:url(#radialGradient18);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke" + d="M 20.245001,320.5176 C 6.7844252,301.90038 31.12444,169.62661 47.34881,134.92299 16.168746,200.33176 13.07219,314.613 13.07219,314.613 Z" + id="path8" + sodipodi:nodetypes="cccc" /> + <path + style="opacity:1;fill:url(#radialGradient19);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke" + d="M 28.749908,48.834683 C 42.655832,67.121646 21.514857,199.94426 6.1326556,235.02929 35.725236,168.88716 36.06312,54.564469 36.06312,54.564469 Z" + id="path18" + sodipodi:nodetypes="cccc" /> + <path + style="opacity:1;fill:url(#radialGradient22);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke" + d="M 353.28303,5.5957663 C 371.23504,19.931483 383.18269,153.89432 376.83769,191.67411 389.37536,120.30668 361.77441,9.3653543 361.77441,9.3653543 Z" + id="path21" + sodipodi:nodetypes="cccc" /> + <path + style="opacity:1;fill:url(#radialGradient23);fill-opacity:1;stroke-width:0.409226;paint-order:markers fill stroke" + d="M 289.38487,36.930288 C 311.30374,62.340577 245.32976,229.63528 209.84002,272.44105 277.84039,191.82778 301.70235,45.309878 301.70235,45.309878 Z" + id="path22" + sodipodi:nodetypes="cccc" /> + <path + style="opacity:1;fill:url(#radialGradient24);fill-opacity:1;stroke-width:0.278221;paint-order:markers fill stroke" + d="M 445.66805,227.19805 C 470.526,158.58157 466.35246,99.006816 466.35246,99.006816 c 0.89363,4.039804 -0.38714,78.954894 -20.68441,128.191234 z" + id="path23" + sodipodi:nodetypes="ccc" /> + <path + style="opacity:1;fill:url(#radialGradient25);fill-opacity:1;stroke-width:0.252673;paint-order:markers fill stroke" + d="m 364.89053,201.22948 c -2.48036,-55.7436 -29.37206,-72.44382 -29.37206,-72.44382 1.50848,-0.15537 28.53201,28.13093 29.37206,72.44382 z" + id="path24" + sodipodi:nodetypes="ccc" /> + <path + style="opacity:1;fill:url(#radialGradient26);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke" + d="M 505.52554,-17.272921 C 512.52821,4.6074413 448.25566,122.75069 422.04247,150.68704 472.01951,98.219928 510.50617,-9.4302867 510.50617,-9.4302867 Z" + id="path25" + sodipodi:nodetypes="cccc" /> + <path + style="opacity:1;fill:url(#radialGradient27);fill-opacity:1;stroke-width:0.236032;paint-order:markers fill stroke" + d="m 463.29815,277.08966 c 29.61284,-39.94427 21.24301,-71.60683 21.24301,-71.60683 1.11807,0.68728 3.18301,39.68159 -21.24301,71.60683 z" + id="path26" + sodipodi:nodetypes="ccc" /> + <path + style="opacity:1;fill:url(#radialGradient28);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke" + d="m 342.07979,179.19925 c 21.17892,6.6292 69.80877,131.61186 74.24608,172.24252 C 408.57514,279.39715 351.2836,180.46552 351.2836,180.46552 Z" + id="path27" + sodipodi:nodetypes="cccc" /> + <path + style="opacity:1;fill:url(#radialGradient29);fill-opacity:1;stroke-width:0.252673;paint-order:markers fill stroke" + d="M 447.9697,81.108647 C 445.48934,25.365047 418.59764,8.6648247 418.59764,8.6648247 420.10611,8.5094537 447.12965,36.795757 447.9697,81.108647 Z" + id="path28" + sodipodi:nodetypes="ccc" /> + <path + style="opacity:1;fill:url(#radialGradient30);fill-opacity:1;stroke-width:0.278221;paint-order:markers fill stroke" + d="M 375.28008,148.85834 C 409.63924,84.472103 413.97645,24.909041 413.97645,24.909041 414.28676,29.03485 402.37004,103.00716 375.28008,148.85834 Z" + id="path29" + sodipodi:nodetypes="ccc" /> + <path + style="opacity:1;fill:url(#radialGradient31);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke" + d="M 132.06997,-9.0360324 C 154.24318,-9.9535204 242.24625,91.244241 260.15235,127.98539 228.51241,62.797848 141.16027,-10.954379 141.16027,-10.954379 Z" + id="path30" + sodipodi:nodetypes="cccc" /> + <path + style="opacity:1;fill:url(#radialGradient32);fill-opacity:1;stroke-width:0.252673;paint-order:markers fill stroke" + d="m 176.09143,132.66299 c 26.96433,-48.851061 12.73415,-77.127601 12.73415,-77.127601 1.36799,0.654388 9.6664,38.884257 -12.73415,77.127601 z" + id="path31" + sodipodi:nodetypes="ccc" /> + <path + style="opacity:1;fill:url(#radialGradient33);fill-opacity:1;stroke-width:0.278221;paint-order:markers fill stroke" + d="m 252.52221,90.14388 c 24.85796,-68.61648 20.68441,-128.191231 20.68441,-128.191231 0.89363,4.039809 -0.38713,78.954895 -20.68441,128.191231 z" + id="path32" + sodipodi:nodetypes="ccc" /> + <path + style="opacity:1;fill:url(#radialGradient34);fill-opacity:1;stroke-width:0.278221;paint-order:markers fill stroke" + d="m 414.44721,276.41055 c 24.85796,-68.61648 20.68441,-128.19123 20.68441,-128.19123 0.89363,4.0398 -0.38713,78.95489 -20.68441,128.19123 z" + id="path33" + sodipodi:nodetypes="ccc" /> + <path + style="opacity:1;fill:url(#radialGradient35);fill-opacity:1;stroke-width:0.236032;paint-order:markers fill stroke" + d="m 210.88565,59.072993 c 29.61284,-39.94427 21.24301,-71.60683 21.24301,-71.60683 1.11807,0.68728 3.18301,39.68159 -21.24301,71.60683 z" + id="path34" + sodipodi:nodetypes="ccc" /> + <path + style="opacity:1;fill:url(#radialGradient36);fill-opacity:1;stroke-width:0.278221;paint-order:markers fill stroke" + d="m 144.57221,301.81055 c 24.85796,-68.61648 20.68441,-128.19123 20.68441,-128.19123 0.89363,4.0398 -0.38713,78.95489 -20.68441,128.19123 z" + id="path35" + sodipodi:nodetypes="ccc" /> + <path + style="opacity:1;fill:url(#radialGradient37);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke" + d="m 128.95328,157.70314 c 6.39951,22.06432 -61.091348,138.39905 -88.06141,165.60544 51.39821,-51.07569 92.82491,-157.62907 92.82491,-157.62907 z" + id="path36" + sodipodi:nodetypes="cccc" /> + <path + style="opacity:1;fill:url(#radialGradient38);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke" + d="m 317.84224,-17.558062 c 15.27619,17.15883035 4.4524,151.217152 -8.17529,187.384992 24.398,-68.22932 15.90901,-182.236897 15.90901,-182.236897 z" + id="path37" + sodipodi:nodetypes="cccc" /> + <path + style="opacity:1;fill:url(#radialGradient39);fill-opacity:1;stroke-width:0.379027;paint-order:markers fill stroke" + d="m 152.86855,151.94887 c 40.17315,-58.428075 4.14217,-87.669483 4.14217,-87.669483 2.89756,0.517335 29.87342,41.730563 -4.14217,87.669483 z" + id="path38" + sodipodi:nodetypes="ccc" /> + <path + style="opacity:1;fill:url(#radialGradient40);stroke-width:0.264583;paint-order:markers fill stroke" + d="m 49.382576,217.43743 c 0,0 -6.653671,-14.88405 8.304578,-22.20819 14.958243,-7.32414 13.273203,-8.13708 15.367897,-13.85674 2.094691,-5.71967 -1.198008,-13.70608 9.063795,-15.85949 10.261807,-2.15341 18.283724,-2.9363 18.283724,-2.9363 0,0 -19.814459,1.20074 -21.594628,5.49345 -1.780168,4.29271 -3.367799,16.28098 -5.50391,19.07253 -2.136113,2.79156 -16.533591,8.35055 -19.322419,12.32281 -2.788824,3.97228 -5.515291,6.43747 -4.599037,17.97193 z" + id="path39" /> + <path + style="opacity:1;fill:url(#radialGradient41);stroke-width:0.497551;paint-order:markers fill stroke" + d="m 83.525533,-13.021216 c 0,0 22.757677,-18.774764 38.480827,10.4224151 15.72313,29.1971799 16.63815,25.3856049 26.53783,27.9768449 9.8997,2.591216 22.17978,-6.897867 28.25778,14.048196 6.07803,20.946062 9.35264,37.607416 9.35264,37.607416 0,0 -6.88631,-41.388032 -14.36784,-43.771411 -7.4815,-2.383401 -27.53547,-1.905957 -32.64353,-5.518604 -5.10808,-3.612626 -17.79732,-32.1907473 -25.00364,-36.8027354 -7.20635,-4.6119666 -11.92571,-9.5732386 -30.614067,-3.9621216 z" + id="path40" /> + <path + style="opacity:1;fill:url(#radialGradient42);fill-opacity:1;stroke-width:0.411073;paint-order:markers fill stroke" + d="m 179.71605,117.729 c 39.14772,16.27179 31.27592,150.72682 5.75661,187.54809 49.53029,-69.49442 13.58154,-182.84346 13.58154,-182.84346 z" + id="path41" + sodipodi:nodetypes="cccc" /> + <path + style="opacity:1;fill:url(#radialGradient43);stroke-width:0.497551;paint-order:markers fill stroke" + d="m 275.69438,334.06791 c 0,0 -27.39639,-10.94721 -9.33533,-38.7589 18.06108,-27.81169 14.2783,-26.78409 11.79767,-36.71206 -2.48065,-9.92799 -16.70081,-16.1433 -1.24602,-31.53255 15.45478,-15.38928 28.49726,-26.26206 28.49726,-26.26206 0,0 -32.99779,25.91404 -31.49597,33.62106 1.50178,7.70702 11.55003,25.06854 10.83388,31.28389 -0.71612,6.21537 -19.69098,31.06855 -20.27617,39.60429 -0.58517,8.53577 -2.67094,15.05774 11.22468,28.75633 z" + id="path42" /> + <g + id="g45" + style="fill:#222d2f;fill-opacity:1" + transform="matrix(0.46951769,0,0,0.46951769,41.781874,49.804731)"> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="path43" + cx="80.380508" + cy="86.709709" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse44" + cx="85.1054" + cy="87.707977" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse45" + cx="80.849136" + cy="92.244881" + rx="1.6184589" + ry="1.640871" /> + </g> + <g + id="g48" + transform="matrix(0.0163187,0.47012984,-0.47012984,0.0163187,270.01266,112.55728)" + style="fill:#222d2f;fill-opacity:1"> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse46" + cx="80.380508" + cy="86.709709" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse47" + cx="85.1054" + cy="87.707977" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse48" + cx="80.849136" + cy="92.244881" + rx="1.6184589" + ry="1.640871" /> + </g> + <g + id="g51" + transform="matrix(0.023208,0.66860544,-0.66860544,0.023208,180.70099,196.14232)" + style="fill:#222d2f;fill-opacity:1"> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse49" + cx="80.380508" + cy="86.709709" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse50" + cx="85.1054" + cy="87.707977" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse51" + cx="80.849136" + cy="92.244881" + rx="1.6184589" + ry="1.640871" /> + </g> + <g + id="g54" + transform="matrix(0.01520429,0.43802433,-0.43802433,0.01520429,471.34681,27.597705)" + style="fill:#222d2f;fill-opacity:1"> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse52" + cx="80.380508" + cy="86.709709" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse53" + cx="85.1054" + cy="87.707977" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse54" + cx="80.849136" + cy="92.244881" + rx="1.6184589" + ry="1.640871" /> + </g> + <g + id="g57" + transform="matrix(0.59664612,0.3078445,-0.3078445,0.59664612,475.59896,71.35259)" + style="fill:#222d2f;fill-opacity:1"> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse55" + cx="80.380508" + cy="86.709709" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse56" + cx="85.1054" + cy="87.707977" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse57" + cx="80.849136" + cy="92.244881" + rx="1.6184589" + ry="1.640871" /> + </g> + <g + id="g60" + style="fill:#222d2f;fill-opacity:1" + transform="matrix(0.46951769,0,0,0.46951769,207.54243,5.6518051)"> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse58" + cx="80.380508" + cy="86.709709" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse59" + cx="85.1054" + cy="87.707977" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse60" + cx="80.849136" + cy="92.244881" + rx="1.6184589" + ry="1.640871" /> + </g> + <g + id="g63" + style="fill:#222d2f;fill-opacity:1" + transform="matrix(0.46951769,0,0,0.46951769,128.96519,22.489785)"> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse61" + cx="80.380508" + cy="86.709709" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse62" + cx="85.1054" + cy="87.707977" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse63" + cx="80.849136" + cy="92.244881" + rx="1.6184589" + ry="1.640871" /> + </g> + <g + id="g66" + style="fill:#222d2f;fill-opacity:1" + transform="matrix(0.46951769,0,0,0.46951769,282.75208,196.48225)"> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse64" + cx="80.380508" + cy="86.709709" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse65" + cx="85.1054" + cy="87.707977" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse66" + cx="80.849136" + cy="92.244881" + rx="1.6184589" + ry="1.640871" /> + </g> + <g + id="g69" + style="fill:#222d2f;fill-opacity:1" + transform="matrix(0.46951769,0,0,0.46951769,359.08426,164.67718)"> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse67" + cx="80.380508" + cy="86.709709" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse68" + cx="85.1054" + cy="87.707977" + rx="1.6184589" + ry="1.640871" /> + <ellipse + style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke" + id="ellipse69" + cx="80.849136" + cy="92.244881" + rx="1.6184589" + ry="1.640871" /> + </g> + </g> +</svg> diff --git a/src/constants/themes/index.ts b/src/constants/themes/index.ts index ab72f641..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 { @@ -28,4 +29,13 @@ export const themeColors: Record<Theme, ThemeColors> = { ], 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/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/styles.module.scss b/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/styles.module.scss index 5c033998..cd91dcf2 100644 --- a/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/styles.module.scss +++ b/src/containers/node/NodeComments/components/Comment/components/CommentContent/components/CommentEmbedBlock/styles.module.scss @@ -43,7 +43,7 @@ left: 0; width: 100%; height: 100%; - background: $content_bg_backdrop 50% 50%; + background: $content_bg_backdrop; background-size: cover; z-index: 15; border-radius: $radius; diff --git a/src/containers/settings/ThemeSwitcher/index.tsx b/src/containers/settings/ThemeSwitcher/index.tsx index cca45663..1ecb45c2 100644 --- a/src/containers/settings/ThemeSwitcher/index.tsx +++ b/src/containers/settings/ThemeSwitcher/index.tsx @@ -28,7 +28,7 @@ const ThemeSwitcher: FC<ThemeSwitcherProps> = () => { > <Group> <div className={styles.palette}> - {item.colors.map((color) => ( + {[...item.colors].reverse().map((color) => ( <div key={color} className={styles.sample} diff --git a/src/styles/_global.scss b/src/styles/_global.scss index 33f1f85a..8d469f08 100644 --- a/src/styles/_global.scss +++ b/src/styles/_global.scss @@ -1,5 +1,6 @@ @use './themes/default' as theme_default; @use './themes/horizon' as theme_horizon; +@use './themes/sanseviria' as theme_sanseviria; @import 'src/styles/variables'; @@ -12,6 +13,7 @@ @include theme_default.apply(); @include theme_horizon.apply(); +@include theme_sanseviria.apply(); html { min-height: 100vh; diff --git a/src/styles/themes/_sanseviria.scss b/src/styles/themes/_sanseviria.scss new file mode 100644 index 00000000..f9150b23 --- /dev/null +++ b/src/styles/themes/_sanseviria.scss @@ -0,0 +1,51 @@ +@mixin apply { + :root.theme-sansevieria { + --color_primary: #e28166; + --color_danger: rgb(180, 109, 99); + --color_online: #1eb1ac; + --color_offline: #a3584b; + --color_link: #7199d7; + --color_like: #d56c68; + --color_flow: rgb(123, 60, 65); + --color_lab: #2c2f4c; + --color_boris: #5c827f; + --danger_gradient: linear-gradient(165deg, #ff7e56 -50%, #280003 150%); + --info_gradient: linear-gradient(170deg, #476695, #22252d); + --warning_gradient: linear-gradient(165deg, #f4e7aa -50%, #a23500 150%); + --primary_gradient: linear-gradient(170deg, #fd9bce -150%, #59361c); + --magic_gradient: linear-gradient(260deg, #e95678 -50%, #ff7549 150%); + --global_loader_gradient: linear-gradient(90deg, #c9ab8e, #694b5a, #43040a); + --flow_gradient: var(--primary_gradient); + --lab_gradient: var(--info_gradient); + --content_bg: #181e1d; + --content_bg_dark: #181d1e; + --content_bg_darker: #151a13; + --content_bg_light: #23292b; + --content_bg_lighter: #2f3530; + --content_bg_lightest: #2e2c31; + --content_bg_success: #e956784d; + --content_bg_info: #fab7954d; + --content_bg_danger: #ff334480; + --content_bg_backdrop: 50% 50% / cover no-repeat + url('/images/sansivieria.svg') #1f2625dd; + --content_bg_hero: url('/images/noise.png') #4d322677; + --white: #fff; + --gray_25: #ffffffbf; + --gray_50: #ffffff80; + --gray_75: #ffffff40; + --gray_90: #ffffff0d; + --page-background: 50% 50% / cover no-repeat url('/images/sansivieria.svg') + #101315 fixed; + --page-background-top: linear-gradient( + #050505 -150%, + #25b0bc03 100px, + #25b0bc00 200px + ); + --boris-background: linear-gradient( + 170deg, + #080332 -150%, + #54302850 250px, + #00000000 600px + ); + } +} diff --git a/src/utils/providers/ThemeProvider.tsx b/src/utils/providers/ThemeProvider.tsx index 556bec1a..ae748f31 100644 --- a/src/utils/providers/ThemeProvider.tsx +++ b/src/utils/providers/ThemeProvider.tsx @@ -21,6 +21,7 @@ const ThemeContext = createContext({ const themeClass: Record<Theme, string> = { [Theme.Default]: '', [Theme.Horizon]: 'theme-horizon', + [Theme.Sansevieria]: 'theme-sansevieria', }; const ThemeProvider: FC<ProvidersProps> = ({ children }) => {