1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-07-07 16:58:29 +07:00

Отрефакторил бэк, исправил ошибки ()

* fixed paths to match refactored backend

* fixed some paths according to new backend

* fixed auth urls for new endpoints

* fixed urls

* fixed error handling

* fixes

* fixed error handling on user form

* fixed error handling on oauth

* using fallback: true on node pages

* type button for comment attach buttons

* fixed return types of social delete

* changed the way we upload user avatars
This commit is contained in:
muerwre 2022-09-16 14:53:52 +07:00 committed by GitHub
parent 1745cc636d
commit 080d59858c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 544 additions and 420 deletions
.env.local.eslintrc.jspackage.json
src
api
components
comment
CommentAvatar
CommentFormAttachButtons
CommentFormAttaches
editors
AudioGrid
ImageGrid
menu
node
MenuButton
NodeEditMenu
NodeTitle
sortable
SortableGrid
SortableList
index.ts
constants
containers
profile/ProfileSidebarLogoutButton
settings/UserSettingsView
sidebars/ProfileSidebar
hooks
layouts/LabLayout
pages/node
utils
tsconfig.tsbuildinfoyarn.lock

View file

@ -1,6 +1,6 @@
NEXT_PUBLIC_API_HOST=https://pig.staging.vault48.org/
NEXT_PUBLIC_REMOTE_CURRENT=https://pig.staging.vault48.org/static/
# NEXT_PUBLIC_API_HOST=http://localhost:8888/
# NEXT_PUBLIC_REMOTE_CURRENT=http://localhost:8888/static/
# NEXT_PUBLIC_API_HOST=https://pig.staging.vault48.org/
# 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/

View file

@ -23,6 +23,18 @@ module.exports = {
pathGroupsExcludedImportTypes: ['react'],
},
],
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'ramda',
message:
'import from \'~/utils/ramda\' instead',
},
],
},
]
},
parserOptions: {
ecmaVersion: 7,

View file

@ -24,7 +24,7 @@
"mobx": "^6.3.10",
"mobx-persist-store": "^1.0.4",
"mobx-react-lite": "^3.2.3",
"next": "^12.1.0",
"next": "^12.3.0",
"photoswipe": "^4.1.3",
"raleway-cyrillic": "^4.0.2",
"ramda": "^0.26.1",
@ -76,8 +76,8 @@
]
},
"devDependencies": {
"@next/bundle-analyzer": "^12.0.8",
"@next/eslint-plugin-next": "^12.0.8",
"@next/bundle-analyzer": "^12.3.0",
"@next/eslint-plugin-next": "^12.3.0",
"@types/marked": "^4.0.1",
"@types/node": "^11.13.22",
"@types/ramda": "^0.26.33",

View file

@ -15,6 +15,7 @@ import {
ApiLoginWithSocialResult,
ApiRestoreCodeRequest,
ApiRestoreCodeResult,
ApiUpdatePhotoRequest,
ApiUpdateUserRequest,
ApiUpdateUserResult,
ApiUserLoginRequest,
@ -28,44 +29,72 @@ export const apiUserLogin = ({ username, password }: ApiUserLoginRequest) =>
.post<ApiUserLoginResult>(API.USER.LOGIN, { username, password })
.then(cleanResult);
export const apiAuthGetUser = () => api.get<ApiAuthGetUserResult>(API.USER.ME).then(cleanResult);
export const apiAuthGetUser = () =>
api.get<ApiAuthGetUserResult>(API.USER.ME).then(cleanResult);
export const apiAuthGetUserProfile = ({ username }: ApiAuthGetUserProfileRequest) =>
api.get<ApiAuthGetUserProfileResult>(API.USER.PROFILE(username)).then(cleanResult);
export const apiAuthGetUpdates = ({ exclude_dialogs, last }: ApiAuthGetUpdatesRequest) =>
export const apiAuthGetUserProfile = ({
username,
}: ApiAuthGetUserProfileRequest) =>
api
.get<ApiAuthGetUpdatesResult>(API.USER.GET_UPDATES, { params: { exclude_dialogs, last } })
.get<ApiAuthGetUserProfileResult>(API.USER.PROFILE(username))
.then(cleanResult);
export const apiAuthGetUpdates = ({
exclude_dialogs,
last,
}: ApiAuthGetUpdatesRequest) =>
api
.get<ApiAuthGetUpdatesResult>(API.USER.GET_UPDATES, {
params: { exclude_dialogs, last },
})
.then(cleanResult);
export const apiUpdateUser = ({ user }: ApiUpdateUserRequest) =>
api.patch<ApiUpdateUserResult>(API.USER.ME, user).then(cleanResult);
export const apiUpdatePhoto = ({ file }: ApiUpdatePhotoRequest) =>
api.post<ApiUpdateUserResult>(API.USER.UPDATE_PHOTO, file).then(cleanResult);
export const apiUpdateCover = ({ file }: ApiUpdatePhotoRequest) =>
api.post<ApiUpdateUserResult>(API.USER.UPDATE_COVER, file).then(cleanResult);
export const apiRequestRestoreCode = (field: string) =>
api
.post<{ field: string }>(API.USER.REQUEST_CODE(), { field })
.then(cleanResult);
export const apiCheckRestoreCode = ({ code }: ApiCheckRestoreCodeRequest) =>
api.get<ApiCheckRestoreCodeResult>(API.USER.REQUEST_CODE(code)).then(cleanResult);
api
.get<ApiCheckRestoreCodeResult>(API.USER.REQUEST_CODE(code))
.then(cleanResult);
export const apiRestoreCode = ({ code, password }: ApiRestoreCodeRequest) =>
api
.post<ApiRestoreCodeResult>(API.USER.REQUEST_CODE(code), { password })
.put<ApiRestoreCodeResult>(API.USER.REQUEST_CODE(code), { password })
.then(cleanResult);
export const apiGetSocials = () =>
api.get<ApiGetSocialsResult>(API.USER.GET_SOCIALS).then(cleanResult);
export const apiDropSocial = ({ id, provider }: ApiDropSocialRequest) =>
api.delete<ApiDropSocialResult>(API.USER.DROP_SOCIAL(provider, id)).then(cleanResult);
api
.delete<ApiDropSocialResult>(API.USER.DROP_SOCIAL(provider, id))
.then(cleanResult);
export const apiAttachSocial = ({ token }: ApiAttachSocialRequest) =>
api
.post<ApiAttachSocialResult>(API.USER.ATTACH_SOCIAL, { token })
.then(cleanResult);
export const apiLoginWithSocial = ({ token, username, password }: ApiLoginWithSocialRequest) =>
export const apiLoginWithSocial = ({
token,
username,
password,
}: ApiLoginWithSocialRequest) =>
api
.post<ApiLoginWithSocialResult>(API.USER.LOGIN_WITH_SOCIAL, { token, username, password })
.put<ApiLoginWithSocialResult>(API.USER.LOGIN_WITH_SOCIAL, {
token,
username,
password,
})
.then(cleanResult);

View file

@ -1,4 +1,4 @@
import { INotification } from '~/types';
import { IFile, INotification } from '~/types';
import { ISocialAccount, IUser } from '~/types/auth';
export type ApiUserLoginRequest = Record<'username' | 'password', string>;
@ -8,7 +8,13 @@ export type ApiAuthGetUserResult = { user: IUser };
export type ApiUpdateUserRequest = {
user: Partial<IUser & { password: string; newPassword: string }>;
};
export type ApiUpdateUserResult = { user: IUser; errors: Record<Partial<keyof IUser>, string> };
export type ApiUpdatePhotoRequest = {
file: IFile;
};
export type ApiUpdateUserResult = {
user: IUser;
errors: Record<Partial<keyof IUser>, string>;
};
export type ApiAuthGetUserProfileRequest = { username: string };
export type ApiAuthGetUserProfileResult = { user: IUser };
export type ApiAuthGetUpdatesRequest = {
@ -25,7 +31,7 @@ export type ApiRestoreCodeRequest = { code: string; password: string };
export type ApiRestoreCodeResult = { token: string; user: IUser };
export type ApiGetSocialsResult = { accounts: ISocialAccount[] };
export type ApiDropSocialRequest = { id: string; provider: string };
export type ApiDropSocialResult = { accounts: ISocialAccount[] };
export type ApiDropSocialResult = {};
export type ApiAttachSocialRequest = { token: string };
export type ApiAttachSocialResult = { account: ISocialAccount };
export type ApiLoginWithSocialRequest = {

View file

@ -5,10 +5,16 @@ import { api, cleanResult } from '~/utils/api';
export const postCellView = ({ id, flow }: PostCellViewRequest) =>
api
.post<PostCellViewResult>(API.NODE.SET_CELL_VIEW(id), { flow })
.post<PostCellViewResult>(API.NODES.SET_CELL_VIEW(id), { flow })
.then(cleanResult);
export const getSearchResults = ({ text, skip, take }: GetSearchResultsRequest) =>
export const getSearchResults = ({
text,
skip,
take,
}: GetSearchResultsRequest) =>
api
.get<GetSearchResultsResult>(API.SEARCH.NODES, { params: { text, skip, take } })
.get<GetSearchResultsResult>(API.SEARCH.NODES, {
params: { text, skip, take },
})
.then(cleanResult);

View file

@ -38,10 +38,13 @@ export type ApiGetNodeCommentsRequest = {
take?: number;
skip?: number;
};
export type ApiGetNodeCommentsResponse = { comments: IComment[]; comment_count: number };
export type ApiGetNodeCommentsResponse = {
comments: IComment[];
comment_count: number;
};
export const apiPostNode = ({ node }: ApiPostNodeRequest) =>
api.post<ApiPostNodeResult>(API.NODE.SAVE, node).then(cleanResult);
api.post<ApiPostNodeResult>(API.NODES.SAVE, node).then(cleanResult);
export const getNodeDiff = ({
start,
@ -53,7 +56,7 @@ export const getNodeDiff = ({
with_valid,
}: GetNodeDiffRequest) =>
api
.get<GetNodeDiffResult>(API.NODE.GET_DIFF, {
.get<GetNodeDiffResult>(API.NODES.LIST, {
params: {
start,
end,
@ -66,17 +69,20 @@ export const getNodeDiff = ({
})
.then(cleanResult);
export const apiGetNode = ({ id }: ApiGetNodeRequest, config?: AxiosRequestConfig) =>
export const apiGetNode = (
{ id }: ApiGetNodeRequest,
config?: AxiosRequestConfig,
) =>
api
.get<ApiGetNodeResponse>(API.NODE.GET_NODE(id), config)
.get<ApiGetNodeResponse>(API.NODES.GET(id), config)
.then(cleanResult)
.then(data => ({ node: data.node, last_seen: data.last_seen }));
.then((data) => ({ node: data.node, last_seen: data.last_seen }));
export const apiGetNodeWithCancel = ({ id }: ApiGetNodeRequest) => {
const cancelToken = axios.CancelToken.source();
return {
request: api
.get<ApiGetNodeResponse>(API.NODE.GET_NODE(id), {
.get<ApiGetNodeResponse>(API.NODES.GET(id), {
cancelToken: cancelToken.token,
})
.then(cleanResult),
@ -85,7 +91,7 @@ export const apiGetNodeWithCancel = ({ id }: ApiGetNodeRequest) => {
};
export const apiPostComment = ({ id, data }: ApiPostCommentRequest) =>
api.post<ApiPostCommentResult>(API.NODE.COMMENT(id), data).then(cleanResult);
api.post<ApiPostCommentResult>(API.NODES.COMMENT(id), data).then(cleanResult);
export const apiGetNodeComments = ({
id,
@ -93,32 +99,44 @@ export const apiGetNodeComments = ({
skip = 0,
}: ApiGetNodeCommentsRequest) =>
api
.get<ApiGetNodeCommentsResponse>(API.NODE.COMMENT(id), { params: { take, skip } })
.get<ApiGetNodeCommentsResponse>(API.NODES.COMMENT(id), {
params: { take, skip },
})
.then(cleanResult);
export const apiGetNodeRelated = ({ id }: ApiGetNodeRelatedRequest) =>
api.get<ApiGetNodeRelatedResult>(API.NODE.RELATED(id)).then(cleanResult);
api.get<ApiGetNodeRelatedResult>(API.NODES.RELATED(id)).then(cleanResult);
export const apiPostNodeTags = ({ id, tags }: ApiPostNodeTagsRequest) =>
api
.post<ApiPostNodeTagsResult>(API.NODE.UPDATE_TAGS(id), { tags })
.post<ApiPostNodeTagsResult>(API.NODES.UPDATE_TAGS(id), { tags })
.then(cleanResult);
export const apiDeleteNodeTag = ({ id, tagId }: ApiDeleteNodeTagsRequest) =>
api.delete<ApiDeleteNodeTagsResult>(API.NODE.DELETE_TAG(id, tagId)).then(cleanResult);
api
.delete<ApiDeleteNodeTagsResult>(API.NODES.DELETE_TAG(id, tagId))
.then(cleanResult);
export const apiPostNodeLike = ({ id }: ApiPostNodeLikeRequest) =>
api.post<ApiPostNodeLikeResult>(API.NODE.POST_LIKE(id)).then(cleanResult);
api.post<ApiPostNodeLikeResult>(API.NODES.LIKE(id)).then(cleanResult);
export const apiPostNodeHeroic = ({ id }: ApiPostNodeHeroicRequest) =>
api.post<ApiPostNodeHeroicResponse>(API.NODE.POST_HEROIC(id)).then(cleanResult);
api.post<ApiPostNodeHeroicResponse>(API.NODES.HEROIC(id)).then(cleanResult);
export const apiLockNode = ({ id, is_locked }: ApiLockNodeRequest) =>
api
.post<ApiLockNodeResult>(API.NODE.POST_LOCK(id), { is_locked })
.delete<ApiLockNodeResult>(API.NODES.DELETE(id), { params: { is_locked } })
.then(cleanResult);
export const apiLockComment = ({ id, isLocked, nodeId }: ApiLockCommentRequest) =>
export const apiLockComment = ({
id,
isLocked,
nodeId,
}: ApiLockCommentRequest) =>
api
.post<ApiLockcommentResult>(API.NODE.LOCK_COMMENT(nodeId, id), { is_locked: isLocked })
.delete<ApiLockcommentResult>(API.NODES.LOCK_COMMENT(nodeId, id), {
params: {
is_locked: isLocked,
},
})
.then(cleanResult);

View file

@ -1,7 +1,7 @@
import React, { FC } from 'react';
import { Avatar } from '~/components/common/Avatar';
import { MenuButton } from '~/components/menu';
import { MenuButton } from '~/components/menu/MenuButton';
import { ProfileQuickInfo } from '~/containers/profile/ProfileQuickInfo';
import { IUser } from '~/types/auth';
import { path } from '~/utils/ramda';
@ -16,7 +16,11 @@ const CommentAvatar: FC<Props> = ({ user, className }) => {
<MenuButton
position="auto"
icon={
<Avatar url={path(['photo', 'url'], user)} username={user.username} className={className} />
<Avatar
url={path(['photo', 'url'], user)}
username={user.username}
className={className}
/>
}
>
<ProfileQuickInfo user={user} />

View file

@ -10,26 +10,32 @@ interface IProps {
const CommentFormAttachButtons: FC<IProps> = ({ onUpload }) => {
const onInputChange = useCallback(
event => {
(event) => {
event.preventDefault();
const files = Array.from(event.target?.files as File[]).filter((file: File) =>
COMMENT_FILE_TYPES.includes(file.type)
const files = Array.from(event.target?.files as File[]).filter(
(file: File) => COMMENT_FILE_TYPES.includes(file.type),
);
if (!files || !files.length) return;
onUpload(files);
},
[onUpload]
[onUpload],
);
return (
<ButtonGroup>
<Button iconLeft="photo" size="small" color="gray" iconOnly>
<Button iconLeft="photo" size="small" color="gray" iconOnly type="button">
<input type="file" onInput={onInputChange} multiple accept="image/*" />
</Button>
<Button iconRight="audio" size="small" color="gray" iconOnly>
<Button
iconRight="audio"
size="small"
color="gray"
iconOnly
type="button"
>
<input type="file" onInput={onInputChange} multiple accept="audio/*" />
</Button>
</ButtonGroup>

View file

@ -1,6 +1,7 @@
import React, { FC, useCallback } from 'react';
import { SortableAudioGrid, SortableImageGrid } from '~/components/sortable';
import { SortableAudioGrid } from '~/components/sortable/SortableAudioGrid';
import { SortableImageGrid } from '~/components/sortable/SortableImageGrid';
import { COMMENT_FILE_TYPES } from '~/constants/uploads';
import { useFileDropZone } from '~/hooks';
import { IFile } from '~/types';
@ -27,34 +28,36 @@ const CommentFormAttaches: FC = () => {
const onImageMove = useCallback(
(newFiles: IFile[]) => {
setFiles([...filesAudios, ...newFiles.filter(it => it)]);
setFiles([...filesAudios, ...newFiles.filter((it) => it)]);
},
[setFiles, filesImages, filesAudios]
[setFiles, filesImages, filesAudios],
);
const onAudioMove = useCallback(
(newFiles: IFile[]) => {
setFiles([...filesImages, ...newFiles]);
},
[setFiles, filesImages, filesAudios]
[setFiles, filesImages, filesAudios],
);
const onFileDelete = useCallback(
(fileId: IFile['id']) => {
setFiles(files.filter(file => file.id !== fileId));
setFiles(files.filter((file) => file.id !== fileId));
},
[files, setFiles]
[files, setFiles],
);
const onAudioTitleChange = useCallback(
(fileId: IFile['id'], title: string) => {
setFiles(
files.map(file =>
file.id === fileId ? { ...file, metadata: { ...file.metadata, title } } : file
)
files.map((file) =>
file.id === fileId
? { ...file, metadata: { ...file.metadata, title } }
: file,
),
);
},
[files, setFiles]
[files, setFiles],
);
if (!hasAttaches) return null;

View file

@ -1,6 +1,6 @@
import React, { FC, useCallback } from 'react';
import { SortableAudioGrid } from '~/components/sortable';
import { SortableAudioGrid } from '~/components/sortable/SortableAudioGrid';
import { UploadStatus } from '~/store/uploader/UploaderStore';
import { IFile } from '~/types';
@ -15,25 +15,27 @@ const AudioGrid: FC<IProps> = ({ files, setFiles, locked }) => {
(newFiles: IFile[]) => {
setFiles(newFiles);
},
[setFiles, files]
[setFiles, files],
);
const onDrop = useCallback(
(remove_id: IFile['id']) => {
setFiles(files.filter(file => file && file.id !== remove_id));
setFiles(files.filter((file) => file && file.id !== remove_id));
},
[setFiles, files]
[setFiles, files],
);
const onTitleChange = useCallback(
(changeId: IFile['id'], title: string) => {
setFiles(
files.map(file =>
file && file.id === changeId ? { ...file, metadata: { ...file.metadata, title } } : file
)
files.map((file) =>
file && file.id === changeId
? { ...file, metadata: { ...file.metadata, title } }
: file,
),
);
},
[setFiles, files]
[setFiles, files],
);
return (

View file

@ -1,6 +1,6 @@
import React, { FC, useCallback } from 'react';
import { SortableImageGrid } from '~/components/sortable';
import { SortableImageGrid } from '~/components/sortable/SortableImageGrid';
import { useWindowSize } from '~/hooks/dom/useWindowSize';
import { UploadStatus } from '~/store/uploader/UploaderStore';
import { IFile } from '~/types';
@ -16,14 +16,14 @@ const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
const onMove = useCallback(
(newFiles: IFile[]) => {
setFiles(newFiles.filter(it => it));
setFiles(newFiles.filter((it) => it));
},
[setFiles, files],
);
const onDrop = useCallback(
(id: IFile['id']) => {
setFiles(files.filter(file => file && file.id !== id));
setFiles(files.filter((file) => file && file.id !== id));
},
[setFiles, files],
);

View file

@ -1,5 +0,0 @@
export * from './VerticalMenu';
export * from './HorizontalMenu';
export * from './MenuButton';
export * from './MenuItemWithIcon';
export * from './SeparatedMenu';

View file

@ -1,38 +0,0 @@
import React, { FC } from 'react';
import { Filler } from '~/components/containers/Filler';
import { Group } from '~/components/containers/Group';
import styles from './styles.module.scss';
interface IProps {
title: string;
description?: string;
icon: string;
}
const MenuButton: FC<IProps> = ({
title,
icon,
description,
}) => (
<div
className={styles.button}
>
<Group horizontal>
<div className={styles.icon}>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0V0z" />
<path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z" />
</svg>
</div>
<Filler>
<div className={styles.title}>{title}</div>
{ description && <div className={styles.description}>{description}</div> }
</Filler>
</Group>
</div>
);
export { MenuButton };

View file

@ -1,29 +0,0 @@
@import 'src/styles/variables';
.button {
fill: white;
height: 48px;
display: flex;
align-items: center;
}
.icon {
flex: 0 0 38px;
align-items: center;
justify-content: center;
display: flex;
}
.title {
font: $font_16_semibold;
text-transform: uppercase;
margin-bottom: 2px;
white-space: nowrap;
flex: 1;
}
.description {
font: $font_12_regular;
color: $gray_75;
white-space: nowrap;
}

View file

@ -3,7 +3,9 @@ import React, { VFC } from 'react';
import Tippy from '@tippyjs/react';
import { Icon } from '~/components/input/Icon';
import { MenuButton, MenuItemWithIcon, SeparatedMenu } from '~/components/menu';
import { MenuButton } from '~/components/menu/MenuButton';
import { MenuItemWithIcon } from '~/components/menu/MenuItemWithIcon';
import { SeparatedMenu } from '~/components/menu/SeparatedMenu';
import { useWindowSize } from '~/hooks/dom/useWindowSize';
import styles from './styles.module.scss';

View file

@ -3,7 +3,7 @@ import React, { memo, VFC } from 'react';
import classNames from 'classnames';
import { Icon } from '~/components/input/Icon';
import { SeparatedMenu } from '~/components/menu';
import { SeparatedMenu } from '~/components/menu/SeparatedMenu';
import { NodeEditMenu } from '~/components/node/NodeEditMenu';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { getPrettyDate } from '~/utils/dom';
@ -35,7 +35,6 @@ interface IProps {
const NodeTitle: VFC<IProps> = memo(
({
id,
title,
username,
createdAt,
@ -69,7 +68,9 @@ const NodeTitle: VFC<IProps> = memo(
{isLoading ? (
<Placeholder width="100px" />
) : (
`~${username.toLocaleLowerCase()}, ${getPrettyDate(createdAt)}`
`~${username.toLocaleLowerCase()}, ${getPrettyDate(
createdAt,
)}`
)}
</aside>
)}
@ -90,7 +91,9 @@ const NodeTitle: VFC<IProps> = memo(
{canLike && (
<div
className={classNames(styles.button, styles.like, { [styles.is_liked]: isLiked })}
className={classNames(styles.button, styles.like, {
[styles.is_liked]: isLiked,
})}
>
{isLiked ? (
<Icon icon="heart_full" size={24} onClick={onLike} />
@ -107,7 +110,7 @@ const NodeTitle: VFC<IProps> = memo(
</div>
</div>
);
}
},
);
export { NodeTitle };

View file

@ -5,7 +5,7 @@ import { rectSortingStrategy, SortableContext } from '@dnd-kit/sortable';
import classNames from 'classnames';
import { DragOverlayItem } from '~/components/sortable/DragOverlayItem';
import { useSortableActions } from '~/hooks/sortable';
import { useSortableActions } from '~/hooks/sortable/useSortableActions';
import { DivProps } from '~/utils/types';
import { SortableItem } from '../SortableItem';
@ -16,7 +16,7 @@ interface SortableGridProps<
ItemRendererProps extends {},
LockedRendererProps extends {},
Item extends {},
Locked extends {}
Locked extends {},
> {
items: Item[];
locked: Locked[];
@ -44,18 +44,18 @@ const SortableGrid = <RIP, RLP, I, L>({
onSortEnd,
size,
}: SortableGridProps<RIP, RLP, I, L>) => {
const { sensors, onDragEnd, onDragStart, draggingItem, ids } = useSortableActions(
items,
getID,
onSortEnd
);
const { sensors, onDragEnd, onDragStart, draggingItem, ids } =
useSortableActions(items, getID, onSortEnd);
const gridStyle = useMemo<DivProps['style']>(
() =>
size
? { gridTemplateColumns: size && `repeat(auto-fill, minmax(${size}px, 1fr))` }
? {
gridTemplateColumns:
size && `repeat(auto-fill, minmax(${size}px, 1fr))`,
}
: undefined,
[size]
[size],
);
return (
@ -67,30 +67,35 @@ const SortableGrid = <RIP, RLP, I, L>({
>
<SortableContext items={ids} strategy={rectSortingStrategy}>
<div className={classNames(styles.grid, className)} style={gridStyle}>
{items.map(item => (
{items.map((item) => (
<SortableItem
key={getID(item)}
id={getID(item)}
className={
draggingItem && getID(item) === getID(draggingItem) ? styles.dragging : undefined
draggingItem && getID(item) === getID(draggingItem)
? styles.dragging
: undefined
}
>
{createElement(renderItem, { ...renderItemProps, item })}
</SortableItem>
))}
{locked.map(item =>
{locked.map((item) =>
createElement(renderLocked, {
...renderLockedProps,
locked: item,
key: getLockedID(item),
})
}),
)}
<DragOverlay>
{draggingItem ? (
<DragOverlayItem>
{createElement(renderItem, { ...renderItemProps, item: draggingItem })}
{createElement(renderItem, {
...renderItemProps,
item: draggingItem,
})}
</DragOverlayItem>
) : null}
</DragOverlay>

View file

@ -1,12 +1,15 @@
import React, { createElement, FC } from 'react';
import { closestCenter, DndContext, DragOverlay } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import {
SortableContext,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import classNames from 'classnames';
import { DragOverlayItem } from '~/components/sortable/DragOverlayItem';
import { SortableItem } from '~/components/sortable/SortableItem';
import { useSortableActions } from '~/hooks/sortable';
import { useSortableActions } from '~/hooks/sortable/useSortableActions';
import styles from './styles.module.scss';
@ -14,7 +17,7 @@ interface SortableListProps<
RenderItemProps extends {},
RenderLockedProps extends {},
Item extends {},
Locked extends {}
Locked extends {},
> {
items: Item[];
locked: Locked[];
@ -40,11 +43,8 @@ const SortableList = <RIP, RLP, I, L>({
renderLockedProps,
onSortEnd,
}: SortableListProps<RIP, RLP, I, L>) => {
const { sensors, onDragEnd, onDragStart, draggingItem, ids } = useSortableActions(
items,
getID,
onSortEnd
);
const { sensors, onDragEnd, onDragStart, draggingItem, ids } =
useSortableActions(items, getID, onSortEnd);
return (
<DndContext
@ -55,30 +55,39 @@ const SortableList = <RIP, RLP, I, L>({
>
<SortableContext items={ids} strategy={verticalListSortingStrategy}>
<div className={classNames(styles.grid, className)}>
{items.map(item => (
{items.map((item) => (
<SortableItem
key={getID(item)}
id={getID(item)}
className={
draggingItem && getID(item) === getID(draggingItem) ? styles.dragging : undefined
draggingItem && getID(item) === getID(draggingItem)
? styles.dragging
: undefined
}
>
{createElement(renderItem, { ...renderItemProps, item, key: getID(item) })}
{createElement(renderItem, {
...renderItemProps,
item,
key: getID(item),
})}
</SortableItem>
))}
{locked.map(item =>
{locked.map((item) =>
createElement(renderLocked, {
...renderLockedProps,
locked: item,
key: getLockedID(item),
})
}),
)}
<DragOverlay>
{draggingItem ? (
<DragOverlayItem>
{createElement(renderItem, { ...renderItemProps, item: draggingItem })}
{createElement(renderItem, {
...renderItemProps,
item: draggingItem,
})}
</DragOverlayItem>
) : null}
</DragOverlay>

View file

@ -1,2 +0,0 @@
export * from './SortableImageGrid';
export * from './SortableAudioGrid';

View file

@ -5,38 +5,43 @@ import { CONFIG } from '~/utils/config';
export const API = {
BASE: CONFIG.apiHost,
USER: {
LOGIN: '/user/login',
OAUTH_WINDOW: (provider: OAuthProvider) => `${CONFIG.apiHost}oauth/${provider}/redirect`,
ME: '/user/',
PROFILE: (username: string) => `/user/user/${username}/profile`,
MESSAGES: (username: string) => `/user/user/${username}/messages`,
MESSAGE_SEND: (username: string) => `/user/user/${username}/messages`,
MESSAGE_DELETE: (username: string, id: number) => `/user/user/${username}/messages/${id}`,
GET_UPDATES: '/user/updates',
REQUEST_CODE: (code?: string) => `/user/restore/${code || ''}`,
LOGIN: '/auth',
OAUTH_WINDOW: (provider: OAuthProvider) =>
`${CONFIG.apiHost}oauth/${provider}/redirect`,
ME: '/auth',
UPDATE_PHOTO: '/auth/photo',
UPDATE_COVER: '/auth/photo',
PROFILE: (username: string) => `/users/${username}/profile`,
MESSAGES: (username: string) => `/users/${username}/messages`,
MESSAGE_SEND: (username: string) => `/users/${username}/messages`,
MESSAGE_DELETE: (username: string, id: number) =>
`/users/${username}/messages/${id}`,
GET_UPDATES: '/auth/updates',
REQUEST_CODE: (code?: string) => `/auth/restore/${code || ''}`,
UPLOAD: (target, type) => `/upload/${target}/${type}`,
GET_SOCIALS: '/oauth/',
GET_SOCIALS: '/oauth',
DROP_SOCIAL: (provider, id) => `/oauth/${provider}/${id}`,
ATTACH_SOCIAL: `/oauth/attach`,
LOGIN_WITH_SOCIAL: `/oauth/login`,
ATTACH_SOCIAL: `/oauth`,
LOGIN_WITH_SOCIAL: `/oauth`,
},
NODE: {
SAVE: '/node/',
GET: '/node/',
GET_DIFF: '/flow/diff',
GET_NODE: (id: number | string) => `/node/${id}`,
NODES: {
SAVE: '/nodes/',
LIST: '/nodes/',
GET: (id: number | string) => `/nodes/${id}`,
DELETE: (id: INode['id']) => `/nodes/${id}`,
LIKE: (id: INode['id']) => `/nodes/${id}/like`,
HEROIC: (id: INode['id']) => `/nodes/${id}/heroic`,
SET_CELL_VIEW: (id: INode['id']) => `/nodes/${id}/cell-view`,
RELATED: (id: INode['id']) => `/nodes/${id}/related`,
COMMENT: (id: INode['id'] | string) => `/node/${id}/comment`,
RELATED: (id: INode['id']) => `/node/${id}/related`,
UPDATE_TAGS: (id: INode['id']) => `/node/${id}/tags`,
DELETE_TAG: (id: INode['id'], tagId: ITag['ID']) => `/node/${id}/tags/${tagId}`,
POST_LIKE: (id: INode['id']) => `/node/${id}/like`,
POST_HEROIC: (id: INode['id']) => `/node/${id}/heroic`,
POST_LOCK: (id: INode['id']) => `/node/${id}/lock`,
UPDATE_TAGS: (id: INode['id']) => `/nodes/${id}/tags`,
DELETE_TAG: (id: INode['id'], tagId: ITag['ID']) =>
`/nodes/${id}/tags/${tagId}`,
COMMENT: (id: INode['id'] | string) => `/nodes/${id}/comments`,
LOCK_COMMENT: (id: INode['id'], comment_id: IComment['id']) =>
`/node/${id}/comment/${comment_id}/lock`,
SET_CELL_VIEW: (id: INode['id']) => `/node/${id}/cell-view`,
`/nodes/${id}/comments/${comment_id}`,
},
SEARCH: {
NODES: '/search/nodes',
@ -49,12 +54,12 @@ export const API = {
GITHUB_ISSUES: 'https://api.github.com/repos/muerwre/vault-frontend/issues',
},
TAG: {
NODES: `/tag/nodes`,
AUTOCOMPLETE: `/tag/autocomplete`,
NODES: `/tags/nodes`,
AUTOCOMPLETE: `/tags/autocomplete`,
},
LAB: {
NODES: `/lab/`,
STATS: '/lab/stats',
UPDATES: '/lab/updates',
NODES: `/nodes/lab`,
STATS: '/nodes/lab/stats',
UPDATES: '/nodes/lab/updates',
},
};

View file

@ -3,8 +3,7 @@ import React, { FC } from 'react';
import { Group } from '~/components/containers/Group';
import { Padder } from '~/components/containers/Padder';
import { Button } from '~/components/input/Button';
import { Icon } from '~/components/input/Icon';
import { MenuButton } from '~/components/menu';
import { MenuButton } from '~/components/menu/MenuButton';
import styles from './styles.module.scss';
@ -12,17 +11,30 @@ interface ProfileSidebarLogoutButtonProps {
onLogout?: () => void;
}
const ProfileSidebarLogoutButton: FC<ProfileSidebarLogoutButtonProps> = ({ onLogout }) => (
<MenuButton icon={<Button color="link" iconRight="logout">Выйти</Button>} position="top-end">
const ProfileSidebarLogoutButton: FC<ProfileSidebarLogoutButtonProps> = ({
onLogout,
}) => (
<MenuButton
icon={
<Button color="link" iconRight="logout">
Выйти
</Button>
}
position="top-end"
>
<Padder className={styles.wrapper}>
<Group>
<h5>Захотелось наружу?</h5>
<div>Там холодно, страшно и больше не раздают пончики!</div>
<div />
<div><Button onClick={onLogout} color="primary" stretchy>Выпустите меня!</Button></div>
<div>
<Button onClick={onLogout} color="primary" stretchy>
Выпустите меня!
</Button>
</div>
</Group>
</Padder>
</MenuButton>
);
export { ProfileSidebarLogoutButton }
export { ProfileSidebarLogoutButton };

View file

@ -16,9 +16,6 @@ import styles from './styles.module.scss';
interface UserSettingsViewProps {}
const getError = (error?: string) =>
error && has(error, ERROR_LITERAL) ? error : undefined;
const UserSettingsView: FC<UserSettingsViewProps> = () => {
const { values, handleChange, errors } = useSettings();
const { isPhone } = useWindowSize();
@ -41,7 +38,7 @@ const UserSettingsView: FC<UserSettingsViewProps> = () => {
value={values.fullname}
handler={handleChange('fullname')}
title="Полное имя"
error={getError(errors.fullname)}
error={errors.fullname}
/>
<Textarea
@ -79,14 +76,14 @@ const UserSettingsView: FC<UserSettingsViewProps> = () => {
value={values.username}
handler={handleChange('username')}
title="Логин"
error={getError(errors.username)}
error={errors.username}
/>
<InputText
value={values.email}
handler={handleChange('email')}
title="E-mail"
error={getError(errors.email)}
error={errors.email}
/>
<InputText
@ -94,7 +91,7 @@ const UserSettingsView: FC<UserSettingsViewProps> = () => {
handler={handleChange('newPassword')}
title="Новый пароль"
type="password"
error={getError(errors.newPassword)}
error={errors.newPassword}
/>
<InputText
@ -102,7 +99,7 @@ const UserSettingsView: FC<UserSettingsViewProps> = () => {
handler={handleChange('password')}
title="Старый пароль"
type="password"
error={getError(errors.password)}
error={errors.password}
/>
<div className={styles.small}>

View file

@ -1,7 +1,5 @@
import React, { useCallback, useEffect, useMemo, VFC } from 'react';
import { isNil } from 'ramda';
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
import { ProfileSidebarNotes } from '~/components/profile/ProfileSidebarNotes';
import { ProfileSidebarSettings } from '~/components/profile/ProfileSidebarSettings';
@ -13,6 +11,7 @@ import { ProfileSidebarMenu } from '~/containers/profile/ProfileSidebarMenu';
import { useAuth } from '~/hooks/auth/useAuth';
import { useUser } from '~/hooks/auth/useUser';
import type { SidebarComponentProps } from '~/types/sidebar';
import { isNil } from '~/utils/ramda';
const tabs = ['profile', 'bookmarks'] as const;
type TabName = typeof tabs[number];

View file

@ -2,7 +2,12 @@ import { useCallback, useMemo } from 'react';
import useSWR from 'swr';
import { apiAttachSocial, apiDropSocial, apiGetSocials, apiLoginWithSocial } from '~/api/auth';
import {
apiAttachSocial,
apiDropSocial,
apiGetSocials,
apiLoginWithSocial,
} from '~/api/auth';
import { API } from '~/constants/api';
import { Dialog } from '~/constants/modal';
import { useAuth } from '~/hooks/auth/useAuth';
@ -15,13 +20,14 @@ export const useOAuth = () => {
const { isUser, setToken } = useAuth();
const { showModal, hideModal } = useModal();
const { data, isValidating: isLoading, mutate } = useSWR(
isUser ? API.USER.GET_SOCIALS : null,
async () => {
const {
data,
isValidating: isLoading,
mutate,
} = useSWR(isUser ? API.USER.GET_SOCIALS : null, async () => {
const result = await apiGetSocials();
return result.accounts;
}
);
});
const openOauthWindow = useCallback((provider: OAuthProvider) => {
window.open(API.USER.OAUTH_WINDOW(provider), '', 'width=600,height=400');
@ -37,10 +43,9 @@ export const useOAuth = () => {
setToken(result.token);
hideModal();
} catch (error) {
const needsRegister: string | undefined = path(
['response', 'data', 'needs_register'],
error
);
console.log(path(['response'], error));
const needsRegister = path(['response', 'status'], error) === 428;
if (needsRegister && token) {
showModal(Dialog.LoginSocialRegister, { token });
@ -50,7 +55,7 @@ export const useOAuth = () => {
showErrorToast(error);
}
},
[showModal, hideModal, setToken]
[showModal, hideModal, setToken],
);
const loginWithSocial = useCallback(
@ -62,7 +67,7 @@ export const useOAuth = () => {
setToken(token);
hideModal();
},
[setToken, hideModal]
[setToken, hideModal],
);
const attachAccount = useCallback(
@ -76,7 +81,7 @@ export const useOAuth = () => {
showErrorToast(error);
}
},
[mutate]
[mutate],
);
const dropAccount = useCallback(
@ -88,7 +93,7 @@ export const useOAuth = () => {
showErrorToast(error);
}
},
[mutate]
[mutate],
);
const accounts = useMemo(() => data || [], [data]);

View file

@ -1,14 +1,14 @@
import { useEffect } from 'react';
import { EventMessageType } from '~/constants/events';
import { Dialog } from '~/constants/modal';
import { useAuth } from '~/hooks/auth/useAuth';
import { useOAuth } from '~/hooks/auth/useOAuth';
import { useModal } from '~/hooks/modal/useModal';
import { includes, path, values } from '~/utils/ramda';
import { showToastError } from '~/utils/toast';
/** reacts to events passed by window.postMessage */
export const useMessageEventReactions = () => {
export const useOauthEventListeners = () => {
const { loginWithSocial, createSocialAccount, attachAccount } = useOAuth();
const { showModal } = useModal();
const { isUser } = useAuth();
@ -25,7 +25,6 @@ export const useMessageEventReactions = () => {
switch (type) {
case EventMessageType.OAuthLogin:
// TODO: do we really need it?
loginWithSocial(path(['data', 'payload', 'token'], event));
break;
case EventMessageType.OAuthProcessed:
@ -35,6 +34,12 @@ export const useMessageEventReactions = () => {
void createSocialAccount(path(['data', 'payload', 'token'], event));
}
break;
case EventMessageType.OAuthError:
const message = path(['data', 'payload', 'error'], event);
if (message && typeof message === 'string') {
showToastError(message);
}
break;
default:
console.log('unknown message', event.data);
}

View file

@ -1,15 +1,19 @@
import { useCallback } from 'react';
import { apiUpdateUser } from '~/api/auth';
import { apiUpdatePhoto, apiUpdateUser } from '~/api/auth';
import { ApiUpdateUserRequest } from '~/api/auth/types';
import { UploadSubject, UploadTarget } from '~/constants/uploads';
import { useUser } from '~/hooks/auth/useUser';
import { useUploader } from '~/hooks/data/useUploader';
import { IFile } from '~/types';
import { showErrorToast } from '~/utils/errors/showToast';
export const usePatchUser = () => {
const { update } = useUser();
const { uploadFile } = useUploader(UploadSubject.Avatar, UploadTarget.Profiles);
const { uploadFile } = useUploader(
UploadSubject.Avatar,
UploadTarget.Profiles,
);
const save = useCallback(
async (user: Partial<ApiUpdateUserRequest['user']>) => {
@ -17,19 +21,25 @@ export const usePatchUser = () => {
await update(result.user);
return result.user;
},
[update]
[update],
);
const updatePhoto = useCallback(
async (file: File) => {
async (photo: File) => {
try {
const photo = await uploadFile(file);
await save({ photo });
const file = await uploadFile(photo);
if (!file) {
return;
}
const result = await apiUpdatePhoto({ file: file! });
await update(result.user);
} catch (error) {
showErrorToast(error);
}
},
[uploadFile, save]
[uploadFile, save],
);
return { updatePhoto, save };

View file

@ -8,16 +8,14 @@ import { COMMENTS_DISPLAY } from '~/constants/node';
import { IComment } from '~/types';
import { flatten, isNil } from '~/utils/ramda';
const getKey: (nodeId: number) => SWRInfiniteKeyLoader = (nodeId: number) => (
pageIndex,
previousPageData: IComment[]
) => {
const getKey: (nodeId: number) => SWRInfiniteKeyLoader =
(nodeId: number) => (pageIndex, previousPageData: IComment[]) => {
if (pageIndex > 0 && !previousPageData?.length) return null;
return `${API.NODE.COMMENT(nodeId)}?page=${pageIndex}`;
};
return `${API.NODES.COMMENT(nodeId)}?page=${pageIndex}`;
};
const extractKey = (key: string) => {
const re = new RegExp(`${API.NODE.COMMENT('\\d+')}\\?page=(\\d+)`);
const re = new RegExp(`${API.NODES.COMMENT('\\d+')}\\?page=(\\d+)`);
const match = key.match(re);
if (!match || !Array.isArray(match) || isNil(match[1])) {
@ -41,14 +39,26 @@ export const useGetComments = (nodeId: number, fallbackData?: IComment[]) => {
},
{
fallbackData: fallbackData && [fallbackData],
}
},
);
const comments = useMemo(() => flatten(data || []), [data]);
const hasMore = (data?.[size - 1]?.length || 0) >= COMMENTS_DISPLAY ||
const hasMore =
(data?.[size - 1]?.length || 0) >= COMMENTS_DISPLAY ||
(!!data?.length && data?.length > 0 && isValidating);
const onLoadMoreComments = useCallback(() => setSize(size + 1), [setSize, size]);
const onLoadMoreComments = useCallback(
() => setSize(size + 1),
[setSize, size],
);
return { comments, hasMore, onLoadMoreComments, isLoading: !data && isValidating, mutate, data, isLoadingMore: !!data?.length && isValidating };
return {
comments,
hasMore,
onLoadMoreComments,
isLoading: !data && isValidating,
mutate,
data,
isLoadingMore: !!data?.length && isValidating,
};
};

View file

@ -8,8 +8,9 @@ import { INode } from '~/types';
import { ApiGetNodeRelatedResult } from '~/types/node';
export const useGetNodeRelated = (id?: INode['id']) => {
const { data, isValidating, mutate } = useSWR<ApiGetNodeRelatedResult>(API.NODE.RELATED(id), () =>
apiGetNodeRelated({ id })
const { data, isValidating, mutate } = useSWR<ApiGetNodeRelatedResult>(
API.NODES.RELATED(id),
() => apiGetNodeRelated({ id }),
);
const refresh = useCallback(() => mutate(data, true), [data, mutate]);

View file

@ -12,7 +12,7 @@ import { ApiGetNodeResponse } from '~/types/node';
const getKey = (nodeId: number, userId = 0) =>
JSON.stringify({
url: API.NODE.GET_NODE(nodeId),
url: API.NODES.GET(nodeId),
userId,
});
@ -21,7 +21,7 @@ export const useLoadNode = (id: number, fallbackData?: ApiGetNodeResponse) => {
const { data, isValidating, mutate } = useSWR<ApiGetNodeResponse>(
getKey(id, user.id),
() => apiGetNode({ id }),
{ fallbackData, revalidateOnMount: true }
{ fallbackData, revalidateOnMount: true },
);
const update = useCallback(
@ -33,7 +33,7 @@ export const useLoadNode = (id: number, fallbackData?: ApiGetNodeResponse) => {
await mutate({ node: { ...data.node, ...node } }, true);
},
[data, mutate]
[data, mutate],
);
useOnNodeSeen(data?.node);

View file

@ -1 +0,0 @@
export * from './useSortableActions';

View file

@ -1,4 +1,5 @@
import React, { FC } from 'react';
import { Group } from '~/components/containers/Group';
import { Sticky } from '~/components/containers/Sticky';
import { LabHead } from '~/components/lab/LabHead';

View file

@ -23,6 +23,13 @@ import { NodeRelatedProvider } from '~/utils/providers/NodeRelatedProvider';
import { uniqBy } from '~/utils/ramda';
export const getStaticPaths = async () => {
if (process.env.NODE_ENV === 'development') {
return {
paths: [],
fallback: 'blocking',
};
}
const recent = await getNodeDiff({
with_heroes: false,
with_recent: true,
@ -30,40 +37,48 @@ export const getStaticPaths = async () => {
with_valid: false,
});
const recentIDs = uniqBy(it => it.id, [
const recentIDs = uniqBy(
(it) => it.id,
[
...(recent.after || []),
...(recent.before || []),
...(recent.recent || []),
])
.filter(it => it.id)
.map(it => it.id!.toString());
],
)
.filter((it) => it.id)
.map((it) => it.id!.toString());
return {
paths: recentIDs.map(id => ({ params: { id } })),
fallback: 'blocking',
paths: recentIDs.map((id) => ({ params: { id } })),
fallback: true,
};
};
export const getStaticProps = async (
context
): Promise<GetStaticPropsResult<{ fallbackData: ApiGetNodeResponse; comments?: IComment[] }>> => {
context,
): Promise<
GetStaticPropsResult<{
fallbackData: ApiGetNodeResponse;
comments?: IComment[];
}>
> => {
try {
if (!context.params?.id) {
return { notFound: true };
}
const id = parseInt(context.params.id, 10);
if (!id) {
return { notFound: true };
}
const fallbackData = await apiGetNode({ id });
const comments = await apiGetNodeComments({
const [fallbackData, { comments }] = await Promise.all([
apiGetNode({ id }),
apiGetNodeComments({
id,
take: COMMENTS_DISPLAY,
});
}),
]);
return {
props: {
@ -71,7 +86,7 @@ export const getStaticProps = async (
...fallbackData,
last_seen: fallbackData.last_seen ?? null,
},
comments: comments.comments,
comments,
},
revalidate: 7 * 86400, // every week
};
@ -83,11 +98,15 @@ export const getStaticProps = async (
}
};
type Props = RouteComponentProps<{ id: string }> & InferGetStaticPropsType<typeof getStaticProps>;
type Props = RouteComponentProps<{ id: string }> &
InferGetStaticPropsType<typeof getStaticProps>;
const NodePage: FC<Props> = observer(props => {
const NodePage: FC<Props> = observer((props) => {
const id = useNodePageParams();
const { node, isLoading, update, lastSeen } = useLoadNode(parseInt(id, 10), props.fallbackData);
const { node, isLoading, update, lastSeen } = useLoadNode(
parseInt(id, 10),
props.fallbackData,
);
const onShowImageModal = useImageModal();
@ -101,9 +120,11 @@ const NodePage: FC<Props> = observer(props => {
isLoadingMore: isLoadingMoreComments,
} = useNodeComments(parseInt(id, 10), props.comments);
const { onDelete: onTagDelete, onChange: onTagsChange, onClick: onTagClick } = useNodeTags(
parseInt(id, 10)
);
const {
onDelete: onTagDelete,
onChange: onTagsChange,
onClick: onTagClick,
} = useNodeTags(parseInt(id, 10));
const [canEdit] = useNodePermissions(node);
if (!node) {

View file

@ -6,6 +6,10 @@ export const getErrorMessage = (error: unknown): string | undefined => {
return undefined;
}
if (path(['response', 'data', 'message'], error)) {
return path(['response', 'data', 'message'], error) as string;
}
if (typeof error === 'string' && has(error, ERROR_LITERAL)) {
return ERROR_LITERAL[error];
}

View file

@ -1,6 +1,8 @@
import { hasPath, path } from '~/utils/ramda';
export const getValidationErrors = (error: unknown): Record<string, string> | undefined => {
export const getValidationErrors = (
error: unknown,
): Record<string, string> | undefined => {
if (hasPath(['response', 'data', 'errors'], error)) {
return path(['response', 'data', 'errors'], error);
}

View file

@ -4,7 +4,7 @@ import { observer } from 'mobx-react-lite';
import { EMPTY_USER } from '~/constants/auth';
import { useAuth } from '~/hooks/auth/useAuth';
import { useMessageEventReactions } from '~/hooks/auth/useMessageEventReactions';
import { useOauthEventListeners } from '~/hooks/auth/useOauthEventListeners';
import { useRestorePasswordRedirect } from '~/hooks/auth/useRestorePasswordRedirect';
import { useSessionCookie } from '~/hooks/auth/useSessionCookie';
@ -14,7 +14,7 @@ const AuthContext = createContext<AuthProviderContextType>({
user: EMPTY_USER,
isUser: false,
isTester: false,
setIsTester: isTester => isTester,
setIsTester: (isTester) => isTester,
logout: () => {},
login: async () => EMPTY_USER,
setToken: () => {},
@ -23,7 +23,7 @@ const AuthContext = createContext<AuthProviderContextType>({
export const AuthProvider: FC = observer(({ children }) => {
const value = useAuth();
useMessageEventReactions();
useOauthEventListeners();
useRestorePasswordRedirect();
useSessionCookie();

View file

@ -1,6 +1,11 @@
import { FC, PropsWithChildren, useCallback, useRef } from 'react';
import { FormikConfig, useFormik, FormikProvider, useFormikContext } from 'formik';
import {
FormikConfig,
useFormik,
FormikProvider,
useFormikContext,
} from 'formik';
import { Asserts, object, string } from 'yup';
import { ERRORS } from '~/constants/errors';
@ -13,15 +18,11 @@ import { showErrorToast } from '~/utils/errors/showToast';
import { showToastSuccess } from '~/utils/toast';
const validationSchema = object({
username: string()
.default('')
.required(ERRORS.REQUIRED),
username: string().default('').required(ERRORS.REQUIRED),
fullname: string().default(''),
newPassword: string().optional(),
description: string().default(''),
email: string()
.default('')
.email(ERRORS.NOT_AN_EMAIL),
email: string().default('').email(ERRORS.NOT_AN_EMAIL),
password: string().optional(),
});
@ -29,7 +30,7 @@ export type ProfileFormData = Asserts<typeof validationSchema>;
export const useSettingsForm = (
values: ProfileFormData,
submitter: (data: ProfileFormData) => Promise<IUser>
submitter: (data: ProfileFormData) => Promise<IUser>,
) => {
const initialValues = useRef(values).current;
@ -39,7 +40,9 @@ export const useSettingsForm = (
const fields = {
...values,
password: values.password?.length ? values.password : undefined,
new_password: values.newPassword?.length ? values.newPassword : undefined,
new_password: values.newPassword?.length
? values.newPassword
: undefined,
};
const result = await submitter(fields);
@ -51,11 +54,12 @@ export const useSettingsForm = (
const validationErrors = getValidationErrors(error);
if (validationErrors) {
console.log(validationErrors);
setErrors(validationErrors);
}
}
},
[submitter]
[submitter],
);
return useFormik({
@ -71,10 +75,10 @@ export const SettingsProvider: FC<PropsWithChildren<{}>> = ({ children }) => {
const formik = useSettingsForm(
{ ...user, password: '', newPassword: '' },
save
save,
);
return <FormikProvider value={formik}>{children}</FormikProvider>
}
return <FormikProvider value={formik}>{children}</FormikProvider>;
};
export const useSettings = () => useFormikContext<ProfileFormData>();

View file

@ -9,12 +9,12 @@ import {
} from 'react';
import { useRouter } from 'next/router';
import { has, omit } from 'ramda';
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
import { SidebarName } from '~/constants/sidebar';
import { sidebarComponents } from '~/constants/sidebar/components';
import { SidebarComponent, SidebarProps } from '~/types/sidebar';
import { has, omit } from '~/utils/ramda';
type ContextValue = typeof SidebarContext extends Context<infer U> ? U : never;

View file

@ -7,9 +7,8 @@ import React, {
useState,
} from 'react';
import { keys } from 'ramda';
import { Theme } from '~/constants/themes';
import { keys } from '~/utils/ramda';
interface ProvidersProps {}

View file

@ -1,12 +1,13 @@
import React from 'react';
import { CSSProperties } from 'react';
import { Toaster } from 'react-hot-toast';
const containerStyle = {
const containerStyle: CSSProperties = {
top: 10,
left: 10,
bottom: 10,
right: 10,
textAlign: 'center',
};
export const ToastProvider = () => <Toaster containerStyle={containerStyle} />;

File diff suppressed because one or more lines are too long

222
yarn.lock
View file

@ -116,79 +116,89 @@
"@types/yargs" "^15.0.0"
chalk "^4.0.0"
"@next/bundle-analyzer@^12.0.8":
version "12.0.9"
resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-12.0.9.tgz#6fbfbb39b5306da58d9f3f70cc1fb8f85111d959"
integrity sha512-IXr1gUqnp48PEP6ej7mJzYJbU6P6ricdUVO6pkkzXU3xjzhDGTTz/CNMEC9+Iga8IynbIKg/c4vFy8ragY+eYA==
"@next/bundle-analyzer@^12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-12.3.0.tgz#0a4ef7462c5d18fecc119bf681fdc1dc394318ed"
integrity sha512-hzRLHIrtwOiGEku9rmG7qZk+OQhnqQOL+ycl2XrjBaztBN/xaqnjoG4+HEf9L7ELN943BR+K/ZlaF2OEgbGm+Q==
dependencies:
webpack-bundle-analyzer "4.3.0"
"@next/env@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.1.0.tgz#73713399399b34aa5a01771fb73272b55b22c314"
integrity sha512-nrIgY6t17FQ9xxwH3jj0a6EOiQ/WDHUos35Hghtr+SWN/ntHIQ7UpuvSi0vaLzZVHQWaDupKI+liO5vANcDeTQ==
"@next/env@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.0.tgz#85f971fdc668cc312342761057c59cb8ab1abadf"
integrity sha512-PTJpjAFVbzBQ9xXpzMTroShvD5YDIIy46jQ7d4LrWpY+/5a8H90Tm8hE3Hvkc5RBRspVo7kvEOnqQms0A+2Q6w==
"@next/eslint-plugin-next@^12.0.8":
version "12.0.9"
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.0.9.tgz#86edc490141fe3495765224d6cc25be158881802"
integrity sha512-Pi7eV7Omt4wkY1UI9Kvv3KQLvAkmWKb5EHePCE1fyR+LeYmKCNYtjVst3QTDkNeULbpPwnDqIISWu+x2y5G9EA==
"@next/eslint-plugin-next@^12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.3.0.tgz#302c1f03618d5001ce92ea6826c329268759128e"
integrity sha512-jVdq1qYTNDjUtulnE8/hkPv0pHILV4jMg5La99iaY/FFm20WxVnsAZtbNnMvlPbf8dc010oO304SX9yXbg5PAw==
dependencies:
glob "7.1.7"
"@next/swc-android-arm64@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.0.tgz#865ba3a9afc204ff2bdeea49dd64d58705007a39"
integrity sha512-/280MLdZe0W03stA69iL+v6I+J1ascrQ6FrXBlXGCsGzrfMaGr7fskMa0T5AhQIVQD4nA/46QQWxG//DYuFBcA==
"@next/swc-android-arm-eabi@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.0.tgz#9a934904643591cb6f66eb09803a92d2b10ada13"
integrity sha512-/PuirPnAKsYBw93w/7Q9hqy+KGOU9mjYprZ/faxMUJh/dc6v3rYLxkZKNG9nFPIW4QKNTCnhP40xF9hLnxO+xg==
"@next/swc-darwin-arm64@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.0.tgz#08e8b411b8accd095009ed12efbc2f1d4d547135"
integrity sha512-R8vcXE2/iONJ1Unf5Ptqjk6LRW3bggH+8drNkkzH4FLEQkHtELhvcmJwkXcuipyQCsIakldAXhRbZmm3YN1vXg==
"@next/swc-android-arm64@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.3.0.tgz#c1e3e24d0625efe88f45a2135c8f5c4dff594749"
integrity sha512-OaI+FhAM6P9B6Ybwbn0Zl8YwWido0lLwhDBi9WiYCh4RQmIXAyVIoIJPHo4fP05+mXaJ/k1trvDvuURvHOq2qw==
"@next/swc-darwin-x64@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.0.tgz#fcd684497a76e8feaca88db3c394480ff0b007cd"
integrity sha512-ieAz0/J0PhmbZBB8+EA/JGdhRHBogF8BWaeqR7hwveb6SYEIJaDNQy0I+ZN8gF8hLj63bEDxJAs/cEhdnTq+ug==
"@next/swc-darwin-arm64@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.0.tgz#37a9f971b9ad620184af69f38243a36757126fb9"
integrity sha512-9s4d3Mhii+WFce8o8Jok7WC3Bawkr9wEUU++SJRptjU1L5tsfYJMrSYCACHLhZujziNDLyExe4Hwwsccps1sfg==
"@next/swc-linux-arm-gnueabihf@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.0.tgz#9ec6380a27938a5799aaa6035c205b3c478468a7"
integrity sha512-njUd9hpl6o6A5d08dC0cKAgXKCzm5fFtgGe6i0eko8IAdtAPbtHxtpre3VeSxdZvuGFh+hb0REySQP9T1ttkog==
"@next/swc-darwin-x64@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.0.tgz#fb017f1066c8cf2b8da49ef3588c8731d8bf1bf3"
integrity sha512-2scC4MqUTwGwok+wpVxP+zWp7WcCAVOtutki2E1n99rBOTnUOX6qXkgxSy083yBN6GqwuC/dzHeN7hIKjavfRA==
"@next/swc-linux-arm64-gnu@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.0.tgz#7f4196dff1049cea479607c75b81033ae2dbd093"
integrity sha512-OqangJLkRxVxMhDtcb7Qn1xjzFA3s50EIxY7mljbSCLybU+sByPaWAHY4px97ieOlr2y4S0xdPKkQ3BCAwyo6Q==
"@next/swc-freebsd-x64@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.0.tgz#e7955b016f41e0f95088e3459ff4197027871fbf"
integrity sha512-xAlruUREij/bFa+qsE1tmsP28t7vz02N4ZDHt2lh3uJUniE0Ne9idyIDLc1Ed0IF2RjfgOp4ZVunuS3OM0sngw==
"@next/swc-linux-arm64-musl@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.0.tgz#b445f767569cdc2dddee785ca495e1a88c025566"
integrity sha512-hB8cLSt4GdmOpcwRe2UzI5UWn6HHO/vLkr5OTuNvCJ5xGDwpPXelVkYW/0+C3g5axbDW2Tym4S+MQCkkH9QfWA==
"@next/swc-linux-arm-gnueabihf@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.0.tgz#d2233267bffaa24378245b328f2f8a01a37eab29"
integrity sha512-jin2S4VT/cugc2dSZEUIabhYDJNgrUh7fufbdsaAezgcQzqfdfJqfxl4E9GuafzB4cbRPTaqA0V5uqbp0IyGkQ==
"@next/swc-linux-x64-gnu@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.0.tgz#67610e9be4fbc987de7535f1bcb17e45fe12f90e"
integrity sha512-OKO4R/digvrVuweSw/uBM4nSdyzsBV5EwkUeeG4KVpkIZEe64ZwRpnFB65bC6hGwxIBnTv5NMSnJ+0K/WmG78A==
"@next/swc-linux-arm64-gnu@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.0.tgz#149a0cb877352ab63e81cf1dd53b37f382929d2a"
integrity sha512-RqJHDKe0WImeUrdR0kayTkRWgp4vD/MS7g0r6Xuf8+ellOFH7JAAJffDW3ayuVZeMYOa7RvgNFcOoWnrTUl9Nw==
"@next/swc-linux-x64-musl@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.0.tgz#ea19a23db08a9f2e34ac30401f774cf7d1669d31"
integrity sha512-JohhgAHZvOD3rQY7tlp7NlmvtvYHBYgY0x5ZCecUT6eCCcl9lv6iV3nfu82ErkxNk1H893fqH0FUpznZ/H3pSw==
"@next/swc-linux-arm64-musl@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.0.tgz#73ec7f121f56fd7cf99cf2b00cf41f62c4560e90"
integrity sha512-nvNWoUieMjvDjpYJ/4SQe9lQs2xMj6ZRs8N+bmTrVu9leY2Fg3WD6W9p/1uU9hGO8u+OdF13wc4iRShu/WYIHg==
"@next/swc-win32-arm64-msvc@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.0.tgz#eadf054fc412085659b98e145435bbba200b5283"
integrity sha512-T/3gIE6QEfKIJ4dmJk75v9hhNiYZhQYAoYm4iVo1TgcsuaKLFa+zMPh4056AHiG6n9tn2UQ1CFE8EoybEsqsSw==
"@next/swc-linux-x64-gnu@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.0.tgz#6812e52ef21bfd091810f271dd61da11d82b66b9"
integrity sha512-4ajhIuVU9PeQCMMhdDgZTLrHmjbOUFuIyg6J19hZqwEwDTSqQyrSLkbJs2Nd7IRiM6Ul/XyrtEFCpk4k+xD2+w==
"@next/swc-win32-ia32-msvc@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.0.tgz#68faeae10c89f698bf9d28759172b74c9c21bda1"
integrity sha512-iwnKgHJdqhIW19H9PRPM9j55V6RdcOo6rX+5imx832BCWzkDbyomWnlzBfr6ByUYfhohb8QuH4hSGEikpPqI0Q==
"@next/swc-linux-x64-musl@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.0.tgz#c9e7ffb6d44da330961c1ce651c5b03a1becfe22"
integrity sha512-U092RBYbaGxoMAwpauePJEu2PuZSEoUCGJBvsptQr2/2XIMwAJDYM4c/M5NfYEsBr+yjvsYNsOpYfeQ88D82Yg==
"@next/swc-win32-x64-msvc@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.0.tgz#d27e7e76c87a460a4da99c5bfdb1618dcd6cd064"
integrity sha512-aBvcbMwuanDH4EMrL2TthNJy+4nP59Bimn8egqv6GHMVj0a44cU6Au4PjOhLNqEh9l+IpRGBqMTzec94UdC5xg==
"@next/swc-win32-arm64-msvc@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.0.tgz#e0d9d26297f52b0d3b3c2f5138ddcce30601bc98"
integrity sha512-pzSzaxjDEJe67bUok9Nxf9rykbJfHXW0owICFsPBsqHyc+cr8vpF7g9e2APTCddtVhvjkga9ILoZJ9NxWS7Yiw==
"@next/swc-win32-ia32-msvc@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.0.tgz#37daeac1acc68537b8e76cd81fde96dce11f78b4"
integrity sha512-MQGUpMbYhQmTZ06a9e0hPQJnxFMwETo2WtyAotY3GEzbNCQVbCGhsvqEKcl+ZEHgShlHXUWvSffq1ZscY6gK7A==
"@next/swc-win32-x64-msvc@12.3.0":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.0.tgz#c1b983316307f8f55fee491942b5d244bd2036e2"
integrity sha512-C/nw6OgQpEULWqs+wgMHXGvlJLguPRFFGqR2TAqWBerQ8J+Sg3z1ZTqwelkSi4FoqStGuZ2UdFHIDN1ySmR1xA==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@ -221,6 +231,13 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9"
integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==
"@swc/helpers@0.4.11":
version "0.4.11"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.11.tgz#db23a376761b3d31c26502122f349a21b592c8de"
integrity sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==
dependencies:
tslib "^2.4.0"
"@testing-library/dom@^7.28.1":
version "7.31.2"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.31.2.tgz#df361db38f5212b88555068ab8119f5d841a8c4a"
@ -741,10 +758,10 @@ callsites@^3.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
caniuse-lite@^1.0.30001283:
version "1.0.30001367"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001367.tgz"
integrity sha512-XDgbeOHfifWV3GEES2B8rtsrADx4Jf+juKX2SICJcaUhjYBO3bR96kvEIHa15VU6ohtOhBZuPGGYGbXMRn0NCw==
caniuse-lite@^1.0.30001332:
version "1.0.30001399"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001399.tgz#1bf994ca375d7f33f8d01ce03b7d5139e8587873"
integrity sha512-4vQ90tMKS+FkvuVWS5/QY1+d805ODxZiKFzsU8o/RsVJz49ZSRR8EjykLJbqhzdPgadbX6wB538wOzle3JniRA==
chalk@^2.0.0:
version "2.4.2"
@ -2022,10 +2039,10 @@ nanoclone@^0.2.1:
resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4"
integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==
nanoid@^3.1.30:
version "3.2.0"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c"
integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==
nanoid@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
natural-compare@^1.4.0:
version "1.4.0"
@ -2040,28 +2057,31 @@ next-transpile-modules@^9.0.0:
enhanced-resolve "^5.7.0"
escalade "^3.1.1"
next@^12.1.0:
version "12.1.0"
resolved "https://registry.yarnpkg.com/next/-/next-12.1.0.tgz#c33d753b644be92fc58e06e5a214f143da61dd5d"
integrity sha512-s885kWvnIlxsUFHq9UGyIyLiuD0G3BUC/xrH0CEnH5lHEWkwQcHOORgbDF0hbrW9vr/7am4ETfX4A7M6DjrE7Q==
next@^12.3.0:
version "12.3.0"
resolved "https://registry.yarnpkg.com/next/-/next-12.3.0.tgz#0e4c1ed0092544c7e8f4c998ca57cf6529e286cb"
integrity sha512-GpzI6me9V1+XYtfK0Ae9WD0mKqHyzQlGq1xH1rzNIYMASo4Tkl4rTe9jSqtBpXFhOS33KohXs9ZY38Akkhdciw==
dependencies:
"@next/env" "12.1.0"
caniuse-lite "^1.0.30001283"
postcss "8.4.5"
styled-jsx "5.0.0"
use-subscription "1.5.1"
"@next/env" "12.3.0"
"@swc/helpers" "0.4.11"
caniuse-lite "^1.0.30001332"
postcss "8.4.14"
styled-jsx "5.0.6"
use-sync-external-store "1.2.0"
optionalDependencies:
"@next/swc-android-arm64" "12.1.0"
"@next/swc-darwin-arm64" "12.1.0"
"@next/swc-darwin-x64" "12.1.0"
"@next/swc-linux-arm-gnueabihf" "12.1.0"
"@next/swc-linux-arm64-gnu" "12.1.0"
"@next/swc-linux-arm64-musl" "12.1.0"
"@next/swc-linux-x64-gnu" "12.1.0"
"@next/swc-linux-x64-musl" "12.1.0"
"@next/swc-win32-arm64-msvc" "12.1.0"
"@next/swc-win32-ia32-msvc" "12.1.0"
"@next/swc-win32-x64-msvc" "12.1.0"
"@next/swc-android-arm-eabi" "12.3.0"
"@next/swc-android-arm64" "12.3.0"
"@next/swc-darwin-arm64" "12.3.0"
"@next/swc-darwin-x64" "12.3.0"
"@next/swc-freebsd-x64" "12.3.0"
"@next/swc-linux-arm-gnueabihf" "12.3.0"
"@next/swc-linux-arm64-gnu" "12.3.0"
"@next/swc-linux-arm64-musl" "12.3.0"
"@next/swc-linux-x64-gnu" "12.3.0"
"@next/swc-linux-x64-musl" "12.3.0"
"@next/swc-win32-arm64-msvc" "12.3.0"
"@next/swc-win32-ia32-msvc" "12.3.0"
"@next/swc-win32-x64-msvc" "12.3.0"
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
@ -2246,14 +2266,14 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
postcss@8.4.5:
version "8.4.5"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95"
integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==
postcss@8.4.14:
version "8.4.14"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
dependencies:
nanoid "^3.1.30"
nanoid "^3.3.4"
picocolors "^1.0.0"
source-map-js "^1.0.1"
source-map-js "^1.0.2"
prelude-ls@^1.2.1:
version "1.2.1"
@ -2643,7 +2663,7 @@ slice-ansi@^5.0.0:
ansi-styles "^6.0.0"
is-fullwidth-code-point "^4.0.0"
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1:
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
@ -2760,10 +2780,10 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
styled-jsx@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.0.tgz#816b4b92e07b1786c6b7111821750e0ba4d26e77"
integrity sha512-qUqsWoBquEdERe10EW8vLp3jT25s/ssG1/qX5gZ4wu15OZpmSMFI2v+fWlRhLfykA5rFtlJ1ME8A8pm/peV4WA==
styled-jsx@5.0.6:
version "5.0.6"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.6.tgz#fa684790a9cc3badded14badea163418fe568f77"
integrity sha512-xOeROtkK5MGMDimBQ3J6iPId8q0t/BDoG5XN6oKkZClVz9ISF/hihN8OCn2LggMU6N32aXnrXBdn3auSqNS9fA==
supports-color@^5.3.0:
version "5.5.0"
@ -2887,7 +2907,7 @@ tslib@^1.10.0, tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.0:
tslib@^2.0.0, tslib@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
@ -2943,12 +2963,10 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
use-subscription@1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1"
integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA==
dependencies:
object-assign "^4.1.1"
use-sync-external-store@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
uuid4@^1.1.4:
version "1.1.4"