diff --git a/config/app.php b/config/app.php index 423eed5..b62456c 100644 --- a/config/app.php +++ b/config/app.php @@ -123,4 +123,11 @@ return [ 'store' => env('APP_MAINTENANCE_STORE', 'database'), ], + //Типы всплывающих уведомлений, использующихся на фронте. От типа зависит внешний вид уведомления и некоторые другие параметры. Прописаны в resources/js/components/MagicPopup.tsx + 'FRONT_notification_types' => [ + 'info', + 'success', + 'error', + 'attention' + ] ]; diff --git a/resources/css/components/MagicPopupContainer.css b/resources/css/components/MagicPopupContainer.css new file mode 100644 index 0000000..d4634db --- /dev/null +++ b/resources/css/components/MagicPopupContainer.css @@ -0,0 +1,8 @@ +#popup-parent-container{ + padding: 10px; + top: 150px; + z-index: 901; + position: fixed; + left: 50%; + transform: translate(-50%, -50px); +} \ No newline at end of file diff --git a/resources/css/components/magicPopup.css b/resources/css/components/magicPopup.css new file mode 100644 index 0000000..26a43fc --- /dev/null +++ b/resources/css/components/magicPopup.css @@ -0,0 +1,104 @@ +@import url('./../variables.css'); +@import '@fortawesome/fontawesome-free/css/all.css'; + +/* Анимация таймера popup, по истечению которого он скроется */ +@keyframes popupTimer { + from{ + width: 100%; + } + to { + width: 0; + } +} + +.magic-popup-container{ + padding: 5px; + margin-bottom: 15px; + border-radius: 5px; + display: flex; + box-shadow: 0px 3px 4px 1px #918787; + background: var(--color_graphite_main); + width: 500px; + transition: 0.3s; + opacity: 0; + transform: translateY(-10px); + + &.hide{ + opacity: 0; + } + + &.show{ + opacity: 1; + transform: translateY(0px); + } + + &.hidePopup{ + transform: translateY(10px); + opacity: 0; + } + + &>.popup__icon-block{ + flex-basis: 10%; + align-self: center; + + &>i{ + font-size: 4rem; + color: var(--color_purple_main); + + &.fa-circle-check{ + color: var(--color_emerald_main); + } + &.fa-circle-xmark{ + color: var(--color_ruby_main); + } + &.fa-circle-exclamation{ + color: orange; + } + } + } + + &>.popup__content-block{ + flex-basis: 90%; + border-left: 2px solid white; + + &>.popup__content__text-block{ + color: white; + padding: 5px; + text-align: center; + font-size: 1.2rem; + } + + &>.popup__button-block{ + margin: 10px 0; + + &>button{ + font-size: 1rem; + margin: auto; + opacity: 0.5; + background: var(--color_purple_main); + transition: 0.3s; + + &:hover{ + opacity: 1; + } + } + } + + .popup__timer-block__timer{ + + &.popup__timer-block__timer { + height: 5px; + margin: 0 5px; + border-radius: 10px; + + &.timerProgress{ + background: #63707a; + animation: popupTimer; + animation-duration: 3s; + animation-fill-mode: forwards; + animation-timing-function: linear; + } + } + } + } +} diff --git a/resources/js/components/MagicPopup.tsx b/resources/js/components/MagicPopup.tsx new file mode 100644 index 0000000..7825335 --- /dev/null +++ b/resources/js/components/MagicPopup.tsx @@ -0,0 +1,108 @@ +import React, { useEffect, useState } from "react"; +import './../../css/components/magicPopup.css'; +import { Button } from '@SharePoint/rencredit_uikit'; + +/** + * Компонент всплывающего окна + */ + +/** + * Варианты типов для всплывающих окон: info - стандартный вид, error - произошла ошибка, attention - обратить внимание, success - успешное событие + */ +type PopupType = 'info' | 'success' | 'error' | 'attention'; + +//Интерфейс для передаваемых пропсов объекта всплывающего окна +interface MagicPopupProps { + key: number, + id: number, //Уникальный идентификатор, по котором всплывающее окно будет в том числе размонтироваться + message: string, //Текст всплывающего окна + renderIndex: number, //Индекс рендеринга компонента. Так как может отрисовываться сразу несколько popup, чтобы корректно их отрисовывать с анимацией с задержкой по времени, нужно передавать индекс компонента в наборе. Передавать "вручную" не нужно, он сам рассчитывается в компоненте MagicPopupContainer + type: PopupType, //Тип всплывающего окна, определяющий его внешний вид + timeOut?: boolean, //Если передается true, для элемента будет запущен стандартный таймер исчезновения. ВАЖНО! для всплывающих окон типа error таймер не будет активироваться, даже если передается. Пользователь должен гарантированно ознакомиться с сообщением и сам его закрыть + onClose?: (id: number) => void //Колбэк для полного размонтирования компонента со страницы +} + +/** + * Компонент всплывающих окон + * @param props + * @returns + */ +export default function MagicPopup (props: MagicPopupProps) +{ + //Объект с классами для иконок font-awesome в зависимости от переданного типа всплывающего окна. При изменении, необходимо поменять названия классов в magicPopup.css! + const iconNameObj = { + info: 'fa-circle-info', + success: 'fa-circle-check', + error: 'fa-circle-xmark', + attention: 'fa-circle-exclamation' + } + //С помощью деструктуризации указываем значение типа окна по умолчанию + const {message, timeOut = true, onClose} = props; + //Задержка исчезновения popup + const popupDelay = 300; + //Итоговая длительность таймера перед закрытием конкретного popup, учитывая его положение в наборе передаваемых попапов + const hideTimeOut: number = 3000 + popupDelay; + //Стейт для размонтирования попапа + const [popupVisible, setPopupVisible] = useState(true) + //Стейт для плавного сокрытия popup. Класс плавного сокрытия присваивается за пол секунды до полного исчезновения через popupVisible, чтобы сначала плавно сокрыть блок из интерфейса, а затем полностью удалить его из DOM + const [popupHideClass, setPopupHideClass] = useState('popup-visible') + //Стейт плавного появления popup. Каждый следующий popup в передаваемом наборе должен появляться с небольшой задержкой + const [popupShowClass, setPopupFadeinClass] = useState(''); + //Стейт запуска анимированного таймера исчезновения popup + const [popupTimer, setPopupTimer] = useState(''); + //Функция сокрытия popup. Сначала присваиваем класс, который анимированно скрывает popup, а через небольшую задержку размонтируем компонент + function hidePopupBlock (): void + { + setPopupHideClass('hidePopup'); + setTimeout ( () => { + setPopupVisible(false); + }, 300) + //Вызываем метод удаления экземпляра компонента из набора. Так как попапы отрисовываются на основе компонента PopupContainer с набором компонентов MagicPopup, то после их размонтирования, их необходимо и удалить из набора в PopupContainer. Без их удаления, при вызове новых попапов, весь набор с уже просмотренными попапами будет снова отрисовываться. + onClose && onClose(props.id) + }; + + //При рендеринге сразу же запускается таймер сокрытия popup, если аргумент timeOut = true и тип окна != error + useEffect( () => { + if (timeOut && props.type !== 'error') { + setTimeout( () => { + //Задержка для отображения таймера нужна, так как при вызове рендеринга нескольких окон, каждое последующее появляется с задержкой. Без задержки ниже, таймер будет запускаться для всех popup одновременно и когда появится последний popup, его таймер уже может закончиться + setPopupTimer('timerProgress') + }, popupDelay) + //Через небольшую задержку после сокрытия компонента срабатывает таймер на размонтирование компонента и удаления его из набора + setTimeout( () => { + hidePopupBlock() + }, hideTimeOut) + } + //Появление popup при рендеринге + setTimeout ( () => { + setPopupFadeinClass('show') + }, popupDelay) + }, []) + + return ( + popupVisible ? + //
Раз
+
+
+ +
+
+
+ {message} +
+
+
+
+
+
+
+
+ : null + ); +}; diff --git a/resources/js/components/MagicPopupContainer.tsx b/resources/js/components/MagicPopupContainer.tsx new file mode 100644 index 0000000..1f66931 --- /dev/null +++ b/resources/js/components/MagicPopupContainer.tsx @@ -0,0 +1,45 @@ +import React, { useContext, ComponentProps } from "react"; +import MagicPopup from "./MagicPopup"; +import './../../css/components/magicPopupContainer.css'; +import { PopupContext } from "../contexts/PopupContext"; + +/** + * Компонент контейнера для всплывающих окон, который вызывается на странице и куда передается массив всплывающих окон + */ + +//ГАВРИЛОВ. ЛОГИЧНЕЕ ЭКСПОРТИРОВАТЬ ТИП С ПРОПСАМИ КОМПОНЕНТА ИЗ САМОГО КОМПОНЕНТА MAGICPOPUP? +/** + * Дополнительный экспорт типа MagicPopup из компонента всплывающего окна для возможности его импорта, в свою очередь, из компонента, собирающего страницу, например TaxiPage TaxiPage + */ +export type MagicPopupType = ComponentProps; +/** + * Метод рендеринга контейнара для всплывающих окон + * @param {array} magicPopupArr массив объектов типа MagicPopup с информацией о всплывающем окне: текст, таймер и т.д. + * @param {callable} delHiddenPopupFunc колбэк для закрытия конкретного всплывающего окна по его уникальному идентификатору + * @returns ReactNode + */ +export default function MagicPopupContainer () +{ + const popupArr = useContext(PopupContext); + + return ( + //Родительский контейнер для всех всплывающих окон + + ) +} diff --git a/resources/js/contexts/PopupContext.tsx b/resources/js/contexts/PopupContext.tsx new file mode 100644 index 0000000..3368310 --- /dev/null +++ b/resources/js/contexts/PopupContext.tsx @@ -0,0 +1,41 @@ +import React, { createContext, useState } from "react"; +import { MagicPopupType } from "../components/MagicPopupContainer"; +import MagicPopupContainer from "../components/MagicPopupContainer"; + +//гаврилов. передай значением по умолчанию null и спроси у ChatGPT (deepseek не справляется) почему при указании null возникает ошибка в аргументе value +export const PopupContext = createContext(''); + +/** + * Контекст для компонента всплывающих окон + */ +export const PopupProvider = ({ children }) => { + const [popupArrTestVar, setPopupArrTestVar] = useState( [] ); + + function addPopupTest (newPopupArrData: MagicPopupType[]) { + setPopupArrTestVar(prev => { + //Конкатенируем предыдущее состояние набора попапов и новые попапы, присваивая новому попапу параметр id с уникальным рандомным значением + return [...prev, ...newPopupArrData].map(popup => popup.id ? popup : {...popup, id: getRandomId()}) + }); + }; + + //Колбэк для удаления попапа из набора + function delPopupTest (popupDelKey: number) { + setPopupArrTestVar(prev => { + return prev.filter(popup => popup.id !== popupDelKey); + }); + }; + + function getRandomId(): number { return Date.now() - Math.random() }; + + const value = { + popupArrTest: popupArrTestVar, + //ГАВРИЛОВ. переименуй + addPopupArrTest: addPopupTest, + delPopupArrTest: delPopupTest, + }; + + return + {children} + + +}