diff --git a/.env b/.env index e248afe1..6bd7b4ee 100644 --- a/.env +++ b/.env @@ -1,4 +1,3 @@ NEXT_PUBLIC_API=http://192.168.1.250:3001/ -NEXT_PUBLIC_OLD_API=https://graff.estate/api NEXT_PUBLIC_S3_BUCKET=https://storage.yandexcloud.net/dult-faib-knac-fint/ NEXT_PUBLIC_TINYMCE_API_KEY=2vf68779upg45y46o6g5gaxldy9gzr399eyaaqa0ki3mj2h2 \ No newline at end of file diff --git a/env.d.ts b/env.d.ts index ba3e6ccf..e668abe4 100644 --- a/env.d.ts +++ b/env.d.ts @@ -1,7 +1,6 @@ declare namespace NodeJS { interface ProcessEnv { readonly NEXT_PUBLIC_API: string; - readonly NEXT_PUBLIC_OLD_API: string; readonly NEXT_PUBLIC_S3_BUCKET: string; readonly NEXT_PUBLIC_TINYMCE_API_KEY: string; } diff --git a/src/api/index.ts b/src/api/index.ts index 0cdc7d75..15864f46 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,23 +1,6 @@ import ky from 'ky'; -export const oldApi = ky.extend({ - prefixUrl: process.env.NEXT_PUBLIC_OLD_API, -}); - export const api = ky.extend({ prefixUrl: process.env.NEXT_PUBLIC_API, credentials: 'include', - // hooks: { - // beforeRequest: [ - // async (req) => { - // if (new URL(req.url).pathname === '/auth/check') { - // await ky - // .get(process.env.NEXT_PUBLIC_API + 'auth/refresh', { - // credentials: 'include', - // }) - // .json(); - // } - // }, - // ], - // }, }); diff --git a/src/app/(main)/blog/page.tsx b/src/app/(main)/blog/page.tsx index fbba5d18..7db4ccab 100644 --- a/src/app/(main)/blog/page.tsx +++ b/src/app/(main)/blog/page.tsx @@ -3,6 +3,7 @@ import { ArticlesList } from '@/components/pages/BlogPage/ArticlesList'; import { ArticlesPageActions } from '@/components/pages/BlogPage/ArticlesPageActions'; import { getQueryClient } from '@/lib/queryClient'; import { IArticle } from '@/types/IArticle'; +import { IStory } from '@/types/IStory'; import { dehydrate, HydrationBoundary } from '@tanstack/react-query'; export default async function BlogPage({ @@ -33,6 +34,11 @@ export default async function BlogPage({ .json(), }); + await queryClient.prefetchQuery({ + queryKey: ['stories'], + queryFn: async () => await api.get('stories').json(), + }); + return ( diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx index 608b2309..bd0e9ddb 100644 --- a/src/app/(main)/page.tsx +++ b/src/app/(main)/page.tsx @@ -58,7 +58,6 @@ export default async function HomePage() { - {/* */} ); } diff --git a/src/app/globals.css b/src/app/globals.css index c6307e2b..b44d4a80 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -111,7 +111,7 @@ html { } @utility line1 { - @apply 2xl:text-[128px] lg:max-2xl:text-[clamp(96px,4.444vw,128px)] md:max-lg:text-[clamp(56px,56px+(100vw-768px)/672*40,96px)] xs:max-md:text-[clamp(40px,40px+(100vw-360px)/408*16,56px)] text-[40px] leading-[85%]; + @apply 2xl:text-[128px] lg:max-2xl:text-[clamp(96px,96px+(100vw-1440px)/96*32,128px)] md:max-lg:text-[clamp(56px,56px+(100vw-768px)/672*40,96px)] xs:max-md:text-[clamp(40px,40px+(100vw-360px)/408*16,56px)] text-[40px] leading-[85%]; } @utility line2 { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d440e41b..809d08e8 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -39,7 +39,7 @@ export default function RootLayout({ }>) { return ( - + {children} diff --git a/src/components/DeleteItemModal.tsx b/src/components/DeleteItemModal.tsx index 65ee69eb..666fda28 100644 --- a/src/components/DeleteItemModal.tsx +++ b/src/components/DeleteItemModal.tsx @@ -9,7 +9,7 @@ export function DeleteItemModal({ id, }: { title: string; - entity: 'projects' | 'companies' | 'articles'; + entity: 'projects' | 'companies' | 'articles' | 'stories'; id: string; }) { const { setModal } = useModalStore(); @@ -21,7 +21,7 @@ export function DeleteItemModal({

{title}

); diff --git a/src/components/ImageUploader.tsx b/src/components/ImageUploader.tsx index db70e517..6835c69f 100644 --- a/src/components/ImageUploader.tsx +++ b/src/components/ImageUploader.tsx @@ -16,20 +16,17 @@ import TrashIcon from '../../public/icons/trash.svg'; export function ImageUploader({ dest, - fieldName, - // item, + name, label, className = '', required = false, }: { dest: string; - fieldName: Path; - item: Record<'img', string> & Record<'id', string>; + name: Path; label: string; required?: boolean; className?: string; }) { - // const [currentImg, setCurrentImg] = useState(item.img); const [file, setFile] = useState(); const [previewFile, setPreviewFile] = useState(''); @@ -55,24 +52,19 @@ export function ImageUploader({ const filePaths = await api .post('upload', { body: formData }) .json>[]>(); - setValue(fieldName, filePaths[0]!); + setValue(name, filePaths[0]!); } catch (error) { if (error instanceof Error) { alert(`Error: ${error.message}`); } } - }, [dest, fieldName, file, setValue]); + }, [dest, name, file, setValue]); useEffect(() => { uploadFile(); }, [file, uploadFile]); - const currentImg = useWatch({ control, name: fieldName }); - - // useEffect(() => { - // const { unsubscribe } = watch((input) => setCurrentImg(input[fieldName])); - // return unsubscribe; - // }, [watch, fieldName]); + const currentImg = useWatch({ control, name: name }); return ( ({ type="file" accept={'image/*'} required={required} - {...{ ...register(fieldName, { required }), ref: inputRef }} + {...{ ...register(name, { required }), ref: inputRef }} onChange={handleChangeFile} {...getInputProps()} /> @@ -101,7 +93,7 @@ export function ImageUploader({ <>
({ } alt={''} className={`pointer-events-none aspect-square object-${ - fieldName === 'image' ? 'cover rounded-2xl' : 'contain' + name === 'image' ? 'cover rounded-2xl' : 'contain' } !relative`} fill sizes="100%" @@ -133,7 +125,7 @@ export function ImageUploader({ onClick={(e) => { e.preventDefault(); setFile(undefined!); - setValue(fieldName, undefined!); + setValue(name, undefined!); setPreviewFile(''); }} className="bg-[#37393B99] px-3 py-2 rounded-xl cursor-pointer" diff --git a/src/components/ItemActions.tsx b/src/components/ItemActions.tsx index f3320541..975dcb62 100644 --- a/src/components/ItemActions.tsx +++ b/src/components/ItemActions.tsx @@ -5,6 +5,8 @@ import { useModalStore } from '@/stores/useModalStore'; import { IArticle } from '@/types/IArticle'; import { ICompany } from '@/types/ICompany'; import { IProject } from '@/types/IProject'; +import { IStory } from '@/types/IStory'; +import { isArticle } from '@/utils/isArticle'; import { isCompany } from '@/utils/isCompany'; import { isProject } from '@/utils/isProject'; import { SyntheticEvent } from 'react'; @@ -14,11 +16,12 @@ import { DeleteItemModal } from './DeleteItemModal'; import { ArticleContentFormModal } from './modals/ArticleContentFormModal'; import { CompanyFormModal } from './modals/CompanyFormModal'; import { ProjectFormModal } from './modals/ProjectFormModal'; +import { StoryFormModal } from './modals/StoryFormModal'; export function ItemActions({ item, }: { - item: IProject | ICompany | IArticle; + item: IProject | ICompany | IArticle | IStory; }) { const { data: auth } = useCheckAuthQuery(); @@ -28,21 +31,12 @@ export function ItemActions({ e.stopPropagation(); if (isProject(item)) { const { company, ...project } = item; - setModal( - , - 'editProjectForm' - ); + setModal(); } else if (isCompany(item)) { const { projects, ...company } = item; - setModal( - , - 'editCompanyForm' - ); - } else - setModal( - , - 'articleContentFormModal' - ); + setModal(); + } else if (isArticle(item)) setModal(); + else setModal(); } function handleDelete(e: SyntheticEvent) { @@ -56,19 +50,20 @@ export function ItemActions({ ? 'проекта' : isCompany(item) ? 'компании' - : 'статьи') + : isArticle(item) + ? 'статьи' + : 'истории') } entity={ isProject(item) ? 'projects' : isCompany(item) ? 'companies' - : 'articles' + : isArticle(item) + ? 'articles' + : 'stories' } - />, - `delete${ - isProject(item) ? 'Project' : isCompany(item) ? 'Company' : 'Article' - }` + /> ); } diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index 8571ac4d..0a503d60 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -4,7 +4,6 @@ import { api } from '@/api'; import { useMediaQueries } from '@/hooks/useMediaQueries'; import { useScroll } from '@/hooks/useScroll'; import { useCheckAuthQuery } from '@/queries/checkAuth'; -import { useModalStore } from '@/stores/useModalStore'; import { HeaderLink } from '@/ui/HeaderLink'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { AnimatePresence, motion } from 'framer-motion'; @@ -21,8 +20,6 @@ import SkolkovoIcon from '../../../public/icons/skolkovo.svg'; import { Products } from './Products'; export function Header() { - const { setModal } = useModalStore(); - const queryClient = useQueryClient(); const { data: auth } = useCheckAuthQuery(); diff --git a/src/components/Layout/ModalContainer.tsx b/src/components/Layout/ModalContainer.tsx index 943ee01b..aaeb0874 100644 --- a/src/components/Layout/ModalContainer.tsx +++ b/src/components/Layout/ModalContainer.tsx @@ -4,11 +4,11 @@ import { useModalStore } from '@/stores/useModalStore'; import { useEffect } from 'react'; export function ModalContainer() { - const { modal, name, setModal } = useModalStore(); + const { modal, setModal } = useModalStore(); useEffect(() => { const listener = (e: KeyboardEvent) => { - if (e.key === 'Escape') setModal(null, ''); + if (e.key === 'Escape') setModal(null); }; document.addEventListener('keydown', listener); @@ -17,16 +17,7 @@ export function ModalContainer() { return ( modal && ( -
+
{modal}
diff --git a/src/components/VideoUploader.tsx b/src/components/VideoUploader.tsx new file mode 100644 index 00000000..8f1abe65 --- /dev/null +++ b/src/components/VideoUploader.tsx @@ -0,0 +1,133 @@ +import { api } from '@/api'; +import { Button } from '@/ui/Button'; +import { ChangeEvent, useCallback, useEffect, useState } from 'react'; +import Dropzone from 'react-dropzone'; +import { useFormContext, useWatch } from 'react-hook-form'; +import AddIcon from '../../public/icons/add.svg'; +import RestartIcon from '../../public/icons/restart.svg'; +import TrashIcon from '../../public/icons/trash.svg'; + +export function VideoUploader({ name, dest }: { name: string; dest: string }) { + const { register, control, setValue } = useFormContext(); + + const [file, setFile] = useState(); + const [previewFile, setPreviewFile] = useState(''); + + const currentVideo = useWatch({ control, name }); + + function handleChangeFile(e: ChangeEvent) { + if (!e.target.files) return; + + const targetFile = e.target.files[0]; + + setFile(targetFile); + setPreviewFile(URL.createObjectURL(targetFile)); + } + + const uploadFile = useCallback(async () => { + if (!file) return; + + const formData = new FormData(); + formData.append('dest', dest); + formData.append('files', file); + + try { + const filePaths = await api + .post('upload', { body: formData }) + .json(); + setValue(name, filePaths[0]!); + } catch (error) { + if (error instanceof Error) { + alert(`Error: ${error.message}`); + } + } + }, [file, name, setValue]); + + useEffect(() => { + uploadFile(); + }, [file, uploadFile]); + + return ( + { + setFile(file); + setPreviewFile(URL.createObjectURL(file)); + }} + accept={{ + 'video/*': ['*'], + }} + noClick + > + {({ getRootProps, getInputProps, inputRef }) => ( +
+ + {previewFile || currentVideo ? ( + <> +
+
+
+ + +
+ + ) : ( +
+

+ Выберите или перетащите видео +

+ +
+ )} +
+ )} +
+ ); +} diff --git a/src/components/articleInputs/ArticleQuoteInput.tsx b/src/components/articleInputs/ArticleQuoteInput.tsx index fe070fe6..fc811837 100644 --- a/src/components/articleInputs/ArticleQuoteInput.tsx +++ b/src/components/articleInputs/ArticleQuoteInput.tsx @@ -27,9 +27,8 @@ export function ArticleQuoteInput({
diff --git a/src/components/articleInputs/ArticleSliderImageInput.tsx b/src/components/articleInputs/ArticleSliderImageInput.tsx index 6328aacc..13e0a8eb 100644 --- a/src/components/articleInputs/ArticleSliderImageInput.tsx +++ b/src/components/articleInputs/ArticleSliderImageInput.tsx @@ -26,8 +26,7 @@ export function ArticleSliderImageInput({
)} - + */} + diff --git a/src/components/modals/ArticleContentFormModal.tsx b/src/components/modals/ArticleContentFormModal.tsx index 1e220d0b..b1b0313a 100644 --- a/src/components/modals/ArticleContentFormModal.tsx +++ b/src/components/modals/ArticleContentFormModal.tsx @@ -57,7 +57,7 @@ export function ArticleContentFormModal({ blocks: JSON.stringify(getValues('blocks')), drafted, }); - setModal(null, ''); + setModal(null); } return ( @@ -83,8 +83,7 @@ export function ArticleContentFormModal({ posterImage, id, }} - />, - 'editArticleModal' + /> ) } icon={ diff --git a/src/components/modals/ArticleFormActions.tsx b/src/components/modals/ArticleFormActions.tsx index 20902ca0..2fe19f1c 100644 --- a/src/components/modals/ArticleFormActions.tsx +++ b/src/components/modals/ArticleFormActions.tsx @@ -36,7 +36,7 @@ export function ArticleFormActions({
diff --git a/src/components/modals/ArticleFormModal.tsx b/src/components/modals/ArticleFormModal.tsx index 5a1d0514..5cbb3cc4 100644 --- a/src/components/modals/ArticleFormModal.tsx +++ b/src/components/modals/ArticleFormModal.tsx @@ -39,10 +39,7 @@ export function ArticleFormModal({ ...data, blocks: JSON.stringify(defaultValues ? defaultValues.blocks : []), }); - setModal( - , - 'articleContentFormModal' - ); + setModal(); } const { handleSubmit, getValues, control } = form; @@ -57,7 +54,7 @@ export function ArticleFormModal({ ), drafted, }); - setModal(null, ''); + setModal(null); } const { mutateAsync } = useArticleMutation( @@ -77,8 +74,8 @@ export function ArticleFormModal({ submitHandler={handleSubmit(onSubmit)} disabled={!title || !tags || !cardImage || !posterImage} /> - -
+ + ({
@@ -103,12 +99,11 @@ export function ArticleFormModal({ - - + +
); diff --git a/src/components/modals/CompanyFormModal.tsx b/src/components/modals/CompanyFormModal.tsx index 1cc7f244..f1ac10b9 100644 --- a/src/components/modals/CompanyFormModal.tsx +++ b/src/components/modals/CompanyFormModal.tsx @@ -43,7 +43,7 @@ export function CompanyFormModal({ : await api.put(`companies/${defaultValues?.id}`, { json }).json(), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['companies'] }); - setModal(null, ''); + setModal(null); }, }); @@ -51,7 +51,7 @@ export function CompanyFormModal({ <> @@ -69,15 +69,13 @@ export function CompanyFormModal({
diff --git a/src/components/modals/DeleteArticleModal.tsx b/src/components/modals/DeleteArticleModal.tsx deleted file mode 100644 index aa4d835f..00000000 --- a/src/components/modals/DeleteArticleModal.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useModalStore } from '@/stores/useModalStore'; -import { Button } from '@/ui/Button'; -import { Icon } from '@/ui/Icon'; - -export function DeleteArticleModal({ id }: { id: string }) { - const { setModal } = useModalStore(); - - async function handleDeleteArticle() {} - - return ( -
-
-

Удаление статьи

- -
- - -
- ); -} diff --git a/src/components/modals/ProjectFormModal.tsx b/src/components/modals/ProjectFormModal.tsx index 7a35fe08..e64daca2 100644 --- a/src/components/modals/ProjectFormModal.tsx +++ b/src/components/modals/ProjectFormModal.tsx @@ -96,8 +96,7 @@ export function ProjectFormModal({
@@ -126,7 +125,6 @@ export function ProjectFormModal({ } - modalName="companyForm" >
diff --git a/src/components/modals/StoriesModal.tsx b/src/components/modals/StoriesModal.tsx index fad06b87..92b1dafb 100644 --- a/src/components/modals/StoriesModal.tsx +++ b/src/components/modals/StoriesModal.tsx @@ -1,9 +1,10 @@ -import { stories } from '@/consts/stories'; import { useMediaQueries } from '@/hooks/useMediaQueries'; +import { useGetStories } from '@/queries/getStories'; import { useModalStore } from '@/stores/useModalStore'; import { createRef, RefObject, useEffect, useRef, useState } from 'react'; import { useSwipeable } from 'react-swipeable'; import CloseIcon from '../../../public/icons/close.svg'; +import { ItemActions } from '../ItemActions'; export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) { const { setModal } = useModalStore(); @@ -16,9 +17,12 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) { const ref = useRef(null); + const { data: stories } = useGetStories(); + useEffect(() => { + if (!stories) return; setVideoRefs(stories.map(createRef)); - }, []); + }, [stories]); useEffect(() => { if ( @@ -40,7 +44,7 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) { useEffect(() => { if (currentProgress === 1 && videoRefs.length) { - if (currentIndex === videoRefs.length - 1) setModal(null, ''); + if (currentIndex === videoRefs.length - 1) setModal(null); else setCurrentIndex((prev) => prev + 1); } }, [currentProgress, setModal, videoRefs]); @@ -68,7 +72,7 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) { preventScrollOnSwipe: true, touchEventOptions: { passive: false }, onSwipedLeft: () => - setCurrentIndex((prev) => Math.min(stories.length - 1, prev + 1)), + setCurrentIndex((prev) => Math.min((stories?.length ?? 0) - 1, prev + 1)), onSwipedRight: () => setCurrentIndex((prev) => Math.max(0, prev - 1)), onTouchStartOrOnMouseDown: (e) => { const slider = ref.current; @@ -80,26 +84,30 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) { ) return; if ((e.event as TouchEvent).touches[0].clientX > slider!.clientWidth / 2) - setCurrentIndex((prev) => Math.min(stories.length - 1, prev + 1)); + setCurrentIndex((prev) => + Math.min((stories?.length ?? 0) - 1, prev + 1) + ); else setCurrentIndex((prev) => Math.max(0, prev - 1)); }, }); + if (!stories) return null; + return ( <>
{!!videoRefs.length && - stories.map(({ id, video, title, poster }, index) => ( + stories?.map(({ id, video, preview, text, createdAt }, index) => (