diff --git a/.vscode/settings.json b/.vscode/settings.json index 68e9c50..9c66003 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,11 +4,14 @@ "editor.snippetSuggestions": "top", "editor.rulers": [80, 120], "editor.formatOnSave": false, - "[javascript]": { - "editor.formatOnSave": true - }, - "[javascriptreact]": { - "editor.formatOnSave": true + "[javascript]": { + "editor.formatOnSave": true + }, + "[scss]": { + "editor.formatOnSave": true + }, + "[javascriptreact]": { + "editor.formatOnSave": true }, "terminal.integrated.fontFamily": "Consolas", "workbench.colorTheme": "Dracula Soft", diff --git a/package.json b/package.json index 20af087..bc01aa1 100644 --- a/package.json +++ b/package.json @@ -9,14 +9,21 @@ "gh-pages": "^3.1.0", "js-sha256": "^0.9.0", "mdbreact": "mdbreact-4.26.1.tgz", + "mdb-react-sortable": "mdb-react-sortable-4.26.0.tgz", "node-sass": "^4.12.0", "obelisk.js": "^1.2.2", + "react-avatar-editor": "^11.0.9", "react-chartjs-2": "^2.9.0", "react-dropzone": "^11.0.1", + "react-redux": "^7.2.0", + "react-redux-loading-bar": "^4.6.0", "react-router-dom": "^5.0.1", "react-text-loop": "^2.3.0", "react-typed": "^1.2.0", "reactjs-oauth": "file:reactjs-oauth-0.1.0.tgz", + "redux": "^4.0.5", + "redux-thunk": "^2.3.0", + "serialize-error": "^7.0.1", "snek-intel": "^0.1.1", "tippy.js": "^6.2.3" }, diff --git a/src/App.js b/src/App.js index 9137495..d0bb353 100644 --- a/src/App.js +++ b/src/App.js @@ -1,16 +1,17 @@ //#region > Imports //> React // Contains all the functionality necessary to define React components -import React from "react"; +import React, { useEffect } from "react"; // DOM bindings for React Router import { withRouter } from "react-router-dom"; -//> Additional -// SHA Hashing algorithm -import sha256 from "js-sha256"; -//> MDB -// "Material Design for Bootstrap" is a great UI design framework -import { MDBProgress } from "mdbreact"; +//> Redux +// Allows to React components read data from a Redux store, and dispatch actions +// to the store to update data. +import { useDispatch } from "react-redux"; +//> Actions +// Functions to send data from the application to the store +import { loginAction } from "./store/actions/authActions"; //> Components /** * Footer: Global Footer @@ -21,287 +22,44 @@ import { Footer, Navbar } from "./components/molecules"; import { ScrollToTop } from "./components/atoms"; //> Routes import Routes from "./Routes"; -//> Core -import { ferryIntel } from "./actions"; -//> Actions -import { - login, - logout, - fetchGitLabServers, - appendSourceObjects, - getAllPageUrls, - getData, - saveSettings, - register, - readCache, - updateCache, - writeCache, - getAllTalks, - getTalk, - uploadTalk, - deleteTalk, -} from "./actions/intel"; +import LoadingBarContainer from "react-redux-loading-bar"; //#endregion //#region > Components /** * @class Root component which loads all other components */ -class App extends React.Component { - state = { - loggedUser: undefined, - fetchedUser: undefined, - loading: true, - caching: false, - }; - - globalFunctions = { - //> Authentication - login: async (username, password) => - this.handleLoginSession({ username, password: sha256(password) }), - logout: async () => this.handleLogout(), - //> General - fetchGitLabServers: async () => ferryIntel(fetchGitLabServers()), - appendSourceObjects: async (sourceList) => - ferryIntel(appendSourceObjects(sourceList)), - users: async () => ferryIntel(getAllPageUrls()), - saveSettings: async (nextSettings) => this.handleSaveSettings(nextSettings), - //> User - updateCache: async (fetchedUser) => this.handleCacheRenewal(fetchedUser), - writeCache: async (platformData) => ferryIntel(writeCache(platformData)), - registerUser: async (registrationData) => - this.handleRegistration(registrationData), - fetchCacheData: async (username) => this.handleProfileFetching(username), - //> Talk - deleteTalk: async (talk) => this.handleTalkDeletion(talk), - uploadTalk: async (file, talkInfo) => this.handleTalkUpload(file, talkInfo), - getTalk: (uid, username) => ferryIntel(getTalk(uid, username)), - //> State checking - refetchRequired: (username) => this.refetchRequired(username), - usernameMatchesFetchedUsername: (username) => - this.usernameMatchesFetchedUsername(username), - }; - - componentDidMount = () => { - // Start a session as anonymous user - this.handleLoginSession(); - }; - - /** - * Handle login session. - * - * @param user A user to login with - * @description Handles states for login - */ - handleLoginSession = async (user) => { - return ferryIntel(login(user)).then((loggedUser) => { - if (loggedUser) { - this.setState({ - loggedUser, - loading: false, - }); - } else { - if (this.state.loggedUser !== null) { - this.setState({ - loggedUser: null, - loading: false, - }); - } - } - }); - }; - - /** - * Handle logout. - * - * @description Handles states for logout - */ - handleLogout = () => { - this.setState( - { - loggedUser: undefined, - fetchedUser: undefined, - loading: false, - caching: false, - }, - () => ferryIntel(logout()).then(() => this.handleLoginSession()) - ); - }; - - /** - * Handle registration - * - * @param registrationData Data to register a user - * @description Handles states for registration - */ - handleRegistration = (registrationData) => { - ferryIntel(register(registrationData)).then((res) => { - this.globalFunctions.login(res.username, res.password).then(() => { - this.globalFunctions.writeCache(registrationData.platform_data); - this.setState({ caching: true, loading: false }); - }); - }); - }; - - /** - * Handle cache renewal. - * - * @param fetchedUser A fetched user object - * @description Handles states for cache renewal - */ - handleCacheRenewal = async (fetchedUser) => { - if ( - !this.state.caching && - this.state.loggedUser?.username === fetchedUser?.username - ) { - // Renew cache - const fetchedUser = await ferryIntel(updateCache(fetchedUser)); - - this.setState({ - fetchedUser, - caching: true, - }); - } - }; - - /** - * Handle profile fetching. - * - * @param username A username to read the cache from - * @description Handles states for profile fetching - */ - handleProfileFetching = async (username) => { - const fetchedUser = await ferryIntel(readCache(username)); - // Update visible data - this.setState({ - fetchedUser: fetchedUser ? fetchedUser : false, - loading: false, - }); - }; - - /** - * Handle talk upload. - * - * @param file A file to be uploaded - * @param talkInfo Additional information to add to the talk - * @description Handles states for talk uploading - */ - handleTalkUpload = async (file, talkInfo) => { - ferryIntel(uploadTalk(file, talkInfo), { - currentCache: this.state.fetchedUser.platformData, - }).then((platformData) => { - this.setState({ - fetchedUser: { - ...this.state.fetchedUser, - platformData, - }, - }); - }); - }; - - /** - * Handle talk deletion. - * - * @param talk A talk that should be deleted - * @description Handles states for talk deleting - */ - handleTalkDeletion = async (talk) => { - ferryIntel(deleteTalk(talk), { - currentCache: this.state.fetchedUser.platformData, - }).then((platformData) => { - this.setState({ - fetchedUser: { - ...this.state.fetchedUser, - platformData, - }, - }); - }); - }; - - /** - * Handle save settings. - * - * @param nextSettings A settings object that should be applied - * @description Handles states for saving settings - */ - handleSaveSettings = async (nextSettings) => { - ferryIntel(saveSettings(nextSettings), { - currentCache: this.state.fetchedUser.platformData, - }).then((platformData) => { - this.setState({ - fetchedUser: { - ...this.state.fetchedUser, - platformData, - }, - }); - }); - }; - - //#region > Refetch Checking - /** - * Check for refetch for a specific username. - * - * @param {string} username The username associated with a profile page - * @returns {boolean} True if a refetch is required otherwise False - */ - refetchRequired = (username) => { - const loading = this.state.loading; - const fetchedUser = this.state.fetchedUser; - - if (!loading) { - if (!fetchedUser && fetchedUser !== false) { - return true; - } else if ( - fetchedUser && - !this.usernameMatchesFetchedUsername(username) - ) { - return true; - } - return false; - } - }; - - /** - * Check if the provided username matches with the current fetched user. - * - * @param {string} username The username associated with a profile page - * @returns {boolean} True if the usernames matches otherwise False - */ - usernameMatchesFetchedUsername = (username) => { - return username === this.state.fetchedUser?.username; - }; - //#endregion - - render() { - return ( +function App() { + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(loginAction()); + }, []); + + return ( + <> +
+ +
- {!this.state.caching && - this.state.fetchedUser && - this.state.loggedUser?.username === - this.state.fetchedUser.username && ( - - )} - +
- +
- ); - } + + ); } //#endregion //#region > Exports -//> Default Class +/** + * Got access to the history object’s properties and the closest + * 's match. + */ export default withRouter(App); //#endregion diff --git a/src/Routes.js b/src/Routes.js index 2d3c66d..f4fa545 100644 --- a/src/Routes.js +++ b/src/Routes.js @@ -11,6 +11,7 @@ import { ProfilePage, CompanyPage, TalkPage, + SettingsPage, } from "./components/pages"; //#endregion @@ -18,51 +19,23 @@ import { /** @class Route component which includes all routes to specified components */ class Routes extends React.Component { render() { - const { globalState, globalFunctions } = this.props; - return ( - ( - - )} - /> + } /> ( - - )} + component={(props) => } /> ( - - )} + component={(props) => } /> ( - - )} + component={(props) => } /> - {/* Some debugging routes */} + } + /> + {/* Some debugging routes start*/} Not Found; @@ -100,6 +78,7 @@ class Routes extends React.Component { return

Third Layer

; }} /> + {/* Some debugging routes end*/}
); } diff --git a/src/actions/core.js b/src/actions/core.js deleted file mode 100644 index ea96736..0000000 --- a/src/actions/core.js +++ /dev/null @@ -1,39 +0,0 @@ -//#region > Imports -//> Intel -import { Intel } from "snek-intel"; -//#endregion - -//#region > Constant Variables -const INTEL = new Intel(); -//#endregion - -//#region > Core -/** - * The intel ferry. - * - * @param actionFn The action to use - * @param args Arguments to use for the action - * @description The intel ferry is used to deliver calls to the SNEK-Intel - */ -const ferryIntel = async (actionFn, args) => { - return actionFn(INTEL, args).then((res) => { - switch (res.errorCode) { - case undefined: - return res; - default: - //#ERROR - console.error(res.message); - return null; - } - }); -}; -//#endregion - -//#region > Exports -export { ferryIntel }; -//#endregion - -/** - * SPDX-License-Identifier: (EUPL-1.2) - * Copyright © 2019-2020 Simon Prast - */ diff --git a/src/actions/index.js b/src/actions/index.js deleted file mode 100644 index a0031ec..0000000 --- a/src/actions/index.js +++ /dev/null @@ -1,14 +0,0 @@ -//#region > Imports -//> Core -import { ferryIntel } from "./core"; -//#endregion - -//#region > Export -//> Core -export { ferryIntel }; -//#endregion - -/** - * SPDX-License-Identifier: (EUPL-1.2) - * Copyright © 2019-2020 Simon Prast - */ diff --git a/src/actions/intel/authActions.js b/src/actions/intel/authActions.js deleted file mode 100644 index 458a84d..0000000 --- a/src/actions/intel/authActions.js +++ /dev/null @@ -1,65 +0,0 @@ -//#region > Authentication -/** - * Handle login - * - * @param user A user to login with - * @description Handles states for login - */ -const login = (user) => { - return async (intel) => { - try { - const session = intel.snekclient.session; - - return await session.begin(user).then((whoami) => { - if (whoami?.username !== process.env.REACT_APP_ANONYMOUS_USER) { - return { - username: whoami.username, - avatarUrl: - "https://www.clipartmax.com/png/full/166-1669056_the-20-cooler-octocat-github-octocat.png", - }; - } else { - return false; - } - }); - } catch (ex) { - return { - errorCode: 600, - message: "Login failed", - raw: ex, - }; - } - }; -}; - -/** - * Logout user. - * - * @description Handles the logging out of active users - */ -const logout = () => { - return async (intel) => { - try { - const session = intel.snekclient.session; - - await session.end(); - - return true; - } catch (ex) { - return { - errorCode: 601, - message: "Logout failed", - raw: ex, - }; - } - }; -}; -//#endregion - -//#region > Exports -export { login, logout }; -//#endregion - -/** - * SPDX-License-Identifier: (EUPL-1.2) - * Copyright © 2019-2020 Simon Prast - */ diff --git a/src/actions/intel/generalActions.js b/src/actions/intel/generalActions.js deleted file mode 100644 index 07b0aed..0000000 --- a/src/actions/intel/generalActions.js +++ /dev/null @@ -1,175 +0,0 @@ -//#region > Registration -/** - * Append Source Objects - * - * @param sourceList A source object - * @description Hands source list over to intel - */ -const appendSourceObjects = (sourceList) => { - return async (intel) => { - try { - return intel.appendList(sourceList); - } catch (ex) { - return { - errorCode: 602, - message: "Appending source objects failed", - raw: ex, - }; - } - }; -}; - -/** - * Fetch GitLab Servers - * - * @description Retrieves a list of available GitLab servers - */ -const fetchGitLabServers = () => { - return async (intel) => { - try { - const session = intel.snekclient.session; - - return session.tasks.general.gitlabServer().then(({ data }) => { - const gitLabServers = data?.page?.supportedGitlabs; - - if (gitLabServers) { - return gitLabServers; - } else { - return false; - } - }); - } catch (ex) { - return { - errorCode: 605, - message: "Fetching GitLab server failed", - raw: ex, - }; - } - }; -}; -//#endregion - -//#region > Data Handling -/** - * Get intel data - * - * @description Retrieves data from current applied source list - */ -const getData = () => { - return async (intel) => { - try { - return intel.get(); - } catch (ex) { - return { - errorCode: 603, - message: "Getting intel data failed", - raw: ex, - }; - } - }; -}; - -/** - * Save settings - * - * @param nextSettings The settings that should be applied - * @description Saves the user settings - */ -const saveSettings = (nextSettings) => { - return async (intel, { currentCache }) => { - try { - const session = intel.snekclient.session; - - // Check for mandatory fields - if (nextSettings.email) { - currentCache.user.firstName = nextSettings.first_name - ? nextSettings.first_name - : ""; - currentCache.user.lastName = nextSettings.last_name - ? nextSettings.last_name - : ""; - currentCache.user.email = nextSettings.email - ? nextSettings.email - : currentCache.user.email; - currentCache.profile.websiteUrl = nextSettings.website - ? nextSettings.website - : ""; - currentCache.profile.location = nextSettings.location - ? nextSettings.location - : ""; - currentCache.profile.company = nextSettings.company - ? nextSettings.company - : ""; - currentCache.user.settings = { - showTopLanguages: nextSettings.showTopLanguages, - showLocalRanking: nextSettings.showLocalRanking, - show3DDiagram: nextSettings.show3DDiagram, - show2DDiagram: nextSettings.show2DDiagram, - showEmailPublic: nextSettings.showEmailPublic, - showCompanyPublic: nextSettings.showCompanyPublic, - activeTheme: nextSettings.activeTheme, - }; - } - - session.tasks.user.cache(JSON.stringify(currentCache)); - - return currentCache; - } catch (ex) { - return { - errorCode: 604, - message: "Saving settings failed", - raw: ex, - }; - } - }; -}; - -/** - * Get all users - * - * @description Retrieves a list of all users - */ -const getAllPageUrls = () => { - return async (intel) => { - try { - const session = intel.snekclient.session; - - return await session.tasks.general.allPageUrls().then((res) => { - let urls = []; - - res.data.pages && - res.data.pages.forEach((page) => { - if (page.urlPath.includes("registration/")) { - let url = page.urlPath.split("/")[2]; - - urls.push(url); - } - }); - - return urls; - }); - } catch (ex) { - return { - errorCode: 616, - message: "Getting all page urls failed", - raw: ex, - }; - } - }; -}; -//#endregion - -//#region > Exports -export { - appendSourceObjects, - getData, - saveSettings, - getAllPageUrls, - fetchGitLabServers, -}; -//#endregion - -/** - * SPDX-License-Identifier: (EUPL-1.2) - * Copyright © 2019-2020 Simon Prast - */ diff --git a/src/actions/intel/index.js b/src/actions/intel/index.js deleted file mode 100644 index 0eadb9e..0000000 --- a/src/actions/intel/index.js +++ /dev/null @@ -1,40 +0,0 @@ -//#region > Imports -//> Intel -// Import all components to export them for easy access from parent components -import { login, logout } from "./authActions"; -import { - fetchGitLabServers, - appendSourceObjects, - getAllPageUrls, - getData, - saveSettings, -} from "./generalActions"; -import { register, readCache, updateCache, writeCache } from "./userActions"; -import { getAllTalks, getTalk, uploadTalk, deleteTalk } from "./talksActions"; -//#endregion - -//#region > Exports -//> Actions -export { - login, - logout, - appendSourceObjects, - getData, - saveSettings, - getAllPageUrls, - fetchGitLabServers, - register, - readCache, - updateCache, - writeCache, - getAllTalks, - getTalk, - uploadTalk, - deleteTalk, -}; -//#endregion - -/** - * SPDX-License-Identifier: (EUPL-1.2) - * Copyright © 2019-2020 Simon Prast - */ diff --git a/src/actions/intel/talksActions.js b/src/actions/intel/talksActions.js deleted file mode 100644 index d6032bb..0000000 --- a/src/actions/intel/talksActions.js +++ /dev/null @@ -1,133 +0,0 @@ -//#region > Talks -/** - * Get all talks. - * - * @description Handles the call for getting all talks. - */ -const getAllTalks = () => { - return async (intel) => { - try { - return intel.getTalks(); - } catch (ex) { - return { - errorCode: 611, - message: "Getting intel talks failed", - raw: ex, - }; - } - }; -}; - -/** - * Get a talk. - * - * @param uid A unique id to find a talk - * @param username A username associated with the talk - * @description Handles the call for getting one specific talk - */ -const getTalk = (uid, username) => { - return async (intel) => { - try { - const session = intel.snekclient.session; - - return session.tasks.user - .profile("/registration/" + username) - .then(async ({ data }) => { - if (data.profile) { - let talks = JSON.parse(data.profile.platformData).talks; - - talks = talks.filter((talk) => { - return talk.uid === uid; - }); - - return talks[0]; - } else { - return { - errorCode: 613, - message: "Cannot get specific talk " + uid, - raw: undefined, - }; - } - }); - } catch (ex) { - return { - errorCode: 614, - message: "Getting talks failed", - raw: ex, - }; - } - }; -}; - -/** - * Upload talk. - * - * @param file A file to be uploaded - * @param talkInfo Additional information to add to the talk - * @description Handles the call for uploading a talk - */ -const uploadTalk = (file, talkInfo) => { - return async (intel, { currentCache }) => { - try { - const session = intel.snekclient.session; - - return intel.appendTalk(file).then(() => { - return intel.getTalks().then((talks) => { - talks[talks.length - 1].repository = talkInfo; - - currentCache.talks.push(talks[talks.length - 1]); - - session.tasks.user.cache(JSON.stringify(currentCache)); - - return currentCache; - }); - }); - } catch (ex) { - return { - errorCode: 612, - message: "Uploading talk failed", - raw: ex, - }; - } - }; -}; - -/** - * Delete talk. - * - * @param talk A talk that should be deleted - * @description Handles the call for deleting a talk. - */ -const deleteTalk = (talk) => { - return async (intel, { currentCache }) => { - try { - const session = intel.snekclient.session; - for (const index in currentCache.talks) { - if (talk.uid === currentCache.talks[index].uid) { - currentCache.talks.splice(index, 1); - } - } - - session.tasks.user.cache(JSON.stringify(currentCache)); - - return currentCache; - } catch (ex) { - return { - errorCode: 615, - message: "Uploading talk failed", - raw: ex, - }; - } - }; -}; -//#endregion - -//#region > Exports -//> Export > Constant Variables -export { getAllTalks, getTalk, uploadTalk, deleteTalk }; -//#endregion - -/** - * SPDX-License-Identifier: (EUPL-1.2) - * Copyright © 2019-2020 Simon Prast - */ diff --git a/src/actions/intel/userActions.js b/src/actions/intel/userActions.js deleted file mode 100644 index 3546321..0000000 --- a/src/actions/intel/userActions.js +++ /dev/null @@ -1,250 +0,0 @@ -//#region > Imports -//> Additional -// SHA Hashing algorithm -import sha256 from "js-sha256"; -//#endregion - -//#region > Register -/** - * Register user. - * - * @param registrationData Data to register a user - * @description Handles the registration of users - */ -const register = (registrationData) => { - return async (intel) => { - try { - const session = intel.snekclient.session; - const clearPassword = registrationData.password; - - // Hash password - registrationData.password = sha256(registrationData.password); - - // Append Source objects - await intel.appendList(registrationData.sources); - // Generate talks based on the previous appended list - await intel.generateTalks(registrationData.sources); - - // Get fresh platform data - const intelData = { - ...(await intel.get()), - talks: await intel.getTalks(), - }; - - // Save Object to platformData as JSON - registrationData.platform_data = JSON.stringify(intelData); - // Create JSON string out of sources for backend use - registrationData.sources = JSON.stringify(registrationData.sources); - - return await session.tasks.user - .registration(registrationData) - .then((res) => { - if (res.result === "FAIL") { - return { - errorCode: 606, - message: "Registration failed due to false registration result", - raw: undefined, - }; - } else { - return { - registrationData, - username: registrationData.username, - password: clearPassword, - }; - } - }); - } catch (ex) { - return { - errorCode: 617, - message: "Registration failed", - raw: ex, - }; - } - }; -}; -//#endregion - -//#region > Caching -/** - * Write cache. - * - * @param platformData A data object to store in cache - * @description Handles the calls for writing to cache - */ -const writeCache = (platformData) => { - return async (intel) => { - try { - const session = intel.snekclient.session; - - return session.tasks.user.cache(platformData); - } catch (ex) { - return { - errorCode: 607, - message: "Writing to cache failed", - raw: ex, - }; - } - }; -}; - -/** - * Read cache. - * - * @param username A username to read the cache from - * @description Handles the calls for reading the cache - */ -const readCache = (username) => { - return async (intel) => { - try { - const session = intel.snekclient.session; - - return session.tasks.user - .profile("/registration/" + username) - .then(async ({ data }) => { - if (!data.profile) { - return { - errorCode: 608, - message: "Cache not loaded", - raw: undefined, - }; - } else { - // Split profile to chunks - const profile = data.profile; - const sources = profile.sources - ? JSON.parse(profile.sources) - : null; - - let platformData = profile.platformData - ? JSON.parse(profile.platformData) - : {}; - - let user = platformData.user ? platformData.user : {}; - - // Check if data is valid - if (!sources) { - return { - errorCode: 609, - message: "Sources are empty", - raw: undefined, - }; - } else { - // Set settings for first time fetching - if (Object.keys(user).length === 0) { - user.firstName = profile.firstName; - user.lastName = profile.lastName; - user.email = profile.email; - } - - if (!user.settings) { - user.settings = { - show3DDiagram: true, - show2DDiagram: true, - showCompanyPublic: true, - showEmailPublic: true, - showLocalRanking: true, - activeTheme: null, - }; - } - - // Build fetchedUser object - let fetchedUser = { - username: profile.username, - platformData: { - ...platformData, - user, - }, - sources, - verified: data.profile.verified, - accessories: { - badges: data.profile.bids - ? JSON.parse(data.profile.bids) - : null, - themes: data.profile.tids - ? JSON.parse(data.profile.tids) - : null, - }, - }; - - return fetchedUser; - } - } - }); - } catch (ex) { - return { - errorCode: 610, - message: "Reading from cache failed", - raw: ex, - }; - } - }; -}; - -/** - * Update cache. - * - * @param fetchedUser A fetched user object - * @description Handles the calls for updating the cache - */ -const updateCache = (fetchedUser) => { - return async (intel) => { - try { - const session = intel.snekclient.session; - - // Appned Source objects - await intel.appendList(fetchedUser.sources); - // Generate talks based on the previous appended list - await intel.generateTalks(fetchedUser.sources); - - // Get fresh platform data - const intelData = { - ...(await intel.get()), - talks: await intel.getTalks(), - }; - - // Fix duplicates - for (const i in intelData.talks) { - let state = true; - - for (const i2 in fetchedUser.platformData.talks) { - if ( - intelData.talks[i].url === fetchedUser.platformData.talks[i2].url - ) { - state = false; - } - } - - if (state) { - fetchedUser.platformData.talks.push(intelData.talks[i]); - } - } - - fetchedUser.platformData = { - ...intelData, - user: fetchedUser.platformData.user, - talks: fetchedUser.platformData.talks, - }; - intel.resetReducer(); - - session.tasks.user.cache(JSON.stringify(fetchedUser.platformData)); - - return fetchedUser; - } catch (ex) { - return { - errorCode: 618, - message: "Updating cache failed", - raw: ex, - }; - } - }; -}; -//#endregion - -//#region > Exports -//> Default Constant Variable -export { register, writeCache, readCache, updateCache }; -//#endregion - -/** - * SPDX-License-Identifier: (EUPL-1.2) - * Copyright © 2019-2020 Simon Prast - */ diff --git a/src/components/atoms/Pinned/index.jsx b/src/components/atoms/Pinned/index.jsx index cd32f6a..e73a48e 100644 --- a/src/components/atoms/Pinned/index.jsx +++ b/src/components/atoms/Pinned/index.jsx @@ -63,7 +63,6 @@ Pinned.propTypes = { //#endregion //#region > Exports -//> Default Class export default Pinned; //#endregion diff --git a/src/components/atoms/Pinned/pinned.scss b/src/components/atoms/Pinned/pinned.scss index 5df5704..b819fa5 100644 --- a/src/components/atoms/Pinned/pinned.scss +++ b/src/components/atoms/Pinned/pinned.scss @@ -1,7 +1,19 @@ .pinned-item { .pinned-header { width: 100%; - padding: 0.2rem; + padding: 0.1rem; + + .card { + border: 1px #e1e4e8 solid; + margin-bottom: 2rem; + overflow: hidden; + border-radius: 0.3rem; + } + + .card-body { + border: none !important; + padding: 0.3rem; + } } } diff --git a/src/components/atoms/Project/index.jsx b/src/components/atoms/Project/index.jsx index 59a6fb9..4fbe2a4 100644 --- a/src/components/atoms/Project/index.jsx +++ b/src/components/atoms/Project/index.jsx @@ -7,6 +7,9 @@ import PropTypes from "prop-types"; //> MDB // "Material Design for Bootstrap" is a great UI design framework import { MDBCol, MDBIcon } from "mdbreact"; + +//> Components +import { LanguageChart } from "../../atoms"; //#endregion //#region > Components @@ -20,30 +23,40 @@ class Project extends React.Component {
  • -

    - {repo.name.length > 25 - ? repo.name.substring(0, 25) + "..." - : repo.name} -

    +

    {repo.name.split("/")[1]}

    +
    +
    + + + @ + {repo.owner.username ? ( + repo.owner.username + ) : ( + unknown + )} + +
    +
    {repo.languages.length > 0 && ( - + + )} + {repo.languages.length > 0 && ( + - {repo.languages[0].name} + {repo.languages[0].name ? repo.languages[0].name : "Unknown"} )}
    -
    -
    - {repo.name} - Owned by {repo.owner.username} -
    -
    - -
  • @@ -59,7 +72,6 @@ Project.propTypes = { //#endregion //#region > Exports -//> Default Class export default Project; //#endregion diff --git a/src/components/atoms/ScrollToTop/index.jsx b/src/components/atoms/ScrollToTop/index.jsx index 275ef97..3d3710f 100644 --- a/src/components/atoms/ScrollToTop/index.jsx +++ b/src/components/atoms/ScrollToTop/index.jsx @@ -29,7 +29,8 @@ class ScrollToTop extends React.Component { //#endregion //#region > Exports -//> Default Class +// Got access to the history object’s properties and the closest +// 's match. export default withRouter(ScrollToTop); //#endregion diff --git a/src/components/atoms/SearchBar/index.jsx b/src/components/atoms/SearchBar/index.jsx index 6c0f473..531de91 100644 --- a/src/components/atoms/SearchBar/index.jsx +++ b/src/components/atoms/SearchBar/index.jsx @@ -4,8 +4,6 @@ import React from "react"; // DOM bindings for React Router import { withRouter } from "react-router-dom"; -// React PropTypes -import PropTypes from "prop-types"; //> MDB // "Material Design for Bootstrap" is a great UI design framework import { @@ -17,6 +15,14 @@ import { //> Fuzzysort // Fast SublimeText-like fuzzy search for JavaScript import * as fuzzysort from "fuzzysort"; +//> Redux +// Allows to React components read data from a Redux store, and dispatch actions +// to the store to update data. +import { connect } from "react-redux"; + +//> Actions +// Functions to send data from the application to the store +import { getAllPageUrlsAction } from "../../../store/actions/generalActions"; //> CSS import "./search.scss"; //#endregion @@ -27,15 +33,21 @@ import "./search.scss"; */ class SearchBar extends React.Component { state = { + loading: true, filter: "", - usernames: [], + usernames: this.props.allRegisteredUsernames, }; - componentWillReceiveProps = (nextProps) => { - if (!nextProps.globalState.loading) { - this.getUsernameList(); + componentDidMount() { + if (this.state.loading) { + this.props.allUsernames().then(() => { + this.setState({ + loading: false, + usernames: this.props.allRegisteredUsernames, + }); + }); } - }; + } handleSelection = (event, value) => { if (event === "user") { @@ -60,19 +72,7 @@ class SearchBar extends React.Component { } }; - getUsernameList = () => { - const { globalFunctions } = this.props; - - globalFunctions.users().then((usernames) => { - this.setState({ - usernames, - }); - }); - }; - render() { - const { globalState } = this.props; - //Select component does not support onChange event. Instead, you can use getValue or getTextContent methods. return ( - {this.state.usernames ? ( + {!this.state.loading && this.state.usernames ? ( this.state.usernames.length > 0 && this.state.filter.length > 0 ? ( fuzzysort .go(this.state.filter, this.state.usernames) @@ -108,13 +108,29 @@ class SearchBar extends React.Component { } //#endregion -//#region > PropTypes -SearchBar.propTypes = {}; +//#region > Redux Mapping +const mapStateToProps = (state) => ({ + allRegisteredUsernames: state.general.allRegisteredUsernames, +}); + +const mapDispatchToProps = (dispatch) => { + return { + allUsernames: () => dispatch(getAllPageUrlsAction()), + }; +}; //#endregion //#region > Exports -//> Default Class -export default withRouter(SearchBar); +/** + * Provides its connected component with the pieces of the data it needs from + * the store, and the functions it can use to dispatch actions to the store. + * + * Got access to the history object’s properties and the closest + * 's match. + */ +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(SearchBar) +); //#endregion /** diff --git a/src/components/atoms/charts/Calendar2D/index.jsx b/src/components/atoms/charts/Calendar2D/index.jsx index 27ba11a..bf8cc29 100644 --- a/src/components/atoms/charts/Calendar2D/index.jsx +++ b/src/components/atoms/charts/Calendar2D/index.jsx @@ -234,7 +234,6 @@ class Calender2D extends React.Component { //#endregion //#region > Exports -//> Default Class export default Calender2D; //#endregion diff --git a/src/components/atoms/charts/Calendar3D/calendar3d.scss b/src/components/atoms/charts/Calendar3D/calendar3d.scss index 56372e1..c50efe5 100644 --- a/src/components/atoms/charts/Calendar3D/calendar3d.scss +++ b/src/components/atoms/charts/Calendar3D/calendar3d.scss @@ -1,88 +1,73 @@ #calendar3d { position: relative; - width: 80%; - margin: auto; - display: block; + width: 100%; + padding: 0 1rem; + min-height: 350px; - .ic-stats-block { - position: absolute; + canvas { + width: 80%; + display: block; + margin: auto; } - .ic-stats-top { - top: 8px; - right: 20px; - } + .top-stats { + position: absolute; + top: 0; + right: 0; + margin: 2rem 2rem 0 0; - .ic-stats-bottom { - top: 225px; - left: 20px; - } + .stats { + padding: 0 0.5rem 0.2rem 0.5rem; + border-radius: 0.5rem; - .ic-stats-table { - display: table; - } + p { + margin-bottom: 0; - .ic-stats-row { - display: table-row; - } + &.lead { + font-size: 1.5rem; + } - .ic-stats-label { - display: table-cell; - padding-bottom: 12px; - font-size: 14px; - color: #777; - text-align: right; - vertical-align: bottom; - } + &.text-muted { + font-size: 0.7rem; + } + } - .ic-stats-count { - display: block; - font-size: 32px; - font-weight: 600; - line-height: 1; - color: #1e6823; + .item { + padding: 0.5rem; + } + } } - .ic-stats-meta { - display: table-cell; - padding-bottom: 12px; - padding-left: 8px; - text-align: left; - line-height: 1.2; - vertical-align: bottom; - } + .bottom-stats { + position: absolute; + bottom: 0; + left: 0; + margin: 0 0 2rem 2rem; - .ic-stats-total-meta { - vertical-align: middle; - } + .stats { + padding: 0 0.5rem 0.2rem 0.5rem; + border-radius: 0.5rem; - .ic-stats-average { - font-size: 12px; - font-weight: bold; - color: #24292e; - } + p { + margin-bottom: 0; - .ic-stats-unit { - display: block; - font-size: 14px; - } + &.lead { + font-size: 1.5rem; + } - .ic-stats-date { - display: block; - color: #999; - font-size: 12px; - } + &.text-muted { + font-size: 0.7rem; + } - .ic-footer { - position: absolute; - top: 380px; - left: 20px; - font-size: 11px; - color: #999; - } + .days { + font-size: 1rem; + } + } - .ic-footer a { - color: #777; + .item { + padding: 0.5rem; + } + } } } diff --git a/src/components/atoms/charts/Calendar3D/index.jsx b/src/components/atoms/charts/Calendar3D/index.jsx index 5892b18..25fe331 100644 --- a/src/components/atoms/charts/Calendar3D/index.jsx +++ b/src/components/atoms/charts/Calendar3D/index.jsx @@ -39,6 +39,8 @@ class Calendar3D extends React.Component { { width: this.myInput.current.offsetWidth, loading: true, + contrib: this.renderTopStats(), + streak: this.renderBottomStats(), }, () => this.checkCache() ); @@ -82,9 +84,9 @@ class Calendar3D extends React.Component { ) / 100; datesTotal = - moment(contributionCalendar.startDate).format("MMM DD, YYYY") + - " - " + - moment(contributionCalendar.endDate).format("MMM DD, YYYY"); + moment(contributionCalendar.startDate).format("MMM D") + + " → " + + moment(contributionCalendar.endDate).format("MMM D"); /* Busiest day */ maxCount = contribData.busiestDay.total; @@ -92,30 +94,11 @@ class Calendar3D extends React.Component { dateBest = dateBest.isValid() ? dateBest.format("MMM DD") : "-"; return { - __html: `
    \n - \n - \n - 1 year total\n - ${countTotal}\n - ${averageCount} per day\n - \n - \n - contributions\n - ${datesTotal}\n - \n - \n - \n - Busiest day\n - ${maxCount}\n - \n - \n - contributions\n - ${dateBest}\n - \n - \n - \n - \n -
    `, + countTotal, + averageCount, + datesTotal, + maxCount, + dateBest, }; } @@ -135,9 +118,9 @@ class Calendar3D extends React.Component { if (contribData.streak.longest) { streakLongest = contribData.streak.longest.totalDays; datesLongest = - moment(contribData.streak.longest.startDate).format("MMM DD, YYYY") + - " - " + - moment(contribData.streak.longest.endDate).format("MMM DD, YYYY"); + moment(contribData.streak.longest.startDate).format("MMM D") + + " → " + + moment(contribData.streak.longest.endDate).format("MMM D"); } else { streakLongest = "0"; datesLongest = "-"; @@ -145,37 +128,19 @@ class Calendar3D extends React.Component { if (contribData.streak.current.id !== -1) { streakCurrent = contribData.streak.current.totalDays; datesCurrent = - moment(contribData.streak.current.startDate).format("MMM DD, YYYY") + - " - " + - moment(contribData.streak.current.endDate).format("MMM DD, YYYY"); + moment(contribData.streak.current.startDate).format("MMM D") + + " → " + + moment(contribData.streak.current.endDate).format("MMM D"); } else { streakCurrent = "0"; datesCurrent = "-"; } return { - __html: `
    \n - \n - \n - Longest streak\n - ${streakLongest}\n - \n - \n - days\n - ${datesLongest}\n - \n - \n - \n - Current streak\n - ${streakCurrent}\n - \n - \n - days\n - ${datesCurrent}\n - \n - \n - \n -
    `, + streakLongest, + datesLongest, + streakCurrent, + datesCurrent, }; } @@ -209,7 +174,7 @@ class Calendar3D extends React.Component { size = 8; } - const maxHight = 100; + const maxHight = 60; let x = 0; let y = 0; @@ -343,17 +308,74 @@ class Calendar3D extends React.Component { render() { return (
    - {this.props.platformData && this.state.width > 500 && ( + {this.state.width > 500 && ( <> -
    -
    +
    +

    Contributions

    +
    +
    +

    + {this.state.contrib.countTotal} +

    +

    Total

    +

    + {this.state.contrib.datesTotal} +

    +
    +
    +

    + {this.state.contrib.maxCount} +

    +

    Best day

    +

    + {this.state.contrib.dateBest} +

    +
    +
    +

    + Average:{" "} + + {this.state.contrib.averageCount} + {" "} + / day +

    +
    +
    +

    Streaks

    +
    +
    +

    + {this.state.streak.streakLongest}{" "} + + {this.state.streak.streakLongest === 1 ? "day" : "days"} + +

    +

    Longest

    +

    + {this.state.streak.datesLongest} +

    +
    +
    +

    + {this.state.streak.streakCurrent}{" "} + + {this.state.streak.streakCurrent === 1 ? "day" : "days"} + +

    +

    Current

    +

    + {this.state.streak.datesCurrent} +

    +
    +
    +
    )}
    (this.context = c)} width={this.state.width} - height="350" + height="400" >
    {this.state.cache && } @@ -364,7 +386,6 @@ class Calendar3D extends React.Component { //#endregion //#region > Exports -//> Default Class export default Calendar3D; //#endregion diff --git a/src/components/atoms/charts/ContribRadar/index.jsx b/src/components/atoms/charts/ContribRadar/index.jsx index 1acd626..f57bdc7 100644 --- a/src/components/atoms/charts/ContribRadar/index.jsx +++ b/src/components/atoms/charts/ContribRadar/index.jsx @@ -180,7 +180,6 @@ class ContribRadar extends React.Component { //#endregion //#region > Exports -//> Default Class export default ContribRadar; //#endregion diff --git a/src/components/atoms/charts/LanguageBar/index.jsx b/src/components/atoms/charts/LanguageBar/index.jsx new file mode 100644 index 0000000..4e00db3 --- /dev/null +++ b/src/components/atoms/charts/LanguageBar/index.jsx @@ -0,0 +1,76 @@ +//#region > Imports +//> React +// Contains all the functionality necessary to define React components +import React from "react"; + +//> CSS +import "./languages.scss"; +//#endregion + +//#region > Components +/** + * @class A language chart which contains several items with their shares. + */ +class LanguageBar extends React.Component { + state = {}; + + renderBars = (languages) => { + let latest = 0; + + if (languages[0].size !== 0) { + return languages.map((language, i) => { + const { color, share } = language; + const value = latest + share; + + latest += share; + + return ( +
    + ); + }); + } else { + return ( +
    + ); + } + }; + + render() { + const { languages, height } = this.props; + + return ( +
    + {this.renderBars(languages)} +
    + ); + } +} +//#endregion + +//#region > Exports +export default LanguageBar; +//#endregion + +/** + * SPDX-License-Identifier: (EUPL-1.2) + * Copyright © 2019-2020 Simon Prast + */ diff --git a/src/components/atoms/charts/LanguageBar/languages.scss b/src/components/atoms/charts/LanguageBar/languages.scss new file mode 100644 index 0000000..dd3fc01 --- /dev/null +++ b/src/components/atoms/charts/LanguageBar/languages.scss @@ -0,0 +1,27 @@ +.languages { + width: "100%"; + background-color: "#e0e0de"; + margin: 0.3rem 0; + position: relative; + transition: height 0.2s ease; + + .filler { + height: 100%; + border-radius: inherit; + text-align: right; + position: absolute; + + &:last-child { + border-top-right-radius: 0.3rem; + border-bottom-right-radius: 0.3rem; + } + + border-top-left-radius: 0.3rem; + border-bottom-left-radius: 0.3rem; + } +} + +/** + * SPDX-License-Identifier: (EUPL-1.2) + * Copyright © 2019-2020 Simon Prast + */ diff --git a/src/components/atoms/charts/LatestActivity/index.jsx b/src/components/atoms/charts/LatestActivity/index.jsx index 2842de1..fdc8cc6 100644 --- a/src/components/atoms/charts/LatestActivity/index.jsx +++ b/src/components/atoms/charts/LatestActivity/index.jsx @@ -175,7 +175,6 @@ class LatestActivity extends React.Component { //#endregion //#region > Exports -//> Default Class export default LatestActivity; //#endregion diff --git a/src/components/atoms/index.js b/src/components/atoms/index.js index b9e0f83..0b72d1f 100644 --- a/src/components/atoms/index.js +++ b/src/components/atoms/index.js @@ -10,6 +10,7 @@ import Calendar2D from "./charts/Calendar2D"; import Calendar3D from "./charts/Calendar3D"; import ContribRadar from "./charts/ContribRadar"; import LatestActivity from "./charts/LatestActivity"; +import LanguageChart from "./charts/LanguageBar"; //> General import SearchBar from "./SearchBar"; import ErrorBoundary from "./ErrorBoundary"; @@ -25,6 +26,7 @@ export { Calendar3D, ContribRadar, LatestActivity, + LanguageChart, SearchBar, ErrorBoundary, }; diff --git a/src/components/molecules/Footer/index.jsx b/src/components/molecules/Footer/index.jsx index ccaa3b8..7074437 100644 --- a/src/components/molecules/Footer/index.jsx +++ b/src/components/molecules/Footer/index.jsx @@ -207,13 +207,13 @@ class Footer extends React.PureComponent {

    {this.state.slogan}

    {this.props.location.pathname === "/" ? ( - + Join now ) : ( - + Join now @@ -254,7 +254,10 @@ class Footer extends React.PureComponent { //#endregion //#region > Exports -//> Default Class +/** + * Got access to the history object’s properties and the closest + * 's match. + */ export default withRouter(Footer); //#endregion diff --git a/src/components/molecules/MovableBoundary/index.jsx b/src/components/molecules/MovableBoundary/index.jsx new file mode 100644 index 0000000..4429767 --- /dev/null +++ b/src/components/molecules/MovableBoundary/index.jsx @@ -0,0 +1,166 @@ +//#region > Imports +//> React +// Contains all the functionality necessary to define React components +import React from "react"; +// React PropTypes +import PropTypes from "prop-types"; +//> MDB Sortable +// MDB plugin for sortable item lists +import MDBSortable from "mdb-react-sortable"; + +//> CSS +import "./movableboundary.scss"; +//#endregion + +//#region > Components +/** + * @class This enables implementation of horizontally or vertically movable + * items into a page. + */ +class MovableBoundary extends React.Component { + state = { + items: this.props.items ? this.props.items : null, + indexArray: null, + }; + + componentWillReceiveProps = (nextProps) => { + this.setState({ + items: nextProps.items ? nextProps.items : null, + }); + + // If props.edit changes to false, save indexArray + if (!nextProps.edit && this.props.edit) { + this.saveItemOrder(); + } + }; + + componentDidMount() { + // Load item order after mounting + this.loadItemOrder(); + } + + componentDidUpdate(prevProps, prevState) { + // Load items when pool changes + if (prevProps.pool !== this.props.pool) { + this.loadItemOrder(); + } + } + + /** + * Load the indexArray from storage. + * If there's no indexArray, + * default indexArray [0, 1, 2, 3, ...] will be generated. + */ + loadItemOrder() { + let indexArray = this.props.pool[this.props.uid]; + + if (!indexArray) { + let baseIndexArray = []; + + this.state.items.map((item, i) => { + baseIndexArray = [...baseIndexArray, i]; + }); + + indexArray = baseIndexArray; + } else { + indexArray = JSON.parse(indexArray); + } + + this.setState({ indexArray }); + } + + // Returns items ordered by indexArray + reorderItems() { + let orderedItems = []; + + this.state.indexArray.map((index) => { + orderedItems = [...orderedItems, this.state.items[index]]; + }); + + return orderedItems; + } + + // Store indexArray + saveItemOrder() { + this.props.pool[this.props.uid] = JSON.stringify(this.state.indexArray); + } + + // Returns items to be rendered in the Sortable + renderItems(edit) { + const items = this.reorderItems(); + + return items.map((item) => { + if (edit) { + return
    {item}
    ; + } else { + return ( +
    + {item} +
    + ); + } + }); + } + + // Swaps indexArray position of element + swap = (newIndex, oldIndex) => { + let indexArray = this.state.indexArray; + + if (newIndex >= indexArray.length) { + let k = newIndex - indexArray.length + 1; + + while (k--) { + indexArray.push(undefined); + } + } + + indexArray.splice(newIndex, 0, indexArray.splice(oldIndex, 1)[0]); + + // Save item order when items swap + this.saveItemOrder(); + this.setState({ indexArray }); + }; + + render() { + return ( +
    + {this.state.items && this.state.indexArray && ( + <> + { + // When item is let go of, change indexArray position of item + this.swap(obj.newIndex, obj.oldIndex); + }} + /> + + )} +
    + ); + } +} +//#endregion + +//#region > PropTypes +MovableBoundary.propTypes = { + pool: PropTypes.object, + movementAxis: PropTypes.string, + items: PropTypes.array.isRequired, + uid: PropTypes.string.isRequired, + edit: PropTypes.bool.isRequired, +}; +//#endregion + +//#region > Exports +export default MovableBoundary; +//#endregion + +/** + * SPDX-License-Identifier: (EUPL-1.2) + * Copyright © 2019-2020 Simon Prast + */ diff --git a/src/components/molecules/MovableBoundary/movableboundary.scss b/src/components/molecules/MovableBoundary/movableboundary.scss new file mode 100644 index 0000000..65d28c3 --- /dev/null +++ b/src/components/molecules/MovableBoundary/movableboundary.scss @@ -0,0 +1,30 @@ +.SortableItem { + list-style: none; + cursor: move; + outline: none; + user-select: text !important; + + // View when sorting is enabled + &:not(.unsortable) { + background: transparentize($color: white, $amount: 0.5) !important; + margin-bottom: 1rem; + border-radius: 0.3rem; + padding-top: 0.3rem; + user-select: none !important; + } +} + +.SortableList { + display: grid; + grid: initial; + padding: 0; +} + +.unsortable { + cursor: default; +} + +/** + * SPDX-License-Identifier: (EUPL-1.2) + * Copyright © 2019-2020 Simon Prast + */ diff --git a/src/components/molecules/Navbar/index.jsx b/src/components/molecules/Navbar/index.jsx index 02713c8..701825e 100644 --- a/src/components/molecules/Navbar/index.jsx +++ b/src/components/molecules/Navbar/index.jsx @@ -1,9 +1,9 @@ //#region > Imports //> React // Contains all the functionality necessary to define React components -import React, { lazy, Suspense } from "react"; +import React from "react"; // DOM bindings for React Router -import { Link, withRouter } from "react-router-dom"; +import { Link, withRouter, NavLink } from "react-router-dom"; // React PropTypes import PropTypes from "prop-types"; //> MDB @@ -17,21 +17,26 @@ import { MDBCollapse, MDBContainer, MDBDropdown, - MDBDropdownItem, MDBDropdownToggle, MDBDropdownMenu, MDBSmoothScroll, MDBBtn, + MDBIcon, } from "mdbreact"; +//> Redux +// Allows to React components read data from a Redux store, and dispatch actions +// to the store to update data. +import { connect } from "react-redux"; -//> Searchbar +//> Actions +// Functions to send data from the application to the store +import { logoutAction } from "../../../store/actions/authActions"; +//> SearchBar import { SearchBar } from "../../atoms"; //> Images import SNEKLogo from "../../../assets/navigation/logo.png"; //> CSS import "./navbar.scss"; -//> Components -const Settings = lazy(() => import("../modals/SettingsModal")); //#endregion //#region > Components @@ -59,120 +64,115 @@ class Navbar extends React.Component { }; render() { - const { globalState, globalFunctions, location } = this.props; + const { location, loggedUser } = this.props; + const avatarUrl = loggedUser.platformData?.user?.avatarUrl + ? loggedUser.platformData.user.avatarUrl + : loggedUser.avatarUrl; return ( - <> - - - {location.pathname === "/" ? ( - - - SNEK Logo - SNEK - - - ) : ( - <> - {!globalState.loading && globalState.loggedUser ? ( - - - SNEK Logo - SNEK - + + + {location.pathname === "/" ? ( + + + SNEK Logo + SNEK + + + ) : ( + <> + {!loggedUser.anonymous ? ( + + + SNEK Logo + SNEK + + + ) : ( + + + SNEK Logo + SNEK + + + )} + + )} + + + + + + + + + {!loggedUser.anonymous ? ( + <> + + + + Profile + - ) : ( - - - SNEK Logo - SNEK - - - )} - - )} - - - - - - - - - {!globalState.loading && globalState.loggedUser ? ( - <> -
    - - - - {globalState.loggedUser.username} - - - - My profile - - - this.setState({ showSettings: true }) - } - > - Settings - - - Sign Out - - - - - - ) : ( - <> - {location.pathname !== "/" && ( - +
    + + + + {loggedUser.username} + + + + Settings + + + Sign Out + + + + + + ) : ( + <> + {location.pathname !== "/" && ( + + Sign In - )} - - )} - - - - - {this.state.showSettings && ( - Loading...
    }> - - - )} - + + )} + + )} + + + + ); } } @@ -180,15 +180,31 @@ class Navbar extends React.Component { //#region > PropTypes Navbar.propTypes = { - globalState: PropTypes.object, - globalFunctions: PropTypes.object, location: PropTypes.object, }; //#endregion +//#region > Redux Mapping +const mapStateToProps = (state) => ({ + loggedUser: { ...state.auth.loggedUser, ...state.user.loggedUser }, +}); + +const mapDispatchToProps = (dispatch) => { + return { + logout: () => dispatch(logoutAction()), + }; +}; +//#endregion + //#region > Exports -//> Default Class -export default withRouter(Navbar); +/** + * Provides its connected component with the pieces of the data it needs from + * the store, and the functions it can use to dispatch actions to the store. + * + * Got access to the history object’s properties and the closest + * 's match. + */ +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Navbar)); //#endregion /** diff --git a/src/components/molecules/Navbar/navbar.scss b/src/components/molecules/Navbar/navbar.scss index b9ed1c3..ff57e63 100644 --- a/src/components/molecules/Navbar/navbar.scss +++ b/src/components/molecules/Navbar/navbar.scss @@ -5,6 +5,17 @@ box-shadow: none; background: #f0f0f0; + .btn-white { + background: transparentize($color: white, $amount: 0.3) !important; + border: 2px white solid; + padding: 0.5rem 1rem; + + &:hover { + background: white !important; + border: 2px $snekGreen solid; + } + } + li { align-self: center; diff --git a/src/components/molecules/UserActionCard/index.jsx b/src/components/molecules/UserActionCard/index.jsx index ba23daf..705cfad 100644 --- a/src/components/molecules/UserActionCard/index.jsx +++ b/src/components/molecules/UserActionCard/index.jsx @@ -2,8 +2,6 @@ //> React // Contains all the functionality necessary to define React components import React from "react"; -// React PropTypes -import PropTypes from "prop-types"; //> MDB // "Material Design for Bootstrap" is a great UI design framework import { MDBRow, MDBCol, MDBAlert, MDBBtn, MDBIcon } from "mdbreact"; @@ -24,7 +22,7 @@ import "./useractioncard.scss"; */ class UserActionCard extends React.Component { state = { - activeItem: 0, + activeItem: this.props.activeIndex, }; goTo = (item) => { @@ -33,18 +31,20 @@ class UserActionCard extends React.Component { }); }; + setActiveItem = (activeItem) => { + this.setState({ + activeItem, + }); + }; + render() { - const { globalState, globalFunctions } = this.props; const { activeItem } = this.state; return (
    {activeItem === 0 && ( <> - this.setState({ activeItem: 1 })} - > + this.setActiveItem(1)}> Login to SNEK
    @@ -60,7 +60,7 @@ class UserActionCard extends React.Component {
    this.setState({ activeItem: 2 })} + onClick={() => this.setActiveItem(2)} >

    Software Engineer

    @@ -69,7 +69,7 @@ class UserActionCard extends React.Component {
    this.setState({ activeItem: 3 })} + onClick={() => this.setActiveItem(3)} >

    Media Engineer

    @@ -78,12 +78,8 @@ class UserActionCard extends React.Component { )} - {activeItem === 1 && ( - - )} - {activeItem === 2 && ( - - )} + {activeItem === 1 && } + {activeItem === 2 && } {activeItem === 3 && ( <>
    @@ -112,15 +108,7 @@ class UserActionCard extends React.Component { } //#endregion -//#region > PropTypes -UserActionCard.propTypes = { - globalState: PropTypes.object, - globalFunctions: PropTypes.object, -}; -//#endregion - //#region > Exports -//> Default Class export default UserActionCard; //#endregion diff --git a/src/components/molecules/forms/LoginForm/index.jsx b/src/components/molecules/forms/LoginForm/index.jsx index 244d144..5d48a78 100644 --- a/src/components/molecules/forms/LoginForm/index.jsx +++ b/src/components/molecules/forms/LoginForm/index.jsx @@ -4,12 +4,21 @@ import React from "react"; // React PropTypes import PropTypes from "prop-types"; -//> Additional -// Text animations -import TextLoop from "react-text-loop"; //> MDB // "Material Design for Bootstrap" is a great UI design framework import { MDBAlert, MDBBtn, MDBIcon } from "mdbreact"; +//> Additional +// SHA hashing algorithm +import { sha256 } from "js-sha256"; +//> Redux +// Allows to React components read data from a Redux store, and dispatch actions +// to the store to update data. +import { connect } from "react-redux"; + +//> Actions +// Functions to send data from the application to the store +import { loginAction } from "../../../../store/actions/authActions"; + //#endregion //#region > Components @@ -18,6 +27,7 @@ class LoginForm extends React.Component { state = { login_username: "", login_password: "", + loginFail: false, }; testForError = (id) => { @@ -119,17 +129,17 @@ class LoginForm extends React.Component { }); } else { // Proceed to login - const result = await this.props.globalFunctions.login( - this.state.login_username, - this.state.login_password - ); + const result = await this.props.login({ + username: this.state.login_username, + password: sha256(this.state.login_password), // Hash password + }); //#TSID6 //console.log("LOGIN FORM PROCEED TO LOGIN", result); - - if (result) { + if (result?.payload.error) { this.setState({ - loginFail: false, + loginFail: true, + errorMsg: result.payload.message, }); } } @@ -149,7 +159,7 @@ class LoginForm extends React.Component {

    Login to SNEK

    {this.state.loginFail && ( - Can not perform login. Please check your username and password. + {this.state.errorMsg} )}
    @@ -181,7 +191,7 @@ class LoginForm extends React.Component { } value={this.state.login_password} /> - + Login @@ -194,14 +204,24 @@ class LoginForm extends React.Component { //#region > PropTypes LoginForm.propTypes = { - globalFunctions: PropTypes.object, goTo: PropTypes.func, }; //#endregion +//#region > Redux Mapping +const mapStateToProps = (state) => ({}); + +const mapDispatchToProps = (dispatch) => { + return { login: (user) => dispatch(loginAction(user)) }; +}; +//#endregion + //#region > Exports -//> Default Class -export default LoginForm; +/** + * Provides its connected component with the pieces of the data it needs from + * the store, and the functions it can use to dispatch actions to the store. + */ +export default connect(mapStateToProps, mapDispatchToProps)(LoginForm); //#endregion /** diff --git a/src/components/molecules/forms/RegisterForm/index.jsx b/src/components/molecules/forms/RegisterForm/index.jsx index 4078d05..08b4e85 100644 --- a/src/components/molecules/forms/RegisterForm/index.jsx +++ b/src/components/molecules/forms/RegisterForm/index.jsx @@ -29,9 +29,18 @@ import { MDBListGroup, MDBListGroupItem, } from "mdbreact"; - //> OAuth import GitHubOAuth from "reactjs-oauth"; +//> Redux +// Allows to React components read data from a Redux store, and dispatch actions +// to the store to update data. +import { connect } from "react-redux"; + +//> Actions +// Functions to send data from the application to the store +import { registerAction } from "../../../../store/actions/userActions"; +import { fetchGitLabServersAction } from "../../../../store/actions/generalActions"; +import { loginAction } from "../../../../store/actions/authActions"; //#endregion //#region > Components @@ -77,10 +86,10 @@ class RegisterForm extends React.Component { // Check if GitLab Servers have already been set if (this.state.gitlab_servers === undefined) { // Retrieve GitLab servers - const gitlab_servers = await this.props.globalFunctions.fetchGitLabServers(); - - this.setState({ - gitlab_servers, + this.props.fetchGitLabServers().then(() => { + this.setState({ + gitlab_servers: this.props.gitlabServers, + }); }); } }; @@ -405,7 +414,14 @@ class RegisterForm extends React.Component { password: password1, }; - this.props.globalFunctions.registerUser(registrationData); + this.props.register(registrationData).then(() => { + const { username, password } = registrationData; + + this.props.login({ + username, + password, + }); + }); } ); } else { @@ -723,6 +739,7 @@ class RegisterForm extends React.Component {
    PropTypes RegisterForm.propTypes = { - globalState: PropTypes.object, - globalFunctions: PropTypes.object, goto: PropTypes.func, }; //#endregion +//#region > Redux Mapping +const mapStateToProps = (state) => ({ + registrationHistory: state.user.registrationHistory, + gitlabServers: state.general.allGitlabServers, +}); + +const mapDispatchToProps = (dispatch) => { + return { + register: (registrationData) => dispatch(registerAction(registrationData)), + login: (user) => dispatch(loginAction(user)), + fetchGitLabServers: () => dispatch(fetchGitLabServersAction()), + }; +}; +//#endregion + //#region > Exports -//> Default Class -export default RegisterForm; +/** + * Provides its connected component with the pieces of the data it needs from + * the store, and the functions it can use to dispatch actions to the store. + */ +export default connect(mapStateToProps, mapDispatchToProps)(RegisterForm); //#endregion /** diff --git a/src/components/molecules/index.js b/src/components/molecules/index.js index 6ef41db..fae23e9 100644 --- a/src/components/molecules/index.js +++ b/src/components/molecules/index.js @@ -5,13 +5,22 @@ import Footer from "./Footer"; import Navbar from "./Navbar"; import LoginForm from "./forms/LoginForm"; import RegisterForm from "./forms/RegisterForm"; -import UploadModal from "./modals/UploadModal"; +import TalkUploadModal from "./modals/TalkUploadModal"; import UserActionCard from "./UserActionCard"; +import MovableBoundary from "./MovableBoundary"; //#endregion //#region > Exports //> Molecules -export { Footer, Navbar, LoginForm, RegisterForm, UploadModal, UserActionCard }; +export { + Footer, + Navbar, + LoginForm, + RegisterForm, + TalkUploadModal, + UserActionCard, + MovableBoundary, +}; //#endregion /** diff --git a/src/components/molecules/modals/ProfilePictureModal/index.jsx b/src/components/molecules/modals/ProfilePictureModal/index.jsx new file mode 100644 index 0000000..3b967d3 --- /dev/null +++ b/src/components/molecules/modals/ProfilePictureModal/index.jsx @@ -0,0 +1,149 @@ +//#region > Imports +//> React +// Contains all the functionality necessary to define React components +import React from "react"; +// Contains the functionality for editing a file +import AvatarEditor from "react-avatar-editor"; +//> MDB +// "Material Design for Bootstrap" is a great UI design framework +import { + MDBModal, + MDBModalHeader, + MDBIcon, + MDBModalBody, + MDBBtn, + MDBCol, + MDBRow, +} from "mdbreact"; + +//> CSS +import "./profile.scss"; +//#endregion + +//#region > Components +class ProfilePictureModal extends React.Component { + constructor(props) { + super(props); + + this.state = { + scale: 1, + }; + this.handleWheel = this.handleWheel.bind(this); + } + + onClickSave = () => { + if (this.editor) { + const file = this.editor.getImageScaledToCanvas().toDataURL(); + + this.props.setAvatarUrl(file); + this.props.handleProfilePictureModal(); + } + }; + + componentDidMount() { + window.addEventListener("wheel", this.handleWheel, { passive: true }); + } + + componentWillUnmount() { + window.removeEventListener("wheel", this.handleWheel); + } + + handleWheel(event) { + let scale = this.state.scale; + + if (event.deltaY < 0) { + if (scale < 2) { + scale += 0.01; + } + } else { + if (scale > 1) { + scale -= 0.01; + } + } + + this.setState({ scale }); + } + + handleScale = (e) => { + const scale = parseFloat(e.target.value); + + this.setState({ scale }); + }; + + setEditorRef = (editor) => (this.editor = editor); + + render() { + return ( + + + + + + + + Image Editor + + + + Save + + + + + + + + + + ); + } +} +//#endregion + +//#region > Exports +export default ProfilePictureModal; +//#endregion + +/** + * SPDX-License-Identifier: (EUPL-1.2) + * Copyright © Simon Prast + */ diff --git a/src/components/molecules/modals/ProfilePictureModal/profile.scss b/src/components/molecules/modals/ProfilePictureModal/profile.scss new file mode 100644 index 0000000..b176c61 --- /dev/null +++ b/src/components/molecules/modals/ProfilePictureModal/profile.scss @@ -0,0 +1,112 @@ +#profile { + #header { + margin-bottom: -0.5em; + } + + #btn-save { + margin-top: -0.5em; + } + + input[type=range] { + -webkit-appearance: none; + margin: 18px 0; + width: 80%; + } + + input[type=range]:focus { + outline: none; + } + + input[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 8.4px; + cursor: pointer; + box-shadow: 1 1 1 #000, 0 0 1 #0d0d0d; + background: #77bd43; + border-radius: 1.3px; + border: 0.2px solid #010101; + } + + input[type=range]::-webkit-slider-thumb { + box-shadow: 1 1 1 #000, 0 0 1 #0d0d0d; + border: 1px solid #000; + height: 20px; + width: 16px; + border-radius: 3px; + background: #fff; + cursor: pointer; + -webkit-appearance: none; + margin-top: -7px; + } + + input[type=range]:focus::-webkit-slider-runnable-track { + background: #77bd43; + } + + input[type=range]::-moz-range-track { + width: 100%; + height: 8.4px; + cursor: pointer; + box-shadow: 1 1 1 #000, 0 0 1 #0d0d0d; + background: #77bd43; + border-radius: 1.3px; + border: 0.2px solid #010101; + } + + input[type=range]::-moz-range-thumb { + box-shadow: 1 1 1 #000, 0 0 1 #0d0d0d; + border: 1px solid #000; + height: 36px; + width: 16px; + border-radius: 3px; + background: #fff; + cursor: pointer; + } + + input[type=range]::-ms-track { + width: 100%; + height: 8.4px; + cursor: pointer; + background: transparent; + border-color: transparent; + border-width: 16px 0; + color: transparent; + } + + input[type=range]::-ms-fill-lower { + background: #77bd43; + border: 0.2px solid #010101; + border-radius: 2.6px; + box-shadow: 1 1 1 #000, 0 0 1 #0d0d0d; + } + + input[type=range]::-ms-fill-upper { + background: #77bd43; + border: 0.2px solid #010101; + border-radius: 2.6px; + box-shadow: 1 1 1 #000, 0 0 1 #0d0d0d; + } + + input[type=range]::-ms-thumb { + box-shadow: 1 1 1 #000, 0 0 1 #0d0d0d; + border: 1px solid #000; + height: 36px; + width: 16px; + border-radius: 3px; + background: #fff; + cursor: pointer; + } + + input[type=range]:focus::-ms-fill-lower { + background: #77bd43; + } + + input[type=range]:focus::-ms-fill-upper { + background: #77bd43; + } +} + +/** + * SPDX-License-Identifier: (EUPL-1.2) + * Copyright © 2019-2020 Simon Prast + */ diff --git a/src/components/molecules/modals/SettingsModal/index.jsx b/src/components/molecules/modals/SettingsModal/index.jsx deleted file mode 100644 index 5c5a316..0000000 --- a/src/components/molecules/modals/SettingsModal/index.jsx +++ /dev/null @@ -1,483 +0,0 @@ -//#region > Imports -//> React -// Contains all the functionality necessary to define React components -import React from "react"; -//> MDB -// "Material Design for Bootstrap" is a great UI design framework -import { - MDBBtn, - MDBIcon, - MDBModal, - MDBModalBody, - MDBModalHeader, - MDBModalFooter, - MDBTabPane, - MDBTabContent, - MDBRow, - MDBCol, - MDBNav, - MDBNavLink, - MDBNavItem, - MDBInput, - MDBAnimation, - MDBSelect, - MDBSelectInput, - MDBSelectOption, - MDBSelectOptions, -} from "mdbreact"; - -//> CSS -import "./settings.scss"; -//#endregion - -//#region > Constant Variables -//> Settings data -const SETTINGS_TAB = [ - { name: "Profile", icon: "" }, - { name: "Customization", icon: "" }, - { name: "Account", icon: "" }, - { name: "Connections", icon: "" }, - { name: "Blocked users", icon: "" }, - { name: "Billing", icon: "" }, - { name: "Security", icon: "" }, -]; -//#endregion - -//#region > Components -/** @class A settings modal component for displaying and editing user settings */ -class SettingsModal extends React.Component { - state = { - changeDetected: false, - activeItemInnerPills: 0, - }; - - componentDidMount = () => { - // Check for the current values - const platformData = this.props.globalState?.fetchedUser?.platformData; - - if (platformData?.profile && platformData?.user) { - const profile = platformData.profile; - const data = platformData.user; - const enterData = { - first_name: data.firstName ? data.firstName : "", - last_name: data.lastName ? data.lastName : "", - email: data.email ? data.email : "", - showEmailPublic: data.settings.showEmailPublic, - company: profile.company ? profile.company : "", - showCompanyPublic: data.settings.showCompanyPublic, - website: profile.websiteUrl ? profile.websiteUrl : "", - location: profile.location ? profile.location : "", - showLocalRanking: data.settings.showLocalRanking, - showTopLanguages: data.settings.showTopLanguages, - show3DDiagram: data.settings.show3DDiagram, - show2DDiagram: data.settings.show2DDiagram, - activeTheme: data.settings.activeTheme - ? data.settings.activeTheme - : null, - }; - - const dataString = this.stringToHash(JSON.stringify(enterData)); - - this.setState({ - ...enterData, - checksum: dataString, - }); - } else { - this.initBlank(); - } - }; - - initBlank = () => { - this.setState({ - first_name: "", - last_name: "", - email: "", - showEmailPublic: true, - company: "", - showCompanyPublic: true, - website: "", - location: "", - showLocalRanking: true, - showTopLanguages: true, - show3DDiagram: true, - show2DDiagram: true, - activeTheme: null, - }); - }; - - stringToHash = (string) => { - let hash = 0; - - if (string.length == 0) return hash; - - for (let i = 0; i < string.length; i++) { - let char = string.charCodeAt(i); - - hash = (hash << 5) - hash + char; - hash = hash & hash; - } - - return hash; - }; - - handleSelectChange = (val) => { - this.setState( - { - activeTheme: val[0], - }, - () => this.getChange() - ); - }; - - handleCheckChange = (e) => { - this.setState( - { - [e.target.name]: e.target.checked, - }, - () => this.getChange() - ); - }; - - handleTextChange = (e) => { - this.setState( - { - [e.target.name]: e.target.value, - }, - () => this.getChange() - ); - }; - - getChange = () => { - let currentData = { - first_name: this.state.first_name, - last_name: this.state.last_name, - email: this.state.email, - showEmailPublic: this.state.showEmailPublic, - company: this.state.company, - showCompanyPublic: this.state.showCompanyPublic, - website: this.state.website, - location: this.state.location, - showLocalRanking: this.state.showLocalRanking, - showTopLanguages: this.state.showTopLanguages, - show3DDiagram: this.state.show3DDiagram, - show2DDiagram: this.state.show2DDiagram, - activeTheme: this.state.activeTheme ? this.state.activeTheme : null, - }; - - // Get hash of current data - let currentHash = this.stringToHash(JSON.stringify(currentData)); - - if (this.state.changeDetected) { - if (this.state.checksum === currentHash) { - this.setState({ - changeDetected: false, - }); - } - } else { - if (this.state.checksum !== currentHash) { - this.setState({ - changeDetected: true, - }); - } - } - }; - - toggleInnerPills = (tab) => (e) => { - if (this.state.activeItemInnerPills !== tab) { - this.setState({ - activeItemInnerPills: tab, - }); - } - }; - - save = () => { - this.props.saveSettings(this.state); - this.props.closeModal(); - }; - - render() { - return ( - - - - Settings - - - - - - {SETTINGS_TAB.map((tab, i) => { - return ( - - - {tab.name} - {this.state.activeItemInnerPills === i && - tab.icon !== "" && ( - - - - )} - - - ); - })} - - - - - -
    Profile
    -
    -

    Your full name

    - - - - - - - - -

    Public email

    - - - - - - Display email on profile

    } - filled - type="checkbox" - id="checkbox0" - name="showEmailPublic" - onChange={this.handleCheckChange} - checked={this.state.showEmailPublic} - containerClass="mr-5" - /> -

    Your workplace

    - - - - - - - You can @mention your company anywhere on SNEK - - Display company on profile

    } - filled - type="checkbox" - id="checkbox1" - name="showCompanyPublic" - onChange={this.handleCheckChange} - checked={this.state.showCompanyPublic} - containerClass="mr-5" - /> -

    Website

    - - - - - -

    Location

    - - - - - - - This can also assist us in finding you the best local - matches - -
    -
    - - - Show local ranking

    } - filled - type="checkbox" - id="checkbox3" - name="showLocalRanking" - onChange={this.handleCheckChange} - checked={this.state.showLocalRanking} - containerClass="mr-5" - /> -
    - - Show top programming languages

    } - filled - type="checkbox" - id="checkbox4" - name="showTopLanguages" - onChange={this.handleCheckChange} - checked={this.state.showTopLanguages} - containerClass="mr-5" - /> -
    - -
    -
    - - Show 3D work activity diagram

    } - filled - type="checkbox" - id="checkbox5" - name="show3DDiagram" - onChange={this.handleCheckChange} - checked={this.state.show3DDiagram} - containerClass="mr-5" - /> -
    - - Show 2D work activity diagram

    } - filled - type="checkbox" - id="checkbox6" - name="show2DDiagram" - onChange={this.handleCheckChange} - checked={this.state.show2DDiagram} - containerClass="mr-5" - /> -
    -
    -
    - -
    Customization
    -
    -

    Choose your theme

    - - - - - - Default - {this.props.globalState.fetchedUser.accessories - .themes && - this.props.globalState.fetchedUser.accessories.themes.tids.map( - (tid, i) => { - let name = "Unnamed"; - switch (tid) { - case "9d88bda4657dcf17581ee91dfe6ab2a3": - name = "Alpha"; - break; - default: - name = "Unnamed"; - } - name += " Theme"; - return ( - - {tid} - - ); - } - )} - - - - -
    -
    - -
    Panel 3
    -
    -
    -
    -
    -
    - - {this.state.changeDetected && ( - - Save - - )} - - Close - - -
    - ); - } -} -//#endregion - -//#region > Exports -//> Default Class -export default SettingsModal; -//#endregion - -/** - * SPDX-License-Identifier: (EUPL-1.2) - * Copyright © 2019-2020 Simon Prast - */ diff --git a/src/components/molecules/modals/UploadModal/index.jsx b/src/components/molecules/modals/TalkUploadModal/index.jsx similarity index 80% rename from src/components/molecules/modals/UploadModal/index.jsx rename to src/components/molecules/modals/TalkUploadModal/index.jsx index 37cf98e..58fca7b 100644 --- a/src/components/molecules/modals/UploadModal/index.jsx +++ b/src/components/molecules/modals/TalkUploadModal/index.jsx @@ -13,18 +13,26 @@ import { MDBModalBody, MDBProgress, } from "mdbreact"; +//> Redux +// Allows to React components read data from a Redux store, and dispatch actions +// to the store to update data. +import { connect } from "react-redux"; + +//> Actions +// Functions to send data from the application to the store +import { uploadTalkAction } from "../../../../store/actions/userActions"; //#endregion //#region > Components /** @class A upload modal component for uploading files including a drop-zone */ -class UploadModal extends React.Component { +class TalkUploadModal extends React.Component { state = { loading: false, error: [], }; onDrop = async (files) => { - const { globalState, globalFunctions } = this.props; + const { loggedUser, fetchedUser } = this.props; if (files.length > 0) { this.setState({ @@ -32,11 +40,11 @@ class UploadModal extends React.Component { loading: true, }); - globalFunctions + this.props .uploadTalk(files[0], { - avatarUrl: globalState.fetchedUser.platformData.profile.avatarUrl, + avatarUrl: fetchedUser.platformData.user.avatarUrl, owner: { - username: globalState.loggedUser.username, + username: loggedUser.username, }, }) .then(() => { @@ -145,9 +153,23 @@ class UploadModal extends React.Component { } //#endregion +//#region > Redux Mapping +const mapStateToProps = (state) => ({ + loggedUser: state.user.fetchedUser, + fetchedUser: state.user.fetchedUser, +}); + +const mapDispatchToProps = (dispatch) => { + return { uploadTalk: (file) => dispatch(uploadTalkAction(file)) }; +}; +//#endregion + //#region > Exports -//> Default Class -export default UploadModal; +/** + * Provides its connected component with the pieces of the data it needs from + * the store, and the functions it can use to dispatch actions to the store. + */ +export default connect(mapStateToProps, mapDispatchToProps)(TalkUploadModal); //#endregion /** diff --git a/src/components/molecules/modals/index.js b/src/components/molecules/modals/index.js index 008ffd4..f95b0ad 100644 --- a/src/components/molecules/modals/index.js +++ b/src/components/molecules/modals/index.js @@ -1,14 +1,14 @@ //#region > Imports //> Components // Import all components to export them for easy access from parent components -import UploadModal from "./UploadModal"; -import SettingModal from "./SettingsModal"; +import TalkUploadModal from "./TalkUploadModal"; +import ProfilePictureModal from "./ProfilePictureModal"; //#endregion //#region > Exports //> Components // Export the components for easy access from parent components -export { UploadModal, SettingModal }; +export { TalkUploadModal, ProfilePictureModal }; //#endregion /** diff --git a/src/components/organisms/ProfileInfo/index.jsx b/src/components/organisms/ProfileInfo/index.jsx index d64a8eb..21dceb6 100644 --- a/src/components/organisms/ProfileInfo/index.jsx +++ b/src/components/organisms/ProfileInfo/index.jsx @@ -15,18 +15,25 @@ import { MDBIcon, MDBTooltip, } from "mdbreact"; +//> Redux +// Allows to React components read data from a Redux store, and dispatch actions +// to the store to update data. +import { connect } from "react-redux"; + +//> Components +import { LanguageChart } from "../../atoms"; //#endregion //#region > Components /** @class This component displays personal information and status of a user */ class ProfileInfo extends React.Component { - state = {}; + state = { limitLanguages: true }; componentDidMount = () => { - const { globalState } = this.props; + const { fetchedUser } = this.props; - if (this.props.globalState.fetchedUser && !this.state.sources) { - this.displaySources(globalState.fetchedUser.platformData.profile.sources); + if (fetchedUser && !this.state.sources) { + this.displaySources(fetchedUser.platformData.profile.sources); } }; @@ -50,93 +57,80 @@ class ProfileInfo extends React.Component { }; render() { - const { globalState } = this.props; + const { fetchedUser } = this.props; return (

    - {globalState.fetchedUser && - globalState.fetchedUser.platformData.user.firstName && - globalState.fetchedUser.platformData.user.lastName && ( + {fetchedUser && + fetchedUser.platformData.user.firstName && + fetchedUser.platformData.user.lastName && ( <> - {globalState.fetchedUser.platformData.user.firstName + " "} - {globalState.fetchedUser.platformData.user.lastName} + {fetchedUser.platformData.user.firstName + " "} + {fetchedUser.platformData.user.lastName} )}

    - {globalState.fetchedUser && - globalState.fetchedUser.platformData.user.settings && - globalState.fetchedUser.platformData.user.settings - .showLocalRanking && ( + {fetchedUser && + fetchedUser.platformData.user.settings && + fetchedUser.platformData.user.settings.showLocalRanking && (

    #3 in your region

    )} - {globalState.fetchedUser && - globalState.fetchedUser.platformData.profile.company && ( + {fetchedUser && fetchedUser.platformData.user.company && ( + <> + {fetchedUser && + fetchedUser.platformData.user.settings.showCompanyPublic && ( + + {fetchedUser.platformData.user.company} + + )} + + )} +
    + {fetchedUser && fetchedUser.accessories.badges && ( <> - {globalState.fetchedUser && - globalState.fetchedUser.platformData.user.settings - .showCompanyPublic && ( - - {globalState.fetchedUser.platformData.profile.company} - - )} + {fetchedUser.accessories.badges.bids.map((bid, i) => { + switch (bid) { + case "6403bf4d17b8472735a93b71a37e0bd0": + return ( + + Alpha + + ); + } + })} )} -
    - {globalState.fetchedUser && - globalState.fetchedUser.accessories.badges && ( - <> - {globalState.fetchedUser.accessories.badges.bids.map( - (bid, i) => { - switch (bid) { - case "6403bf4d17b8472735a93b71a37e0bd0": - return ( - - Alpha - - ); - } - } - )} - - )}
    - {globalState.fetchedUser && - globalState.fetchedUser.platformData.profile.statusMessage && ( -
    - {globalState.fetchedUser.platformData.profile - .statusEmojiHTML && ( -
    - )} - - {globalState.fetchedUser.platformData.profile.statusMessage} - -
    - )} + {fetchedUser && fetchedUser.platformData.profile.statusMessage && ( +
    + {fetchedUser.platformData.profile.statusEmojiHTML && ( +
    + )} + + {fetchedUser.platformData.profile.statusMessage} + +
    + )}
    - + Follow @@ -181,19 +175,17 @@ class ProfileInfo extends React.Component {

    Organisations

    - {globalState.fetchedUser && ( + {fetchedUser && (
    = 5 + fetchedUser.platformData.profile.organizations.length >= 5 ? "orgs text-center" : "orgs" } > - {globalState.fetchedUser.platformData.profile.organizations - .length > 0 ? ( + {fetchedUser.platformData.profile.organizations.length > 0 ? ( <> - {globalState.fetchedUser.platformData.profile.organizations.map( + {fetchedUser.platformData.profile.organizations.map( (org, i) => { return ( @@ -297,17 +289,58 @@ class ProfileInfo extends React.Component { ) : ( - {globalState.fetchedUser.username} hasn't joined an - organisation yet. + {fetchedUser.username} hasn't joined an organisation yet. )}
    )} -
    -

    Top languages

    -
    -

    Language Chart

    -
    + {fetchedUser.platformData?.statistic?.languages?.length > 0 && ( +
    +
    +

    Top languages

    + + {fetchedUser.platformData.statistic.languages + .slice( + 0, + this.state.limitLanguages + ? 3 + : fetchedUser.platformData.statistic.languages.length - 1 + ) + .map((language, i) => { + return ( + +
    +
    + + {language.name} +
    + + {language.share}% + +
    +
    + ); + })} + {this.state.limitLanguages && + fetchedUser.platformData.statistic.languages.length > 3 && ( +

    this.setState({ limitLanguages: false })} + > + Show more +

    + )} +
    + )}
    ); @@ -315,9 +348,22 @@ class ProfileInfo extends React.Component { } //#endregion +//#region > Redux Mapping +const mapStateToProps = (state) => ({ + fetchedUser: state.user.fetchedUser, +}); + +const mapDispatchToProps = (dispatch) => { + return {}; +}; +//#endregion + //#region > Exports -//> Default Class -export default ProfileInfo; +/** + * Provides its connected component with the pieces of the data it needs from + * the store, and the functions it can use to dispatch actions to the store. + */ +export default connect(mapStateToProps, mapDispatchToProps)(ProfileInfo); //#endregion /** diff --git a/src/components/organisms/SoftwareTabs/index.jsx b/src/components/organisms/SoftwareTabs/index.jsx index 8492a94..f5ea35a 100644 --- a/src/components/organisms/SoftwareTabs/index.jsx +++ b/src/components/organisms/SoftwareTabs/index.jsx @@ -5,6 +5,10 @@ import React from "react"; //> MDB // "Material Design for Bootstrap" is a great UI design framework import { MDBBadge } from "mdbreact"; +//> Redux +// Allows to React components read data from a Redux store, and dispatch actions +// to the store to update data. +import { connect } from "react-redux"; //> Components import { ProjectTab, OverviewTab, TalksTab } from "../tabs"; @@ -15,7 +19,24 @@ import { ProjectTab, OverviewTab, TalksTab } from "../tabs"; class SoftwareTabs extends React.Component { state = { activeTab: 0, - tabItems: [ + }; + + setActiveTab = (activeTab) => { + this.setState({ + activeTab, + }); + }; + + isSameOrigin = () => { + const { fetchedUser, loggedUser } = this.props; + + return fetchedUser.username === loggedUser.username; + }; + + render() { + const { fetchedUser } = this.props; + const { activeTab } = this.state; + const tabItems = [ { title: "Overview", visible: true, @@ -25,10 +46,8 @@ class SoftwareTabs extends React.Component { { title: "Projects", visible: true, - pill: this.props.globalState?.fetchedUser?.platformData?.profile - ?.repositories - ? this.props.globalState?.fetchedUser?.platformData?.profile - ?.repositories?.length + pill: this.props.fetchedUser?.platformData?.profile?.repositories + ? this.props.fetchedUser?.platformData?.profile?.repositories?.length : "0", notification: false, }, @@ -53,27 +72,22 @@ class SoftwareTabs extends React.Component { title: "Talks", visible: true, notification: false, - pill: this.props.globalState.fetchedUser.platformData.talks - ? this.props.globalState.fetchedUser.platformData.talks.length + pill: this.props.fetchedUser?.platformData.talks + ? this.props.fetchedUser?.platformData.talks.length : "0", notification: false, }, - ], - }; - - render() { - const { globalState } = this.props; - const { activeTab } = this.state; + ]; return (
      - {this.state.tabItems.map((item, i) => { + {tabItems.map((item, i) => { return (
    • this.setState({ activeTab: i })} + onClick={() => this.setActiveTab(i)} > {item.title} {item.pill} @@ -82,19 +96,17 @@ class SoftwareTabs extends React.Component { ); })}
    -
    +
    {activeTab === 0 && ( )} {activeTab === 1 && ( )} @@ -111,9 +123,23 @@ class SoftwareTabs extends React.Component { } //#endregion +//#region > Redux Mapping +const mapStateToProps = (state) => ({ + loggedUser: state.auth.loggedUser, + fetchedUser: state.user.fetchedUser, +}); + +const mapDispatchToProps = (dispatch) => { + return {}; +}; +//#endregion + //#region > Exports -//> Default Class -export default SoftwareTabs; +/** + * Provides its connected component with the pieces of the data it needs from + * the store, and the functions it can use to dispatch actions to the store. + */ +export default connect(mapStateToProps, mapDispatchToProps)(SoftwareTabs); //#endregion /** diff --git a/src/components/organisms/profiles/SoftwareEngineer/index.jsx b/src/components/organisms/profiles/SoftwareEngineer/index.jsx index 3c3c391..bb556cd 100644 --- a/src/components/organisms/profiles/SoftwareEngineer/index.jsx +++ b/src/components/organisms/profiles/SoftwareEngineer/index.jsx @@ -16,22 +16,14 @@ class SoftwareEngineer extends React.Component { state = {}; render() { - const { globalState, globalFunctions } = this.props; - return ( - + - + @@ -41,7 +33,6 @@ class SoftwareEngineer extends React.Component { //#endregion //#region > Exports -//> Default Class export default SoftwareEngineer; //#endregion diff --git a/src/components/organisms/tabs/OverviewTab/index.jsx b/src/components/organisms/tabs/OverviewTab/index.jsx index b87c700..68fb873 100644 --- a/src/components/organisms/tabs/OverviewTab/index.jsx +++ b/src/components/organisms/tabs/OverviewTab/index.jsx @@ -7,7 +7,14 @@ import PropTypes from "prop-types"; //> MDB // "Material Design for Bootstrap" is a great UI design framework import { MDBRow, MDBCol, MDBBtn, MDBIcon } from "mdbreact"; +//> Redux +// Allows to React components read data from a Redux store, and dispatch actions +// to the store to update data. +import { connect } from "react-redux"; +//> Actions +// Functions to send data from the application to the store +import { writeCacheAction } from "../../../../store/actions/userActions"; //> CSS import "./overviewtab.scss"; //> Components @@ -19,6 +26,7 @@ import { LatestActivity, ErrorBoundary, } from "../../../atoms"; +import { MovableBoundary } from "../../../molecules"; //#endregion //#region > Constant Variables @@ -54,6 +62,7 @@ const pinned = [ class OverviewTab extends React.Component { state = { selectedYear: undefined, + edit: false, }; selectDay = (day, wkey, dkey) => { @@ -66,127 +75,192 @@ class OverviewTab extends React.Component { }); }; + handleEditClick = (platformData) => { + if (this.state.edit) { + this.props.writeCache(platformData); + } + + this.setState({ edit: !this.state.edit }); + }; + render() { - const { platformData } = this.props; + const { fetchedUser, sameOrigin } = this.props; + const platformData = fetchedUser.platformData; + + // Create empty pool if there isn't already one + if (!fetchedUser.platformData.user.movablePool) { + fetchedUser.platformData.user.movablePool = {}; + } + + const movablePool = fetchedUser.platformData.user.movablePool; return ( <> {/*
    - +
    {talk.social && ( @@ -174,17 +181,17 @@ class Talks extends React.Component { )}
    {talk.repository.name} - Owned by {talk.repository.owner.username} + Owned by {talk.repository?.owner.username}
    @@ -195,7 +202,10 @@ class Talks extends React.Component { })} {this.state.showUpload && ( - + )} ); @@ -203,9 +213,26 @@ class Talks extends React.Component { } //#endregion +//#region > Redux Mapping +const mapStateToProps = (state) => ({ + loggedUser: state.auth.loggedUser, + fetchedUser: state.user.fetchedUser, +}); + +const mapDispatchToProps = (dispatch) => { + return { deleteTalk: (talk) => dispatch(deleteTalkAction(talk)) }; +}; +//#endregion + //#region > Exports -//> Default Class -export default withRouter(Talks); +/** + * Provides its connected component with the pieces of the data it needs from + * the store, and the functions it can use to dispatch actions to the store. + * + * Got access to the history object’s properties and the closest + * 's match. + */ +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Talks)); //#endregion /** diff --git a/src/components/pages/CompanyPage/index.jsx b/src/components/pages/CompanyPage/index.jsx index 463066e..ebe89f5 100644 --- a/src/components/pages/CompanyPage/index.jsx +++ b/src/components/pages/CompanyPage/index.jsx @@ -50,6 +50,11 @@ const CONTRIB_OPTIONS = { legend: { display: false, }, + elements: { + point: { + radius: 0, + }, + }, scales: { xAxes: [ { @@ -160,8 +165,6 @@ class CompanyPage extends React.Component { }; render() { - const { globalState } = this.props; - return (
    @@ -489,6 +492,7 @@ class CompanyPage extends React.Component { Follow @@ -605,7 +609,7 @@ class CompanyPage extends React.Component { ); })} - + Add platform @@ -668,7 +672,6 @@ class CompanyPage extends React.Component { //#endregion //#region > Exports -//> Default Class export default CompanyPage; //#endregion diff --git a/src/components/pages/HomePage/index.jsx b/src/components/pages/HomePage/index.jsx index bee9b69..18be0cd 100644 --- a/src/components/pages/HomePage/index.jsx +++ b/src/components/pages/HomePage/index.jsx @@ -18,6 +18,10 @@ import { MDBBtn, MDBIcon, } from "mdbreact"; +//> Redux +// Allows to React components read data from a Redux store, and dispatch actions +// to the store to update data. +import { connect } from "react-redux"; //> Components import LatestActivity from "../../atoms/charts/LatestActivity"; @@ -97,12 +101,11 @@ class HomePage extends React.Component { }; render() { - const { globalState, globalFunctions } = this.props; + const { loggedUser } = this.props; + const activeActionCard = this.props.location?.state?.actionCard; - if (!globalState.loading && globalState.loggedUser) { - return ; - } else if (globalState.loading) { - return

    Loading

    ; + if (!loggedUser.anonymous) { + return ; } else { return (
    @@ -155,8 +158,7 @@ class HomePage extends React.Component { @@ -301,9 +303,22 @@ class HomePage extends React.Component { } //#endregion +//#region > Redux Mapping +const mapStateToProps = (state) => ({ + loggedUser: state.auth.loggedUser, +}); + +const mapDispatchToProps = (dispatch) => { + return {}; +}; +//#endregion + //#region > Exports -//> Default Class -export default HomePage; +/** + * Provides its connected component with the pieces of the data it needs from + * the store, and the functions it can use to dispatch actions to the store. + */ +export default connect(mapStateToProps, mapDispatchToProps)(HomePage); //#endregion /** diff --git a/src/components/pages/ProfilePage/index.jsx b/src/components/pages/ProfilePage/index.jsx index 20edf71..e5c2594 100644 --- a/src/components/pages/ProfilePage/index.jsx +++ b/src/components/pages/ProfilePage/index.jsx @@ -4,11 +4,23 @@ import React from "react"; // DOM bindings for React Router import { withRouter } from "react-router-dom"; +//> Redux +// Allows to React components read data from a Redux store, and dispatch actions +// to the store to update data. +import { connect } from "react-redux"; +//> Actions +// Functions to send data from the application to the store +import { + readCacheAction, + updateCacheAction, + saveSettingsActions, +} from "../../../store/actions/userActions"; //> Components import { SoftwareEngineer } from "../../organisms/profiles"; //> CSS import "./profile.scss"; + //#endregion //#region > Components @@ -24,38 +36,76 @@ class ProfilePage extends React.Component { this.props.saveSettings(state); }; + /** + * Check for refetch for a specific username. + * + * @param {string} username The username associated with a profile page + * @returns {boolean} True if a refetch is required otherwise False + */ + refetchRequired = (username) => { + const fetchedUser = this.props.fetchedUser; + + if (!fetchedUser) { + return true; + } else if (fetchedUser && !this.usernameMatchesFetchedUsername(username)) { + return true; + } + return false; + }; + + /** + * Check if the provided username matches with the current fetched user. + * + * @param {string} username The username associated with a profile page + * @returns {boolean} True if the usernames matches otherwise False + */ + usernameMatchesFetchedUsername = (username) => { + return username === this.props.fetchedUser?.username; + }; + componentDidMount = () => { - const { match, globalState, globalFunctions } = this.props; + this._isMounted = true; + + const { match, loggedUser, fetchedUser } = this.props; const username = match?.params?.username; if (username) { - if (globalFunctions.refetchRequired(username)) { - globalFunctions.fetchCacheData(username); + if (this.refetchRequired(username)) { + this.props.readCache(username); } + } + }; + + componentDidUpdate() { + const { loggedUser, fetchedUser } = this.props; + if (!this.props.cachingDone) { if ( - globalState.loggedUser && - globalFunctions.usernameMatchesFetchedUsername(username) + !loggedUser.anonymous && + loggedUser.username === fetchedUser?.username ) { - globalFunctions.updateCache(globalState?.fetchedUser); + this.props.updateCache(fetchedUser).then(() => { + if (this._isMounted) { + this.props.readCache(loggedUser.username); + } + }); } } - }; + } - componentWillReceiveProps = (nextProps) => { - const { globalState, globalFunctions } = this.props; + componentWillUnmount() { + this._isMounted = false; + } + componentWillReceiveProps = (nextProps) => { //#TSID10 //console.log("PROFILE PAGE NEXT PROPS", nextProps); }; render() { - const { globalState, globalFunctions } = this.props; + const { fetchedUser } = this.props; - if ( - globalState.loading || - (!globalState.loading && !globalState.fetchedUser) - ) { + if (!fetchedUser) { return (
    @@ -63,18 +113,13 @@ class ProfilePage extends React.Component {
    ); - } else if (!globalState.loading && globalState.fetchedUser === false) { - return

    Error. User can not be fetched.

    ; - } else if (!globalState.loading && globalState.fetchedUser) { + } else if (fetchedUser) { //#TSID11 - //console.dir("PROFILE PAGE RENDER SUCCESS", globalState.fetchedUser); + //console.dir("PROFILE PAGE RENDER SUCCESS", this.props.fetchedUser); return (
    - +
    ); } else { @@ -84,9 +129,33 @@ class ProfilePage extends React.Component { } //#endregion +//#region > Redux Mapping +const mapStateToProps = (state) => ({ + loggedUser: state.auth.loggedUser, + fetchedUser: state.user.fetchedUser, + cachingDone: state.user.cachingDone, +}); + +const mapDispatchToProps = (dispatch) => { + return { + saveSettings: (nextSettings) => dispatch(saveSettingsActions(nextSettings)), + readCache: (username) => dispatch(readCacheAction(username)), + updateCache: (fetchedUser) => dispatch(updateCacheAction(fetchedUser)), + }; +}; +//#endregion + //#region > Exports -//> Default Class -export default withRouter(ProfilePage); +/** + * Provides its connected component with the pieces of the data it needs from + * the store, and the functions it can use to dispatch actions to the store. + * + * Got access to the history object’s properties and the closest + * 's match. + */ +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(ProfilePage) +); //#endregion /** diff --git a/src/components/pages/ProfilePage/profile.scss b/src/components/pages/ProfilePage/profile.scss index 713af8d..6161319 100644 --- a/src/components/pages/ProfilePage/profile.scss +++ b/src/components/pages/ProfilePage/profile.scss @@ -8,17 +8,31 @@ } .bg-elegant { - background: #fafafa; + background: darken(white, 0.5%) !important; + border: 1px solid #e1e4e8; + border-radius: 0.3rem; } .bg-light { - background: #fafafa !important; + background: darken(white, 0.5%) !important; + border: 1px #e1e4e8 solid; + border-radius: 0.3rem; } .badges { .badge { box-shadow: none; margin-right: 0.4rem; + padding: 0.4rem; + border-radius: 0.4rem; + + &.badge-secondary-color { + background: transparentize( + $color: $secondary, + $amount: 0.3 + ) !important; + border: 2px $secondary solid; + } } } @@ -115,7 +129,15 @@ // Right col .profile-content { - background: #fafafa; + background: darken(white, 0.5%) !important; + + // Content + .content { + border-top: none !important; + border: 1px #e1e4e8 solid; + border-bottom-left-radius: 0.3rem; + border-bottom-right-radius: 0.3rem; + } .nav-tabs { background: white; @@ -140,7 +162,6 @@ } } } - // General things } /** diff --git a/src/components/pages/SettingsPage/index.jsx b/src/components/pages/SettingsPage/index.jsx new file mode 100644 index 0000000..a82b1a8 --- /dev/null +++ b/src/components/pages/SettingsPage/index.jsx @@ -0,0 +1,593 @@ +//#region > Imports +//> React +// Contains all the functionality necessary to define React components +import React from "react"; +// DOM bindings for React Router +import { Link } from "react-router-dom"; +// Contains the functionality for uploading a file +import Dropzone from "react-dropzone"; +//> MDB +// "Material Design for Bootstrap" is a great UI design framework +import { + MDBContainer, + MDBCol, + MDBInput, + MDBTabPane, + MDBRow, + MDBSelect, + MDBSelectOption, + MDBSelectOptions, + MDBNav, + MDBNavItem, + MDBNavLink, + MDBAnimation, + MDBIcon, + MDBTabContent, + MDBView, + MDBMask, + MDBSelectInput, + MDBBtn, +} from "mdbreact"; +//> Redux +// Allows to React components read data from a Redux store, and dispatch actions +// to the store to update data. +import { connect } from "react-redux"; + +//> Actions +// Functions to send data from the application to the store +import { + readCacheAction, + saveSettingsActions, +} from "../../../store/actions/userActions"; +//> Components +// Profile Picture Editor +import { ProfilePictureModal } from "../../../components/molecules/modals"; +//> CSS +import "./settings.scss"; +//#endregion + +//#region > Components +/** + * @class This component adds the Settings page, + * used to change user information like. + */ +class SettingsPage extends React.Component { + state = { + loading: true, + changeDetected: false, + showProfilePicture: false, + showNotification: false, + file: undefined, + activeItem: 0, + tabItems: [ + { name: "Profile", icon: "" }, + { name: "Customization", icon: "" }, + { name: "Account", icon: "" }, + { name: "Connections", icon: "" }, + { name: "Blocked users", icon: "" }, + { name: "Billing", icon: "" }, + { name: "Security", icon: "" }, + ], + }; + + // triggers every time the settings menu tab is pressed + componentDidMount = () => { + const { loggedUser } = this.props; + + if (!loggedUser.anonymous) { + this.props.readCache(loggedUser.username); + } + }; + + // important for direct url access + componentDidUpdate = () => { + const { loggedUser, fetchedUser } = this.props; + + // redirect to root if the loggedUser is anonymous + if (loggedUser.anonymous) { + this.props.history.push({ + pathname: "/", + state: { + actionCard: 1, + }, + }); + } + + if (!fetchedUser && !loggedUser.anonymous) { + this.props.readCache(loggedUser.username); + } + + if (fetchedUser && this.state.loading) { + const platformData = this.props.fetchedUser?.platformData; + const data = platformData.user; + const enterData = { + avatar_url: data.avatarUrl ? data.avatarUrl : "", + first_name: data.firstName ? data.firstName : "", + last_name: data.lastName ? data.lastName : "", + email: data.email ? data.email : "", + showEmailPublic: data.settings.showEmailPublic, + company: data.company ? data.company : "", + showCompanyPublic: data.settings.showCompanyPublic, + website: data.websiteUrl ? data.websiteUrl : "", + location: data.location ? data.location : "", + showLocalRanking: data.settings.showLocalRanking, + showTopLanguages: data.settings.showTopLanguages, + show3DDiagram: data.settings.show3DDiagram, + show2DDiagram: data.settings.show2DDiagram, + activeTheme: data.settings.activeTheme + ? data.settings.activeTheme + : null, + }; + + const dataString = this.stringToHash(JSON.stringify(enterData)); + + this.setState({ + ...enterData, + checksum: dataString, + loading: false, + }); + } + }; + + stringToHash = (string) => { + let hash = 0; + + if (string.length == 0) return hash; + + for (let i = 0; i < string.length; i++) { + let char = string.charCodeAt(i); + + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + + return hash; + }; + + handleSelectChange = (val) => { + this.setState( + { + activeTheme: val[0], + }, + () => this.getChange() + ); + }; + + handleCheckChange = (e) => { + this.setState( + { + [e.target.name]: e.target.checked, + }, + () => this.getChange() + ); + }; + + handleTextChange = (e) => { + this.setState( + { + [e.target.name]: e.target.value, + }, + () => this.getChange() + ); + }; + + getChange = () => { + let currentData = { + avatar_url: this.state.avatar_url, + first_name: this.state.first_name, + last_name: this.state.last_name, + email: this.state.email, + showEmailPublic: this.state.showEmailPublic, + company: this.state.company, + showCompanyPublic: this.state.showCompanyPublic, + website: this.state.website, + location: this.state.location, + showLocalRanking: this.state.showLocalRanking, + showTopLanguages: this.state.showTopLanguages, + show3DDiagram: this.state.show3DDiagram, + show2DDiagram: this.state.show2DDiagram, + activeTheme: this.state.activeTheme ? this.state.activeTheme : null, + }; + + // Get hash of current data + let currentHash = this.stringToHash(JSON.stringify(currentData)); + + if (this.state.changeDetected) { + if (this.state.checksum === currentHash) { + this.setState({ + changeDetected: false, + }); + } + } else { + if (this.state.checksum !== currentHash) { + this.setState({ + changeDetected: true, + }); + } + } + }; + + save = () => { + this.props.saveSettings(this.state); + this.setState({ showNotification: true }); + }; + + handleProfilePictureModal = () => { + if (this.state.showProfilePicture) { + this.setState({ + showProfilePicture: !this.state.showProfilePicture, + }); + } + }; + + onDrop = async (files) => { + if (files.length > 0) { + this.setState({ + file: files[0], + showProfilePicture: true, + }); + } + }; + + setAvatarUrl = (avatar_url) => { + this.setState({ avatar_url }, () => this.getChange()); + }; + + render() { + const { fetchedUser, loggedUser } = this.props; + const { activeItem } = this.state; + + if (fetchedUser && this.state.avatar_url) { + return ( + <> + {this.state.showNotification && ( +
    + + + Changes were saved successfully —{" "} + + View your profile + + + +
    + )} + + + + + + + + + + +

    + {loggedUser.username} +

    + Profile Settings +
    +
    +
    + {this.state.tabItems.map((tab, i) => { + return ( + + this.setState({ activeItem: i })} + > + {tab.name} + {activeItem === i && tab.icon !== "" && ( + + + + )} + + + ); + })} +
    +
    + + + +

    Profile

    + + {({ getRootProps, getInputProps }) => ( +
    + + + + + + + +
    + )} +
    +
    +

    Your full name

    + + + + + + + + +

    Public email

    + + + + + + Display email on profile

    } + filled + type="checkbox" + id="checkbox0" + name="showEmailPublic" + onChange={this.handleCheckChange} + checked={this.state.showEmailPublic} + containerClass="mr-5" + /> +

    Your workplace

    + + + + + + + You can @mention your company anywhere on SNEK + + Display company on profile

    } + filled + type="checkbox" + id="checkbox1" + name="showCompanyPublic" + onChange={this.handleCheckChange} + checked={this.state.showCompanyPublic} + containerClass="mr-5" + /> +

    Website

    + + + + + +

    Location

    + + + + + + + This can also assist us in finding you the best local + matches + +
    +
    + + + Show local ranking

    } + filled + type="checkbox" + id="checkbox3" + name="showLocalRanking" + onChange={this.handleCheckChange} + checked={this.state.showLocalRanking} + containerClass="mr-5" + /> +
    + + Show top programming languages

    } + filled + type="checkbox" + id="checkbox4" + name="showTopLanguages" + onChange={this.handleCheckChange} + checked={this.state.showTopLanguages} + containerClass="mr-5" + /> +
    + +
    +
    + + Show 3D work activity diagram

    } + filled + type="checkbox" + id="checkbox5" + name="show3DDiagram" + onChange={this.handleCheckChange} + checked={this.state.show3DDiagram} + containerClass="mr-5" + /> +
    + + Show 2D work activity diagram

    } + filled + type="checkbox" + id="checkbox6" + name="show2DDiagram" + onChange={this.handleCheckChange} + checked={this.state.show2DDiagram} + containerClass="mr-5" + /> +
    +
    +
    + +
    Customization
    +
    +

    Choose your theme

    + + + + + + + Default + + {fetchedUser?.accessories?.themes && + fetchedUser.accessories.themes.tids.map( + (tid, i) => { + let name = "Unnamed"; + switch (tid) { + case "9d88bda4657dcf17581ee91dfe6ab2a3": + name = "Alpha"; + break; + default: + name = "Unnamed"; + } + name += " Theme"; + return ( + + {tid} + + ); + } + )} + + + + +
    +
    + +
    Panel 3
    +
    +
    +
    +
    + {this.state.changeDetected && ( + + + Save Changes + + + )} +
    + {this.state.showProfilePicture && ( + + )} + + ); + } else { + if (!loggedUser) { + //>TODO The active component has to be set to the login component + window.open("/", "_self"); + } else { + return ( +
    +
    + Loading... +
    +
    + ); + } + } + } +} +//#endregion + +//#region > Redux Mapping +const mapStateToProps = (state) => ({ + loggedUser: state.auth.loggedUser, + fetchedUser: state.user.fetchedUser, +}); + +const mapDispatchToProps = (dispatch) => { + return { + saveSettings: (nextSettings) => dispatch(saveSettingsActions(nextSettings)), + readCache: (username) => dispatch(readCacheAction(username)), + }; +}; +//#endregion + +//#region > Exports +/** + * Provides its connected component with the pieces of the data it needs from + * the store, and the functions it can use to dispatch actions to the store. + */ +export default connect(mapStateToProps, mapDispatchToProps)(SettingsPage); +//#endregion + +/** + * SPDX-License-Identifier: (EUPL-1.2) + * Copyright © 2019-2020 Simon Prast + */ diff --git a/src/components/molecules/modals/SettingsModal/settings.scss b/src/components/pages/SettingsPage/settings.scss similarity index 70% rename from src/components/molecules/modals/SettingsModal/settings.scss rename to src/components/pages/SettingsPage/settings.scss index add85fc..ea1257b 100644 --- a/src/components/molecules/modals/SettingsModal/settings.scss +++ b/src/components/pages/SettingsPage/settings.scss @@ -1,6 +1,36 @@ #settings { + padding: 2%; + + .tab-content { + padding: 0; + text-align: left; + + .font-weight-bold { + margin-bottom: 0.4rem; + padding-bottom: 0; + border-bottom: 3px #77bd43 solid; + display: inline-block; + margin-top: 1rem; + } + } + .nav-item { padding: 0; + + img { + width: 50px; + height: 50px; + border-radius: 50%; + } + + .profile { + padding-left: 2%; + padding-bottom: 5%; + + .font-weight-bold { + margin-bottom: -3%; + } + } } .pills-primary .nav-link { @@ -26,19 +56,6 @@ } } - .tab-content { - padding: 0; - text-align: left; - - .font-weight-bold { - margin-bottom: 0.4rem; - padding-bottom: 0; - border-bottom: 3px #77bd43 solid; - display: inline-block; - margin-top: 1rem; - } - } - .form-check-input[type="checkbox"].filled-in:checked + label::after, label.btn input[type="checkbox"].filled-in:checked + label::after { background-color: #77bd43; @@ -51,6 +68,22 @@ margin-top: 0.4rem; } } + + .avatar { + height: 150px; + width: 150px; + background: white; + } +} + +#notification { + background-color: rgba($color: #77bd43, $alpha: 0.5); + color: black; + + .message { + padding-top: 1.5%; + padding-bottom: 1.5%; + } } /** diff --git a/src/components/pages/TalkPage/index.jsx b/src/components/pages/TalkPage/index.jsx index 20d886b..26a1175 100644 --- a/src/components/pages/TalkPage/index.jsx +++ b/src/components/pages/TalkPage/index.jsx @@ -20,7 +20,14 @@ import { MDBPageNav, MDBPagination, } from "mdbreact"; +//> Redux +// Allows to React components read data from a Redux store, and dispatch actions +// to the store to update data. +import { connect } from "react-redux"; +//> Actions +// Functions to send data from the application to the store +import { getTalkAction } from "../../../store/actions/userActions"; //> CSS import "./talk.scss"; //#endregion @@ -29,40 +36,33 @@ import "./talk.scss"; /** @class This component adds the Talk Page which displays a certain talk */ class TalkPage extends React.Component { state = { + loading: true, talk: undefined, }; componentDidMount = () => { - const { globalState, globalFunctions } = this.props; const { uid, username } = this.props.match?.params; - if ( - uid && - username && - !globalState.loading && - !globalState.fetchedUser && - globalState.fetchedUser !== false - ) { - if (this.state.talk === undefined) { - globalFunctions.getTalk(uid, username).then((talk) => { - talk.social = { - likes: 17, - date: new Date().toLocaleDateString("en-US", { - year: "numeric", - month: "numeric", - day: "numeric", - }), - }; + this.props.getTalk(uid, username).then(() => { + const { selectedTalk } = this.props; - talk.interval = { - timeoutID: setInterval(() => this.updateIframe(talk), 4000), - loaded: false, - }; + if (uid && username) { + selectedTalk.social = { + likes: 17, + date: new Date().toLocaleDateString("en-US", { + year: "numeric", + month: "numeric", + day: "numeric", + }), + }; - this.setState({ talk }); - }); + selectedTalk.interval = { + timeoutID: setInterval(() => this.updateIframe(selectedTalk), 4000), + loaded: false, + }; } - } + this.setState({ talk: selectedTalk, loading: false }); + }); }; updateIframe = (talk) => { @@ -123,7 +123,7 @@ class TalkPage extends React.Component { logo @@ -132,9 +132,9 @@ class TalkPage extends React.Component {

    - Owned by {talk.repository.owner.username} + Owned by {talk.repository?.owner.username}

    - {talk.repository.owner && ( + {talk.repository?.owner && (
    @@ -143,7 +143,7 @@ class TalkPage extends React.Component {
    )}

    - {talk.repository.description} + {talk.repository?.description}

    @@ -154,7 +154,7 @@ class TalkPage extends React.Component { - + Follow @@ -226,7 +226,7 @@ class TalkPage extends React.Component { {/* - {talk1.repository.readme} + {talk1.repository?.readme} */} @@ -377,9 +377,20 @@ class TalkPage extends React.Component { } //#endregion +const mapStateToProps = (state) => ({ + selectedTalk: state.user.selectedTalk, +}); + +const mapDispatchToProps = (dispatch) => { + return { getTalk: (uid, username) => dispatch(getTalkAction(uid, username)) }; +}; + //#region > Exports -//> Default Class -export default TalkPage; +/** + * Provides its connected component with the pieces of the data it needs from + * the store, and the functions it can use to dispatch actions to the store. + */ +export default connect(mapStateToProps, mapDispatchToProps)(TalkPage); //#endregion /** diff --git a/src/components/pages/TalkPage/talk.scss b/src/components/pages/TalkPage/talk.scss index 4c1ff16..4c00758 100644 --- a/src/components/pages/TalkPage/talk.scss +++ b/src/components/pages/TalkPage/talk.scss @@ -16,7 +16,6 @@ } .verified-badge { - .badge { background: rgba(0, 200, 81, 0.1) !important; border: 1px #00c851 solid; diff --git a/src/components/pages/index.js b/src/components/pages/index.js index d1847ab..eac3752 100644 --- a/src/components/pages/index.js +++ b/src/components/pages/index.js @@ -5,11 +5,12 @@ import CompanyPage from "./CompanyPage"; import HomePage from "./HomePage"; import ProfilePage from "./ProfilePage"; import TalkPage from "./TalkPage"; +import SettingsPage from "./SettingsPage"; //#endregion //#region > Exports //> Pages -export { CompanyPage, HomePage, ProfilePage, TalkPage }; +export { CompanyPage, HomePage, ProfilePage, TalkPage, SettingsPage }; //#endregion /** diff --git a/src/index.js b/src/index.js index 3f8cc71..5d500cf 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,17 @@ import React from "react"; import { BrowserRouter as Router } from "react-router-dom"; // This serves as an entry point to the DOM and server renderers for React import ReactDOM from "react-dom"; +//> Redux +// Allows to React components read data from a Redux store, and dispatch actions +// to the store to update data. +import { createStore, applyMiddleware, compose } from "redux"; +import { loadingBarMiddleware } from "react-redux-loading-bar"; +import { Provider } from "react-redux"; +// Thunk +import thunk from "redux-thunk"; +//> Intel +import { Intel } from "snek-intel"; + //> Font Awesome // Font Awesome is an awesome icon library import "@fortawesome/fontawesome-free/css/all.min.css"; @@ -21,15 +32,51 @@ import "./index.scss"; //> Components // Root component import App from "./App"; +//> Root Reducer +import rootReducer from "./store/reducers"; //> Service Worker import registerServiceWorker from "./registerServiceWorker"; //#endregion +//#region > Redux Store Initialization +const INTEL = new Intel(); + +//#TODO +// Must be moved to INTEL in future? +const getIntel = () => { + return INTEL; +}; + +const composeEnhancers = + typeof window === "object" && + process.env.NODE_ENV !== "production" && + window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ + ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ + // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize... + }) + : compose; + +const enhancer = composeEnhancers( + applyMiddleware( + loadingBarMiddleware(), + thunk.withExtraArgument({ + // Intel + getIntel, + }) + ) + // other store enhancers if any +); + +const STORE = createStore(rootReducer /* preloadedState, */, enhancer); +//#endregion + // Render the root component to
    ReactDOM.render( - - - , + + + + + , document.getElementById("root") ); diff --git a/src/store/actions/authActions.js b/src/store/actions/authActions.js new file mode 100644 index 0000000..06f5d95 --- /dev/null +++ b/src/store/actions/authActions.js @@ -0,0 +1,99 @@ +//#region > Authentication Actions +/** + * Handle login. + * + * @param user A user to login with + * @description Handles states for login + */ +const loginAction = (user) => { + return (dispatch, getState, { getIntel }) => { + try { + const intel = getIntel(); + const session = intel.snekclient.session; + + return session + .begin(user) + .then((whoami) => { + if (whoami?.username !== process.env.REACT_APP_ANONYMOUS_USER) { + dispatch({ + type: "LOGIN_SUCCESS", + payload: { + username: whoami.username, + avatarUrl: + "https://www.clipartmax.com/png/full/166-1669056_the-20-cooler-octocat-github-octocat.png", + }, + }); + } else { + dispatch({ + type: "LOGIN_ANON_SUCCESS", + payload: {}, + }); + } + }) + .catch((ex) => + dispatch({ + type: "LOGIN_FAILED", + payload: { + errorCode: 619, + message: "Incorrect username or password", + error: ex, + }, + }) + ); + } catch (ex) { + dispatch({ + type: "LOGIN_ERROR", + payload: { errorCode: 600, message: "Login failed", error: ex }, + }); + } + }; +}; + +/** + * Logout user. + * + * @description Handles the logging out of active users + */ +const logoutAction = () => { + return async (dispatch, getState, { getIntel }) => { + try { + const intel = getIntel(); + const session = intel.snekclient.session; + + await session + .end() + .then(() => { + dispatch({ + type: "LOGOUT_SUCCESS", + payload: {}, + }); + + dispatch({ + type: "REMOVE_LOGGED_USER", + }); + }) + .catch((ex) => + dispatch({ type: "LOGOUT_ERROR", payload: { error: ex } }) + ); + } catch (ex) { + dispatch({ + type: "LOGOUT_FAILED", + payload: { + errorCode: 601, + message: "Logout failed", + error: ex, + }, + }); + } + }; +}; +//#endregion + +//#region > Exports +export { loginAction, logoutAction }; +//#endregion + +/** + * SPDX-License-Identifier: (EUPL-1.2) + * Copyright © 2019-2020 Simon Prast + */ diff --git a/src/store/actions/generalActions.js b/src/store/actions/generalActions.js new file mode 100644 index 0000000..761fe1a --- /dev/null +++ b/src/store/actions/generalActions.js @@ -0,0 +1,162 @@ +//#region > Registration Actions +/** + * Append Source Objects + * + * @param sourceList A source object + * @description Hands source list over to intel + */ +const appendSourceObjectsAction = (sourceList) => { + return async (dispatch, getState, { getIntel }) => { + try { + const intel = getIntel(); + + intel.appendList(sourceList); + + dispatch({ + type: "APPEND_SOURCE_OBJECTS_SUCCESS", + payload: {}, + }); + } catch (ex) { + dispatch({ + type: "APPEND_SOURCE_OBJECTS_ERROR", + payload: { + errorCode: 602, + message: "Appending source objects failed", + error: ex, + }, + }); + } + }; +}; + +/** + * Fetch GitLab Servers + * + * @description Retrieves a list of available GitLab servers + */ +const fetchGitLabServersAction = () => { + return async (dispatch, getState, { getIntel }) => { + try { + const intel = getIntel(); + const session = intel.snekclient.session; + + return session.tasks.general + .gitlabServer() + .then(({ data }) => { + const gitLabServers = data?.page?.supportedGitlabs; + + dispatch({ + type: "FETCH_GITLAB_SERVER_SUCCESS", + payload: { gitLabServers }, + }); + }) + .catch((ex) => + dispatch({ + type: "FETCH_GITLAB_SERVER_ERROR", + payload: { error: ex }, + }) + ); + } catch (ex) { + dispatch({ + type: "FETCH_GITLAB_SERVER_ERROR", + payload: { + errorCode: 605, + message: "Fetching GitLab server failed", + error: ex, + }, + }); + } + }; +}; +//#endregion + +//#region > Data Handling Actions +/** + * Get intel data + * + * @description Retrieves data from current applied source list + */ +const getDataAction = () => { + return async (dispatch, getState, { getIntel }) => { + try { + const intel = getIntel(); + + intel + .get() + .then((res) => dispatch({ type: "GET_DATA_SUCCESS", payload: res })) + .catch((ex) => + dispatch({ type: "GET_DATA_ERROR", payload: { error: ex } }) + ); + } catch (ex) { + dispatch({ + type: "GET_DATA_ERROR", + payload: { + errorCode: 603, + message: "Getting intel data failed", + error: ex, + }, + }); + } + }; +}; + +/** + * Get all users + * + * @description Retrieves a list of all users + */ +const getAllPageUrlsAction = () => { + return async (dispatch, getState, { getIntel }) => { + try { + const intel = getIntel(); + const session = intel.snekclient.session; + + return await session.tasks.general + .allPageUrls() + .then((res) => { + let urls = []; + + res.data.pages && + res.data.pages.forEach((page) => { + if (page.urlPath.includes("registration/")) { + let url = page.urlPath.split("/")[2]; + + urls.push(url); + } + }); + + dispatch({ + type: "GET_APP_PAGE_URLS_SUCCESS", + payload: { urls }, + }); + }) + .catch((ex) => + dispatch({ type: "GET_APP_PAGE_URLS_ERROR", payload: { error: ex } }) + ); + } catch (ex) { + dispatch({ + type: "GET_APP_PAGE_URLS_ERROR", + payload: { + errorCode: 616, + message: "Getting all page urls failed", + error: ex, + }, + }); + } + }; +}; +//#endregion + +//#region > Exports +export { + appendSourceObjectsAction, + getDataAction, + getAllPageUrlsAction, + fetchGitLabServersAction, +}; +//#endregion + +/** + * SPDX-License-Identifier: (EUPL-1.2) + * Copyright © 2019-2020 Simon Prast + */ diff --git a/src/store/actions/userActions.js b/src/store/actions/userActions.js new file mode 100644 index 0000000..3b1af48 --- /dev/null +++ b/src/store/actions/userActions.js @@ -0,0 +1,594 @@ +//#region > Imports +//> Additional +// SHA Hashing algorithm +import sha256 from "js-sha256"; +//#endregion +import { showLoading, hideLoading } from "react-redux-loading-bar"; + +//#region > Register Actions +/** + * Register user. + * + * @param registrationData Data to register a user + * @description Handles the registration of users + */ +const registerAction = (registrationData) => { + return async (dispatch, getState, { getIntel }) => { + try { + const intel = getIntel(); + const session = intel.snekclient.session; + const clearPassword = registrationData.password; + + // Hash password + registrationData.password = sha256(registrationData.password); + + // Append Source objects + await intel.appendList(registrationData.sources); + // Generate talks based on the previous appended list + await intel.generateTalks(registrationData.sources); + + // Get fresh platform data + const intelData = { + ...(await intel.get()), + talks: await intel.getTalks(), + }; + + // Save Object to platformData as JSON + registrationData.platform_data = JSON.stringify(intelData); + // Create JSON string out of sources for backend use + registrationData.sources = JSON.stringify(registrationData.sources); + + return session.tasks.user + .registration(registrationData) + .then((res) => { + if (res.result === "FAIL") { + dispatch({ + type: "SIGNUP_FAILED", + payload: { + errorCode: 606, + message: "Registration failed due to false registration result", + error: undefined, + }, + }); + } else { + dispatch({ + type: "SIGNUP_SUCCESS", + payload: { + registrationData, + username: registrationData.username, + password: clearPassword, + }, + }); + } + }) + .catch((ex) => + dispatch({ type: "SIGNUP_ERROR", payload: { error: ex } }) + ); + } catch (ex) { + dispatch({ + type: "SIGNUP_ERROR", + payload: { + errorCode: 617, + message: "Registration failed", + error: ex, + }, + }); + } + }; +}; +//#endregion + +//#region > Caching Actions +/** + * Write cache. + * + * @param platformData A data object to store in cache + * @description Handles the calls for writing to cache + */ +const writeCacheAction = (platformData) => { + return async (dispatch, getState, { getIntel }) => { + try { + const intel = getIntel(); + const session = intel.snekclient.session; + + return session.tasks.user + .cache(JSON.stringify(platformData)) + .then(() => + dispatch({ + type: "WRITE_CACHE_SUCCESS", + payload: {}, + }) + ) + .catch((ex) => + dispatch({ type: "WRITE_CACHE_ERROR", payload: { error: ex } }) + ); + } catch (ex) { + dispatch({ + type: "WRITE_CACHE_ERROR", + payload: { + errorCode: 607, + message: "Writing to cache failed", + error: ex, + }, + }); + } + }; +}; + +/** + * Read cache. + * + * @param username A username to read the cache from + * @description Handles the calls for reading the cache + */ +const readCacheAction = (username) => { + return async (dispatch, getState, { getIntel }) => { + try { + const intel = getIntel(); + const session = intel.snekclient.session; + + return session.tasks.user + .profile("/registration/" + username) + .then(async ({ data }) => { + if (!data.profile) { + dispatch({ + type: "READ_CACHE_FAILED", + payload: { + errorCode: 608, + message: "Cache not loaded", + error: undefined, + }, + }); + } else { + // Split profile to chunks + const profile = data.profile; + const sources = profile.sources + ? JSON.parse(profile.sources) + : null; + + let platformData = profile.platformData + ? JSON.parse(profile.platformData) + : {}; + + let user = platformData.user ? platformData.user : {}; + + // Check if data is valid + if (!sources) { + dispatch({ + type: "READ_CACHE_FAILED", + payload: { + errorCode: 609, + message: "Sources are empty", + error: undefined, + }, + }); + } else { + // Set settings for first time fetching + if ( + ![ + "firstName", + "lastName", + "email", + "avatarUrl", + "websiteUrl", + "websiteUrl", + "location", + "company", + ].every((item) => user.hasOwnProperty(item)) + ) { + user.firstName = platformData.profile.firstName; + user.lastName = platformData.profile.lastName; + user.email = platformData.profile.email; + user.avatarUrl = platformData.profile.avatarUrl; + user.websiteUrl = platformData.profile.websiteUrl; + user.location = platformData.profile.location; + user.company = platformData.profile.company; + } + + if (!user.settings) { + user.settings = { + show3DDiagram: true, + show2DDiagram: true, + showCompanyPublic: true, + showEmailPublic: true, + showLocalRanking: true, + activeTheme: null, + }; + } + + // Build fetchedUser object + let fetchedUser = { + username: profile.username, + platformData: { + ...platformData, + user, + }, + sources, + verified: data.profile.verified, + accessories: { + badges: data.profile.bids + ? JSON.parse(data.profile.bids) + : null, + themes: data.profile.tids + ? JSON.parse(data.profile.tids) + : null, + }, + }; + + dispatch({ + type: "READ_CACHE_SUCCESS", + payload: { fetchedUser }, + }); + + const state = getState(); + + if (fetchedUser.username === state.auth.loggedUser?.username) { + dispatch({ + type: "SET_LOGGED_USER", + payload: { fetchedUser }, + }); + } + } + } + }) + .catch((ex) => + dispatch({ type: "READ_CACHE_ERROR", payload: { error: ex } }) + ); + } catch (ex) { + dispatch({ + type: "READ_CACHE_ERROR", + payload: { + errorCode: 610, + message: "Reading from cache failed", + error: ex, + }, + }); + } + }; +}; + +/** + * Update cache. + * + * @param fetchedUser A fetched user object + * @description Handles the calls for updating the cache + */ +const updateCacheAction = (fetchedUser) => { + return async (dispatch, getState, { getIntel }) => { + try { + dispatch(showLoading()); + + const intel = getIntel(); + const session = intel.snekclient.session; + + // Appned Source objects + await intel.appendList(fetchedUser.sources); + // Generate talks based on the previous appended list + await intel.generateTalks(fetchedUser.sources); + + // Get fresh platform data + const intelData = { + ...(await intel.get()), + talks: await intel.getTalks(), + }; + + // Fix duplicates + for (const i in intelData.talks) { + let state = true; + + for (const i2 in fetchedUser.platformData.talks) { + if ( + intelData.talks[i].url === fetchedUser.platformData.talks[i2].url + ) { + state = false; + } + } + + if (state) { + fetchedUser.platformData.talks.push(intelData.talks[i]); + } + } + + fetchedUser.platformData = { + ...intelData, + user: fetchedUser.platformData.user, + talks: fetchedUser.platformData.talks, + }; + + intel.resetReducer(); + + dispatch(hideLoading()); + + return session.tasks.user + .cache(JSON.stringify(fetchedUser.platformData)) + .then( + dispatch({ + type: "UPDATE_CACHE_SUCCESS", + payload: { fetchedUser }, + }) + ) + .catch((ex) => + dispatch({ type: "UPDATE_CACHE_ERROR", payload: { error: ex } }) + ); + } catch (ex) { + dispatch({ + type: "UPDATE_CACHE_ERROR", + payload: { + errorCode: 618, + message: "Updating cache failed", + error: ex, + }, + }); + } + }; +}; +/** + * Save settings + * + * @param nextSettings The settings that should be applied + * @description Saves the user settings + */ +const saveSettingsActions = (nextSettings) => { + return async (dispatch, getState, { getIntel }) => { + try { + const intel = getIntel(); + const state = getState(); + + const fetchedUser = state.user.fetchedUser; + const session = intel.snekclient.session; + + if (fetchedUser.platformData) { + // Check for mandatory fields + if (nextSettings.email) { + fetchedUser.platformData.user.avatarUrl = nextSettings.avatar_url + ? nextSettings.avatar_url + : ""; + fetchedUser.platformData.user.firstName = nextSettings.first_name + ? nextSettings.first_name + : ""; + fetchedUser.platformData.user.lastName = nextSettings.last_name + ? nextSettings.last_name + : ""; + fetchedUser.platformData.user.email = nextSettings.email + ? nextSettings.email + : fetchedUser.platformData.user.email; + fetchedUser.platformData.user.websiteUrl = nextSettings.website + ? nextSettings.website + : ""; + fetchedUser.platformData.user.location = nextSettings.location + ? nextSettings.location + : ""; + fetchedUser.platformData.user.company = nextSettings.company + ? nextSettings.company + : ""; + fetchedUser.platformData.user.settings = { + showTopLanguages: nextSettings.showTopLanguages, + showLocalRanking: nextSettings.showLocalRanking, + show3DDiagram: nextSettings.show3DDiagram, + show2DDiagram: nextSettings.show2DDiagram, + showEmailPublic: nextSettings.showEmailPublic, + showCompanyPublic: nextSettings.showCompanyPublic, + activeTheme: nextSettings.activeTheme, + }; + } + + session.tasks.user.cache(JSON.stringify(fetchedUser.platformData)); + + dispatch({ + type: "SAVE_SETTING_SUCCESS", + payload: { + fetchedUser, + }, + }); + } + } catch (ex) { + dispatch({ + type: "SAVE_SETTING_ERROR", + payload: { + errorCode: 604, + message: "Saving settings failed", + error: ex, + }, + }); + } + }; +}; + +//#endregion + +//#region > Talks Actions +//> Is currently not needed but has been left for later implementations! +// /** +// * Get all talks. +// * +// * @description Handles the call for getting all talks. +// */ +// const getAllTalksAction = () => { +// return async (dispatch, getState, { getIntel }) => { +// try { +// const intel = getIntel(); + +// return intel +// .getTalks() +// .then((talks) => +// dispatch({ type: "GET_TALKS_SUCCESS", payload: { talks } }) +// ) +// .catch((ex) => +// dispatch({ type: "GET_TALKS_ERROR", payload: { error: ex } }) +// ); +// } catch (ex) { +// dispatch({ +// type: "GET_TALKS_ERROR", +// payload: { +// errorCode: 611, +// message: "Getting intel talks failed", +// error: ex, +// }, +// }); +// } +// }; +// }; + +/** + * Get a talk. + * + * @param uid A unique id to find a talk + * @param username A username associated with the talk + * @description Handles the call for getting one specific talk + */ +const getTalkAction = (uid, username) => { + return async (dispatch, getState, { getIntel }) => { + try { + const intel = getIntel(); + const session = intel.snekclient.session; + + return session.tasks.user + .profile("/registration/" + username) + .then(async ({ data }) => { + if (data.profile) { + let talks = JSON.parse(data.profile.platformData).talks; + + talks = talks.filter((talk) => { + return talk.uid === uid; + }); + + dispatch({ + type: "GET_TALK_SUCCESS", + payload: { talk: talks[0] }, + }); + } else { + dispatch({ + type: "GET_TALK_FAILED", + payload: { + errorCode: 613, + message: "Cannot get specific talk " + uid, + error: undefined, + }, + }); + } + }) + .catch((ex) => + dispatch({ type: "GET_TALK_ERROR", payload: { error: ex } }) + ); + } catch (ex) { + dispatch({ + type: "GET_TALK_ERROR", + payload: { + errorCode: 614, + message: "Getting talks failed", + error: ex, + }, + }); + } + }; +}; + +/** + * Upload talk. + * + * @param file A file to be uploaded + * @param talkInfo Additional information to add to the talk + * @description Handles the call for uploading a talk + */ +const uploadTalkAction = (file, talkInfo) => { + return async (dispatch, getState, { getIntel }) => { + try { + const intel = getIntel(); + const state = getState(); + + const fetchedUser = state.user.fetchedUser; + const session = intel.snekclient.session; + + return intel.appendTalk(file).then(() => { + return intel.getTalks().then((talks) => { + talks[talks.length - 1].repository = talkInfo; + + fetchedUser.platformData.talks.push(talks[talks.length - 1]); + + session.tasks.user + .cache(JSON.stringify(fetchedUser.platformData)) + .then(() => + dispatch({ + type: "UPLOAD_TALK_SUCCESS", + payload: { + fetchedUser, + }, + }) + ); + }); + }); + } catch (ex) { + dispatch({ + type: "UPLOAD_TALK_ERROR", + payload: { + errorCode: 612, + message: "Uploading talk failed", + error: ex, + }, + }); + } + }; +}; + +/** + * @todo currentCache over getState + * Delete talk. + * + * @param talk A talk that should be deleted + * @description Handles the call for deleting a talk. + */ +const deleteTalkAction = (talk) => { + return async (dispatch, getState, { getIntel }) => { + try { + const intel = getIntel(); + const state = getState(); + + const fetchedUser = state.user.fetchedUser; + const session = intel.snekclient.session; + + for (const index in fetchedUser.platformData.talks) { + if (talk.uid === fetchedUser.platformData.talks[index].uid) { + fetchedUser.platformData.talks.splice(index, 1); + } + } + + session.tasks.user.cache(JSON.stringify(fetchedUser.platformData)); + + dispatch({ + type: "DELETING_TALK_SUCCESS", + payload: { + fetchedUser, + }, + }); + } catch (ex) { + dispatch({ + type: "DELETING_TALK_ERROR", + payload: { + errorCode: 615, + message: "Deleting talk failed", + raw: ex, + }, + }); + } + }; +}; +//#endregion + +//#region > Exports +export { + registerAction, + writeCacheAction, + readCacheAction, + updateCacheAction, + saveSettingsActions, + //getAllTalksAction, + getTalkAction, + uploadTalkAction, + deleteTalkAction, +}; +//#endregion + +/** + * SPDX-License-Identifier: (EUPL-1.2) + * Copyright © 2019-2020 Simon Prast + */ diff --git a/src/store/reducers/authReducers.js b/src/store/reducers/authReducers.js new file mode 100644 index 0000000..a6bbec9 --- /dev/null +++ b/src/store/reducers/authReducers.js @@ -0,0 +1,60 @@ +//#region > Imports +//> Error Serialization +// Serialize/deserialize an error into a plain object +import { serializeError } from "serialize-error"; +//#endregion + +//#region > Constant Variables +const INIT_STATE = { + loggedUser: { anonymous: true }, + authError: null, + authErrorDetails: null, +}; +//#endregion + +//#region > Reducers +const authReducer = (state = INIT_STATE, action) => { + switch (action.type) { + //> LOGIN + case "LOGIN_SUCCESS": + return { + ...state, + loggedUser: { ...action.payload, anonymous: false }, + authErrorDetails: null, + }; + + case "LOGIN_ANON_SUCCESS": + return { + ...state, + loggedUser: undefined, + loggedUser: { anonymous: true }, + authErrorDetails: null, + }; + + case "LOGIN_FAILED" || "LOGIN_ERROR": + return { + ...state, + authError: action.payload, + authErrorDetails: serializeError(action.payload.error), + }; + + //> LOGOUT + case "LOGOUT_SUCCESS": + return INIT_STATE; + + case "LOGOUT_FAILED" || "LOGIN_ERROR": + return { + ...state, + authError: action.payload, + authErrorDetails: serializeError(action.payload.error), + }; + + default: + return state; + } +}; +//#endregion + +//#region > Exports +export default authReducer; +//#endregion diff --git a/src/store/reducers/generalReducers.js b/src/store/reducers/generalReducers.js new file mode 100644 index 0000000..c5e06b2 --- /dev/null +++ b/src/store/reducers/generalReducers.js @@ -0,0 +1,83 @@ +//#region > Imports +//> Error Serialization +// Serialize/deserialize an error into a plain object +import { serializeError } from "serialize-error"; +//#endregion + +//#region > Constant Variables +const INIT_STATE = { + allRegisteredUsernames: null, + allGitlabServers: [], + generatedIntelData: null, + generalError: null, + generalErrorDetails: null, +}; +//#endregion + +//#region > Reducers +const generalReducer = (state = INIT_STATE, action) => { + switch (action.type) { + //> appendSourceObjectsAction + case "APPEND_SOURCE_OBJECTS_SUCCESS": + return { + ...state, + }; + + case "APPEND_SOURCE_OBJECTS_ERROR": + return { + ...state, + authError: action.payload, + authErrorDetails: serializeError(action.payload.error), + }; + + //> fetchGitLabServersAction + case "FETCH_GITLAB_SERVER_SUCCESS": + return { + ...state, + allGitlabServers: action.payload.gitLabServers, + }; + + case "FETCH_GITLAB_SERVER_ERROR": + return { + ...state, + authError: action.payload, + authErrorDetails: serializeError(action.payload.error), + }; + + //> getDataAction + case "GET_DATA_SUCCESS": + return { + ...state, + generatedIntelData: action.payload, + }; + + case "GET_DATA_ERROR": + return { + ...state, + authError: action.payload, + authErrorDetails: serializeError(action.payload.error), + }; + + //> getAllPageUrlsAction + case "GET_APP_PAGE_URLS_SUCCESS": + return { + ...state, + allRegisteredUsernames: action.payload.urls, + }; + + case "GET_APP_PAGE_URLS_ERROR": + return { + ...state, + authError: action.payload, + authErrorDetails: serializeError(action.payload.error), + }; + + default: + return state; + } +}; +//#endregion + +//#region > Exports +export default generalReducer; +//#endregion diff --git a/src/store/reducers/index.js b/src/store/reducers/index.js new file mode 100644 index 0000000..b34a270 --- /dev/null +++ b/src/store/reducers/index.js @@ -0,0 +1,33 @@ +//#region > Imports +//> Redux +// Allows to React components read data from a Redux store, and dispatch actions +// to the store to update data. +import { combineReducers } from "redux"; +// LoadingBar +import { loadingBarReducer } from "react-redux-loading-bar"; + +//> Reducers +// Authentication +import authReducer from "./authReducers"; +// General +import generalReducer from "./generalReducers"; +// User +import userReducer from "./userReducers"; +//#endregion + +//#region > Config +const rootReducer = combineReducers({ + // Loading bar + loadingBar: loadingBarReducer, + // User authentication + auth: authReducer, + // General data + general: generalReducer, + // User data + user: userReducer, +}); +//#endregion + +//#region > Exports +export default rootReducer; +//#endregion diff --git a/src/store/reducers/userReducers.js b/src/store/reducers/userReducers.js new file mode 100644 index 0000000..f5caabc --- /dev/null +++ b/src/store/reducers/userReducers.js @@ -0,0 +1,158 @@ +//#region > Imports +//> Error Serialization +// Serialize/deserialize an error into a plain object +import { serializeError } from "serialize-error"; +//#endregion + +//#region > Constant Variables +const INIT_STATE = { + fetchedUser: null, + loggedUser: null, + registrationHistory: null, + selectedTalk: null, + newCachedUser: null, + cachingDone: false, + userError: null, + userErrorDetails: null, +}; +//#endregion + +//#region > User Reducers +const userReducer = (state = INIT_STATE, action) => { + switch (action.type) { + //> registerAction + case "SIGNUP_SUCCESS": + return { + ...state, + registrationHistory: { ...action.payload }, + }; + + case "SIGNUP_FAILED" || "SIGNUP_ERROR": + return { + ...state, + authError: action.payload, + authErrorDetails: serializeError(action.payload.error), + }; + + //> writeCacheAction + case "WRITE_CACHE_SUCCESS": + return { + ...state, + }; + + case "WRITE_CACHE_ERROR": + return { + ...state, + authError: action.payload, + authErrorDetails: serializeError(action.payload.error), + }; + + //> readCacheAction + case "READ_CACHE_SUCCESS": + return { + ...state, + fetchedUser: { ...action.payload.fetchedUser }, + }; + + case "READ_CACHE_FAILED" || "READ_CACHE_ERROR": + return { + ...state, + authError: action.payload, + authErrorDetails: serializeError(action.payload.error), + }; + + //> updateCacheAction + case "UPDATE_CACHE_SUCCESS": + return { + ...state, + newCachedUser: { ...action.payload.fetchedUser }, + cachingDone: true, + }; + + case "UPDATE_CACHE_ERROR": + return { + ...state, + authError: action.payload, + authErrorDetails: serializeError(action.payload.error), + }; + + //> saveSettingsActions + case "SAVE_SETTING_SUCCESS": + return { + ...state, + fetchedUser: { ...action.payload.fetchedUser }, + }; + + case "SAVE_SETTING_ERROR": + return { + ...state, + authError: action.payload, + authErrorDetails: serializeError(action.payload.error), + }; + + //> getTalkAction + case "GET_TALK_SUCCESS": + return { + ...state, + selectedTalk: { ...action.payload.talk }, + }; + + case "GET_TALK_FAILED" || "GET_TALK_ERROR": + return { + ...state, + authError: action.payload, + authErrorDetails: serializeError(action.payload.error), + }; + + //> uploadTalkAction + case "UPLOAD_TALK_SUCCESS": + return { + ...state, + fetchedUser: { ...action.payload.fetchedUser }, + }; + + case "UPLOAD_TALK_ERROR": + return { + ...state, + authError: action.payload, + authErrorDetails: serializeError(action.payload.error), + }; + + //> deleteTalkAction + case "DELETING_TALK_SUCCESS": + return { + ...state, + fetchedUser: { ...action.payload.fetchedUser }, + }; + + case "DELETING_TALK_ERROR": + return { + ...state, + authError: action.payload, + authErrorDetails: serializeError(action.payload.error), + }; + + /** Temporary implementation */ + //> loggedUser + case "SET_LOGGED_USER": + return { + ...state, + loggedUser: action.payload.fetchedUser, + }; + + case "REMOVE_LOGGED_USER": + return { + ...state, + loggedUser: null, + }; + + //> Default + default: + return state; + } +}; +//#endregion + +//#region > Exports +export default userReducer; +//#endregion diff --git a/src/utilities/_base.scss b/src/utilities/_base.scss index e27c219..37b3c5c 100644 --- a/src/utilities/_base.scss +++ b/src/utilities/_base.scss @@ -12,7 +12,7 @@ // Buttons .btn { box-shadow: none; - border-radius: 0; + border-radius: 0.4rem; text-transform: initial; i { @@ -37,13 +37,47 @@ //> Button types // Button green + &.btn-outline-green { + background-color: transparentize( + $color: $snekGreen, + $amount: 0.9 + ) !important; + border: 2px $snekGreen solid; + color: darken($snekGreen, 10%); + + &:hover { + background-color: transparentize( + $color: $snekGreen, + $amount: 0.8 + ) !important; + } + } + &.btn-green { - background-color: #77bd43 !important; + background-color: transparentize( + $color: $snekGreen, + $amount: 0.4 + ) !important; + border: 2px $snekGreen solid; + + &:hover { + background-color: transparentize( + $color: $snekGreen, + $amount: 0.3 + ) !important; + } + } + + &.btn-outline-primary { + background-color: transparentize($color: $blue, $amount: 0.9) !important; + border: 2px $blue solid; + color: darken($blue, 10%); &:hover { - background-color: darken(#77bd43, 5%) !important; + background-color: transparentize($color: $blue, $amount: 0.8) !important; } } + // Button WhatsApp &.btn-wa { background-color: #25d366 !important; @@ -60,21 +94,27 @@ // Custom colors .snek-dark { background-color: $snekDark !important; + &.darken-1 { background-color: darken($snekDark, 5%) !important; } + &.darken-2 { background-color: darken($snekDark, 10%) !important; } + &.darken-3 { background-color: darken($snekDark, 15%) !important; } + &.lighten-1 { background-color: lighten($snekDark, 10%) !important; } + &.lighten-2 { background-color: lighten($snekDark, 20%) !important; } + &.lighten-3 { background-color: lighten($snekDark, 30%) !important; } @@ -143,12 +183,10 @@ // Google icon .fa-google { - background: - - conic-gradient( + background: conic-gradient( from -45deg, #ea4335 110deg, - #4285f4 90deg 180deg, + $blue 90deg 180deg, #34a853 180deg 270deg, #fbbc05 270deg ) @@ -172,6 +210,15 @@ } } +// Loading bar +.loading { + z-index: 99999; + height: 3px; + width: auto; + position: fixed; + background-color: #4285f4; +} + /** * SPDX-License-Identifier: (EUPL-1.2) * Copyright © 2019-2020 Simon Prast diff --git a/src/utilities/_variables.scss b/src/utilities/_variables.scss index 872b0e7..ee2e447 100644 --- a/src/utilities/_variables.scss +++ b/src/utilities/_variables.scss @@ -1,7 +1,12 @@ +// SNEK $snekDark: #24292e; $snekGreen: #77bd43; -/* Themes */ +// Generic +$blue: #4285f4; +$secondary: #a6c; + +// Themes $themeAlphaHighlight: #a6c; $themeAlphaHighlightText: #fff; $themeAlphaBackground: #a6c;