1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-28 22:26:41 +07:00

Compare commits

..

No commits in common. "71306d4c142c62c41b341dbe2f9d253f9f5a6df6" and "5ef19f49c51e214f048fb3a9b496934d34a55a27" have entirely different histories.

77 changed files with 305 additions and 545 deletions

View file

@ -1,7 +1,6 @@
module.exports = { module.exports = {
extends: ['plugin:react/recommended', 'plugin:@next/next/recommended'], extends: ['plugin:react/recommended', 'plugin:@next/next/recommended'],
rules: { rules: {
'prettier/prettier': 'error',
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
'react-hooks/exhaustive-deps': 'warn', // Checks effect dependencies 'react-hooks/exhaustive-deps': 'warn', // Checks effect dependencies
'react/prop-types': 0, 'react/prop-types': 0,
@ -10,21 +9,13 @@ module.exports = {
'@next/next/no-img-element': 0, '@next/next/no-img-element': 0,
'unused-imports/no-unused-imports': 'warn', 'unused-imports/no-unused-imports': 'warn',
// 'no-unused-vars': 'warn', // 'no-unused-vars': 'warn',
quotes: [2, 'single', { avoidEscape: true }], 'quotes': [2, 'single', { 'avoidEscape': true }],
'import/order': [ 'import/order': [
'error', 'error',
{ {
alphabetize: { order: 'asc' }, alphabetize: { order: 'asc' },
'newlines-between': 'always', 'newlines-between': 'always',
groups: [ groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'unknown'],
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
'unknown',
],
pathGroups: [ pathGroups: [
{ {
pattern: 'react', pattern: 'react',
@ -43,17 +34,18 @@ module.exports = {
paths: [ paths: [
{ {
name: 'ramda', name: 'ramda',
message: "import from '~/utils/ramda' instead", message:
'import from \'~/utils/ramda\' instead',
}, },
], ],
}, },
], ]
}, },
parserOptions: { parserOptions: {
ecmaVersion: 7, ecmaVersion: 7,
sourceType: 'module', sourceType: 'module',
}, },
plugins: ['import', 'react-hooks', 'unused-imports', 'prettier'], plugins: ['import', 'react-hooks', 'unused-imports'],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
settings: { settings: {
react: { react: {

View file

@ -92,14 +92,13 @@
"@typescript-eslint/parser": "^5.10.1", "@typescript-eslint/parser": "^5.10.1",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-plugin-import": "^2.25.4", "eslint-plugin-import": "^2.25.4",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.28.0", "eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-unused-imports": "^3.0.0", "eslint-plugin-unused-imports": "^3.0.0",
"husky": "^7.0.4", "husky": "^7.0.4",
"lint-staged": "^12.1.6", "lint-staged": "^12.1.6",
"next-transpile-modules": "^9.0.0", "next-transpile-modules": "^9.0.0",
"prettier": "^3.0.0" "prettier": "^2.7.1"
}, },
"lint-staged": { "lint-staged": {
"./**/*.{js,jsx,ts,tsx}": [ "./**/*.{js,jsx,ts,tsx}": [

View file

@ -14,7 +14,7 @@ interface Props extends DivProps {
username?: string; username?: string;
size?: number; size?: number;
hasUpdates?: boolean; hasUpdates?: boolean;
preset?: (typeof imagePresets)[keyof typeof imagePresets]; preset?: typeof imagePresets[keyof typeof imagePresets];
} }
const Avatar = forwardRef<HTMLDivElement, Props>( const Avatar = forwardRef<HTMLDivElement, Props>(

View file

@ -8,8 +8,6 @@ import { URLS } from '~/constants/urls';
import { INode } from '~/types'; import { INode } from '~/types';
import { getPrettyDate } from '~/utils/dom'; import { getPrettyDate } from '~/utils/dom';
import { getNewCommentAnchor } from '../../../constants/dom/links';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
interface Props { interface Props {
@ -18,11 +16,12 @@ interface Props {
onClick?: MouseEventHandler; onClick?: MouseEventHandler;
} }
const NodeHorizontalCard: FC<Props> = ({ node, hasNew, onClick }) => ( const NodeHorizontalCard: FC<Props> = ({ node, hasNew, onClick }) => {
return (
<Anchor <Anchor
key={node.id} key={node.id}
className={styles.item} className={styles.item}
href={getNewCommentAnchor(URLS.NODE_URL(node.id))} href={URLS.NODE_URL(node.id)}
onClick={onClick} onClick={onClick}
> >
<div <div
@ -43,5 +42,6 @@ const NodeHorizontalCard: FC<Props> = ({ node, hasNew, onClick }) => (
</div> </div>
</Anchor> </Anchor>
); );
};
export { NodeHorizontalCard }; export { NodeHorizontalCard };

View file

@ -9,8 +9,6 @@ import { Square } from '~/components/common/Square';
import { NotificationItem } from '~/types/notifications'; import { NotificationItem } from '~/types/notifications';
import { formatText, getURLFromString } from '~/utils/dom'; import { formatText, getURLFromString } from '~/utils/dom';
import { getCommentAnchor } from '../../../constants/dom/links';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
interface NotificationCommentProps { interface NotificationCommentProps {
@ -19,10 +17,7 @@ interface NotificationCommentProps {
} }
const NotificationComment: FC<NotificationCommentProps> = ({ item, isNew }) => ( const NotificationComment: FC<NotificationCommentProps> = ({ item, isNew }) => (
<Anchor <Anchor href={item.url} className={styles.link}>
href={isNew ? getCommentAnchor(item.url, item.itemId) : item.url}
className={styles.link}
>
<div className={classNames(styles.message, { [styles.new]: isNew })}> <div className={classNames(styles.message, { [styles.new]: isNew })}>
<div className={styles.icon}> <div className={styles.icon}>
<Avatar <Avatar

View file

@ -20,7 +20,7 @@ export const COMMENT_BLOCK_DETECTORS = [
]; ];
export type ICommentBlock = { 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; content: string;
}; };

View file

@ -1,15 +0,0 @@
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);

View file

@ -1,3 +1,4 @@
export enum SidebarName { export enum SidebarName {
Settings = 'settings', Settings = 'settings',
Tag = 'tag', Tag = 'tag',

View file

@ -17,7 +17,7 @@ export const themeColors: Record<Theme, ThemeColors> = {
'linear-gradient(165deg, #ff7549 -50%, #ff3344 150%)', 'linear-gradient(165deg, #ff7549 -50%, #ff3344 150%)',
'linear-gradient(170deg, #582cd0, #592071)', '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]: { [Theme.Horizon]: {
name: 'Веспера', name: 'Веспера',

View file

@ -37,7 +37,7 @@ export const imagePresets = {
flow_horizontal: 'flow_horizontal', flow_horizontal: 'flow_horizontal',
} as const; } 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>> = { export const imageSrcSets: Partial<Record<ImagePreset, number>> = {
[imagePresets[1600]]: 1600, [imagePresets[1600]]: 1600,
@ -49,7 +49,7 @@ export const imageSrcSets: Partial<Record<ImagePreset, number>> = {
export const flowDisplayToPreset: Record< export const flowDisplayToPreset: Record<
FlowDisplayVariant, FlowDisplayVariant,
(typeof imagePresets)[keyof typeof imagePresets] typeof imagePresets[keyof typeof imagePresets]
> = { > = {
single: 'flow_square', single: 'flow_square',
quadro: 'flow_square', quadro: 'flow_square',

View file

@ -3,12 +3,10 @@ import dynamic from 'next/dynamic';
import type { BorisSuperpowersProps } from './index'; import type { BorisSuperpowersProps } from './index';
export const BorisSuperPowersSSR = dynamic<BorisSuperpowersProps>( export const BorisSuperPowersSSR = dynamic<BorisSuperpowersProps>(
() => () => import('~/containers/boris/BorisSuperpowers/index')
import('~/containers/boris/BorisSuperpowers/index').then( .then(it => it.BorisSuperpowers),
(it) => it.BorisSuperpowers,
),
{ {
ssr: false, ssr: false,
loading: () => <div />, loading: () => <div />,
}, }
); );

View file

@ -8,7 +8,7 @@ import { DialogComponentProps } from '~/types/modal';
import { values } from '~/utils/ramda'; import { values } from '~/utils/ramda';
export interface EditorCreateDialogProps extends DialogComponentProps { export interface EditorCreateDialogProps extends DialogComponentProps {
type: (typeof NODE_TYPES)[keyof typeof NODE_TYPES]; type: typeof NODE_TYPES[keyof typeof NODE_TYPES];
isInLab: boolean; isInLab: boolean;
} }

View file

@ -11,7 +11,7 @@ import { TextEditor } from '../components/TextEditor';
import { VideoEditor } from '../components/VideoEditor'; import { VideoEditor } from '../components/VideoEditor';
export const NODE_EDITORS: Record< export const NODE_EDITORS: Record<
(typeof NODE_TYPES)[keyof typeof NODE_TYPES], typeof NODE_TYPES[keyof typeof NODE_TYPES],
FC<NodeEditorProps> FC<NodeEditorProps>
> = { > = {
[NODE_TYPES.IMAGE]: ImageEditor, [NODE_TYPES.IMAGE]: ImageEditor,
@ -22,7 +22,7 @@ export const NODE_EDITORS: Record<
}; };
export const NODE_EDITOR_DATA: Record< export const NODE_EDITOR_DATA: Record<
(typeof NODE_TYPES)[keyof typeof NODE_TYPES], typeof NODE_TYPES[keyof typeof NODE_TYPES],
Partial<INode> Partial<INode>
> = { > = {
[NODE_TYPES.TEXT]: { [NODE_TYPES.TEXT]: {

View file

@ -4,12 +4,9 @@ import type { HeaderProps } from '~/containers/main/Header/index';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
export const HeaderSSR = dynamic<HeaderProps>( export const HeaderSSR = dynamic<HeaderProps>(() => import('./index').then(it => it.Header), {
() => import('./index').then((it) => it.Header),
{
ssr: false, ssr: false,
loading: () => <div className={styles.wrap} />, loading: () => <div className={styles.wrap} />,
}, });
);
export const HeaderSSRPlaceholder = () => <div className={styles.wrap} />; export const HeaderSSRPlaceholder = () => <div className={styles.wrap} />;

View file

@ -3,6 +3,5 @@ import dynamic from 'next/dynamic';
import type { SubmitBarProps } from './index'; import type { SubmitBarProps } from './index';
export const SubmitBarSSR = dynamic<SubmitBarProps>( export const SubmitBarSSR = dynamic<SubmitBarProps>(
() => import('./index').then((it) => it.SubmitBar), () => import('./index').then(it => it.SubmitBar),
{ ssr: false }, { ssr: false });
);

View file

@ -8,7 +8,6 @@ import { CommentWrapper } from '~/containers/comments/CommentWrapper';
import { IComment, ICommentGroup, IFile } from '~/types'; import { IComment, ICommentGroup, IFile } from '~/types';
import { CommendDeleted } from '../../../../../components/node/CommendDeleted'; import { CommendDeleted } from '../../../../../components/node/CommendDeleted';
import { getCommentId } from '../../../../../constants/dom/links';
import { CommentContent } from './components/CommentContent'; import { CommentContent } from './components/CommentContent';
import { CommentDistance } from './components/CommentDistance'; import { CommentDistance } from './components/CommentDistance';
@ -84,9 +83,6 @@ const Comment: FC<Props> = memo(
); );
return ( return (
<>
<a id={getCommentId(comment.id)} className={styles.anchor} />
<CommentContent <CommentContent
prefix={prefix} prefix={prefix}
saveComment={saveComment} saveComment={saveComment}
@ -99,7 +95,6 @@ const Comment: FC<Props> = memo(
onShowImageModal={onShowImageModal} onShowImageModal={onShowImageModal}
key={comment.id} key={comment.id}
/> />
</>
); );
})} })}
</div> </div>

View file

@ -15,9 +15,3 @@
.highlighted { .highlighted {
box-shadow: $color_primary 0 0 0px 2px; box-shadow: $color_primary 0 0 0px 2px;
} }
.anchor {
display: block;
position: relative;
top: -($header_height * 2);
}

View file

@ -1,13 +1,9 @@
import { FC, useEffect, useMemo } from 'react'; import { FC, useMemo } from 'react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { LoadMoreButton } from '~/components/input/LoadMoreButton'; import { LoadMoreButton } from '~/components/input/LoadMoreButton';
import { ANNOUNCE_USER_ID, BORIS_NODE_ID } from '~/constants/boris/constants'; 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 { Comment } from '~/containers/node/NodeComments/components/Comment';
import { useGrouppedComments } from '~/hooks/node/useGrouppedComments'; import { useGrouppedComments } from '~/hooks/node/useGrouppedComments';
import { ICommentGroup } from '~/types'; import { ICommentGroup } from '~/types';
@ -22,11 +18,6 @@ interface Props {
order: 'ASC' | 'DESC'; order: 'ASC' | 'DESC';
} }
const isFirstGroupWithNewComment = (
group: ICommentGroup,
prevGroup: ICommentGroup | undefined,
) => group.hasNew && (!prevGroup || !prevGroup.hasNew);
const NodeComments: FC<Props> = observer(({ order }) => { const NodeComments: FC<Props> = observer(({ order }) => {
const user = useUserContext(); const user = useUserContext();
const { node } = useNodeContext(); const { node } = useNodeContext();
@ -44,7 +35,7 @@ const NodeComments: FC<Props> = observer(({ order }) => {
onSaveComment, onSaveComment,
} = useCommentContext(); } = useCommentContext();
const groupped = useGrouppedComments( const groupped: ICommentGroup[] = useGrouppedComments(
comments, comments,
order, order,
lastSeenCurrent ?? undefined, lastSeenCurrent ?? undefined,
@ -68,33 +59,11 @@ const NodeComments: FC<Props> = observer(({ order }) => {
return null; return null;
} }
useEffect(() => {
const anchor = location.hash?.replace('#', '');
if (!isLoading && isCommentAnchor(anchor)) {
setTimeout(
() =>
document
.getElementById(anchor)
?.scrollIntoView({ behavior: 'smooth' }),
300,
);
}
}, [isLoading]);
return ( return (
<div className={styles.wrap}> <div className={styles.wrap}>
{order === 'DESC' && more} {order === 'DESC' && more}
{groupped.map((group, index) => ( {groupped.map((group) => (
<>
{isFirstGroupWithNewComment(group, groupped[index - 1]) && (
<a
id={NEW_COMMENT_ANCHOR_NAME}
className={styles.newCommentAnchor}
/>
)}
<Comment <Comment
nodeId={node.id!} nodeId={node.id!}
key={group.ids.join()} key={group.ids.join()}
@ -110,7 +79,6 @@ const NodeComments: FC<Props> = observer(({ order }) => {
isSame={group.user.id === user.id} isSame={group.user.id === user.id}
saveComment={onSaveComment} saveComment={onSaveComment}
/> />
</>
))} ))}
{order === 'ASC' && more} {order === 'ASC' && more}

View file

@ -13,9 +13,3 @@
.more { .more {
margin-bottom: $gap; margin-bottom: $gap;
} }
.newCommentAnchor {
position: relative;
top: -($header_height * 2);
display: block;
}

View file

@ -14,7 +14,7 @@ import type { SidebarComponentProps } from '~/types/sidebar';
import { isNil } from '~/utils/ramda'; import { isNil } from '~/utils/ramda';
const tabs = ['profile', 'notifications', 'bookmarks'] as const; const tabs = ['profile', 'notifications', 'bookmarks'] as const;
type TabName = (typeof tabs)[number]; type TabName = typeof tabs[number];
interface SettingsSidebarProps interface SettingsSidebarProps
extends SidebarComponentProps<SidebarName.Settings> { extends SidebarComponentProps<SidebarName.Settings> {

View file

@ -10,7 +10,7 @@ export const useLastSeenBoris = () => {
async (date: string) => { async (date: string) => {
await update({ last_seen_boris: date }, false); await update({ last_seen_boris: date }, false);
}, },
[update], [update]
); );
return { setLastSeen, lastSeen }; return { setLastSeen, lastSeen };

View file

@ -20,7 +20,7 @@ export const useLoginLogoutRestore = () => {
showToastInfo(getRandomPhrase('WELCOME')); showToastInfo(getRandomPhrase('WELCOME'));
return result.user; return result.user;
}, },
[auth], [auth]
); );
return { logout, login }; return { logout, login };

View file

@ -5,9 +5,8 @@ import { API } from '~/constants/api';
import { getErrorMessage } from '~/utils/errors/getErrorMessage'; import { getErrorMessage } from '~/utils/errors/getErrorMessage';
export const useRestoreCode = (code: string) => { export const useRestoreCode = (code: string) => {
const { data, isValidating, error } = useSWR( const { data, isValidating, error } = useSWR(API.USER.REQUEST_CODE(code), () =>
API.USER.REQUEST_CODE(code), apiCheckRestoreCode({ code })
() => apiCheckRestoreCode({ code }),
); );
const codeUser = data?.user; const codeUser = data?.user;

View file

@ -18,7 +18,7 @@ const validationSchema = object({
.test( .test(
'sameAsPassword', 'sameAsPassword',
'Должен совпадать с паролем', 'Должен совпадать с паролем',
(val, ctx) => val === ctx.parent.newPassword, (val, ctx) => val === ctx.parent.newPassword
), ),
}); });
@ -26,21 +26,15 @@ export type RestorePasswordData = Asserts<typeof validationSchema>;
export const useRestorePasswordForm = ( export const useRestorePasswordForm = (
code: string, code: string,
fetcher: (props: { fetcher: (props: { code: string; password: string }) => Promise<{ token: string; user: IUser }>,
code: string; onSuccess: () => void
password: string;
}) => Promise<{ token: string; user: IUser }>,
onSuccess: () => void,
) => { ) => {
const auth = useAuthStore(); const auth = useAuthStore();
const onSubmit = useCallback<FormikConfig<RestorePasswordData>['onSubmit']>( const onSubmit = useCallback<FormikConfig<RestorePasswordData>['onSubmit']>(
async (values, { setErrors }) => { async (values, { setErrors }) => {
try { try {
const { token, user } = await fetcher({ const { token, user } = await fetcher({ password: values.newPassword, code });
password: values.newPassword,
code,
});
auth.setUser(user); auth.setUser(user);
auth.setToken(token); auth.setToken(token);
onSuccess(); onSuccess();
@ -53,7 +47,7 @@ export const useRestorePasswordForm = (
} }
} }
}, },
[onSuccess, fetcher, code, auth], [onSuccess, fetcher, code, auth]
); );
return useFormik<RestorePasswordData>({ return useFormik<RestorePasswordData>({

View file

@ -15,7 +15,7 @@ type RestoreRequestData = Asserts<typeof validationSchema>;
export const useRestoreRequestForm = ( export const useRestoreRequestForm = (
fetcher: (field: string) => Promise<unknown>, fetcher: (field: string) => Promise<unknown>,
onSuccess: () => void, onSuccess: () => void
) => { ) => {
const onSubmit = useCallback<FormikConfig<RestoreRequestData>['onSubmit']>( const onSubmit = useCallback<FormikConfig<RestoreRequestData>['onSubmit']>(
async (values, { setErrors }) => { async (values, { setErrors }) => {
@ -31,7 +31,7 @@ export const useRestoreRequestForm = (
} }
} }
}, },
[fetcher, onSuccess], [fetcher, onSuccess]
); );
return useFormik({ return useFormik({

View file

@ -13,6 +13,6 @@ export const useSessionCookie = () => {
autorun(() => { autorun(() => {
setCookie('session', auth.token, 30); setCookie('session', auth.token, 30);
}), }),
[auth], [auth]
); );
}; };

View file

@ -9,7 +9,9 @@ import { showErrorToast } from '~/utils/errors/showToast';
const validationSchema = object({ const validationSchema = object({
username: string().required(ERRORS.REQUIRED), 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>; type SocialRegisterData = Asserts<typeof validationSchema>;
@ -21,7 +23,7 @@ export const useSocialRegisterForm = (
username: string; username: string;
password: string; password: string;
}) => Promise<{ token: string }>, }) => Promise<{ token: string }>,
onSuccess: (token: string) => void, onSuccess: (token: string) => void
) => { ) => {
const onSubmit = useCallback<FormikConfig<SocialRegisterData>['onSubmit']>( const onSubmit = useCallback<FormikConfig<SocialRegisterData>['onSubmit']>(
async (values, { setErrors }) => { async (values, { setErrors }) => {
@ -41,7 +43,7 @@ export const useSocialRegisterForm = (
} }
} }
}, },
[token, onSuccess, fetcher], [token, onSuccess, fetcher]
); );
return useFormik<SocialRegisterData>({ return useFormik<SocialRegisterData>({

View file

@ -7,10 +7,7 @@ const today = new Date();
export const useUserActiveStatus = (lastSeen?: string) => { export const useUserActiveStatus = (lastSeen?: string) => {
try { try {
const lastSeenDate = lastSeen ? parseISO(lastSeen) : undefined; const lastSeenDate = lastSeen ? parseISO(lastSeen) : undefined;
return ( return lastSeenDate && differenceInDays(today, lastSeenDate) < INACTIVE_ACCOUNT_DAYS;
lastSeenDate &&
differenceInDays(today, lastSeenDate) < INACTIVE_ACCOUNT_DAYS
);
} catch (e) { } catch (e) {
return false; return false;
} }

View file

@ -6,15 +6,13 @@ import { initialBackendStats } from '~/constants/boris/constants';
import { BorisUsageStats } from '~/types/boris'; import { BorisUsageStats } from '~/types/boris';
export const useBorisStats = () => { export const useBorisStats = () => {
const { const { data: backend = initialBackendStats, isValidating: isValidatingBackend } = useSWR(
data: backend = initialBackendStats, API.BORIS.GET_BACKEND_STATS,
isValidating: isValidatingBackend, () => getBorisBackendStats()
} = 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 = { const stats: BorisUsageStats = {
backend, backend,
issues, issues,

View file

@ -3,16 +3,9 @@ import { useMemo } from 'react';
import { normalizeBrightColor } from '~/utils/color'; import { normalizeBrightColor } from '~/utils/color';
import { stringToColour } from '~/utils/dom'; import { stringToColour } from '~/utils/dom';
export const useColorFromString = ( export const useColorFromString = (val?: string, saturation = 3, lightness = 3) => {
val?: string,
saturation = 3,
lightness = 3,
) => {
return useMemo( return useMemo(
() => () => (val && normalizeBrightColor(stringToColour(val), saturation, lightness)) || '',
(val && [lightness, saturation, val]
normalizeBrightColor(stringToColour(val), saturation, lightness)) ||
'',
[lightness, saturation, val],
); );
}; };

View file

@ -6,7 +6,7 @@ export const useColorGradientFromString = (
val?: string, val?: string,
saturation = 3, saturation = 3,
lightness = 3, lightness = 3,
angle = 155, angle = 155
) => ) =>
useMemo(() => { useMemo(() => {
if (!val) { if (!val) {

View file

@ -1,9 +1,6 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
export const usePersistedState = ( export const usePersistedState = (key: string, initial: string): [string, (val: string) => any] => {
key: string,
initial: string,
): [string, (val: string) => any] => {
const stored = useMemo(() => { const stored = useMemo(() => {
try { try {
return localStorage.getItem(`vault_${key}`) || initial; return localStorage.getItem(`vault_${key}`) || initial;

View file

@ -4,18 +4,15 @@ export const useFocusEvent = (initialState = false, delay = 0) => {
const [focused, setFocused] = useState(initialState); const [focused, setFocused] = useState(initialState);
const onFocus = useCallback( const onFocus = useCallback(
(event) => { event => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
setFocused(true); setFocused(true);
}, },
[setFocused], [setFocused]
);
const onBlur = useCallback(
() => setTimeout(() => setFocused(false), delay),
[delay],
); );
const onBlur = useCallback(() => setTimeout(() => setFocused(false), delay), [delay]);
return { focused, onBlur, onFocus }; return { focused, onBlur, onFocus };
}; };

View file

@ -7,13 +7,12 @@ export const useFormatWrapper = (onChange: (val: string) => void) => {
target: HTMLTextAreaElement, target: HTMLTextAreaElement,
prefix = '', prefix = '',
suffix = '', suffix = ''
) => ) => event => {
(event) => {
event.preventDefault(); event.preventDefault();
wrapTextInsideInput(target, prefix, suffix, onChange); wrapTextInsideInput(target, prefix, suffix, onChange);
}, },
[onChange], [onChange]
); );
}; };
@ -22,7 +21,7 @@ export const wrapTextInsideInput = (
target: HTMLTextAreaElement, target: HTMLTextAreaElement,
prefix: string, prefix: string,
suffix: string, suffix: string,
onChange: (val: string) => void, onChange: (val: string) => void
) => { ) => {
if (!target) return; if (!target) return;
@ -35,7 +34,7 @@ export const wrapTextInsideInput = (
onChange( onChange(
target.value.substring(0, start) + target.value.substring(0, start) +
replacement + replacement +
target.value.substring(end, target.value.length), target.value.substring(end, target.value.length)
); );
target.focus(); target.focus();

View file

@ -2,8 +2,7 @@ import { useCallback, useEffect } from 'react';
export const useInfiniteLoader = (loader: () => void, isLoading?: boolean) => { export const useInfiniteLoader = (loader: () => void, isLoading?: boolean) => {
const onLoadMore = useCallback(() => { const onLoadMore = useCallback(() => {
const pos = const pos = window.scrollY + window.innerHeight - document.body.scrollHeight;
window.scrollY + window.innerHeight - document.body.scrollHeight;
if (isLoading || pos < -600) return; if (isLoading || pos < -600) return;

View file

@ -5,13 +5,13 @@ import { getImageFromPaste } from '~/utils/uploader';
// useInputPasteUpload attaches event listener to input, that calls onUpload if user pasted any image // useInputPasteUpload attaches event listener to input, that calls onUpload if user pasted any image
export const useInputPasteUpload = (onUpload: (files: File[]) => void) => { export const useInputPasteUpload = (onUpload: (files: File[]) => void) => {
return useCallback( return useCallback(
async (event) => { async event => {
const image = await getImageFromPaste(event); const image = await getImageFromPaste(event);
if (!image) return; if (!image) return;
onUpload([image]); onUpload([image]);
}, },
[onUpload], [onUpload]
); );
}; };

View file

@ -17,11 +17,7 @@ const sameWidth = {
}, },
}; };
export const usePopperModifiers = ( export const usePopperModifiers = (offsetX = 0, offsetY = 10, justify?: boolean): Modifier<any>[] =>
offsetX = 0,
offsetY = 10,
justify?: boolean,
): Modifier<any>[] =>
useMemo( useMemo(
() => () =>
[ [
@ -39,5 +35,5 @@ export const usePopperModifiers = (
}, },
...(justify ? [sameWidth] : []), ...(justify ? [sameWidth] : []),
] as Modifier<any>[], ] as Modifier<any>[],
[offsetX, offsetY, justify], [offsetX, offsetY, justify]
); );

View file

@ -11,7 +11,7 @@ const getHeight = () => {
body.offsetHeight, body.offsetHeight,
html.clientHeight, html.clientHeight,
html.scrollHeight, html.scrollHeight,
html.offsetHeight, html.offsetHeight
); );
}; };
export const useScrollHeight = () => getHeight(); export const useScrollHeight = () => getHeight();

View file

@ -18,6 +18,6 @@ export const useScrollToTop = (deps?: any[]) => {
}); });
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
deps && Array.isArray(deps) ? deps : [], deps && Array.isArray(deps) ? deps : []
); );
}; };

View file

@ -1,9 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
export const useScrollTop = () => { export const useScrollTop = () => {
const [top, setTop] = useState( const [top, setTop] = useState(typeof window !== 'undefined' ? window.scrollY : 0);
typeof window !== 'undefined' ? window.scrollY : 0,
);
useEffect(() => { useEffect(() => {
setTop(window.scrollY); setTop(window.scrollY);

View file

@ -6,12 +6,11 @@ export const useFlowCellControls = (
id: INode['id'], id: INode['id'],
description: string | undefined, description: string | undefined,
flow: FlowDisplay, flow: FlowDisplay,
onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void, onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void
) => { ) => {
const onChange = useCallback( const onChange = useCallback(
(value: Partial<FlowDisplay>) => (value: Partial<FlowDisplay>) => onChangeCellView(id, { ...flow, ...value }),
onChangeCellView(id, { ...flow, ...value }), [flow, id, onChangeCellView]
[flow, id, onChangeCellView],
); );
const hasDescription = !!description && description.length > 32; const hasDescription = !!description && description.length > 32;

View file

@ -17,6 +17,6 @@ export const useFlowSetCellView = () => {
showErrorToast(error); showErrorToast(error);
} }
}, },
[updateNode], [updateNode]
); );
}; };

View file

@ -21,19 +21,15 @@ export const useGetLabStats = () => {
heroes: lab.heroes, heroes: lab.heroes,
tags: lab.tags, tags: lab.tags,
}, },
onSuccess: (data) => { onSuccess: data => {
lab.setHeroes(data.heroes); lab.setHeroes(data.heroes);
lab.setTags(data.tags); lab.setTags(data.tags);
}, },
refreshInterval, refreshInterval,
}, }
); );
const { const { data: updatesData, isValidating: isValidatingUpdates, mutate: mutateUpdates } = useSWR(
data: updatesData,
isValidating: isValidatingUpdates,
mutate: mutateUpdates,
} = useSWR(
isUser ? API.LAB.UPDATES : null, isUser ? API.LAB.UPDATES : null,
async () => { async () => {
const result = await getLabUpdates(); const result = await getLabUpdates();
@ -41,27 +37,26 @@ export const useGetLabStats = () => {
}, },
{ {
fallbackData: lab.updates, fallbackData: lab.updates,
onSuccess: (data) => { onSuccess: data => {
lab.setUpdates(data); lab.setUpdates(data);
}, },
refreshInterval, refreshInterval,
}, }
); );
const heroes = useMemo(() => stats?.heroes || [], [stats]); const heroes = useMemo(() => stats?.heroes || [], [stats]);
const tags = useMemo(() => stats?.tags || [], [stats]); const tags = useMemo(() => stats?.tags || [], [stats]);
const updates = useMemo(() => updatesData || [], [updatesData]); const updates = useMemo(() => updatesData || [], [updatesData]);
const isLoading = const isLoading = (!stats || !updates) && (isValidatingStats || isValidatingUpdates);
(!stats || !updates) && (isValidatingStats || isValidatingUpdates);
const seenNode = useCallback( const seenNode = useCallback(
async (nodeId: number) => { async (nodeId: number) => {
await mutateUpdates( await mutateUpdates(
updates.filter((it) => it.id !== nodeId), updates.filter(it => it.id !== nodeId),
false, false
); );
}, },
[mutateUpdates, updates], [mutateUpdates, updates]
); );
return { heroes, tags, updates, isLoading, seenNode }; return { heroes, tags, updates, isLoading, seenNode };

View file

@ -11,7 +11,7 @@ const getKey = (username: string): string | null => {
}; };
export const useMessages = (username: string) => { export const useMessages = (username: string) => {
const { data, isValidating } = useSWR(getKey(username), async () => const { data, isValidating } = useSWR(getKey(username), async () =>
apiGetUserMessages({ username }), apiGetUserMessages({ username })
); );
const messages: IMessage[] = useMemo(() => data?.messages || [], [data]); const messages: IMessage[] = useMemo(() => data?.messages || [], [data]);

View file

@ -5,9 +5,7 @@ import { useModalStore } from '~/store/modal/useModalStore';
import { DialogComponentProps } from '~/types/modal'; import { DialogComponentProps } from '~/types/modal';
export type DialogContentProps = { export type DialogContentProps = {
[K in keyof typeof DIALOG_CONTENT]: (typeof DIALOG_CONTENT)[K] extends ( [K in keyof typeof DIALOG_CONTENT]: typeof DIALOG_CONTENT[K] extends (props: infer U) => any
props: infer U,
) => any
? U extends DialogComponentProps ? U extends DialogComponentProps
? keyof Omit<U, 'onRequestClose' | 'children'> extends never ? keyof Omit<U, 'onRequestClose' | 'children'> extends never
? {} ? {}
@ -23,7 +21,7 @@ export const useModal = () => {
<T extends Dialog>(dialog: T, props: DialogContentProps[T]) => { <T extends Dialog>(dialog: T, props: DialogContentProps[T]) => {
setCurrent(dialog, props); setCurrent(dialog, props);
}, },
[setCurrent], [setCurrent]
); );
return { showModal, hideModal: hide, current, isOpened: !!current }; return { showModal, hideModal: hide, current, isOpened: !!current };

View file

@ -10,6 +10,6 @@ export const useShowModal = <T extends Dialog>(dialog: T) => {
(props: DialogContentProps[T]) => { (props: DialogContentProps[T]) => {
modal.showModal(dialog, props); modal.showModal(dialog, props);
}, },
[dialog, modal], [dialog, modal]
); );
}; };

View file

@ -11,6 +11,6 @@ export const useImageModal = () => {
(images: IFile[], index: number) => { (images: IFile[], index: number) => {
showModal({ items: images, index }); showModal({ items: images, index });
}, },
[showModal], [showModal]
); );
}; };

View file

@ -17,7 +17,7 @@ export const useNavigation = () => {
craHistory.push(url); craHistory.push(url);
} }
}, },
[craHistory, nextRouter], [craHistory, nextRouter]
); );
return { push }; return { push };

View file

@ -16,13 +16,9 @@ export const useCreateNode = () => {
if (node.is_promoted) { if (node.is_promoted) {
flow.setNodes([result.node, ...flow.nodes]); flow.setNodes([result.node, ...flow.nodes]);
} else { } else {
await lab.unshift({ await lab.unshift({ node: result.node, comment_count: 0, last_seen: node.created_at });
node: result.node,
comment_count: 0,
last_seen: node.created_at,
});
} }
}, },
[flow, lab], [flow, lab]
); );
}; };

View file

@ -6,13 +6,13 @@ import { groupCommentsByUser } from '~/utils/fn';
export const useGrouppedComments = ( export const useGrouppedComments = (
comments: IComment[], comments: IComment[],
order: 'ASC' | 'DESC', order: 'ASC' | 'DESC',
lastSeen?: string, lastSeen?: string
) => ) =>
useMemo( useMemo(
() => () =>
(order === 'DESC' ? [...comments].reverse() : comments).reduce( (order === 'DESC' ? [...comments].reverse() : comments).reduce(
groupCommentsByUser(lastSeen), groupCommentsByUser(lastSeen),
[], []
), ),
[comments, lastSeen, order], [comments, lastSeen, order]
); );

View file

@ -6,10 +6,7 @@ import { useModal } from '~/hooks/modal/useModal';
import { INode } from '~/types'; import { INode } from '~/types';
import { showErrorToast } from '~/utils/errors/showToast'; import { showErrorToast } from '~/utils/errors/showToast';
export const useNodeActions = ( export const useNodeActions = (node: INode, update: (node: Partial<INode>) => Promise<unknown>) => {
node: INode,
update: (node: Partial<INode>) => Promise<unknown>,
) => {
const { showModal } = useModal(); const { showModal } = useModal();
const onLike = useCallback(async () => { const onLike = useCallback(async () => {
@ -38,20 +35,17 @@ export const useNodeActions = (
const onLock = useCallback(async () => { const onLock = useCallback(async () => {
try { try {
const result = await apiLockNode({ const result = await apiLockNode({ id: node.id, is_locked: !node.deleted_at });
id: node.id,
is_locked: !node.deleted_at,
});
await update({ deleted_at: result.deleted_at }); await update({ deleted_at: result.deleted_at });
} catch (error) { } catch (error) {
showErrorToast(error); showErrorToast(error);
} }
}, [node.deleted_at, node.id, update]); }, [node.deleted_at, node.id, update]);
const onEdit = useCallback( const onEdit = useCallback(() => showModal(Dialog.EditNode, { nodeId: node.id! }), [
() => showModal(Dialog.EditNode, { nodeId: node.id! }), node,
[node, showModal], showModal,
); ]);
return { onLike, onStar, onLock, onEdit }; return { onLike, onStar, onLock, onEdit };
}; };

View file

@ -4,8 +4,7 @@ import { UploadType } from '~/constants/uploads';
import { INode } from '~/types'; import { INode } from '~/types';
export const useNodeAudios = (node: INode) => { export const useNodeAudios = (node: INode) => {
return useMemo( return useMemo(() => node.files.filter(file => file && file.type === UploadType.Audio), [
() => node.files.filter((file) => file && file.type === UploadType.Audio), node.files,
[node.files], ]);
);
}; };

View file

@ -1,11 +1,6 @@
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { import { FormikConfig, FormikHelpers, useFormik, useFormikContext } from 'formik';
FormikConfig,
FormikHelpers,
useFormik,
useFormikContext,
} from 'formik';
import { object } from 'yup'; import { object } from 'yup';
import { INode } from '~/types'; import { INode } from '~/types';
@ -15,9 +10,9 @@ import { showErrorToast } from '~/utils/errors/showToast';
const validationSchema = object().shape({}); const validationSchema = object().shape({});
const afterSubmit = const afterSubmit = ({ resetForm, setSubmitting, setErrors }: FormikHelpers<INode>) => (
({ resetForm, setSubmitting, setErrors }: FormikHelpers<INode>) => error?: unknown
(error?: unknown) => { ) => {
setSubmitting(false); setSubmitting(false);
if (error) { if (error) {
@ -39,7 +34,7 @@ export const useNodeFormFormik = (
values: INode, values: INode,
uploader: Uploader, uploader: Uploader,
stopEditing: () => void, stopEditing: () => void,
sendSaveRequest: (node: INode) => Promise<unknown>, sendSaveRequest: (node: INode) => Promise<unknown>
) => { ) => {
const { current: initialValues } = useRef(values); const { current: initialValues } = useRef(values);
@ -58,7 +53,7 @@ export const useNodeFormFormik = (
afterSubmit(helpers)(error); afterSubmit(helpers)(error);
} }
}, },
[sendSaveRequest, uploader.files], [sendSaveRequest, uploader.files]
); );
return useFormik<INode>({ return useFormik<INode>({

View file

@ -4,8 +4,7 @@ import { UploadType } from '~/constants/uploads';
import { INode } from '~/types'; import { INode } from '~/types';
export const useNodeImages = (node: INode) => { export const useNodeImages = (node: INode) => {
return useMemo( return useMemo(() => node.files.filter(file => file && file.type === UploadType.Image), [
() => node.files.filter((file) => file && file.type === UploadType.Image), node.files,
[node.files], ]);
);
}; };

View file

@ -27,6 +27,6 @@ export const useUpdateNode = (id: number) => {
await lab.updateNode(result.node.id!, result.node); await lab.updateNode(result.node.id!, result.node);
} }
}, },
[update, flow, lab], [update, flow, lab]
); );
}; };

View file

@ -20,7 +20,7 @@ export const useGetProfile = (username?: string) => {
}, },
{ {
refreshInterval: 60000, refreshInterval: 60000,
}, }
); );
const profile = data || EMPTY_USER; const profile = data || EMPTY_USER;
@ -29,7 +29,7 @@ export const useGetProfile = (username?: string) => {
async (user: Partial<IUser>) => { async (user: Partial<IUser>) => {
await mutate({ ...profile, ...user }); await mutate({ ...profile, ...user });
}, },
[mutate, profile], [mutate, profile]
); );
return { profile, isLoading: !data && isValidating, update }; return { profile, isLoading: !data && isValidating, update };

View file

@ -9,8 +9,10 @@ import { flatten } from '~/utils/ramda';
const RESULTS_COUNT = 20; const RESULTS_COUNT = 20;
const getKey: (text: string) => SWRInfiniteKeyLoader = const getKey: (text: string) => SWRInfiniteKeyLoader = text => (
(text) => (pageIndex, previousPageData: INode[]) => { pageIndex,
previousPageData: INode[]
) => {
if ((pageIndex > 0 && !previousPageData?.length) || !text) return null; if ((pageIndex > 0 && !previousPageData?.length) || !text) return null;
const props: GetSearchResultsRequest = { const props: GetSearchResultsRequest = {
@ -38,7 +40,7 @@ export const useSearch = () => {
const result = await getSearchResults(props); const result = await getSearchResults(props);
return result.nodes; return result.nodes;
}, }
); );
const loadMore = useCallback(() => setSize(size + 1), [setSize, size]); const loadMore = useCallback(() => setSize(size + 1), [setSize, size]);

View file

@ -26,5 +26,5 @@ export const useTagAutocomplete = (
}, },
); );
return useMemo(() => (search ? (data ?? []) : []), [data, search]); return useMemo(() => (search ? data ?? [] : []), [data, search]);
}; };

View file

@ -9,8 +9,10 @@ import { flatten, isNil } from '~/utils/ramda';
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
const getKey: (tag: string) => SWRInfiniteKeyLoader = const getKey: (tag: string) => SWRInfiniteKeyLoader = tag => (
(tag) => (pageIndex, previousPageData: INode[]) => { pageIndex,
previousPageData: INode[]
) => {
if (pageIndex > 0 && !previousPageData?.length) return null; if (pageIndex > 0 && !previousPageData?.length) return null;
return `${API.TAG.NODES}?tag=${tag}&page=${pageIndex}`; return `${API.TAG.NODES}?tag=${tag}&page=${pageIndex}`;
}; };
@ -37,7 +39,7 @@ export const useTagNodes = (tag: string) => {
}); });
return result.nodes; return result.nodes;
}, }
); );
const nodes = useMemo(() => flatten(data || []), [data]); const nodes = useMemo(() => flatten(data || []), [data]);
@ -45,12 +47,5 @@ export const useTagNodes = (tag: string) => {
const loadMore = useCallback(() => setSize(size + 1), [setSize, size]); const loadMore = useCallback(() => setSize(size + 1), [setSize, size]);
return { return { nodes, hasMore, loadMore, isLoading: !data && isValidating, mutate, data };
nodes,
hasMore,
loadMore,
isLoading: !data && isValidating,
mutate,
data,
};
}; };

View file

@ -9,7 +9,7 @@ export const useUpdates = () => {
const { data } = useSWR( const { data } = useSWR(
isUser ? API.USER.GET_UPDATES : null, isUser ? API.USER.GET_UPDATES : null,
() => apiAuthGetUpdates({ exclude_dialogs: 0, last: '' }), () => apiAuthGetUpdates({ exclude_dialogs: 0, last: '' }),
{ refreshInterval: 5 * 60 * 1000 }, { refreshInterval: 5 * 60 * 1000 }
); );
const borisCommentedAt = data?.boris?.commented_at || ''; const borisCommentedAt = data?.boris?.commented_at || '';

View file

@ -1,4 +1,4 @@
const reportWebVitals = (onPerfEntry) => { const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) { if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry); getCLS(onPerfEntry);

View file

@ -24,13 +24,11 @@ export class FlowStore {
/** removes node from updated after user seen it */ /** removes node from updated after user seen it */
seenNode = (nodeId: number) => { 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 */ /** replaces node with value */
updateNode = (id: number, node: Partial<IFlowNode>) => { updateNode = (id: number, node: Partial<IFlowNode>) => {
this.setNodes( this.setNodes(this.nodes.map(it => (it.id === id ? { ...it, ...node } : it)));
this.nodes.map((it) => (it.id === id ? { ...it, ...node } : it)),
);
}; };
} }

View file

@ -12,9 +12,7 @@ export class MetadataStore {
pending: string[] = []; pending: string[] = [];
constructor( constructor(
protected apiMetadataLoader: ( protected apiMetadataLoader: (ids: string[]) => Promise<Record<string, EmbedMetadata>>
ids: string[],
) => Promise<Record<string, EmbedMetadata>>,
) { ) {
makeAutoObservable(this); makeAutoObservable(this);
} }
@ -61,7 +59,7 @@ export class MetadataStore {
try { try {
const result = await this.apiMetadataLoader(items); const result = await this.apiMetadataLoader(items);
const fetchedIDs = values(result).map((it) => it.address); const fetchedIDs = values(result).map(it => it.address);
runInAction(() => { runInAction(() => {
this.pushMetadataItems(result); this.pushMetadataItems(result);
@ -74,11 +72,7 @@ export class MetadataStore {
/** adds items to queue */ /** adds items to queue */
enqueue = (id: string) => { enqueue = (id: string) => {
if ( if (this.queue.includes(id) || keys(this.metadata).includes(id) || this.pending.includes(id)) {
this.queue.includes(id) ||
keys(this.metadata).includes(id) ||
this.pending.includes(id)
) {
return; return;
} }

View file

@ -153,13 +153,13 @@ export const NOTIFICATION_TYPES = {
}; };
export type IMessageNotification = { export type IMessageNotification = {
type: (typeof NOTIFICATION_TYPES)['message']; type: typeof NOTIFICATION_TYPES['message'];
content: Partial<IMessage>; content: Partial<IMessage>;
created_at: string; created_at: string;
}; };
export type ICommentNotification = { export type ICommentNotification = {
type: (typeof NOTIFICATION_TYPES)['comment']; type: typeof NOTIFICATION_TYPES['comment'];
content: Partial<IComment>; content: Partial<IComment>;
created_at: string; created_at: string;
}; };

View file

@ -2,7 +2,6 @@ import { ShallowUser } from '../auth';
export interface NotificationItem { export interface NotificationItem {
id: number; id: number;
itemId: number;
url: string; url: string;
type: NotificationType; type: NotificationType;
title: string; title: string;

View file

@ -5,8 +5,9 @@ import type { SidebarComponents } from '~/constants/sidebar/components';
export type SidebarComponent = keyof SidebarComponents; export type SidebarComponent = keyof SidebarComponents;
// TODO: use it to store props for sidebar // TODO: use it to store props for sidebar
export type SidebarProps<T extends SidebarComponent> = export type SidebarProps<
SidebarComponents[T] extends FunctionComponent<infer U> T extends SidebarComponent
> = SidebarComponents[T] extends FunctionComponent<infer U>
? U extends object ? U extends object
? U extends SidebarComponentProps<T> ? U extends SidebarComponentProps<T>
? Omit<U, keyof SidebarComponentProps<T>> ? Omit<U, keyof SidebarComponentProps<T>>

View file

@ -1,19 +1,9 @@
import { import { adjustHue, darken, desaturate, parseToHsla, transparentize } from 'color2k';
adjustHue,
darken,
desaturate,
parseToHsla,
transparentize,
} from 'color2k';
import { DEFAULT_DOMINANT_COLOR } from '~/constants/node'; import { DEFAULT_DOMINANT_COLOR } from '~/constants/node';
import { stringToColour } from '~/utils/dom'; import { stringToColour } from '~/utils/dom';
export const normalizeBrightColor = ( export const normalizeBrightColor = (color?: string, saturationExp = 3, lightnessExp = 3) => {
color?: string,
saturationExp = 3,
lightnessExp = 3,
) => {
if (!color) { if (!color) {
return ''; return '';
} }
@ -22,23 +12,12 @@ export const normalizeBrightColor = (
const saturation = hsla[1]; const saturation = hsla[1];
const lightness = hsla[2]; const lightness = hsla[2];
const desaturated = const desaturated = saturationExp > 1 ? desaturate(color, saturation ** saturationExp) : color;
saturationExp > 1 ? desaturate(color, saturation ** saturationExp) : color; return lightnessExp > 1 ? darken(desaturated, lightness ** lightnessExp) : desaturated;
return lightnessExp > 1
? darken(desaturated, lightness ** lightnessExp)
: desaturated;
}; };
export const generateColorTriplet = ( export const generateColorTriplet = (val: string, saturation: number, lightness: number) => {
val: string, const color = normalizeBrightColor(stringToColour(val), saturation, lightness);
saturation: number,
lightness: number,
) => {
const color = normalizeBrightColor(
stringToColour(val),
saturation,
lightness,
);
return [ return [
color, color,
@ -52,13 +31,9 @@ export const generateGradientFromColor = (
saturation = 3, saturation = 3,
lightness = 3, lightness = 3,
angle = 155, angle = 155,
opacity = 1, opacity = 1
) => { ) => {
const [first, second, third] = generateColorTriplet( const [first, second, third] = generateColorTriplet(val, saturation, lightness).map(it => {
val,
saturation,
lightness,
).map((it) => {
if (opacity > 1 || opacity < 0) { if (opacity > 1 || opacity < 0) {
return it; return it;
} }

View file

@ -6,6 +6,5 @@ export const CONFIG = {
// image storage endpoint (sames as backend, but with /static usualy) // image storage endpoint (sames as backend, but with /static usualy)
remoteCurrent: process.env.NEXT_PUBLIC_REMOTE_CURRENT || '', remoteCurrent: process.env.NEXT_PUBLIC_REMOTE_CURRENT || '',
// transitional prop, marks migration to nextjs // transitional prop, marks migration to nextjs
isNextEnvironment: isNextEnvironment: !!process.env.NEXT_PUBLIC_REMOTE_CURRENT || typeof window === 'undefined',
!!process.env.NEXT_PUBLIC_REMOTE_CURRENT || typeof window === 'undefined',
}; };

View file

@ -3,19 +3,11 @@ import { differenceInDays, isAfter, isValid, parseISO } from 'date-fns';
import { IComment, ICommentGroup } from '~/types'; import { IComment, ICommentGroup } from '~/types';
import { curry, insert, nth, path, remove } from '~/utils/ramda'; import { curry, insert, nth, path, remove } from '~/utils/ramda';
export const moveArrItem = curry((at, to, list) => export const moveArrItem = curry((at, to, list) => insert(to, nth(at, list), remove(at, 1, list)));
insert(to, nth(at, list), remove(at, 1, list)),
);
export const objFromArray = (array: any[], key: string) => export const objFromArray = (array: any[], key: string) =>
array.reduce( array.reduce((obj, el) => (key && el[key] ? { ...obj, [el[key]]: el } : obj), {});
(obj, el) => (key && el[key] ? { ...obj, [el[key]]: el } : obj),
{},
);
const compareCommentDates = ( const compareCommentDates = (commentDateValue?: string, lastSeenDateValue?: string) => {
commentDateValue?: string,
lastSeenDateValue?: string,
) => {
if (!commentDateValue || !lastSeenDateValue) { if (!commentDateValue || !lastSeenDateValue) {
return false; return false;
} }
@ -45,13 +37,11 @@ const getCommentDistance = (firstDate?: string, secondDate?: string) => {
} }
}; };
export const groupCommentsByUser = export const groupCommentsByUser = (lastSeen?: string) => (
(lastSeen?: string) => grouppedComments: ICommentGroup[],
(grouppedComments: ICommentGroup[], comment: IComment): ICommentGroup[] => { comment: IComment
const last: ICommentGroup | undefined = path( ): ICommentGroup[] => {
[grouppedComments.length - 1], const last: ICommentGroup | undefined = path([grouppedComments.length - 1], grouppedComments);
grouppedComments,
);
if (!comment.user) { if (!comment.user) {
return grouppedComments; return grouppedComments;
@ -79,14 +69,12 @@ export const groupCommentsByUser =
...last.distancesInDays, ...last.distancesInDays,
getCommentDistance( getCommentDistance(
comment?.created_at, comment?.created_at,
last.comments[last.comments.length - 1]?.created_at, last.comments[last.comments.length - 1]?.created_at
), ),
], ],
comments: [...last.comments, comment], comments: [...last.comments, comment],
ids: [...last.ids, comment.id], ids: [...last.ids, comment.id],
hasNew: hasNew: last.hasNew || compareCommentDates(comment.created_at, lastSeen),
last.hasNew ||
compareCommentDates(comment.created_at, lastSeen),
}, },
]), ]),
]; ];

View file

@ -18,16 +18,11 @@ const ProfileContext = createContext<ProfileContextValue>({
isLoading: false, isLoading: false,
}); });
export const ProfileProvider: FC<ProfileProviderProps> = ({ export const ProfileProvider: FC<ProfileProviderProps> = ({ children, username }) => {
children,
username,
}) => {
const { profile, isLoading } = useGetProfile(username); const { profile, isLoading } = useGetProfile(username);
return ( return (
<ProfileContext.Provider value={{ profile, isLoading }}> <ProfileContext.Provider value={{ profile, isLoading }}>{children}</ProfileContext.Provider>
{children}
</ProfileContext.Provider>
); );
}; };

View file

@ -2,12 +2,10 @@ import { flatten, isEmpty } from '~/utils/ramda';
export const splitTextByYoutube = (strings: string[]): string[] => export const splitTextByYoutube = (strings: string[]): string[] =>
flatten( flatten(
strings.map((str) => strings.map(str =>
str.split( str.split(/(https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?[\w\-&=]+)/)
/(https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?[\w\-&=]+)/, )
),
),
); );
export const splitTextOmitEmpty = (strings: string[]): string[] => export const splitTextOmitEmpty = (strings: string[]): string[] =>
strings.map((el) => el.trim()).filter((el) => !isEmpty(el)); strings.map(el => el.trim()).filter(el => !isEmpty(el));

View file

@ -1,4 +1,4 @@
/** just combines title elements to form title of the page */ /** just combines title elements to form title of the page */
export const getPageTitle = (...props: string[]): string => { export const getPageTitle = (...props: string[]): string => {
return ['Убежище', ...props].filter((it) => it.trim()).join(' • '); return ['Убежище', ...props].filter(it => it.trim()).join(' • ');
}; };

View file

@ -3,13 +3,11 @@ import { ITag } from '~/types';
export const separateTags = (tags: Partial<ITag>[]): Partial<ITag>[][] => export const separateTags = (tags: Partial<ITag>[]): Partial<ITag>[][] =>
(tags || []).reduce( (tags || []).reduce(
(obj, tag) => (obj, tag) =>
tag?.title?.substr(0, 1) === '/' tag?.title?.substr(0, 1) === '/' ? [[...obj[0], tag], obj[1]] : [obj[0], [...obj[1], tag]],
? [[...obj[0], tag], obj[1]] [[], []] as Partial<ITag>[][]
: [obj[0], [...obj[1], tag]],
[[], []] as Partial<ITag>[][],
); );
export const separateTagOptions = (options: string[]): string[][] => export const separateTagOptions = (options: string[]): string[][] =>
separateTags(options.map((title): Partial<ITag> => ({ title }))).map((item) => separateTags(options.map((title): Partial<ITag> => ({ title }))).map(item =>
item.filter((tag) => tag.title).map(({ title }) => title!), item.filter(tag => tag.title).map(({ title }) => title!)
); );

View file

@ -1,6 +1,5 @@
import { ERROR_LITERAL, ERRORS } from '~/constants/errors'; import { ERROR_LITERAL, ERRORS } from '~/constants/errors';
import { ValueOf } from '~/types'; import { ValueOf } from '~/types';
export const t = ( export const t = (string: ValueOf<typeof ERRORS>): ValueOf<typeof ERROR_LITERAL> =>
string: ValueOf<typeof ERRORS>, ERROR_LITERAL[string] || string;
): ValueOf<typeof ERROR_LITERAL> => ERROR_LITERAL[string] || string;

View file

@ -2,10 +2,10 @@ import { FILE_MIMES, UploadType } from '~/constants/uploads';
import { isMimeOfImage } from '~/utils/validators'; import { isMimeOfImage } from '~/utils/validators';
/** if file is image, returns data-uri of thumbnail */ /** 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 ''; if (!file.type || !isMimeOfImage(file.type)) return '';
return new Promise<string>((resolve) => { return new Promise<string>(resolve => {
const reader = new FileReader(); const reader = new FileReader();
reader.onloadend = () => resolve(reader.result?.toString() || ''); reader.onloadend = () => resolve(reader.result?.toString() || '');
reader.readAsDataURL(file); reader.readAsDataURL(file);
@ -15,17 +15,14 @@ export const uploadGetThumb = async (file) => {
/** returns UploadType by file */ /** returns UploadType by file */
export const getFileType = (file: File): UploadType | undefined => export const getFileType = (file: File): UploadType | undefined =>
((file.type && ((file.type &&
Object.keys(FILE_MIMES).find((mime) => Object.keys(FILE_MIMES).find(mime => FILE_MIMES[mime].includes(file.type))) as UploadType) ||
FILE_MIMES[mime].includes(file.type), undefined;
)) as UploadType) || undefined;
/** getImageFromPaste returns any images from paste event */ /** getImageFromPaste returns any images from paste event */
export const getImageFromPaste = ( export const getImageFromPaste = (event: ClipboardEvent): Promise<File | undefined> => {
event: ClipboardEvent,
): Promise<File | undefined> => {
const items = event.clipboardData?.items; const items = event.clipboardData?.items;
return new Promise((resolve) => { return new Promise(resolve => {
for (let index in items) { for (let index in items) {
const item = items[index]; const item = items[index];
@ -43,7 +40,7 @@ export const getImageFromPaste = (
new File([e.target?.result], 'paste.png', { new File([e.target?.result], 'paste.png', {
type, type,
lastModified: new Date().getTime(), lastModified: new Date().getTime(),
}), })
); );
}; };

View file

@ -1,4 +1,3 @@
import { IMAGE_MIME_TYPES } from '~/constants/uploads'; import { IMAGE_MIME_TYPES } from '~/constants/uploads';
export const isMimeOfImage = (mime): boolean => export const isMimeOfImage = (mime): boolean => !!mime && IMAGE_MIME_TYPES.indexOf(mime) >= 0;
!!mime && IMAGE_MIME_TYPES.indexOf(mime) >= 0;

View file

@ -221,11 +221,6 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" 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": "@polka/url@^1.0.0-next.20":
version "1.0.0-next.21" version "1.0.0-next.21"
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
@ -1211,14 +1206,6 @@ eslint-plugin-import@^2.25.4:
resolve "^1.20.0" resolve "^1.20.0"
tsconfig-paths "^3.12.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: eslint-plugin-react-hooks@^4.6.0:
version "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" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3"
@ -1407,11 +1394,6 @@ 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" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 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: fast-fifo@^1.1.0, fast-fifo@^1.2.0:
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c"
@ -2470,17 +2452,10 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier-linter-helpers@^1.0.0: prettier@^2.7.1:
version "1.0.0" version "2.7.1"
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
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: pretty-format@^26.6.2:
version "26.6.2" version "26.6.2"
@ -3112,14 +3087,6 @@ swr@^1.0.1:
resolved "https://registry.yarnpkg.com/swr/-/swr-1.2.0.tgz#8649f6e9131ce94bbcf7ffd65c21334da3d1ec20" resolved "https://registry.yarnpkg.com/swr/-/swr-1.2.0.tgz#8649f6e9131ce94bbcf7ffd65c21334da3d1ec20"
integrity sha512-C3IXeKOREn0jQ1ewXRENE7ED7jjGbFTakwB64eLACkCqkF/A0N2ckvpCTftcaSYi5yV36PzoehgVCOVRmtECcA== 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: table@^6.0.9:
version "6.8.0" version "6.8.0"
resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca" resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca"
@ -3254,11 +3221,6 @@ tslib@^2.0.3, tslib@^2.1.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== 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: tsutils@^3.21.0:
version "3.21.0" version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"