diff --git a/.babelrc b/.babelrc
index 6b3ad3440..209724ac5 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,8 +1,5 @@
{
- "presets": [
- "@babel/preset-env",
- "@babel/preset-react"
- ],
+ "presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [
[
"@babel/plugin-proposal-decorators",
@@ -13,6 +10,7 @@
"@babel/plugin-transform-runtime",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-object-rest-spread",
- "babel-plugin-styled-components"
+ "babel-plugin-styled-components",
+ "transform-react-remove-prop-types"
]
}
diff --git a/.eslintrc.yml b/.eslintrc.yml
index 855c3f4b8..8f2dfabb3 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -62,7 +62,7 @@ rules:
react/self-closing-comp: ['warn']
react/jsx-indent: ['warn']
react/no-array-index-key: ['warn']
- react/prefer-stateless-function: ['warn']
+ react/prefer-stateless-function: off
# JSX
jsx-a11y/anchor-is-valid:
- warn
@@ -77,6 +77,7 @@ rules:
jsx-a11y/click-events-have-key-events: ['warn']
jsx-a11y/no-static-element-interactions: ['warn']
jsx-a11y/media-has-caption: ['warn']
+ jsx-a11y/accessible-emoji: ['warn']
jsx-a11y/anchor-has-content: off
# Following rule haves been disabled for compatibility with
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 4624fea19..000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve
-
----
-
-**Describe the bug**
-A clear and concise description of what the bug is.
-
-**To Reproduce**
-Steps to reproduce the behavior:
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-**Screenshots**
-If applicable, add screenshots to help explain your problem.
-
-**System:**
- - OS: [e.g. iOS, Ubuntu, Windows, Mac OSx]
- - Browser [e.g. chrome, safari]
- - Version [e.g. 22]
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index 7561bced3..000000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for this project
-
----
-
-**Ensure that the feature request is not already listed on [Feathub](https://feathub.com/CaptainFact/captain-fact)**
-
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-**Additional context**
-Add any other context or screenshots about the feature request here.
diff --git a/.gitignore b/.gitignore
index ca25e8247..dd11aa5d9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,7 @@
.tmproj
nbproject
Thumbs.db
+.history
# NPM packages folder.
node_modules/
@@ -31,6 +32,9 @@ coverage
# Ignore cypress videos and screenshots
cypress/videos
+# Ignore styleguide build
+styleguide
+
# API dev image resources
dev/dev_docker_api_resources/*
!dev/dev_docker_api_resources/.keep
diff --git a/.idea/captain-fact-frontend.iml b/.idea/captain-fact-frontend.iml
deleted file mode 100644
index 3ed9fa791..000000000
--- a/.idea/captain-fact-frontend.iml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index 1c12d1e1d..9275ffa9f 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,8 @@ should be enough. Otherwise follow the procedure below:
- `npm start` - Start the frontend
- `npm run test` - run all unit tests
+Frontend is started on http://localhost:3333
+
A default account should have been created for you with
email=`admin@captainfact.io` and password=`password`.
diff --git a/app/API/graphql_api.js b/app/API/graphql_api.js
index 77253695c..36ecbcc1c 100644
--- a/app/API/graphql_api.js
+++ b/app/API/graphql_api.js
@@ -1,8 +1,23 @@
import ApolloClient from 'apollo-boost'
import { GRAPHQL_API_URL } from '../config'
+const authMiddleware = operation => {
+ const token = localStorage.getItem('token')
+
+ if (token) {
+ operation.setContext({
+ headers: {
+ authorization: `Bearer ${token}`
+ }
+ })
+ }
+
+ return operation
+}
+
const GraphQLClient = new ApolloClient({
- uri: GRAPHQL_API_URL
+ uri: GRAPHQL_API_URL,
+ request: authMiddleware
})
export default GraphQLClient
diff --git a/app/API/graphql_queries.js b/app/API/graphql_queries.js
index ea75742e9..d7e86c59b 100644
--- a/app/API/graphql_queries.js
+++ b/app/API/graphql_queries.js
@@ -43,3 +43,20 @@ export const VideosAddedByUserQuery = gql`
}
}
`
+
+export const loggedInUserSubscriptionsQuery = gql`
+ query LoggedInUserSubscriptions($scopes: [String]) {
+ loggedInUser {
+ subscriptions(scopes: $scopes) {
+ id
+ scope
+ videoId
+ isSubscribed
+ video {
+ hashId
+ title
+ }
+ }
+ }
+ }
+`
diff --git a/app/components/App/index.jsx b/app/components/App/Layout.jsx
similarity index 91%
rename from app/components/App/index.jsx
rename to app/components/App/Layout.jsx
index 9fc58c049..29eb8fe3a 100644
--- a/app/components/App/index.jsx
+++ b/app/components/App/Layout.jsx
@@ -1,9 +1,10 @@
import React from 'react'
import { connect } from 'react-redux'
-
import { Helmet } from 'react-helmet'
+import { Flex } from '@rebass/grid'
import { FlashMessages } from '../Utils'
+import Navbar from './Navbar'
import Sidebar from './Sidebar'
import { MainModalContainer } from '../Modal/MainModalContainer'
import PublicAchievementUnlocker from '../Users/PublicAchievementUnlocker'
@@ -13,7 +14,7 @@ import BackgroundNotifier from './BackgroundNotifier'
locale: state.UserPreferences.locale,
sidebarExpended: state.UserPreferences.sidebarExpended
}))
-export default class App extends React.PureComponent {
+export default class Layout extends React.PureComponent {
render() {
const { locale, sidebarExpended, children } = this.props
const mainContainerClass = sidebarExpended ? undefined : 'expended'
@@ -25,6 +26,7 @@ export default class App extends React.PureComponent {
+
{children}
diff --git a/app/components/App/Logo.jsx b/app/components/App/Logo.jsx
index 0f3f2976a..73be680d0 100644
--- a/app/components/App/Logo.jsx
+++ b/app/components/App/Logo.jsx
@@ -1,15 +1,56 @@
import React from 'react'
+import PropTypes from 'prop-types'
+import styled, { css } from 'styled-components'
+import { themeGet } from 'styled-system'
+import { Flex } from '@rebass/grid'
import logo from '../../assets/logo.svg'
import borderlessLogo from '../../assets/logo-borderless.svg'
+import { Span, Small } from '../StyledUtils/Text'
-const Logo = ({ borderless = false }) => (
-
-
- aptain
- Fact
- (Beta)
-
+const LogoContainer = styled(Flex)`
+ max-height: 100%;
+ align-items: center;
+ font-family: ${themeGet('fontFamily.serif')};
+ color: ${themeGet('colors.black.500')};
+
+ ${props => css`
+ font-size: ${props.size / 2}px;
+ height: ${props.size}px;
+ `}
+`
+
+const Image = styled.img`
+ height: 100%;
+ width: ${props => props.size}px;
+`
+
+/**
+ * The main website logo.
+ */
+const Logo = ({ borderless, height }) => (
+
+
+ aptain
+
+ Fact
+
+
+ (Beta)
+
+
)
+Logo.propTypes = {
+ /** If true, a version of the logo without border will be used */
+ borderless: PropTypes.bool,
+ /** Base height of the component in pixels */
+ height: PropTypes.number
+}
+
+Logo.defaultProps = {
+ borderless: false,
+ height: 30
+}
+
export default Logo
diff --git a/app/components/App/Navbar.jsx b/app/components/App/Navbar.jsx
new file mode 100644
index 000000000..4b20bcec8
--- /dev/null
+++ b/app/components/App/Navbar.jsx
@@ -0,0 +1,234 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import { Link, withRouter } from 'react-router'
+import styled, { withTheme, css } from 'styled-components'
+import { Flex, Box } from '@rebass/grid'
+import { themeGet } from 'styled-system'
+
+import { Menu } from 'styled-icons/boxicons-regular/Menu'
+import { Bell } from 'styled-icons/fa-solid/Bell'
+import { CaretDown } from 'styled-icons/fa-solid/CaretDown'
+import { UserCircle } from 'styled-icons/fa-regular/UserCircle'
+
+import { withNamespaces } from 'react-i18next'
+import Popup from 'reactjs-popup'
+import Logo from './Logo'
+import { toggleSidebar } from '../../state/user_preferences/reducer'
+import UserPicture from '../Users/UserPicture'
+import { USER_PICTURE_LARGE } from '../../constants'
+import { withLoggedInUser } from '../LoggedInUser/UserProvider'
+import UnstyledButton from '../StyledUtils/UnstyledButton'
+import { LoadingFrame } from '../Utils/LoadingFrame'
+import { fadeIn } from '../StyledUtils/Keyframes'
+import UserMenu from '../Users/UserMenu'
+import StyledLink from '../StyledUtils/StyledLink'
+import Notifications from '../LoggedInUser/Notifications'
+import { ErrorView } from '../Utils/ErrorView'
+import Container from '../StyledUtils/Container'
+import NotificationsPopupContent from '../Notifications/NotificationsPopupContent'
+
+const NavbarContainer = styled(Flex)`
+ position: fixed;
+ z-index: 9999;
+ top: 0;
+ width: 100%;
+ justify-content: space-between;
+ align-items: center;
+ background: rgba(255, 255, 255, 0.9);
+ height: ${themeGet('navbarHeight')}px;
+ border-bottom: 1px solid ${themeGet('colors.black.200')};
+ box-shadow: 0px 0px 20px 5px rgba(125, 125, 125, 0.3);
+ transition: top 0.3s;
+ animation: ${fadeIn} 0.3s;
+`
+
+const UserMenuTrigger = styled(Flex)`
+ align-items: center;
+ height: 38px;
+ cursor: pointer;
+
+ &:hover {
+ color: ${themeGet('colors.black.300')};
+ }
+
+ figure {
+ max-height: 100%;
+ max-width: 38px;
+ }
+`
+
+const UserMenuEntry = styled(({ isActive, ...props }) =>
)`
+ display: block;
+ border-left: 2px solid white;
+ background: white;
+
+ &:hover {
+ background: ${themeGet('colors.black.50')};
+ }
+
+ ${props => props.index > 0
+ && css`
+ border-top: 1px solid ${themeGet('colors.black.100')};
+ `}
+
+ ${props => props.isActive
+ && css`
+ border-left: 2px solid ${themeGet('colors.primary')};
+ `}
+`
+
+UserMenuEntry.defaultProps = {
+ px: 2,
+ py: 1
+}
+
+const UserLoading = styled(UserCircle)`
+ animation: ${fadeIn} 0.75s infinite linear alternate;
+ margin-right: ${themeGet('space.2')};
+`
+
+const MenuToggleSwitch = styled(Menu)`
+ height: 100%;
+ width: 45px;
+ cursor: pointer;
+ user-select: none;
+
+ &:hover {
+ color: ${themeGet('colors.black.500')};
+ }
+`
+
+const Navbar = ({
+ t,
+ theme,
+ toggleSidebar,
+ loggedInUser,
+ isAuthenticated,
+ loggedInUserLoading,
+ location
+}) => {
+ const loginRedirect = !location.pathname.startsWith('/login') && !location.pathname.startsWith('/signup')
+ ? location.pathname
+ : '/videos'
+
+ return (
+
+
+
+ {/* Left */}
+
+
+ toggleSidebar()} />
+
+
+
+
+
+ {/* Center - will hold the search bar in the future */}
+ {/* Right */}
+ {loggedInUserLoading ? (
+
+ ) : (
+
+ {isAuthenticated ? (
+
+
+
+
+ )}
+ >
+
+ {({ loading, error, notifications, markAsSeen }) => {
+ if (loading) {
+ return
+ } else if (error) {
+ return
+ }
+
+ return (
+
+ )
+ }}
+
+
+
+
+
+
+ )}
+ >
+
+ {({ Icon, key, route, title, index, isActive, onClick }) => key !== '/notifications' && (
+
+
+
+
+ {title}
+
+
+ )
+ }
+
+
+
+ ) : (
+
+
+ {t('menu.login')}
+
+
+ {t('menu.extension')}
+
+
+ {t('menu.signup')}
+
+
+ )}
+
+ )}
+
+
+ )
+}
+
+export default withTheme(
+ connect(
+ null,
+ { toggleSidebar }
+ )(withLoggedInUser(withNamespaces('main')(withRouter(Navbar))))
+)
diff --git a/app/components/App/Sidebar.jsx b/app/components/App/Sidebar.jsx
index bb0a9d026..84b448910 100644
--- a/app/components/App/Sidebar.jsx
+++ b/app/components/App/Sidebar.jsx
@@ -4,30 +4,22 @@ import { Link } from 'react-router'
import { withNamespaces } from 'react-i18next'
import classNames from 'classnames'
import capitalize from 'voca/capitalize'
+import { Flex } from '@rebass/grid'
-import { Icon } from '../Utils'
-import {
- MOBILE_WIDTH_THRESHOLD,
- USER_PICTURE_SMALL,
- MIN_REPUTATION_MODERATION
-} from '../../constants'
-import { LoadingFrame } from '../Utils/LoadingFrame'
+import { LinkExternal } from 'styled-icons/octicons/LinkExternal'
+
+import { MOBILE_WIDTH_THRESHOLD, MIN_REPUTATION_MODERATION } from '../../constants'
import RawIcon from '../Utils/RawIcon'
import ReputationGuard from '../Utils/ReputationGuard'
-import ScoreTag from '../Users/ScoreTag'
import { closeSidebar, toggleSidebar } from '../../state/user_preferences/reducer'
-import UserPicture from '../Users/UserPicture'
-import Logo from './Logo'
-import Button from '../Utils/Button'
-import { withLoggedInUser } from '../LoggedInUser/UserProvider'
import UserLanguageSelector from '../LoggedInUser/UserLanguageSelector'
+import ExternalLinkNewTab from '../Utils/ExternalLinkNewTab'
@connect(
state => ({ sidebarExpended: state.UserPreferences.sidebarExpended }),
{ toggleSidebar, closeSidebar }
)
@withNamespaces('main')
-@withLoggedInUser
export default class Sidebar extends React.PureComponent {
constructor(props) {
super(props)
@@ -47,6 +39,7 @@ export default class Sidebar extends React.PureComponent {
className={classes}
activeClassName="is-active"
onClick={this.closeSideBarIfMobile}
+ title={children}
{...props}
>
{iconName &&
}
@@ -59,113 +52,23 @@ export default class Sidebar extends React.PureComponent {
return
{this.MenuLink(props)}
}
- renderUserLinks() {
- const {
- loggedInUser: { username, reputation },
- t
- } = this.props
- const baseLink = `/u/${username}`
- return (
-
-
-
-
- {t('menu.myActivity')}
-
-
- {t('menu.settings')}
-
-
-
- )
- }
-
- renderConnectLinks() {
- return (
-
-
-
- {this.props.t('menu.loginSignup')}
-
-
- )
- }
-
- renderUserSection() {
- if (this.props.loggedInUserLoading) {
- return (
-
-
-
- )
- }
-
- return this.props.isAuthenticated ? this.renderUserLinks() : this.renderConnectLinks()
- }
-
render() {
const { sidebarExpended, className, t } = this.props
return (
-
+
)
}
@@ -190,19 +101,11 @@ export default class Sidebar extends React.PureComponent {
{capitalize(t('entities.video_plural'))}
-
+
{t('menu.moderation')}
)
}
-
- usernameFontSize() {
- return `${1.4 - this.props.loggedInUser.username.length / 30}em`
- }
}
diff --git a/app/components/Home/CFSocialProfiles.jsx b/app/components/Home/CFSocialProfiles.jsx
index e0a5aeabe..752627a96 100644
--- a/app/components/Home/CFSocialProfiles.jsx
+++ b/app/components/Home/CFSocialProfiles.jsx
@@ -1,7 +1,8 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Flex } from '@rebass/grid'
-import styled from 'styled-components'
+import styled, { withTheme } from 'styled-components'
+import { get } from 'lodash'
import { Discord } from 'styled-icons/fa-brands/Discord'
import { Facebook } from 'styled-icons/fa-brands/Facebook'
@@ -11,7 +12,6 @@ import { Github } from 'styled-icons/fa-brands/Github'
import ExternalLinkNewTab from '../Utils/ExternalLinkNewTab'
const IconLinkContainer = styled(ExternalLinkNewTab)`
- color: white;
margin: 0 0.5em;
`
@@ -19,10 +19,10 @@ const IconLinkContainer = styled(ExternalLinkNewTab)`
* Render a social icon with the proper link and add a popup with the title
* of the social network.
*/
-const SocialIconLink = ({ Icon, name, url, size }) => {
+const SocialIconLink = ({ Icon, name, url, size, color }) => {
return (
-
+
)
}
@@ -31,32 +31,37 @@ const SocialIconLink = ({ Icon, name, url, size }) => {
* Render social profiles icons for other CaptainFact profiles on
* Facebook, Twitter...
*/
-const CFSocialProfiles = ({ size = '2em' }) => {
+const CFSocialProfiles = ({ size, color, justifyContent, theme }) => {
+ const themeColor = get(theme, `colors.${color}`, color)
return (
-
+
)
@@ -64,7 +69,14 @@ const CFSocialProfiles = ({ size = '2em' }) => {
CFSocialProfiles.propTypes = {
/** Icons size. Default to 2em */
- size: PropTypes.string
+ size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ /** Passed to Flex */
+ justifyContent: PropTypes.string
}
-export default CFSocialProfiles
+CFSocialProfiles.defaultProps = {
+ size: '2em',
+ justifyContent: 'center'
+}
+
+export default withTheme(CFSocialProfiles)
diff --git a/app/components/Home/Home.jsx b/app/components/Home/Home.jsx
index 4ae1b5e21..498a4f6ca 100644
--- a/app/components/Home/Home.jsx
+++ b/app/components/Home/Home.jsx
@@ -1,10 +1,9 @@
import React from 'react'
-import { connect } from 'react-redux'
import { Link } from 'react-router'
import { withNamespaces, Trans } from 'react-i18next'
import { Flex, Box } from '@rebass/grid'
-import * as Matomo from '../../API/matomo'
+import * as Matomo from '../../API/matomo'
import { Icon } from '../Utils'
import InvitationRequestForm from '../Users/InvitationRequestForm'
import { INVITATION_SYSTEM } from '../../config'
@@ -94,9 +93,9 @@ export default class Home extends React.PureComponent {
-
+
{t('ambassadors')}
@@ -111,7 +110,7 @@ export default class Home extends React.PureComponent {