mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
added notes sidebar
This commit is contained in:
parent
fe3db608d6
commit
cedf0adcfa
14 changed files with 376 additions and 153 deletions
|
@ -1,8 +1,20 @@
|
||||||
import { ApiGetNotesRequest, ApiGetNotesResponse } from '~/api/notes/types';
|
import {
|
||||||
import { URLS } from '~/constants/urls';
|
ApiGetNotesRequest,
|
||||||
import { api, cleanResult } from '~/utils/api';
|
ApiGetNotesResponse,
|
||||||
|
ApiPostNoteRequest,
|
||||||
|
ApiPostNoteResponse,
|
||||||
|
} from "~/api/notes/types";
|
||||||
|
import { URLS } from "~/constants/urls";
|
||||||
|
import { api, cleanResult } from "~/utils/api";
|
||||||
|
|
||||||
export const apiGetNotes = ({ limit, offset, search }: ApiGetNotesRequest) =>
|
export const apiGetNotes = ({ limit, offset, search }: ApiGetNotesRequest) =>
|
||||||
api
|
api
|
||||||
.get<ApiGetNotesResponse>(URLS.NOTES, { params: { limit, offset, search } })
|
.get<ApiGetNotesResponse>(URLS.NOTES, { params: { limit, offset, search } })
|
||||||
.then(cleanResult);
|
.then(cleanResult);
|
||||||
|
|
||||||
|
export const apiPostNote = ({ text }: ApiPostNoteRequest) =>
|
||||||
|
api
|
||||||
|
.post<ApiPostNoteResponse>(URLS.NOTES, {
|
||||||
|
text,
|
||||||
|
})
|
||||||
|
.then(cleanResult);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Note } from '~/types/notes';
|
import { Note } from "~/types/notes";
|
||||||
|
|
||||||
export interface ApiGetNotesRequest {
|
export interface ApiGetNotesRequest {
|
||||||
limit: number;
|
limit: number;
|
||||||
|
@ -10,3 +10,9 @@ export interface ApiGetNotesResponse {
|
||||||
list: Note[];
|
list: Note[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiPostNoteRequest {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiPostNoteResponse extends Note {}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import React, { VFC } from 'react';
|
import React, { VFC } from "react";
|
||||||
|
|
||||||
import { Card } from '~/components/containers/Card';
|
import { Card } from "~/components/containers/Card";
|
||||||
import { Markdown } from '~/components/containers/Markdown';
|
import { Markdown } from "~/components/containers/Markdown";
|
||||||
import { Padder } from '~/components/containers/Padder';
|
import { Padder } from "~/components/containers/Padder";
|
||||||
import { NoteMenu } from '~/components/notes/NoteMenu';
|
import { NoteMenu } from "~/components/notes/NoteMenu";
|
||||||
import { formatText, getPrettyDate } from '~/utils/dom';
|
import { formatText, getPrettyDate } from "~/utils/dom";
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from "./styles.module.scss";
|
||||||
|
|
||||||
interface NoteCardProps {
|
interface NoteCardProps {
|
||||||
content: string;
|
content: string;
|
||||||
|
@ -17,7 +17,10 @@ const NoteCard: VFC<NoteCardProps> = ({ content, createdAt }) => (
|
||||||
<Card className={styles.note}>
|
<Card className={styles.note}>
|
||||||
<Padder>
|
<Padder>
|
||||||
<NoteMenu onEdit={console.log} onDelete={console.log} />
|
<NoteMenu onEdit={console.log} onDelete={console.log} />
|
||||||
<Markdown className={styles.wrap} dangerouslySetInnerHTML={{ __html: formatText(content) }} />
|
<Markdown
|
||||||
|
className={styles.wrap}
|
||||||
|
dangerouslySetInnerHTML={{ __html: formatText(content) }}
|
||||||
|
/>
|
||||||
</Padder>
|
</Padder>
|
||||||
|
|
||||||
<Padder className={styles.footer}>{getPrettyDate(createdAt)}</Padder>
|
<Padder className={styles.footer}>{getPrettyDate(createdAt)}</Padder>
|
||||||
|
|
|
@ -6,10 +6,6 @@
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
& > * {
|
|
||||||
@include row_shadow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
|
|
97
src/components/notes/NoteCreationForm/index.tsx
Normal file
97
src/components/notes/NoteCreationForm/index.tsx
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import React, { FC, useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import { FormikConfig, useFormik } from "formik";
|
||||||
|
import { object, string, Asserts } from "yup";
|
||||||
|
|
||||||
|
import { Card } from "~/components/containers/Card";
|
||||||
|
import { Filler } from "~/components/containers/Filler";
|
||||||
|
import { Group } from "~/components/containers/Group";
|
||||||
|
import { Button } from "~/components/input/Button";
|
||||||
|
import { Textarea } from "~/components/input/Textarea";
|
||||||
|
import { useRandomPhrase } from "~/constants/phrases";
|
||||||
|
import { Note } from "~/types/notes";
|
||||||
|
import { getErrorMessage } from "~/utils/errors/getErrorMessage";
|
||||||
|
import { showErrorToast } from "~/utils/errors/showToast";
|
||||||
|
|
||||||
|
import styles from "./styles.module.scss";
|
||||||
|
|
||||||
|
interface NoteCreationFormProps {
|
||||||
|
onSubmit: (text: string, callback: (note: Note) => void) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationSchema = object({
|
||||||
|
text: string().required("Напишите что-нибудь"),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Values = Asserts<typeof validationSchema>;
|
||||||
|
|
||||||
|
const NoteCreationForm: FC<NoteCreationFormProps> = ({
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
}) => {
|
||||||
|
const placeholder = useRandomPhrase("SIMPLE");
|
||||||
|
|
||||||
|
const submit = useCallback<FormikConfig<Values>["onSubmit"]>(
|
||||||
|
async ({ text }, { resetForm, setSubmitting, setErrors }) => {
|
||||||
|
try {
|
||||||
|
await onSubmit(text, () => resetForm());
|
||||||
|
} catch (error) {
|
||||||
|
const message = getErrorMessage(error);
|
||||||
|
if (message) {
|
||||||
|
setErrors({ text: message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showErrorToast(error);
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onSubmit],
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
values,
|
||||||
|
errors,
|
||||||
|
handleChange,
|
||||||
|
handleSubmit,
|
||||||
|
touched,
|
||||||
|
handleBlur,
|
||||||
|
} = useFormik<Values>({
|
||||||
|
initialValues: { text: "" },
|
||||||
|
validationSchema,
|
||||||
|
onSubmit: submit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Card className={styles.card}>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<Textarea
|
||||||
|
handler={handleChange("text")}
|
||||||
|
value={values.text}
|
||||||
|
error={touched.text ? errors.text : undefined}
|
||||||
|
onBlur={handleBlur("text")}
|
||||||
|
placeholder={placeholder}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Group horizontal className={styles.row}>
|
||||||
|
<Filler />
|
||||||
|
|
||||||
|
<Button size="mini" type="button" color="link" onClick={onCancel}>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button size="mini" type="submit" color="gray">
|
||||||
|
ОК
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NoteCreationForm };
|
11
src/components/notes/NoteCreationForm/styles.module.scss
Normal file
11
src/components/notes/NoteCreationForm/styles.module.scss
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
@import "src/styles/variables";
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
@include row_shadow;
|
||||||
|
|
||||||
|
padding: $gap / 2;
|
||||||
|
}
|
|
@ -1,10 +1,8 @@
|
||||||
import React, { VFC } from 'react';
|
import { VFC } from "react";
|
||||||
|
|
||||||
import { useStackContext } from '~/components/sidebar/SidebarStack';
|
import { useStackContext } from "~/components/sidebar/SidebarStack";
|
||||||
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';
|
import { SidebarStackCard } from "~/components/sidebar/SidebarStackCard";
|
||||||
import { SettingsNotes } from '~/containers/settings/SettingsNotes';
|
import { SettingsNotes } from "~/containers/settings/SettingsNotes";
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
interface ProfileSidebarNotesProps {}
|
interface ProfileSidebarNotesProps {}
|
||||||
|
|
||||||
|
@ -12,10 +10,13 @@ const ProfileSidebarNotes: VFC<ProfileSidebarNotesProps> = () => {
|
||||||
const { closeAllTabs } = useStackContext();
|
const { closeAllTabs } = useStackContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarStackCard width={800} headerFeature="back" title="Заметки" onBackPress={closeAllTabs}>
|
<SidebarStackCard
|
||||||
<div className={styles.scroller}>
|
width={480}
|
||||||
<SettingsNotes />
|
headerFeature="back"
|
||||||
</div>
|
title="Заметки"
|
||||||
|
onBackPress={closeAllTabs}
|
||||||
|
>
|
||||||
|
<SettingsNotes />
|
||||||
</SidebarStackCard>
|
</SidebarStackCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
import React, { useCallback, VFC } from 'react';
|
import React, { useCallback, VFC } from "react";
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { Filler } from '~/components/containers/Filler';
|
import { Filler } from "~/components/containers/Filler";
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from "~/components/containers/Group";
|
||||||
import { Button } from '~/components/input/Button';
|
import { Button } from "~/components/input/Button";
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from "~/components/input/Icon";
|
||||||
import { MenuButton, MenuItemWithIcon } from '~/components/menu';
|
import { MenuButton, MenuItemWithIcon } from "~/components/menu";
|
||||||
import { VerticalMenu } from '~/components/menu/VerticalMenu';
|
import { VerticalMenu } from "~/components/menu/VerticalMenu";
|
||||||
import { useStackContext } from '~/components/sidebar/SidebarStack';
|
import { useStackContext } from "~/components/sidebar/SidebarStack";
|
||||||
import { ProfileSidebarHead } from '~/containers/profile/ProfileSidebarHead';
|
import { ProfileSidebarHead } from "~/containers/profile/ProfileSidebarHead";
|
||||||
import { ProfileStats } from '~/containers/profile/ProfileStats';
|
import { ProfileStats } from "~/containers/profile/ProfileStats";
|
||||||
import { useAuth } from '~/hooks/auth/useAuth';
|
import { useAuth } from "~/hooks/auth/useAuth";
|
||||||
import markdown from '~/styles/common/markdown.module.scss';
|
import markdown from "~/styles/common/markdown.module.scss";
|
||||||
|
|
||||||
import { ProfileSidebarLogoutButton } from '../ProfileSidebarLogoutButton';
|
import { ProfileSidebarLogoutButton } from "../ProfileSidebarLogoutButton";
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from "./styles.module.scss";
|
||||||
|
|
||||||
interface ProfileSidebarMenuProps {
|
interface ProfileSidebarMenuProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
@ -40,7 +40,13 @@ const ProfileSidebarMenu: VFC<ProfileSidebarMenuProps> = ({ onClose }) => {
|
||||||
<Filler className={classNames(markdown.wrapper, styles.text)}>
|
<Filler className={classNames(markdown.wrapper, styles.text)}>
|
||||||
<Group>
|
<Group>
|
||||||
<VerticalMenu className={styles.menu}>
|
<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>
|
</VerticalMenu>
|
||||||
|
|
||||||
<div className={styles.stats}>
|
<div className={styles.stats}>
|
||||||
|
@ -51,7 +57,7 @@ const ProfileSidebarMenu: VFC<ProfileSidebarMenuProps> = ({ onClose }) => {
|
||||||
|
|
||||||
<Group className={styles.buttons} horizontal>
|
<Group className={styles.buttons} horizontal>
|
||||||
<Filler />
|
<Filler />
|
||||||
<ProfileSidebarLogoutButton onLogout={onLogout}/>
|
<ProfileSidebarLogoutButton onLogout={onLogout} />
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,56 +1,69 @@
|
||||||
import React, { useState, VFC } from 'react';
|
import { FC, useState, VFC } from "react";
|
||||||
|
|
||||||
import { Card } from '~/components/containers/Card';
|
import { Filler } from "~/components/containers/Filler";
|
||||||
import { Columns } from '~/components/containers/Columns';
|
import { Group } from "~/components/containers/Group";
|
||||||
import { Filler } from '~/components/containers/Filler';
|
import { Button } from "~/components/input/Button";
|
||||||
import { Group } from '~/components/containers/Group';
|
import { NoteCard } from "~/components/notes/NoteCard";
|
||||||
import { Padder } from '~/components/containers/Padder';
|
import { NoteCreationForm } from "~/components/notes/NoteCreationForm";
|
||||||
import { Button } from '~/components/input/Button';
|
import { NoteProvider, useNotesContext } from "~/utils/providers/NoteProvider";
|
||||||
import { Icon } from '~/components/input/Icon';
|
|
||||||
import { InputText } from '~/components/input/InputText';
|
import styles from "./styles.module.scss";
|
||||||
import { Textarea } from '~/components/input/Textarea';
|
|
||||||
import { HorizontalMenu } from '~/components/menu/HorizontalMenu';
|
|
||||||
import { NoteCard } from '~/components/notes/NoteCard';
|
|
||||||
import { useGetNotes } from '~/hooks/notes/useGetNotes';
|
|
||||||
|
|
||||||
interface SettingsNotesProps {}
|
interface SettingsNotesProps {}
|
||||||
|
|
||||||
const SettingsNotes: VFC<SettingsNotesProps> = () => {
|
const List = () => {
|
||||||
const [text, setText] = useState('');
|
const { notes } = useNotesContext();
|
||||||
const { notes } = useGetNotes('');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<Padder>
|
{notes.map(note => (
|
||||||
<Group horizontal>
|
<NoteCard
|
||||||
<HorizontalMenu>
|
key={note.id}
|
||||||
<HorizontalMenu.Item active>Новые</HorizontalMenu.Item>
|
content={note.content}
|
||||||
<HorizontalMenu.Item>Старые</HorizontalMenu.Item>
|
createdAt={note.created_at}
|
||||||
</HorizontalMenu>
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
<Filler />
|
const Form: FC<{ onCancel: () => void }> = ({ onCancel }) => {
|
||||||
|
const { submit } = useNotesContext();
|
||||||
|
return <NoteCreationForm onSubmit={submit} onCancel={onCancel} />;
|
||||||
|
};
|
||||||
|
|
||||||
<InputText suffix={<Icon icon="search" size={24} />} />
|
const SettingsNotes: VFC<SettingsNotesProps> = () => {
|
||||||
</Group>
|
const [formIsShown, setFormIsShown] = useState(false);
|
||||||
</Padder>
|
|
||||||
|
|
||||||
<Columns>
|
return (
|
||||||
<Card>
|
<NoteProvider>
|
||||||
<Group>
|
<div className={styles.grid}>
|
||||||
<Textarea handler={setText} value={text} />
|
<div className={styles.head}>
|
||||||
|
{formIsShown ? (
|
||||||
<Group horizontal>
|
<Form onCancel={() => setFormIsShown(false)} />
|
||||||
|
) : (
|
||||||
|
<Group className={styles.showForm} horizontal>
|
||||||
<Filler />
|
<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>
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</div>
|
||||||
|
</div>
|
||||||
{notes.map(note => (
|
</NoteProvider>
|
||||||
<NoteCard key={note.id} content={note.content} createdAt={note.created_at} />
|
|
||||||
))}
|
|
||||||
</Columns>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,55 +0,0 @@
|
||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite';
|
|
||||||
import { apiGetNotes } from '~/api/notes';
|
|
||||||
import { ApiGetNotesRequest } from '~/api/notes/types';
|
|
||||||
import { useAuth } from '~/hooks/auth/useAuth';
|
|
||||||
import { GetLabNodesRequest, ILabNode } from '~/types/lab';
|
|
||||||
import { flatten, uniqBy } from '~/utils/ramda';
|
|
||||||
|
|
||||||
const DEFAULT_COUNT = 20;
|
|
||||||
|
|
||||||
const getKey: (isUser: boolean, search: string) => SWRInfiniteKeyLoader = (isUser, search) => (
|
|
||||||
index,
|
|
||||||
prev: ILabNode[]
|
|
||||||
) => {
|
|
||||||
if (!isUser) return null;
|
|
||||||
if (index > 0 && (!prev?.length || prev.length < 20)) return null;
|
|
||||||
|
|
||||||
const props: GetLabNodesRequest = {
|
|
||||||
limit: DEFAULT_COUNT,
|
|
||||||
offset: index * DEFAULT_COUNT,
|
|
||||||
search: search || '',
|
|
||||||
};
|
|
||||||
|
|
||||||
return JSON.stringify(props);
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseKey = (key: string): ApiGetNotesRequest => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(key);
|
|
||||||
} catch (error) {
|
|
||||||
return { limit: DEFAULT_COUNT, offset: 0, search: '' };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetNotes = (search: string) => {
|
|
||||||
const { isUser } = useAuth();
|
|
||||||
|
|
||||||
const { data, isValidating, size, setSize, mutate } = useSWRInfinite(
|
|
||||||
getKey(isUser, search),
|
|
||||||
async (key: string) => {
|
|
||||||
const result = await apiGetNotes(parseKey(key));
|
|
||||||
return result.list;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dedupingInterval: 300,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const notes = useMemo(() => uniqBy(n => n.id, flatten(data || [])), [data]);
|
|
||||||
const hasMore = (data?.[size - 1]?.length || 0) >= 1;
|
|
||||||
const loadMore = useCallback(() => setSize(size + 1), [setSize, size]);
|
|
||||||
|
|
||||||
return { notes, hasMore, loadMore, isLoading: !data && isValidating };
|
|
||||||
};
|
|
81
src/hooks/notes/useNotes.ts
Normal file
81
src/hooks/notes/useNotes.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import useSWRInfinite from "swr/infinite";
|
||||||
|
import { SWRInfiniteKeyLoader } from "swr/infinite";
|
||||||
|
|
||||||
|
import { apiGetNotes, apiPostNote } from "~/api/notes";
|
||||||
|
import { ApiGetNotesRequest } from "~/api/notes/types";
|
||||||
|
import { useAuth } from "~/hooks/auth/useAuth";
|
||||||
|
import { GetLabNodesRequest, ILabNode } from "~/types/lab";
|
||||||
|
import { Note } from "~/types/notes";
|
||||||
|
import { showErrorToast } from "~/utils/errors/showToast";
|
||||||
|
import { flatten, uniqBy } from "~/utils/ramda";
|
||||||
|
|
||||||
|
const DEFAULT_COUNT = 20;
|
||||||
|
|
||||||
|
const getKey: (isUser: boolean, search: string) => SWRInfiniteKeyLoader = (
|
||||||
|
isUser,
|
||||||
|
search,
|
||||||
|
) => (index, prev: ILabNode[]) => {
|
||||||
|
if (!isUser) return null;
|
||||||
|
if (index > 0 && (!prev?.length || prev.length < 20)) return null;
|
||||||
|
|
||||||
|
const props: GetLabNodesRequest = {
|
||||||
|
limit: DEFAULT_COUNT,
|
||||||
|
offset: index * DEFAULT_COUNT,
|
||||||
|
search: search || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.stringify(props);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseKey = (key: string): ApiGetNotesRequest => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(key);
|
||||||
|
} catch (error) {
|
||||||
|
return { limit: DEFAULT_COUNT, offset: 0, search: "" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useNotes = (search: string) => {
|
||||||
|
const { isUser } = useAuth();
|
||||||
|
|
||||||
|
const { data, isValidating, size, setSize, mutate } = useSWRInfinite(
|
||||||
|
getKey(isUser, search),
|
||||||
|
async (key: string) => {
|
||||||
|
const result = await apiGetNotes(parseKey(key));
|
||||||
|
return result.list;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dedupingInterval: 300,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const submit = useCallback(
|
||||||
|
async (text: string, onSuccess: (note: Note) => void) => {
|
||||||
|
const result = await apiPostNote({ text });
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
mutate(data?.map((it, index) => (index === 0 ? [result, ...it] : it)));
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuccess(result);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const notes = useMemo(() => uniqBy(n => n.id, flatten(data || [])), [data]);
|
||||||
|
const hasMore = (data?.[size - 1]?.length || 0) >= 1;
|
||||||
|
const loadMore = useCallback(() => setSize(size + 1), [setSize, size]);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
notes,
|
||||||
|
hasMore,
|
||||||
|
loadMore,
|
||||||
|
isLoading: !data && isValidating,
|
||||||
|
submit,
|
||||||
|
}),
|
||||||
|
[notes, hasMore, loadMore, data, isValidating, submit],
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,5 +1,7 @@
|
||||||
import { ERRORS } from '~/constants/errors';
|
import { Context } from "react";
|
||||||
import { IUser } from '~/types/auth';
|
|
||||||
|
import { ERRORS } from "~/constants/errors";
|
||||||
|
import { IUser } from "~/types/auth";
|
||||||
|
|
||||||
export interface ITag {
|
export interface ITag {
|
||||||
ID: number;
|
ID: number;
|
||||||
|
@ -16,10 +18,11 @@ export interface ITag {
|
||||||
export type IIcon = string;
|
export type IIcon = string;
|
||||||
|
|
||||||
export type ValueOf<T> = T[keyof T];
|
export type ValueOf<T> = T[keyof T];
|
||||||
|
export type ContextValue<T> = T extends Context<infer U> ? U : never;
|
||||||
|
|
||||||
export type UUID = string;
|
export type UUID = string;
|
||||||
|
|
||||||
export type IUploadType = 'image' | 'text' | 'audio' | 'video' | 'other';
|
export type IUploadType = "image" | "text" | "audio" | "video" | "other";
|
||||||
|
|
||||||
export interface IFile {
|
export interface IFile {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -52,17 +55,21 @@ export interface IFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBlockText {
|
export interface IBlockText {
|
||||||
type: 'text';
|
type: "text";
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBlockEmbed {
|
export interface IBlockEmbed {
|
||||||
type: 'video';
|
type: "video";
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IBlock = IBlockText | IBlockEmbed;
|
export type IBlock = IBlockText | IBlockEmbed;
|
||||||
export type FlowDisplayVariant = 'single' | 'vertical' | 'horizontal' | 'quadro';
|
export type FlowDisplayVariant =
|
||||||
|
| "single"
|
||||||
|
| "vertical"
|
||||||
|
| "horizontal"
|
||||||
|
| "quadro";
|
||||||
export interface FlowDisplay {
|
export interface FlowDisplay {
|
||||||
display: FlowDisplayVariant;
|
display: FlowDisplayVariant;
|
||||||
show_description: boolean;
|
show_description: boolean;
|
||||||
|
@ -102,7 +109,7 @@ export interface INode {
|
||||||
|
|
||||||
export type IFlowNode = Pick<
|
export type IFlowNode = Pick<
|
||||||
INode,
|
INode,
|
||||||
'id' | 'flow' | 'description' | 'title' | 'thumbnail' | 'created_at'
|
"id" | "flow" | "description" | "title" | "thumbnail" | "created_at"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export interface IComment {
|
export interface IComment {
|
||||||
|
@ -116,7 +123,7 @@ export interface IComment {
|
||||||
deleted_at?: string;
|
deleted_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IMessage = Omit<IComment, 'user' | 'node'> & {
|
export type IMessage = Omit<IComment, "user" | "node"> & {
|
||||||
from: IUser;
|
from: IUser;
|
||||||
to: IUser;
|
to: IUser;
|
||||||
};
|
};
|
||||||
|
@ -125,7 +132,7 @@ export interface ICommentGroup {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
comments: IComment[];
|
comments: IComment[];
|
||||||
distancesInDays: number[];
|
distancesInDays: number[];
|
||||||
ids: IComment['id'][];
|
ids: IComment["id"][];
|
||||||
hasNew: boolean;
|
hasNew: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,19 +140,19 @@ export type IUploadProgressHandler = (progress: ProgressEvent) => void;
|
||||||
export type IError = ValueOf<typeof ERRORS>;
|
export type IError = ValueOf<typeof ERRORS>;
|
||||||
|
|
||||||
export const NOTIFICATION_TYPES = {
|
export const NOTIFICATION_TYPES = {
|
||||||
message: 'message',
|
message: "message",
|
||||||
comment: 'comment',
|
comment: "comment",
|
||||||
node: 'node',
|
node: "node",
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IMessageNotification = {
|
export type IMessageNotification = {
|
||||||
type: typeof NOTIFICATION_TYPES['message'];
|
type: typeof NOTIFICATION_TYPES["message"];
|
||||||
content: Partial<IMessage>;
|
content: Partial<IMessage>;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ICommentNotification = {
|
export type ICommentNotification = {
|
||||||
type: typeof NOTIFICATION_TYPES['comment'];
|
type: typeof NOTIFICATION_TYPES["comment"];
|
||||||
content: Partial<IComment>;
|
content: Partial<IComment>;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
};
|
};
|
||||||
|
|
19
src/utils/providers/NoteProvider.tsx
Normal file
19
src/utils/providers/NoteProvider.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { createContext, FC, useContext } from "react";
|
||||||
|
|
||||||
|
import { useNotes } from "~/hooks/notes/useNotes";
|
||||||
|
|
||||||
|
const NoteContext = createContext<ReturnType<typeof useNotes>>({
|
||||||
|
notes: [],
|
||||||
|
hasMore: false,
|
||||||
|
loadMore: async () => Promise.resolve(undefined),
|
||||||
|
isLoading: false,
|
||||||
|
submit: () => Promise.resolve(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NoteProvider: FC = ({ children }) => {
|
||||||
|
const notes = useNotes("");
|
||||||
|
|
||||||
|
return <NoteContext.Provider value={notes}>{children}</NoteContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useNotesContext = () => useContext(NoteContext);
|
Loading…
Add table
Add a link
Reference in a new issue