From ccbcc727330c1302c3ec28d8c3753eeef4c1815b Mon Sep 17 00:00:00 2001 From: Oleksandr Trypolskyi <37958052+trypolski@users.noreply.github.com> Date: Tue, 12 Jun 2018 17:38:34 +0300 Subject: [PATCH] because the task it was interesting --- src/app/Bookmarks/scenes/Overview/Overview.js | 25 +-- src/app/actions/index.js | 98 +++++++++++ src/app/additional/additionalFunc.js | 81 ++++++++++ src/app/components/BookmarkApp.js | 15 ++ .../components/Columns/BookmarkAddColumn.js | 71 ++++++++ src/app/components/Columns/BookmarkCard.js | 153 ++++++++++++++++++ src/app/components/Columns/BookmarkColumn.js | 22 +++ .../components/Columns/BookmarkColumnHead.js | 50 ++++++ .../components/Columns/BookmarkContainer.js | 26 +++ src/app/components/DropDown/DropDown.js | 23 +++ src/app/components/DropDown/DropDownButton.js | 44 +++++ src/app/components/DropDown/InfoString.js | 40 +++++ src/app/components/Search/SearchInput.js | 73 +++++++++ src/app/components/Search/SearchItem.js | 58 +++++++ src/app/components/Search/SearchItemsList.js | 23 +++ .../containers/Columns/DndBookmarkColumn.js | 54 +++++++ .../Columns/DndBookmarkColumnContainer.js | 15 ++ .../Columns/DndBookmarkColumnCover.js | 55 +++++++ .../Columns/ShowBookmarkAddColumn.js | 64 ++++++++ .../containers/Columns/ShowBookmarkCard.js | 96 +++++++++++ .../Columns/ShowBookmarkColumnHead.js | 60 +++++++ .../Columns/ShowBookmarkContainer.js | 14 ++ .../containers/DropDown/DropDownInfoString.js | 19 +++ src/app/containers/DropDown/ShowDropDown.js | 56 +++++++ .../containers/DropDown/ShowDropDownButton.js | 46 ++++++ src/app/containers/Search/ShowSearchInput.js | 39 +++++ src/app/global.js | 17 +- src/app/index.js | 7 +- src/app/reducer.js | 7 +- src/app/reducers/bookmarks.js | 44 +++++ src/app/reducers/search.js | 26 +++ src/app/store.js | 8 + 32 files changed, 1386 insertions(+), 43 deletions(-) create mode 100644 src/app/actions/index.js create mode 100644 src/app/additional/additionalFunc.js create mode 100644 src/app/components/BookmarkApp.js create mode 100644 src/app/components/Columns/BookmarkAddColumn.js create mode 100644 src/app/components/Columns/BookmarkCard.js create mode 100644 src/app/components/Columns/BookmarkColumn.js create mode 100644 src/app/components/Columns/BookmarkColumnHead.js create mode 100644 src/app/components/Columns/BookmarkContainer.js create mode 100644 src/app/components/DropDown/DropDown.js create mode 100644 src/app/components/DropDown/DropDownButton.js create mode 100644 src/app/components/DropDown/InfoString.js create mode 100644 src/app/components/Search/SearchInput.js create mode 100644 src/app/components/Search/SearchItem.js create mode 100644 src/app/components/Search/SearchItemsList.js create mode 100644 src/app/containers/Columns/DndBookmarkColumn.js create mode 100644 src/app/containers/Columns/DndBookmarkColumnContainer.js create mode 100644 src/app/containers/Columns/DndBookmarkColumnCover.js create mode 100644 src/app/containers/Columns/ShowBookmarkAddColumn.js create mode 100644 src/app/containers/Columns/ShowBookmarkCard.js create mode 100644 src/app/containers/Columns/ShowBookmarkColumnHead.js create mode 100644 src/app/containers/Columns/ShowBookmarkContainer.js create mode 100644 src/app/containers/DropDown/DropDownInfoString.js create mode 100644 src/app/containers/DropDown/ShowDropDown.js create mode 100644 src/app/containers/DropDown/ShowDropDownButton.js create mode 100644 src/app/containers/Search/ShowSearchInput.js create mode 100644 src/app/reducers/bookmarks.js create mode 100644 src/app/reducers/search.js diff --git a/src/app/Bookmarks/scenes/Overview/Overview.js b/src/app/Bookmarks/scenes/Overview/Overview.js index 548f079..324980b 100644 --- a/src/app/Bookmarks/scenes/Overview/Overview.js +++ b/src/app/Bookmarks/scenes/Overview/Overview.js @@ -1,29 +1,10 @@ import React from 'react' -import styled from 'styled-components' +import BookmarkApp from '../../../components/BookmarkApp' + export default class Home extends React.PureComponent { render () { return ( - - - Happy Coding! - - - + ) } } - -const Wrapper = styled.div` - display: flex; - height: 100%; - align-items: center; - justify-content: center; -` - -const Message = styled.div` - font-size: 62px; -` - -const Icon = styled.div` - -` diff --git a/src/app/actions/index.js b/src/app/actions/index.js new file mode 100644 index 0000000..9afd5d8 --- /dev/null +++ b/src/app/actions/index.js @@ -0,0 +1,98 @@ +// import fetch from 'cross-fetch' +import axios from 'axios' + +export const SEARCH_ONCHANGE = 'SEARCH_ONCHANGE' +export const SEARCH_RECEIVE_DATA = 'SEARCH_RECEIVE_DATA' +export const DROPDOWN_SHOW = 'DROPDOWN_SHOW' +export const DROPDOWN_HIDE = 'DROPDOWN_HIDE' + +export const ADD_BOOKMARK = 'ADD_BOOKMARK' +export const DELETE_BOOKMARK = 'DELETE_BOOKMARK' +export const MOVE_BOOKMARK = 'MOVE_BOOKMARK' + +export const COLUMN_NAME_ONCHANGE = 'COLUMN_NAME_ONCHANGE' +export const ADD_COLUMN = 'ADD_COLUMN' +export const DELETE_COLUMN = 'DELETE_COLUMN' + +// Changing value of search input +export const searchOnchange = text => ({ + type: SEARCH_ONCHANGE, + data: text +}) + +// Received data of 5 first results +export const searchReaceiveData = json => ({ + type: SEARCH_RECEIVE_DATA, + items: json +}) + +// Also we can get data from GitHUB API using fetch +// export const searchFetchQuery = text => dispatch => { +// return fetch(`https://api.github.com/search/repositories?q=${text}+in:name&sort=stars`) +// .then(response => response.json()) +// .then(json => dispatch(searchReaceiveData(json.items.slice(0, 5)))) +// } + +// Fetching data from GitHUB API +export const searchFetchQuery = text => dispatch => { + return axios.get(`https://api.github.com/search/repositories?q=${text}+in:name&sort=stars`) + .then(function (response) { + // here we can change results amount + dispatch(searchReaceiveData(response.data.items.slice(0, 5))) + }) + .catch(function (error) { + console.log(error) + }) +} + +// Showing dropdown box +export const dropdownShow = () => ({ + type: DROPDOWN_SHOW +}) + +// Hiding dropdown box +export const dropdownHide = () => ({ + type: DROPDOWN_HIDE +}) + +// Adding new bookmark +export const addBookmark = item => ({ + type: ADD_BOOKMARK, + item: item +}) + +// Deleting bookmark +export const deleteBookmark = (id, columnName) => ({ + type: DELETE_BOOKMARK, + id: id, + column: columnName +}) + +// Moving bookmark +export const moveBookmark = (id, idColumnName, indexOfId, atIndexColumnName, indexOfatIndex) => ({ + type: MOVE_BOOKMARK, + id: id, + idColumnName: idColumnName, + indexOfId: indexOfId, + atIndexColumnName: atIndexColumnName, + indexOfatIndex: indexOfatIndex +}) + +// Changing value of add column input +export const columnNameOnchange = text => ({ + type: COLUMN_NAME_ONCHANGE, + text: text +}) + +// Adding new column +export const addColumn = name => ({ + type: ADD_COLUMN, + name: name +}) + +// Deleting column +export const deleteColumn = (name, cards) => ({ + type: DELETE_COLUMN, + name: name, + cards: cards +}) diff --git a/src/app/additional/additionalFunc.js b/src/app/additional/additionalFunc.js new file mode 100644 index 0000000..97e354f --- /dev/null +++ b/src/app/additional/additionalFunc.js @@ -0,0 +1,81 @@ +// Creating update info +export function wasUpdated (dateString) { + const updatedAtMilisec = Date.parse(dateString) + const todayMilisec = Date.now() + let dayDiffirence = parseInt((todayMilisec - updatedAtMilisec) / (1000 * 60 * 60 * 24)) + let result = '' + if (dayDiffirence > 1) { + result = 'Updated ' + dayDiffirence.toString() + ' days ago' + return result + } else if (dayDiffirence === 1) { + result = 'Updated a day ago' + return result + } + result = 'Updated today' + return result +} + +// Creating stars info +export function showStars (stars) { + let result = '' + const starsAbsolute = stars % 1000 + if (stars < 1000) { + result = stars.toString() + return result + } else if (starsAbsolute > 50 && starsAbsolute < 951) { + let starsKilo = (stars / 1000).toFixed(1) + result = starsKilo + 'k' + return result + } else if (starsAbsolute < 50 || starsAbsolute > 950) { + let starsKilo = (stars / 1000).toFixed() + result = starsKilo + 'k' + return result + } +} + +// Creating issues info +export function showIssues (issues) { + let result = 'No issues' + if (issues > 1) { + result = issues.toString() + ' issues need help' + return result + } else if (issues === 1) { + result = '1 issue need help' + return result + } + return result +} + +// Creating license info +export function showLicense (license) { + let result = '' + if (!license) { + result = 'No info' + return result + } + result = license.name + return result +} + +// Loading the state from LocalStorage +export const loadState = () => { + try { + const serializedState = localStorage.getItem('bookmark') + if (serializedState === null) { + return { 'bookmarks': { name: '', list: {}, columns: [{ name: 'Collection', cards: [] }] } } + } + return JSON.parse(serializedState) + } catch (err) { + return undefined + } +} + +// Saving the state to LocalStorage +export const saveState = (state) => { + try { + const serializedState = JSON.stringify(state) + localStorage.setItem('bookmark', serializedState) + } catch (err) { + // Ignore errors + } +} diff --git a/src/app/components/BookmarkApp.js b/src/app/components/BookmarkApp.js new file mode 100644 index 0000000..3d2253b --- /dev/null +++ b/src/app/components/BookmarkApp.js @@ -0,0 +1,15 @@ +import React from 'react' + +import ShowSearchInput from '../containers/Search/ShowSearchInput' +import ShowDropDown from '../containers/DropDown/ShowDropDown' +import ShowBookmarkContainer from '../containers/Columns/ShowBookmarkContainer' + +const BookmarkApp = () => ( +
+ + + +
+) + +export default BookmarkApp diff --git a/src/app/components/Columns/BookmarkAddColumn.js b/src/app/components/Columns/BookmarkAddColumn.js new file mode 100644 index 0000000..c01bab2 --- /dev/null +++ b/src/app/components/Columns/BookmarkAddColumn.js @@ -0,0 +1,71 @@ +import React from 'react' +import styled from 'styled-components' + +// Visualisation of add column block +const BookmarkAddColumn = ({ className, value, onChange, onClick, onKeyPress }) => ( +
+ + +
+) + +export default styled(BookmarkAddColumn)` + position: relative; + display: inline-block; + width: 196px; + height: 50px; + background-color: rgb(245, 247, 250); + border: 2px dashed rgb(153, 153, 153); + font-family: 'Open Sans', sans-serif; + font-weight: bold; + font-size: 17px; + margin: 7px 15px; + + input[type="text"] { + color: rgb(73, 73, 73); + border-radius: 5px; + border: 1px solid transparent; + width: 70%; + height: 27px; + font-size: 17px; + background-color: rgb(245, 247, 250); + font-family: 'Open Sans', sans-serif; + font-weight: bold; + margin: 12px 0px 12px 15px; + } + + input[type="text"]::placeholder { + font-size: 17px; + font-family: 'Open Sans', sans-serif; + font-weight: bold; + } + + button { + position: absolute; + right: 0; + top: 0; + border: 0; + background: transparent; + cursor: pointer; + border-radius: 50%; + width: 30px; + height: 30px; + padding: 0; + margin: 6%; + } + + button svg { + fill: #999999; + width: 30px; + height: 30px; + padding: 0; + } + +` diff --git a/src/app/components/Columns/BookmarkCard.js b/src/app/components/Columns/BookmarkCard.js new file mode 100644 index 0000000..a482604 --- /dev/null +++ b/src/app/components/Columns/BookmarkCard.js @@ -0,0 +1,153 @@ +import React from 'react' +import styled from 'styled-components' + +// Visualisation of the bookmark card +const BookmarkCard = ({ className, + onClick, + imgUrl, + htmlUrl, + fullName, + description, + updatedAt, + stars, + issues }) => ( +
+ +
+ {fullName} +
+
+

{fullName}

+
+

{description}

+
+
+
+
{stars}
+
{issues}
+
{updatedAt}
+
+
+
+) + +export default styled(BookmarkCard)` + display: ${props => props.dragging ? '0.5' : '1'}; + opacity: ${props => props.dragging ? '0' : '1'}; + position: relative; + width: 255px; + min-height: 110px; + background-color: white; + box-shadow: 1px 2px 5px 0px rgba(0,0,0,0.5); + -moz-box-shadow: 1px 2px 5px 0px rgba(0,0,0,0.5); + -webkit-box-shadow: 1px 2px 5px 0px rgba(0,0,0,0.5); + font-family: 'Open Sans', sans-serif; + font-weight: bold; + font-size: 17px; + text-align: left; + margin-bottom: 1px; + cursor: move; + + .delete-bookmark { + position: absolute; + right: 0; + top: 0; + border: 0; + background: transparent; + cursor: pointer; + border-radius: 50%; + width: 15px; + height: 15px; + padding: 0; + margin: 4px; + } + + .delete-bookmark svg { + width: 15px; + height: 15px; + padding: 0; + } + + .card-image { + position: relative; + display: inline-block; + height: auto; + width: 20%; + vertical-align: top; + } + + .card-image img { + position: absolute; + top: 20px; + left: 5px; + width: 80%; + border-radius: 50%; + } + + .card-info { + display: inline-block; + height: auto; + width: 80%; + line-height: 10px; + } + + .card-info p { + margin: 15px 15px 0 0; + } + + .card-info a { + color: rgb(43, 123, 232); + text-decoration: none; + font-family: 'Open Sans', sans-serif; + font-size: 11px; + font-weight: 800; + white-space: pre-line; + word-wrap: break-word; + } + + .card-description { + min-height: 40px; + } + + .card-description p { + color: rgb(158, 158, 158); + font-family: 'Open Sans', sans-serif; + font-size: 10px; + white-space: pre-line; + word-wrap: break-word; + } + + .card-info-string { + margin: 10px 15px 5px 0; + } + + .card-info-string::before { + content: ""; + display: inline-block; + vertical-align: middle; + height: 100%; + } + + .card-info-string div { + color: rgb(158, 158, 158); + font-family: 'Open Sans', sans-serif; + font-size: 7px; + display: inline-block; + vertical-align: middle; + padding-right: 7px; + } + + .card-info-string .star { + padding-right: 1px; + padding-bottom: 3px; + } + + .card-info-string svg { + padding-top: 1px; + width: 10px; + height: 10px; + fill: #9e9e9e; + } +` diff --git a/src/app/components/Columns/BookmarkColumn.js b/src/app/components/Columns/BookmarkColumn.js new file mode 100644 index 0000000..ffacc27 --- /dev/null +++ b/src/app/components/Columns/BookmarkColumn.js @@ -0,0 +1,22 @@ +import React from 'react' +import styled from 'styled-components' + +import ShowBookmarkColumnHead from '../../containers/Columns/ShowBookmarkColumnHead' +import DndBookmarkColumnCover from '../../containers/Columns/DndBookmarkColumnCover' + +// Visualisation of the column for bookmark cards +const BookmarkColumn = ({ className, id, name, cards }) => ( +
+ + +
+) + +export default styled(BookmarkColumn)` + display: inline-block; + width: 250px; + height: auto; + margin: 5px 15px; + vertical-align: top; +` diff --git a/src/app/components/Columns/BookmarkColumnHead.js b/src/app/components/Columns/BookmarkColumnHead.js new file mode 100644 index 0000000..76c7d81 --- /dev/null +++ b/src/app/components/Columns/BookmarkColumnHead.js @@ -0,0 +1,50 @@ +import React from 'react' +import styled from 'styled-components' + +// Visualisation of columns head block +const BookmarkColumnHead = ({className, name, onClick}) => ( +
+ + {name} +
+) + +export default styled(BookmarkColumnHead)` + position: ${props => props.sticky ? 'fixed' : 'relative'}; + top: ${props => props.sticky ? '0' : '0'}; + z-index: 1; + width: 240px; + height: 25px; + background-color: white; + box-shadow: ${props => props.sticky ? '1px 3px 5px 0px rgba(0, 0, 0, 0.5)' : '1px 1px 5px 0px rgba(0, 0, 0, 0.5)'}; + -moz-box-shadow: ${props => props.sticky ? '1px 3px 5px 0px rgba(0, 0, 0, 0.5)' : '1px 1px 5px 0px rgba(0, 0, 0, 0.5)'}; + -webkit-box-shadow: ${props => props.sticky ? '1px 3px 5px 0px rgba(0, 0, 0, 0.5)' : '1px 1px 5px 0px rgba(0, 0, 0, 0.5)'}; + font-family: 'Open Sans', sans-serif; + font-weight: bold; + font-size: 17px; + text-align: left; + padding: 17px 0 14px 15px; + margin-bottom: 1px; + + .delete-column { + position: absolute; + right: 0; + top: 0; + border: 0; + background: transparent; + cursor: pointer; + border-radius: 50%; + width: 20px; + height: 20px; + padding: 0; + margin: 4px; + } + + .delete-column svg { + width: 20px; + height: 20px; + padding: 0; + } +` diff --git a/src/app/components/Columns/BookmarkContainer.js b/src/app/components/Columns/BookmarkContainer.js new file mode 100644 index 0000000..7987e2a --- /dev/null +++ b/src/app/components/Columns/BookmarkContainer.js @@ -0,0 +1,26 @@ +import React from 'react' +import styled from 'styled-components' + +import BookmarkColumn from './BookmarkColumn' +import ShowBookmarkAddColumn from '../../containers/Columns/ShowBookmarkAddColumn' +import DndBookmarkColumnContainer from '../../containers/Columns/DndBookmarkColumnContainer' + +// Visualisation of the main container for columns +const BookmarkContainer = ({ className, columns }) => ( +
+ + {columns.map(column => + + )} + + +
+) + +export default styled(BookmarkContainer)` + position: absolute; + left 50px; + top: 100px; + overflow-x: auto; + white-space: nowrap; +` diff --git a/src/app/components/DropDown/DropDown.js b/src/app/components/DropDown/DropDown.js new file mode 100644 index 0000000..3180b18 --- /dev/null +++ b/src/app/components/DropDown/DropDown.js @@ -0,0 +1,23 @@ +import React from 'react' +import styled from 'styled-components' + +import SearchItemsList from '../../components/Search/SearchItemsList' + +// Visualisation of dropdown block +const DropDown = (props) => ( +
+ +
+) + +export default styled(DropDown)` + display: block; + position: relative; + left: 10%; + top: 40px; + background-color: #f9f9f9; + width: 40%; + min-width: 350px; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 2; +` diff --git a/src/app/components/DropDown/DropDownButton.js b/src/app/components/DropDown/DropDownButton.js new file mode 100644 index 0000000..f6feeeb --- /dev/null +++ b/src/app/components/DropDown/DropDownButton.js @@ -0,0 +1,44 @@ +import React from 'react' +import styled from 'styled-components' + +// Visualisation of dropdown add button +const DropDownButton = ({ className, added, onClick }) => ( + +) + +export default styled(DropDownButton)` + position: absolute; + right: 0; + top: 0; + bottom: 0; + border: 0; + background: transparent; + cursor: pointer; + border-radius: 50%; + width: 35px; + height: 35px; + padding: 0; + margin-top: auto; + margin-bottom: auto; + margin-right: 6%; + + svg { + width: 35px; + height: 35px; + padding: 0; + } + + .checked { + fill: #31cc73; + } + + .add { + fill: #999999; + } +` diff --git a/src/app/components/DropDown/InfoString.js b/src/app/components/DropDown/InfoString.js new file mode 100644 index 0000000..ee2ad7d --- /dev/null +++ b/src/app/components/DropDown/InfoString.js @@ -0,0 +1,40 @@ +import React from 'react' +import styled from 'styled-components' + +// Visualisation of info string for dropdown search blocks +const InfoString = ({ updatedAt, stars, issues, license, className }) => ( +
+
{license}
+
{updatedAt}
+
{issues}
+
+
{stars}
+
+) + +export default styled(InfoString)` + :before { + content: ""; + display: inline-block; + vertical-align: middle; + } + + div { + color: rgb(158, 158, 158); + font-family: 'Open Sans', sans-serif; + font-size: 10px; + display: inline-block; + vertical-align: middle; + padding: 0 10px 0 0; + } + + .star { + padding-right: 3px; + } + + svg { + width: 10px; + height: 10px; + fill: #9e9e9e; + } +` diff --git a/src/app/components/Search/SearchInput.js b/src/app/components/Search/SearchInput.js new file mode 100644 index 0000000..1679586 --- /dev/null +++ b/src/app/components/Search/SearchInput.js @@ -0,0 +1,73 @@ +import React from 'react' +import styled from 'styled-components' + +// Visualisation of search input +const SearchInput = ({ value, onChange, className }) => ( +
+
+ + +

Github Bookmarks

+
+
+) + +export default styled(SearchInput)` + .dropdown { + display: block; + } + + .logo { + color: rgb(55, 55, 55); + float: right; + font-family: 'Open Sans', sans-serif; + font-weight: bold; + margin: 20px 5% 0 0; + } + + div { + position: absolute; + height: 60px; + width: 100%; + top: 0; + left: 0; + z-index: 1; + background-color: white; + -webkit-box-shadow: 0px 8px 10px -7px rgba(0,0,0,0.5); + -moz-box-shadow: 0px 8px 10px -7px rgba(0,0,0,0.5); + box-shadow: 0px 8px 10px -7px rgba(0,0,0,0.5); + } + + svg { + fill: #9e9e9e; + float: left; + width: 30px; + height: 30px; + margin: 14px 0 0 10%; + text-align: center; + } + + input[type="text"] { + color: rgb(73, 73, 73); + border-radius: 5px; + border: 3px solid transparent; + width: 25%; + height: 28px; + margin: 15px 0 0 10px; + font-size: 17px; + background-color: white; + font-family: 'Open Sans', sans-serif; + font-weight: 300; + } + + input[type="text"]::placeholder { + font-style: italic; + font-size: 17px; + font-family: 'Open Sans', sans-serif; + font-weight: 300; + } +` diff --git a/src/app/components/Search/SearchItem.js b/src/app/components/Search/SearchItem.js new file mode 100644 index 0000000..cff0723 --- /dev/null +++ b/src/app/components/Search/SearchItem.js @@ -0,0 +1,58 @@ +import React from 'react' +import styled from 'styled-components' + +import DropDownInfoString from '../../containers/DropDown/DropDownInfoString' +import ShowDropDownButton from '../../containers/DropDown/ShowDropDownButton' + +// Visualisation of the search item block +const SearchItem = ({ id, + full_name, + html_url, + description, + updated_at, + stargazers_count, + open_issues, + license, + className }) => ( +
  • +
    + {full_name} + +

    {description}

    + + +
    +
  • +) + +export default styled(SearchItem)` + list-style-type: none; + + .item-box { + position: relative; + padding: 15px 0px 5px 35px; + border-bottom: 3px solid rgb(230, 230, 230); + } + + .description { + width: 80%; + padding: 0 0 15px 0; + font-size: 12px; + } + + a { + color: rgb(43, 123, 232); + text-decoration: none; + font-family: 'Open Sans', sans-serif; + font-size: 14px; + font-weight: bold; + } + + p, .description { + color: rgb(158, 158, 158); + font-family: 'Open Sans', sans-serif; + } +` diff --git a/src/app/components/Search/SearchItemsList.js b/src/app/components/Search/SearchItemsList.js new file mode 100644 index 0000000..be851cc --- /dev/null +++ b/src/app/components/Search/SearchItemsList.js @@ -0,0 +1,23 @@ +import React from 'react' + +import styled from 'styled-components' +import SearchItem from './SearchItem' + +// Visualisation of the search items list +const SearchItemsList = ({ items, className }) => ( +
    + +
    +) + +export default styled(SearchItemsList)` + ul { + padding-left: 0; + } +` diff --git a/src/app/containers/Columns/DndBookmarkColumn.js b/src/app/containers/Columns/DndBookmarkColumn.js new file mode 100644 index 0000000..d740a4f --- /dev/null +++ b/src/app/containers/Columns/DndBookmarkColumn.js @@ -0,0 +1,54 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import { DropTarget } from 'react-dnd' + +import ShowBookmarkCard from '../../containers/Columns/ShowBookmarkCard' + +const mapStateToProps = state => { + return { columns: state.bookmarks.columns } +} + +const cardTarget = { + drop () { + // + }, + hover (props, monitor, component) { + const isOver = monitor.isOver({ shallow: true }) + const currentColumn = props.findBookmark(monitor.getItem().id).findedColumnName + const isDifferentColumn = currentColumn !== props.columnName + // when the mouse pointer is over different column + if (isOver && isDifferentColumn) { + props.moveCard(monitor.getItem().id, props.columnName) + } + } +} + +@DropTarget('bookmark', cardTarget, connect => ({ + connectDropTarget: connect.dropTarget() +})) +@connect(mapStateToProps) +export default class DndBookmarkColumn extends Component { + constructor (props) { + super(props) + } + + render () { + const { connectDropTarget } = this.props + const cards = this.props.cards + + return ( + connectDropTarget && + connectDropTarget( +
    + {cards.map(card => + + )} +
    + ) + ) + } +} diff --git a/src/app/containers/Columns/DndBookmarkColumnContainer.js b/src/app/containers/Columns/DndBookmarkColumnContainer.js new file mode 100644 index 0000000..fb7a4f4 --- /dev/null +++ b/src/app/containers/Columns/DndBookmarkColumnContainer.js @@ -0,0 +1,15 @@ +import React, { Component } from 'react' +import { DragDropContext } from 'react-dnd' +import HTML5Backend from 'react-dnd-html5-backend' + +// Cover all drag-and-drop elements by html5 backend +@DragDropContext(HTML5Backend) +export default class DndBookmarkColumnContainer extends Component { + render () { + return ( +
    + {this.props.children} +
    + ) + } +} diff --git a/src/app/containers/Columns/DndBookmarkColumnCover.js b/src/app/containers/Columns/DndBookmarkColumnCover.js new file mode 100644 index 0000000..d995f73 --- /dev/null +++ b/src/app/containers/Columns/DndBookmarkColumnCover.js @@ -0,0 +1,55 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import DndBookmarkColumn from '../../containers/Columns/DndBookmarkColumn' +import { moveBookmark } from '../../actions' + +const mapStateToProps = state => { + return { columns: state.bookmarks.columns } +} + +@connect(mapStateToProps) +export default class DndBookmarkColumnCover extends Component { + constructor (props) { + super(props) + + this.moveCard = this.moveCard.bind(this) + this.findBookmark = this.findBookmark.bind(this) + } + + // Changing bookmark position + moveCard (id, atIndex) { + const { dispatch } = this.props + const { findedColumnName: idColumnName, index: indexOfId } = this.findBookmark(id) + const { findedColumnName: atIndexColumnName, index: indexOfatIndex } = this.findBookmark(atIndex) + + dispatch(moveBookmark(id, idColumnName, indexOfId, atIndexColumnName, indexOfatIndex)) + } + + // Finding a column name and an index of bookmark card + findBookmark (id) { + const columns = this.props.columns + if (!Number.isInteger(id)) { + return { + findedColumnName: id, + index: 0 + } + } + const column = columns.filter(col => col.cards.includes(id))[0] + const findedColumnName = column.name + const findedCard = column.cards.indexOf(id) + return { + findedColumnName: findedColumnName, + index: findedCard + } + } + + render () { + return ( + + ) + } +} diff --git a/src/app/containers/Columns/ShowBookmarkAddColumn.js b/src/app/containers/Columns/ShowBookmarkAddColumn.js new file mode 100644 index 0000000..02ed981 --- /dev/null +++ b/src/app/containers/Columns/ShowBookmarkAddColumn.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import { columnNameOnchange, addColumn } from '../../actions/' +import BookmarkAddColumn from '../../components/Columns/BookmarkAddColumn' + +const mapStateToProps = state => { + return { value: state.bookmarks.name, + columns: state.bookmarks.columns } +} + +@connect(mapStateToProps) +export default class ShowBookmarkAddColumn extends Component { + constructor (props) { + super(props) + + this.handleInputChange = this.handleInputChange.bind(this) + this.handleAddClick = this.handleAddClick.bind(this) + this.handlePressEnter = this.handlePressEnter.bind(this) + } + + // Changing value of add column block input + handleInputChange (e) { + const { dispatch } = this.props + dispatch(columnNameOnchange(e.target.value)) + } + + // Adding new column by button click + handleAddClick (e) { + const { dispatch } = this.props + if (this.props.value && this.props.value !== '') { + // if there is no columns with current value + if (this.props.columns.every((el) => el.name !== this.props.value)) { + dispatch(addColumn(this.props.value)) + } else { + // show message if column with current value already exists + alert('The column already exists') + } + } + } + + // Adding new column by Enter press + handlePressEnter (e) { + const { dispatch } = this.props + if (e.key === 'Enter') { + if (this.props.value && this.props.value !== '') { + if (this.props.columns.every((el) => el.name !== this.props.value)) { + dispatch(addColumn(this.props.value)) + } else { + alert('The column already exists') + } + } + } + } + + render () { + return ( + this.handleAddClick(e)} + onKeyPress={(e) => this.handlePressEnter(e)} /> + ) + } +} diff --git a/src/app/containers/Columns/ShowBookmarkCard.js b/src/app/containers/Columns/ShowBookmarkCard.js new file mode 100644 index 0000000..4b23065 --- /dev/null +++ b/src/app/containers/Columns/ShowBookmarkCard.js @@ -0,0 +1,96 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import { DragSource, DropTarget } from 'react-dnd' + +import { wasUpdated, showStars, showIssues } from '../../additional/additionalFunc' +import BookmarkCard from '../../components/Columns/BookmarkCard' + +import { deleteBookmark } from '../../actions/' + +const mapStateToProps = state => { + return { list: state.bookmarks.list } +} + +const cardSource = { + beginDrag (props) { + return { + id: props.card, + itemList: props.list + } + }, + endDrag (props, monitor) { + + }, + isDragging (props, monitor) { + return props.card === monitor.getItem().id + } +} + +const cardTarget = { + canDrop () { + return false + }, + + hover (props, monitor, component) { + if (monitor.isOver({ shallow: true })) { + const draggedId = monitor.getItem().id + const overId = props.card + // move a bookmark if the current one isn't over itself + if (draggedId !== overId) { + props.moveCard(draggedId, overId) + } + } + } +} + +@DropTarget('bookmark', cardTarget, (connect, monitor) => ({ + connectDropTarget: connect.dropTarget(), + isOverCurrent: monitor.isOver({ shallow: true }) +})) +@DragSource('bookmark', cardSource, (connect, monitor) => ({ + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging() +})) +@connect(mapStateToProps) +export default class ShowBookmarkCard extends Component { + constructor (props) { + super(props) + + this.handleDeleteBookmark = this.handleDeleteBookmark.bind(this) + } + + // Deleting a bookmark by click + handleDeleteBookmark (e) { + const { dispatch } = this.props + dispatch(deleteBookmark(this.props.card, this.props.columnName)) + } + + render () { + const itemList = this.props.list + const card = this.props.card + const { connectDragSource, connectDropTarget, isDragging } = this.props + + return ( + connectDragSource && + connectDropTarget && + connectDragSource( + connectDropTarget( +
    + this.handleDeleteBookmark(e)} + dragging={isDragging} + id={card.toString()} + key={card} + /> +
    + ) + ) + ) + } +} diff --git a/src/app/containers/Columns/ShowBookmarkColumnHead.js b/src/app/containers/Columns/ShowBookmarkColumnHead.js new file mode 100644 index 0000000..8281657 --- /dev/null +++ b/src/app/containers/Columns/ShowBookmarkColumnHead.js @@ -0,0 +1,60 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import BookmarkColumnHead from '../../components/Columns/BookmarkColumnHead' +import { deleteColumn } from '../../actions' + +const mapStateToProps = state => { + return { columns: state.bookmarks.columns } +} + +@connect(mapStateToProps) +export default class ShowBookmarkColumnHead extends Component { + constructor (props) { + super(props) + this.state = { sticky: false } + + this.handleDeleteColumn = this.handleDeleteColumn.bind(this) + this.onScroll = this.onScroll.bind(this) + } + + componentDidMount () { + // Creating the event listener for a scroll event + window.addEventListener('scroll', this.onScroll, false) + } + componentWillUnmount () { + // Deleting the event listener for a scroll event + window.removeEventListener('scroll', this.onScroll, false) + } + + // Changing column head style between position relative and fixed + onScroll () { + if (window.pageYOffset >= 106 && !this.state.sticky) { + this.setState({ sticky: true }) + } else if (window.pageYOffset < 106 && this.state.sticky) { + this.setState({ sticky: false }) + } + } + + // Handling a click on column delete button + handleDeleteColumn (e) { + const { dispatch } = this.props + // delete any column except the Collection column + if (this.props.name !== 'Collection') { + const props = this.props + const columnCards = props.columns.filter(el => el.name === props.name)[0].cards + dispatch(deleteColumn(this.props.name, columnCards)) + } else { + // message if trying delete the Collection column + alert("You can't delete the collection column") + } + } + + render () { + return ( + this.handleDeleteColumn(e)} + sticky={this.state.sticky} /> + ) + } +} diff --git a/src/app/containers/Columns/ShowBookmarkContainer.js b/src/app/containers/Columns/ShowBookmarkContainer.js new file mode 100644 index 0000000..8ec35c2 --- /dev/null +++ b/src/app/containers/Columns/ShowBookmarkContainer.js @@ -0,0 +1,14 @@ +import React from 'react' +import { connect } from 'react-redux' + +import BookmarkContainer from '../../components/Columns/BookmarkContainer' + +const ShowBookmarkContainer = ({ columns }) => ( + +) + +const mapStateToProps = state => { + return { columns: state.bookmarks.columns } +} + +export default connect(mapStateToProps)(ShowBookmarkContainer) diff --git a/src/app/containers/DropDown/DropDownInfoString.js b/src/app/containers/DropDown/DropDownInfoString.js new file mode 100644 index 0000000..8e2fcfa --- /dev/null +++ b/src/app/containers/DropDown/DropDownInfoString.js @@ -0,0 +1,19 @@ +import React, { Component } from 'react' + +import InfoString from '../../components/DropDown/InfoString' +import { wasUpdated, showStars, showIssues, showLicense } from '../../additional/additionalFunc' + +export default class DropDownInfoString extends Component { + constructor (props) { + super(props) + } + + render () { + return ( + + ) + } +} diff --git a/src/app/containers/DropDown/ShowDropDown.js b/src/app/containers/DropDown/ShowDropDown.js new file mode 100644 index 0000000..ce65f6d --- /dev/null +++ b/src/app/containers/DropDown/ShowDropDown.js @@ -0,0 +1,56 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import { dropdownHide } from '../../actions/' +import DropDown from '../../components/DropDown/DropDown' + +const mapStateToProps = state => { + return { + items: state.search.items, + dropdown: state.search.dropdown + } +} + +@connect(mapStateToProps) +export default class ShowDropDown extends Component { + constructor (props) { + super(props) + + this.handleClick = this.handleClick.bind(this) + } + + componentWillMount () { + // Creating the event listener for mousedown event + document.addEventListener('mousedown', this.handleClick, false) + } + + componentWillUnmount () { + // Deleting the event listener for mousedown event + document.removeEventListener('mousedown', this.handleClick, false) + } + + // Getting ref of the dropdown block + ddBox = React.createRef(); + + // Handlick a click outside the dropdown block + handleClick = (e) => { + const { dispatch } = this.props + // Hide the dropdown if the click was outside the block + if (this.props.dropdown && !this.ddBox.current.contains(e.target)) { + dispatch(dropdownHide()) + } + } + + render () { + return ( +
    + { + (this.props.dropdown) + ? + : null + } +
    + ) + } +} diff --git a/src/app/containers/DropDown/ShowDropDownButton.js b/src/app/containers/DropDown/ShowDropDownButton.js new file mode 100644 index 0000000..b4bfa05 --- /dev/null +++ b/src/app/containers/DropDown/ShowDropDownButton.js @@ -0,0 +1,46 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import { addBookmark } from '../../actions' + +import DropDownButton from '../../components/DropDown/DropDownButton' + +const mapStateToProps = state => { + return { columns: state.bookmarks.columns, + items: state.search.items } +} + +@connect(mapStateToProps) +export default class ShowDropDownButton extends Component { + constructor (props) { + super(props) + + this.handleClickButton = this.handleClickButton.bind(this) + } + + // Adding a bookmark by click on the button + handleClickButton (e) { + const { dispatch } = this.props + if (!this.hasBookmark(this.props.itemId, this.props.columns)) { + let bookmarkItem = {} + this.props.items.forEach((el, i) => { + if (el.id === this.props.itemId) { + bookmarkItem = this.props.items[i] + } + }) + dispatch(addBookmark(bookmarkItem)) + } + } + + // Checking the current bookmark is added or not + hasBookmark (id, columns) { + const result = columns.every(el => el.cards.includes(id) === false) + return !result + } + + render () { + return ( + this.handleClickButton(e)} + added={this.hasBookmark(this.props.itemId, this.props.columns)} /> + ) + } +} diff --git a/src/app/containers/Search/ShowSearchInput.js b/src/app/containers/Search/ShowSearchInput.js new file mode 100644 index 0000000..c1b4020 --- /dev/null +++ b/src/app/containers/Search/ShowSearchInput.js @@ -0,0 +1,39 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import { searchOnchange, searchFetchQuery } from '../../actions' +import SearchInput from '../../components/Search/SearchInput' + +const mapStateToProps = state => { + return { searchtext: state.search.searchtext } +} + +@connect(mapStateToProps) +export default class ShowSearchInput extends Component { + constructor (props) { + super(props) + + this.handleOnChange = this.handleOnChange.bind(this) + } + + // Handling changing of search input value + handleOnChange (e) { + const { dispatch } = this.props + // if the value has more than 2 symbols + if (e.target.value.length < 2) { + dispatch(searchOnchange(e.target.value)) + } else { + dispatch(searchOnchange(e.target.value)) + dispatch(searchFetchQuery(e.target.value)) + } + } + + render () { + return ( +
    + +
    + ) + } +} diff --git a/src/app/global.js b/src/app/global.js index 0a9a538..a1aef05 100644 --- a/src/app/global.js +++ b/src/app/global.js @@ -1,27 +1,12 @@ // @flow + import { injectGlobal } from 'styled-components' injectGlobal` @import url('https://fonts.googleapis.com/css?family=Open+Sans:300,400'); - * { - box-sizing: border-box; - } - html, body, #root { - width: 100%; - height: 100%; - margin: 0; - font-weight: 300; font-family: 'Open Sans', sans-serif; } - form { - margin: 0; - } - - #modal-root { - position: relative; - z-index: 999; - } ` diff --git a/src/app/index.js b/src/app/index.js index 288598b..45e4fea 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -1,5 +1,5 @@ // @flow -import React from 'react' +import React, { Component } from 'react' import ReactDOM from 'react-dom' import { Provider } from 'react-redux' import { HashRouter as Router, Switch, Redirect, Route } from 'react-router-dom' @@ -9,7 +9,7 @@ import { routes } from './routes' import './global' -class Application extends React.PureComponent<{||}> { +class Application extends Component { render () { return ( @@ -29,4 +29,5 @@ class Application extends React.PureComponent<{||}> { } ReactDOM.render( - , (document.getElementById('root'): any)) + , (document.getElementById('root')) +) diff --git a/src/app/reducer.js b/src/app/reducer.js index 1933a8c..9dfd682 100644 --- a/src/app/reducer.js +++ b/src/app/reducer.js @@ -1,6 +1,9 @@ // @flow -import { combineReducers } from 'redux-immutable' +import { combineReducers } from 'redux' +import search from './reducers/search' +import bookmarks from './reducers/bookmarks' export const reducer = combineReducers({ - + search, + bookmarks }) diff --git a/src/app/reducers/bookmarks.js b/src/app/reducers/bookmarks.js new file mode 100644 index 0000000..8590dc5 --- /dev/null +++ b/src/app/reducers/bookmarks.js @@ -0,0 +1,44 @@ +import update from 'immutability-helper' +import { loadState } from '../additional/additionalFunc' + +import { ADD_BOOKMARK, + DELETE_BOOKMARK, + MOVE_BOOKMARK, + COLUMN_NAME_ONCHANGE, + ADD_COLUMN, + DELETE_COLUMN } from '../actions' + +// loading state from the Local Storage +const persistedState = loadState() + +const bookmarks = (state = persistedState.bookmarks, action) => { + switch (action.type) { + case MOVE_BOOKMARK: + return update(state, { + columns: { + $set: state.columns.map(el => el.name === action.idColumnName ? update(el, { cards: { $splice: [[action.indexOfId, 1]] } }) : el) + .map(el => el.name === action.atIndexColumnName ? update(el, { cards: { $splice: [[action.indexOfatIndex, 0, action.id]] } }) : el) + } + }) + case ADD_BOOKMARK: + return update(state, { list: { $set: { ...state.list, [action.item.id]: action.item } }, + columns: { 0: { cards: { $push: [action.item.id] } } } + }) + case DELETE_BOOKMARK: + return update(state, { list: { $unset: [action.id] }, + columns: { $set: state.columns.map(el => (el.name === action.column) ? { name: el.name, cards: el.cards.filter(arrEl => arrEl !== action.id) } : el) }}) + case COLUMN_NAME_ONCHANGE: + return update(state, { name: { $set: action.text } }) + case ADD_COLUMN: + return update(state, { columns: { $push: [{ name: action.name, cards: [] }] }, + name: { $set: '' } + }) + case DELETE_COLUMN: + return update(state, { columns: { $set: state.columns.filter(el => el.name !== action.name) + .map((el, i) => i === 0 ? update(el, { cards: { $push: action.cards } }) : el) } }) + default: + return state + } +} + +export default bookmarks diff --git a/src/app/reducers/search.js b/src/app/reducers/search.js new file mode 100644 index 0000000..33c81b7 --- /dev/null +++ b/src/app/reducers/search.js @@ -0,0 +1,26 @@ +import update from 'immutability-helper' +import { SEARCH_ONCHANGE, + SEARCH_RECEIVE_DATA, + DROPDOWN_SHOW, + DROPDOWN_HIDE } from '../actions' + +// The valuse of initial state +const initialState = { searchtext: '', items: [], dropdown: false } + +const search = (state = initialState, action) => { + switch (action.type) { + case SEARCH_ONCHANGE: + return update(state, { searchtext: { $set: action.data } }) + case SEARCH_RECEIVE_DATA: + return update(state, { items: { $set: action.items }, + dropdown: { $set: true } }) + case DROPDOWN_SHOW: + return update(state, { dropdown: { $set: true } }) + case DROPDOWN_HIDE: + return update(state, { dropdown: { $set: false } }) + default: + return state + } +} + +export default search diff --git a/src/app/store.js b/src/app/store.js index d59f62e..fe7c698 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -3,7 +3,15 @@ import { createStore } from 'redux' import { middleware } from './middleware' import { reducer } from './reducer' +import { saveState } from './additional/additionalFunc' export const store = createStore(reducer, middleware) +// Saving the store changing to the Local Storage +store.subscribe(() => { + saveState({ + bookmarks: store.getState().bookmarks + }) +}) + window.store = store