mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
Добавили заметки в сайдбар (#126)
* added notes sidebar * added note dropping and editing * added sidebar navigation * handling sidebarchanges over time * using router back for closing sidebar * fixed tripping inside single sidebar * added superpowers toggle to sidebar * user button opens sidebar now * added profile cover for profile sidebar * removed profile sidebar completely * ran prettier over project * added note not found error literal
This commit is contained in:
parent
fe3db608d6
commit
5d34090238
72 changed files with 1241 additions and 664 deletions
22
src/containers/auth/SuperPowersToggle/index.tsx
Normal file
22
src/containers/auth/SuperPowersToggle/index.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { BorisSuperpowers } from '~/components/boris/BorisSuperpowers';
|
||||
import { useAuth } from '~/hooks/auth/useAuth';
|
||||
import { useSuperPowers } from '~/hooks/auth/useSuperPowers';
|
||||
|
||||
interface SuperPowersToggleProps {}
|
||||
|
||||
const SuperPowersToggle: FC<SuperPowersToggleProps> = observer(() => {
|
||||
const { isUser } = useAuth();
|
||||
const { isTester, setIsTester } = useSuperPowers();
|
||||
|
||||
if (!isUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <BorisSuperpowers active={isTester} onChange={setIsTester} />;
|
||||
});
|
||||
|
||||
export { SuperPowersToggle };
|
30
src/containers/boris/BorisSidebar/index.tsx
Normal file
30
src/containers/boris/BorisSidebar/index.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
import { BorisContacts } from '~/components/boris/BorisContacts';
|
||||
import { BorisStats } from '~/components/boris/BorisStats';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { SuperPowersToggle } from '~/containers/auth/SuperPowersToggle';
|
||||
import styles from '~/layouts/BorisLayout/styles.module.scss';
|
||||
import { BorisUsageStats } from '~/types/boris';
|
||||
|
||||
interface Props {
|
||||
isUser: boolean;
|
||||
stats: BorisUsageStats;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const BorisSidebar: FC<Props> = ({ isUser, stats, isLoading }) => (
|
||||
<Group className={styles.stats__container}>
|
||||
<div className={styles.super_powers}>
|
||||
<SuperPowersToggle />
|
||||
</div>
|
||||
|
||||
<BorisContacts />
|
||||
|
||||
<div className={styles.stats__wrap}>
|
||||
<BorisStats stats={stats} isLoading={isLoading} />
|
||||
</div>
|
||||
</Group>
|
||||
);
|
||||
|
||||
export { BorisSidebar };
|
|
@ -1,39 +1,18 @@
|
|||
import { FC, useCallback } from 'react';
|
||||
|
||||
import { useRouter } from 'next/router';
|
||||
import { FC } from 'react';
|
||||
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { SidebarName } from '~/constants/sidebar';
|
||||
import { URLS } from '~/constants/urls';
|
||||
import { useSidebar } from '~/utils/providers/SidebarProvider';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
import { Markdown } from '~/components/containers/Markdown';
|
||||
|
||||
export interface BorisSuperpowersProps {}
|
||||
|
||||
const BorisSuperpowers: FC<BorisSuperpowersProps> = () => {
|
||||
const { open } = useSidebar();
|
||||
const openProfileSidebar = useCallback(() => {
|
||||
open(SidebarName.Settings);
|
||||
}, [open]);
|
||||
const { push } = useRouter();
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<h2>Штучи, находящиеся в разработке</h2>
|
||||
<h2>Штучки, находящиеся в разработке</h2>
|
||||
|
||||
<div className={styles.grid}>
|
||||
<Button size="mini" onClick={() => openProfileSidebar()}>
|
||||
Открыть
|
||||
</Button>
|
||||
<div className={styles.label}>Профиль в сайдбаре</div>
|
||||
|
||||
<Button size="mini" onClick={() => push(URLS.SETTINGS.BASE)}>
|
||||
Открыть
|
||||
</Button>
|
||||
<div className={styles.label}>Профиль на отдельной странице</div>
|
||||
</div>
|
||||
<Markdown>
|
||||
{`> На данный момент в разработке нет вещей, которые можно показать.\n\n// Приходите завтра`}
|
||||
</Markdown>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
import React, { FC, useCallback } from "react";
|
||||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import { LoginDialogButtons } from "~/components/auth/login/LoginDialogButtons";
|
||||
import { LoginScene } from "~/components/auth/login/LoginScene";
|
||||
import { Group } from "~/components/containers/Group";
|
||||
import { Padder } from "~/components/containers/Padder";
|
||||
import { BetterScrollDialog } from "~/components/dialogs/BetterScrollDialog";
|
||||
import { DialogTitle } from "~/components/dialogs/DialogTitle";
|
||||
import { Button } from "~/components/input/Button";
|
||||
import { InputText } from "~/components/input/InputText";
|
||||
import { Dialog } from "~/constants/modal";
|
||||
import { useCloseOnEscape } from "~/hooks";
|
||||
import { useAuth } from "~/hooks/auth/useAuth";
|
||||
import { useLoginForm } from "~/hooks/auth/useLoginForm";
|
||||
import { useOAuth } from "~/hooks/auth/useOAuth";
|
||||
import { useShowModal } from "~/hooks/modal/useShowModal";
|
||||
import { DialogComponentProps } from "~/types/modal";
|
||||
import { LoginDialogButtons } from '~/components/auth/login/LoginDialogButtons';
|
||||
import { LoginScene } from '~/components/auth/login/LoginScene';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Padder } from '~/components/containers/Padder';
|
||||
import { BetterScrollDialog } from '~/components/dialogs/BetterScrollDialog';
|
||||
import { DialogTitle } from '~/components/dialogs/DialogTitle';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { InputText } from '~/components/input/InputText';
|
||||
import { Dialog } from '~/constants/modal';
|
||||
import { useCloseOnEscape } from '~/hooks';
|
||||
import { useAuth } from '~/hooks/auth/useAuth';
|
||||
import { useLoginForm } from '~/hooks/auth/useLoginForm';
|
||||
import { useOAuth } from '~/hooks/auth/useOAuth';
|
||||
import { useShowModal } from '~/hooks/modal/useShowModal';
|
||||
import { DialogComponentProps } from '~/types/modal';
|
||||
|
||||
import styles from "./styles.module.scss";
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
type LoginDialogProps = DialogComponentProps & {};
|
||||
|
||||
|
@ -55,7 +55,7 @@ const LoginDialog: FC<LoginDialogProps> = ({ onRequestClose }) => {
|
|||
|
||||
<InputText
|
||||
title="Логин"
|
||||
handler={handleChange("username")}
|
||||
handler={handleChange('username')}
|
||||
value={values.username}
|
||||
error={errors.username}
|
||||
autoFocus
|
||||
|
@ -63,7 +63,7 @@ const LoginDialog: FC<LoginDialogProps> = ({ onRequestClose }) => {
|
|||
|
||||
<InputText
|
||||
title="Пароль"
|
||||
handler={handleChange("password")}
|
||||
handler={handleChange('password')}
|
||||
value={values.password}
|
||||
error={errors.password}
|
||||
type="password"
|
||||
|
|
|
@ -22,7 +22,7 @@ export interface PhotoSwipeProps extends DialogComponentProps {
|
|||
const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
||||
let ref = useRef<HTMLDivElement>(null);
|
||||
const { hideModal } = useModal();
|
||||
const { isMobile } = useWindowSize();
|
||||
const { isTablet } = useWindowSize();
|
||||
|
||||
useEffect(() => {
|
||||
new Promise(async resolve => {
|
||||
|
@ -34,7 +34,10 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
|||
|
||||
img.onload = () => {
|
||||
resolveImage({
|
||||
src: getURL(image, isMobile ? ImagePresets[900] : ImagePresets[1600]),
|
||||
src: getURL(
|
||||
image,
|
||||
isTablet ? ImagePresets[900] : ImagePresets[1600],
|
||||
),
|
||||
h: img.naturalHeight,
|
||||
w: img.naturalWidth,
|
||||
});
|
||||
|
@ -45,8 +48,8 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
|||
};
|
||||
|
||||
img.src = getURL(image, ImagePresets[1600]);
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
resolve(images);
|
||||
|
@ -61,10 +64,16 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
|||
ps.listen('destroy', hideModal);
|
||||
ps.listen('close', hideModal);
|
||||
});
|
||||
}, [hideModal, items, index, isMobile]);
|
||||
}, [hideModal, items, index, isTablet]);
|
||||
|
||||
return (
|
||||
<div className="pswp" tabIndex={-1} role="dialog" aria-hidden="true" ref={ref}>
|
||||
<div
|
||||
className="pswp"
|
||||
tabIndex={-1}
|
||||
role="dialog"
|
||||
aria-hidden="true"
|
||||
ref={ref}
|
||||
>
|
||||
<div className={classNames('pswp__bg', styles.bg)} />
|
||||
<div className={classNames('pswp__scroll-wrap', styles.wrap)}>
|
||||
<div className="pswp__container">
|
||||
|
@ -76,7 +85,10 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
|||
<div className="pswp__ui pswp__ui--hidden">
|
||||
<div className={classNames('pswp__top-bar', styles.bar)}>
|
||||
<div className="pswp__counter" />
|
||||
<button className="pswp__button pswp__button--close" title="Close (Esc)" />
|
||||
<button
|
||||
className="pswp__button pswp__button--close"
|
||||
title="Close (Esc)"
|
||||
/>
|
||||
|
||||
<div className="pswp__preloader">
|
||||
<div className="pswp__preloader__icn">
|
||||
|
@ -96,7 +108,10 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
|||
title="Previous (arrow left)"
|
||||
/>
|
||||
|
||||
<button className="pswp__button pswp__button--arrow--right" title="Next (arrow right)" />
|
||||
<button
|
||||
className="pswp__button pswp__button--arrow--right"
|
||||
title="Next (arrow right)"
|
||||
/>
|
||||
|
||||
<div className="pswp__caption">
|
||||
<div className="pswp__caption__center" />
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
|
||||
import { Tabs } from '~/components/dialogs/Tabs';
|
||||
import { ProfileDescription } from '~/components/profile/ProfileDescription';
|
||||
import { ProfileSettings } from '~/components/profile/ProfileSettings';
|
||||
import { ProfileAccounts } from '~/containers/profile/ProfileAccounts';
|
||||
import { ProfileInfo } from '~/containers/profile/ProfileInfo';
|
||||
import { ProfileMessages } from '~/containers/profile/ProfileMessages';
|
||||
import { useUser } from '~/hooks/auth/useUser';
|
||||
import { useGetProfile } from '~/hooks/profile/useGetProfile';
|
||||
import { DialogComponentProps } from '~/types/modal';
|
||||
import { ProfileProvider } from '~/utils/providers/ProfileProvider';
|
||||
|
||||
import { BetterScrollDialog } from '../../../components/dialogs/BetterScrollDialog';
|
||||
|
||||
export interface ProfileDialogProps extends DialogComponentProps {
|
||||
username: string;
|
||||
}
|
||||
|
||||
const ProfileDialog: FC<ProfileDialogProps> = ({ username, onRequestClose }) => {
|
||||
const { isLoading, profile } = useGetProfile(username);
|
||||
const {
|
||||
user: { id },
|
||||
} = useUser();
|
||||
|
||||
return (
|
||||
<ProfileProvider username={username}>
|
||||
<Tabs>
|
||||
<BetterScrollDialog
|
||||
header={<ProfileInfo isOwn={profile.id === id} isLoading={isLoading} />}
|
||||
backdrop={<CoverBackdrop cover={profile.cover} />}
|
||||
onClose={onRequestClose}
|
||||
>
|
||||
<Tabs.Content>
|
||||
<ProfileDescription />
|
||||
<ProfileMessages />
|
||||
<ProfileSettings />
|
||||
<ProfileAccounts />
|
||||
</Tabs.Content>
|
||||
</BetterScrollDialog>
|
||||
</Tabs>
|
||||
</ProfileProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export { ProfileDialog };
|
|
@ -1,5 +0,0 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.messages {
|
||||
padding: $gap;
|
||||
}
|
|
@ -11,6 +11,7 @@ import { Button } from '~/components/input/Button';
|
|||
import { Logo } from '~/components/main/Logo';
|
||||
import { UserButton } from '~/components/main/UserButton';
|
||||
import { Dialog } from '~/constants/modal';
|
||||
import { SidebarName } from '~/constants/sidebar';
|
||||
import { URLS } from '~/constants/urls';
|
||||
import { useAuth } from '~/hooks/auth/useAuth';
|
||||
import { useScrollTop } from '~/hooks/dom/useScrollTop';
|
||||
|
@ -18,6 +19,7 @@ import { useFlow } from '~/hooks/flow/useFlow';
|
|||
import { useGetLabStats } from '~/hooks/lab/useGetLabStats';
|
||||
import { useModal } from '~/hooks/modal/useModal';
|
||||
import { useUpdates } from '~/hooks/updates/useUpdates';
|
||||
import { useSidebar } from '~/utils/providers/SidebarProvider';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
|
@ -27,15 +29,15 @@ const Header: FC<HeaderProps> = observer(() => {
|
|||
const labStats = useGetLabStats();
|
||||
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const { logout } = useAuth();
|
||||
const { showModal } = useModal();
|
||||
const { isUser, user } = useAuth();
|
||||
const { updates: flowUpdates } = useFlow();
|
||||
const { borisCommentedAt } = useUpdates();
|
||||
const { open } = useSidebar();
|
||||
|
||||
const openProfile = useCallback(() => {
|
||||
showModal(Dialog.Profile, { username: user.username });
|
||||
}, [user.username, showModal]);
|
||||
const openProfileSidebar = useCallback(() => {
|
||||
open(SidebarName.Settings, {});
|
||||
}, [open]);
|
||||
|
||||
const onLogin = useCallback(() => showModal(Dialog.Login, {}), [showModal]);
|
||||
|
||||
|
@ -47,10 +49,12 @@ const Header: FC<HeaderProps> = observer(() => {
|
|||
borisCommentedAt &&
|
||||
(!user.last_seen_boris ||
|
||||
isBefore(new Date(user.last_seen_boris), new Date(borisCommentedAt))),
|
||||
[borisCommentedAt, isUser, user.last_seen_boris]
|
||||
[borisCommentedAt, isUser, user.last_seen_boris],
|
||||
);
|
||||
|
||||
const hasLabUpdates = useMemo(() => labStats.updates.length > 0, [labStats.updates]);
|
||||
const hasLabUpdates = useMemo(() => labStats.updates.length > 0, [
|
||||
labStats.updates,
|
||||
]);
|
||||
const hasFlowUpdates = useMemo(() => flowUpdates.length > 0, [flowUpdates]);
|
||||
|
||||
// Needed for SSR
|
||||
|
@ -59,7 +63,9 @@ const Header: FC<HeaderProps> = observer(() => {
|
|||
}, [top]);
|
||||
|
||||
return (
|
||||
<header className={classNames(styles.wrap, { [styles.is_scrolled]: isScrolled })}>
|
||||
<header
|
||||
className={classNames(styles.wrap, { [styles.is_scrolled]: isScrolled })}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.logo_wrapper}>
|
||||
<Logo />
|
||||
|
@ -98,10 +104,21 @@ const Header: FC<HeaderProps> = observer(() => {
|
|||
</Authorized>
|
||||
</nav>
|
||||
|
||||
{isUser && <UserButton user={user} onLogout={logout} authOpenProfile={openProfile} />}
|
||||
{isUser && (
|
||||
<UserButton
|
||||
username={user.username}
|
||||
photo={user.photo}
|
||||
onClick={openProfileSidebar}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isUser && (
|
||||
<Button className={styles.user_button} onClick={onLogin} round color="secondary">
|
||||
<Button
|
||||
className={styles.user_button}
|
||||
onClick={onLogin}
|
||||
round
|
||||
color="secondary"
|
||||
>
|
||||
ВДОХ
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
@ -26,17 +26,19 @@ const ProfilePageLeft: FC<IProps> = ({ username, profile, isLoading }) => {
|
|||
/>
|
||||
|
||||
<div className={styles.region}>
|
||||
<div className={styles.name}>{isLoading ? <Placeholder /> : profile?.fullname}</div>`
|
||||
<div className={styles.name}>
|
||||
{isLoading ? <Placeholder /> : profile?.fullname}
|
||||
</div>
|
||||
`
|
||||
<div className={styles.username}>
|
||||
{isLoading ? <Placeholder /> : `~${profile?.username}`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!!profile?.description && (
|
||||
<Markdown
|
||||
className={styles.description}
|
||||
dangerouslySetInnerHTML={{ __html: formatText(profile.description) }}
|
||||
/>
|
||||
<Markdown className={styles.description}>
|
||||
{formatText(profile.description)}
|
||||
</Markdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -15,6 +15,7 @@ import { useAuth } from '~/hooks/auth/useAuth';
|
|||
import markdown from '~/styles/common/markdown.module.scss';
|
||||
|
||||
import { ProfileSidebarLogoutButton } from '../ProfileSidebarLogoutButton';
|
||||
import { ProfileToggles } from '../ProfileToggles';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
|
@ -40,9 +41,19 @@ const ProfileSidebarMenu: VFC<ProfileSidebarMenuProps> = ({ onClose }) => {
|
|||
<Filler className={classNames(markdown.wrapper, styles.text)}>
|
||||
<Group>
|
||||
<VerticalMenu className={styles.menu}>
|
||||
<VerticalMenu.Item onClick={() => setActiveTab(0)}>Настройки</VerticalMenu.Item>
|
||||
<VerticalMenu.Item onClick={() => setActiveTab(0)}>
|
||||
Настройки
|
||||
</VerticalMenu.Item>
|
||||
|
||||
<VerticalMenu.Item onClick={() => setActiveTab(1)}>
|
||||
Заметки
|
||||
</VerticalMenu.Item>
|
||||
</VerticalMenu>
|
||||
|
||||
<div className={styles.toggles}>
|
||||
<ProfileToggles />
|
||||
</div>
|
||||
|
||||
<div className={styles.stats}>
|
||||
<ProfileStats />
|
||||
</div>
|
||||
|
@ -51,7 +62,7 @@ const ProfileSidebarMenu: VFC<ProfileSidebarMenuProps> = ({ onClose }) => {
|
|||
|
||||
<Group className={styles.buttons} horizontal>
|
||||
<Filler />
|
||||
<ProfileSidebarLogoutButton onLogout={onLogout}/>
|
||||
<ProfileSidebarLogoutButton onLogout={onLogout} />
|
||||
</Group>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -19,3 +19,7 @@
|
|||
.stats {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggles {
|
||||
padding-top: $gap * 2;
|
||||
}
|
||||
|
|
17
src/containers/profile/ProfileToggles/index.tsx
Normal file
17
src/containers/profile/ProfileToggles/index.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Zone } from '~/components/containers/Zone';
|
||||
import { SuperPowersToggle } from '~/containers/auth/SuperPowersToggle';
|
||||
|
||||
interface ProfileTogglesProps {}
|
||||
|
||||
const ProfileToggles: FC<ProfileTogglesProps> = () => (
|
||||
<Zone>
|
||||
<Group>
|
||||
<SuperPowersToggle />
|
||||
</Group>
|
||||
</Zone>
|
||||
);
|
||||
|
||||
export { ProfileToggles };
|
|
@ -1,56 +1,80 @@
|
|||
import React, { useState, VFC } from 'react';
|
||||
import { FC, useCallback, useState, VFC } from 'react';
|
||||
|
||||
import { Card } from '~/components/containers/Card';
|
||||
import { Columns } from '~/components/containers/Columns';
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
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 { InputText } from '~/components/input/InputText';
|
||||
import { Textarea } from '~/components/input/Textarea';
|
||||
import { HorizontalMenu } from '~/components/menu/HorizontalMenu';
|
||||
import { NoteCard } from '~/components/notes/NoteCard';
|
||||
import { useGetNotes } from '~/hooks/notes/useGetNotes';
|
||||
import { NoteCreationForm } from '~/components/notes/NoteCreationForm';
|
||||
import { useConfirmation } from '~/hooks/dom/useConfirmation';
|
||||
import { NoteProvider, useNotesContext } from '~/utils/providers/NoteProvider';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface SettingsNotesProps {}
|
||||
|
||||
const SettingsNotes: VFC<SettingsNotesProps> = () => {
|
||||
const [text, setText] = useState('');
|
||||
const { notes } = useGetNotes('');
|
||||
const List = () => {
|
||||
const { notes, remove, update } = useNotesContext();
|
||||
const confirm = useConfirmation();
|
||||
|
||||
const onRemove = useCallback(
|
||||
async (id: number) => {
|
||||
confirm('Удалить? Это удалит заметку навсегда', () => remove(id));
|
||||
},
|
||||
[remove],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Padder>
|
||||
<Group horizontal>
|
||||
<HorizontalMenu>
|
||||
<HorizontalMenu.Item active>Новые</HorizontalMenu.Item>
|
||||
<HorizontalMenu.Item>Старые</HorizontalMenu.Item>
|
||||
</HorizontalMenu>
|
||||
<>
|
||||
{notes.map(note => (
|
||||
<NoteCard
|
||||
remove={() => onRemove(note.id)}
|
||||
update={(text, callback) => update(note.id, text, callback)}
|
||||
key={note.id}
|
||||
content={note.content}
|
||||
createdAt={note.created_at}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
<Filler />
|
||||
const Form: FC<{ onCancel: () => void }> = ({ onCancel }) => {
|
||||
const { create: submit } = useNotesContext();
|
||||
return <NoteCreationForm onSubmit={submit} onCancel={onCancel} />;
|
||||
};
|
||||
|
||||
<InputText suffix={<Icon icon="search" size={24} />} />
|
||||
</Group>
|
||||
</Padder>
|
||||
const SettingsNotes: VFC<SettingsNotesProps> = () => {
|
||||
const [formIsShown, setFormIsShown] = useState(false);
|
||||
|
||||
<Columns>
|
||||
<Card>
|
||||
<Group>
|
||||
<Textarea handler={setText} value={text} />
|
||||
|
||||
<Group horizontal>
|
||||
return (
|
||||
<NoteProvider>
|
||||
<div className={styles.grid}>
|
||||
<div className={styles.head}>
|
||||
{formIsShown ? (
|
||||
<Form onCancel={() => setFormIsShown(false)} />
|
||||
) : (
|
||||
<Group className={styles.showForm} horizontal>
|
||||
<Filler />
|
||||
<Button size="mini">Добавить</Button>
|
||||
<Button
|
||||
onClick={() => setFormIsShown(true)}
|
||||
size="mini"
|
||||
iconRight="plus"
|
||||
color="secondary"
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.list}>
|
||||
<Group>
|
||||
<Group>
|
||||
<List />
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
|
||||
{notes.map(note => (
|
||||
<NoteCard key={note.id} content={note.content} createdAt={note.created_at} />
|
||||
))}
|
||||
</Columns>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NoteProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
z-index: 4;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.head {
|
||||
@include row_shadow;
|
||||
|
||||
width: 100%;
|
||||
padding: $gap;
|
||||
}
|
||||
|
||||
.list {
|
||||
@include row_shadow;
|
||||
|
||||
overflow-y: auto;
|
||||
flex: 1 1;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
}
|
|
@ -1,17 +1,18 @@
|
|||
import { FC } from "react";
|
||||
import { FC } from 'react';
|
||||
|
||||
import { Superpower } from "~/components/boris/Superpower";
|
||||
import { Filler } from "~/components/containers/Filler";
|
||||
import { Group } from "~/components/containers/Group";
|
||||
import { Zone } from "~/components/containers/Zone";
|
||||
import { InputText } from "~/components/input/InputText";
|
||||
import { Textarea } from "~/components/input/Textarea";
|
||||
import { ERROR_LITERAL } from "~/constants/errors";
|
||||
import { ProfileAccounts } from "~/containers/profile/ProfileAccounts";
|
||||
import { useSettings } from "~/utils/providers/SettingsProvider";
|
||||
import { has } from "~/utils/ramda";
|
||||
import { Superpower } from '~/components/boris/Superpower';
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Zone } from '~/components/containers/Zone';
|
||||
import { InputText } from '~/components/input/InputText';
|
||||
import { Textarea } from '~/components/input/Textarea';
|
||||
import { ERROR_LITERAL } from '~/constants/errors';
|
||||
import { ProfileAccounts } from '~/containers/profile/ProfileAccounts';
|
||||
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
||||
import { useSettings } from '~/utils/providers/SettingsProvider';
|
||||
import { has } from '~/utils/ramda';
|
||||
|
||||
import styles from "./styles.module.scss";
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface UserSettingsViewProps {}
|
||||
|
||||
|
@ -20,10 +21,11 @@ const getError = (error?: string) =>
|
|||
|
||||
const UserSettingsView: FC<UserSettingsViewProps> = () => {
|
||||
const { values, handleChange, errors } = useSettings();
|
||||
const { isPhone } = useWindowSize();
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Group horizontal className={styles.base_info}>
|
||||
<Group horizontal={!isPhone} className={styles.base_info}>
|
||||
<Superpower>
|
||||
<Zone className={styles.avatar} title="Фото">
|
||||
<small>
|
||||
|
@ -33,18 +35,18 @@ const UserSettingsView: FC<UserSettingsViewProps> = () => {
|
|||
</Zone>
|
||||
</Superpower>
|
||||
|
||||
<Zone title="О себе">
|
||||
<Zone title="О себе" className={styles.about}>
|
||||
<Group>
|
||||
<InputText
|
||||
value={values.fullname}
|
||||
handler={handleChange("fullname")}
|
||||
handler={handleChange('fullname')}
|
||||
title="Полное имя"
|
||||
error={getError(errors.fullname)}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
value={values.description}
|
||||
handler={handleChange("description")}
|
||||
handler={handleChange('description')}
|
||||
title="Описание"
|
||||
/>
|
||||
|
||||
|
@ -75,21 +77,21 @@ const UserSettingsView: FC<UserSettingsViewProps> = () => {
|
|||
<Group>
|
||||
<InputText
|
||||
value={values.username}
|
||||
handler={handleChange("username")}
|
||||
handler={handleChange('username')}
|
||||
title="Логин"
|
||||
error={getError(errors.username)}
|
||||
/>
|
||||
|
||||
<InputText
|
||||
value={values.email}
|
||||
handler={handleChange("email")}
|
||||
handler={handleChange('email')}
|
||||
title="E-mail"
|
||||
error={getError(errors.email)}
|
||||
/>
|
||||
|
||||
<InputText
|
||||
value={values.newPassword}
|
||||
handler={handleChange("newPassword")}
|
||||
handler={handleChange('newPassword')}
|
||||
title="Новый пароль"
|
||||
type="password"
|
||||
error={getError(errors.newPassword)}
|
||||
|
@ -97,7 +99,7 @@ const UserSettingsView: FC<UserSettingsViewProps> = () => {
|
|||
|
||||
<InputText
|
||||
value={values.password}
|
||||
handler={handleChange("password")}
|
||||
handler={handleChange('password')}
|
||||
title="Старый пароль"
|
||||
type="password"
|
||||
error={getError(errors.password)}
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
$pad_danger: mix($red, $content_bg, 70%);
|
||||
$pad_usual: mix(white, $content_bg, 10%);
|
||||
|
||||
.about {
|
||||
flex: 4;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
padding: $gap;
|
||||
z-index: 4;
|
||||
|
@ -21,5 +25,5 @@ div.base_info.base_info {
|
|||
}
|
||||
|
||||
.avatar {
|
||||
flex: 0 0 150px;
|
||||
}
|
||||
flex: 1 0 90px;
|
||||
}
|
||||
|
|
|
@ -1,22 +1,72 @@
|
|||
import { VFC } from 'react';
|
||||
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';
|
||||
import { SidebarStack } from '~/components/sidebar/SidebarStack';
|
||||
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';
|
||||
import { SidebarName } from '~/constants/sidebar';
|
||||
import { ProfileSidebarMenu } from '~/containers/profile/ProfileSidebarMenu';
|
||||
import { SidebarWrapper } from '~/containers/sidebars/SidebarWrapper';
|
||||
import { DialogComponentProps } from '~/types/modal';
|
||||
import { useAuth } from '~/hooks/auth/useAuth';
|
||||
import { useUser } from '~/hooks/auth/useUser';
|
||||
import type { SidebarComponentProps } from '~/types/sidebar';
|
||||
|
||||
interface ProfileSidebarProps extends DialogComponentProps {
|
||||
page: string;
|
||||
const tabs = ['profile', 'bookmarks'] as const;
|
||||
type TabName = typeof tabs[number];
|
||||
|
||||
interface ProfileSidebarProps
|
||||
extends SidebarComponentProps<SidebarName.Settings> {
|
||||
page?: TabName;
|
||||
}
|
||||
|
||||
const ProfileSidebar: VFC<ProfileSidebarProps> = ({ onRequestClose }) => {
|
||||
const ProfileSidebar: VFC<ProfileSidebarProps> = ({
|
||||
onRequestClose,
|
||||
page,
|
||||
openSidebar,
|
||||
}) => {
|
||||
const { isUser } = useAuth();
|
||||
const {
|
||||
user: { cover },
|
||||
} = useUser();
|
||||
|
||||
const tab = useMemo(
|
||||
() => (page ? Math.max(tabs.indexOf(page), 0) : undefined),
|
||||
[page],
|
||||
);
|
||||
|
||||
const onTabChange = useCallback(
|
||||
(val: number | undefined) => {
|
||||
openSidebar(SidebarName.Settings, {
|
||||
page: !isNil(val) ? tabs[val] : undefined,
|
||||
});
|
||||
},
|
||||
[open, onRequestClose],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isUser) {
|
||||
onRequestClose();
|
||||
}
|
||||
}, [isUser]);
|
||||
|
||||
if (!isUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarWrapper onClose={onRequestClose}>
|
||||
<SidebarStack>
|
||||
<SidebarStackCard headerFeature="close" title="Профиль" onBackPress={onRequestClose}>
|
||||
<SidebarWrapper
|
||||
onClose={onRequestClose}
|
||||
backdrop={cover && <CoverBackdrop cover={cover} />}
|
||||
>
|
||||
<SidebarStack tab={tab} onTabChange={onTabChange}>
|
||||
<SidebarStackCard
|
||||
headerFeature="close"
|
||||
title="Профиль"
|
||||
onBackPress={onRequestClose}
|
||||
>
|
||||
<ProfileSidebarMenu onClose={onRequestClose} />
|
||||
</SidebarStackCard>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { FC, useEffect, useRef } from 'react';
|
||||
import React, { FC, ReactNode, useEffect, useRef } from 'react';
|
||||
|
||||
import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock';
|
||||
|
||||
|
@ -9,9 +9,15 @@ import styles from './styles.module.scss';
|
|||
interface IProps {
|
||||
onClose?: () => void;
|
||||
closeOnBackdropClick?: boolean;
|
||||
backdrop?: ReactNode;
|
||||
}
|
||||
|
||||
const SidebarWrapper: FC<IProps> = ({ children, onClose, closeOnBackdropClick = true }) => {
|
||||
const SidebarWrapper: FC<IProps> = ({
|
||||
children,
|
||||
onClose,
|
||||
closeOnBackdropClick = true,
|
||||
backdrop,
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useCloseOnEscape(onClose);
|
||||
|
@ -25,7 +31,12 @@ const SidebarWrapper: FC<IProps> = ({ children, onClose, closeOnBackdropClick =
|
|||
|
||||
return (
|
||||
<div className={styles.wrapper} ref={ref}>
|
||||
{closeOnBackdropClick && <div className={styles.backdrop} onClick={onClose} />}
|
||||
{(closeOnBackdropClick || backdrop) && (
|
||||
<div className={styles.backdrop} onClick={onClose}>
|
||||
{backdrop}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import React, { ChangeEvent, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
FC,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { TagAutocomplete } from '~/components/tags/TagAutocomplete';
|
||||
import { TagWrapper } from '~/components/tags/TagWrapper';
|
||||
|
@ -15,7 +23,7 @@ const prepareInput = (input: string): string[] => {
|
|||
title
|
||||
.trim()
|
||||
.substring(0, 64)
|
||||
.toLowerCase()
|
||||
.toLowerCase(),
|
||||
)
|
||||
.filter(el => el.length > 0);
|
||||
};
|
||||
|
@ -49,7 +57,7 @@ const TagInput: FC<IProps> = ({ exclude, onAppend, onClearTag, onSubmit }) => {
|
|||
|
||||
setInput(items[items.length - 1] || '');
|
||||
},
|
||||
[onAppend]
|
||||
[onAppend],
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
|
@ -69,14 +77,13 @@ const TagInput: FC<IProps> = ({ exclude, onAppend, onClearTag, onSubmit }) => {
|
|||
const created = prepareInput(input);
|
||||
|
||||
if (created.length) {
|
||||
console.log('appending?!!')
|
||||
onAppend(created);
|
||||
}
|
||||
|
||||
setInput('');
|
||||
}
|
||||
},
|
||||
[input, setInput, onClearTag, onAppend]
|
||||
[input, setInput, onClearTag, onAppend],
|
||||
);
|
||||
|
||||
const onFocus = useCallback(() => setFocused(true), []);
|
||||
|
@ -99,7 +106,7 @@ const TagInput: FC<IProps> = ({ exclude, onAppend, onClearTag, onSubmit }) => {
|
|||
|
||||
onSubmit([]);
|
||||
},
|
||||
[input, setInput, onSubmit]
|
||||
[input, setInput, onSubmit],
|
||||
);
|
||||
|
||||
const onAutocompleteSelect = useCallback(
|
||||
|
@ -109,13 +116,15 @@ const TagInput: FC<IProps> = ({ exclude, onAppend, onClearTag, onSubmit }) => {
|
|||
if (!val.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
onAppend([val]);
|
||||
},
|
||||
[onAppend, setInput]
|
||||
[onAppend, setInput],
|
||||
);
|
||||
|
||||
const feature = useMemo(() => (input?.substr(0, 1) === '/' ? 'green' : ''), [input]);
|
||||
const feature = useMemo(() => (input?.substr(0, 1) === '/' ? 'green' : ''), [
|
||||
input,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!focused) return;
|
||||
|
@ -126,7 +135,11 @@ const TagInput: FC<IProps> = ({ exclude, onAppend, onClearTag, onSubmit }) => {
|
|||
|
||||
return (
|
||||
<div className={styles.wrap} ref={wrapper}>
|
||||
<TagWrapper title={input || placeholder} hasInput={true} feature={feature}>
|
||||
<TagWrapper
|
||||
title={input || placeholder}
|
||||
hasInput={true}
|
||||
feature={feature}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue