diff --git a/.eslintrc b/.eslintrc index a25f3b78..ebfc2883 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,9 +4,10 @@ "airbnb/rules/react", ], "plugins": [ + "@typescript-eslint", "simple-import-sort" ], - "parser": "babel-eslint", + "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 2018, "sourceType": "module" @@ -27,10 +28,24 @@ "import/prefer-default-export": "warn", "import/named": "warn", "import/no-named-as-default": "off", - "arrow-parens": 0, + "import/extensions": [ + "error", + "ignorePackages", + { + "js": "never", + "jsx": "never", + "ts": "never", + "tsx": "never" + } + ], + "@typescript-eslint/type-annotation-spacing": ["warn", { "before": false, "after": true, "overrides": { "colon": { "before": true, "after": true }, "arrow": { "before": true, "after": true } } }], + 'arrow-parens': ['error', 'as-needed'], "no-nested-ternary": "off", - "no-unused-vars": "warn", - "no-unused-expressions": "warn", + "no-unused-expressions": "off", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "off", + "no-useless-constructor": "off", + "@typescript-eslint/no-useless-constructor": "warn", "space-before-function-paren": ["warn", "always"], "no-spaced-func": "off", "func-call-spacing": ["warn", "always"], @@ -46,6 +61,7 @@ "no-trailing-spaces": "warn", "indent": ["warn", 2], "semi": ["warn", "always"], + "no-param-reassign": ["warn", { "props": true, "ignorePropertyModificationsFor": ["draft"] }], "simple-import-sort/sort": [ 1, { @@ -90,8 +106,13 @@ "settings": { "import/resolver": { "node": { - "moduleDirectory": ["node_modules", "src", "src/components"] + "moduleDirectory": ["node_modules", "src", "src/components"], + "extensions": [".js",".jsx",".ts",".tsx"] } + }, + "import/extensions": [".js",".jsx",".ts",".tsx"], + "import/parsers": { + "@typescript-eslint/parser": [".ts",".tsx"] } }, "overrides": [ diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index 877bc3d5..00000000 --- a/jsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "baseUrl": "src" -} \ No newline at end of file diff --git a/package.json b/package.json index 849be248..bc75302c 100644 --- a/package.json +++ b/package.json @@ -4,16 +4,16 @@ "version": "0.1.0", "private": false, "scripts": { - "env:path": "cross-env NODE_PATH=src:src/components", - "start": "cross-env NODE_PATH=src:src/components react-scripts start", - "build": "$npm_execpath run env:path react-scripts build", - "test": "$npm_execpath run env:path react-scripts test", + "start": "react-scripts start", + "build": "react-scripts build", + "analyze": "source-map-explorer 'build/static/js/*.js'", + "test": "react-scripts test", "eject": "react-scripts eject", - "storybook": "$npm_execpath run env:path start-storybook -p 9009 -s public", - "build-storybook": "$npm_execpath run env:path build-storybook -s public", + "storybook": "start-storybook -p 9009 -s public", + "build-storybook": "build-storybook -s public", "postbuild": "gzipper --verbose --brotli ./build && gzipper --verbose ./build && ./tools/moveBuildFolder.sh", "generate": "python tools/generate-component.py", - "lint:css": "stylelint './src/**/*.js'" + "lint:css": "stylelint './src/**/*.js' './src/**/*.ts*'" }, "husky": { "hooks": { @@ -21,7 +21,7 @@ } }, "lint-staged": { - "src/**/*.{js,jsx,json,scss}": [ + "src/**/*.{js,jsx,ts,tsx,json,scss}": [ "eslint --fix", "stylelint", "git add" @@ -50,6 +50,17 @@ "@material-ui/icons": "^4.2.1", "@material-ui/pickers": "^3.2.9", "@sparcs-kaist/react-grid-layout": "^0.18.0", + "@types/jest": "^25.1.4", + "@types/jsonwebtoken": "^8.3.8", + "@types/lodash": "^4.14.149", + "@types/node": "^13.9.1", + "@types/react": "^16.9.23", + "@types/react-dom": "^16.9.5", + "@types/react-helmet": "^5.0.15", + "@types/react-redux": "^7.1.7", + "@types/react-router-dom": "^5.1.3", + "@types/redux-actions": "^2.6.1", + "@types/styled-components": "^5.0.1", "animate.css": "^3.7.2", "awesome-debounce-promise": "^2.1.0", "axios": "^0.19.0", @@ -69,9 +80,11 @@ "immer": "^5.1.0", "immutable": "^4.0.0-rc.12", "jsonwebtoken": "^8.5.1", + "lodash": "^4.17.15", "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", "lodash.isequal": "^4.5.0", + "lodash.set": "^4.3.2", "lodash.throttle": "^4.1.1", "lodash.uniq": "^4.5.0", "lodash.uniqby": "^4.7.0", @@ -144,11 +157,12 @@ "prettier": "^1.19.1", "regenerator-runtime": "^0.13.3", "require-context.macro": "^1.0.4", + "source-map-explorer": "^2.3.1", "stylelint": "^12.0.1", "stylelint-config-recommended": "^3.0.0", "stylelint-config-styled-components": "^0.1.1", "stylelint-processor-styled-components": "^1.9.0", - "typescript": "^3.7.5" + "typescript": "^3.8.3" }, "license": "MIT" } diff --git a/public/main.js b/public/main.js new file mode 100644 index 00000000..cfeb3b11 --- /dev/null +++ b/public/main.js @@ -0,0 +1,44 @@ +// Modules to control application life and create native browser window +const {app, BrowserWindow} = require('electron') +const path = require('path') + +function createWindow () { + // Create the browser window. + const mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js') + }, + frame: false, + }) + + // and load the index.html of the app. + mainWindow.loadFile(`file://${path.join (__dirname, '../deploy/index.html')}`) + + // Open the DevTools. + // mainWindow.webContents.openDevTools() +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(() => { + createWindow() + + app.on('activate', function () { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') app.quit() +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/src/App.js b/src/App.tsx similarity index 100% rename from src/App.js rename to src/App.tsx diff --git a/src/boot.js b/src/boot.js index 848222ad..93a71a08 100644 --- a/src/boot.js +++ b/src/boot.js @@ -1,12 +1,15 @@ import 'devtools-detect'; +import moment from 'moment'; + import store from 'store'; import { checkAuth } from 'store/reducers/auth'; import { initialize } from 'store/reducers/upload'; import axios from 'lib/axios'; -import serializer from 'lib/immutable'; import storage from 'lib/storage'; +import { parseJSON } from './lib/utils'; + const pwaInstallPromptListener = () => { window.addEventListener ('beforeinstallprompt', e => { // Prevent Chrome 67 and earlier from automatically showing the prompt @@ -45,11 +48,16 @@ export default () => { throttle ('scroll', 'optimizedScroll'); }) ()); + pwaInstallPromptListener (); - const uploadPersist = storage.getItem ('uploadPersist', false); + const uploadPersist = storage.getItem ('uploadPersist'); if (uploadPersist) { - store.dispatch (initialize (serializer.parse (uploadPersist))); + if (uploadPersist.date) { + store.dispatch (initialize (uploadPersist)); + } else { + storage.removeItem ('uploadPersist'); + } } const token = storage.getItem ('token'); @@ -61,6 +69,7 @@ export default () => { }; window.addEventListener ('devtoolschange', () => { + if (process.env.NODE_ENV !== 'production') return; import ('static/images/recruitAscii').then (asciiArt => { // eslint-disable-next-line no-console console.log (asciiArt.default); diff --git a/src/components/atoms/Button/Button.js b/src/components/atoms/Button/Button.js index fce2d6ab..5020a85c 100644 --- a/src/components/atoms/Button/Button.js +++ b/src/components/atoms/Button/Button.js @@ -9,7 +9,7 @@ const Button = ({ }) => ( { + onClick={event => { if (typeof onClick === 'function') onClick (event); if (to) history.push (to); }} diff --git a/src/components/atoms/InputBase/InputBase.js b/src/components/atoms/InputBase/InputBase.js index 3189c0d6..7601e577 100644 --- a/src/components/atoms/InputBase/InputBase.js +++ b/src/components/atoms/InputBase/InputBase.js @@ -14,7 +14,7 @@ const useStyles = makeStyles ({ }, }); -const InputBase = (props) => { +const InputBase = props => { const classes = useStyles (); return ; }; diff --git a/src/components/atoms/Modal/Modal.js b/src/components/atoms/Modal/Modal.js index f3508a29..7b87ec47 100644 --- a/src/components/atoms/Modal/Modal.js +++ b/src/components/atoms/Modal/Modal.js @@ -38,7 +38,7 @@ const ModalWrapper = styled.div` justify-content: center; `; -const Modal = (props) => ( +const Modal = props => ( {props.children} diff --git a/src/components/atoms/TwoCol/TwoCol.js b/src/components/atoms/TwoCol/TwoCol.js index f5f09af5..01512799 100644 --- a/src/components/atoms/TwoCol/TwoCol.js +++ b/src/components/atoms/TwoCol/TwoCol.js @@ -7,6 +7,8 @@ const colMixin = css` display: flex; flex-direction: column; flex: ${props => props.flex} 0 0; + flex-shrink: 0; + flex-basis: auto; min-width: 0; ${media.tablet (css` flex: ${props => props.flex}; @@ -39,10 +41,11 @@ const TwoCol = styled.section` display: flex; width: 100%; flex-wrap: wrap; + flex-shrink: 0; + flex-basis: auto; ${props => (props.mobileWrap ? css` flex-direction: column; - ` : css` - `)}; + ` : '')}; ${media.tablet (css` flex-direction: row; diff --git a/src/components/containers/ChannelTalk.js b/src/components/containers/ChannelTalk.tsx similarity index 64% rename from src/components/containers/ChannelTalk.js rename to src/components/containers/ChannelTalk.tsx index 91fd419a..f4fda391 100644 --- a/src/components/containers/ChannelTalk.js +++ b/src/components/containers/ChannelTalk.tsx @@ -1,29 +1,21 @@ -import { useEffect, useMemo } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useEffect } from 'react'; +import { RouteComponentProps, useLocation } from 'react-router-dom'; import { useSelector } from 'react-redux'; +import { get } from 'lodash'; import queryString from 'query-string'; +import { IState } from 'types/store.d'; import ChannelService from 'lib/channel_io'; import { isAuthedSelector } from 'lib/utils'; -const ChannelTalk = ({ match }) => { +const ChannelTalk = ({ match } : RouteComponentProps<{top : string}>) => { const { search, pathname } = useLocation (); const { top } = match.params; const isAuthenticated = useSelector (isAuthedSelector); - const infoImmutable = useSelector (state => state.getIn (['auth', 'info'])); - const info = useMemo (() => infoImmutable.toJS (), [infoImmutable]); - const { - _id, - username, - koreanName, - groups, - flags, - email, - profilePhoto, - } = info; + const info = useSelector ((state : IState) => get (state, ['auth', 'info'])); useEffect (() => { - // if (process.env.NODE_ENV !== 'production') return; + if (process.env.NODE_ENV !== 'production') return; if (top === 'settings') { ChannelService.shutdown (); return; @@ -38,7 +30,16 @@ const ChannelTalk = ({ match }) => { pluginKey: '5fe8c634-bcbd-4499-ba99-967191a2ef77', }; if (isAuthenticated) { - if (!_id) return; + if (!info) return; + const { + _id, + username, + koreanName, + groups, + flags, + email, + profilePhoto, + } = info; Object.assign (settings, { userId: _id, profile: { @@ -52,7 +53,7 @@ const ChannelTalk = ({ match }) => { }); } ChannelService.boot (settings); - }, [search, isAuthenticated, infoImmutable, top, pathname]); + }, [search, isAuthenticated, info, top, pathname]); return null; }; diff --git a/src/components/containers/ScrollToTop.js b/src/components/containers/ScrollToTop.tsx similarity index 87% rename from src/components/containers/ScrollToTop.js rename to src/components/containers/ScrollToTop.tsx index c917cbcb..62dda51d 100644 --- a/src/components/containers/ScrollToTop.js +++ b/src/components/containers/ScrollToTop.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import PropTypes from 'prop-types'; import { useLocation, useParams } from 'react-router-dom'; -const ScrollToTop = ({ updateWithPath }) => { +const ScrollToTop = ({ updateWithPath } : { updateWithPath : boolean }) => { const { route } = useParams (); const { pathname } = useLocation (); useEffect (() => { diff --git a/src/components/containers/WindowResizeListener.js b/src/components/containers/WindowResizeListener.tsx similarity index 100% rename from src/components/containers/WindowResizeListener.js rename to src/components/containers/WindowResizeListener.tsx diff --git a/src/components/organisms/AuthCallback/AuthCallback.js b/src/components/organisms/AuthCallback/AuthCallback.js index d3ce2582..c119340b 100644 --- a/src/components/organisms/AuthCallback/AuthCallback.js +++ b/src/components/organisms/AuthCallback/AuthCallback.js @@ -11,7 +11,7 @@ const AuthCallback = ({ location, history }) => { const { code, state } = queryString.parse (location.search); if (code && state) { dispatch (loginCallback (code, state)) - .then ((res) => { + .then (res => { const referrer = storage.getItem ('referrer'); if (referrer) { storage.removeItem ('referrer'); @@ -20,7 +20,7 @@ const AuthCallback = ({ location, history }) => { } history.replace (`/${res.user.username}`); }) - .catch ((error) => { + .catch (error => { alert (error.message); history.replace ('/'); }); diff --git a/src/components/organisms/GroupBox/GroupBoxP.js b/src/components/organisms/GroupBox/GroupBoxP.js index 5ab71cbb..f9646162 100644 --- a/src/components/organisms/GroupBox/GroupBoxP.js +++ b/src/components/organisms/GroupBox/GroupBoxP.js @@ -14,7 +14,7 @@ const GroupBox = ({ group, ...props }) => { const { name, profilePhoto, zabosCount, followersCount, recentUpload, isPending, } = group; - const timePast = recentUpload ? getLabeledTimeDiff (recentUpload, true, true, true, true, true, true) : '없음'; + const timePast = recentUpload ? getLabeledTimeDiff (recentUpload, 60, 60, 24, 7, 5, 12) : '없음'; const stats = [{ name: '올린 자보', value: zabosCount, diff --git a/src/components/organisms/GroupBox/GroupBoxS.js b/src/components/organisms/GroupBox/GroupBoxS.js index 0860cd0e..52d9a867 100644 --- a/src/components/organisms/GroupBox/GroupBoxS.js +++ b/src/components/organisms/GroupBox/GroupBoxS.js @@ -1,5 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; +import get from 'lodash.get'; import SuperTooltip from 'atoms/SuperTooltip'; @@ -16,7 +17,7 @@ const GroupBoxS = ({ group, ...props }) => { const { name, profilePhoto, subtitle, } = group; - const width = useSelector (state => state.getIn (['app', 'windowSize', 'width'])); + const width = useSelector (state => get (state, ['app', 'windowSize', 'width'])); const nameRef = useRef (null); const [showTooltip, setShowTooltip] = useState (false); useEffect (() => { setShowTooltip (isElemWidthOverflown (nameRef.current)); }, [nameRef, width]); diff --git a/src/components/organisms/List/List.js b/src/components/organisms/List/List.js index e36b4712..01ce139c 100644 --- a/src/components/organisms/List/List.js +++ b/src/components/organisms/List/List.js @@ -20,7 +20,7 @@ List.propTypes = { }; List.defaultProps = { - renderItem: (item) =>
{item}
, + renderItem: item =>
{item}
, }; export default List; diff --git a/src/components/organisms/ZaboCard/ZaboCardL.js b/src/components/organisms/ZaboCard/ZaboCardL.js index 4bcd6a77..9b3272d5 100644 --- a/src/components/organisms/ZaboCard/ZaboCardL.js +++ b/src/components/organisms/ZaboCard/ZaboCardL.js @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { Link } from 'react-router-dom'; import { useSelector } from 'react-redux'; import Tooltip from '@material-ui/core/Tooltip'; +import get from 'lodash.get'; import moment from 'moment'; import { CategoryListW, CategoryW } from 'atoms/Category'; @@ -43,7 +44,7 @@ const ZaboCardL = ({ zabo }) => { const { _id, category, title, owner, createdAt, views, effectiveViews, } = zabo; - const width = useSelector (state => state.getIn (['app', 'windowSize', 'width'])); + const width = useSelector (state => get (state, ['app', 'windowSize', 'width'])); const titleRef = useRef (null); const [showTooltip, setShowTooltip] = useState (false); useEffect (() => { setShowTooltip (isElemWidthOverflown (titleRef.current)); }, [width, titleRef]); diff --git a/src/components/organisms/ZaboCard/ZaboCardM.js b/src/components/organisms/ZaboCard/ZaboCardM.js index faacf126..b93b354a 100644 --- a/src/components/organisms/ZaboCard/ZaboCardM.js +++ b/src/components/organisms/ZaboCard/ZaboCardM.js @@ -60,7 +60,7 @@ const Writing = ({ zabo }) => { likesCount, isLiked, pinsCount, isPinned, } = zabo; - const timePast = getLabeledTimeDiff (createdAt, true, true, true, true, false, false); + const timePast = getLabeledTimeDiff (createdAt, 60, 60, 24, 7, 5, 0); return ( diff --git a/src/components/pages/AdminPage/Dashboard.js b/src/components/pages/AdminPage/Dashboard.js index b5eee880..10223345 100755 --- a/src/components/pages/AdminPage/Dashboard.js +++ b/src/components/pages/AdminPage/Dashboard.js @@ -60,7 +60,7 @@ for (let i = 0; i < 7; i++) { today.setDate (today.getDate () - 1); } -const parseData = (data) => { +const parseData = data => { const cnt = [0, 0, 0, 0, 0, 0, 0]; data.forEach (({ _id, createdAt }) => { const pos = new Date (); diff --git a/src/components/pages/AdminPage/GroupAdminPage.js b/src/components/pages/AdminPage/GroupAdminPage.js index 6975cdeb..7cec0a66 100644 --- a/src/components/pages/AdminPage/GroupAdminPage.js +++ b/src/components/pages/AdminPage/GroupAdminPage.js @@ -17,6 +17,7 @@ import Typography from '@material-ui/core/Typography'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import clsx from 'clsx'; +import get from 'lodash.get'; import moment from 'moment'; import StyledQuill from 'organisms/StyledQuill'; @@ -144,10 +145,8 @@ const GroupItem = ({ group, isPending }) => { }; const GroupAdminPage = () => { - const pendingGroupsImmutable = useSelector (state => state.getIn (['admin', 'pendingGroups'])); - const pendingGroups = useMemo (() => pendingGroupsImmutable.toJS (), [pendingGroupsImmutable]); - const groupsImmutable = useSelector (state => state.getIn (['admin', 'groups'])); - const groups = useMemo (() => groupsImmutable.toJS (), [groupsImmutable]); + const pendingGroups = useSelector (state => get (state, ['admin', 'pendingGroups'], [])); + const groups = useSelector (state => get (state, ['admin', 'groups'], [])); return (
diff --git a/src/components/pages/AdminPage/GroupDetailPage.js b/src/components/pages/AdminPage/GroupDetailPage.js index ef0f196f..2ff56d31 100644 --- a/src/components/pages/AdminPage/GroupDetailPage.js +++ b/src/components/pages/AdminPage/GroupDetailPage.js @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; -import Tooltip from '@material-ui/core/Tooltip'; +import get from 'lodash.get'; import SuperTooltip from 'atoms/SuperTooltip'; import StyledQuill from 'organisms/StyledQuill'; @@ -13,8 +13,7 @@ import UserCard from './UserCard'; const GroupDetailPage = ({ match }) => { const { name } = match.params; - const groupIm = useSelector (state => state.getIn (['admin', 'groupsMap', name])); - const group = useMemo (() => (groupIm ? groupIm.toJS () : null), [groupIm]); + const group = useSelector (state => get (state, ['admin', 'groupsMap', name])); if (!group) return null; const { profilePhoto, members, recentUpload, description, subtitle, diff --git a/src/components/pages/AdminPage/UserAdminPage.js b/src/components/pages/AdminPage/UserAdminPage.js index f1dc5686..f4fd78eb 100644 --- a/src/components/pages/AdminPage/UserAdminPage.js +++ b/src/components/pages/AdminPage/UserAdminPage.js @@ -1,15 +1,12 @@ -import React, { - useMemo, -} from 'react'; +import React from 'react'; import { useSelector } from 'react-redux'; +import get from 'lodash.get'; import GridContainer from './components/Grid/GridContainer'; -import UserCard from './UserCard'; import UserList from './UsersTable'; const UserAdminPage = () => { - const usersIm = useSelector (state => state.getIn (['admin', 'users'])); - const users = useMemo (() => usersIm.toJS (), [usersIm]); + const users = useSelector (state => get (state, ['admin', 'users'], [])); return (
diff --git a/src/components/pages/AdminPage/UserDetailPage.js b/src/components/pages/AdminPage/UserDetailPage.js index b757f824..6d95863a 100644 --- a/src/components/pages/AdminPage/UserDetailPage.js +++ b/src/components/pages/AdminPage/UserDetailPage.js @@ -1,5 +1,6 @@ import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; +import get from 'lodash.get'; import SuperTooltip from 'atoms/SuperTooltip'; import Header from 'templates/Header'; @@ -11,8 +12,7 @@ import UserInfo from './UserInfo'; const UserDetailPage = ({ match }) => { const { username } = match.params; - const userIm = useSelector (state => state.getIn (['admin', 'usersMap', username])); - const user = useMemo (() => (userIm ? userIm.toJS () : null), [userIm]); + const user = useSelector (state => get (state, ['admin', 'usersMap', username])); if (!user) return null; const { _id, profilePhoto, createdAt, groups, diff --git a/src/components/pages/AdminPage/UsersTable.js b/src/components/pages/AdminPage/UsersTable.js index 42cc3456..d478f25b 100644 --- a/src/components/pages/AdminPage/UsersTable.js +++ b/src/components/pages/AdminPage/UsersTable.js @@ -56,7 +56,6 @@ export default function UserList ({ users }) { ], data: users, }); - return ( ({ - zaboList: state.getIn (['zabo', 'zaboList']), + zaboList: get (state, ['zabo', 'zaboList']), }); // HomePage 에서 변경 사항이 생긴다면 널리 알려라. @@ -25,4 +24,4 @@ const mapDispatchToProps = { }; // index.js 가 HomePage 를 import 하는 것이 아닌, HomePage -export default connect (mapStateToProps, mapDispatchToProps) (toJS (HomePageContainer)); +export default connect (mapStateToProps, mapDispatchToProps) (HomePageContainer); diff --git a/src/components/pages/LandingPage/LandingPage.js b/src/components/pages/LandingPage/LandingPage.js index cc068662..8b4a0cdd 100644 --- a/src/components/pages/LandingPage/LandingPage.js +++ b/src/components/pages/LandingPage/LandingPage.js @@ -6,6 +6,7 @@ import { Link, useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import Slider from 'react-slick'; import Tooltip from '@material-ui/core/Tooltip'; +import get from 'lodash.get'; import moment from 'moment'; import useSWR from 'swr'; @@ -147,10 +148,10 @@ const CategoryBanner = () => ( ); -const ArrowLeft = (props) => ( +const ArrowLeft = props => ( ); -const ArrowRight = (props) => ( +const ArrowRight = props => ( ); @@ -172,7 +173,7 @@ const SlickItem = ({ zabo, width }) => ( ); -const twoDigits = (number) => { +const twoDigits = number => { if (number >= 0 && number < 10) return `0${number}`; return number; }; @@ -198,7 +199,7 @@ const CountDown = ({ initialValue }) => { const Upcoming = () => { const history = useHistory (); const [current, setCurrent] = useState (0); - const width = useSelector (state => state.getIn (['app', 'windowSize', 'width'])); + const width = useSelector (state => get (state, ['app', 'windowSize', 'width'])); const { data: zabos, zaboError } = useSWR ('/zabo/list/deadline', getDeadlineZaboList, swrOpts); if (!zabos) { diff --git a/src/components/pages/LandingPage/LandingPage.styled.js b/src/components/pages/LandingPage/LandingPage.styled.js index d0dd9fcd..3d5e9535 100644 --- a/src/components/pages/LandingPage/LandingPage.styled.js +++ b/src/components/pages/LandingPage/LandingPage.styled.js @@ -250,7 +250,7 @@ export const UpcomingW = styled.section` background: ${props => props.theme.gray90}; color: ${props => props.theme.gray1}; width: 100%; - height: 200px; + /* height: 200px; */ padding: 36px 0; ${Container} { /* padding: 60px 24px 12px 24px; */ @@ -394,8 +394,11 @@ UpcomingW.Image = styled.img` `; UpcomingW.NoMagamImBak = styled.div` - font-size: 24px; + font-size: 16px; font-weight: 500; + ${media.tablet (css` + font-size: 24px; + `)}; `; export const RecommendsW = styled.section` diff --git a/src/components/pages/NotFound/NotFound.js b/src/components/pages/NotFound/NotFound.js index 66b6b382..67a36f77 100644 --- a/src/components/pages/NotFound/NotFound.js +++ b/src/components/pages/NotFound/NotFound.js @@ -1,5 +1,27 @@ import React from 'react'; +import { NavLink } from 'react-router-dom'; -const NotFound = () =>

Not Found

; +import notFoundImage from 'static/images/notFoundImage.jpg'; +import logo from 'static/logo/logo.svg'; + +import { Page } from './NotFound.styled'; + +const NotFound = () => ( + + + + logo + + + + + + 요청하신 페이지를 찾을 수 없습니다.
+ 메인페이지 + 로 이동하기 +
+
+
+); export default NotFound; diff --git a/src/components/pages/NotFound/NotFound.styled.js b/src/components/pages/NotFound/NotFound.styled.js new file mode 100644 index 00000000..4c16a04f --- /dev/null +++ b/src/components/pages/NotFound/NotFound.styled.js @@ -0,0 +1,55 @@ +import styled, { css } from 'styled-components'; + +import { media } from 'lib/utils/style'; + +export const Page = styled.section` + width: 100%; + height: 100%; +`; + +Page.Header = styled.div` + position: fixed; + top: 0; + width: 100%; + height: 55px; + border-top: 5px solid ${props => props.theme.main}; + padding: 10px 18px; +`; + +Page.Body = styled.div` + width: 90vw; + margin: 240px 5vw 0; + ${media.tablet (css` + width: 34vw; + margin: 280px 33vw 0; + `)}; +`; + +Page.Title = styled.img` + width: 60vw; + margin: 0 15vw; + ${media.tablet (css` + width: 34vw; + margin: 0; + `)}; +`; + +Page.Description = styled.p` + text-align: center; + margin-top: 28px; + line-height: 1.6; + font-size: 16px; + color: ${props => props.theme.gray30}; + ${media.tablet (css` + margin-top: 48px; + font-size: 18px; + `)}; +`; + +Page.Description.Link = styled.a` + color: ${props => props.theme.main} !important; + font-weight: bold; + text-decoration: underline; +`; + +export default Page; diff --git a/src/components/pages/ProfilePage/GroupProfilePage.js b/src/components/pages/ProfilePage/GroupProfilePage.js index b62039dc..2d106d0d 100644 --- a/src/components/pages/ProfilePage/GroupProfilePage.js +++ b/src/components/pages/ProfilePage/GroupProfilePage.js @@ -4,7 +4,7 @@ import React, { } from 'react'; import { Link, useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; -import SettingsIcon from '@material-ui/icons/Settings'; +import get from 'lodash.get'; import SuperTooltip from 'atoms/SuperTooltip'; import ProfileStats from 'organisms/ProfileStats'; @@ -30,7 +30,7 @@ const GroupProfile = ({ profile }) => { const dispatch = useDispatch (); const history = useHistory (); const isAuthed = useSelector (isAuthedSelector); - const width = useSelector (state => state.getIn (['app', 'windowSize', 'width'])); + const width = useSelector (state => get (state, ['app', 'windowSize', 'width'])); const isMobile = useMemo (() => mediaSizes.tablet > width, [width]); const descRef = useRef (null); @@ -40,7 +40,7 @@ const GroupProfile = ({ profile }) => { }, [name]); useEffect (() => { setShowTooltip (isElemWidthOverflown (descRef.current)); }, [descRef, width]); - const timePast = recentUpload ? getLabeledTimeDiff (recentUpload, true, true, true, true, true, true) : '없음'; + const timePast = recentUpload ? getLabeledTimeDiff (recentUpload, 60, 60, 24, 7, 5, 12) : '없음'; const stats = [{ name: '올린 자보', value: zabosCount, diff --git a/src/components/pages/ProfilePage/ProfilePage.js b/src/components/pages/ProfilePage/ProfilePage.js index d61855fa..5266dfca 100644 --- a/src/components/pages/ProfilePage/ProfilePage.js +++ b/src/components/pages/ProfilePage/ProfilePage.js @@ -1,6 +1,7 @@ import React, { useEffect, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import get from 'lodash.get'; import { NotFound } from 'pages'; @@ -16,8 +17,7 @@ const ProfilePage = () => { useEffect (() => { dispatch (getProfile (name)).catch (error => {}); }, [name]); - const profileImmutable = useSelector (state => state.getIn (['profile', 'profiles', name])); - const profile = useMemo (() => (profileImmutable ? profileImmutable.toJS () : null), [profileImmutable]); + const profile = useSelector (state => get (state, ['profile', 'profiles', name])); if (!profile) return null; if (profile.error) return ; if (profile.username === name) return ; diff --git a/src/components/pages/ProfilePage/UserProfilePage.js b/src/components/pages/ProfilePage/UserProfilePage.js index f95d2ab2..1821291c 100644 --- a/src/components/pages/ProfilePage/UserProfilePage.js +++ b/src/components/pages/ProfilePage/UserProfilePage.js @@ -3,6 +3,7 @@ import React, { useRef, useState, } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import get from 'lodash.get'; import Button from 'atoms/Button'; import SuperTooltip from 'atoms/SuperTooltip'; @@ -26,11 +27,10 @@ const UserProfile = ({ profile }) => { const { username, profilePhoto, groups, description, boards, stats: { likesCount, followingsCount } = {}, following, } = profile; - const width = useSelector (state => state.getIn (['app', 'windowSize', 'width'])); + const width = useSelector (state => get (state, ['app', 'windowSize', 'width'])); const dispatch = useDispatch (); - const myUsername = useSelector (state => state.getIn (['auth', 'info', 'username'])); - const pendingGroupsImmutable = useSelector (state => state.getIn (['auth', 'info', 'pendingGroups'])); - const pendingGroups = useMemo (() => pendingGroupsImmutable.toJS (), [pendingGroupsImmutable]); + const myUsername = useSelector (state => get (state, ['auth', 'info', 'username'])); + const pendingGroups = useSelector (state => get (state, ['auth', 'info', 'pendingGroups'])); const isMyProfile = (myUsername === username); const isAdmin = useSelector (isAdminSelector); const logout = () => dispatch (logoutAction ()); diff --git a/src/components/pages/SearchPage/SearchPage.js b/src/components/pages/SearchPage/SearchPage.js index 0dbd88b7..6c007270 100644 --- a/src/components/pages/SearchPage/SearchPage.js +++ b/src/components/pages/SearchPage/SearchPage.js @@ -82,7 +82,7 @@ const SearchPage = () => { .catch (err => _updateResults (initialState)); }, [safeQuery, safeCategory.join ('')]); - const onTagClick = (newCat) => { + const onTagClick = newCat => { let newCats = safeCategory.slice (); if (safeCategory.includes (newCat)) { newCats = newCats.filter (c => c !== newCat); diff --git a/src/components/pages/SettingsPage/GroupApply.js b/src/components/pages/SettingsPage/GroupApply.js index 68000b30..8bb72221 100644 --- a/src/components/pages/SettingsPage/GroupApply.js +++ b/src/components/pages/SettingsPage/GroupApply.js @@ -5,6 +5,7 @@ import React, { import { useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import Tooltip from '@material-ui/core/Tooltip'; +import get from 'lodash.get'; import SimpleSelect from 'molecules/SimpleSelect'; import StyledQuill from 'organisms/StyledQuill'; @@ -31,7 +32,7 @@ const categoryOptions = GROUP_CATEGORIES.map (category => ( const ApplyForm = ({ profilePhoto }) => { const dispatch = useDispatch (); const history = useHistory (); - const myName = useSelector (state => state.getIn (['auth', 'info', 'username'])); + const myName = useSelector (state => get (state, ['auth', 'info', 'username'])); const [state, setState, onChange] = useSetState ({ name: '', description: '', diff --git a/src/components/pages/SettingsPage/GroupMembersSetting.js b/src/components/pages/SettingsPage/GroupMembersSetting.js index 2a35dd0b..642d34a2 100644 --- a/src/components/pages/SettingsPage/GroupMembersSetting.js +++ b/src/components/pages/SettingsPage/GroupMembersSetting.js @@ -55,7 +55,7 @@ const GroupMembersSetting = ({ profile }) => { dispatch (profileActions.updateGroupMember ({ groupName: name, userId, role })) .catch (error => alert (error.error)); }, [dispatch]); - const removeMember = useCallback ((userId) => { + const removeMember = useCallback (userId => { if (!window.confirm (alerts.deleteMember)) return; dispatch (profileActions.removeGroupMember ({ groupName: name, userId })) .catch (error => alert (error.error)); diff --git a/src/components/pages/SettingsPage/ProfileSetting.js b/src/components/pages/SettingsPage/ProfileSetting.js index e83b6356..1407183b 100644 --- a/src/components/pages/SettingsPage/ProfileSetting.js +++ b/src/components/pages/SettingsPage/ProfileSetting.js @@ -8,6 +8,7 @@ import { useDispatch, useSelector } from 'react-redux'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; import AwesomeDebouncePromise from 'awesome-debounce-promise'; +import get from 'lodash.get'; import Footer from 'templates/Footer'; import Header from 'templates/Header'; @@ -72,7 +73,7 @@ const ProfileForm = ({ initialValue, newProfilePhoto }) => { }); }, [username, description, newProfilePhoto]); - const onChange = (e) => { + const onChange = e => { setState ({ success: false, error: null }); onChangeHandler (e); }; @@ -137,9 +138,8 @@ ProfileForm.propTypes = { newProfilePhoto: PropTypes.object, }; -const UserProfileSetting = (props) => { - const infoImmutable = useSelector (state => state.getIn (['auth', 'info'])); - const info = useMemo (() => infoImmutable.toJS (), [infoImmutable]); +const UserProfileSetting = props => { + const info = useSelector (state => get (state, ['auth', 'info'])); const { username = '', profilePhoto, description = '' } = info; const [profilePreview, setProfilePreview] = useState (profilePhoto); diff --git a/src/components/pages/SettingsPage/withGroupProfile.js b/src/components/pages/SettingsPage/withGroupProfile.js index fbc1f3e2..74917f02 100644 --- a/src/components/pages/SettingsPage/withGroupProfile.js +++ b/src/components/pages/SettingsPage/withGroupProfile.js @@ -1,11 +1,12 @@ import React, { useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import get from 'lodash.get'; import { NotFound } from 'pages'; import { getProfile } from 'store/reducers/profile'; -const withGroupProfile = (WrappedComponent, isPrivate = false) => (props) => { +const withGroupProfile = (WrappedComponent, isPrivate = false) => props => { const dispatch = useDispatch (); const { groupName } = props; @@ -13,8 +14,7 @@ const withGroupProfile = (WrappedComponent, isPrivate = false) => (props) => { dispatch (getProfile (groupName)); }, [groupName]); - const profileImmutable = useSelector (state => state.getIn (['profile', 'profiles', groupName])); - const profile = useMemo (() => (profileImmutable ? profileImmutable.toJS () : null), [profileImmutable]); + const profile = useSelector (state => get (state, ['profile', 'profiles', groupName])); if (!profile) return null; if (profile.error) return ; if (isPrivate && !profile.myRole) return ; diff --git a/src/components/pages/ZaboPage/ZaboDetailPage.js b/src/components/pages/ZaboPage/ZaboDetailPage.js index dbeaf5fe..5fedb281 100644 --- a/src/components/pages/ZaboPage/ZaboDetailPage.js +++ b/src/components/pages/ZaboPage/ZaboDetailPage.js @@ -5,6 +5,7 @@ import { useDispatch, useSelector } from 'react-redux'; import Carousel from 'react-airbnb-carousel'; import { Helmet } from 'react-helmet'; import Tooltip from '@material-ui/core/Tooltip'; +import get from 'lodash.get'; import moment from 'moment'; import Button from 'atoms/Button'; @@ -54,7 +55,7 @@ const OwnerInfo = ({ alert ('Error'); }); }); - const following = useSelector (state => state.getIn (['profile', 'profiles', name, 'following'])); + const following = useSelector (state => get (state, ['profile', 'profiles', name, 'following'])); return (
@@ -109,14 +110,14 @@ OwnerInfo.propTypes = { OwnerInfo.defaultProps = {}; -const ZaboDetailPage = (props) => { +const ZaboDetailPage = props => { const { zabo, zaboId } = props; const { title, owner = {}, schedules, createdAt, description, category = [], photos = [{}], isLiked, likesCount, isPinned, pinsCount, views, effectiveViews, isMyZabo, createdBy, } = zabo; const schedule = schedules[0]; - const timePast = getLabeledTimeDiff (createdAt, true, true, 6, false, false, false); + const timePast = getLabeledTimeDiff (createdAt, 60, 60, 6, 0); const due = schedule ? moment (schedule.startAt).diff (moment (), 'days') : 0; const dueFormat = schedule && moment (schedule.startAt).format ('MM/DD h:mm'); const stats = [{ diff --git a/src/components/pages/ZaboUploadPage/ZaboUploadPage.js b/src/components/pages/ZaboUploadPage/ZaboUploadPage.js index 7a088c74..a6aa6b74 100644 --- a/src/components/pages/ZaboUploadPage/ZaboUploadPage.js +++ b/src/components/pages/ZaboUploadPage/ZaboUploadPage.js @@ -8,6 +8,7 @@ import { import { useDispatch, useSelector } from 'react-redux'; import SwipeableViews from 'react-swipeable-views'; import styled from 'styled-components'; +import get from 'lodash.get'; import Footer from 'templates/Footer'; import Header from 'templates/Header'; @@ -69,13 +70,13 @@ Loading.Inactive = styled.div` border-top: 10px solid gainsboro; `; -const UploadFooter = (props) => { +const UploadFooter = props => { const { prev, next, step } = props; const dispatch = useDispatch (); - const currentGroup = useSelector (state => state.getIn (['auth', 'info', 'currentGroup'])); - const filesImmutable = useSelector (state => state.getIn (['upload', 'images'])); - const infoImmutable = useSelector (state => state.getIn (['upload', 'info'])); - const submitted = useSelector (state => state.getIn (['upload', 'submitted'])); + const currentGroup = useSelector (state => get (state, ['auth', 'info', 'currentGroup'])); + const files = useSelector (state => get (state, ['upload', 'images'])); + const info = useSelector (state => get (state, ['upload', 'info'])); + const submitted = useSelector (state => get (state, ['upload', 'submitted'])); const validatedNext = useCallback (() => { // xxSelected : Currently Not Being Used @@ -88,10 +89,9 @@ const UploadFooter = (props) => { return; } next (); - }, [step, currentGroup, filesImmutable, infoImmutable]); + }, [step, currentGroup, files, info]); const step2Valid = useMemo (() => { - const info = infoImmutable.toJS (); const { title, description, hasSchedule, schedules, } = info; @@ -101,18 +101,18 @@ const UploadFooter = (props) => { const { title: scheduleTitle, startAt, eventType } = schedule; const scheduleValid = (scheduleTitle && startAt && eventType); return zaboValid && scheduleValid; - }, [infoImmutable]); + }, [info]); const isValid = useMemo (() => { if (step === 0) { return !!currentGroup; } if (step === 1) { - return !!filesImmutable.size; + return !!files.length; } if (step === 2) { return step2Valid; } return false; - }, [step, currentGroup, filesImmutable, step2Valid]); + }, [step, currentGroup, files, step2Valid]); const isSubmit = step >= 2; @@ -164,7 +164,7 @@ UploadFooter.propTypes = { const ZaboUploadPage = () => { const dispatch = useDispatch (); - const step = useSelector (state => state.getIn (['upload', 'step'])); + const step = useSelector (state => get (state, ['upload', 'step'])); const setStep = (newStep => dispatch (setReduxStep (newStep))); const next = useCallback (() => setStep (step + 1), [step]); const prev = useCallback (() => setStep (step - 1), [step]); diff --git a/src/components/templates/FloatingNavigator/FloatingNavigator.container.js b/src/components/templates/FloatingNavigator/FloatingNavigator.container.js deleted file mode 100644 index 25184108..00000000 --- a/src/components/templates/FloatingNavigator/FloatingNavigator.container.js +++ /dev/null @@ -1,18 +0,0 @@ -import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; - -import toJS from 'hoc/toJS'; - -import FloatingNavigator from './FloatingNavigator'; - -class FloatingNavigatorContainer extends PureComponent { - render () { - return ; - } -} - -const mapStateToProps = state => ({}); - -const mapDispatchToProps = dispatch => ({}); - -export default connect (mapStateToProps, mapDispatchToProps) (toJS (FloatingNavigatorContainer)); diff --git a/src/components/templates/FloatingNavigator/index.js b/src/components/templates/FloatingNavigator/index.js index 094190a7..8e725566 100644 --- a/src/components/templates/FloatingNavigator/index.js +++ b/src/components/templates/FloatingNavigator/index.js @@ -1 +1 @@ -export { default } from './FloatingNavigator.container'; +export { default } from './FloatingNavigator'; diff --git a/src/components/templates/Header/Header.js b/src/components/templates/Header/Header.js index e733a207..6bbb7e8c 100644 --- a/src/components/templates/Header/Header.js +++ b/src/components/templates/Header/Header.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { Link, NavLink, useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import { css } from 'styled-components'; +import get from 'lodash.get'; import Container from 'atoms/Container'; import SVG from 'atoms/SVG'; @@ -95,7 +96,7 @@ Header.defaultProps = { Header.AuthButton = ({ type, groupName, transparent }) => { const dispatch = useDispatch (); const isAuthenticated = useSelector (isAuthedSelector); - const username = useSelector (state => state.getIn (['auth', 'info', 'username'])); + const username = useSelector (state => get (state, ['auth', 'info', 'username'])); const toUpload = useCallback (() => { if (groupName) dispatch (setCurrentGroup (groupName)); diff --git a/src/components/templates/PWAPrompt/PWAPrompt.js b/src/components/templates/PWAPrompt/PWAPrompt.js index b99f26f5..85948cde 100644 --- a/src/components/templates/PWAPrompt/PWAPrompt.js +++ b/src/components/templates/PWAPrompt/PWAPrompt.js @@ -8,7 +8,7 @@ import PWAPromptWrapper from './PWAPrompt.styled'; class PWAPrompt extends PureComponent { state = { active: false }; - handleScroll = (e) => { + handleScroll = e => { if (window.scrollY < 10) { document.body.classList.add ('pwa-prompt-active'); this.setState ({ active: true }); @@ -18,7 +18,7 @@ class PWAPrompt extends PureComponent { } }; - handleScroll = (e) => { + handleScroll = e => { if (window.scrollY < 10) { document.body.classList.add ('pwa-prompt-active'); this.setState ({ active: true }); diff --git a/src/components/templates/SearchBar/SearchBar.js b/src/components/templates/SearchBar/SearchBar.js index 0f935d25..6a85502c 100644 --- a/src/components/templates/SearchBar/SearchBar.js +++ b/src/components/templates/SearchBar/SearchBar.js @@ -95,7 +95,7 @@ const SearchBar = ({ setFocused (false); }, [setFocused]); - const onTagClick = useCallback ((newCat) => { + const onTagClick = useCallback (newCat => { setFocused (false); history.push (`/search?${queryString.stringify ({ category: newCat })}`); }, [setFocused]); @@ -119,7 +119,7 @@ const SearchBar = ({

자보

    - {zabos.map ((zabo) => ( + {zabos.map (zabo => (
  • {zabo.title}
  • @@ -131,7 +131,7 @@ const SearchBar = ({

    그룹

      - {groups.map ((group) => ( + {groups.map (group => (
    • { group.profilePhoto diff --git a/src/components/templates/ZaboList/ZaboList.container.js b/src/components/templates/ZaboList/ZaboList.container.js index 41c5dc7a..91236410 100644 --- a/src/components/templates/ZaboList/ZaboList.container.js +++ b/src/components/templates/ZaboList/ZaboList.container.js @@ -1,11 +1,11 @@ import { connect } from 'react-redux'; import { List } from 'immutable'; +import get from 'lodash.get'; import { getGroupZaboList, getPins, getSearchZaboList, getZaboList, } from 'store/reducers/zabo'; -import toJS from 'hoc/toJS'; import ZaboList from './ZaboList'; @@ -16,14 +16,13 @@ const reduxKey = { group: name => ['zabo', 'lists', name], search: () => ['zabo', 'lists', 'search'], }; - const emptyList = List ([]); const mapStateToProps = (state, ownProps) => { const { type, query } = ownProps; - const zaboIdList = state.getIn (reduxKey[type] (query)) || emptyList; + const zaboIdList = get (state, reduxKey[type] (query)) || emptyList; return { zaboIdList, - width: state.getIn (['app', 'windowSize', 'width']), + width: get (state, ['app', 'windowSize', 'width']), }; }; @@ -34,4 +33,4 @@ const mapDispatchToProps = { getSearchZaboList, }; -export default connect (mapStateToProps, mapDispatchToProps) (toJS (ZaboList)); +export default connect (mapStateToProps, mapDispatchToProps) (ZaboList); diff --git a/src/components/templates/ZaboUpload/InfoForm.js b/src/components/templates/ZaboUpload/InfoForm.js index 744f7fc7..d087caa4 100644 --- a/src/components/templates/ZaboUpload/InfoForm.js +++ b/src/components/templates/ZaboUpload/InfoForm.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import MomentUtils from '@date-io/moment'; import { KeyboardDateTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'; +import get from 'lodash.get'; import moment from 'moment'; import ToggleButton from 'atoms/ToggleButton'; @@ -223,12 +224,10 @@ Form.propTypes = { const InfoForm = () => { const dispatch = useDispatch (); const [preview, setPreview] = useState (); - const infoImmutable = useSelector (state => state.getIn (['upload', 'info'])); - const imageFilesImmutable = useSelector (state => state.getIn (['upload', 'images'])); - const info = useMemo (() => infoImmutable.toJS (), [infoImmutable]); + const info = useSelector (state => get (state, ['upload', 'info'])); + const imageFiles = useSelector (state => get (state, ['upload', 'images'])); useEffect (() => { - const imageFiles = imageFilesImmutable.toJS (); const sortedImageFiles = imageFiles.slice (); sortedImageFiles.sort (gridLayoutCompareFunction); const titleImageFile = sortedImageFiles[0]; @@ -240,7 +239,7 @@ const InfoForm = () => { return () => { URL.revokeObjectURL (url); }; - }, [imageFilesImmutable]); + }, [imageFiles]); const setState = useCallback (updates => { dispatch (setInfo ({ ...info, ...updates })); diff --git a/src/components/templates/ZaboUpload/LeavingAlert.js b/src/components/templates/ZaboUpload/LeavingAlert.js index b134181b..b546135c 100644 --- a/src/components/templates/ZaboUpload/LeavingAlert.js +++ b/src/components/templates/ZaboUpload/LeavingAlert.js @@ -1,11 +1,12 @@ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import get from 'lodash.get'; import { reset, setModal } from 'store/reducers/upload'; const LeavingAlert = () => { const dispatch = useDispatch (); - const showModal = useSelector (state => state.getIn (['upload', 'showModal'])); + const showModal = useSelector (state => get (state, ['upload', 'showModal'])); useEffect (() => { if (showModal) { alert ('데이터를 저장하시겠습니까?'); diff --git a/src/components/templates/ZaboUpload/SelectGroup.js b/src/components/templates/ZaboUpload/SelectGroup.js index 1340d2a5..0688d802 100644 --- a/src/components/templates/ZaboUpload/SelectGroup.js +++ b/src/components/templates/ZaboUpload/SelectGroup.js @@ -5,6 +5,7 @@ import styled from 'styled-components'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; import { makeStyles } from '@material-ui/core/styles'; +import get from 'lodash.get'; import { setCurrentGroup } from 'store/reducers/auth'; import { setGroupSelected } from 'store/reducers/upload'; @@ -98,10 +99,10 @@ const useStyles = makeStyles (theme => ({ const InsetDividers = ({ groupsInfo }) => { const classes = useStyles (); - const currentGroup = useSelector (state => state.getIn (['auth', 'info', 'currentGroup'])); + const currentGroup = useSelector (state => get (state, ['auth', 'info', 'currentGroup'])); const dispatch = useDispatch (); - const updateGroup = useCallback ((groupName) => { + const updateGroup = useCallback (groupName => { dispatch (setCurrentGroup (groupName)) .then (() => dispatch (setGroupSelected (true))) .catch (error => console.error (error)); @@ -118,7 +119,7 @@ const InsetDividers = ({ groupsInfo }) => { updateGroup (name)} > { @@ -155,8 +156,7 @@ InsetDividers.defaultProps = { const SelectGroup = () => { - const groupsInfoImmutable = useSelector (state => state.getIn (['auth', 'info', 'groups'])); - const groupsInfo = useMemo (() => groupsInfoImmutable.toJS (), [groupsInfoImmutable]); + const groupsInfo = useSelector (state => get (state, ['auth', 'info', 'groups'])); return ( diff --git a/src/components/templates/ZaboUpload/UploadImages.js b/src/components/templates/ZaboUpload/UploadImages.js index 2de82a38..da1442b0 100644 --- a/src/components/templates/ZaboUpload/UploadImages.js +++ b/src/components/templates/ZaboUpload/UploadImages.js @@ -10,6 +10,7 @@ import styled, { css } from 'styled-components'; import CloseIcon from '@material-ui/icons/Close'; import { Responsive, WidthProvider } from '@sparcs-kaist/react-grid-layout'; import debounce from 'lodash.debounce'; +import get from 'lodash.get'; import throttle from 'lodash.throttle'; import { setImages } from 'store/reducers/upload'; @@ -230,7 +231,7 @@ Wrapper.Placeholder = styled.div` `; let alerted; -const alertOnce = (message) => { +const alertOnce = message => { if (alerted) return; alerted = true; alert (message); @@ -238,8 +239,7 @@ const alertOnce = (message) => { const UploadImages = props => { const reduxDispatch = useDispatch (); - const filesImmutable = useSelector (state => state.getIn (['upload', 'images'])); - const files = useMemo (() => filesImmutable.toJS (), [filesImmutable]); + const files = useSelector (state => get (state, ['upload', 'images'])); const setFiles = newFiles => reduxDispatch (setImages (newFiles)); const [widthInfo, setWidthInfo] = useState ({ width: 0, @@ -276,9 +276,9 @@ const UploadImages = props => { }); }), ]); - }, [filesImmutable]); + }, [files]); - const removeImage = useCallback ((key) => { + const removeImage = useCallback (key => { const clone = files.slice ().map (x => Object.assign (x, { layout: { ...x.updatedLayout } })); clone.sort (gridLayoutCompareFunction); const deleteIndex = clone.findIndex (l => l.key === key); @@ -295,7 +295,7 @@ const UploadImages = props => { } } setFiles (clone); - }, [filesImmutable]); + }, [files]); const updateImagesInfo = useCallback (async () => { const titleInfo = { @@ -343,7 +343,7 @@ const UploadImages = props => { newImagesInfo.title = titleInfo; setImagesInfo (newImagesInfo); - }, [filesImmutable, imagesInfo]); + }, [files, imagesInfo]); const reducer = (state, action) => { switch (action.type) { @@ -397,7 +397,7 @@ const UploadImages = props => { useEffect (() => () => dispatch ({ type: 'revokeObjectURL' }), []); // Make sure to revoke the data uris to avoid memory leaks useEffect (() => { dispatch ({ type: 'updateImagesInfo' }); - }, [filesImmutable]); + }, [files]); useEffect (() => { dispatch ({ type: 'relocate' }); @@ -439,7 +439,7 @@ const UploadImages = props => { - )), [filesImmutable, imagesInfo, dispatch]); + )), [files, imagesInfo, dispatch]); const style = useMemo ( () => ({ diff --git a/src/components/templates/ZaboUpload/UploadProcess.js b/src/components/templates/ZaboUpload/UploadProcess.js index d1e63305..9d041db1 100644 --- a/src/components/templates/ZaboUpload/UploadProcess.js +++ b/src/components/templates/ZaboUpload/UploadProcess.js @@ -6,6 +6,7 @@ import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; +import get from 'lodash.get'; import { reset } from 'store/reducers/upload'; import { uploadZabo } from 'store/reducers/zabo'; @@ -31,13 +32,11 @@ Loading.Inactive = styled.div` const UploadProcess = ({ children }) => { const dispatch = useDispatch (); const history = useHistory (); - const infoImmutable = useSelector (state => state.getIn (['upload', 'info'])); - const info = useMemo (() => infoImmutable.toJS (), [infoImmutable]); + const info = useSelector (state => get (state, ['upload', 'info'])); const { title, description, hasSchedule, schedules, category, } = info; - const imageFilesImmutable = useSelector (state => state.getIn (['upload', 'images'])); - const imageFiles = useMemo (() => imageFilesImmutable.toJS (), [imageFilesImmutable]); + const imageFiles = useSelector (state => get (state, ['upload', 'images'])); const [progress, setProgress] = useState (0); const [error, setError] = useState (null); @@ -76,7 +75,7 @@ const UploadProcess = ({ children }) => { setProgress (0); alert (err.error); }); - }, [infoImmutable, imageFilesImmutable]); + }, [info, imageFiles]); useEffect (() => { upload (); diff --git a/src/hoc/AuthRoutes.js b/src/hoc/AuthRoutes.tsx similarity index 74% rename from src/hoc/AuthRoutes.js rename to src/hoc/AuthRoutes.tsx index 76a8d59f..311c7912 100644 --- a/src/hoc/AuthRoutes.js +++ b/src/hoc/AuthRoutes.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { Redirect, Route } from 'react-router-dom'; +import { Redirect, Route, RouteProps } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { isAdminOrPendingSelector, isAuthedSelector } from 'lib/utils'; import { alerts } from '../lib/variables'; -export const PrivateRoute = (({ component: Component, render, ...rest }) => { +export const PrivateRoute = (({ component: Component, render = () => null, ...rest } : RouteProps) => { const isAuthenticated = useSelector (isAuthedSelector); // TODO: Return loading while auth info not fetched return ( @@ -24,10 +24,7 @@ export const PrivateRoute = (({ component: Component, render, ...rest }) => { ); }); -PrivateRoute.propTypes = Route.propTypes; -PrivateRoute.defaultProps = Route.defaultProps; - -export const PublicRoute = ({ component: Component, render, ...rest }) => { +export const PublicRoute = ({ component: Component, render = () => null, ...rest } : RouteProps) => { const isAuthenticated = useSelector (isAuthedSelector); return ( { if (isAuthenticated) { const { location, history } = props; const { state } = location; + // @ts-ignore if (state && state.referrer === 'comeback') history.goBack (); + // @ts-ignore else if (state && state.referrer) history.replace (state.referrer); else history.push ('/', { referrer: 'public' }); return null; @@ -48,10 +47,7 @@ export const PublicRoute = ({ component: Component, render, ...rest }) => { ); }; -PublicRoute.propTypes = Route.propTypes; -PublicRoute.defaultProps = Route.defaultProps; - -export const AdminRoute = ({ component: Component, render, ...rest }) => { +export const AdminRoute = ({ component: Component, render = () => null, ...rest } : RouteProps) => { const [isAdmin, pending] = useSelector (isAdminOrPendingSelector); return ( { /> ); }; - -AdminRoute.propTypes = Route.propTypes; -AdminRoute.defaultProps = Route.defaultProps; diff --git a/src/hoc/toJS.js b/src/hoc/toJS.js deleted file mode 100644 index b6a097a1..00000000 --- a/src/hoc/toJS.js +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable */ -import React from 'react'; -import { Iterable } from 'immutable'; - -export default WrappedComponent => wrappedComponentProps => { - const KEY = 0; - const VALUE = 1; - - const propsJS = Object.entries (wrappedComponentProps).reduce ((newProps, wrappedComponentProp) => { - newProps[wrappedComponentProp[KEY]] = Iterable.isIterable (wrappedComponentProp[VALUE]) - ? wrappedComponentProp[VALUE].toJS () - : wrappedComponentProp[VALUE]; - return newProps; - }, {}); - - return ; -}; diff --git a/src/hoc/withZabo.js b/src/hoc/withZabo.js index 2ab3ebbb..df70e383 100644 --- a/src/hoc/withZabo.js +++ b/src/hoc/withZabo.js @@ -1,13 +1,14 @@ import React, { useEffect, useMemo } from 'react'; import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; - -import { NotFound } from 'pages'; +import get from 'lodash.get'; import { getZabo } from 'store/reducers/zabo'; +const NotFound = () =>

      Zabo Not Found

      ; + const withZabo = (WrappedComponent, isPrivate = false, fetch = true) => { - const Sub = (props) => { + const Sub = props => { const dispatch = useDispatch (); const { zaboId } = props; @@ -15,8 +16,7 @@ const withZabo = (WrappedComponent, isPrivate = false, fetch = true) => { if (fetch) dispatch (getZabo (zaboId)); }, [zaboId]); - const zaboImmutable = useSelector (state => state.getIn (['zabo', 'zabos', zaboId])); - const zabo = useMemo (() => (zaboImmutable ? zaboImmutable.toJS () : null), [zaboImmutable]); + const zabo = useSelector (state => get (state, ['zabo', 'zabos', zaboId])); if (!zabo) return null; if (zabo.error) return ; if (isPrivate && !zabo.isMyZabo) return ; diff --git a/src/hooks/useSetState.js b/src/hooks/useSetState.js index d32dd085..21e62f7a 100644 --- a/src/hooks/useSetState.js +++ b/src/hooks/useSetState.js @@ -3,7 +3,7 @@ import { useRef, useState } from 'react'; export default function useSetState (initialForm) { const [state, setStateInternal] = useState (initialForm); const clone = { ...state }; - const setState = (param) => { + const setState = param => { if (typeof param === 'function') { setStateInternal (prevState => ({ ...prevState, ...param (prevState) })); return; diff --git a/src/index.js b/src/index.tsx similarity index 100% rename from src/index.js rename to src/index.tsx diff --git a/src/lib/api/group.js b/src/lib/api/group.js index 8ea79b21..ad3038f7 100644 --- a/src/lib/api/group.js +++ b/src/lib/api/group.js @@ -12,7 +12,7 @@ export const updateGroupInfoWithImage = ({ curName, formData }) => axios.post (` export const addGroupMember = ({ groupName, userId, role }) => axios.put (`/group/${groupName}/member`, { userId, role }); export const updateGroupMember = ({ groupName, userId, role }) => axios.post (`/group/${groupName}/member`, { userId, role }); export const removeGroupUser = ({ groupName, userId }) => axios.delete (`/group/${groupName}/member`, { data: { userId } }); -export const applyNewGroup = (data) => axios.post ('/group/apply', data); +export const applyNewGroup = data => axios.post ('/group/apply', data); /* Main */ export const getRecommendedGroups = () => axios.get ('/group/recommends'); diff --git a/src/lib/api/profile.js b/src/lib/api/profile.js index 248fa362..375fd5d4 100644 --- a/src/lib/api/profile.js +++ b/src/lib/api/profile.js @@ -1,5 +1,5 @@ import axios from '../axios'; -export const fetchProfile = (name) => axios.get (`/profile/${name}`); +export const fetchProfile = name => axios.get (`/profile/${name}`); export const validateName = ({ name }) => axios.get (`/profile/${name}/isValid`); export const followProfile = ({ name }) => axios.post (`/profile/${name}/follow`); diff --git a/src/lib/api/user.js b/src/lib/api/user.js index 04d81014..3a084b31 100644 --- a/src/lib/api/user.js +++ b/src/lib/api/user.js @@ -1,6 +1,6 @@ import axios from '../axios'; /* Auth */ -export const updateUserInfo = (data) => axios.post ('/user', data); -export const updateUserInfoWithImage = (formData) => axios.post ('/user', formData); +export const updateUserInfo = data => axios.post ('/user', data); +export const updateUserInfoWithImage = formData => axios.post ('/user', formData); export const setCurrentGroup = groupName => axios.post (`/user/currentGroup/${groupName}`); diff --git a/src/lib/channel_io.js b/src/lib/channel_io.js index 77192ff6..9ffd3d12 100644 --- a/src/lib/channel_io.js +++ b/src/lib/channel_io.js @@ -1,15 +1,21 @@ class ChannelService { constructor() { - this.loadScript(); + this.loaded = false; + this.onloaded = () => {}; + setTimeout (() => { + this.loadScript(); + this.loaded = true; + this.onloaded (); + }, 5000); } loadScript() { - var w = window; + let w = window; if (w.ChannelIO) { return (window.console.error || window.console.log || function(){})('ChannelIO script included twice.'); } - var d = window.document; - var ch = function() { + let d = window.document; + let ch = function() { ch.c(arguments); }; ch.q = []; @@ -22,12 +28,12 @@ class ChannelService { return; } w.ChannelIOInitialized = true; - var s = document.createElement('script'); + let s = document.createElement('script'); s.type = 'text/javascript'; s.async = true; s.src = 'https://cdn.channel.io/plugin/ch-plugin-web.js'; s.charset = 'UTF-8'; - var x = document.getElementsByTagName('script')[0]; + let x = document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); } if (document.readyState === 'complete') { @@ -41,10 +47,18 @@ class ChannelService { } boot(settings) { + if (!this.loaded) { + this.onloaded = () => this.boot (settings); + return; + } window.ChannelIO('boot', settings); } shutdown() { + if (!this.loaded) { + this.onloaded = () => this.shutdown (); + return; + } window.ChannelIO('shutdown'); } } diff --git a/src/lib/i18n/index.js b/src/lib/i18n/index.js index 5a222a25..ee1d63c4 100644 --- a/src/lib/i18n/index.js +++ b/src/lib/i18n/index.js @@ -7,7 +7,7 @@ import Li from './i18next-react-li-postprocessor'; import ReactPostProcessor from './i18next-react-react-postprocessor'; -i18n.on ('languageChanged', (lng) => { +i18n.on ('languageChanged', lng => { if (!lng.split ('-')) return; if (lng.split ('-')[0] !== lng) i18n.changeLanguage (lng.split ('-')[0]); }); diff --git a/src/lib/utils/index.js b/src/lib/utils/index.ts similarity index 66% rename from src/lib/utils/index.js rename to src/lib/utils/index.ts index 2e31ee67..8f01f09d 100644 --- a/src/lib/utils/index.js +++ b/src/lib/utils/index.ts @@ -1,10 +1,24 @@ +import { useHistory } from 'react-router-dom'; import moment from 'moment'; -import queryString from 'query-string'; +import queryString, { ParsedQuery } from 'query-string'; import { alerts, RESERVED_ROUTES_USERNAME_EXCEPTIONS } from '../variables'; export * from './selector'; +export const parseJSON = (jsonString : string | object, fallback = {}) => { + if (typeof jsonString === 'object') { + return jsonString; + } + try { + return JSON.parse (jsonString); + } catch (error) { + console.error (jsonString); + console.error (error.message); + return fallback; + } +}; + export const mediaSizes = { xs: 360, s: 520, @@ -13,14 +27,14 @@ export const mediaSizes = { xl: 1440, }; -export const gridLayoutCompareFunction = (a, b) => { +export const gridLayoutCompareFunction = (a : any, b : any) => { const { x: ax, y: ay } = a.updatedLayout; const { x: bx, y: by } = b.updatedLayout; if (ay - by) return ay - by; return ax - bx; }; -export const to2Digits = (number, sign = false) => { +export const to2Digits = (number : number, sign = false) => { let result; let pos; if (number < 0) { @@ -37,7 +51,7 @@ export const to2Digits = (number, sign = false) => { return result; }; -export const dataURLToBlob = (dataURL) => { +export const dataURLToBlob = (dataURL : string) => { const blobBin = atob (dataURL.split (',')[1]); const array = []; for (let i = 0; i < blobBin.length; i += 1) { @@ -47,7 +61,7 @@ export const dataURLToBlob = (dataURL) => { return new Blob ([new Uint8Array (array)]); }; -export const loadImageFile = (file) => { +export const loadImageFile = (file : File) : Promise => { const _URL = window.URL || window.webkitURL; const img = new Image (); const objectUrl = _URL.createObjectURL (file); @@ -60,13 +74,13 @@ export const loadImageFile = (file) => { }); }; -export const imageFileGetWidthHeight = async (file) => { - const image = await loadImageFile (file); +export const imageFileGetWidthHeight = async (file : File) => { + const image : HTMLImageElement = await loadImageFile (file); const { width, height } = image; return { width, height, ratio: width / height }; }; -export const cropImage = async (file, ratio) => { +export const cropImage = async (file : File, ratio : number) : Promise => { const image = await loadImageFile (file); const { width, height } = image; const ownRatio = width / height; @@ -83,6 +97,7 @@ export const cropImage = async (file, ratio) => { canvas.width = dWidth; canvas.height = dHeight; const context = canvas.getContext ('2d'); + if (!context) return ''; context.fillStyle = 'white'; context.fillRect (0, 0, canvas.width, canvas.height); @@ -93,7 +108,7 @@ export const cropImage = async (file, ratio) => { return canvas.toDataURL ('image/jpeg'); }; -export const validateName = (name) => { +export const validateName = (name : string) => { // 1~25자 제한 if (name.length === 0 || name.length > 25) return false; // 첫 글자로는 _, 알파벳, 한글, 숫자만 입력 가능 @@ -106,7 +121,7 @@ export const validateName = (name) => { return !match; }; -export const getLabeledTimeDiff = (time, showSecs = true, showMins = true, showHours = true, showDays = 2, showWeeks = false, showMonths = false) => { +export const getLabeledTimeDiff = (time : string, showSecs = 60, showMins = 60, showHours = 24, showDays = 2, showWeeks = 0, showMonths = 0) => { const curMoment = moment (); const momentTime = moment (time); const momentLabel = momentTime.format ('YYYY.MM.DD'); @@ -117,7 +132,7 @@ export const getLabeledTimeDiff = (time, showSecs = true, showMins = true, showH const weeksDiff = curMoment.diff (momentTime, 'weeks'); const monthsDiff = curMoment.diff (momentTime, 'months'); - const isShow = (showVar, diff) => (typeof showVar === 'number' && diff <= showVar) || (typeof showVar !== 'number' && showVar); + const isShow = (showVar : number, diff : number) => (diff < showVar); return ( secDiff < 60 ? (isShow (showSecs, secDiff) ? '방금 전' : momentLabel) @@ -129,44 +144,49 @@ export const getLabeledTimeDiff = (time, showSecs = true, showMins = true, showH ); }; -export const isElemWidthOverflown = (element) => element.scrollWidth > element.clientWidth; +export const isElemWidthOverflown = (element : HTMLElement) => element.scrollWidth > element.clientWidth; // export const isElementOverflown = (element) => element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth; -export const escapeRegExp = string => string.replace (/[.*+?^${}()|[\]\\]/g, '\\$&'); -export const parseQuery = (search) => { +export const escapeRegExp = (str : string) => str.replace (/[.*+?^${}()|[\]\\]/g, '\\$&'); +export const parseQuery = (search : string) : { safeQuery : string, safeCategory : string[] } => { const { query, category } = queryString.parse (search); - const safeQuery = (query ? escapeRegExp (query) : '').trim (); + const safeQuery = (!query ? '' + : escapeRegExp ((Array.isArray (query)) ? query[0] : query).trim () + ); const safeCategory = !category ? [] : !Array.isArray (category) ? [category] : category; return { safeQuery, safeCategory }; }; -export const jsonStringify = (json) => { +export const jsonPrettyStringify = (json : any) => { let result = json; if (typeof result !== 'string') { result = JSON.stringify (result, undefined, 2); } result = result.replace (/&/g, '&').replace (//g, '>'); - return result.replace (/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, (match) => { - let cls = 'number'; - if (/^"/.test (match)) { - if (/:$/.test (match)) { - cls = 'key'; - } else { - cls = 'string'; + return result.replace ( + /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, + (match : string) => { + let cls = 'number'; + if (/^"/.test (match)) { + if (/:$/.test (match)) { + cls = 'key'; + } else { + cls = 'string'; + } + } else if (/true|false/.test (match)) { + cls = 'boolean'; + } else if (/null/.test (match)) { + cls = 'null'; } - } else if (/true|false/.test (match)) { - cls = 'boolean'; - } else if (/null/.test (match)) { - cls = 'null'; - } - return `${match}`; - }); + return `${match}`; + }, + ); }; -export const pushWithAuth = (to, history, isAuthed) => { +export const pushWithAuth = (to : string, history : ReturnType, isAuthed : boolean) : void => { if (!isAuthed) { if (window.confirm (alerts.login)) { history.replace ({ pathname: '/auth/login', state: { referrer: history.location.pathname } }); @@ -176,7 +196,7 @@ export const pushWithAuth = (to, history, isAuthed) => { } }; -export const withAuth = (history, isAuthed) => { +export const withAuth = (history : ReturnType, isAuthed : boolean) => { if (!isAuthed) { if (window.confirm (alerts.login)) { history.replace ({ pathname: '/auth/login', state: { referrer: history.location.pathname } }); diff --git a/src/lib/utils/selector.js b/src/lib/utils/selector.js deleted file mode 100644 index a2ea03fa..00000000 --- a/src/lib/utils/selector.js +++ /dev/null @@ -1,36 +0,0 @@ -import { createSelector } from 'reselect'; - -import { CHECK_AUTH } from 'store/reducers/auth'; - -const decodedTokenSelector = state => state.getIn (['auth', 'jwt']); -const isDecodedTokenAlive = decoded => { - if (decoded && decoded.get ('exp')) { - return 1000 * decoded.get ('exp') - new Date ().getTime () >= 5000; - } - return false; -}; -const decodedTokenLifetime = decoded => { - if (decoded && decoded.get ('exp')) { - const remain = decoded.get ('exp') * 1000 - Date.now (); - return remain > 0 ? remain : 0; - } - return 0; -}; - -export const isAuthedSelector = createSelector (decodedTokenSelector, isDecodedTokenAlive); -export const tokenTimeLeft = createSelector (decodedTokenSelector, decodedTokenLifetime); - -export const isAdminSelector = state => state.getIn (['auth', 'info', 'isAdmin']); -export const authPendingSelector = state => state.get ('pender').pending[CHECK_AUTH]; -export const isAdminOrPendingSelector = state => [isAdminSelector (state), authPendingSelector (state)]; - -const zabosSelector = (state, zaboIds) => [state.getIn (['zabo', 'zabos']), zaboIds]; -const zabosComputer = ([zabos, zaboIds]) => zaboIds.map (id => zabos.get (id)); - -// TODO: Factory function (due to cache size of 1) -export const zaboListFromIdsSelector = createSelector (zabosSelector, zabosComputer); - -const zaboSelector = (state, zaboId) => state.getIn (['zabo', 'zabos', zaboId]); -const ownGroupsSelector = (state) => state.getIn (['auth', 'info', 'groups']); -const isMyZabo = (zabo, groups) => !!groups.find (group => zabo && group.get ('_id') === zabo.getIn (['owner', '_id'])); -export const isMyZaboSelector = createSelector ([zaboSelector, ownGroupsSelector], isMyZabo); diff --git a/src/lib/utils/selector.ts b/src/lib/utils/selector.ts new file mode 100644 index 00000000..db61e093 --- /dev/null +++ b/src/lib/utils/selector.ts @@ -0,0 +1,41 @@ +import { get } from 'lodash'; +import { createSelector, ParametricSelector, Selector } from 'reselect'; +import { + IGroup, IJwt, IZabo, IZaboMap, +} from 'types/index.d'; +import { IState } from 'types/store.d'; + +import { CHECK_AUTH } from 'store/reducers/auth'; + +const decodedTokenSelector : Selector = (state : IState) => get (state, ['auth', 'jwt']); +const isDecodedTokenAlive = (decoded : IJwt | undefined) => { + if (decoded) { + return 1000 * decoded.exp - new Date ().getTime () >= 5000; + } + return false; +}; +const decodedTokenLifetime = (decoded : IJwt | undefined) : number => { + if (decoded) { + const remain = decoded.exp * 1000 - Date.now (); + return remain > 0 ? remain : 0; + } + return 0; +}; + +export const isAuthedSelector = createSelector (decodedTokenSelector, isDecodedTokenAlive); +export const tokenTimeLeft = createSelector (decodedTokenSelector, decodedTokenLifetime); + +export const isAdminSelector = (state : IState) => get (state, ['auth', 'info', 'isAdmin']); +export const authPendingSelector = (state : IState) => state.pender.pending[CHECK_AUTH]; +export const isAdminOrPendingSelector = (state : IState) => [isAdminSelector (state), authPendingSelector (state)]; + +const zabosSelector : ParametricSelector = (state : IState, zaboIds : string[]) : [IZaboMap, string[]] => [get (state, ['zabo', 'zabos']), zaboIds]; +const zabosComputer = ([zabos, zaboIds] : [IZaboMap, string[]]) => zaboIds.map ((id : string) => zabos[id]); + +// TODO: Factory function (due to cache size of 1) +export const zaboListFromIdsSelector = createSelector (zabosSelector, zabosComputer); + +const zaboSelector = (state : IState, zaboId : string) => get (state, ['zabo', 'zabos', zaboId]); +const ownGroupsSelector = (state : IState) => get (state, ['auth', 'info', 'groups']); +const isMyZabo = (zabo : IZabo, groups : IGroup[]) => !!groups.find ((group : IGroup) => zabo && group._id === get (zabo, ['owner', '_id'])); +export const isMyZaboSelector = createSelector ([zaboSelector, ownGroupsSelector], isMyZabo); diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts new file mode 100644 index 00000000..4f221e69 --- /dev/null +++ b/src/react-app-env.d.ts @@ -0,0 +1,12 @@ +/// +declare namespace NodeJS { + interface ProcessEnv { + NODE_ENV: 'development' | 'production' | 'test' | 'ci' + PUBLIC_URL: string + EXTEND_ESLINT: string + NODE_PATH: string + } +} +interface Window { + Stripe: any +} diff --git a/src/serviceWorker.js b/src/serviceWorker.ts similarity index 90% rename from src/serviceWorker.js rename to src/serviceWorker.ts index 9cb41321..134d4c8e 100644 --- a/src/serviceWorker.js +++ b/src/serviceWorker.ts @@ -10,6 +10,11 @@ // To learn more about the benefits of this model and instructions on how to // opt-in, read https://bit.ly/CRA-PWA +interface Config { + onUpdate : (registration : ServiceWorkerRegistration) => any; + onSuccess : (registration : ServiceWorkerRegistration) => any; +} + const isLocalhost = Boolean ( window.location.hostname === 'localhost' // [::1] is the IPv6 localhost address. @@ -18,10 +23,10 @@ const isLocalhost = Boolean ( || window.location.hostname.match (/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/), ); -function registerValidSW (swUrl, config) { +function registerValidSW (swUrl : string, config? : Config) { navigator.serviceWorker .register (swUrl) - .then (registration => { + .then ((registration) => { // eslint-disable-next-line no-param-reassign registration.onupdatefound = () => { const installingWorker = registration.installing; @@ -58,15 +63,15 @@ function registerValidSW (swUrl, config) { }; }; }) - .catch (error => { + .catch ((error) => { console.error ('Error during service worker registration:', error); }); } -function checkValidServiceWorker (swUrl, config) { +function checkValidServiceWorker (swUrl : string, config? : Config) { // Check if the service worker can be found. If it can't reload the page. fetch (swUrl) - .then (response => { + .then ((response) => { // Ensure service worker exists, and that we really are getting a JS file. const contentType = response.headers.get ('content-type'); if ( @@ -74,7 +79,7 @@ function checkValidServiceWorker (swUrl, config) { || (contentType != null && contentType.indexOf ('javascript') === -1) ) { // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then (registration => { + navigator.serviceWorker.ready.then ((registration) => { registration.unregister ().then (() => { window.location.reload (); }); @@ -89,7 +94,7 @@ function checkValidServiceWorker (swUrl, config) { }); } -export function register (config) { +export function register (config? : Config) { if ((process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) || true) { // The URL constructor is available in all browsers that support SW. const publicUrl = new URL (process.env.PUBLIC_URL, window.location.href); @@ -131,7 +136,7 @@ export function register (config) { export function unregister () { if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready.then (registration => { + navigator.serviceWorker.ready.then ((registration) => { registration.unregister (); }); } diff --git a/src/static/images/notFoundImage.jpg b/src/static/images/notFoundImage.jpg new file mode 100644 index 00000000..31805c70 Binary files /dev/null and b/src/static/images/notFoundImage.jpg differ diff --git a/src/store/example.js b/src/store/example.ts similarity index 55% rename from src/store/example.js rename to src/store/example.ts index 7a8d7a22..505c801e 100644 --- a/src/store/example.js +++ b/src/store/example.ts @@ -1,18 +1,31 @@ -const createStore = reducer => { - let state = {}; - let listeners = []; +type State = object; +interface IAction { + type : string; + [key : string] : any; +} +interface IReducer { + (state : State, action : IAction) : State; +} +interface IListener { + (state : State) : void; +} + +const createStore = (reducer : IReducer) => { + let state : State = {}; + let listeners : IListener[] = []; + const getState = () => state; - const dispatch = action => { + const dispatch = (action : IAction) => { state = reducer (state, action); listeners.forEach (listener => listener (state)); }; - const subscribe = listener => { + const subscribe = (listener : IListener) => { listeners.push (listener); return () => { listeners = listeners.filter (l => l !== listener); }; }; - dispatch ({}); + dispatch ({ type: 'Init' }); return { getState, dispatch, @@ -21,14 +34,16 @@ const createStore = reducer => { }; const reducer = ( - state = { + state : State = { /* Prev State */ }, - action, + action : IAction, ) => ({ // New State }); +export default createStore; + /* { showVisualText: false, diff --git a/src/store/index.js b/src/store/index.js index e42a19b4..76743204 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,4 +1,3 @@ -import { Map } from 'immutable'; import { applyMiddleware, createStore } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension'; import penderMiddleware from 'redux-pender'; @@ -7,13 +6,17 @@ import persist from './persist'; import rootReducer from './reducers'; const composeEnhancers = composeWithDevTools ({ - actionBlacklist: ['@@redux-pender/SUCCESS', '@@redux-pender/FAILURE', '@@redux-pender/PENDING'], + actionBlacklist: [ + '@@redux-pender/SUCCESS', + '@@redux-pender/FAILURE', + '@@redux-pender/PENDING', + ], maxAge: 1000, }); const store = createStore ( rootReducer, - Map (), // Initial state + {}, // Initial state composeEnhancers (applyMiddleware ( penderMiddleware (), persist, diff --git a/src/store/persist.js b/src/store/persist.js deleted file mode 100644 index 240a620d..00000000 --- a/src/store/persist.js +++ /dev/null @@ -1,22 +0,0 @@ -import { List } from 'immutable'; - -import serializer from 'lib/immutable'; -import storage from 'lib/storage'; - -const persistUpload = store => next => action => { - const result = next (action); - - if (action.type.substring (0, 6) === 'upload') { - const upload = store.getState () - .get ('upload') - .delete ('step', 0) - .delete ('imagesSelected') - .delete ('submitted', false) - .set ('images', List ([])); - storage.setItem ('uploadPersist', serializer.stringify (upload)); - } - - return result; // 여기서 반환하는 값은 store.dispatch(ACTION_TYPE) 했을때의 결과로 설정됩니다 -}; - -export default persistUpload; diff --git a/src/store/persist.ts b/src/store/persist.ts new file mode 100644 index 00000000..61015048 --- /dev/null +++ b/src/store/persist.ts @@ -0,0 +1,22 @@ +import { Store } from 'redux'; +import { Action } from 'redux-actions'; + +import storage from 'lib/storage'; + +const persistUpload = (store : Store) => (next : (action : Action) => any) => (action : Action) => { + const result = next (action); + + if (action.type.substring (0, 6) === 'upload') { + const { upload } = store.getState (); + const { + step, imagesSelected, submitted, ...persisted + } = upload; + persisted.images = []; + persisted.date = new Date (); + storage.setItem ('uploadPersist', JSON.stringify (persisted)); + } + + return result; // 여기서 반환하는 값은 store.dispatch(ACTION_TYPE) 했을때의 결과로 설정됩니다 +}; + +export default persistUpload; diff --git a/src/store/reducers/admin.js b/src/store/reducers/admin.ts similarity index 50% rename from src/store/reducers/admin.js rename to src/store/reducers/admin.ts index a7003c07..3ff690f1 100644 --- a/src/store/reducers/admin.js +++ b/src/store/reducers/admin.ts @@ -1,10 +1,16 @@ -import { fromJS, List, Map } from 'immutable'; +import produce, { setAutoFreeze } from 'immer'; import { createAction, handleActions } from 'redux-actions'; import { pender } from 'redux-pender'; +import { + IGroup, IGroupMap, IUser, IUserMap, +} from 'types/index.d'; import axios from 'lib/axios'; -const AdminAPIs = {}; +const AdminAPIs : { + [key : string] : (...params : any[]) => Promise +} = {}; + AdminAPIs.getUserList = () => axios.get ('/admin/user/list'); AdminAPIs.getGroupList = () => axios.get ('/admin/group/list'); AdminAPIs.getGroupApplyList = () => axios.get ('/admin/group/applies'); @@ -22,14 +28,24 @@ export const getGroupList = createAction (GET_GROUP_LIST, AdminAPIs.getGroupList export const getGroupApplyList = createAction (GET_GROUP_APPLY_LIST, AdminAPIs.getGroupApplyList); export const acceptApplyGroup = createAction (ACCEPT_APPLY_GROUP, AdminAPIs.acceptGroup, meta => meta); +export interface IAdminState { + pendingGroups : IGroup[]; + groups : IGroup[]; + users : IUser[]; + groupsMap : IGroupMap, + usersMap : IUserMap, +} + // initial state -const initialState = Map ({ - pendingGroups: List ([]), - groups: List ([]), - users: List ([]), - groupsMap: Map ({}), - usersMap: Map ({}), -}); +const initialState : IAdminState = { + pendingGroups: [], + groups: [], + users: [], + groupsMap: {}, + usersMap: {}, +}; + +setAutoFreeze (false); // reducer export default handleActions ( @@ -38,41 +54,45 @@ export default handleActions ( type: GET_USER_LIST, onSuccess: (state, action) => { const users = action.payload; - const usersMap = users.reduce ((acc, cur) => ({ ...acc, [cur.username]: cur }), {}); - return state - .set ('users', fromJS (action.payload)) - .set ('usersMap', fromJS (usersMap)); + const usersMap = users.reduce ((acc : IUserMap, cur : IUser) => ({ ...acc, [cur.username]: cur }), {}); + return produce (state, (draft : IAdminState) => { + draft.users = action.payload; + draft.usersMap = usersMap; + }); }, }), ...pender ({ type: GET_GROUP_LIST, onSuccess: (state, action) => { const groups = action.payload; - const groupsMap = groups.reduce ((acc, cur) => ({ ...acc, [cur.name]: cur }), {}); - return state - .set ('groups', fromJS (action.payload)) - .update ('groupsMap', prev => prev.merge (fromJS (groupsMap))); + const groupsMap = groups.reduce ((acc : IGroupMap, cur : IGroup) => ({ ...acc, [cur.name]: cur }), {}); + return produce (state, (draft : IAdminState) => { + draft.groups = action.payload; + Object.assign (draft.groupsMap, groupsMap); + }); }, }), ...pender ({ type: GET_GROUP_APPLY_LIST, onSuccess: (state, action) => { const groups = action.payload; - const groupsMap = groups.reduce ((acc, cur) => ({ ...acc, [cur.name]: cur }), {}); - return state - .set ('pendingGroups', fromJS (action.payload)) - .update ('groupsMap', prev => prev.merge (fromJS (groupsMap))); + const groupsMap = groups.reduce ((acc : IGroupMap, cur : IGroup) => ({ ...acc, [cur.name]: cur }), {}); + return produce (state, (draft : IAdminState) => { + draft.pendingGroups = action.payload; + Object.assign (draft.groupsMap, groupsMap); + }); }, }), ...pender ({ type: ACCEPT_APPLY_GROUP, onSuccess: (state, action) => { const { name } = action.meta; - const group = fromJS (action.payload); - return state - .update ('pendingGroups', prev => prev.filter (group => group.get ('name') !== name)) - .update ('groups', prev => prev.unshift (group)) - .update ('groupsMap', prev => prev.merge ({ [group.name]: group })); + const group = action.payload; + return produce (state, (draft : IAdminState) => { + draft.pendingGroups = draft.pendingGroups.filter (pendingGroup => pendingGroup.name !== name); + draft.groups.unshift (group); + Object.assign (draft.groupsMap, { [group.name]: group }); + }); }, }), }, diff --git a/src/store/reducers/app.js b/src/store/reducers/app.js deleted file mode 100644 index 842e1c17..00000000 --- a/src/store/reducers/app.js +++ /dev/null @@ -1,33 +0,0 @@ -import { fromJS, Map } from 'immutable'; -import { createAction, handleActions } from 'redux-actions'; - -// action types -const UPDATE_WINDOW_SIZE = 'app/UPDATE_WINDOW_SIZE'; - -// action creators -export const setWindowSize = createAction (UPDATE_WINDOW_SIZE); - -// initial state -const initialState = Map ({ - windowSize: Map ({ - width: window.innerWidth, - height: window.innerHeight, - }), -}); - -// reducer -export default handleActions ( - { - [UPDATE_WINDOW_SIZE]: (state, action) => { - const { width, height } = action.payload; - return state.set ( - 'windowSize', - fromJS ({ - width, - height, - }), - ); - }, - }, - initialState, -); diff --git a/src/store/reducers/app.ts b/src/store/reducers/app.ts new file mode 100644 index 00000000..f45a4d1e --- /dev/null +++ b/src/store/reducers/app.ts @@ -0,0 +1,34 @@ +import produce from 'immer'; +import { Action, createAction, handleActions } from 'redux-actions'; + +// action types +const UPDATE_WINDOW_SIZE = 'app/UPDATE_WINDOW_SIZE'; + +// action creators +export const setWindowSize = createAction (UPDATE_WINDOW_SIZE); + +interface IWindowSize { + width : number; + height : number; +} +export interface IAppState { + windowSize : IWindowSize; +} + +// initial state +const initialState : IAppState = { + windowSize: { + width: window.innerWidth, + height: window.innerHeight, + }, +}; + +// reducer +export default handleActions ( + { + [UPDATE_WINDOW_SIZE]: (state, action : Action) => produce (state, (draft : IAppState) => { + draft.windowSize = action.payload; + }), + }, + initialState, +); diff --git a/src/store/reducers/auth.js b/src/store/reducers/auth.ts similarity index 60% rename from src/store/reducers/auth.js rename to src/store/reducers/auth.ts index 909b0981..ef511d29 100644 --- a/src/store/reducers/auth.js +++ b/src/store/reducers/auth.ts @@ -1,7 +1,9 @@ -import { fromJS, List, Map } from 'immutable'; +import produce from 'immer'; import jwt from 'jsonwebtoken'; +import { get } from 'lodash'; import { createAction, handleActions } from 'redux-actions'; import { pender } from 'redux-pender'; +import { IGroup, IJwt, IUser } from 'types/index.d'; import * as AuthAPIs from 'lib/api/auth'; import * as GroupAPIs from 'lib/api/group'; @@ -29,14 +31,18 @@ export const updateGroupInfoWithImage = createAction (UPDATE_GROUP_INFO, GroupAP export const setCurrentGroup = createAction (SET_CURRENT_GROUP, UserAPIs.setCurrentGroup); export const applyNewGroup = createAction (APPLY_NEW_GROUP, GroupAPIs.applyNewGroup); +export interface IAuthState { + jwt ? : IJwt, + info ? : IUser, +} /* * group : { _id: String, name: String, members: [member] } * member: { _id: String, studentId: String, isAdmin: Boolean } * board: { _id: String, title: String } */ -const initialState = Map ({ - jwt: Map ({}), - info: Map ({ +const initialState : IAuthState = { + jwt: undefined, + info: { _id: '', sso_uid: '', sso_sid: '', @@ -51,12 +57,12 @@ const initialState = Map ({ studentId: '', currentGroup: null, isAdmin: false, - flags: List ([]), - boards: List ([]), - groups: List ([]), - pendingGroups: List ([]), - }), -}); + flags: [], + boards: [], + groups: [], + pendingGroups: [], + }, +}; export default handleActions ( { @@ -67,52 +73,69 @@ export default handleActions ( const decoded = jwt.decode (token); storage.setItem ('token', token); axios.updateToken (token); - const currentGroup = user.groups.find (group => group._id === user.currentGroup); + const currentGroup = user.groups.find ((group : IGroup) => group._id === user.currentGroup); if (currentGroup) user.currentGroup = currentGroup; - return state - .set ('jwt', fromJS (decoded)) - .set ('info', fromJS (user)); + return produce (state, (draft : IAuthState) => { + if (decoded && typeof decoded === 'object') draft.jwt = decoded as IJwt; + draft.info = user; + }); }, }), ...pender ({ type: CHECK_AUTH, - onPending: (state, action) => state.set ('jwt', fromJS (jwt.decode (action.meta))), + onPending: (state, action) => produce (state, (draft : IAuthState) => { + const decoded = jwt.decode (action.meta); + if (decoded && typeof decoded === 'object') draft.jwt = decoded as IJwt; + }), onSuccess: (state, action) => { const user = action.payload; - const currentGroup = user.groups.find (group => group._id === user.currentGroup); + const currentGroup = user.groups.find ((group : IGroup) => group._id === user.currentGroup); if (currentGroup) user.currentGroup = currentGroup; - return state.set ('info', fromJS (user)); + return produce (state, (draft : IAuthState) => { + draft.info = user; + }); }, }), - [LOGOUT]: (state) => { + [LOGOUT]: state => { storage.removeItem ('token'); axios.updateToken (''); window.location.href = '/'; - return state.set ('jwt', initialState.get ('jwt')).set ('info', initialState.get ('info')); + return produce (state, (draft : IAuthState) => { + draft.jwt = initialState.jwt; + draft.info = initialState.info; + }); }, ...pender ({ type: SET_CURRENT_GROUP, onSuccess: (state, action) => { const { currentGroup: currentGroupId } = action.payload; - const currentGroup = state.getIn (['info', 'groups']).find (group => group.get ('_id') === currentGroupId); - return state.setIn (['info', 'currentGroup'], currentGroup); + const currentGroup = get (state, ['info', 'groups']).find ((group : IGroup) => group._id === currentGroupId); + return produce (state, (draft : IAuthState) => { + if (draft.info) draft.info.currentGroup = currentGroup; + }); }, }), ...pender ({ type: UPDATE_USER_INFO, - onSuccess: (state, action) => state.update ('info', prev => prev.merge (fromJS (action.payload))), + onSuccess: (state, action) => produce (state, (draft : IAuthState) => { + Object.assign (draft.info, action.payload); + }), }), ...pender ({ type: UPDATE_GROUP_INFO, onSuccess: (state, action) => { const { name } = action.meta; - const groupIndex = state.getIn (['info', 'groups']).findIndex (group => group.get ('name') === name); - return state.updateIn (['info', 'groups', groupIndex], prev => prev.merge (fromJS (action.payload))); + const groupIndex = get (state, ['info', 'groups']).findIndex ((group : IGroup) => group.name === name); + return produce (state, (draft : IAuthState) => { + if (draft.info) Object.assign (draft.info.groups[groupIndex], action.payload); + }); }, }), ...pender ({ type: APPLY_NEW_GROUP, - onSuccess: (state, action) => state.updateIn (['info', 'pendingGroups'], prev => prev.push (fromJS (action.payload))), + onSuccess: (state, action) => produce (state, (draft : IAuthState) => { + if (draft.info) draft.info.pendingGroups.push (action.payload); + }), }), }, initialState, diff --git a/src/store/reducers/index.js b/src/store/reducers/index.ts similarity index 63% rename from src/store/reducers/index.js rename to src/store/reducers/index.ts index bc412050..6fad0d4a 100644 --- a/src/store/reducers/index.js +++ b/src/store/reducers/index.ts @@ -1,17 +1,21 @@ -import { combineReducers } from 'redux-immutable'; +import { combineReducers } from 'redux'; +import { handleActions } from 'redux-actions'; import { penderReducer } from 'redux-pender'; // import immutableTransform from 'redux-persist-transform-immutable'; // import storage from 'redux-persist/lib/storage'; // import { persistReducer } from 'redux-persist'; // import all files except index.js -const req = require.context ('.', true, /^(?!.\/index).*.js$/); +const req = require.context ('.', true, /^(?!.\/index).*.ts$/); -const modules = {}; +const modules : { + [key : string] : ReturnType | typeof penderReducer +} = {}; req.keys ().forEach (key => { - const regex = /.\/(.*?).js$/; - const moduleName = regex.test (key) && key.match (regex)[1]; + const regex = /.\/(.*?).ts$/; + const moduleName = regex.test (key) && (key.match (regex) || [])[1]; + if (!moduleName) return; modules[moduleName] = req (key).default; }); diff --git a/src/store/reducers/profile.js b/src/store/reducers/profile.ts similarity index 63% rename from src/store/reducers/profile.js rename to src/store/reducers/profile.ts index dc47c4bf..b2e85b5c 100644 --- a/src/store/reducers/profile.js +++ b/src/store/reducers/profile.ts @@ -1,9 +1,11 @@ -import { fromJS, Map } from 'immutable'; +import produce from 'immer'; +import { set } from 'lodash'; import { createAction, handleActions } from 'redux-actions'; import { pender } from 'redux-pender'; +import { IProfileMap } from 'types/index.d'; -import * as GroupAPIs from '../../lib/api/group'; -import * as ProfileAPIs from '../../lib/api/profile'; +import * as GroupAPIs from 'lib/api/group'; +import * as ProfileAPIs from 'lib/api/profile'; const GET_PROFILE = 'profile/GET_PROFILE'; const ADD_GROUP_MEMBER = 'profile/ADD_GROUP_MEMBER'; @@ -17,9 +19,12 @@ export const updateGroupMember = createAction (UPDATE_GROUP_MEMBER, GroupAPIs.up export const removeGroupMember = createAction (REMOVE_GROUP_MEMBER, GroupAPIs.removeGroupUser, meta => meta); export const followProfile = createAction (FOLLOW_PROFILE, ProfileAPIs.followProfile, meta => meta); -const initialState = Map ({ - profiles: Map ({}), -}); +export interface IProfileState { + profiles : IProfileMap +} +const initialState : IProfileState = { + profiles: {}, +}; export default handleActions ({ ...pender ({ @@ -27,12 +32,16 @@ export default handleActions ({ onSuccess: (state, action) => { const profile = action.payload; const name = action.meta; - return state.setIn (['profiles', name], fromJS (profile)); + return produce (state, (draft : IProfileState) => { + draft.profiles[name] = profile; + }); }, onFailure: (state, action) => { const error = action.payload; const name = action.meta; - return state.setIn (['profiles', name], fromJS (error)); + return produce (state, (draft : IProfileState) => { + draft.profiles[name] = error; + }); }, }), ...pender ({ @@ -40,7 +49,9 @@ export default handleActions ({ onSuccess: (state, action) => { const { members } = action.payload; const { groupName } = action.meta; - return state.setIn (['profiles', groupName, 'members'], fromJS (members)); + return produce (state, (draft : IProfileState) => { + set (draft, ['profile', groupName, 'members'], members); + }); }, }), ...pender ({ @@ -48,7 +59,9 @@ export default handleActions ({ onSuccess: (state, action) => { const { members } = action.payload; const { groupName } = action.meta; - return state.setIn (['profiles', groupName, 'members'], fromJS (members)); + return produce (state, (draft : IProfileState) => { + set (draft, ['profiles', groupName, 'members'], members); + }); }, }), ...pender ({ @@ -56,15 +69,18 @@ export default handleActions ({ onSuccess: (state, action) => { const { members } = action.payload; const { groupName } = action.meta; - return state.setIn (['profiles', groupName, 'members'], fromJS (members)); + return produce (state, (draft : IProfileState) => { + set (draft, ['profiles', groupName, 'members'], members); + }); }, }), ...pender ({ type: FOLLOW_PROFILE, onSuccess: (state, action) => { - const updatedProfile = action.payload; const { name } = action.meta; - return state.updateIn (['profiles', name], profile => profile.merge (updatedProfile)); + return produce (state, (draft : IProfileState) => { + Object.assign (draft.profiles[name], action.payload); + }); }, }), }, initialState); diff --git a/src/store/reducers/upload.js b/src/store/reducers/upload.js deleted file mode 100644 index 3d4515d6..00000000 --- a/src/store/reducers/upload.js +++ /dev/null @@ -1,88 +0,0 @@ -import { fromJS, List, Map } from 'immutable'; -import { createAction, handleActions } from 'redux-actions'; - -import storage from 'lib/storage'; -import { ZABO_CATEGORIES } from 'lib/variables'; - -// action types -const INITIALIZE = 'upload/INITIALIZE'; -const SET_STEP = 'upload/SET_STEP'; -const SET_GROUP_SELECTED = 'upload/SET_GROUP_SELECTED'; -const SET_IMAGES_SELECTED = 'upload/SET_IMAGES_SELECTED'; -const SET_IMAGES = 'upload/SET_IMAGES'; -const SET_INFO = 'upload/SET_INFO'; -const RESET = 'upload/RESET'; -const SET_MODAL = 'upload/SET_MODAL'; -const SUBMIT = 'upload/SUBMIT'; - -// action creators -export const initialize = createAction (INITIALIZE); -export const setStep = createAction (SET_STEP); -export const setGroupSelected = createAction (SET_GROUP_SELECTED); -export const setImagesSeleted = createAction (SET_IMAGES_SELECTED); -export const setImages = createAction (SET_IMAGES); -export const setInfo = createAction (SET_INFO); -export const reset = createAction (RESET); -export const setModal = createAction (SET_MODAL); -export const submit = createAction (SUBMIT); - -const date = new Date (); -date.setDate (date.getDate () + 7); -date.setHours (0); -date.setMinutes (0); -date.setSeconds (0); -export const defaultSchedule = { - title: '', - startAt: date, - eventType: '행사', -}; - -// initial state -const initialState = Map ({ - step: 0, - groupSelected: false, - imagesSelected: false, - submitted: false, - images: List ([]), - info: Map ({ - title: '', - description: '', - hasSchedule: false, - schedules: List ([ - Map (defaultSchedule), - ]), - category: List (ZABO_CATEGORIES.map (tag => ({ name: tag, clicked: false }))), - }), - edit: Map ({}), - showModal: false, -}); - -// reducer -export default handleActions ( - { - [INITIALIZE]: (state, action) => state.merge ( - action.payload, - ), - [SET_STEP]: (state, action) => state.set ('step', action.payload), - [SET_GROUP_SELECTED]: (state, action) => { - const groupSelected = action.payload; - return state.set ('groupSelected', groupSelected); - }, - [SET_IMAGES_SELECTED]: (state, action) => state.set ('imagesSelected', action.payload), - [SUBMIT]: (state, action) => state.set ('submitted', action.payload), - [SET_IMAGES]: (state, action) => { - const images = action.payload; - return state.set ('images', fromJS (images)); - }, - [SET_INFO]: (state, action) => { - const info = action.payload; - return state.set ('info', fromJS (info)); - }, - [SET_MODAL]: (state, action) => state.set ('showModal', action.payload), - [RESET]: () => { - storage.removeItem ('uploadPersist'); - return initialState; - }, - }, - initialState, -); diff --git a/src/store/reducers/upload.ts b/src/store/reducers/upload.ts new file mode 100644 index 00000000..63da7257 --- /dev/null +++ b/src/store/reducers/upload.ts @@ -0,0 +1,113 @@ +import produce from 'immer'; +import { Action, createAction, handleActions } from 'redux-actions'; + +import storage from 'lib/storage'; +import { ZABO_CATEGORIES } from 'lib/variables'; + +// action types +const INITIALIZE = 'upload/INITIALIZE'; +const SET_STEP = 'upload/SET_STEP'; +const SET_GROUP_SELECTED = 'upload/SET_GROUP_SELECTED'; +const SET_IMAGES_SELECTED = 'upload/SET_IMAGES_SELECTED'; +const SET_IMAGES = 'upload/SET_IMAGES'; +const SET_INFO = 'upload/SET_INFO'; +const RESET = 'upload/RESET'; +const SET_MODAL = 'upload/SET_MODAL'; +const SUBMIT = 'upload/SUBMIT'; + +// action creators +export const initialize = createAction (INITIALIZE); +export const setStep = createAction (SET_STEP); +export const setGroupSelected = createAction (SET_GROUP_SELECTED); +export const setImagesSeleted = createAction (SET_IMAGES_SELECTED); +export const setImages = createAction (SET_IMAGES); +export const setInfo = createAction (SET_INFO); +export const reset = createAction (RESET); +export const setModal = createAction (SET_MODAL); +export const submit = createAction (SUBMIT); + +const date = new Date (); +date.setDate (date.getDate () + 7); +date.setHours (0); +date.setMinutes (0); +date.setSeconds (0); + +export interface ISchedule { + title : string; + startAt : Date + eventType : string; +} + +export interface IUploadState { + step : number, + groupSelected : boolean, + imagesSelected : boolean, + submitted : boolean, + images : any[], + info : { + title : string, + description : string, + hasSchedule : boolean, + schedules : ISchedule[], + category : { name : string, clicked : boolean }[], + }, + showModal : boolean, +} + +export const defaultSchedule : ISchedule = { + title: '', + startAt: date, + eventType: '행사', +}; + +// initial state +const initialState : IUploadState = { + step: 0, + groupSelected: false, + imagesSelected: false, + submitted: false, + images: [], + info: { + title: '', + description: '', + hasSchedule: false, + schedules: [defaultSchedule], + category: ZABO_CATEGORIES.map (tag => ({ name: tag, clicked: false })), + }, + showModal: false, +}; + +// reducer +export default handleActions ( + { + [INITIALIZE]: (state, action) => produce (state, (draft : IUploadState) => { + Object.assign (draft, action.payload); + }), + [SET_STEP]: (state, action : Action) => produce (state, (draft : IUploadState) => { + draft.step = action.payload; + }), + [SET_GROUP_SELECTED]: (state, action : Action) => produce (state, (draft : IUploadState) => { + draft.groupSelected = action.payload; + }), + [SET_IMAGES_SELECTED]: (state, action : Action) => produce (state, (draft : IUploadState) => { + draft.imagesSelected = action.payload; + }), + [SUBMIT]: (state, action : Action) => produce (state, (draft : IUploadState) => { + draft.submitted = action.payload; + }), + [SET_IMAGES]: (state, action : Action) => produce (state, (draft : IUploadState) => { + draft.images = action.payload; + }), + [SET_INFO]: (state, action : Action) => produce (state, (draft : IUploadState) => { + draft.info = action.payload; + }), + [SET_MODAL]: (state, action : Action) => produce (state, (draft : IUploadState) => { + draft.showModal = action.payload; + }), + [RESET]: () => { + storage.removeItem ('uploadPersist'); + return initialState; + }, + }, + initialState, +); diff --git a/src/store/reducers/zabo.js b/src/store/reducers/zabo.js deleted file mode 100644 index bf253afa..00000000 --- a/src/store/reducers/zabo.js +++ /dev/null @@ -1,184 +0,0 @@ -import { fromJS, List, Map } from 'immutable'; -import uniq from 'lodash.uniq'; -import { createAction, handleActions } from 'redux-actions'; -import { pender } from 'redux-pender'; - -import * as SearchAPI from 'lib/api/search'; -import * as ZaboAPI from 'lib/api/zabo'; - -// Action types -const GET_HOT_ZABO_LIST = 'zabo/GET_HOT_ZABO_LIST'; -const GET_ZABO_LIST = 'zabo/GET_ZABO_LIST'; -const UPLOAD_ZABO = 'zabo/UPLOAD_ZABO'; -const PATCH_ZABO = 'zabo/PATCH_ZABO'; -const GET_ZABO = 'zabo/GET_ZABO'; -const GET_PINS = 'zabo/GET_PINS'; -const TOGGLE_ZABO_PIN = 'zabo/TOGGLE_ZABO_PIN'; -const TOGGLE_ZABO_LIKE = 'zabo/TOGGLE_ZABO_LIKE'; -const GET_GROUP_ZABO_LIST = 'zabo/GET_GROUP_ZABO_LIST'; -const GET_SEARCH_ZABO_LIST = 'zabo/GET_SEARCH_ZABO_LIST'; -const GET_SEARCH = 'zabo/GET_SEARCH'; -const DELETE_ZABO = 'zabo/DELETE_ZABO'; - -// Action creator : action 객체를 만들어주는 함수 -export const getHotZaboList = createAction (GET_HOT_ZABO_LIST, ZaboAPI.getHotZaboList); -export const uploadZabo = createAction (UPLOAD_ZABO, ZaboAPI.uploadZabo, meta => meta); -export const patchZabo = createAction (PATCH_ZABO, ZaboAPI.patchZabo, meta => meta); -export const getZaboList = createAction (GET_ZABO_LIST, ZaboAPI.getZaboList, meta => meta); -export const getZabo = createAction (GET_ZABO, ZaboAPI.getZabo, meta => meta); -export const getPins = createAction (GET_PINS, ZaboAPI.getPins, meta => meta); -export const toggleZaboPin = createAction (TOGGLE_ZABO_PIN, ZaboAPI.toggleZaboPin, meta => meta); -export const toggleZaboLike = createAction (TOGGLE_ZABO_LIKE, ZaboAPI.toggleZaboLike, meta => meta); -export const getGroupZaboList = createAction (GET_GROUP_ZABO_LIST, ZaboAPI.getGroupZaboList, meta => meta); -export const getSearchZaboList = createAction (GET_SEARCH_ZABO_LIST, ZaboAPI.getSearchZaboList, meta => meta); -export const getSearch = createAction (GET_SEARCH, SearchAPI.searchAPI, meta => meta); -export const deleteZabo = createAction (DELETE_ZABO, ZaboAPI.deleteZabo, meta => meta); - -// 초기값 설정 -const initialState = Map ({ - lists: Map ({ - // Using group name as a key, keys in this map should be RESERVED as name in server side - pins: List ([]), - main: List ([]), - search: List ([]), - }), - zabos: Map ({}), -}); - -// Reducer 함수 : state, action 을 받아 다음 상태를 만들어서 반환. -export default handleActions ( - { - ...pender ({ - type: UPLOAD_ZABO, - onSuccess: (state, action) => { - const zabo = action.payload; - return state.setIn (['zabos', zabo._id], fromJS (zabo)); - }, - }), - ...pender ({ - type: PATCH_ZABO, - onSuccess: (state, action) => { - const { zaboId } = action.meta; - return state.updateIn (['zabos', zaboId], prev => prev.merge (fromJS (action.payload))); - }, - }), - ...pender ({ - type: DELETE_ZABO, - onSuccess: (state, action) => state, - }), - ...pender ({ - type: GET_ZABO, - onSuccess: (state, action) => { - const zabo = action.payload; - return state.setIn (['zabos', zabo._id], fromJS (zabo)); - }, - onFailure: (state, action) => { - const error = action.payload; - const zaboId = action.meta; - return state.setIn (['zabos', zaboId], fromJS (error)); - }, - }), - ...pender ({ - type: GET_HOT_ZABO_LIST, - onSuccess: (state, action) => { - const zaboList = action.payload; - const zaboMap = zaboList.reduce ((acc, cur) => ({ ...acc, [cur._id]: cur }), {}); - const zaboIds = zaboList.map (zabo => zabo._id); - return state - .update ('zabos', zabos => zabos.merge (fromJS (zaboMap))) - .setIn (['lists', 'hot'], zaboIds); - }, - }), - ...pender ({ - type: GET_ZABO_LIST, - onSuccess: (state, action) => { - const zaboList = action.payload; - const { lastSeen, relatedTo } = action.meta; - const key = relatedTo || 'main'; - - const zaboMap = zaboList.reduce ((acc, cur) => ({ ...acc, [cur._id]: cur }), {}); - const zaboIds = zaboList.map (zabo => zabo._id); - - if (!lastSeen) { - return state - .updateIn (['lists', key], prevList => fromJS (uniq ([...zaboIds, ...(prevList ? prevList.toJS () : [])]))) - .update ('zabos', zabos => zabos.merge (fromJS (zaboMap))); - } - return state - .update ('zabos', zabos => zabos.merge (fromJS (zaboMap))) - .updateIn (['lists', key], prevList => fromJS (uniq ([...(prevList ? prevList.toJS () : []), ...zaboIds]))); - }, - }), - ...pender ({ - type: GET_PINS, - onSuccess: (state, action) => { - const pins = action.payload; - const { lastSeen } = action.meta; - - const zaboMap = pins.reduce ((acc, cur) => ({ ...acc, [cur._id]: cur }), {}); - const zaboIds = pins.map (pin => pin._id); - - if (!lastSeen) { - return state - .update ('zabos', zabos => zabos.merge (fromJS (zaboMap))) - .setIn (['lists', 'pins'], fromJS (zaboIds)); - } - return state - .update ('zabos', zabos => zabos.merge (fromJS (zaboMap))) - .updateIn (['lists', 'pins'], prevList => prevList.merge (fromJS (zaboIds))); - }, - }), - ...pender ({ - type: TOGGLE_ZABO_PIN, - onSuccess: (state, action) => state.updateIn (['zabos', action.meta], zabo => zabo.merge (fromJS (action.payload))), - }), - ...pender ({ - type: TOGGLE_ZABO_LIKE, - onSuccess: (state, action) => state.updateIn (['zabos', action.meta], zabo => zabo.merge (fromJS (action.payload))), - }), - ...pender ({ - type: GET_GROUP_ZABO_LIST, - onSuccess: (state, action) => { - const zabos = action.payload; - const { groupName } = action.meta; - const zaboMap = zabos.reduce ((acc, cur) => ({ ...acc, [cur._id]: cur }), {}); - const zaboIds = zabos.map (zabo => zabo._id); - return state - .update ('zabos', zabos => zabos.merge (fromJS (zaboMap))) - .setIn (['lists', groupName], fromJS (zaboIds)); - }, - }), - ...pender ({ - type: GET_SEARCH_ZABO_LIST, - onSuccess: (state, action) => { - const zabos = action.payload; - const { lastSeen, text } = action.meta; - const zaboMap = zabos.reduce ((acc, cur) => ({ ...acc, [cur._id]: cur }), {}); - const zaboIds = zabos.map (zabo => zabo._id); - - // !lastSeen case don't need -> check this case when zaboList fetch - if (!lastSeen) { - return state - .update ('zabos', zabos => zabos.merge (fromJS (zaboMap))) - .setIn (['lists', 'search'], fromJS (zaboIds)); - } - return state - .update ('zabos', zabos => zabos.merge (fromJS (zaboMap))) - .updateIn (['lists', 'search'], prevList => prevList.merge (fromJS (zaboIds))); - }, - }), - ...pender ({ - type: GET_SEARCH, - onSuccess: (state, action) => { - const data = action.payload; - const { zabos } = data; - const zaboMap = zabos.reduce ((acc, cur) => ({ ...acc, [cur._id]: cur }), {}); - const zaboIds = zabos.map (zabo => zabo._id); - return state - .update ('zabos', zabos => zabos.merge (fromJS (zaboMap))) - .setIn (['lists', 'search'], fromJS (zaboIds)); - }, - }), - }, - initialState, -); diff --git a/src/store/reducers/zabo.ts b/src/store/reducers/zabo.ts new file mode 100644 index 00000000..2b5885c2 --- /dev/null +++ b/src/store/reducers/zabo.ts @@ -0,0 +1,170 @@ +import produce from 'immer'; +import { get, set, uniq } from 'lodash'; +import { createAction, handleActions } from 'redux-actions'; +import { pender } from 'redux-pender'; +import { IZabo, IZaboMap } from 'types/index.d'; + +import * as SearchAPI from 'lib/api/search'; +import * as ZaboAPI from 'lib/api/zabo'; + +// Action types +const GET_HOT_ZABO_LIST = 'zabo/GET_HOT_ZABO_LIST'; +const GET_ZABO_LIST = 'zabo/GET_ZABO_LIST'; +const UPLOAD_ZABO = 'zabo/UPLOAD_ZABO'; +const PATCH_ZABO = 'zabo/PATCH_ZABO'; +const GET_ZABO = 'zabo/GET_ZABO'; +const GET_PINS = 'zabo/GET_PINS'; +const TOGGLE_ZABO_PIN = 'zabo/TOGGLE_ZABO_PIN'; +const TOGGLE_ZABO_LIKE = 'zabo/TOGGLE_ZABO_LIKE'; +const GET_GROUP_ZABO_LIST = 'zabo/GET_GROUP_ZABO_LIST'; +const GET_SEARCH_ZABO_LIST = 'zabo/GET_SEARCH_ZABO_LIST'; +const GET_SEARCH = 'zabo/GET_SEARCH'; +const DELETE_ZABO = 'zabo/DELETE_ZABO'; + +// Action creator : action 객체를 만들어주는 함수 +export const getHotZaboList = createAction (GET_HOT_ZABO_LIST, ZaboAPI.getHotZaboList); +export const uploadZabo = createAction (UPLOAD_ZABO, ZaboAPI.uploadZabo, meta => meta); +export const patchZabo = createAction (PATCH_ZABO, ZaboAPI.patchZabo, meta => meta); +export const getZaboList = createAction (GET_ZABO_LIST, ZaboAPI.getZaboList, meta => meta); +export const getZabo = createAction (GET_ZABO, ZaboAPI.getZabo, meta => meta); +export const getPins = createAction (GET_PINS, ZaboAPI.getPins, meta => meta); +export const toggleZaboPin = createAction (TOGGLE_ZABO_PIN, ZaboAPI.toggleZaboPin, meta => meta); +export const toggleZaboLike = createAction (TOGGLE_ZABO_LIKE, ZaboAPI.toggleZaboLike, meta => meta); +export const getGroupZaboList = createAction (GET_GROUP_ZABO_LIST, ZaboAPI.getGroupZaboList, meta => meta); +export const getSearchZaboList = createAction (GET_SEARCH_ZABO_LIST, ZaboAPI.getSearchZaboList, meta => meta); +export const getSearch = createAction (GET_SEARCH, SearchAPI.searchAPI, meta => meta); +export const deleteZabo = createAction (DELETE_ZABO, ZaboAPI.deleteZabo, meta => meta); + +export interface IZaboState { + lists : { + pins : IZabo[], + main : IZabo[], + search : IZabo[], + [key : string] : IZabo[], + }, + zabos : IZaboMap, +} + +// 초기값 설정 +const initialState : IZaboState = { + lists: { + // Using group name as a key, keys in this map should be RESERVED as name in server side + pins: [], + main: [], + search: [], + }, + zabos: {}, +}; + +const zaboListParser = (zaboList : IZabo[]) => { + const map = zaboList.reduce ((acc, cur) => ({ ...acc, [cur._id]: cur }), {}); + const ids = zaboList.map (zabo => zabo._id); + return { map, ids }; +}; + +const zaboListProducer = (zaboList : IZabo[], key : string, lastSeen : string | undefined | boolean, override : boolean = false) => { + const { map: zaboMap, ids: zaboIds } = zaboListParser (zaboList); + return (draft : IZaboState) => { + const prevIdList = get (draft, ['lists', key], []); + const newIdList = override + ? zaboIds + : lastSeen ? [...prevIdList, ...zaboIds] : [...zaboIds, ...prevIdList]; + set (draft, ['lists', key], uniq (newIdList)); + Object.assign (draft.zabos, zaboMap); + }; +}; + +// Reducer 함수 : state, action 을 받아 다음 상태를 만들어서 반환. +export default handleActions ( + { + ...pender ({ + type: UPLOAD_ZABO, + onSuccess: (state, action) => { + const zabo = action.payload; + return produce (state, (draft : IZaboState) => { + set (draft, ['zabos', zabo._id], zabo); + }); + }, + }), + ...pender ({ + type: PATCH_ZABO, + onSuccess: (state, action) => { + const { zaboId } = action.meta; + return produce (state, (draft : IZaboState) => { + Object.assign (draft.zabos[zaboId], action.payload); + }); + }, + }), + ...pender ({ + type: DELETE_ZABO, + onSuccess: (state, action) => state, + }), + ...pender ({ + type: GET_ZABO, + onSuccess: (state, action) => { + const zabo = action.payload; + return produce (state, (draft : IZaboState) => { + set (draft, ['zabos', zabo._id], zabo); + }); + }, + onFailure: (state, action) => produce (state, (draft : IZaboState) => { + set (draft, ['zabos', action.meta], action.payload); + }), + }), + ...pender ({ + type: GET_HOT_ZABO_LIST, + onSuccess: (state, action) => produce (state, zaboListProducer (action.payload, 'hot', false, true)), + }), + ...pender ({ + type: GET_ZABO_LIST, + onSuccess: (state, action) => { + const zaboList = action.payload; + const { lastSeen, relatedTo } = action.meta; + const key = relatedTo || 'main'; + return produce (state, zaboListProducer (zaboList, key, lastSeen, false)); + }, + }), + ...pender ({ + type: GET_PINS, + onSuccess: (state, action) => { + const { lastSeen } = action.meta; + return produce (state, zaboListProducer (action.payload, 'pins', lastSeen, false)); + }, + }), + ...pender ({ + type: TOGGLE_ZABO_PIN, + onSuccess: (state, action) => produce (state, (draft : IZaboState) => { + Object.assign (draft.zabos[action.meta], action.payload); + }), + }), + ...pender ({ + type: TOGGLE_ZABO_LIKE, + onSuccess: (state, action) => produce (state, (draft : IZaboState) => { + Object.assign (draft.zabos[action.meta], action.payload); + }), + }), + ...pender ({ + type: GET_GROUP_ZABO_LIST, + onSuccess: (state, action) => { + const { groupName, lastSeen } = action.meta; + return produce (state, zaboListProducer (action.payload, groupName, lastSeen, false)); + }, + }), + ...pender ({ + type: GET_SEARCH_ZABO_LIST, + onSuccess: (state, action) => { + const { lastSeen, text } = action.meta; + return produce (state, zaboListProducer (action.payload, 'search', lastSeen, !lastSeen)); + }, + }), + ...pender ({ + type: GET_SEARCH, + onSuccess: (state, action) => { + const data = action.payload; + const { zabos } = data; + return produce (state, zaboListProducer (zabos, 'search', false, true)); + }, + }), + }, + initialState, +); diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 00000000..67c3d6ef --- /dev/null +++ b/src/types/index.d.ts @@ -0,0 +1,102 @@ +export interface IJwt { + _id : string; + sid : string; + email : string; + username : string; + iat : number; + exp : number; + iss : string; +} + +export interface IZabo { + _id : string, + title : string, + owner : { + _id : string, + name : string, + profilePhoto : string, + subtitle : string, + }, + description : string, + category : string[], + photos : [{ + height : number, + width : number, + url : string, + }], + views : number, + effectiveViews : number, + createdAt : string, + schedules : [{ + title : string, + startAt : string, + endAt : string, + type : string, + }], + isLiked : boolean, + isPinned : boolean, + isMyZabo : boolean, +} + +export interface IZaboMap { + [key : string] : IZabo; +} + +export interface IGroup { + _id : string, + name : string, + profilePhoto : string, + stats : { + zaboCount : number, + followerCount : number, + recentUploadDate : string, + }, + myRole : 'admin' | 'editor', +} + +export interface IGroupMap { + [key : string] : IGroup; +} + +export interface IUser { + _id : string, + // eslint-disable-next-line camelcase + sso_uid ? : string, + // eslint-disable-next-line camelcase + sso_sid ? : string, + gender ? : string, + email ?: string, + kaistPersonType ? : string, + isAdmin ? : boolean, + flags : string[], + pendingGroups : IGroup[], + username : string, + description ? : string, + profilePhoto ? : string, + backgroundPhoto ? : string, + birthday ? : string, + lastName ? : string, + firstName ? : string, + studentId ? : string, + koreanName ? : string, + boards ? : { + pinsCount : number, + pins : any[], + }[], + currentGroup ? : IGroup | string | null, + groups : IGroup[], + stats ? : { + likesCount : number, + followingsCount : number, + }, +} + +export interface IUserMap { + [key : string] : IGroup; +} + +export type IProfile = IUser | IGroup; + +export interface IProfileMap { + [key : string] : IProfile +} diff --git a/src/types/store.d.ts b/src/types/store.d.ts new file mode 100644 index 00000000..8ea698d6 --- /dev/null +++ b/src/types/store.d.ts @@ -0,0 +1,18 @@ +import { PenderState } from 'redux-pender'; + +import { IAdminState } from 'store/reducers/admin'; +import { IAppState } from 'store/reducers/app'; +import { IAuthState } from 'store/reducers/auth'; +import { IProfileState } from 'store/reducers/profile'; +import { IUploadState } from 'store/reducers/upload'; +import { IZaboState } from 'store/reducers/zabo'; + +export interface IState { + admin : IAdminState; + app : IAppState; + auth : IAuthState; + profile : IProfileState; + upload : IUploadState; + zabo : IZaboState; + pender : PenderState; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..c8121d5e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.paths.json", + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": [ + "src" + ] +} diff --git a/tsconfig.paths.json b/tsconfig.paths.json new file mode 100644 index 00000000..a0c4b3a1 --- /dev/null +++ b/tsconfig.paths.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "paths": { + "*": ["*", "components/*"] + } + } +} diff --git a/yarn.lock b/yarn.lock index 74ec2401..784c0e6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1453,6 +1453,16 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" +"@jest/types@^25.1.0": + version "25.1.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.1.0.tgz#b26831916f0d7c381e11dbb5e103a72aed1b4395" + integrity sha512-VpOtt7tCrgvamWZh1reVsGADujKigBUFTi19mlRjqEGsE8qH4r3s+skY33dNdXOwyZIvuftZ5tqdF1IgsMejMA== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^15.0.0" + chalk "^3.0.0" + "@loadable/component@^5.12.0": version "5.12.0" resolved "https://registry.yarnpkg.com/@loadable/component/-/component-5.12.0.tgz#34d056d15f53dc08d04e9203cad6867cf4f7306c" @@ -2538,6 +2548,14 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.4.tgz#06cbceb0ace6a342a9aafcb655a688cf38f6150d" integrity sha512-+o2igcuZA3xtOoFH56s+MCZVidwlJNcJID57DSCyawS2i910yG9vkwehCjJNZ6ImhCR5S9DbvIJKyYHcMyOfMw== +"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/is-function@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/is-function/-/is-function-1.0.0.tgz#1b0b819b1636c7baf0d6785d030d12edf70c3e83" @@ -2577,6 +2595,14 @@ dependencies: jest-diff "^24.3.0" +"@types/jest@^25.1.4": + version "25.1.4" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.1.4.tgz#9e9f1e59dda86d3fd56afce71d1ea1b331f6f760" + integrity sha512-QDDY2uNAhCV7TMCITrxz+MRk1EizcsevzfeS6LykIlq2V1E5oO4wXG8V2ZEd9w7Snxeeagk46YbMgZ8ESHx3sw== + dependencies: + jest-diff "^25.1.0" + pretty-format "^25.1.0" + "@types/js-cookie@2.2.4": version "2.2.4" resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.4.tgz#f79720b4755aa197c2e15e982e2f438f5748e348" @@ -2587,6 +2613,18 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== +"@types/jsonwebtoken@^8.3.8": + version "8.3.8" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.3.8.tgz#b27c9156dde2049ae03e56528a53ef5a8294aa82" + integrity sha512-g2ke5+AR/RKYpQxd+HJ2yisLHGuOV0uourOcPtKlcT5Zqv4wFg9vKhFpXEztN4H/6Y6RSUKioz/2PTFPP30CTA== + dependencies: + "@types/node" "*" + +"@types/lodash@^4.14.149": + version "4.14.149" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" + integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -2597,6 +2635,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.7.tgz#db51d28b8dfacfe4fb2d0da88f5eb0a2eca00675" integrity sha512-HU0q9GXazqiKwviVxg9SI/+t/nAsGkvLDkIdxz+ObejG2nX6Si00TeLqHMoS+a/1tjH7a8YpKVQwtgHuMQsldg== +"@types/node@^13.9.1": + version "13.9.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.1.tgz#96f606f8cd67fb018847d9b61e93997dabdefc72" + integrity sha512-E6M6N0blf/jiZx8Q3nb0vNaswQeEyn0XlupO+xN6DtJ6r6IT4nXrTry7zhIfYvFCl3/8Cu6WIysmUBKiqV0bqQ== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -2644,6 +2687,54 @@ dependencies: "@types/react" "*" +"@types/react-dom@^16.9.5": + version "16.9.5" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.5.tgz#5de610b04a35d07ffd8f44edad93a71032d9aaa7" + integrity sha512-BX6RQ8s9D+2/gDhxrj8OW+YD4R+8hj7FEM/OJHGNR0KipE1h1mSsf39YeyC81qafkq+N3rU3h3RFbLSwE5VqUg== + dependencies: + "@types/react" "*" + +"@types/react-helmet@^5.0.15": + version "5.0.15" + resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-5.0.15.tgz#af0370617307e9f062c6aee0f1f5588224ce646e" + integrity sha512-CCjqvecDJTXRrHG8aTc2YECcQCl26za/q+NaBRvy/wtm0Uh38koM2dpv2bG1xJV4ckz3t1lm2/5KU6nt2s9BWg== + dependencies: + "@types/react" "*" + +"@types/react-native@*": + version "0.61.22" + resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.61.22.tgz#c624cce728f7d3f57c0f0c53bf4d95a8903f5b16" + integrity sha512-17FmMa157l5lSfGf0ft5asMsKGsgFGYr9N3ULRat5YPfZSOlhwNJ0u9OR7MJOmxILKCdLvsfotPH2Vh+VVuGlQ== + dependencies: + "@types/react" "*" + +"@types/react-redux@^7.1.7": + version "7.1.7" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.7.tgz#12a0c529aba660696947384a059c5c6e08185c7a" + integrity sha512-U+WrzeFfI83+evZE2dkZ/oF/1vjIYgqrb5dGgedkqVV8HEfDFujNgWCwHL89TDuWKb47U0nTBT6PLGq4IIogWg== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + +"@types/react-router-dom@^5.1.3": + version "5.1.3" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.3.tgz#b5d28e7850bd274d944c0fbbe5d57e6b30d71196" + integrity sha512-pCq7AkOvjE65jkGS5fQwQhvUp4+4PVD9g39gXLZViP2UqFiFzsEpB3PKf0O6mdbKsewSK8N14/eegisa/0CwnA== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "5.1.4" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.4.tgz#7d70bd905543cb6bcbdcc6bd98902332054f31a6" + integrity sha512-PZtnBuyfL07sqCJvGg3z+0+kt6fobc/xmle08jBiezLS8FrmGeiGkJnuxL/8Zgy9L83ypUhniV5atZn/L8n9MQ== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-syntax-highlighter@11.0.2": version "11.0.2" resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.2.tgz#a2e3ff657d7c47813f80ca930f3d959c31ec51e3" @@ -2673,11 +2764,34 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/react@^16.9.23": + version "16.9.23" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.23.tgz#1a66c6d468ba11a8943ad958a8cb3e737568271c" + integrity sha512-SsGVT4E7L2wLN3tPYLiF20hmZTPGuzaayVunfgXzUn1x4uHVsKH6QDJQ/TdpHqwsTLd4CwrmQ2vOgxN7gE24gw== + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + +"@types/redux-actions@^2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@types/redux-actions/-/redux-actions-2.6.1.tgz#0940e97fa35ad3004316bddb391d8e01d2efa605" + integrity sha512-zKgK+ATp3sswXs6sOYo1tk8xdXTy4CTaeeYrVQlClCjeOpag5vzPo0ASWiiBJ7vsiQRAdb3VkuFLnDoBimF67g== + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/styled-components@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.0.1.tgz#44d210b0a0218a70df998d1a8e1f69c82d9cc68b" + integrity sha512-1yRYO1dAE2MGEuYKF1yQFeMdoyerIQn6ZDnFFkxZamcs3rn8RQVn98edPsTROAxbTz81tqnVN4BJ3Qs1cm/tKg== + dependencies: + "@types/hoist-non-react-statics" "*" + "@types/react" "*" + "@types/react-native" "*" + csstype "^2.2.0" + "@types/styled-jsx@^2.2.8": version "2.2.8" resolved "https://registry.yarnpkg.com/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234" @@ -2723,6 +2837,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^15.0.0": + version "15.0.4" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.4.tgz#7e5d0f8ca25e9d5849f2ea443cf7c402decd8299" + integrity sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg== + dependencies: + "@types/yargs-parser" "*" + "@typescript-eslint/eslint-plugin@^2.16.0", "@typescript-eslint/eslint-plugin@^2.8.0": version "2.16.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.16.0.tgz#bf339b7db824c7cc3fd1ebedbc88dd17016471af" @@ -3121,7 +3242,7 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== @@ -4163,6 +4284,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +btoa@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" + integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" @@ -4628,6 +4754,15 @@ cliui@^5.0.0: strip-ansi "^5.2.0" wrap-ansi "^5.1.0" +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + clone-deep@^0.2.4: version "0.2.4" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.2.4.tgz#4e73dd09e9fb971cc38670c5dced9c1896481cc6" @@ -5657,6 +5792,11 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== +diff-sequences@^25.1.0: + version "25.1.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.1.0.tgz#fd29a46f1c913fd66c22645dc75bffbe43051f32" + integrity sha512-nFIfVk5B/NStCsJ+zaPO4vYuLjlzQ6uFvPxzYyHlejNZ/UGa7G/n7peOXVrVNvRuyfstt+mZQYGpjxg9Z6N8Kw== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -5886,6 +6026,11 @@ ejs@^2.7.4: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== +ejs@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.0.1.tgz#30c8f6ee9948502cc32e85c37a3f8b39b5a614a5" + integrity sha512-cuIMtJwxvzumSAkqaaoGY/L6Fc/t6YvoP9/VIaK0V/CyqKLEQ8sqODmYfy/cjXEdZ9+OOL8TecbJu+1RsofGDw== + electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.306, electron-to-chromium@^1.3.322: version "1.3.335" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.335.tgz#5fb6084a25cb1e2542df91e62b62e1931a602303" @@ -7373,7 +7518,7 @@ gud@^1.0.0: resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw== -gzip-size@5.1.1: +gzip-size@5.1.1, gzip-size@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" integrity sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA== @@ -8212,6 +8357,11 @@ is-directory@^0.3.1: resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= +is-docker@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b" + integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ== + is-dom@^1.0.9: version "1.1.0" resolved "https://registry.yarnpkg.com/is-dom/-/is-dom-1.1.0.tgz#af1fced292742443bb59ca3f76ab5e80907b4e8a" @@ -8490,7 +8640,7 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= -is-wsl@^2.1.0: +is-wsl@^2.1.0, is-wsl@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.1.1.tgz#4a1c152d429df3d441669498e2486d3596ebaf1d" integrity sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog== @@ -8663,6 +8813,16 @@ jest-diff@^24.3.0, jest-diff@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" +jest-diff@^25.1.0: + version "25.1.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.1.0.tgz#58b827e63edea1bc80c1de952b80cec9ac50e1ad" + integrity sha512-nepXgajT+h017APJTreSieh4zCqnSHEJ1iT8HDlewu630lSJ4Kjjr9KNzm+kzGwwcpsDE6Snx1GJGzzsefaEHw== + dependencies: + chalk "^3.0.0" + diff-sequences "^25.1.0" + jest-get-type "^25.1.0" + pretty-format "^25.1.0" + jest-docblock@^24.3.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2" @@ -8718,6 +8878,11 @@ jest-get-type@^24.9.0: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== +jest-get-type@^25.1.0: + version "25.1.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.1.0.tgz#1cfe5fc34f148dc3a8a3b7275f6b9ce9e2e8a876" + integrity sha512-yWkBnT+5tMr8ANB6V+OjmrIJufHtCAqI5ic2H40v+tRqxDmE0PGnIiTyvRWFOMtmVHYpwRqyazDbTnhpjsGvLw== + jest-haste-map@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d" @@ -9640,6 +9805,11 @@ lodash.once@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= +lodash.set@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" + integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -10765,6 +10935,14 @@ open@^7.0.0: dependencies: is-wsl "^2.1.0" +open@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.0.2.tgz#fb3681f11f157f2361d2392307548ca1792960e8" + integrity sha512-70E/pFTPr7nZ9nLDPNTcj3IVqnNvKuP4VsBmoKV9YGTnChe0mlS3C4qM7qKarhZ8rGaHKLfo+vBTHXDp6ZSyLQ== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + opencollective-postinstall@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" @@ -12060,6 +12238,16 @@ pretty-format@^24.9.0: ansi-styles "^3.2.0" react-is "^16.8.4" +pretty-format@^25.1.0: + version "25.1.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.1.0.tgz#ed869bdaec1356fc5ae45de045e2c8ec7b07b0c8" + integrity sha512-46zLRSGLd02Rp+Lhad9zzuNZ+swunitn8zIpfD2B4OPCRLXbM87RJT2aBLBWYOznNUML/2l/ReMyWNC80PJBUQ== + dependencies: + "@jest/types" "^25.1.0" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^16.12.0" + pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" @@ -12689,6 +12877,11 @@ react-inspector@^4.0.0: prop-types "^15.6.1" storybook-chromatic "^2.2.2" +react-is@^16.12.0: + version "16.13.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527" + integrity sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA== + react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.3, react-is@^16.8.4, react-is@^16.9.0: version "16.12.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" @@ -13262,7 +13455,7 @@ redux-pender@^2.0.12: dependencies: proxy-polyfill "^0.3.0" -redux@^4.0.1, redux@^4.0.2: +redux@^4.0.0, redux@^4.0.1, redux@^4.0.2: version "4.0.5" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== @@ -13687,7 +13880,7 @@ rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.3, rimraf@^2.7.1: dependencies: glob "^7.1.3" -rimraf@2.6.3: +rimraf@2.6.3, rimraf@~2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== @@ -14219,6 +14412,24 @@ source-list-map@^2.0.0: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== +source-map-explorer@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/source-map-explorer/-/source-map-explorer-2.3.1.tgz#d91615a19ad1f4e08d05616bee6e30ddb770ae70" + integrity sha512-l3WQUCwaqia5x7EBnNp4GYwhXnROMz3NqKM2QMwQ3ADgjekp+enP+PHkjjbjoVX6WJ2G5mbvM6TjeE/q7fnIFw== + dependencies: + btoa "^1.2.1" + chalk "^3.0.0" + convert-source-map "^1.7.0" + ejs "^3.0.1" + escape-html "^1.0.3" + glob "^7.1.6" + gzip-size "^5.1.1" + lodash "^4.17.15" + open "^7.0.2" + source-map "^0.7.3" + temp "^0.9.1" + yargs "^15.1.0" + source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" @@ -14265,6 +14476,11 @@ source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= +source-map@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + sourcemap-codec@^1.4.1: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" @@ -15026,6 +15242,13 @@ telejson@^3.2.0: lodash "^4.17.15" memoizerific "^1.11.3" +temp@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/temp/-/temp-0.9.1.tgz#2d666114fafa26966cd4065996d7ceedd4dd4697" + integrity sha512-WMuOgiua1xb5R56lE0eH6ivpVmg/lq2OHm4+LtT/xtEtPQ+sz6N3bBM6WZ5FvO1lO4IKIOb43qnhoc4qxP5OeA== + dependencies: + rimraf "~2.6.2" + term-size@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.1.1.tgz#f81ec25854af91a480d2f9d0c77ffcb26594ed1a" @@ -15409,10 +15632,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.7.5: - version "3.7.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" - integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw== +typescript@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" + integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== ua-parser-js@^0.7.18: version "0.7.21" @@ -16263,6 +16486,15 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -16383,6 +16615,14 @@ yargs-parser@^13.1.1: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^16.1.0: + version "16.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-16.1.0.tgz#73747d53ae187e7b8dbe333f95714c76ea00ecf1" + integrity sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" @@ -16450,6 +16690,23 @@ yargs@^13.3.0: y18n "^4.0.0" yargs-parser "^13.1.1" +yargs@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.1.0.tgz#e111381f5830e863a89550bd4b136bb6a5f37219" + integrity sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^16.1.0" + yargs@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"