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 }) => {