1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 04:46:40 +07:00

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

* 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

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 result = await apiGetSocials();
return result.accounts;
}
);
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[]
) => {
if (pageIndex > 0 && !previousPageData?.length) return null;
return `${API.NODE.COMMENT(nodeId)}?page=${pageIndex}`;
};
const getKey: (nodeId: number) => SWRInfiniteKeyLoader =
(nodeId: number) => (pageIndex, previousPageData: IComment[]) => {
if (pageIndex > 0 && !previousPageData?.length) return null;
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, [
...(recent.after || []),
...(recent.before || []),
...(recent.recent || []),
])
.filter(it => it.id)
.map(it => it.id!.toString());
const recentIDs = uniqBy(
(it) => it.id,
[
...(recent.after || []),
...(recent.before || []),
...(recent.recent || []),
],
)
.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({
id,
take: COMMENTS_DISPLAY,
});
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({
@ -70,11 +74,11 @@ export const SettingsProvider: FC<PropsWithChildren<{}>> = ({ children }) => {
const { save } = usePatchUser();
const formik = useSettingsForm(
{ ...user, password: '', newPassword: '' },
save
{ ...user, password: '', newPassword: '' },
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} />;