diff --git a/.travis.yml b/.travis.yml index 17a41682..65a02d2c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,7 +34,7 @@ jobs: - stage: Deploy if: branch = master AND type = pull_request before_install: - - openssl aes-256-cbc -K $encrypted_278d2d9eb060_key -iv $encrypted_278d2d9eb060_iv -in env.tar.enc -out env.tar -d + - openssl aes-256-cbc -K $encrypted_0395e9fbd9ff_key -iv $encrypted_0395e9fbd9ff_iv -in env.tar.enc -out env.tar -d - tar xvf env.tar install: - sudo apt-get install sshpass diff --git a/cocode/dev.env b/cocode/dev.env index 26366660..da0b0cf0 100644 --- a/cocode/dev.env +++ b/cocode/dev.env @@ -1,4 +1,5 @@ SKIP_PREFLIGHT_CHECK=true + DEV_API_SERVER_IP= PROD_API_SERVER_IP= @@ -7,3 +8,9 @@ DEV_DEPENDENCY_SERVER_IP= PROD_COCONUT_SERVER_IP= DEV_COCONUT_SERVER_IP= + +PROD_LIVE_SERVER_IP= +DEV_LIVE_SERVER_IP= + +PROD_COCODE_SERVER_IP= +DEV_COCODE_SERVER_IP= \ No newline at end of file diff --git a/cocode/package.json b/cocode/package.json index 38a7e5bc..426ddd20 100644 --- a/cocode/package.json +++ b/cocode/package.json @@ -55,6 +55,7 @@ "react-router-dom": "^5.1.2", "react-scripts": "^3.2.0", "react-style-proptype": "^3.2.2", + "socket.io-client": "^2.3.0", "styled-components": "^4.4.1", "webpack": "^4.41.2", "worker-loader": "^2.0.0" diff --git a/cocode/public/favicon-16x16.png b/cocode/public/favicon-16x16.png new file mode 100644 index 00000000..38f6a74a Binary files /dev/null and b/cocode/public/favicon-16x16.png differ diff --git a/cocode/public/index.html b/cocode/public/index.html index e0ab6b04..7d41b699 100644 --- a/cocode/public/index.html +++ b/cocode/public/index.html @@ -2,6 +2,12 @@ cocode! - the SaaS IDE + + @@ -29,7 +28,13 @@ function App() { - + + + + + + + diff --git a/cocode/src/actions/Live.js b/cocode/src/actions/Live.js index e9ca1565..b04f9e52 100644 --- a/cocode/src/actions/Live.js +++ b/cocode/src/actions/Live.js @@ -1,31 +1,26 @@ import { - FETCH_LIVE, LIVE_ON, LIVE_OFF, LIVE_JOIN_USER, - LIVE_LEFT_USER + LIVE_LEAVE_USER } from './types'; -function fetchLiveActionCreator(payload) { - return { type: FETCH_LIVE, payload }; -} function liveOnActionCreator(payload) { return { type: LIVE_ON, payload }; } -function liveOffActionCreator(payload) { - return { type: LIVE_OFF, payload }; +function liveOffActionCreator() { + return { type: LIVE_OFF }; } function liveJoinUserActionCreator(payload) { return { type: LIVE_JOIN_USER, payload }; } -function liveLeftUserActionCreator(payload) { - return { type: LIVE_LEFT_USER, payload }; +function liveLeaveUserActionCreator(payload) { + return { type: LIVE_LEAVE_USER, payload }; } export { - fetchLiveActionCreator, liveOnActionCreator, liveOffActionCreator, liveJoinUserActionCreator, - liveLeftUserActionCreator + liveLeaveUserActionCreator }; diff --git a/cocode/src/actions/Project.js b/cocode/src/actions/Project.js index 29d4ca36..0c84f278 100644 --- a/cocode/src/actions/Project.js +++ b/cocode/src/actions/Project.js @@ -1,6 +1,7 @@ import { UPDATE_PROJECT_INFO, UPDATE_CODE, + UPDATE_CODE_FROM_FILE_ID, FETCH_PROJECT, SELECT_FILE, UPDATE_FILE_NAME, @@ -23,6 +24,9 @@ function fetchProjectActionCreator(payload) { function updateCodeActionCreator(payload) { return { type: UPDATE_CODE, payload }; } +function updateCodeFromFileIdActionCreator(payload) { + return { type: UPDATE_CODE_FROM_FILE_ID, payload }; +} function selectFileActionCreator(payload) { return { type: SELECT_FILE, payload }; @@ -59,6 +63,7 @@ function saveFileActionCreator(payload) { export { updateProjectInfoActionCreator, updateCodeActionCreator, + updateCodeFromFileIdActionCreator, fetchProjectActionCreator, selectFileActionCreator, updateFileNameActionCreator, diff --git a/cocode/src/actions/types.js b/cocode/src/actions/types.js index a88427d4..6532f671 100644 --- a/cocode/src/actions/types.js +++ b/cocode/src/actions/types.js @@ -7,6 +7,7 @@ const API_FAIL = 'API_FAILURE'; // Project const UPDATE_PROJECT_INFO = 'updateProjectInfo'; const UPDATE_CODE = 'updateCode'; +const UPDATE_CODE_FROM_FILE_ID = 'updateCodeFromFileId'; const FETCH_PROJECT = 'fetchProject'; const SELECT_FILE = 'selectFile'; const CREATE_FILE = 'createFile'; @@ -23,11 +24,10 @@ const UPDATE_COCONUT_NAME = 'updateCoconutName'; const DELETE_COCONUT = 'deleteCoconut'; //Live -const FETCH_LIVE = 'fetchLive'; const LIVE_ON = 'liveOn'; const LIVE_OFF = 'liveOff'; const LIVE_JOIN_USER = 'liveJoinUser'; -const LIVE_LEFT_USER = 'liveLeftUser'; +const LIVE_LEAVE_USER = 'liveLeaveUser'; export { API_READY, @@ -36,6 +36,7 @@ export { API_FAIL, UPDATE_PROJECT_INFO, UPDATE_CODE, + UPDATE_CODE_FROM_FILE_ID, FETCH_PROJECT, SELECT_FILE, UPDATE_FILE_NAME, @@ -47,10 +48,9 @@ export { FETCH_COCONUT, UPDATE_COCONUT_NAME, DELETE_COCONUT, - FETCH_LIVE, LIVE_ON, LIVE_OFF, LIVE_JOIN_USER, - LIVE_LEFT_USER, + LIVE_LEAVE_USER, SAVE_FILE }; diff --git a/cocode/src/apis/Dependency.js b/cocode/src/apis/Dependency.js deleted file mode 100644 index d1435fb0..00000000 --- a/cocode/src/apis/Dependency.js +++ /dev/null @@ -1,14 +0,0 @@ -import { DEPENDENCY } from 'config'; - -function getModule(moduleName, moduleVersion) { - return { - url: `${DEPENDENCY.modules}`, - method: 'post', - data: { - moduleName, - moduleVersion - } - }; -} - -export { getModule }; diff --git a/cocode/src/components/Common/DropZone/index.js b/cocode/src/components/Common/DropZone/index.js index f31fd37b..3a065c7a 100644 --- a/cocode/src/components/Common/DropZone/index.js +++ b/cocode/src/components/Common/DropZone/index.js @@ -26,7 +26,7 @@ function DropZone({ draggableComponentOverColor, ...props }) { onDragLeave={handleDragLeave} onDrop={handleDrop} {...props} - > + /> ); } diff --git a/cocode/src/components/Common/LoginModalBody/index.js b/cocode/src/components/Common/LoginModalBody/index.js index c086585e..b9ad3c54 100644 --- a/cocode/src/components/Common/LoginModalBody/index.js +++ b/cocode/src/components/Common/LoginModalBody/index.js @@ -6,7 +6,10 @@ import { API } from 'config'; import Github from './github.svg'; function LoginModalBody() { - const handleClickLoginButton = () => (window.location.href = API.login); + const handleClickLoginButton = () => { + window.location.href = API.login; + localStorage.setItem('redirectURL', window.location.href); + }; return ( diff --git a/cocode/src/components/Common/Modal/style.js b/cocode/src/components/Common/Modal/style.js index 5ed807dd..e5ecf616 100644 --- a/cocode/src/components/Common/Modal/style.js +++ b/cocode/src/components/Common/Modal/style.js @@ -2,6 +2,7 @@ import styled from 'styled-components'; const ModalBackGround = styled.div` & { + z-index: 10; position: fixed; top: 0; left: 0; diff --git a/cocode/src/components/DashBoard/ProjectCard/index.js b/cocode/src/components/DashBoard/ProjectCard/index.js index 61fca6ce..12e882db 100644 --- a/cocode/src/components/DashBoard/ProjectCard/index.js +++ b/cocode/src/components/DashBoard/ProjectCard/index.js @@ -15,7 +15,7 @@ import { updateCoconutNameActionCreator, deleteCoconutActionCreator } from 'actions/Dashboard'; -import DashBoardContext from 'contexts/DashBoardContext'; +import { DashBoardContext } from 'contexts'; import useFetch from 'hooks/useFetch'; import { updateCoconutsAPICreator, diff --git a/cocode/src/components/DashBoard/ProjectCard/style.js b/cocode/src/components/DashBoard/ProjectCard/style.js index 3deb81ad..445e5505 100644 --- a/cocode/src/components/DashBoard/ProjectCard/style.js +++ b/cocode/src/components/DashBoard/ProjectCard/style.js @@ -15,6 +15,7 @@ const ProjectArticle = styled.article` background-color: ${PROJECT_CARD_THEME.cardBackgroundColor}; border-radius: 1rem; + cursor: pointer; } `; diff --git a/cocode/src/components/Project/LiveUserProfile/index.js b/cocode/src/components/Live/LiveUsers/index.js similarity index 79% rename from cocode/src/components/Project/LiveUserProfile/index.js rename to cocode/src/components/Live/LiveUsers/index.js index d2691b77..108b8459 100644 --- a/cocode/src/components/Project/LiveUserProfile/index.js +++ b/cocode/src/components/Live/LiveUsers/index.js @@ -16,11 +16,13 @@ function LiveUserProfile({ username, avatar }) { ); } -function LiveUsers({ owner: { username, avatar }, participants = [] }) { +function LiveUsers({ owner, participants }) { + if (!participants.length) participants = []; + return ( - <> + OWNERS - + USERS {participants.map(({ username, avatar }, index) => { return ( @@ -31,7 +33,7 @@ function LiveUsers({ owner: { username, avatar }, participants = [] }) { /> ); })} - + ); } diff --git a/cocode/src/components/Project/LiveUserProfile/style.js b/cocode/src/components/Live/LiveUsers/style.js similarity index 78% rename from cocode/src/components/Project/LiveUserProfile/style.js rename to cocode/src/components/Live/LiveUsers/style.js index 8f4a7679..5bad1c07 100644 --- a/cocode/src/components/Project/LiveUserProfile/style.js +++ b/cocode/src/components/Live/LiveUsers/style.js @@ -1,6 +1,12 @@ import styled from 'styled-components'; import { TAB_CONTAINER_THEME, LIVE_TAB_THEME } from 'constants/theme'; +const Container = styled.div` + & { + margin: 1rem 0; + } +`; + const Title = styled.h1` & { color: ${TAB_CONTAINER_THEME.tabContainerTitleColor}; @@ -12,13 +18,14 @@ const Title = styled.h1` const UserProfile = styled.div` & { display: flex; + margin: 0.7rem 0; } `; const UserName = styled.div` & { align-self: center; - margin-left: 0.4rem; + margin-left: 0.5rem; font-weight: 100; font-size: 1rem; } @@ -38,4 +45,11 @@ const SelfLabel = styled(UserName)` } `; -export { Title, UserProfile, UserName, UserAvatar, SelfLabel }; +export { + Container, + Title, + UserProfile, + UserName, + UserAvatar, + SelfLabel +}; diff --git a/cocode/src/components/Project/BrowserV1/index.js b/cocode/src/components/Project/BrowserV1/index.js deleted file mode 100644 index 6555dcf3..00000000 --- a/cocode/src/components/Project/BrowserV1/index.js +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useState, useEffect, useContext } from 'react'; -import * as Styled from './style'; - -import ProjectContext from 'contexts/ProjectContext'; -// import { updateCodeActionCreator } from 'actions/Project'; -// import * as bundler from 'bundler'; - -// import * as babel from '@babel/core'; -// import reactPreset from '@babel/preset-react'; - -function BrowserV1({ code, ...props }) { - const { project, dispatchProject } = useContext(ProjectContext); - const { files, entry, selectedFileId } = project; - const [fileSystem, setFileSystem] = useState({}); - // const buildCode = () => { - // try { - // const parsedCode = babel.transform(code, { - // presets: [reactPreset] - // }); - // eval(parsedCode.code); - // } catch (e) { - // console.log(e); - // } - // }; - - // useEffect(buildCode, [files]); - // useEffect(() => { - // const fileTemp = {}; - // Object.keys(bundler.exports).forEach(key => { - // delete bundler.exports[key]; - // }); - // function fileParser(id, path = '') { - // if (files[id].type !== 'directory') { - // fileTemp[`${path}/${files[id].name}`] = { - // contents: files[id].contents - // }; - // bundler.exports[`${path}/${files[id].name}`] = { - // contents: files[id].contents - // }; - // } else { - // files[id].child.forEach(file => { - // fileParser(file, `${path}/${files[id].name}`); - // }); - // } - // } - // if (project) fileParser(project.root); - - // setFileSystem(fileTemp); - // }, [files]); - - // useEffect(() => { - // console.log(fileSystem); - // try { - // bundler.init(); - // bundler.require('/root/src/index'); - // } catch (error) { - // console.log(error); - // } - // }, [fileSystem]); - return ; -} - -export default BrowserV1; diff --git a/cocode/src/components/Project/BrowserV1/style.js b/cocode/src/components/Project/BrowserV1/style.js deleted file mode 100644 index 0fa19cf7..00000000 --- a/cocode/src/components/Project/BrowserV1/style.js +++ /dev/null @@ -1,12 +0,0 @@ -import styled from 'styled-components'; - -const BrowserV1 = styled.div` - & { - height: ${({ height }) => height}; - - background-color: white; - color: black; - } -`; - -export { BrowserV1 }; diff --git a/cocode/src/components/Project/BrowserV2/index.js b/cocode/src/components/Project/BrowserV2/index.js index 702a5276..e7c8a6b7 100644 --- a/cocode/src/components/Project/BrowserV2/index.js +++ b/cocode/src/components/Project/BrowserV2/index.js @@ -8,10 +8,10 @@ import React, { import { useParams } from 'react-router-dom'; import * as Styled from './style'; +import search from './search.svg'; import addToast from 'components/Common/Toast'; -import CoconutSpinner from 'components/Common/CoconutSpinner'; -import ProjectContext from 'contexts/ProjectContext'; +import { ProjectContext } from 'contexts'; import { installDependencyActionCreator } from 'actions/Project'; @@ -24,38 +24,31 @@ import getUpdatedPackageJSON from 'pages/Project/getUpdatedPackageJSON'; import { COCONUT_SERVER } from 'config'; import * as NOTIFICATION from 'constants/notificationMessage'; +import { KEY_CODE_ENTER } from 'constants/keyCode'; // Constants const MIN_WAIT_TIME = 1500; const UPDATE_PROJECT = 'updateProject'; +const PROTOCOLS = ['http://', 'https://']; function BrowserV2({ ...props }) { const { projectId } = useParams(); + const DEFAULT_URL = `${COCONUT_SERVER}/${projectId}`; + const { project, dispatchProject } = useContext(ProjectContext); const [{ data, error }, setRequest] = useFetch({}); const [isReadyToReceiveMessage, setIsReadyToReceiveMessage] = useState( false ); const [dependency, setDependency] = useState(undefined); - const [isBuildingCoconut, setIsBuildingCoconut] = useState(true); + const [addressInputURL, setAddressInput] = useState(DEFAULT_URL); + const iframeReference = useRef(); + const addressReference = useRef(); const { files, root, dependencyInstalling } = project; - const handleComponentDidMount = () => { - window.addEventListener('message', receiveMsgFromChild); - }; - - const receiveMsgFromChild = e => { - const { command, dependency } = e.data; - - const cocodeActions = { buildEnd }; - cocodeActions[command] && cocodeActions[command](dependency); - }; - - const buildEnd = () => setIsBuildingCoconut(false); - const endInstallDependency = useCallback(dependency => { setTimeout(() => { const installDependencyAction = installDependencyActionCreator({ @@ -66,6 +59,9 @@ function BrowserV2({ ...props }) { }, MIN_WAIT_TIME); }); + const isHaveProtocol = (value) => + PROTOCOLS.some(PROTOCOL => value.includes(PROTOCOL)); + const handleUpdateDependency = () => { if (!isReadyToReceiveMessage) return; if (!dependencyInstalling) return; @@ -115,6 +111,20 @@ function BrowserV2({ ...props }) { addToast.error(NOTIFICATION.FAIL_INSTALL_DEPENDENCY); }; + const handleAddressInputKeyDown = ({ keyCode, target: { value } }) => { + if (keyCode === KEY_CODE_ENTER) { + const address = isHaveProtocol(value) ? value : `${PROTOCOLS[0]}${value}`; + setAddressInput(address); + addressReference.current.value = address; + } + }; + + const handleChangeCurrentURL = () => { + const address = `${COCONUT_SERVER}/${projectId}`; + setAddressInput(address); + addressReference.current.value = address; + }; + const handleIframeOnLoad = useCallback(() => { setIsReadyToReceiveMessage(true); @@ -127,7 +137,7 @@ function BrowserV2({ ...props }) { iframeReference.current.contentWindow.postMessage(data, '*'); }, [project]); - useEffect(handleComponentDidMount, []); + useEffect(handleChangeCurrentURL, [projectId]); useEffect(handleUpdateDependency, [dependencyInstalling]); useEffect(handleUpdateFile, [files]); @@ -136,15 +146,18 @@ function BrowserV2({ ...props }) { return ( - {isBuildingCoconut && ( - - -

Please wait to build complete...

-
- )} + + + + diff --git a/cocode/src/components/Project/BrowserV2/search.svg b/cocode/src/components/Project/BrowserV2/search.svg new file mode 100644 index 00000000..a1739b09 --- /dev/null +++ b/cocode/src/components/Project/BrowserV2/search.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/cocode/src/components/Project/BrowserV2/style.js b/cocode/src/components/Project/BrowserV2/style.js index 5c57a943..4b547453 100644 --- a/cocode/src/components/Project/BrowserV2/style.js +++ b/cocode/src/components/Project/BrowserV2/style.js @@ -1,37 +1,17 @@ import styled from 'styled-components'; +import { BROWSER_THEME } from 'constants/theme'; const Frame = styled.div` & { position: relative; } `; -const ErrorDisplay = styled.div` - & { - position: absolute; - z-index: ${({ errorDescription }) => (errorDescription ? 1 : -1)}; - overflow-x: scroll; - - padding: 1rem; - - height: 100%; - width: 100%; - - background-color: ${({ errorDescription }) => - errorDescription ? 'rgba(0, 0, 0, 0.7)' : 'transparent'}; - - font-size: 2rem; - font-weight: lighter; - } -`; const BrowserV2 = styled.iframe` & { - position: absolute; - - height: 100%; + height: calc(100% - 3.1rem); width: 100%; - - background-color: white; + background-color: ${BROWSER_THEME.iframeBGColor}; } `; @@ -50,15 +30,51 @@ const LoadingOverlay = styled.section` justify-content: center; align-items: center; - background-color: black; + background-color: ${BROWSER_THEME.loadingOverlayBGColor}; p { margin-top: 2rem; - font-size: 3rem; font-weight: lighter; } } `; -export { Frame, ErrorDisplay, BrowserV2, LoadingOverlay }; +const AddressContainer = styled.div` + & { + display: flex; + align-items: center; + height: 3.1rem; + width: 100%; + padding: 0.8rem; + background: ${BROWSER_THEME.browserHeaderBGColor}; + font-size: 1rem; + } +`; + +const AddressInput = styled.input` + & { + width: 100%; + height: 100%; + padding: 0.3rem; + background: ${BROWSER_THEME.addressInputBGColor}; + color: ${BROWSER_THEME.addressInputTextColor}; + } +`; + +const SearchIcon = styled.img` + & { + height: 100%; + padding: 0.4rem 0; + background: ${BROWSER_THEME.addressInputBGColor}; + } +`; + +export { + Frame, + BrowserV2, + LoadingOverlay, + AddressContainer, + AddressInput, + SearchIcon +}; diff --git a/cocode/src/components/Project/Directory/index.js b/cocode/src/components/Project/Directory/index.js index 2a9d2813..de1f9c2c 100644 --- a/cocode/src/components/Project/Directory/index.js +++ b/cocode/src/components/Project/Directory/index.js @@ -7,7 +7,7 @@ import DropZone from 'components/Common/DropZone'; import File from 'components/Project/File'; import NewFile from 'components/Project/NewFile'; -import ProjectContext from 'contexts/ProjectContext'; +import { ProjectContext } from 'contexts'; import { EXPLORER_TAB_CONTAINER_THEME } from 'constants/theme'; import * as NOTIFICATION from 'constants/notificationMessage'; @@ -37,11 +37,11 @@ function isProtectedFile({ files, root, entry, fileId }) { return false; } -function isFileNotMoveable({ files, fileId, newParentId }) { +function isFileNotMovable({ files, fileId, newParentId }) { const fileName = files[fileId].name; - const childsOfNewParent = files[newParentId].child; + const childrenOfNewParent = files[newParentId].child; - return childsOfNewParent + return childrenOfNewParent .map(id => files[id].name) .some(name => name === fileName); } @@ -93,7 +93,7 @@ function Directory({ .filter(isNotPackageJSON) : []; - // Evnet handler + // Event handler const handleToggleDirectory = () => setToggleDirectoryOpen(!toggleDirectoryOpen); const handleEditFileName = changedName => { @@ -114,7 +114,7 @@ function Directory({ if ( fileId === id || isProtectedFile({ files, root, entry, fileId }) || - isFileNotMoveable({ files, fileId, newParentId: id }) + isFileNotMovable({ files, fileId, newParentId: id }) ) return addToast.error(NOTIFICATION.FILE_IS_NOT_MOVABLE); diff --git a/cocode/src/components/Project/File/index.js b/cocode/src/components/Project/File/index.js index b51b6177..fa2f5f47 100644 --- a/cocode/src/components/Project/File/index.js +++ b/cocode/src/components/Project/File/index.js @@ -25,7 +25,7 @@ import { deleteFileAPICreator, updateFileAPICreator } from 'apis/File'; import { DELETE_FILE, UPDATE_FILE_NAME } from 'actions/types'; -import ProjectContext from 'contexts/ProjectContext'; +import { ProjectContext } from 'contexts'; // Constants const API_NOTIFICATION = { @@ -34,9 +34,11 @@ const API_NOTIFICATION = { }; function isNotChangeableFileName({ files, changedName, parentId }) { - const childsOfParent = files[parentId].child; + if (!changedName.trim().length) return true; - return childsOfParent + const childrenOfParent = files[parentId].child; + + return childrenOfParent .map(id => files[id].name) .some(name => name === changedName); } @@ -60,7 +62,7 @@ function File({ const [toggleEdit, setToggleEdit] = useState(false); const [requestedAPI, setRequestedAPI] = useState(null); - const nameEditReferenece = useRef(null); + const nameEditReference = useRef(null); const [{ data, error }, setRequest] = useFetch({}); const { @@ -71,7 +73,7 @@ function File({ const successHandler = { [DELETE_FILE]: handleDeleteFile, [UPDATE_FILE_NAME]: () => { - const changedName = nameEditReferenece.current.textContent; + const changedName = nameEditReference.current.textContent; setFileName(changedName); handleEditFileName(changedName); } @@ -87,7 +89,7 @@ function File({ return; } - changeDivEditable(nameEditReferenece.current, true); + changeDivEditable(nameEditReference.current, true); setToggleEdit(true); }; @@ -138,7 +140,7 @@ function File({ const handleKeyDown = e => { if (e.keyCode === KEY_CODE_ENTER) { setToggleEdit(false); - nameEditReferenece.current.contentEditable = false; + nameEditReference.current.contentEditable = false; } }; @@ -177,7 +179,7 @@ function File({ > { @@ -76,17 +76,17 @@ function NewFile({ depth, type, parentId, handleEndCreateFile }) { type }); dispatchProject(createFileAction); - changeDivEditable(fileNameInputReferenece.current, false); + changeDivEditable(fileNameInputReference.current, false); }; const handleErrorResponse = () => { if (!error) return; addToast.error(NOTIFICATION.FAIL_TO_CREATE_FILE); - changeDivEditable(fileNameInputReferenece.current, false); + changeDivEditable(fileNameInputReference.current, false); }; useEffect(() => { - fileNameInputReferenece.current.focus(); + fileNameInputReference.current.focus(); }, []); useEffect(handleSetNewFileState, [data]); useEffect(handleErrorResponse, [error]); @@ -95,7 +95,7 @@ function NewFile({ depth, type, parentId, handleEndCreateFile }) { `${API_SERVER}/dependency/search?name=${name}` }; -const DEPENDENCY = { - modules: `${DEPENDENCY_SERVER}/modules` +export { + DEFAULT_REQUEST_OPTION, + API, + COCONUT_SERVER, + LIVE_SERVER, + COCODE_SERVER }; - -export { DEFAULT_REQUEST_OPTION, API, DEPENDENCY, COCONUT_SERVER }; diff --git a/cocode/src/constants/fileImagesSrc.js b/cocode/src/constants/fileImagesSrc.js index 8f0fbeef..4361fdd5 100644 --- a/cocode/src/constants/fileImagesSrc.js +++ b/cocode/src/constants/fileImagesSrc.js @@ -1,16 +1,11 @@ const FILE_IMAGES_SRC = { file: 'https://codesandbox.io/static/media/file.6cbc0ce8.svg', directory: 'https://codesandbox.io/static/media/folder.30a30d83.svg', - directoryOpen: - 'https://codesandbox.io/static/media/folder-open.df474ba4.svg', - js: - 'https://cdn.jsdelivr.net/gh/PKief/vscode-material-icon-theme@master/icons/javascript.svg', - css: - 'https://cdn.jsdelivr.net/gh/PKief/vscode-material-icon-theme@master/icons/css.svg', - html: - 'https://cdn.jsdelivr.net/gh/PKief/vscode-material-icon-theme@master/icons/html.svg', - npm: - 'https://cdn.jsdelivr.net/gh/PKief/vscode-material-icon-theme@master/icons/npm.svg' + directoryOpen: 'https://codesandbox.io/static/media/folder-open.df474ba4.svg', + js: 'https://cdn.jsdelivr.net/gh/PKief/vscode-material-icon-theme@master/icons/javascript.svg', + css: 'https://cdn.jsdelivr.net/gh/PKief/vscode-material-icon-theme@master/icons/css.svg', + html: 'https://cdn.jsdelivr.net/gh/PKief/vscode-material-icon-theme@master/icons/html.svg', + npm: 'https://cdn.jsdelivr.net/gh/PKief/vscode-material-icon-theme@master/icons/npm.svg' }; export default FILE_IMAGES_SRC; diff --git a/cocode/src/constants/notificationMessage.js b/cocode/src/constants/notificationMessage.js index e4b69809..f312f88c 100644 --- a/cocode/src/constants/notificationMessage.js +++ b/cocode/src/constants/notificationMessage.js @@ -14,6 +14,13 @@ const CONFIRM_DELETE_FILE = 'Are you delete this file?'; const CONFIRM_DELETE_COCONUT = 'Are you delete this coconut?'; const LOADING_DASHBOARD = 'Please wait to fetch coconuts'; +const LOADING_PROJECT = 'Please wait to fetch coconut'; +const LOADING_LIVE = 'Please wait to connect live share'; + +const SUCCESS_FORK = 'Forked Coconut, Success!'; +const CONFLICT_FORK = 'Already forked! Enjoy Coconut!'; + +const SHUT_DOWN_LIVE_SHARE = 'The live share has been shut down'; export { FAIL_INSTALL_DEPENDENCY, @@ -29,4 +36,9 @@ export { CONFIRM_DELETE_FILE, CONFIRM_DELETE_COCONUT, LOADING_DASHBOARD, + LOADING_PROJECT, + LOADING_LIVE, + SUCCESS_FORK, + CONFLICT_FORK, + SHUT_DOWN_LIVE_SHARE, }; diff --git a/cocode/src/constants/theme.js b/cocode/src/constants/theme.js index dba7d1b5..04a17bec 100644 --- a/cocode/src/constants/theme.js +++ b/cocode/src/constants/theme.js @@ -6,6 +6,7 @@ const DEFAULT_THEME = { textColor: '#ffffff', exceptHeaderHeight: '88vh', + headerMinHeight: '9vh', headerHeight: '12vh' }; @@ -16,11 +17,11 @@ const DROPDOWN_THEME = { }; const BROWSER_THEME = { + iframeBGColor: '#ffffff', + loadingOverlayBGColor: '#000000', browserHeaderBGColor: '#1d2022', - addressInputBGColor: '#000', - addressInputTextColor: '#fff', - - browserHeight: '88vh' + addressInputBGColor: '#000000', + addressInputTextColor: '#ffffff', }; const TAB_CONTAINER_THEME = { @@ -100,12 +101,13 @@ const INFO_TAB_THEME = { }; const LIVE_TAB_THEME = { - liveButtonBGColorHover: '#C74040B2', - liveButtonBGColor: '#880000', + liveButtonBGColorHover: '#880000', + liveButtonBGColor: '#880000ba', liveFontColor: '#bcbcbc', liveStatusLabelColor: '#c74040b3', liveLinkBGColor: '#000000', - liveSelfLabelColor: '#333333' + liveSelfLabelColor: '#676767', + liveCircleBGColor: '#ffffff' }; const FILE_TAB_THEME = { @@ -125,6 +127,12 @@ const TOAST_THEME = { toastErrorDeco: 'rgba(233,42,61,0.98)' }; +const SIGN_IN_THEME = { + signInButtonBGColor: '#e7e7e7', + signInButtonBGHoverColor: '#ffffff', + signInButtonTextColor: '#000000', +}; + export { DEFAULT_THEME, DROPDOWN_THEME, @@ -138,5 +146,6 @@ export { FILE_TAB_THEME, MONACO_THEME, LIVE_TAB_THEME, - TOAST_THEME + TOAST_THEME, + SIGN_IN_THEME }; diff --git a/cocode/src/containers/Common/Header/index.js b/cocode/src/containers/Common/Header/index.js index fe8cf8dd..2190d833 100644 --- a/cocode/src/containers/Common/Header/index.js +++ b/cocode/src/containers/Common/Header/index.js @@ -2,7 +2,7 @@ import React, { useState, useContext } from 'react'; import * as Styled from './style'; import { Link, useHistory } from 'react-router-dom'; -import deleteCookie from 'utils/deleteCookie'; +import { deleteCookie } from 'utils/controlCookie'; import Logo from 'components/Common/Logo'; import Modal from 'components/Common/Modal'; @@ -10,20 +10,24 @@ import UserProfile from 'components/Common/UserProfile'; import ModalPortal from 'components/Common/ModalPortal'; import LoginModalBody from 'components/Common/LoginModalBody'; -import UserContext from 'contexts/UserContext'; +import { UserContext } from 'contexts'; -function Header() { +const CONFIRM_LOGOUT = '로그아웃 하시겠습니까?'; + +function Header({ name }) { const history = useHistory(); - const { user } = useContext(UserContext); + const { user, setUser } = useContext(UserContext); const [isSignInModalOpen, setIsSignInModalOpen] = useState(false); const handleOpenSignInModal = () => setIsSignInModalOpen(true); const handleCloseSignInModal = () => setIsSignInModalOpen(false); const handleClickDashBoard = () => history.push('/dashboard'); const handleSignOut = () => { - const confirm = window.confirm('로그아웃 하시겠습니까?'); + const confirm = window.confirm(CONFIRM_LOGOUT); if (!confirm) return; deleteCookie('jwt'); + setUser(null); + history.replace('../'); }; const profileDropDownMenuItems = [ @@ -38,14 +42,12 @@ function Header() { ]; return ( - + - {/* - History - */} - + {name || ''} +
{user ? ( )} - +
); } diff --git a/cocode/src/containers/Common/Header/style.js b/cocode/src/containers/Common/Header/style.js index 61ab4b5c..d0c5f304 100644 --- a/cocode/src/containers/Common/Header/style.js +++ b/cocode/src/containers/Common/Header/style.js @@ -4,32 +4,20 @@ const Header = styled.header` & { display: flex; flex-direction: row; - justify-content: flex-start; + justify-content: space-between; align-items: center; - height: ${({ theme }) => theme.headerHeight}; + height: ${({ theme, isMinHeight }) => + isMinHeight ? theme.headerMinHeight : theme.headerHeight}; background-color: ${({ theme }) => theme.backgroundColor}; padding: 2rem 2.3rem; } `; -const HeaderCategory = styled.button` +const ProjectName = styled.div` & { - margin-left: 1.5rem; - - font-size: 1.4rem; - font-weight: 100; - } - - &:hover { - color: ${({ theme }) => theme.mainColor}; - } -`; - -const HeaderRightSideArea = styled.div` - & { - margin-left: auto; + font-size: 1.3rem; } `; @@ -44,4 +32,4 @@ const SignInButton = styled.button` } `; -export { Header, SignInButton, HeaderCategory, HeaderRightSideArea }; +export { Header, SignInButton, ProjectName }; diff --git a/cocode/src/containers/Common/LoadingSpinner/index.js b/cocode/src/containers/Common/LoadingSpinner/index.js new file mode 100644 index 00000000..f6dfb12d --- /dev/null +++ b/cocode/src/containers/Common/LoadingSpinner/index.js @@ -0,0 +1,14 @@ +import React from 'react'; +import * as Styled from './style'; +import CoconutSpinner from 'components/Common/CoconutSpinner'; + +function LoadingSpinner({ message }) { + return ( + + + {message} + + ); +} + +export default LoadingSpinner; diff --git a/cocode/src/pages/DashBoard/style.js b/cocode/src/containers/Common/LoadingSpinner/style.js similarity index 100% rename from cocode/src/pages/DashBoard/style.js rename to cocode/src/containers/Common/LoadingSpinner/style.js diff --git a/cocode/src/containers/DashBoard/ProjectCardList/style.js b/cocode/src/containers/DashBoard/ProjectCardList/style.js index 72c9ab4c..bd6b1488 100644 --- a/cocode/src/containers/DashBoard/ProjectCardList/style.js +++ b/cocode/src/containers/DashBoard/ProjectCardList/style.js @@ -1,7 +1,7 @@ import styled from 'styled-components'; const Main = styled.main` - height: 100%; + height: 88vh; padding: 3rem; `; diff --git a/cocode/src/containers/Live/DependencyTab/index.js b/cocode/src/containers/Live/DependencyTab/index.js new file mode 100644 index 00000000..ec529a8d --- /dev/null +++ b/cocode/src/containers/Live/DependencyTab/index.js @@ -0,0 +1,44 @@ +import React, { useContext } from 'react'; +import * as Styled from './style'; + +import CoconutSpinner from 'components/Common/CoconutSpinner'; +import Dependency from 'components/Project/Dependency'; +import DependencyNow from 'components/Project/DependencyNow'; +import DependencySearch from 'components/Project/DependencySearch'; + +import { ProjectContext } from 'contexts'; + +const TabTitleFirst = 'DEPENDENCIES'; +const TabTitleSecond = 'SEARCH DEPENDENCY'; + +function InstallingDisplay() { + return ( + + + + Please wait to install module... + + + ); +} + +function DependencyTab() { + const { project } = useContext(ProjectContext); + const { dependencyInstalling } = project; + + return ( + + {dependencyInstalling && } + + + + + + + + + + ); +} + +export default DependencyTab; diff --git a/cocode/src/containers/Live/DependencyTab/style.js b/cocode/src/containers/Live/DependencyTab/style.js new file mode 100644 index 00000000..f036d895 --- /dev/null +++ b/cocode/src/containers/Live/DependencyTab/style.js @@ -0,0 +1,47 @@ +import styled from 'styled-components'; + +const Frame = styled.div` + & { + position: relative; + + height: 100%; + } + + & > * { + width: 100%; + } +`; + +const InstallingDisplay = styled.div` + & { + position: absolute; + z-index: 1; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + height: 100%; + width: 100%; + + background-color: rgba(0, 0, 0, 0.7); + } +`; + +const InstallPhrase = styled.p` + & { + margin-top: 2rem; + + font-size: 1rem; + font-weight: lighter; + } +`; + +const DependencyArea = styled.div` + & { + position: absolute; + } +`; + +export { Frame, InstallingDisplay, InstallPhrase, DependencyArea }; diff --git a/cocode/src/containers/Live/Editor/index.js b/cocode/src/containers/Live/Editor/index.js new file mode 100644 index 00000000..c75558fc --- /dev/null +++ b/cocode/src/containers/Live/Editor/index.js @@ -0,0 +1,221 @@ +import React, { useState, useContext, useEffect, useRef } from 'react'; +import { useParams } from 'react-router-dom'; +import * as Styled from './style'; + +import FileTabBar from 'components/Project/FileTabBar'; +import MonacoEditor from 'components/Project/MonacoEditor'; + +import { LiveContext, UserContext, ProjectContext } from 'contexts'; +import { + updateCodeActionCreator, + updateCodeFromFileIdActionCreator +} from 'actions/Project'; + +import useFetch from 'hooks/useFetch'; + +import { CursorWidget } from 'utils/monacoWidget'; + +let timer; +const DEBOUNCING_TIME = 1000; +const EVENT_DELAY = 10; + +const MAX_RANGE = { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 9999, + endColumn: 9999 +}; + +const userCursor = {}; + +function Editor({ handleForkCoconut }) { + const { user } = useContext(UserContext); + const { projectId } = useParams(); + const { project, dispatchProject } = useContext(ProjectContext); + const { socket } = useContext(LiveContext); + const [code, setCode] = useState(project.editingCode); + const [isEditorMounted, setIsEditorMounted] = useState(false); + const [_, setRequest] = useFetch({}); + + const [fileSelectFlag, setFileSelectFlag] = useState(undefined); + const { selectedFileId, files } = project; + + const editorRef = useRef(); + const isBusy = useRef(true); + const pendingEvent = useRef(false); + const selectedRef = useRef(); + const filesRef = useRef(); + + const handleOnChangeCodeInMonaco = (_, changedCode) => { + if (timer) clearTimeout(timer); + + timer = setTimeout(() => { + setCode(changedCode); + }, DEBOUNCING_TIME); + }; + + const handleChnageSelectedFileMonaco = ( + source, + text, + range = MAX_RANGE + ) => { + isBusy.current = true; + if (editorRef.current) { + editorRef.current.executeEdits(source, [ + { + range, + text, + forceMoveMarkers: true + } + ]); + } + setTimeout(() => { + isBusy.current = false; + }, 0); + }; + + const handleChangedSelectedFile = () => { + if (!project) return; + if (!filesRef.current) return; + selectedRef.current = selectedFileId; + setCode(project.editingCode); + + handleChnageSelectedFileMonaco( + 'changeFile', + filesRef.current[selectedFileId].contents + ); + }; + + const handleUpdateCode = () => { + if (fileSelectFlag !== selectedFileId) { + setFileSelectFlag(selectedFileId); + return; + } + const updateCodeAction = updateCodeActionCreator({ + changedCode: code + }); + dispatchProject(updateCodeAction); + }; + + const handleEmit = (e, timeStamp) => { + if (isBusy.current) return; + if (!timeStamp) timeStamp = Date(); + // if (pendingEvent.current) handleEmit(e, timeStamp); + const change = e.changes[0]; + const operation = { + rangeLength: change.rangeLength, + rangeOffset: change.rangeOffset, + text: change.text.replace(/\r\n/g, '\n'), + timeStamp: timeStamp + }; + if (!socket) return; + // pendingEvent.current = true; + socket.emit('change', selectedRef.current, operation); + }; + + const handleCursor = e => { + if (!socket) return; + socket.emit('moveCursor', selectedRef.current, e.position); + }; + + const handleEditorDidMount = (_, editor) => { + editorRef.current = editor; + editor.onDidChangeModelContent(handleEmit); + editor.onDidChangeCursorPosition(handleCursor); + setIsEditorMounted(true); + }; + + useEffect(handleUpdateCode, [code]); + useEffect(handleChangedSelectedFile, [project.selectedFileId]); + + useEffect(() => { + if (!isEditorMounted) return; + selectedRef.current = selectedFileId; + isBusy.current = true; + handleChnageSelectedFileMonaco('initial', project.editingCode); + }, [isEditorMounted]); + + useEffect(() => { + //initialize + if (!socket) return; + if (!isEditorMounted) return; + isBusy.current = false; + filesRef.current = JSON.parse(JSON.stringify(files)); + socket.on('change', handleOnChangeCode); + socket.on('moveCursor', handleMoveCursor); + }, [socket, isEditorMounted]); + + const handleOnChangeCode = (socketId, fileId, op) => { + if (socket.id === socketId) { + setTimeout(() => { + pendingEvent.current = false; + }, EVENT_DELAY); + return; + } + + if (selectedRef.current !== fileId) { + const originCode = filesRef.current[fileId].contents; + + const str1 = originCode.slice(0, op.rangeOffset); + const str2 = originCode.slice(op.rangeOffset + op.rangeLength); + const changedCode = `${str1}${op.text}${str2}`; + filesRef.current[fileId].contents = changedCode; + const updateCodeFromFileIdAction = updateCodeFromFileIdActionCreator( + { + fileId, + changedCode + } + ); + dispatchProject(updateCodeFromFileIdAction); + return; + } + + const rangeOffset = op.rangeOffset; + const rangeLength = op.rangeLength; + const text = op.text; + + const startPosition = editorRef.current + .getModel() + .getPositionAt(rangeOffset); + const endPosition = editorRef.current + .getModel() + .getPositionAt(rangeOffset + rangeLength); + + handleChnageSelectedFileMonaco(socketId, text, { + startLineNumber: startPosition.lineNumber, + startColumn: startPosition.column, + endLineNumber: endPosition.lineNumber, + endColumn: endPosition.column + }); + }; + + const handleMoveCursor = (username, fileId, position) => { + if (!userCursor[username]) { + const widget = new CursorWidget( + editorRef.current, + username, + position + ); + userCursor[username] = widget; + editorRef.current.addContentWidget(widget); + } + if (selectedRef.current === fileId) + userCursor[username].showCursor(position); + else userCursor[username].hiddenCursor(); + }; + + return ( + + + + + ); +} + +export default Editor; diff --git a/cocode/src/containers/Live/Editor/style.js b/cocode/src/containers/Live/Editor/style.js new file mode 100644 index 00000000..24eb50c5 --- /dev/null +++ b/cocode/src/containers/Live/Editor/style.js @@ -0,0 +1,14 @@ +import styled from 'styled-components'; + +const Editor = styled.section` + & { + display: flex; + flex-direction: column; + } + + .Stretch-width { + flex-grow: 2; + } +`; + +export { Editor }; diff --git a/cocode/src/containers/Live/ExplorerTab/index.js b/cocode/src/containers/Live/ExplorerTab/index.js new file mode 100644 index 00000000..a3afa388 --- /dev/null +++ b/cocode/src/containers/Live/ExplorerTab/index.js @@ -0,0 +1,104 @@ +import React, { useState, useContext } from 'react'; +import * as Styled from './style'; + +import { + NewFolderIcon, + NewFileIcon +} from 'components/Project/ExplorerTabIcons'; +import Directory from 'components/Project/Directory'; +import NewFile from 'components/Project/NewFile'; + +import { ProjectContext } from 'contexts'; +import { + selectFileActionCreator, + updateFileNameActionCreator, + deleteFileActionCreator, + moveFileActionCreator +} from 'actions/Project'; + +const TAB_TITLE = 'EXPLORER'; + +function TabHeader({ handleCreateFile }) { + return ( + + {TAB_TITLE} + + + + + + ); +} + +function ExplorerTab() { + const [isNewFileCreating, setIsNewFileCreating] = useState(false); + const [createFileType, setCreateFileType] = useState(null); + + const { project, dispatchProject } = useContext(ProjectContext); + const { files, root } = project; + const childIdsInRoot = files[root].child; + + const handleCreateFile = type => { + setCreateFileType(type); + setIsNewFileCreating(true); + }; + + const handleEndCreateFile = () => setIsNewFileCreating(false); + + const handleSelectFile = selectedFileId => { + const selectFileAction = selectFileActionCreator({ selectedFileId }); + dispatchProject(selectFileAction); + }; + + const handleEditFileName = (selectedFileId, changedName) => { + const updateFileNameAction = updateFileNameActionCreator({ + selectedFileId, + changedName + }); + dispatchProject(updateFileNameAction); + }; + + const handleDeleteFile = deleteFileId => { + const deleteFileAction = deleteFileActionCreator({ deleteFileId }); + dispatchProject(deleteFileAction); + }; + + const handleMoveFile = (directoryId, fileId) => { + const moveFileAction = moveFileActionCreator({ + directoryId, + fileId + }); + dispatchProject(moveFileAction); + }; + + return ( + <> + + {isNewFileCreating && ( + + )} + + + + + ); +} + +export default ExplorerTab; diff --git a/cocode/src/containers/Live/ExplorerTab/style.js b/cocode/src/containers/Live/ExplorerTab/style.js new file mode 100644 index 00000000..4c6f6aa1 --- /dev/null +++ b/cocode/src/containers/Live/ExplorerTab/style.js @@ -0,0 +1,67 @@ +import styled from 'styled-components'; +import { + TAB_CONTAINER_THEME, + EXPLORER_TAB_CONTAINER_THEME +} from 'constants/theme'; + +const { + explorerTabContainerSelectedFileBGColor, +} = EXPLORER_TAB_CONTAINER_THEME; + +const { + tabContainerHeaderBGColor, + tabContainerTitleColor, + tabContainerTitleSize, + tabContainerTitleWeight +} = TAB_CONTAINER_THEME; + +const TabBody = styled.div` + & { + height: 100%; + } + + .Is-selected-file { + background-color: ${explorerTabContainerSelectedFileBGColor}; + } +`; + +const TabHeader = styled.header` + & { + display: flex; + flex-direction: row; + background-color: ${tabContainerHeaderBGColor}; + } + + .Tab-header-Side-icons { + margin: auto 0; + margin-left: auto; + margin-right: 1rem; + } +`; + +const Title = styled.h1` + & { + padding: 0.7rem 1rem; + + color: ${tabContainerTitleColor}; + font-size: ${tabContainerTitleSize}; + font-weight: ${tabContainerTitleWeight}; + } +`; + +const SideIcons = styled.span` + & { + display: flex; + flex-direction: row; + + margin-left: auto; + } + + & > svg { + margin: 0 0.2rem; + + cursor: pointer; + } +`; + +export { TabHeader, TabBody, Title, SideIcons }; diff --git a/cocode/src/containers/Live/LiveOnTab/close.svg b/cocode/src/containers/Live/LiveOnTab/close.svg new file mode 100644 index 00000000..6adde6f1 --- /dev/null +++ b/cocode/src/containers/Live/LiveOnTab/close.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/cocode/src/containers/Live/LiveOnTab/copy.svg b/cocode/src/containers/Live/LiveOnTab/copy.svg new file mode 100644 index 00000000..912733d5 --- /dev/null +++ b/cocode/src/containers/Live/LiveOnTab/copy.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/cocode/src/containers/Live/LiveOnTab/index.js b/cocode/src/containers/Live/LiveOnTab/index.js new file mode 100644 index 00000000..61529764 --- /dev/null +++ b/cocode/src/containers/Live/LiveOnTab/index.js @@ -0,0 +1,59 @@ +import React, { useContext, useRef } from 'react'; +import { useParams, useHistory } from 'react-router-dom'; +import * as Styled from './style'; +import { LiveContext, UserContext } from 'contexts'; + +import copy from './copy.svg'; +import close from './close.svg'; +import LiveUsers from 'components/Live/LiveUsers'; + +import { liveOffActionCreator } from 'actions/Live'; +import { copyToClipboard } from 'utils/domControl'; + +const LIVE_STATUS_LABEL = 'You’ve gone live!'; +const ON_BUTTON_LABEL = 'Stop Live'; +const ON_DESCRIPTION = + 'Share this link with others to invite them to the live.'; + +function LiveOnTab() { + const link = useRef(); + const history = useHistory(); + const { projectId } = useParams(); + const { user } = useContext(UserContext); + const { url, socket, participants, owner, dispatchLive } = useContext(LiveContext); + + const handleCloseSocket = () => dispatchLive(liveOffActionCreator()); + + const handleDisconnectSocket = () => { + socket.emit('close'); + socket.on('close', handleCloseSocket); + history.replace(`../project/${projectId}`); + }; + + const handleCopyLink = () => copyToClipboard(link.current); + + return ( + + + + {LIVE_STATUS_LABEL} + + {ON_DESCRIPTION} + + + {url} + + {user === owner ? ( + + + {ON_BUTTON_LABEL} + + ) : ''} + {owner ? ( + + ) : ''} + + ); +} + +export default LiveOnTab; \ No newline at end of file diff --git a/cocode/src/containers/Live/LiveOnTab/style.js b/cocode/src/containers/Live/LiveOnTab/style.js new file mode 100644 index 00000000..29a21573 --- /dev/null +++ b/cocode/src/containers/Live/LiveOnTab/style.js @@ -0,0 +1,99 @@ +import styled from 'styled-components'; +import { LIVE_TAB_THEME } from 'constants/theme'; + +const Container = styled.div` + & { + margin: 0.7rem 1rem; + } +`; + +const Description = styled.div` + & { + margin-bottom: 1rem; + color: ${LIVE_TAB_THEME.liveFontColor}; + font-size: 1rem; + font-weight: 100; + } +`; + +const Button = styled.button` + & { + display: flex; + align-items: center; + justify-content: center; + width: -webkit-fill-available; + padding: 0.7rem 2.2rem; + border-radius: 0.7rem; + background-color: ${LIVE_TAB_THEME.liveButtonBGColor}; + font-size: 1rem; + font-weight: 400; + } + + &:hover { + background-color: ${LIVE_TAB_THEME.liveButtonBGColorHover}; + } +`; + +const Close = styled.img` + & { + width: 0.8rem; + height: 0.8rem; + margin-right: 0.5rem; + } +`; + +const LiveStatusLabel = styled.div` + & { + display: flex; + margin-bottom: 1rem; + color: ${LIVE_TAB_THEME.liveStatusLabelColor}; + font-size: 1.1rem; + font-weight: lighter; + } +`; + +const LiveStatusSpan = styled.span` + & { + background-color: ${LIVE_TAB_THEME.liveStatusLabelColor}; + margin: auto 1rem auto 0.3rem; + width: 0.6rem; + height: 0.6rem; + border-radius: 50%; + } +`; + +const LinkURL = styled.div` + & { + display: flex; + width: 100%; + overflow: auto; + padding: 0.5rem 1rem; + margin-bottom: 0.5rem; + align-items: center; + background-color: ${LIVE_TAB_THEME.liveLinkBGColor}; + color: ${LIVE_TAB_THEME.liveFontColor}; + font-size: 0.9rem; + user-select: text; + } +`; + +const Copy = styled.img` + & { + width: 0.8rem; + height: 0.8rem; + cursor: pointer; + margin-right: 0.5rem; + user-select: none; + } +`; + +export { + Container, + Description, + Button, + Close, + LiveStatusLabel, + LiveStatusSpan, + LinkURL, + Copy +}; diff --git a/cocode/src/containers/Live/LiveTab/index.js b/cocode/src/containers/Live/LiveTab/index.js new file mode 100644 index 00000000..b2f78b93 --- /dev/null +++ b/cocode/src/containers/Live/LiveTab/index.js @@ -0,0 +1,19 @@ +import React from 'react'; +import * as Styled from './style'; + +import LiveOnTab from 'containers/Live/LiveOnTab'; + +const TAB_TITLE = 'LIVE'; + +function LiveTab() { + return ( + <> + {TAB_TITLE} +
+ +
+ + ); +} + +export default LiveTab; diff --git a/cocode/src/containers/Live/LiveTab/style.js b/cocode/src/containers/Live/LiveTab/style.js new file mode 100644 index 00000000..d2378806 --- /dev/null +++ b/cocode/src/containers/Live/LiveTab/style.js @@ -0,0 +1,16 @@ +import styled from 'styled-components'; +import { TAB_CONTAINER_THEME } from 'constants/theme'; + +const Title = styled.h1` + & { + padding: 0.7rem 1rem; + color: ${TAB_CONTAINER_THEME.tabContainerTitleColor}; + font-size: ${TAB_CONTAINER_THEME.tabContainerTitleSize}; + font-weight: ${TAB_CONTAINER_THEME.tabContainerTitleWeight}; + background-color: ${TAB_CONTAINER_THEME.tabContainerHeaderBGColor}; + } +`; + +export { + Title +}; diff --git a/cocode/src/containers/Live/TabBar/dependency.svg b/cocode/src/containers/Live/TabBar/dependency.svg new file mode 100644 index 00000000..285034f5 --- /dev/null +++ b/cocode/src/containers/Live/TabBar/dependency.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/cocode/src/containers/Live/TabBar/explorer.svg b/cocode/src/containers/Live/TabBar/explorer.svg new file mode 100644 index 00000000..5f7c2792 --- /dev/null +++ b/cocode/src/containers/Live/TabBar/explorer.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/cocode/src/containers/Live/TabBar/index.js b/cocode/src/containers/Live/TabBar/index.js new file mode 100644 index 00000000..e8caca58 --- /dev/null +++ b/cocode/src/containers/Live/TabBar/index.js @@ -0,0 +1,44 @@ +import React, { useContext } from 'react'; +import * as Styled from './style'; + +import { ProjectContext } from 'contexts'; +import TabIcon from 'components/Project/TabIcon'; +import Explorer from './explorer.svg'; +import Live from './live.svg'; + +function TabBar({ theme }) { + const { clickedTabIndex, setClickedTabIndex } = useContext(ProjectContext); + + const handleSetClickedIndex = index => setClickedTabIndex(index); + + const tabIcons = [ + { + name: 'explorer', + icon: Explorer + }, + { + name: 'live', + icon: Live, + } + ]; + + return ( + + {tabIcons.map(({ name, icon }, index) => { + return ( + + ); + })} + + ); +} + +export default TabBar; diff --git a/cocode/src/containers/Live/TabBar/live.svg b/cocode/src/containers/Live/TabBar/live.svg new file mode 100644 index 00000000..79bd94bb --- /dev/null +++ b/cocode/src/containers/Live/TabBar/live.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/cocode/src/containers/Live/TabBar/style.js b/cocode/src/containers/Live/TabBar/style.js new file mode 100644 index 00000000..f1aa8450 --- /dev/null +++ b/cocode/src/containers/Live/TabBar/style.js @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +const TabBar = styled.nav` + & { + min-width: 4rem; + display: inline-flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + + background-color: ${({ tabBarBGColor }) => tabBarBGColor}; + } +`; + +export { TabBar }; diff --git a/cocode/src/containers/Live/TabContainer/index.js b/cocode/src/containers/Live/TabContainer/index.js new file mode 100644 index 00000000..963ee87a --- /dev/null +++ b/cocode/src/containers/Live/TabContainer/index.js @@ -0,0 +1,25 @@ +import React, { useEffect, useContext } from 'react'; +import * as Styled from './style'; + +import { ProjectContext } from 'contexts'; +import ExplorerTab from '../ExplorerTab'; +import LiveTab from '../LiveTab'; + +function TabContainer() { + const { clickedTabIndex } = useContext(ProjectContext); + + const tapMapping = { + 0: , + 1: + }; + + const renderTab = () => tapMapping[clickedTabIndex]; + + useEffect(() => { + renderTab(); + }, [clickedTabIndex]); + + return {renderTab()}; +} + +export default TabContainer; diff --git a/cocode/src/containers/Live/TabContainer/style.js b/cocode/src/containers/Live/TabContainer/style.js new file mode 100644 index 00000000..1c75d251 --- /dev/null +++ b/cocode/src/containers/Live/TabContainer/style.js @@ -0,0 +1,10 @@ +import styled from 'styled-components'; +import { TAB_CONTAINER_THEME } from 'constants/theme'; + +const Container = styled.section` + & { + background-color: ${TAB_CONTAINER_THEME.tabContainerBGColor}; + } +`; + +export { Container }; diff --git a/cocode/src/containers/Project/DependencyTab/index.js b/cocode/src/containers/Project/DependencyTab/index.js index d027436c..ec529a8d 100644 --- a/cocode/src/containers/Project/DependencyTab/index.js +++ b/cocode/src/containers/Project/DependencyTab/index.js @@ -6,7 +6,7 @@ import Dependency from 'components/Project/Dependency'; import DependencyNow from 'components/Project/DependencyNow'; import DependencySearch from 'components/Project/DependencySearch'; -import ProjectContext from 'contexts/ProjectContext'; +import { ProjectContext } from 'contexts'; const TabTitleFirst = 'DEPENDENCIES'; const TabTitleSecond = 'SEARCH DEPENDENCY'; diff --git a/cocode/src/containers/Project/Editor/index.js b/cocode/src/containers/Project/Editor/index.js index 89839b54..c0b1b959 100644 --- a/cocode/src/containers/Project/Editor/index.js +++ b/cocode/src/containers/Project/Editor/index.js @@ -5,7 +5,7 @@ import * as Styled from './style'; import FileTabBar from 'components/Project/FileTabBar'; import MonacoEditor from 'components/Project/MonacoEditor'; -import { UserContext, ProjectContext } from 'contexts'; +import { ProjectContext } from 'contexts'; import { updateCodeActionCreator, saveFileActionCreator @@ -20,10 +20,11 @@ import { isPressCtrlAndS } from 'utils/keyDownEvent'; let timer; const DEBOUNCING_TIME = 800; -function Editor({ handleForkCoconut }) { - const { user } = useContext(UserContext); +function Editor() { const { projectId } = useParams(); - const { project, dispatchProject } = useContext(ProjectContext); + const { project, dispatchProject, forkCoconut } = useContext( + ProjectContext + ); const [code, setCode] = useState(project.editingCode); const [isEditorMounted, setIsEditorMounted] = useState(false); const [_, setRequest] = useFetch({}); @@ -50,8 +51,6 @@ function Editor({ handleForkCoconut }) { setRequest(updateFileAPI); }; - const isNotMyProject = !user || user.username !== project.author; - const handleOnKeyDown = e => { if (!isPressCtrlAndS(e)) return; @@ -59,10 +58,8 @@ function Editor({ handleForkCoconut }) { const { files, selectedFileId } = project; if (!files[selectedFileId].isEditing) return; - if (isNotMyProject) { - handleForkCoconut(); - return; - } + const isProgress = forkCoconut({}); + if (isProgress) return; handleRequestUpdateCode(); dispatchProject(saveFileActionCreator()); diff --git a/cocode/src/containers/Project/ExplorerTab/index.js b/cocode/src/containers/Project/ExplorerTab/index.js index 016fc38b..a3afa388 100644 --- a/cocode/src/containers/Project/ExplorerTab/index.js +++ b/cocode/src/containers/Project/ExplorerTab/index.js @@ -8,7 +8,7 @@ import { import Directory from 'components/Project/Directory'; import NewFile from 'components/Project/NewFile'; -import ProjectContext from 'contexts/ProjectContext'; +import { ProjectContext } from 'contexts'; import { selectFileActionCreator, updateFileNameActionCreator, @@ -16,7 +16,7 @@ import { moveFileActionCreator } from 'actions/Project'; -const TAB_TITLE = 'EXPLOLER'; +const TAB_TITLE = 'EXPLORER'; function TabHeader({ handleCreateFile }) { return ( diff --git a/cocode/src/containers/Project/InfoTab/index.js b/cocode/src/containers/Project/InfoTab/index.js index 2fcab4b0..a35980c0 100644 --- a/cocode/src/containers/Project/InfoTab/index.js +++ b/cocode/src/containers/Project/InfoTab/index.js @@ -4,7 +4,7 @@ import * as Styled from './style'; import Modify from './modify.svg'; import { KEY_CODE_ENTER } from 'constants/keyCode'; -import ProjectContext from 'contexts/ProjectContext'; +import { ProjectContext } from 'contexts'; import { updateProjectInfoActionCreator } from 'actions/Project'; import useFetch from 'hooks/useFetch'; import { @@ -29,6 +29,7 @@ function Info({ setRequest, dispatchProject }) { + const { forkCoconut } = useContext(ProjectContext); const input = useRef(); const [isEditable, setIsEditable] = useState(false); const [value, setValue] = useState(content); @@ -44,6 +45,12 @@ function Info({ const newContent = event.currentTarget.textContent; if (content === newContent) return; setValue(event.currentTarget.textContent); + + const isProgress = forkCoconut({ + info: { [title]: newContent } + }); + if (isProgress) return; + setRequest( updateCoconutsAPICreator(projectId, { [title]: newContent }) ); diff --git a/cocode/src/containers/Project/LiveOffTab/index.js b/cocode/src/containers/Project/LiveOffTab/index.js new file mode 100644 index 00000000..2aeb4341 --- /dev/null +++ b/cocode/src/containers/Project/LiveOffTab/index.js @@ -0,0 +1,32 @@ +import React, { useContext } from 'react'; +import { useParams, useHistory } from 'react-router-dom'; +import * as Styled from './style'; +import { ProjectContext } from 'contexts'; + +const OFF_BUTTON_LABEL = 'Go Live'; +const OFF_DESCRIPTION = + 'Invite others to live edit this coconut with you. We’re doing it live!'; + +function LiveOffTab() { + const history = useHistory(); + const { projectId } = useParams(); + const { forkCoconut } = useContext(ProjectContext); + const handleConnectSocket = () => { + const idOfNewProject = forkCoconut({ live: true }); + if (idOfNewProject) return; + + history.replace(`../live/${projectId}`); + }; + + return ( + + {OFF_DESCRIPTION} + + + {OFF_BUTTON_LABEL} + + + ); +} + +export default LiveOffTab; diff --git a/cocode/src/containers/Project/LiveOffTab/style.js b/cocode/src/containers/Project/LiveOffTab/style.js new file mode 100644 index 00000000..5cb0fa7a --- /dev/null +++ b/cocode/src/containers/Project/LiveOffTab/style.js @@ -0,0 +1,52 @@ +import styled from 'styled-components'; +import { LIVE_TAB_THEME } from 'constants/theme'; + +const Container = styled.div` + & { + margin: 0.7rem 1rem; + } +`; + +const Description = styled.div` + & { + color: ${LIVE_TAB_THEME.liveFontColor}; + font-size: 1rem; + font-weight: 100; + margin-bottom: 1rem; + } +`; + +const Button = styled.button` + & { + display: flex; + align-items: center; + justify-content: center; + width: -webkit-fill-available; + padding: 0.7rem 2.2rem; + border-radius: 0.7rem; + background-color: ${LIVE_TAB_THEME.liveButtonBGColor}; + font-size: 1rem; + font-weight: 400; + } + + &:hover { + background-color: ${LIVE_TAB_THEME.liveButtonBGColorHover}; + } +`; + +const Circle = styled.div` + & { + width: 0.5rem; + height: 0.5rem; + margin-right: 0.5rem; + background-color: ${LIVE_TAB_THEME.liveCircleBGColor}; + border-radius: 50%; + } +`; + +export { + Container, + Description, + Button, + Circle +}; diff --git a/cocode/src/containers/Project/LiveTab/LiveOff.js b/cocode/src/containers/Project/LiveTab/LiveOff.js deleted file mode 100644 index 36732394..00000000 --- a/cocode/src/containers/Project/LiveTab/LiveOff.js +++ /dev/null @@ -1,17 +0,0 @@ -import * as Styled from './style'; -import React from 'react'; - -const OFF_BUTTON_LABEL = 'Go Live'; -const OFF_DESCRIPTION = - 'Invite others to live edit this coconut with you. We’re doing it live!'; - -function LiveOff({ onClick }) { - return ( - <> - {OFF_DESCRIPTION} - {OFF_BUTTON_LABEL} - - ); -} - -export default LiveOff; diff --git a/cocode/src/containers/Project/LiveTab/LiveOn.js b/cocode/src/containers/Project/LiveTab/LiveOn.js deleted file mode 100644 index 3e0e03ac..00000000 --- a/cocode/src/containers/Project/LiveTab/LiveOn.js +++ /dev/null @@ -1,28 +0,0 @@ -import React, { useContext } from 'react'; -import { LiveContext } from 'contexts'; -import * as Styled from './style'; -import LiveUserProfile from 'components/Project/LiveUserProfile'; - -const LIVE_STATUS_LABEL = 'You’ve gone live!'; -const ON_BUTTON_LABEL = 'Stop Live'; -const ON_DESCRIPTION = - 'Share this link with others to invite them to the live.'; - -function LiveOn({ onClick }) { - const { url, participants, owner } = useContext(LiveContext); - - return ( - <> - - - {LIVE_STATUS_LABEL} - - {ON_DESCRIPTION} - {url} - {ON_BUTTON_LABEL} - - - ); -} - -export default LiveOn; diff --git a/cocode/src/containers/Project/LiveTab/index.js b/cocode/src/containers/Project/LiveTab/index.js index d58cc615..113b97b0 100644 --- a/cocode/src/containers/Project/LiveTab/index.js +++ b/cocode/src/containers/Project/LiveTab/index.js @@ -1,53 +1,17 @@ -import React, { useContext } from 'react'; +import React from 'react'; import * as Styled from './style'; -import { LiveContext } from 'contexts'; -import LiveOn from './LiveOn'; -import LiveOff from './LiveOff'; -import { liveOffActionCreator, fetchLiveActionCreator } from 'actions/Live'; -import avatar from 'components/Common/UserProfile/avatar.jpeg'; +import LiveOffTab from 'containers/Project/LiveOffTab'; -const TAB_TATILE = 'LIVE'; - -const dummy = { - url: 'https://cocode.com/live/', - participants: [ - { - username: 'basiltoast', - avatar - }, - { - username: 'basiltoast', - avatar - }, - { - username: 'basiltoast', - avatar - } - ], - owner: { - username: 'lallaheeee', - avatar - } -}; +const TAB_TITLE = 'LIVE'; function LiveTab() { - const { url, dispatchLive } = useContext(LiveContext); - - const handleTurnLive = () => { - if (url) dispatchLive(liveOffActionCreator()); - else dispatchLive(fetchLiveActionCreator(dummy)); - }; return ( <> - {TAB_TATILE} - - {url ? ( - - ) : ( - - )} - + {TAB_TITLE} +
+ +
); } diff --git a/cocode/src/containers/Project/LiveTab/style.js b/cocode/src/containers/Project/LiveTab/style.js index 67666437..d2378806 100644 --- a/cocode/src/containers/Project/LiveTab/style.js +++ b/cocode/src/containers/Project/LiveTab/style.js @@ -1,15 +1,5 @@ import styled from 'styled-components'; -import { TAB_CONTAINER_THEME, LIVE_TAB_THEME } from 'constants/theme'; - -const Wrapper = styled.div` - & { - padding: 0.7rem 1rem; - } - - & > * { - padding: 0.5rem 0; - } -`; +import { TAB_CONTAINER_THEME } from 'constants/theme'; const Title = styled.h1` & { @@ -21,64 +11,6 @@ const Title = styled.h1` } `; -const Description = styled.div` - & { - color: ${LIVE_TAB_THEME.liveFontColor}; - font-size: 1rem; - font-weight: 100; - } -`; - -const Button = styled.button` - & { - width: -webkit-fill-available; - margin: 1rem 0; - padding: 1rem 2.2rem; - border-radius: 0.3rem; - background-color: ${LIVE_TAB_THEME.liveButtonBGColor}; - font-size: 1rem; - font-weight: 400; - } - - &:hover { - background-color: ${LIVE_TAB_THEME.liveButtonBGColorHover}; - } -`; - -const LiveStatusLabel = styled.div` - & { - display: flex; - color: ${LIVE_TAB_THEME.liveStatusLabelColor}; - font-size: 1.1rem; - font-weight: lighter; - } -`; - -const LiveStatusSpan = styled.span` - & { - background-color: ${LIVE_TAB_THEME.liveStatusLabelColor}; - margin: auto 1rem auto 0.3rem; - width: 0.6rem; - height: 0.6rem; - border-radius: 50%; - } -`; - -const LinkURL = styled.div` - & { - background-color: ${LIVE_TAB_THEME.liveLinkBGColor}; - color: ${LIVE_TAB_THEME.liveFontColor}; - font-size: 0.9rem; - padding-left: 1rem; - } -`; - export { - Wrapper, - Title, - Description, - Button, - LiveStatusLabel, - LiveStatusSpan, - LinkURL + Title }; diff --git a/cocode/src/containers/Project/TabBar/index.js b/cocode/src/containers/Project/TabBar/index.js index ad6e3d1f..aeb86c43 100644 --- a/cocode/src/containers/Project/TabBar/index.js +++ b/cocode/src/containers/Project/TabBar/index.js @@ -1,7 +1,7 @@ import React, { useContext } from 'react'; import * as Styled from './style'; -import ProjectContext from 'contexts/ProjectContext'; +import { ProjectContext } from 'contexts'; import TabIcon from 'components/Project/TabIcon'; diff --git a/cocode/src/containers/Project/TabContainer/index.js b/cocode/src/containers/Project/TabContainer/index.js index de4b2249..d54c8e2f 100644 --- a/cocode/src/containers/Project/TabContainer/index.js +++ b/cocode/src/containers/Project/TabContainer/index.js @@ -1,7 +1,7 @@ import React, { useEffect, useContext } from 'react'; import * as Styled from './style'; -import ProjectContext from 'contexts/ProjectContext'; +import { ProjectContext } from 'contexts'; import InfoTab from '../InfoTab'; import ExplorerTab from '../ExplorerTab'; diff --git a/cocode/src/containers/SignIn/index.js b/cocode/src/containers/SignIn/index.js new file mode 100644 index 00000000..b67a4a46 --- /dev/null +++ b/cocode/src/containers/SignIn/index.js @@ -0,0 +1,22 @@ +import React from 'react'; +import * as Styled from './style'; +import Github from 'components/Common/LoginModalBody/github.svg'; +import { API } from 'config'; + +const SIGN_IN_TITLE = 'Sorry, This service requires a login.'; + +function SignIn() { + const handleClickLoginButton = () => (window.location.href = API.login); + + return ( + + {SIGN_IN_TITLE} + + + Sign In With GitHub + + + ); +} + +export default SignIn; diff --git a/cocode/src/containers/SignIn/style.js b/cocode/src/containers/SignIn/style.js new file mode 100644 index 00000000..cabf42fd --- /dev/null +++ b/cocode/src/containers/SignIn/style.js @@ -0,0 +1,50 @@ +import styled from 'styled-components'; +import { SIGN_IN_THEME } from 'constants/theme'; + +const Logo = styled.img` + height: 1.2rem; + margin-right: 0.8rem; + filter: invert(1); +`; + +const Wrapper = styled.div` + & { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + height: 70vh; + padding: 5rem; + } +`; + +const Title = styled.h1` + & { + text-align: center; + font-size: 3rem; + font-weight: 100; + } +`; + +const LoginButton = styled.button` + & { + display: flex; + justify-content: center; + align-items: center; + margin: 2rem 0 1.5rem 0; + padding: 1rem 2.5rem; + + font-size: 1.5rem; + + background-color: ${SIGN_IN_THEME.signInButtonBGColor}; + color: ${SIGN_IN_THEME.signInButtonTextColor}; + border-radius: 0.5rem; + } + + &:hover { + background-color: ${SIGN_IN_THEME.signInButtonBGHoverColor}; + } +`; + +export { Logo, Wrapper, LoginButton, Title }; diff --git a/cocode/src/hooks/useFetch.js b/cocode/src/hooks/useFetch.js index ad3522da..22889a38 100644 --- a/cocode/src/hooks/useFetch.js +++ b/cocode/src/hooks/useFetch.js @@ -1,7 +1,7 @@ import { useState, useEffect, useReducer } from 'react'; import axios from 'axios'; import { DEFAULT_REQUEST_OPTION } from 'config'; -import APIReducer from 'reducers/APIReducer'; +import { APIReducer } from 'reducers'; import { fetchReadyActionCreator, fetchLoadActionCreator, diff --git a/cocode/src/pages/DashBoard/index.js b/cocode/src/pages/DashBoard/index.js index 3b0cd468..6ddcacd3 100644 --- a/cocode/src/pages/DashBoard/index.js +++ b/cocode/src/pages/DashBoard/index.js @@ -2,24 +2,15 @@ import React, { useContext, useEffect, useReducer } from 'react'; import { useHistory } from 'react-router-dom'; import ProjectCardList from 'containers/DashBoard/ProjectCardList'; import Header from 'containers/Common/Header'; -import CoconutSpinner from 'components/Common/CoconutSpinner'; -import * as Styled from './style'; +import LoadingSpinner from 'containers/Common/LoadingSpinner'; import { UserContext, DashBoardContext } from 'contexts'; -import DashBoardReducer from 'reducers/DashboardReducer'; +import { DashBoardReducer } from 'reducers'; import useFetch from 'hooks/useFetch'; import { getCoconutsAPICreator } from 'apis/DashBoard'; import { fetchCoconutActionCreator } from 'actions/Dashboard'; import { LOADING_DASHBOARD } from 'constants/notificationMessage'; - -function LoadingSpinner() { - return ( - - - {LOADING_DASHBOARD} - - ); -} +import { getCookie } from 'utils/controlCookie'; function DashBoard() { const { user } = useContext(UserContext); @@ -34,10 +25,15 @@ function DashBoard() { data && dispatchDashboard(fetchCoconutActionCreator(data)); }; + if (!getCookie('jwt')) { + localStorage.setItem('redirectURL', window.location.href); + history.replace('../signin'); + } + useEffect(handleRequestGetCoconutAPI, [user]); useEffect(handleSetDashBoardState, [data]); - if (loading) return ; + if (loading) return ; if (error) history.push('/weAreSorry'); return ( diff --git a/cocode/src/pages/Empty/index.js b/cocode/src/pages/Empty/index.js new file mode 100644 index 00000000..4aa19349 --- /dev/null +++ b/cocode/src/pages/Empty/index.js @@ -0,0 +1,18 @@ +import React, { useEffect } from 'react'; +import Header from 'containers/Common/Header'; + +function Empty() { + useEffect(() => { + const redirectURL = localStorage.getItem('redirectURL'); + if (redirectURL) { + window.location.href = redirectURL; + localStorage.removeItem('redirectURL'); + } + }, []); + + return ( +
+ ); +} + +export default Empty; \ No newline at end of file diff --git a/cocode/src/pages/History/index.js b/cocode/src/pages/History/index.js deleted file mode 100644 index 246ca545..00000000 --- a/cocode/src/pages/History/index.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { Route } from 'react-router-dom'; - -import { Version1 } from 'pages'; -import Header from 'containers/Common/Header'; -import CocodeHistory from 'containers/History/CocodeHistory'; - -function HistoryHome() { - return ; -} - -function History({ match }) { - return ( - <> -
- - - - ); -} - -export default History; diff --git a/cocode/src/pages/Live/index.js b/cocode/src/pages/Live/index.js new file mode 100644 index 00000000..f5d490da --- /dev/null +++ b/cocode/src/pages/Live/index.js @@ -0,0 +1,170 @@ +import React, { + useReducer, + useEffect, + useState, + useContext, + useCallback +} from 'react'; +import { useHistory, useParams } from 'react-router-dom'; +import * as Styled from './style'; +import io from 'socket.io-client'; + +import Header from 'containers/Common/Header'; +import TabBar from 'containers/Live/TabBar'; +import TabContainer from 'containers/Live/TabContainer'; +import Editor from 'containers/Live/Editor'; +import LoadingSpinner from 'containers/Common/LoadingSpinner'; +import BrowserV2 from 'components/Project/BrowserV2'; +import { SplitPaneContainer } from 'components/Common/SplitPane'; +import addToast from 'components/Common/Toast'; + +import { LiveContext, ProjectContext, UserContext } from 'contexts'; +import { ProjectReducer } from 'reducers'; +import { fetchProjectActionCreator } from 'actions/Project'; +import { + liveOnActionCreator, + liveOffActionCreator, + liveJoinUserActionCreator, + liveLeaveUserActionCreator +} from 'actions/Live'; + +import { TAB_BAR_THEME } from 'constants/theme'; +import { COCODE_SERVER } from 'config'; +import useFetch from 'hooks/useFetch'; +import { getProjectInfoAPICreator } from 'apis/Project'; +import { SHUT_DOWN_LIVE_SHARE, LOADING_LIVE } from 'constants/notificationMessage'; +import { getCookie } from 'utils/controlCookie'; + +const DEFAULT_CLICKED_TAB_INDEX = 0; +let socket; + +function Live() { + const history = useHistory(); + const { projectId } = useParams(); + const { user } = useContext(UserContext); + const { liveServer, dispatchLive } = useContext(LiveContext); + const [{ data, loading, error }, setRequest] = useFetch({}); + const [isFetched, setIsFetched] = useState(false); + const [isConnected, setIsConnected] = useState(false); + const [clickedTabIndex, setClickedTabIndex] = useState( + DEFAULT_CLICKED_TAB_INDEX + ); + const [project, dispatchProject] = useReducer(ProjectReducer, {}); + + if (!getCookie('jwt')) { + localStorage.setItem('redirectURL', window.location.href); + history.replace('../signin'); + } + + const handleFetchProject = () => { + const getProjectInfoAPI = getProjectInfoAPICreator(projectId); + setRequest(getProjectInfoAPI); + }; + + const handleSetProjectState = project => { + const fetchProjectAction = fetchProjectActionCreator({ project }); + dispatchProject(fetchProjectAction); + setIsFetched(true); + }; + + const handleSetProject = () => { + if (!data) return; + if (!isFetched) handleSetProjectState(data); + }; + + const handleConnected = () => { + socket.emit('createRoom', { user, projectId, project }); + }; + + const handleAlreadyExistRoom = ({ host, project, participants }) => { + dispatchLive( + liveOnActionCreator({ + socket, + url: `${COCODE_SERVER}/live/${projectId}`, + owner: host, + project, + participants + }) + ); + }; + + const handleSuccessCreatedRoom = ({ project, participants }) => { + dispatchLive( + liveOnActionCreator({ + socket, + url: `${COCODE_SERVER}/live/${projectId}`, + owner: user, + project, + participants + }) + ); + }; + + const handleJoinUser = ({ participants }) => { + dispatchLive( + liveJoinUserActionCreator({ + participants + }) + ); + }; + + const handleLeaveUser = ({ participants }) => { + dispatchLive( + liveLeaveUserActionCreator({ + participants + }) + ); + }; + + const handleCloseSocket = () => { + socket.close(); + dispatchLive(liveOffActionCreator()); + addToast.error(SHUT_DOWN_LIVE_SHARE); + }; + + const handleConnectSocket = useCallback(() => { + if (!Object.keys(project).length || isConnected || !user) return; + setIsConnected(true); + socket = io(liveServer); + socket.on('connected', handleConnected); + socket.on('alreadyExistRoom', handleAlreadyExistRoom); + socket.on('successCreatedRoom', handleSuccessCreatedRoom); + socket.on('joinUser', handleJoinUser); + socket.on('leaveUser', handleLeaveUser); + socket.on('close', handleCloseSocket); + }, [project]); + + useEffect(handleFetchProject, []); + useEffect(handleConnectSocket, [project]); + useEffect(handleSetProject, [data, isFetched]); + + if (loading) return ; + if (error) history.push('/weAreSorry'); + + return ( + +
+ {isFetched && ( + + + + + + + + + + + )} + + ); +} + +export default Live; diff --git a/cocode/src/pages/Version1/style.js b/cocode/src/pages/Live/style.js similarity index 60% rename from cocode/src/pages/Version1/style.js rename to cocode/src/pages/Live/style.js index 7689cf6c..2a84f910 100644 --- a/cocode/src/pages/Version1/style.js +++ b/cocode/src/pages/Live/style.js @@ -5,11 +5,11 @@ const Main = styled.main` display: flex; flex-direction: row; - height: ${({ theme }) => theme.exceptHeaderHeight}; - } + height: 91vh; - .Stretch-item { - width: 100%; + .Project-main-stretch { + flex-grow: 2; + } } `; diff --git a/cocode/src/pages/Project/index.js b/cocode/src/pages/Project/index.js index a7c3fcd6..ce1ea936 100644 --- a/cocode/src/pages/Project/index.js +++ b/cocode/src/pages/Project/index.js @@ -6,46 +6,67 @@ import Header from 'containers/Common/Header'; import TabBar from 'containers/Project/TabBar'; import TabContainer from 'containers/Project/TabContainer'; import Editor from 'containers/Project/Editor'; +import LoadingSpinner from 'containers/Common/LoadingSpinner'; import BrowserV2 from 'components/Project/BrowserV2'; import { SplitPaneContainer } from 'components/Common/SplitPane'; import addToast from 'components/Common/Toast'; -import ProjectReducer from 'reducers/ProjectReducer'; -import ProjectContext from 'contexts/ProjectContext'; +import { ProjectReducer } from 'reducers'; +import { ProjectContext, UserContext } from 'contexts'; import { fetchProjectActionCreator } from 'actions/Project'; import { TAB_BAR_THEME } from 'constants/theme'; -import UserContext from 'contexts/UserContext'; import useFetch from 'hooks/useFetch'; import { reactTemplate } from 'template/react'; import copyProject from 'template/copyProject'; import { getProjectInfoAPICreator, forkProjectAPICreator } from 'apis/Project'; -import { LiveStore } from 'stores'; import parseProject from 'pages/Project/parseProject'; import { CREATED, CONFLICT } from 'constants/statusCode'; +import { + SUCCESS_FORK, + CONFLICT_FORK, + LOADING_PROJECT +} from 'constants/notificationMessage'; const DEFAULT_CLICKED_TAB_INDEX = 0; function Project() { const { user } = useContext(UserContext); - const history = useHistory(); const { projectId } = useParams(); + const history = useHistory(); const [{ data, loading, error, status }, setRequest] = useFetch({}); + const [isLive, setIsLive] = useState(false); const [isFetched, setIsFetched] = useState(false); const [clickedTabIndex, setClickedTabIndex] = useState( DEFAULT_CLICKED_TAB_INDEX ); const [project, dispatchProject] = useReducer(ProjectReducer, {}); + const isNotMyProject = !user || user.username !== project.author; - const handleForkCoconut = () => { - const username = user ? user.username : 'anonymous'; - const parsedProject = parseProject(project, username); + const forkCoconut = ({ live, info }) => { + if (!isNotMyProject) return false; + + const parsedProject = preTreatBeforeFork({ live, info }); const forkProjectInfoAPI = forkProjectAPICreator(parsedProject); setRequest(forkProjectInfoAPI); handleSetProjectState(parsedProject); - return project; + return parsedProject._id; + }; + + const preTreatBeforeFork = ({ live, info }) => { + if (live) setIsLive(true); + + const username = user ? user.username : 'anonymous'; + let parsedProject = parseProject(project, username); + + if (info) { + Object.entries(info).forEach(([title, value]) => { + parsedProject[title] = value; + }); + } + return parsedProject; }; const handleFetchProject = () => { @@ -66,17 +87,13 @@ function Project() { }; const handleChangeHistoryAtForked = () => { - if (status === CONFLICT) { - addToast.error('already forked! enjoy Coconut '); - } - + if (status === CONFLICT) addToast.error(CONFLICT_FORK); if (status !== CREATED) return; - projectId !== 'new' - ? history.push(`../project/${data._id}`) - : history.replace(`../project/${data._id}`); + const url = isLive ? `../live/${data._id}` : `../project/${data._id}`; + projectId !== 'new' ? history.push(url) : history.replace(url); - addToast.info('Forked Coconut, Success !'); + addToast.info(SUCCESS_FORK); }; const handleSetProject = () => { @@ -91,9 +108,8 @@ function Project() { useEffect(handleChangeHistoryAtForked, [status]); - // //TODO loading 컴포넌트 만들기 - if (loading) return

Loading...

; - if (error) return

다시 시도해주세요.

; + if (loading) return ; + if (error) history.push('/weAreSorry'); return ( - -
- {isFetched && ( - - - - - - - - +
+ {isFetched && ( + + + + + + + - - )} - + + + )} ); } diff --git a/cocode/src/pages/Project/style.js b/cocode/src/pages/Project/style.js index f290ce40..2a84f910 100644 --- a/cocode/src/pages/Project/style.js +++ b/cocode/src/pages/Project/style.js @@ -5,7 +5,7 @@ const Main = styled.main` display: flex; flex-direction: row; - height: 88vh; + height: 91vh; .Project-main-stretch { flex-grow: 2; diff --git a/cocode/src/pages/SignIn/index.js b/cocode/src/pages/SignIn/index.js new file mode 100644 index 00000000..cc65d251 --- /dev/null +++ b/cocode/src/pages/SignIn/index.js @@ -0,0 +1,21 @@ +import React, { useContext } from 'react'; +import { useHistory } from 'react-router-dom'; +import Header from 'containers/Common/Header'; +import SignInContainer from 'containers/SignIn'; +import { UserContext } from 'contexts'; + +function SignIn() { + const { user } = useContext(UserContext); + const history = useHistory(); + + if (user) history.replace('../'); + + return ( + <> +
+ + + ); +} + +export default SignIn; diff --git a/cocode/src/pages/Version1/index.js b/cocode/src/pages/Version1/index.js deleted file mode 100644 index 852be306..00000000 --- a/cocode/src/pages/Version1/index.js +++ /dev/null @@ -1,52 +0,0 @@ -import React, { useReducer, useEffect } from 'react'; -import * as Styled from './style'; - -import MonacoEditor from 'components/Project/MonacoEditor'; -import BrowserV1 from 'components/Project/BrowserV1'; -import { SplitPaneContainer } from 'components/Common/SplitPane'; - -import ProjectReducer from 'reducers/ProjectReducer'; - -import reactTemplate from 'template/copyProject'; - -import { - fetchProjectActionCreator, - updateCodeActionCreator -} from 'actions/Project'; - -function Version1() { - const [project, dispatchProject] = useReducer(ProjectReducer, {}); - - const handleFetchProject = () => { - const fetchProjectAction = fetchProjectActionCreator({ - project: reactTemplate() - }); - dispatchProject(fetchProjectAction); - }; - - const handleChangeCode = (_, changedCode) => { - const updateCodeAction = updateCodeActionCreator(changedCode); - dispatchProject(updateCodeAction); - }; - - useEffect(handleFetchProject, []); - - return ( - - - - - - - ); -} - -export default Version1; diff --git a/cocode/src/pages/index.js b/cocode/src/pages/index.js index a9f7f95e..ea53e9b0 100644 --- a/cocode/src/pages/index.js +++ b/cocode/src/pages/index.js @@ -1,8 +1,9 @@ import Home from './Home'; import DashBoard from './DashBoard'; import Project from './Project'; -import History from './History'; -import Version1 from './Version1'; import NotFound from './NotFound'; +import Live from './Live'; +import SignIn from './SignIn'; +import Empty from './Empty'; -export { Home, DashBoard, Project, History, Version1, NotFound }; +export { Home, DashBoard, Project, NotFound, Live, SignIn, Empty }; diff --git a/cocode/src/reducers/DashboardReducer.js b/cocode/src/reducers/DashBoardReducer.js similarity index 100% rename from cocode/src/reducers/DashboardReducer.js rename to cocode/src/reducers/DashBoardReducer.js diff --git a/cocode/src/reducers/LiveReducer.js b/cocode/src/reducers/LiveReducer.js index dbc21e3a..0509c9ab 100644 --- a/cocode/src/reducers/LiveReducer.js +++ b/cocode/src/reducers/LiveReducer.js @@ -1,49 +1,44 @@ import { - FETCH_LIVE, LIVE_ON, LIVE_OFF, LIVE_JOIN_USER, - LIVE_LEFT_USER + LIVE_LEAVE_USER } from 'actions/types'; -const fetchLive = (state, { url, participants, owner }) => ({ +const liveOn = (state, { url, socket, project, owner }) => { + return ({ + ...state, + url, + socket, + project, + owner, + participants: [] + }); +}; + +const liveOff = (state) => ({ ...state, - url, - participants, - owner -}); - -const liveOn = (state, { owner }) => ({ - ...state, - owner, - participants: [] -}); - -const liveOff = () => ({ - url: null, + socket: null, owner: undefined, participants: [] }); -const joinUser = (state, { joinUser }) => ({ +const joinUser = (state, { participants }) => ({ ...state, - participants: [...state.participants, joinUser] + participants, }); -const leftUser = (state, { leftUser }) => ({ +const leaveUser = (state, { participants }) => ({ ...state, - participants: state.participants.filter( - ({ username }) => username !== leftUser.username - ) + participants, }); function LiveReducer(state, { type, payload }) { const reducers = { - [FETCH_LIVE]: fetchLive, [LIVE_ON]: liveOn, [LIVE_OFF]: liveOff, [LIVE_JOIN_USER]: joinUser, - [LIVE_LEFT_USER]: leftUser + [LIVE_LEAVE_USER]: leaveUser }; const reducer = reducers[type]; diff --git a/cocode/src/reducers/ProjectReducer.js b/cocode/src/reducers/ProjectReducer.js index d36d9d81..dea3c481 100644 --- a/cocode/src/reducers/ProjectReducer.js +++ b/cocode/src/reducers/ProjectReducer.js @@ -2,6 +2,7 @@ import { UPDATE_PROJECT_INFO, UPDATE_CODE, + UPDATE_CODE_FROM_FILE_ID, FETCH_PROJECT, SELECT_FILE, CREATE_FILE, @@ -109,6 +110,20 @@ const updateCode = (state, { changedCode }) => { }; }; +const updateCodeFromFileId = (state, { fileId, changedCode }) => { + return { + ...state, + files: { + ...state.files, + [fileId]: { + ...state.files[fileId], + contents: changedCode, + isEditing: true + } + } + }; +}; + // Select file const selectFile = (state, { selectedFileId }) => { return { @@ -314,6 +329,7 @@ function ProjectReducer(state, { type, payload }) { [UPDATE_PROJECT_INFO]: updateProjectInfo, [FETCH_PROJECT]: fetchProject, [UPDATE_CODE]: updateCode, + [UPDATE_CODE_FROM_FILE_ID]: updateCodeFromFileId, [SELECT_FILE]: selectFile, [UPDATE_FILE_NAME]: updateFileName, [CREATE_FILE]: createFile, diff --git a/cocode/src/reducers/index.js b/cocode/src/reducers/index.js index e69de29b..e334352b 100644 --- a/cocode/src/reducers/index.js +++ b/cocode/src/reducers/index.js @@ -0,0 +1,6 @@ +import APIReducer from './APIReducer'; +import DashBoardReducer from './DashBoardReducer'; +import LiveReducer from './LiveReducer'; +import ProjectReducer from './ProjectReducer'; + +export { APIReducer, DashBoardReducer, LiveReducer, ProjectReducer }; \ No newline at end of file diff --git a/cocode/src/stores/LiveStore.js b/cocode/src/stores/LiveStore.js index 7aeba0e4..d894ebda 100644 --- a/cocode/src/stores/LiveStore.js +++ b/cocode/src/stores/LiveStore.js @@ -1,20 +1,31 @@ import React, { useReducer } from 'react'; -import LiveReducer from 'reducers/LiveReducer'; +import { LiveReducer } from 'reducers'; import { LiveContext } from 'contexts'; +import { LIVE_SERVER } from 'config'; function LiveStore({ children }) { - const [{ url, participants, owner }, dispatchLive] = useReducer( - LiveReducer, - { - url: null, - owner: undefined, - participants: [] - } - ); + const initialValue = { + liveServer: LIVE_SERVER, + url: '', + project: {}, + socket: null, + owner: undefined, + participants: [] + }; + + const [live, dispatchLive] = useReducer(LiveReducer, initialValue); + const { liveServer, url, socket, participants, owner } = live; return ( {children} diff --git a/cocode/src/utils/controlCookie.js b/cocode/src/utils/controlCookie.js new file mode 100644 index 00000000..a380f642 --- /dev/null +++ b/cocode/src/utils/controlCookie.js @@ -0,0 +1,13 @@ +import { DELETE_COOKIE_VALUE } from 'constants/cookie'; + +function deleteCookie(key) { + document.cookie = key + DELETE_COOKIE_VALUE; +} + +//참고: https://cofs.tistory.com/363 +function getCookie(name) { + const value = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)'); + return value ? value[2] : null; +} + +export { deleteCookie, getCookie }; diff --git a/cocode/src/utils/deleteCookie.js b/cocode/src/utils/deleteCookie.js deleted file mode 100644 index ac26e1d5..00000000 --- a/cocode/src/utils/deleteCookie.js +++ /dev/null @@ -1,8 +0,0 @@ -import { DELETE_COOKIE_VALUE } from 'constants/cookie'; - -function deleteCookie(key) { - document.cookie = key + DELETE_COOKIE_VALUE; - window.location.reload(); -}; - -export default deleteCookie; \ No newline at end of file diff --git a/cocode/src/utils/domControl.js b/cocode/src/utils/domControl.js index 13b6378e..e717734f 100644 --- a/cocode/src/utils/domControl.js +++ b/cocode/src/utils/domControl.js @@ -7,4 +7,12 @@ function changeDivEditable(node, status) { if (status) node.focus(); } -export { selectAllTextAboutFocusedDom, changeDivEditable }; +function copyToClipboard(node) { + const range = document.createRange(); + range.selectNode(node); + window.getSelection().removeAllRanges(); + window.getSelection().addRange(range); + document.execCommand('copy'); +} + +export { selectAllTextAboutFocusedDom, changeDivEditable, copyToClipboard }; diff --git a/cocode/src/utils/monacoWidget.js b/cocode/src/utils/monacoWidget.js new file mode 100644 index 00000000..83bbe24a --- /dev/null +++ b/cocode/src/utils/monacoWidget.js @@ -0,0 +1,46 @@ +class CursorWidget { + constructor(editor, userName, position) { + this.editor = editor; + this.id = userName; + this.domNode = null; + this.position = position; + } + + getId() { + return this.id; + } + + getDomNode() { + if (!this.domNode) { + this.domNode = document.createElement('div'); + this.domNode.innerHTML = this.id; + this.domNode.style.background = 'grey'; + this.domNode.id = this.id; + } + return this.domNode; + } + + getPosition() { + return { + position: this.position, + preference: [0] + }; + } + updatePosition(position) { + this.position = position; + this.editor.layoutContentWidget(this); + } + + showCursor(position) { + this.domNode.style.visibility = 'inherit'; + this.position = position; + this.editor.layoutContentWidget(this); + } + + hiddenCursor() { + this.domNode.style.visibility = 'hidden'; + this.editor.layoutContentWidget(this); + } +} + +export { CursorWidget }; diff --git a/cocode/yarn.lock b/cocode/yarn.lock index bebc62d7..24d05277 100644 --- a/cocode/yarn.lock +++ b/cocode/yarn.lock @@ -2505,6 +2505,11 @@ adjust-sourcemap-loader@2.0.0: object-path "0.11.4" regex-parser "2.2.10" +after@0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" + integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= + aggregate-error@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" @@ -2792,6 +2797,11 @@ array.prototype.flatmap@^1.2.1: es-abstract "^1.15.0" function-bind "^1.1.1" +arraybuffer.slice@~0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" + integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== + arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -3358,11 +3368,21 @@ babylon@^6.18.0: resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== +backo2@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base64-arraybuffer@0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" + integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= + base64-js@^1.0.2: version "1.3.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" @@ -3398,6 +3418,13 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +better-assert@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" + integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI= + dependencies: + callsite "1.0.0" + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -3408,6 +3435,11 @@ binary-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== +blob@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" + integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== + bluebird@^3.3.5, bluebird@^3.5.5: version "3.7.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.1.tgz#df70e302b471d7473489acf26a93d63b53f874de" @@ -3710,6 +3742,11 @@ caller-path@^2.0.0: dependencies: caller-callsite "^2.0.0" +callsite@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" + integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= + callsites@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" @@ -4119,11 +4156,26 @@ compare-func@^1.3.1: array-ify "^1.0.0" dot-prop "^3.0.0" +component-bind@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" + integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= + +component-emitter@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== +component-inherit@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" + integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= + compose-function@3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/compose-function/-/compose-function-3.0.3.tgz#9ed675f13cc54501d30950a486ff6a7ba3ab185f" @@ -4782,7 +4834,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6. dependencies: ms "2.0.0" -debug@=3.1.0: +debug@=3.1.0, debug@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== @@ -4796,7 +4848,7 @@ debug@^3.0.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: dependencies: ms "^2.1.1" -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@~4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -5322,6 +5374,34 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" +engine.io-client@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.0.tgz#82a642b42862a9b3f7a188f41776b2deab643700" + integrity sha512-a4J5QO2k99CM2a0b12IznnyQndoEvtA4UAldhGzKqnHf42I3Qs2W5SPnDvatZRcMaNZs4IevVicBPayxYt6FwA== + dependencies: + component-emitter "1.2.1" + component-inherit "0.0.3" + debug "~4.1.0" + engine.io-parser "~2.2.0" + has-cors "1.1.0" + indexof "0.0.1" + parseqs "0.0.5" + parseuri "0.0.5" + ws "~6.1.0" + xmlhttprequest-ssl "~1.5.4" + yeast "0.1.2" + +engine.io-parser@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed" + integrity sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w== + dependencies: + after "0.8.2" + arraybuffer.slice "~0.0.7" + base64-arraybuffer "0.1.5" + blob "0.0.5" + has-binary2 "~1.0.2" + enhanced-resolve@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" @@ -6680,6 +6760,18 @@ has-ansi@^2.0.0: dependencies: ansi-regex "^2.0.0" +has-binary2@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" + integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== + dependencies: + isarray "2.0.1" + +has-cors@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" + integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -7144,6 +7236,11 @@ indexes-of@^1.0.1: resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= + infer-owner@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" @@ -7684,6 +7781,11 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +isarray@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" + integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -9570,6 +9672,11 @@ object-assign@4.1.1, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= +object-component@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" + integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE= + object-copy@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" @@ -9983,6 +10090,20 @@ parse5@5.1.0: resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== +parseqs@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" + integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0= + dependencies: + better-assert "~1.0.0" + +parseuri@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" + integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo= + dependencies: + better-assert "~1.0.0" + parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -12628,6 +12749,35 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +socket.io-client@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4" + integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA== + dependencies: + backo2 "1.0.2" + base64-arraybuffer "0.1.5" + component-bind "1.0.0" + component-emitter "1.2.1" + debug "~4.1.0" + engine.io-client "~3.4.0" + has-binary2 "~1.0.2" + has-cors "1.1.0" + indexof "0.0.1" + object-component "0.0.3" + parseqs "0.0.5" + parseuri "0.0.5" + socket.io-parser "~3.3.0" + to-array "0.1.4" + +socket.io-parser@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f" + integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng== + dependencies: + component-emitter "1.2.1" + debug "~3.1.0" + isarray "2.0.1" + sockjs-client@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177" @@ -13353,6 +13503,11 @@ tmpl@1.0.x: resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= +to-array@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" + integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= + to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" @@ -14357,6 +14512,13 @@ ws@^6.1.2, ws@^6.2.1: dependencies: async-limiter "~1.0.0" +ws@~6.1.0: + version "6.1.4" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" + integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA== + dependencies: + async-limiter "~1.0.0" + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" @@ -14367,6 +14529,11 @@ xmlchars@^2.1.1: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmlhttprequest-ssl@~1.5.4: + version "1.5.5" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" + integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= + xregexp@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020" @@ -14490,3 +14657,8 @@ yargs@^13.3.0: which-module "^2.0.0" y18n "^4.0.0" yargs-parser "^13.1.1" + +yeast@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" + integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= diff --git a/env.tar.enc b/env.tar.enc new file mode 100644 index 00000000..941672d8 Binary files /dev/null and b/env.tar.enc differ