diff --git a/.env.local b/.env.local index 91636404..8e3e9501 100644 --- a/.env.local +++ b/.env.local @@ -1,6 +1,6 @@ NEXT_PUBLIC_API_HOST=https://pig.staging.vault48.org/ NEXT_PUBLIC_REMOTE_CURRENT=https://pig.staging.vault48.org/static/ -#NEXT_PUBLIC_API_HOST=http://localhost:8888/ -#NEXT_PUBLIC_REMOTE_CURRENT=http://localhost:8888/static/ -#NEXT_PUBLIC_API_HOST=https://pig.vault48.org/ -#NEXT_PUBLIC_REMOTE_CURRENT=https://pig.vault48.org/static/ +# NEXT_PUBLIC_API_HOST=http://localhost:8888/ +# NEXT_PUBLIC_REMOTE_CURRENT=http://localhost:8888/static/ +# NEXT_PUBLIC_API_HOST=https://pig.vault48.org/ +# NEXT_PUBLIC_REMOTE_CURRENT=https://pig.vault48.org/static/ diff --git a/.gitignore b/.gitignore index af8eab43..64b4f398 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ /.next /.vscode /.history -.DS_Store \ No newline at end of file +.DS_Store +/tsconfig.tsbuildinfo +.env.local \ No newline at end of file diff --git a/package.json b/package.json index f1a0721a..bf70f4f4 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "husky": "^7.0.4", "lint-staged": "^12.1.6", "next-transpile-modules": "^9.0.0", - "prettier": "^1.18.2" + "prettier": "^2.7.1" }, "lint-staged": { "./**/*.{js,jsx,ts,tsx}": [ diff --git a/src/api/notes/index.ts b/src/api/notes/index.ts index 21206e45..898008f8 100644 --- a/src/api/notes/index.ts +++ b/src/api/notes/index.ts @@ -1,8 +1,31 @@ -import { ApiGetNotesRequest, ApiGetNotesResponse } from '~/api/notes/types'; -import { URLS } from '~/constants/urls'; -import { api, cleanResult } from '~/utils/api'; +import { + ApiGetNotesRequest as ApiListNotesRequest, + ApiGetNotesResponse, + ApiCreateNoteRequest, + ApiUpdateNoteResponse, + ApiUpdateNoteRequest, +} from "~/api/notes/types"; +import { URLS } from "~/constants/urls"; +import { api, cleanResult } from "~/utils/api"; -export const apiGetNotes = ({ limit, offset, search }: ApiGetNotesRequest) => +export const apiListNotes = ({ limit, offset, search }: ApiListNotesRequest) => api .get(URLS.NOTES, { params: { limit, offset, search } }) .then(cleanResult); + +export const apiCreateNote = ({ text }: ApiCreateNoteRequest) => + api + .post(URLS.NOTES, { + text, + }) + .then(cleanResult); + +export const apiDeleteNote = (id: number) => + api.delete(URLS.NOTE(id)).then(cleanResult); + +export const apiUpdateNote = ({ id, text }: ApiUpdateNoteRequest) => + api + .put(URLS.NOTE(id), { + content: text, + }) + .then(cleanResult); diff --git a/src/api/notes/types.ts b/src/api/notes/types.ts index 921b686e..bc94d315 100644 --- a/src/api/notes/types.ts +++ b/src/api/notes/types.ts @@ -1,4 +1,4 @@ -import { Note } from '~/types/notes'; +import { Note } from "~/types/notes"; export interface ApiGetNotesRequest { limit: number; @@ -10,3 +10,14 @@ export interface ApiGetNotesResponse { list: Note[]; totalCount: number; } + +export interface ApiCreateNoteRequest { + text: string; +} + +export interface ApiUpdateNoteResponse extends Note {} + +export interface ApiUpdateNoteRequest { + id: number; + text: string; +} diff --git a/src/components/auth/login/LoginScene/index.tsx b/src/components/auth/login/LoginScene/index.tsx index 49fe29da..f03417af 100644 --- a/src/components/auth/login/LoginScene/index.tsx +++ b/src/components/auth/login/LoginScene/index.tsx @@ -1,10 +1,10 @@ -import { FC, memo, useCallback, useEffect, useRef, useState } from "react"; +import { FC, memo, useCallback, useEffect, useRef, useState } from 'react'; -import { debounce, throttle } from "throttle-debounce"; +import { debounce, throttle } from 'throttle-debounce'; -import { useWindowSize } from "~/hooks/dom/useWindowSize"; +import { useWindowSize } from '~/hooks/dom/useWindowSize'; -import styles from "./styles.module.scss"; +import styles from './styles.module.scss'; interface LoginSceneProps {} @@ -17,31 +17,31 @@ interface Layer { const layers: Layer[] = [ { - src: "/images/clouds__bg.svg", + src: '/images/clouds__bg.svg', velocity: -0.3, width: 3840, height: 1080, }, { - src: "/images/clouds__cube.svg", + src: '/images/clouds__cube.svg', velocity: -0.1, width: 3840, height: 1080, }, { - src: "/images/clouds__cloud.svg", + src: '/images/clouds__cloud.svg', velocity: 0.2, width: 3840, height: 1080, }, { - src: "/images/clouds__dudes.svg", + src: '/images/clouds__dudes.svg', velocity: 0.5, width: 3840, height: 1080, }, { - src: "/images/clouds__trash.svg", + src: '/images/clouds__trash.svg', velocity: 0.8, width: 3840, height: 1080, @@ -52,7 +52,7 @@ const LoginScene: FC = memo(() => { const containerRef = useRef(null); const [loaded, setLoaded] = useState(false); const imageRefs = useRef>([]); - const { isMobile } = useWindowSize(); + const { isTablet } = useWindowSize(); const domRect = useRef(); const onMouseMove = useCallback( @@ -84,11 +84,11 @@ const LoginScene: FC = memo(() => { useEffect(() => { const listener = throttle(100, onMouseMove); - document.addEventListener("mousemove", listener); - return () => document.removeEventListener("mousemove", listener); + document.addEventListener('mousemove', listener); + return () => document.removeEventListener('mousemove', listener); }, []); - if (isMobile) { + if (isTablet) { return null; } @@ -103,16 +103,16 @@ const LoginScene: FC = memo(() => { > - + - + diff --git a/src/components/boris/Superpower/index.tsx b/src/components/boris/Superpower/index.tsx index 89348516..1ee45c4e 100644 --- a/src/components/boris/Superpower/index.tsx +++ b/src/components/boris/Superpower/index.tsx @@ -1,15 +1,17 @@ import React, { FC } from 'react'; +import { observer } from 'mobx-react-lite'; + import { useAuth } from '~/hooks/auth/useAuth'; interface IProps {} -const Superpower: FC = ({ children }) => { +const Superpower: FC = observer(({ children }) => { const { isTester } = useAuth(); if (!isTester) return null; return <>{children}; -}; +}); export { Superpower }; diff --git a/src/components/containers/Markdown/index.tsx b/src/components/containers/Markdown/index.tsx index 622aa068..3d9e349e 100644 --- a/src/components/containers/Markdown/index.tsx +++ b/src/components/containers/Markdown/index.tsx @@ -1,13 +1,21 @@ -import React, { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; +import React, { DetailedHTMLProps, VFC, HTMLAttributes } from 'react'; import classNames from 'classnames'; import styles from '~/styles/common/markdown.module.scss'; +import { formatText } from '~/utils/dom'; -interface IProps extends DetailedHTMLProps, HTMLDivElement> {} +interface IProps + extends DetailedHTMLProps, HTMLDivElement> { + children?: string; +} -const Markdown: FC = ({ className, ...props }) => ( -
+const Markdown: VFC = ({ className, children = '', ...props }) => ( +
); export { Markdown }; diff --git a/src/components/containers/Zone/index.tsx b/src/components/containers/Zone/index.tsx index e26a63c8..86c13f71 100644 --- a/src/components/containers/Zone/index.tsx +++ b/src/components/containers/Zone/index.tsx @@ -1,22 +1,26 @@ -import React, { FC } from "react"; +import React, { FC } from 'react'; -import classNames from "classnames"; +import classNames from 'classnames'; -import styles from "./styles.module.scss"; +import styles from './styles.module.scss'; interface ZoneProps { title?: string; className?: string; - color?: "danger" | "normal"; + color?: 'danger' | 'normal'; } const Zone: FC = ({ title, className, children, - color = "normal", + color = 'normal', }) => ( -
+
{!!title && (
{title} diff --git a/src/components/containers/Zone/styles.module.scss b/src/components/containers/Zone/styles.module.scss index 4767c09a..707673b0 100644 --- a/src/components/containers/Zone/styles.module.scss +++ b/src/components/containers/Zone/styles.module.scss @@ -8,7 +8,7 @@ $pad_usual: mix(white, $content_bg, 10%); span { position: absolute; - top: -5px; + top: -$gap; left: $radius; transform: translate(0, -100%); background: $pad_usual; @@ -25,7 +25,7 @@ $pad_usual: mix(white, $content_bg, 10%); } .pad { - padding: $gap * 1.5 $gap $gap; + padding: $gap; box-shadow: inset $pad_usual 0 0 0 2px; border-radius: $radius; position: relative; @@ -33,4 +33,8 @@ $pad_usual: mix(white, $content_bg, 10%); &.danger { box-shadow: inset $pad_danger 0 0 0 2px; } + + &.with_title { + padding-top: $gap * 2; + } } diff --git a/src/components/editors/EditorButtons/index.tsx b/src/components/editors/EditorButtons/index.tsx index 1b11ddba..56b04a40 100644 --- a/src/components/editors/EditorButtons/index.tsx +++ b/src/components/editors/EditorButtons/index.tsx @@ -11,7 +11,7 @@ import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik'; const EditorButtons: FC = () => { const { values, handleChange, isSubmitting } = useNodeFormContext(); - const { isMobile } = useWindowSize(); + const { isTablet } = useWindowSize(); return ( @@ -23,14 +23,14 @@ const EditorButtons: FC = () => { title="Название" value={values.title} handler={handleChange('title')} - autoFocus={!isMobile} + autoFocus={!isTablet} maxLength={256} disabled={isSubmitting} />
- } +
- Профиль - Настройки - Выдох - + {(!photo || !photo.id) && } +
-
+ ); }; diff --git a/src/components/node/NodeAuthorBlock/index.tsx b/src/components/node/NodeAuthorBlock/index.tsx index 75015025..94f35103 100644 --- a/src/components/node/NodeAuthorBlock/index.tsx +++ b/src/components/node/NodeAuthorBlock/index.tsx @@ -3,7 +3,6 @@ import React, { FC, useCallback } from 'react'; import { Avatar } from '~/components/common/Avatar'; import { useUserDescription } from '~/hooks/auth/useUserDescription'; import { INodeUser } from '~/types'; -import { openUserProfile } from '~/utils/user'; import styles from './styles.module.scss'; @@ -12,8 +11,6 @@ interface Props { } const NodeAuthorBlock: FC = ({ user }) => { - const onOpenProfile = useCallback(() => openUserProfile(user?.username), [user]); - const description = useUserDescription(user); if (!user) { @@ -23,7 +20,7 @@ const NodeAuthorBlock: FC = ({ user }) => { const { fullname, username, photo } = user; return ( -
+
diff --git a/src/components/node/NodeEditMenu/index.tsx b/src/components/node/NodeEditMenu/index.tsx index ca500616..c7edb676 100644 --- a/src/components/node/NodeEditMenu/index.tsx +++ b/src/components/node/NodeEditMenu/index.tsx @@ -30,16 +30,19 @@ const NodeEditMenu: VFC = ({ onLock, onEdit, }) => { - const { isMobile } = useWindowSize(); + const { isTablet } = useWindowSize(); - if (isMobile) { + if (isTablet) { return ( } className={className} > {canStar && ( - + {isHeroic ? 'Убрать с главной' : 'На главную'} )} @@ -48,7 +51,10 @@ const NodeEditMenu: VFC = ({ Редактировать - + {isLocked ? 'Восстановить' : 'Удалить'} diff --git a/src/components/notes/NoteCard/index.tsx b/src/components/notes/NoteCard/index.tsx index a87cf6aa..b2d54601 100644 --- a/src/components/notes/NoteCard/index.tsx +++ b/src/components/notes/NoteCard/index.tsx @@ -1,4 +1,4 @@ -import React, { VFC } from 'react'; +import React, { useCallback, useState, VFC } from 'react'; import { Card } from '~/components/containers/Card'; import { Markdown } from '~/components/containers/Markdown'; @@ -6,22 +6,56 @@ import { Padder } from '~/components/containers/Padder'; import { NoteMenu } from '~/components/notes/NoteMenu'; import { formatText, getPrettyDate } from '~/utils/dom'; +import { NoteCreationForm } from '../NoteCreationForm'; + import styles from './styles.module.scss'; interface NoteCardProps { content: string; + remove: () => Promise; + update: (text: string, callback?: () => void) => Promise; createdAt: string; } -const NoteCard: VFC = ({ content, createdAt }) => ( - - - - - +const NoteCard: VFC = ({ + content, + createdAt, + remove, + update, +}) => { + const [editing, setEditing] = useState(false); - {getPrettyDate(createdAt)} - -); + const toggleEditing = useCallback(() => setEditing(v => !v), []); + const onUpdate = useCallback( + (text: string, callback?: () => void) => + update(text, () => { + setEditing(false); + callback?.(); + }), + [], + ); + + return ( + + {editing ? ( + + ) : ( + <> + + + + {formatText(content)} + + + {getPrettyDate(createdAt)} + + )} + + ); +}; export { NoteCard }; diff --git a/src/components/notes/NoteCard/styles.module.scss b/src/components/notes/NoteCard/styles.module.scss index 2a7e7c8c..c909430b 100644 --- a/src/components/notes/NoteCard/styles.module.scss +++ b/src/components/notes/NoteCard/styles.module.scss @@ -6,10 +6,6 @@ word-break: break-word; padding: 0; position: relative; - - & > * { - @include row_shadow; - } } .footer { diff --git a/src/components/notes/NoteCreationForm/index.tsx b/src/components/notes/NoteCreationForm/index.tsx new file mode 100644 index 00000000..e07935b6 --- /dev/null +++ b/src/components/notes/NoteCreationForm/index.tsx @@ -0,0 +1,99 @@ +import { FC, useCallback } from 'react'; + +import { FormikConfig, useFormik } from 'formik'; +import { Asserts, object, string } 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 { getErrorMessage } from '~/utils/errors/getErrorMessage'; +import { showErrorToast } from '~/utils/errors/showToast'; + +import styles from './styles.module.scss'; + +interface NoteCreationFormProps { + text?: string; + onSubmit: (text: string, callback: () => void) => void; + onCancel: () => void; +} + +const validationSchema = object({ + text: string().required('Напишите что-нибудь'), +}); + +type Values = Asserts; + +const NoteCreationForm: FC = ({ + text = '', + onSubmit, + onCancel, +}) => { + const placeholder = useRandomPhrase('SIMPLE'); + + const submit = useCallback['onSubmit']>( + async (values, { resetForm, setSubmitting, setErrors }) => { + try { + await onSubmit(values.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, + isSubmitting, + } = useFormik({ + initialValues: { text }, + validationSchema, + onSubmit: submit, + }); + + return ( +
+ +
+