diff --git a/package.json b/package.json index 5d7b5e24..cfdc226f 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "raleway-cyrillic": "^4.0.2", "ramda": "^0.26.1", "react": "16.8.6", - "react-dom": "16.8.6", + "react-dom": "^16.8.6", "react-grid-layout": "^0.16.6", "react-hot-loader": "^4.1.1", "react-layout-pack": "^0.2.3", diff --git a/src/components/containers/BlurWrapper/index.tsx b/src/components/containers/BlurWrapper/index.tsx new file mode 100644 index 00000000..34c619c6 --- /dev/null +++ b/src/components/containers/BlurWrapper/index.tsx @@ -0,0 +1,13 @@ +import React, { AllHTMLAttributes, FC } from "react"; +import * as styles from "./styles.scss"; + +type IProps = AllHTMLAttributes & { is_blurred: boolean }; + +export const BlurWrapper: FC = ({ children, is_blurred }) => ( +
+ {children} +
+); diff --git a/src/components/containers/BlurWrapper/styles.scss b/src/components/containers/BlurWrapper/styles.scss new file mode 100644 index 00000000..e73deeea --- /dev/null +++ b/src/components/containers/BlurWrapper/styles.scss @@ -0,0 +1,4 @@ +.blur { + filter: blur(0) grayscale(0); + transition: filter 0.25s; +} diff --git a/src/containers/App.tsx b/src/containers/App.tsx index 09a6fd4e..92f47525 100644 --- a/src/containers/App.tsx +++ b/src/containers/App.tsx @@ -13,36 +13,41 @@ import { EditorExample } from "~/containers/examples/EditorExample"; import { HorizontalExample } from "~/containers/examples/HorizontalExample"; import { Sprites } from "~/sprites/Sprites"; import { URLS } from "~/constants/urls"; +import { Modal } from "~/containers/dialogs/Modal"; +import { selectModal } from "~/redux/modal/selectors"; +import { BlurWrapper } from "../components/containers/BlurWrapper/index"; -interface IAppProps {} -interface IAppState {} +const mapStateToProps = selectModal; +const mapDispatchToProps = {}; -class Component extends React.Component { +type IProps = typeof mapDispatchToProps & ReturnType & {}; + +class Component extends React.Component { render() { return ( - - + + + + - - - - - + + + + + - + - - - + + + + ); } } -const mapStateToProps = (state, props) => ({}); -const mapDispatchToProps = dispatch => bindActionCreators({}, dispatch); - export default connect( mapStateToProps, mapDispatchToProps diff --git a/src/containers/dialogs/Modal/index.tsx b/src/containers/dialogs/Modal/index.tsx new file mode 100644 index 00000000..e371e0b0 --- /dev/null +++ b/src/containers/dialogs/Modal/index.tsx @@ -0,0 +1,55 @@ +import React, { Attributes, FC, useCallback } from "react"; +import * as styles from "./styles.scss"; +import { IState } from "~/redux/store"; +import * as ACTIONS from "~/redux/modal/actions"; +import { connect } from "react-redux"; +import { DIALOG_CONTENT } from "~/redux/modal/constants"; +import ReactDOM from "react-dom"; + +const mapStateToProps = ({ modal }: IState) => ({ ...modal }); +const mapDispatchToProps = { + modalSetShown: ACTIONS.modalSetShown, + modalSetDialog: ACTIONS.modalSetDialog, + modalShowDialog: ACTIONS.modalShowDialog +}; + +type IProps = typeof mapDispatchToProps & ReturnType & {}; + +const ModalUnconnected: FC = ({ + modalSetShown, + modalSetDialog, + modalShowDialog, + is_shown, + dialog +}) => { + const onRequestClose = useCallback(() => { + modalSetShown(false); + modalSetDialog(null); + }, [modalSetShown, modalSetDialog]); + + if (!dialog || !DIALOG_CONTENT[dialog] || !is_shown) return null; + + return ReactDOM.createPortal( +
+
+
+
+
+ {React.createElement(DIALOG_CONTENT[dialog], { + onRequestClose, + onDialogChange: modalShowDialog + } as Attributes)} +
+
+
+
, + document.body + ); +}; + +const Modal = connect( + mapStateToProps, + mapDispatchToProps +)(ModalUnconnected); + +export { ModalUnconnected, Modal }; diff --git a/src/containers/dialogs/Modal/styles.scss b/src/containers/dialogs/Modal/styles.scss new file mode 100644 index 00000000..e5b5b5c0 --- /dev/null +++ b/src/containers/dialogs/Modal/styles.scss @@ -0,0 +1,54 @@ +.fixed { + position: fixed; + z-index: 10; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +@keyframes appear { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.content { + position: absolute; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + opacity: 0; + animation: appear 0.25s forwards; +} + +.content_scroller { + width: 100%; + max-width: 100vw; + max-height: 100vh; + overflow: auto; +} + +.content_padder { + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; +} + +.overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: transparentize($color: #000000, $amount: 0.5); + cursor: pointer; +} diff --git a/src/containers/examples/HorizontalExample/styles.scss b/src/containers/examples/HorizontalExample/styles.scss index dfc775c5..9d26e32a 100644 --- a/src/containers/examples/HorizontalExample/styles.scss +++ b/src/containers/examples/HorizontalExample/styles.scss @@ -12,6 +12,7 @@ flex-direction: column; justify-content: center; background: darken($content_bg, 4%); + box-shadow: transparentize(black, 0.7) 0 10px 5px; } .editor { @@ -29,7 +30,6 @@ } .panel { - } .features { @@ -78,7 +78,7 @@ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - @media(max-width: 600px) { + @media (max-width: 600px) { grid-template-columns: repeat(auto-fill, minmax(30vw, 1fr)); } } diff --git a/src/index.tsx b/src/index.tsx index 62a91f27..d5f585fb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,3 +1,6 @@ +/* + sortable grid: http://clauderic.github.io/react-sortable-hoc/#/basic-configuration/grid?_k=hjqdj1 +*/ import * as React from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; diff --git a/src/redux/modal/actions.ts b/src/redux/modal/actions.ts new file mode 100644 index 00000000..ca0a003a --- /dev/null +++ b/src/redux/modal/actions.ts @@ -0,0 +1,17 @@ +import { IModalState } from '~/redux/modal/reducer'; +import { MODAL_ACTIONS } from '~/redux/modal/constants'; + +export const modalSetShown = (is_shown: IModalState['is_shown']) => ({ + is_shown, + type: MODAL_ACTIONS.SET_SHOWN, +}); + +export const modalSetDialog = (dialog: IModalState['dialog']) => ({ + dialog, + type: MODAL_ACTIONS.SET_DIALOG, +}); + +export const modalShowDialog = (dialog: IModalState['dialog']) => ({ + dialog, + type: MODAL_ACTIONS.SHOW_DIALOG, +}); diff --git a/src/redux/modal/constants.ts b/src/redux/modal/constants.ts new file mode 100644 index 00000000..2ae647d7 --- /dev/null +++ b/src/redux/modal/constants.ts @@ -0,0 +1,21 @@ +import { ValueOf } from "~/redux/types"; +import { HorizontalExample } from "~/containers/examples/HorizontalExample"; + +export const MODAL_ACTIONS = { + SET_SHOWN: "MODAL.SET_SHOWN", + SET_DIALOG: "SET_DIALOG", + SHOW_DIALOG: "SHOW_DIALOG" +}; + +export const DIALOGS = { + TEST: "TEST" +}; + +export const DIALOG_CONTENT = { + [DIALOGS.TEST]: HorizontalExample +}; + +export interface IDialogProps { + onRequestClose: () => void; + onDialogChange: (dialog: ValueOf) => void; +} diff --git a/src/redux/modal/handlers.ts b/src/redux/modal/handlers.ts new file mode 100644 index 00000000..6b8ef4ca --- /dev/null +++ b/src/redux/modal/handlers.ts @@ -0,0 +1,11 @@ +import { MODAL_ACTIONS } from '~/redux/modal/constants'; + +const setShown = (state, { is_shown }) => ({ ...state, is_shown }); +const showDialog = (state, { dialog }) => ({ ...state, dialog, is_shown: true }); +const setDialog = (state, { dialog }) => ({ ...state, dialog }); + +export const MODAL_HANDLERS = { + [MODAL_ACTIONS.SET_SHOWN]: setShown, + [MODAL_ACTIONS.SHOW_DIALOG]: showDialog, + [MODAL_ACTIONS.SET_DIALOG]: setDialog, +}; diff --git a/src/redux/modal/reducer.ts b/src/redux/modal/reducer.ts new file mode 100644 index 00000000..9fd3de2c --- /dev/null +++ b/src/redux/modal/reducer.ts @@ -0,0 +1,16 @@ +import { MODAL_HANDLERS } from "~/redux/modal/handlers"; +import { createReducer } from "~/utils/reducer"; +import { DIALOGS } from "~/redux/modal/constants"; +import { ValueOf } from "~/redux/types"; + +export interface IModalState { + is_shown: boolean; + dialog: ValueOf; +} + +const INITIAL_STATE: IModalState = { + is_shown: true, + dialog: DIALOGS.TEST +}; + +export default createReducer(INITIAL_STATE, MODAL_HANDLERS); diff --git a/src/redux/modal/selectors.ts b/src/redux/modal/selectors.ts new file mode 100644 index 00000000..18bfcfa8 --- /dev/null +++ b/src/redux/modal/selectors.ts @@ -0,0 +1,3 @@ +import { IState } from "~/redux/store"; + +export const selectModal = (state: IState) => state.modal; diff --git a/src/redux/store.ts b/src/redux/store.ts index f9d55d38..22ec4e99 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -1,42 +1,49 @@ -import { createStore, applyMiddleware, combineReducers, compose, Store } from 'redux'; +import { createStore, applyMiddleware, combineReducers, compose, Store } from "redux"; -import { persistStore, persistReducer } from 'redux-persist'; -import storage from 'redux-persist/lib/storage'; -import createSagaMiddleware from 'redux-saga'; -import { connectRouter } from 'connected-react-router' -import userReducer from '~/redux/user/reducer'; -import userSaga from '~/redux/user/sagas'; -import { createBrowserHistory } from 'history'; +import { persistStore, persistReducer } from "redux-persist"; +import storage from "redux-persist/lib/storage"; +import createSagaMiddleware from "redux-saga"; +import { connectRouter, RouterState } from "connected-react-router"; +import { createBrowserHistory } from "history"; import { PersistConfig, Persistor } from "redux-persist/es/types"; -import { routerMiddleware } from 'connected-react-router' +import { routerMiddleware } from "connected-react-router"; + +import userReducer, { IUserState } from "~/redux/user/reducer"; +import userSaga from "~/redux/user/sagas"; + +import modalReducer, { IModalState } from "~/redux/modal/reducer"; +import { IState } from "~/redux/store"; const userPersistConfig: PersistConfig = { - key: 'user', - whitelist: ['profile'], - storage, + key: "user", + whitelist: ["profile"], + storage }; +export interface IState { + user: IUserState; + modal: IModalState; + router: RouterState; +} + export const sagaMiddleware = createSagaMiddleware(); export const history = createBrowserHistory(); const composeEnhancers = - typeof window === 'object' && - (window).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ + typeof window === "object" && (window).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? (window).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose; export const store = createStore( combineReducers({ user: persistReducer(userPersistConfig, userReducer), - router: connectRouter(history), + modal: modalReducer, + router: connectRouter(history) }), - composeEnhancers(applyMiddleware( - routerMiddleware(history), - sagaMiddleware - )) + composeEnhancers(applyMiddleware(routerMiddleware(history), sagaMiddleware)) ); -export function configureStore(): { store: Store, persistor: Persistor } { +export function configureStore(): { store: Store; persistor: Persistor } { sagaMiddleware.run(userSaga); const persistor = persistStore(store); diff --git a/src/redux/types.ts b/src/redux/types.ts index 0cd935f0..f90f4c5b 100644 --- a/src/redux/types.ts +++ b/src/redux/types.ts @@ -2,10 +2,13 @@ import { DetailedHTMLProps, InputHTMLAttributes } from "react"; export type ITag = { title: string; - feature?: 'red' | 'blue' | 'green' | 'olive' | 'black'; -} + feature?: "red" | "blue" | "green" | "olive" | "black"; +}; -export type IInputTextProps = DetailedHTMLProps, HTMLInputElement> & { +export type IInputTextProps = DetailedHTMLProps< + InputHTMLAttributes, + HTMLInputElement +> & { wrapperClassName?: string; handler?: (value: string) => void; required?: boolean; @@ -20,3 +23,5 @@ export type IInputTextProps = DetailedHTMLProps = T[keyof T]; diff --git a/src/redux/user/reducer.ts b/src/redux/user/reducer.ts index 883c7dcc..ac013cee 100644 --- a/src/redux/user/reducer.ts +++ b/src/redux/user/reducer.ts @@ -1,27 +1,27 @@ -import { createReducer } from 'reduxsauce'; import * as ActionCreators from "~/redux/user/actions"; import { USER_ACTIONS } from "~/redux/user/constants"; +import { createReducer } from "~/utils/reducer"; export interface IUserProfile { - id: number, - username: string, - email: string, - role: string, - token: string, + id: number; + username: string; + email: string; + role: string; + token: string; - is_activated: boolean, - is_user: boolean, + is_activated: boolean; + is_user: boolean; } export interface IUserFormStateLogin { - error: string, + error: string; } export type IUserState = Readonly<{ - profile: IUserProfile, + profile: IUserProfile; form_state: { - login: IUserFormStateLogin, - }, + login: IUserFormStateLogin; + }; }>; type UnsafeReturnType = T extends (...args: any[]) => infer R ? R : any; @@ -29,13 +29,16 @@ interface ActionHandler { (state: IUserState, payload: UnsafeReturnType): IUserState; } -const setLoginErrorHandler: ActionHandler = (state, { error }) => ({ +const setLoginErrorHandler: ActionHandler = ( + state, + { error } +) => ({ ...state, form_state: { ...state.form_state, login: { ...state.form_state.login, - error, + error } } }); @@ -44,28 +47,28 @@ const setUserHandler: ActionHandler = (state, ...state, profile: { ...state.profile, - ...profile, + ...profile } }); const HANDLERS = { [USER_ACTIONS.SET_LOGIN_ERROR]: setLoginErrorHandler, - [USER_ACTIONS.SET_USER]: setUserHandler, + [USER_ACTIONS.SET_USER]: setUserHandler }; const INITIAL_STATE: IUserState = { profile: { id: 0, - username: '', - email: '', - role: '', - token: '', + username: "", + email: "", + role: "", + token: "", is_activated: false, - is_user: false, + is_user: false }, form_state: { login: { - error: '', + error: "" } } }; diff --git a/src/utils/reducer.ts b/src/utils/reducer.ts new file mode 100644 index 00000000..59ce597f --- /dev/null +++ b/src/utils/reducer.ts @@ -0,0 +1,19 @@ +// create-reducer.ts +import { Action } from 'redux'; + +type Handlers> = { + readonly [Type in Types]: (state: State, action: Actions) => State +} + +// export const createReducer = >( +// initialState: State, +// handlers: Handlers, +// ) => (state = initialState, action: Actions) => +// handlers.hasOwnProperty(action.type) ? handlers[action.type as Types](state, action) : state; + +export const createReducer = ( + initialState, + handlers, +) => (state = initialState, action) => handlers.hasOwnProperty(action.type) + ? handlers[action.type](state, action) + : state;