diff --git a/.gitignore b/.gitignore index f319765e2..cee3f39e9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ coverage .idea .env npm-debug.log -.DS_Store \ No newline at end of file +.DS_Store +dist \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 74f00c3ae..893ea1f8c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,12 @@ language: node_js node_js: -- 5.2.0 +- 5.5.0 install: - npm install +before_script: + chmod +x setenv.sh && ./setenv.sh script: -- npm run lint +- npm run lint && npm run build-navbar sudo: false before_deploy: - npm-prepublish --verbose @@ -14,9 +16,59 @@ deploy: api_key: "$NPM_API_KEY" on: tags: true +- provider: s3 + cache_control: private, no-store, no-cache, must-revalidate, max-age=0 + detect_encoding: true + access_key_id: "$AWS_KEY" + secret_access_key: "$AWS_SECRET" + bucket: components.topcoder-dev.com + skip_cleanup: true + local_dir: dist + on: + branch: dev +- provider: s3 + cache_control: private, no-store, no-cache, must-revalidate, max-age=0 + detect_encoding: true + access_key_id: "$QA_AWS_KEY" + secret_access_key: "$QA_AWS_SECRET" + bucket: components.topcoder-qa.com + skip_cleanup: true + local_dir: dist + on: + branch: release +- provider: s3 + cache_control: private, no-store, no-cache, must-revalidate, max-age=0 + detect_encoding: true + access_key_id: "$PROD_AWS_KEY" + secret_access_key: "$PROD_AWS_SECRET" + bucket: components.topcoder.com + skip_cleanup: true + local_dir: dist + on: + branch: master env: + matrix: + - CXX=g++-4.8 global: # NPM_EMAIL - secure: "kmo0afPY5HwJ5SryNY0XRRRekxd1teBR6lSiw2jtjU64sOdpjN7ZhGWy9q0lReGk6hloPfXewXRlpCbcDRFoxvJUdAbQoAN8gq/BxrwGmWlBepgUew1O6WLkpvqXCMe6lUmPEznjil2anjLHfm/cKiqwaY1n8MI+PSna6eNwAlffx72uPL1TzrijJjb7kKqTtjTHZzNiiUn2jZrc35G+MeryZmnd5xXV7cXgYb+flEMC5sDqhcO2aewYiCGMK2b3f7QDJFbGD4+v+HjN1XispJgCxDLTRLJ0HD8HPj87w/7s7PHqCuRUwga1Z2HTRDniqM7mb7TZ5RK990Olp4d1Fw/ApXhUEe4fwgTdN9PSwOzOrY4VGNyytYvJuMVqSVGIHx6Y2BuCt3gzPeM0ev8bjhE9PItYmDTr5FhNI1XLT7PKu+Ktp72bmdEVqNfnv7jXRKba9AeSBQVk70jKMpYAuGI/d/xXeaD2vImBhCflc0UArYfXf+JYMEwzhiMZQfq6csvg0UV/nlD9xidgBLt8QUGdFaQTjsQLt9CWbwcKM7/yFcfkOKEHrM5By1Do4gtSDxmK5F+81210YMO30OVwllHk/f+jOspti9EJv6r0vAZOkKXXL15VsyKQSrF/p6k+brVf0cQcnUMiSiNsl/aEftNW2ys5VCIRJcEol5bHt90=" # NPM_API_KEY - secure: "OoRE9rbEzfAomE1h+PRDHJb0SgcyManma54qs72nwlBA3N4D+hV2Swy0xAidnVqdNvBlA4p9QmcrE0km+ZGVacFEhvleUGFDn5weWde2bukzkkdHtbaYQjyhxbrqfLA9G9vzs1BlHjhrR7Arl2TQuT21XnfagFBQ/LCgCRRu9yHHkZ4TejhuYu/9Fgdq08wnBpLZn7wllUq0OtrDgEgjK7avZ+yv5ob6zccxr3NAAchbdPJ2yUo82VvD+YdQaWbLq9ac5cwa0I6f/RuQUiZcLZItk/H3NuQqg32746QdwIqKpw5dh9wzddbckIK3WVsoJmE+7WpGAOgUU/TxmPijH6j1YwQ9uqcBGsax8RUFR+mG7imlS4YpL67K438JPUG7NCPgnN19zN/c+4WkVoKlLYEncyPp0C3sBbmauR0uuuVBWWUJL/1xBCuPyhT9JGXaocms/Ldshxr+TYf1tfQQ3uzs6wO3cniw3wIgXbgMF6NBwQdQpgUlSh++XJKgIG2I228GI3qMq8zJu/N/1oEKVkKkO8Z9Y3Vd5uo2VMbkp0KJJEfXnE8GZBBaajD+F8jONRQqYEDhK8EMYuqKhsQZMw3RgPtvOZNG+bhcnbElNnMjhd8CTKioKXAslkTP6Esi4Ce2PS6YR4CskjZaO7IT9KRqHvVZN42vlIVbhb2J0RI=" + # AWS_KEY + - secure: QXxHO6Dt5kGDsN2hjSk//0Rf3eHsjOXBq9CdK1kyePLDks6J1wCUp7hT3PMqZG3wH8yd6i4m8mielDdOurquOHVZgdiWaDRw/FMggRcmOGzCl96Afdz199v4ep6aS1YzndEPmwaKp2jFiCsg8LvByJjlHKtGSWc4Yw7+B1WjSygLoZ1eU4TYeig3xfhHE2ENoIGdnk5DDSjZ2xypqc8upmQIbfFya2dd0nMwA8GjEpVWGAYJy3RhOyKlWKGvO2sW+bf3DVCqnYvLRs8kAGh7qQV//rkkycNWFVUsQZkNd0+mX7bi16aJoaj4HSiW4pR/Z0yDa55Lb7CZKdP0sZgKcojUqkUe8faU8N7LQEkK0RYF7zfSAt2y0ns25Nt7os1xUdY/3Xluw0SMzTR5zX8PiEUVtQyFS33T43GKuQT4joYCseIFCqpZ1a5iYemlR9M7DqZDPQ8TkwHn2yzP59ax25U3FB3AYMz6pd2WuIKsa3XU9irJOiGSHXC3YmS2H70cZpL+r4OxtmsjXVR5RQQWcGEurb3vadk/Iq5S4EXKG9M9HKtLCysGPdynw3/+0GlscdIf+O2eZSvFJfahcVvaPSUxLgyOFM+W3sd0EwjGnTWwXu9zN7LbMr57vqzNUZ0mgSNX7qz2+z9V4jBTNRk5DaG5vRr+7uDHgKdtNQHYWUU= + # AWS_SECRET + - secure: yFpLFrM42STWqxw6dlVvyr0hvBbVAOHhkKQ1yQcuZ4MtTktCUOItUt9hZTExy659sJJxeBsT0YD66FdVggV+rTG8KDFGcBZJ8RZqN2AHa1+PMZfzF5N35bkacOWoKd1aAReRWZcVspvHjC1h6lAlVR69qSRqRhOL7/IMTDYrZMMwuS7/Pqb0jfdCJgiOf8uFwAn1ZVsJvDvC7AcxYYNLMA+GFD6ay7GX4zoqBdHwcyupKU4g+Fwjy3673kZwfWKITtzmwmV18p0SIiqQp68K6lvsZ2H7Hw6TXOEMkvda1YMD+lmV/dcc6o6W6bsDAk2cZx5JxSu3trvhkpYpK9JE6FUJ1I5ITiPkAZYAnIjckSM+rLZ6Vanxtmc0zNL6cxc4DTfu7V8dQ2MlPxHV125rMspYeJcxi8u/nlSRSJy2C8jaPGzaBQmHM9HlzJNhpTeR4A/3YqTcbcRMgMxvuuslHEUXDeTV+lqfhw1pLvD+Ut+Q6IBkNVdMDVBoeLvuwtXwyFF4skuEcTvOezmh9VNNrMgx2ZcPnJR85aUD+A/MoYihn1dPr66rh2vNJSRKp4uQdgRnldtLaHh5JgX3cR0PPtSat99shV/YkhhGG3WGl6lqOLPQTDX9XLLm9mTmOQzAK7p6GjRKM1PVWOc+3VA8dc3XTCMjBaFh9zgtSsfDibs= + # QA_AWS_KEY + - secure: "pQzOYdNLlSsQJ7JChT6KKgT0K7h/g+fvXvlKQzBunAeTWZzbkFm0TQDk2OIq8q5btvmvnMtpiAfaBdEQR1XRVWEFBPfHwXp5nbjSU6HPYYaUYamegXIQHg9fQM6YtGm3sFvAMbGKKzKdaCucSmnm1BiliagVMvgM8o7nQbmWC9fzH//p3ajdIETltBqUmOwKw21cx2DNGWbf/d0E3c7NoGgPBn9WXX4J3ekLKxM/YmSxnPG4/tSKFKbJgUrXne4mhUAPc+oZJZ4rIC21Lj48jeZOdDZ5Ch+8gjIaBjQLJMj/AgWBhz1Wc2ugZ8nLQHiYzjksptoPaBynDTYQa/0xRIzqKwxSVCBN1+KXoGOHhm5NZWo4wEKcEhFCRdcUZbdoOHMZZ7Pfzx5uMcf3z8/wJaT8DagOYQkVhcuGivOjLaoAM8Z0PahZF7kFhufu+QSrWAk8YsTv62g6sWo/BD5ylw0Hl0WYFXSmqh9Z5IfVMQ8Lc0AFs66rTIURrf2UrMVtGGB5uMhE60hHGbaXeK+biIP/Km82d7HRHrRV/D/+1xHLmriNjAcYQGM/O52/42tQhu1d59O4+kMopg50/aNctY7sS9b8q950L3qdz5j18sw+r9oITIBDW0JrN5KTwe60YRCwxOvjaq+WydO8nd5PjealC5zQDeuqWZw0mi4QMwo=" + # QA_AWS_SECRET + - secure: "UlNcBJKlpGJPh8uQ1QdSdkdnC+Zgo2nLe0je2ogupinXXrb+xxfhgn+6LGEAVJBwcryHjPDM92bzcEl632xOz6vlfGWe9qvFxm4rYIUwR/6N9kuCFKubq1HoeUZDY6DmNvXqJwm5FewjA2AWLxM0wtchYxoz1ol859TXq8d09Tv1inhlqdIalPNM9WYRdgikURTtqNgIghaF5afxP6sDmLHjlU4dkn9LHC2JhQVZCg97ALmw5AoUDLQaKrjoZ+tF0raNKFr6VlBgpKN88wNkxMR90o9CELtPerS7a4G4XRioLNMSz6Yk9uOZ0uayqb7D+OrMf+gEa5/uqSvnN5a0M7GOlwaC2j2nEBJWXn6fxzNWr4jLb4rBx9M7QNMpWuCTNPmqsqyT1RYAgM87wvC5+XmHCMb9H3XDPQkSqevt1xZ8z+lKZolWkfHNvblbm93PJmLYtSpYfJryZCkV2atU7A25eSEWBohZ+90DvU8gIff/7crLTSYvhiE/PIRukREe+3Iujehs9rbN0ZiRgYBq3QzV1VE/F+VonUPnsTjAzuTmMVmJ0hm04H6lZ5D+RXlo5VPFt8XanJAiWAATTog9V0r3oy5ORzng4s78if9IwR6SDPD9slNBTEBZrvgHTX4F5Mu/vwx2f1BSWoOcjReVdM6ZeIyjbPVCPNA+zX4H3cE=" + # PROD_AWS_KEY + - secure: "YRD3JJso8q+Ed2xKaIUS6TlTQeZFtEVOUSPwtQf1JwFuOkZb5NZ+eAO70KXCGuNRtJm4eWNM2bmEEo9REDHxDAMU69br/5QkumoN3I49/6/TNP43l2P44+C4WAGj8Nnwt5uDBmcYaDbdgCTflZ215ny1kXz36zKSy5LKFoXSCU9Ug5rdeo7pDSSYToF9OUBAiA/o+9YtV45q1lmUvJLUE1KNR7w+ts1oQ67iFhOuraSiJMUdhJjYmTKXVUMkEO/kk7iRIAalrWaF3IogqoIGSmsBUk7MeoMsVwh4fnAiSm9zlRPYP+XVNIaQAJVz5thRgzu7LD7XizmbMQY3YHap9TPeRS7dzf6vFGyqpbGe1p4CIxQMEqydgVIy0l1CYjwXN0RGuWfLk6UtOhtxRJmdSm1+0u0saqxhCixSFClj29sSoJbwlhDwUplBfYTCwTJtF86LFUNeSACmkh84F9355A5tf8oSFI5T+NW0zJehZS+92BLKEIzkMamwsjCdKrVB+vbLxTsBe1iiECCK0Izj+TherCBwUPos/JRru+3VwxCATOOSRI+v5qRK5tDaf6a9CVyR1NbMJFWsfq0SOtLKxujmJP2QR/+x32uMwq8uNYJHdLniVrk1VEd/oARP0DHHgFXzKOjmBtKMDj//gjr1wcQp2f+HEW4Tf5Z0YHcqiIA=" + # PROD_AWS_SECRET + - secure: "XCVGHnw7eYrE6SdgG4kBNAjLK0ZVgo9mGOzOVSA9+caWx6sQJsLWFq8QJsrU7jxqkRIQxM7899NBPUdhHPV9mRYue+0+aH6OwZ6WyylDeyZtwtoq+pnB6uXKlhEnQiz8NmrgI7P92DomX2Dd70AAjrfDpCAbJCQvowLOoO9zkEyyN1c8JQnhA3TJQFfw81WEeh9GBShMyhfD+y3tnpllb4vUdYHYMHE7wEUl7RMjVtFg538V+pEyk/1tXTf+Tvb/ZuazsXCRyzLVZQyXKP0U6SVNsZCu5SZR4Pb2F0JH8PO0p/wRrUoUrHnd7b0yieIlrTAVS5VGeBs1bTG+fgta09mllo0WxedaDoxQGYl9YrOK09EPbjFGJemp/Q9uKoSWymNrkgggMgZlq2CdL/QFbWUfWqhRs9/BWnh4SuQsUW5tsE99qxbFYC1kfuJwR2g8jN3HWAeV37BWC3ct3f5+oOKIgaKQmk2+lH9Z7kII+QOCt10kM21IF5RxZjhvg6AxoORjoRt8Dia+iMSbW1fnU9QCmSC5S0Vvccynjl9ldnPUHOpjEVaoqQOV/6cUlAEw82ySOakyzYpdS8l5DrLwt2Ik3fQhoIgXQe6l1FabGy6ZixcXqoMnDJJh04rUwUD6Tyzn0PZJp3KziDHJMg/BJSTKXmgODRyV2cGxoqbfy8g=" +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 diff --git a/components/Carousel/Carousel.jsx b/components/Carousel/Carousel.jsx new file mode 100644 index 000000000..538398382 --- /dev/null +++ b/components/Carousel/Carousel.jsx @@ -0,0 +1,121 @@ +require('./Carousel.scss') +import classNames from 'classnames' + +import React, { Component } from 'react' +import ReactDOM from 'react-dom' + +import LeftArrowIcon from '../Icons/LeftArrowIcon' +import RightArrowIcon from '../Icons/RightArrowIcon' + +export default class Carousel extends Component { + componentWillMount() { + this.handleResize = this.handleResize.bind(this) + window.addEventListener('resize', this.handleResize) + this.handlePageUp = this.handlePageUp.bind(this) + this.handlePageDown = this.handlePageDown.bind(this) + this.setState({firstVisibleItem: this.props.firstVisibleItem || 0}) + } + + componentWillUnmount() { + window.removeEventListener('resize', this.handleResize) + } + + handleResize() { + this.validatePagers() + } + + componentDidMount() { + this.validatePagers() + } + + componentDidUpdate() { + this.validatePagers() + } + + validatePagers() { + const pageDownClass = classNames( + 'page-down', + { hidden: this.state.firstVisibleItem === 0 } + ) + const pageUpClass = classNames( + 'page-up', + { hidden: this.lastElementVisible(this.state.firstVisibleItem) } + ) + const node = ReactDOM.findDOMNode(this) + const pageDownNode = node.querySelector('.page-down') + const pageUpNode = node.querySelector('.page-up') + pageDownNode.className = pageDownClass + pageUpNode.className = pageUpClass + } + + + lastElementVisible(firstVisibleItem) { + const node = ReactDOM.findDOMNode(this) + const parentNode = node.parentNode + const maxWidth = parentNode.getBoundingClientRect().width + const visibleAreaNode = node.querySelector('.visible-area') + visibleAreaNode.style.width = maxWidth + 'px' + const itemNodes = visibleAreaNode.children + let width = 0 + if (firstVisibleItem > 0) { + // if first item is not visible, account 20px for page-down element + width += 20 + // account the right margin for page-down (see Carousel.scss) + width += 15 + } + for (let i = 0; i < itemNodes.length; i++) { + const itemNode = itemNodes[i] + width += itemNode.getBoundingClientRect().width + if (i < itemNodes.length - 1) { + // account 30px for every carousel-item (see Carousel.scss) + width += 30 + } + if (width > maxWidth) { + return false + } + } + return true + } + + handlePageUp() { + if (!this.lastElementVisible(this.state.firstVisibleItem + 1)) { + const nextFirstVisibleItem = this.state.firstVisibleItem + 1 + this.setState({firstVisibleItem: nextFirstVisibleItem}) + } + } + + handlePageDown() { + if (this.state.firstVisibleItem > 0) { + const nextFirstVisibleItem = this.state.firstVisibleItem - 1 + this.setState({firstVisibleItem: nextFirstVisibleItem}) + } + } + + render() { + const carouselItem = (item, idx) => { + if (idx < this.state.firstVisibleItem) { + return + } + + return ( +
+ {item} +
+ ) + } + + return ( +
+
+ +
+
+ { this.props.children.map(carouselItem) } +
+
+ +
+
+ ) + } +} \ No newline at end of file diff --git a/components/Carousel/Carousel.scss b/components/Carousel/Carousel.scss new file mode 100644 index 000000000..cb699725a --- /dev/null +++ b/components/Carousel/Carousel.scss @@ -0,0 +1,45 @@ +@import 'topcoder/tc-includes'; + +$pager-bg-color: #737380; + +.Carousel { + display: flex; + flex-direction: row; + + .page-down { + width: 20px; + margin-right: 15px; + background-color: $pager-bg-color; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + + &.hidden { + display: none; + } + } + + .page-up { + width: 20px; + background-color: $pager-bg-color; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + + &.hidden { + display: none; + } + } + + .visible-area { + display: flex; + flex-direction: row; + overflow: hidden; + + .carousel-item:not(:first-child) { + margin-left: 30px; + } + } +} \ No newline at end of file diff --git a/components/Carousel/CarouselExamples.jsx b/components/Carousel/CarouselExamples.jsx new file mode 100644 index 000000000..54597e001 --- /dev/null +++ b/components/Carousel/CarouselExamples.jsx @@ -0,0 +1,39 @@ +import React from 'react' +import Carousel from './Carousel' + +import StandardListItem from '../StandardListItem/StandardListItem' + +require('./CarouselExamples.scss') + + +const CarouselExamples = () => ( + +
+

With limited width

+
+ + + + + +
+

With full width

+
+ + + + + +
+

With limited width and custom first visible element

+
+ + + + + +
+
+) + +module.exports = CarouselExamples diff --git a/components/Carousel/CarouselExamples.scss b/components/Carousel/CarouselExamples.scss new file mode 100644 index 000000000..d6d4c8b34 --- /dev/null +++ b/components/Carousel/CarouselExamples.scss @@ -0,0 +1,17 @@ +@import 'topcoder/tc-includes'; + +.Carousel { + .StandardListItem { + padding: 0px; + } +} + +.CarouselExamples { + > p { + border: 1px solid $accent-gray; + margin: 20px 0px; + } + .limited-width { + width: 200px; + } +} \ No newline at end of file diff --git a/components/Carousel/placeholder.svg b/components/Carousel/placeholder.svg new file mode 100644 index 000000000..d3e7220e5 --- /dev/null +++ b/components/Carousel/placeholder.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/components/Icons/HamburgerIcon.jsx b/components/Icons/HamburgerIcon.jsx new file mode 100644 index 000000000..5a5daeda3 --- /dev/null +++ b/components/Icons/HamburgerIcon.jsx @@ -0,0 +1,23 @@ +import React from 'react' + +const HamburgerIcon = ({ width, height, stroke }) => { + const s = (stroke || '#A3A3AE') + return ( + + ico-hamburger + Created with Sketch. + + + + + + + + + + + + ) +} + +export default HamburgerIcon diff --git a/components/Icons/LeftArrowIcon.jsx b/components/Icons/LeftArrowIcon.jsx new file mode 100644 index 000000000..a4e5b13fe --- /dev/null +++ b/components/Icons/LeftArrowIcon.jsx @@ -0,0 +1,19 @@ +import React from 'react' + +const LeftArrowIcon = ({ width, height, fill }) => { + const f = (fill || '#A3A3AE') + return ( + + ico-arrow-big-left + Created with Sketch. + + + + + + + + ) +} + +export default LeftArrowIcon diff --git a/components/Icons/MagnifyGlassIcon.jsx b/components/Icons/MagnifyGlassIcon.jsx new file mode 100644 index 000000000..037ecf491 --- /dev/null +++ b/components/Icons/MagnifyGlassIcon.jsx @@ -0,0 +1,21 @@ +import React from 'react' + +const MagnifyGlassIcon = ({ width, height, stroke }) => { + const s = (stroke || '#A3A3AE') + return ( + + ico-magnify + + + + + + + + + + + ) +} + +export default MagnifyGlassIcon diff --git a/components/Icons/PlaceholderIcon.jsx b/components/Icons/PlaceholderIcon.jsx new file mode 100644 index 000000000..66beca645 --- /dev/null +++ b/components/Icons/PlaceholderIcon.jsx @@ -0,0 +1,12 @@ +import React from 'react' + +const PlaceholderIcon = ({ width, height, fill }) => { + const f = (fill || '#B47DD6') + return ( + + + + ) +} + +export default PlaceholderIcon \ No newline at end of file diff --git a/components/Icons/RightArrowIcon.jsx b/components/Icons/RightArrowIcon.jsx new file mode 100644 index 000000000..73c438db9 --- /dev/null +++ b/components/Icons/RightArrowIcon.jsx @@ -0,0 +1,17 @@ +import React from 'react' + +const RightArrowIcon = ({ width, height, fill }) => { + const f = (fill || '#A3A3AE') + return ( + + ico-arrow-big-right + Created with Sketch. + + + + + + ) +} + +export default RightArrowIcon diff --git a/components/Icons/TopcoderLogo.jsx b/components/Icons/TopcoderLogo.jsx new file mode 100644 index 000000000..c5387db35 --- /dev/null +++ b/components/Icons/TopcoderLogo.jsx @@ -0,0 +1,37 @@ +import React from 'react' + +const TopcoderLogo = ({ width, height }) => { + return ( + + topcoder-logo + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default TopcoderLogo diff --git a/components/Icons/TopcoderMobileLogo.jsx b/components/Icons/TopcoderMobileLogo.jsx new file mode 100644 index 000000000..fd147a5b0 --- /dev/null +++ b/components/Icons/TopcoderMobileLogo.jsx @@ -0,0 +1,23 @@ +import React from 'react' + +const TopcoderMobileLogo = ({ width, height }) => { + return ( + + topcoder-mobile-logo + + + + + + + + + + + + + + ) +} + +export default TopcoderMobileLogo diff --git a/components/Loader/Loader.cjsx b/components/Loader/Loader.cjsx index ea4a674f4..a8464edef 100644 --- a/components/Loader/Loader.cjsx +++ b/components/Loader/Loader.cjsx @@ -6,8 +6,8 @@ React = require 'react' Loader = ->
-
-
+
+
diff --git a/components/Loader/LoaderStyle.scss b/components/Loader/LoaderStyle.scss index db923a85e..4468d4958 100644 --- a/components/Loader/LoaderStyle.scss +++ b/components/Loader/LoaderStyle.scss @@ -10,14 +10,14 @@ justify-content: center; align-items : center; - .container { + .Loader__container { padding : 25px; // background-color: $primary-color; opacity : .98; border-radius : 10px; } - .loader { + .Loader__loader { width: 50px; height: 50px; border-top : 10px solid $grey-light; @@ -30,8 +30,8 @@ -webkit-animation: load8 .8s infinite linear; animation : load8 .8s infinite linear; } - .loader, - .loader:after { + .Loader__loader, + .Loader__loader:after { border-radius: 50%; } diff --git a/components/MenuBar/MenuBar.jsx b/components/MenuBar/MenuBar.jsx index 112e80da1..919bd2ec9 100644 --- a/components/MenuBar/MenuBar.jsx +++ b/components/MenuBar/MenuBar.jsx @@ -30,7 +30,7 @@ export default class MenuBar extends Component { const itemClass = classNames({ [orientation]: true, mobile: this.state.mobile, - selected: window.location.href.indexOf(item.link) !== -1 + selected: item.selected || (item.regex && window.location.href.match(item.regex) !== null) }) const linkTarget = item.target || '_self' diff --git a/components/MenuBar/MenuBarExamples.jsx b/components/MenuBar/MenuBarExamples.jsx index 72d37d6e1..58700351d 100644 --- a/components/MenuBar/MenuBarExamples.jsx +++ b/components/MenuBar/MenuBarExamples.jsx @@ -6,7 +6,7 @@ require('./MenuBarExamples.scss') const primaryNavigationItems = [ {img: '../components/MenuBar/nav-community.svg', text: 'Community', link: '/community'}, {img: '../components/MenuBar/nav-compete.svg', text: 'Compete', link: '/compete', selected: true}, - {img: '../components/MenuBar/nav-learn.svg', text: 'Learn', link: '/MenuBarExamples'} + {img: '../components/MenuBar/nav-learn.svg', text: 'Learn', link: '/MenuBarExamples', regex: '/MenuBar*'} ] const MenuBarExample = () => ( diff --git a/components/Navbar/Navbar.jsx b/components/Navbar/Navbar.jsx index 9392a2c19..b5f7b17cf 100755 --- a/components/Navbar/Navbar.jsx +++ b/components/Navbar/Navbar.jsx @@ -1,35 +1,99 @@ require('./Navbar.scss') import MenuBar from '../MenuBar/MenuBar' -import React, { Component } from 'react' +import React, {PropTypes, Component} from 'react' import SearchBar from '../SearchBar/SearchBar' import QuickLinks from '../QuickLinks/QuickLinks' import UserDropdownMenu from '../UserDropdownMenu/UserDropdownMenu' +import TopcoderLogo from '../Icons/TopcoderLogo' +import TopcoderMobileLogo from '../Icons/TopcoderMobileLogo' +import HamburgerIcon from '../Icons/HamburgerIcon' +import MagnifyGlassIcon from '../Icons/MagnifyGlassIcon' const primaryNavigationItems = [ - {img: '../components/MenuBar/nav-community.svg', text: 'Community', link: 'javascript:;'}, - {img: '../components/MenuBar/nav-compete.svg', text: 'Compete', link: 'javascript:;', selected: true}, - {img: '../components/MenuBar/nav-learn.svg', text: 'Learn', link: 'javascript:;'} + {img: require('./nav-community.svg'), text: 'Community', link: '/community', regex: '/community?\?'}, + {img: require('./nav-compete.svg'), text: 'Compete', link: '/compete', regex: '/compete?\?'}, + {img: require('./nav-learn.svg'), text: 'Learn', link: '/learn', regex: '/learn?\?'} ] +// properties: username, userImage, domain, mobileMenuUrl, mobileSearchUrl, searchSuggestionsFunc +// searchSuggestionsFunc should return a Promise object + class Navbar extends Component { + + constructor(props) { + super(props) + this.handleTermChange = this.handleTermChange.bind(this) + this.handleMobileClick = this.handleMobileClick.bind(this) + this.handleSearch = this.handleSearch.bind(this) + this.state = { recentTerms: [] } + } + + handleTermChange(oldTerm, searchTerm, reqNo, callback) { + // TODO should we check for the return value of the search suggestion function to be promise? + this.props.searchSuggestionsFunc.apply(this, [searchTerm]) + .then(data => { + callback.apply(null, [reqNo, data]) + }) + .catch(error => { + callback.apply(null, [reqNo, [], error]) + }) + } + + handleSearch(searchTerm) { + this.props.onSearch.apply(this, [searchTerm]) + } + + handleMobileClick(se) { + const mobileMenuLink = se.target.querySelector('.mobile-wrap > a') + if (mobileMenuLink) { + mobileMenuLink.click() + } + } + render() { + const username = this.props.username + const userImage = this.props.userImage + const domain = this.props.domain + const mobileMenuUrl = this.props.mobileMenuUrl + const mobileSearchUrl = this.props.mobileSearchUrl + const homePageUrl = '//www.' + domain return (
-
-
-
- +
+ +
+
+ +
+
+
+
-
-
-
- +
+
+
+
) } } +Navbar.propTypes = { + searchSuggestionsFunc : PropTypes.func.isRequired, + onSearch : PropTypes.func.isRequired, + username : PropTypes.string, + userImage : PropTypes.string, + domain : PropTypes.string.isRequired, + mobileMenuUrl : PropTypes.string, + mobileSearchUrl : PropTypes.string +} + +Navbar.defaultProps = { + mobileMenuUrl : '/menu', + mobileSearchUrl : '/search' +} + export default Navbar diff --git a/components/Navbar/Navbar.scss b/components/Navbar/Navbar.scss index d78258c95..b71349d6b 100755 --- a/components/Navbar/Navbar.scss +++ b/components/Navbar/Navbar.scss @@ -1,4 +1,5 @@ -@import "topcoder/tc-includes"; +// this is to include tc styles in the output library +@import "topcoder/tc-styles"; $navbar-bg: #FAFAFB; $border-color: #CFCFD2; @@ -13,9 +14,6 @@ $super-wide: 1376px; background-color: $navbar-bg; border-bottom: 1px solid $border-color; padding: 10px 20px; - position: fixed; - top: 0; - left: 0; width: 100%; @media screen and (max-width: $mobile) { @@ -27,18 +25,32 @@ $super-wide: 1376px; .topcoder-logo { width: 56px; height: 24px; - background-color: $placeholder-bg; margin-right: 40px; flex: 0 0 auto; + display: flex; + align-items: center; @media screen and (min-width: $super-wide) { width: 155px; + + &.non-mobile { + display: flex; + } + &.mobile { + display: none; + } } - @media screen and (max-width: $mobile) { + @media screen and (max-width: $super-wide) { width: 40px; margin-right: 15px; - order: 1; + + &.non-mobile { + display: none; + } + &.mobile { + display: flex; + } } } @@ -53,23 +65,30 @@ $super-wide: 1376px; @media screen and (max-width: $mobile) { flex: 0 0 60px; order: 2; - background-color: $accent-gray; width: 60px; height: 40px; display: flex; - margin-right: 10px; + margin-right: 0px; + justify-content: center; + align-items: center; } - .icon-placeholder { + &:hover { + background-color: $accent-gray; + + .mobile-wrap { + svg > g > g { + stroke: $white; + } + } + } + + .mobile-wrap { display: none; @media screen and (max-width: $mobile) { display: block; - height: 25px; - width: 25px; - background-color: $placeholder-bg; margin: auto; - cursor: pointer; } } @@ -105,24 +124,34 @@ $super-wide: 1376px; } } - .collapse-group { + .menu-wrap { flex: 0 0 auto; position: relative; + display: flex; + align-items: center; @media screen and (max-width :$mobile) { width: 40px; height: 40px; order: 3; cursor: pointer; + + &:hover { + background-color: $accent-gray; + + .mobile-wrap { + svg > g > g { + stroke: $white; + } + } + } } - .icon-placeholder { + .mobile-wrap { + display: none; @media screen and (max-width: $mobile) { display: block; - height: 24px; - width: 24px; - background-color: $placeholder-bg; position: absolute; top: 50%; left: 50%; diff --git a/components/Navbar/NavbarExample.jsx b/components/Navbar/NavbarExample.jsx index 9aba6f991..703a5d372 100755 --- a/components/Navbar/NavbarExample.jsx +++ b/components/Navbar/NavbarExample.jsx @@ -1,8 +1,137 @@ +require('./NavbarExample.scss') + import Navbar from './Navbar' -import React from 'react' +import React, {Component, PropTypes} from 'react' +import fetch from 'isomorphic-fetch' +import _ from 'lodash' + +const suggest = (searchTerm) => { + return fetch('https://internal-api.topcoder-dev.com/v3/tags/_suggest/?q=' + searchTerm) + .then(response => { + if (response.status >= 200 && response.status < 400) { + return Promise.resolve(response) + } else { + return Promise.reject(new Error(response.statusText)) + } + }) + .then(response => { + return response.json() + }) + .then(data => { + const tags = _.get(data, 'result.content') + return tags.map(tag => { + return tag.text + }) + }) +} + +class SearchResults extends Component { + constructor(props) { + super(props) + } + + render() { + const memberRender = (member) => { + const style = {color : member.color} + return ( +
+

Handle: {member.handle}

+

Rank: {member.rank}

+
+ ) + } + const challengeRender = (challenge) => { + return ( +
+

Name: {challenge.name}

+

Starte Date: {challenge.startDate}

+

End Date: {challenge.endDate}

+
+ ) + } + const searchResults = this.props.results + let exactMatch = null + if (searchResults.member) { + exactMatch =

Member

{ memberRender(searchResults.member) }
+ } + let matchedMembers = null + if (searchResults.members) { + matchedMembers =

Members

{ searchResults.members.map(memberRender) }
+ } + let matchedChallenges = null + if (searchResults.challenges) { + matchedChallenges =

Challenges

{ searchResults.challenges.map(challengeRender) }
+ } + + return ( +
+ { exactMatch } + { matchedMembers } + { matchedChallenges } +
+ ) + } +} + +SearchResults.propTypes = { + memberMatch : PropTypes.object, + membersMatch : PropTypes.array, + challengesMatch : PropTypes.array +} + +class NavbarExample extends Component { + constructor(props) { + super(props) + this.state = {searchResults: {}} + this.search = this.search.bind(this) + } + + search(searchTerm) { + let results = {} + if (searchTerm === 'Java') { + results = { + hasResults: true, + member: { + handle : 'Java', + color : 'blue', + rank : 1423 + }, + members: [{ + handle : 'Javaone', + color : 'red', + rank : 2045 + }, { + handle : 'ExpertJava', + color : 'yellow', + rank : 1608 + }], + challenges: [{ + name : 'Tags Lambda Service', + startDate : 'April 4, 2016', + endDate : 'April 20, 2016' + }, { + name : 'Member Search Service', + startDate : 'March 14, 2016', + endDate : 'March 27, 2016' + }] + } + } + this.setState({searchResults: results}) + } -const NavbarExample = () => ( - -) + render() { + return ( +
+

Logged In Example

+ +

Non Logged In Example

+ +
+ +
+
+ ) + } +} module.exports = NavbarExample diff --git a/components/Navbar/NavbarExample.scss b/components/Navbar/NavbarExample.scss new file mode 100644 index 000000000..abced894b --- /dev/null +++ b/components/Navbar/NavbarExample.scss @@ -0,0 +1,15 @@ +@import "topcoder/tc-styles"; + +.search-results { + .exact-match, + .members, + .challenges { + border-bottom: 1px solid $accent-gray; + margin: 10px 0px; + padding: 0px 10px 10px 10px; + + h2 { + margin-bottom: 10px; + } + } +} diff --git a/components/Navbar/nav-community.svg b/components/Navbar/nav-community.svg new file mode 100644 index 000000000..d3afe2b80 --- /dev/null +++ b/components/Navbar/nav-community.svg @@ -0,0 +1,14 @@ + + + + nav-community + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/components/Navbar/nav-compete.svg b/components/Navbar/nav-compete.svg new file mode 100644 index 000000000..1e43d1940 --- /dev/null +++ b/components/Navbar/nav-compete.svg @@ -0,0 +1,12 @@ + + + + nav-compete + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/components/Navbar/nav-learn.svg b/components/Navbar/nav-learn.svg new file mode 100644 index 000000000..d45a64145 --- /dev/null +++ b/components/Navbar/nav-learn.svg @@ -0,0 +1,18 @@ + + + + nav-learn + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/QuickLinks/QuickLinks.jsx b/components/QuickLinks/QuickLinks.jsx index fcec8056d..94db46460 100644 --- a/components/QuickLinks/QuickLinks.jsx +++ b/components/QuickLinks/QuickLinks.jsx @@ -1,34 +1,32 @@ require('./QuickLinks.scss') -import React, { Component } from 'react' +import React from 'react' import Dropdown from '../Dropdown/Dropdown' import StandardListItem from '../StandardListItem/StandardListItem' -class QuickLinks extends Component { - render() { - return ( -
- - -
    -
  • -
    - -
  • -
  • -
    - -
  • -
  • -
    - -
  • -
-
-
- ) - } +const QuickLinks = ({domain}) => { + const orLink = '//software.' + domain + const arenaLink = '//arena.' + domain + const arenaAppletLink = '//' + domain + '/contest/arena/ContestAppletProd.jnlp' + return ( +
+ + +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+ ) } export default QuickLinks \ No newline at end of file diff --git a/components/QuickLinks/QuickLinks.scss b/components/QuickLinks/QuickLinks.scss index 18238b450..8f1490c84 100644 --- a/components/QuickLinks/QuickLinks.scss +++ b/components/QuickLinks/QuickLinks.scss @@ -1,6 +1,6 @@ @import "topcoder/tc-includes"; -$logo-placeholder-bg: #B47DD6; +$link-text-hover-color: #7A7F83; .QuickLinks { position: relative; @@ -22,22 +22,22 @@ $logo-placeholder-bg: #B47DD6; .dropdown-menu-list-item { display: inline-block; - cursor: pointer; - - .icon-placeholder { - width: 30px; - height: 30px; - background-color: $logo-placeholder-bg; - margin: 0 auto; - margin-bottom: 6px; - } + cursor: pointer; .StandardListItem { padding: 0; + &:active, + &:hover { + .label { + color: $link-text-hover-color; + } + } + .label { font-size: 12px; line-height: 26px; + color: $accent-gray-dark; } } } diff --git a/components/QuickLinks/QuickLinksExample.jsx b/components/QuickLinks/QuickLinksExample.jsx index d7ff74f87..4a4727993 100644 --- a/components/QuickLinks/QuickLinksExample.jsx +++ b/components/QuickLinks/QuickLinksExample.jsx @@ -5,7 +5,7 @@ import QuickLinks from './QuickLinks' const QuickLinksExample = () => (
- +
) diff --git a/components/QuickLinks/placeholder.svg b/components/QuickLinks/placeholder.svg new file mode 100644 index 000000000..d3e7220e5 --- /dev/null +++ b/components/QuickLinks/placeholder.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/components/Router/Router.jsx b/components/Router/Router.jsx index 5d900da7b..f162f2c23 100644 --- a/components/Router/Router.jsx +++ b/components/Router/Router.jsx @@ -25,6 +25,8 @@ import SearchSuggestionsExamples from '../SearchSuggestions/SearchSuggestion import SearchBarExample from '../SearchBar/SearchBarExamples.jsx' import NavbarExample from '../Navbar/NavbarExample.jsx' import TCFooterExamples from '../TCFooter/TCFooterExamples.jsx' +import CarouselExamples from '../Carousel/CarouselExamples.jsx' +import SubNavExamples from '../SubNav/SubNavExamples.jsx' import TaggedValueExamples from '../TaggedValue/TaggedValueExamples.jsx' import SubTrackDetailsExample from '../SubTrackDetails/SubTrackDetailsExample.jsx' import PrizeExamples from '../Prize/PrizeExamples.jsx' @@ -79,11 +81,15 @@ const Component = () => ( - + - + + + + + diff --git a/components/SearchBar/SearchBar.jsx b/components/SearchBar/SearchBar.jsx index 62a39f926..18b4ee291 100644 --- a/components/SearchBar/SearchBar.jsx +++ b/components/SearchBar/SearchBar.jsx @@ -1,34 +1,101 @@ require('./SearchBar.scss') -import React, {Component} from 'react' +import React, {Component, PropTypes} from 'react' +import ReactDOM from 'react-dom' import SearchSuggestions from '../SearchSuggestions/SearchSuggestions' +import Loader from '../Loader/Loader' +import classNames from 'classnames' //states: empty, filled, focused class SearchBar extends Component { constructor(props) { super(props) - this.state = { searchState: 'empty' } + const initialTerm = this.getQueryStringValue(props.searchTermKey) + this.state = { + searchState: initialTerm.length > 0 ? 'filled' : 'empty', + suggestions: [], + searchValue: initialTerm + } this.onFocus = this.onFocus.bind(this) - this.onBlur = this.onBlur.bind(this) + this.onChange = this.onChange.bind(this) this.onKeyUp = this.onKeyUp.bind(this) this.clearSearch = this.clearSearch.bind(this) + this.search = this.search.bind(this) + this.handleSuggestionSelect = this.handleSuggestionSelect.bind(this) + this.handleOutsideClick = this.handleOutsideClick.bind(this) + this.handleSuggestionsUpdate = this.handleSuggestionsUpdate.bind(this) + } + + getQueryStringValue (key) { + return unescape(window.location.href.replace(new RegExp('^(?:.*[&\\?]' + escape(key).replace(/[\.\+\*]/g, '\\$&') + '(?:\\=([^&]*))?)?.*$', 'i'), '$1')) + } + + componentDidMount() { + window.addEventListener('click', this.handleOutsideClick) + } + + componentWillUnmount() { + window.removeEventListener('click', this.handleOutsideClick) + } + + handleOutsideClick(evt) { + let t = evt.target + let i = 0 + const searchBarNode = ReactDOM.findDOMNode(this) + let clickedInside = false + while(t !== null && i < 10) { + i++ + if (t === searchBarNode) { + clickedInside = true + break + } + t = t.parentNode + } + if (!clickedInside) { + if(this.state.searchValue) { + this.setState({ searchState: 'filled' }) + } else { + this.setState({ searchState: 'empty' }) + } + } } onFocus() { this.setState({ searchState: 'focused' }) } - onBlur() { - if(this.state.searchValue) { - this.setState({ searchState: 'filled' }) - } else { - this.setState({ searchState: 'empty' }) + handleSuggestionsUpdate(requestNo, data) { + if (requestNo === this.state.maxRequestNo) { + this.setState({loading: false, suggestions: data, selectedSuggestionIdx: null}) } } - onKeyUp() { - this.setState({ searchValue: this.refs.searchValue.value }) + onChange() { + const oldTerm = this.state.searchValue + const newTerm = this.refs.searchValue.value ? this.refs.searchValue.value.trim() : '' + this.setState( + function(prevState) { + const rc = prevState.requestNo ? prevState.requestNo + 1 : 1 + return { + searchValue: this.refs.searchValue.value, + requestNo: rc, + maxRequestNo: rc, + loading: newTerm.length > 0, + searchState: 'focused' + } + }, + function() { + if (newTerm.length > 0) { + this.props.onTermChange.apply(null, [ + oldTerm, + newTerm, + this.state.requestNo, + this.handleSuggestionsUpdate + ]) + } + } + ) } clearSearch() { @@ -37,59 +104,121 @@ class SearchBar extends Component { this.setState({ searchState: 'empty' }) } + onKeyUp(evt) { + const eventKey = evt.keyCode + evt.stopPropagation() + evt.preventDefault() + // if return is pressed + if (eventKey === 13) { + this.setState({ searchState: 'filled' }, function() { + this.search() + }) + } else if (eventKey === 39) { // right arrow key is pressed + const suggestion = this.state.suggestions[0] + if (suggestion) { + this.refs.searchValue.value = suggestion + // trigger the change event handler + this.onChange() + } + } else if (eventKey === 38) { // up arrow key + const currSelectedIdx = this.state.selectedSuggestionIdx + if (currSelectedIdx) { // index is none of (undefined, null, 0) + const suggestionIdx = currSelectedIdx - 1 + const suggestion = this.state.suggestions[suggestionIdx] + this.refs.searchValue.value = suggestion + this.setState({ + selectedSuggestionIdx : suggestionIdx, + searchValue: suggestion + }) + } + } else if (eventKey === 40) { // down arrow key + const currSelectedIdx = this.state.selectedSuggestionIdx + // index is none of (undefined, null, 0) + if (typeof currSelectedIdx === 'undefined' + || currSelectedIdx === null + || this.state.suggestions.length > currSelectedIdx + 1) { + const suggestionIdx = typeof currSelectedIdx === 'number' ? currSelectedIdx + 1 : 0 + const suggestion = this.state.suggestions[suggestionIdx] + this.refs.searchValue.value = suggestion + this.setState({ + selectedSuggestionIdx: suggestionIdx, + searchValue: suggestion + }) + } + } + } + + handleSuggestionSelect(selectedTerm) { + this.setState({ searchValue: selectedTerm, searchState: 'filled' }, function() { + this.search() + }) + } + + search() { + const searchTerm = this.state.searchValue ? this.state.searchValue.trim() : '' + if(searchTerm.length > 0) { + this.props.onSearch.apply(this, [searchTerm]) + } + } + render() { - /* Sample JSON data */ - const recentList = ['Photoshop', 'IBM Bluemix', 'Sketch', 'iOS Icon Design Challenges', 'React.js'] - const popularList = ['Java', 'Javascript', 'CoffeeScript'] + const recentList = this.props.recentTerms + const popularList = this.state.suggestions const searchState = this.state.searchState const searchValue = this.state.searchValue - let classString = 'SearchBar' let typeaheadText = '' - let isPartial = false - let popularForDisplay = [] if(searchValue) { - for(let i=0; i{ searchValue }{ popularList[i].substring(searchValue.length) }) - } } - } else { - popularForDisplay = '' typeaheadText = '' } - if(searchState === 'empty') { - classString += ' state-empty' - typeaheadText = '' - } else if(searchState === 'focused') { - classString += ' state-focused' - } else if(searchState === 'filled') { - classString += ' state-filled' - } + const sbClasses = classNames('SearchBar', { + 'state-empty' : searchState === 'empty', + 'state-focused': searchState === 'focused', + 'state-filled' : searchState === 'filled' + }) + const results = this.state.loading === true + ?
+ : return ( -
- +
+ { typeaheadText } -
+
- +
+ {results} +
) } } + +SearchBar.propTypes = { + onSearch : PropTypes.func.isRequired, + onTermChange : PropTypes.func.isRequired, + recentTerms : PropTypes.array, + searchTermKey: PropTypes.string +} + +SearchBar.defaultProps = { + recentTerms : [], + searchTermKey : 'q' +} + export default SearchBar diff --git a/components/SearchBar/SearchBar.scss b/components/SearchBar/SearchBar.scss index 6fb889678..63ce73fc0 100644 --- a/components/SearchBar/SearchBar.scss +++ b/components/SearchBar/SearchBar.scss @@ -51,10 +51,11 @@ $active-icon-wrap-bg: #888894; } .search-icon-wrap { + cursor: pointer; background-color: $active-icon-wrap-bg; } - .SearchSuggestions { + .suggestions-panel { display: block; } } @@ -67,10 +68,11 @@ $active-icon-wrap-bg: #888894; } .search-icon-wrap { + cursor: pointer; background-color: $active-icon-wrap-bg; } - .SearchSuggestions { + .suggestions-panel { display: none; } } @@ -90,6 +92,16 @@ $active-icon-wrap-bg: #888894; transform: translateY(-50%); background-color: transparent; z-index: 20; + padding-left: 0px; + } + + .search-bar__text { + + &:hover, + &:focus { + border: none; + box-shadow: none; + } } .search-typeahead-text { @@ -126,15 +138,21 @@ $active-icon-wrap-bg: #888894; } } - .SearchSuggestions { + .suggestions-panel { display: none; position: absolute; top: 43px; left: 0; width: 100%; + z-index: 1000; + + .loading-suggestions { + } - strong { - font-weight: 600; + .SearchSuggestions { + strong { + font-weight: 600; + } } } } \ No newline at end of file diff --git a/components/SearchBar/SearchBarExamples.jsx b/components/SearchBar/SearchBarExamples.jsx index 27b6957b0..d785914f5 100644 --- a/components/SearchBar/SearchBarExamples.jsx +++ b/components/SearchBar/SearchBarExamples.jsx @@ -1,8 +1,32 @@ import SearchBar from './SearchBar' import React from 'react' +const recentTerms = ['Photoshop', 'IBM Bluemix', 'Sketch', 'iOS Icon Design Challenges', 'React.js'] + +const handleTermChange = (oldTerm, searchTerm, reqNo, callback) => { + const suggestions = [] + setTimeout(() => { + if (searchTerm && 'java'.indexOf(searchTerm.toLowerCase()) !== -1) { + suggestions.push('Java') + suggestions.push('JavaScript') + } else if (searchTerm && 'javascript'.indexOf(searchTerm.toLowerCase()) !== -1) { + suggestions.push('JavaScript') + } else if (searchTerm && 'coffee'.indexOf(searchTerm.toLowerCase()) !== -1) { + suggestions.push('Coffee') + suggestions.push('CoffeeScript') + } else if (searchTerm && 'coffeescript'.indexOf(searchTerm.toLowerCase()) !== -1) { + suggestions.push('CoffeeScript') + } + callback.apply(null, [reqNo, suggestions]) + }, Math.floor((Math.random() * 1000) + 800)) +} + +const search = (term) => { + console.log('Searched for term: ' + term) +} + const SearchBarExamples = () => ( - + ) module.exports = SearchBarExamples diff --git a/components/SearchSuggestions/SearchSuggestions.jsx b/components/SearchSuggestions/SearchSuggestions.jsx index eb2ee9123..98b4eba6f 100644 --- a/components/SearchSuggestions/SearchSuggestions.jsx +++ b/components/SearchSuggestions/SearchSuggestions.jsx @@ -1,19 +1,58 @@ require('./SearchSuggestions.scss') -import React, { Component } from 'react' +import React, { Component, PropTypes } from 'react' import StandardListItem from '../StandardListItem/StandardListItem' import Panel from '../Panel/Panel' +import classNames from 'classnames' +// properties: onSuggestionSelect, recentSearch, popularList class SearchSuggestions extends Component { constructor(props) { super(props) this.state = { iSEmpty: true } + this.handleClick = this.handleClick.bind(this) + } + + handleClick(evt) { + const term = evt.currentTarget.getAttribute('data-term') + evt.stopPropagation() + this.props.onSuggestionSelect.apply(this, [term]) } render() { const recentList = this.props.recentSearch const popularList = this.props.popularSearch + const suggestionItem = (term, i) => { + let labelDOM = term + const searchTerm = this.props.searchTerm + let exactMatch = false + if (searchTerm.length > 0) { + const idx = term.toLowerCase().indexOf(searchTerm.toLowerCase()) + if (idx !== -1) { + // check if exact match + exactMatch = idx === 0 && term.length === searchTerm.length + // prepare DOM for the content to be rendered under StandardListItem + labelDOM = ( + + { term.substring(0, idx) } + { searchTerm } + { term.substring(idx + searchTerm.length) } + + ) + } + } + // prepares css class for li + const itemClasses = classNames( + { selected : exactMatch } + ) + // prepares and returns the DOM for each popular/recent search item + return ( +
  • + +
  • + ) + } const recentSearches = !recentList ? '' : (
    @@ -28,14 +67,10 @@ class SearchSuggestions extends Component {
      - { - !recentList ? '' : recentList.map((search, i) => { - return
    • - }) - } + { recentList.map(suggestionItem) }
    { - popularList ? '' : ( + popularList.length !== 0 ? '' : ( Learn more about the new Search here @@ -54,19 +89,19 @@ class SearchSuggestions extends Component {
      - { - popularList.map((search, i) => { - return
    • - }) - } + { popularList.map(suggestionItem) }
    ) + const ssClasses = classNames( + 'SearchSuggestions', + { 'empty-state' : recentList && !popularList } + ) return ( -
    +
    { popularSearch } { recentSearches }
    @@ -74,4 +109,17 @@ class SearchSuggestions extends Component { } } +SearchSuggestions.propTypes = { + onSuggestionSelect : PropTypes.func.isRequired, + recentSearch : PropTypes.array, + popularSearch : PropTypes.array, + searchTerm : PropTypes.string +} + +SearchSuggestions.defaultProps = { + recentSearch : [], + popularSearch : [], + searchTerm : '' +} + export default SearchSuggestions \ No newline at end of file diff --git a/components/SearchSuggestions/SearchSuggestions.scss b/components/SearchSuggestions/SearchSuggestions.scss index 5658fc2a7..52668c273 100644 --- a/components/SearchSuggestions/SearchSuggestions.scss +++ b/components/SearchSuggestions/SearchSuggestions.scss @@ -68,6 +68,10 @@ padding: 0; padding-bottom: 5px; + li.selected { + background-color: $gray-light; + } + .StandardListItem { align-items: initial; text-align: left; @@ -83,6 +87,7 @@ text-overflow: ellipsis; width: 95%; overflow-x: hidden; + overflow-y: hidden; } } diff --git a/components/SearchSuggestions/SearchSuggestionsExamples.jsx b/components/SearchSuggestions/SearchSuggestionsExamples.jsx index a9ac097cb..48391bd78 100644 --- a/components/SearchSuggestions/SearchSuggestionsExamples.jsx +++ b/components/SearchSuggestions/SearchSuggestionsExamples.jsx @@ -4,12 +4,16 @@ import SearchSuggestions from './SearchSuggestions' const recentList = ['Photoshop', 'IBM Bluemix', 'Sketch', 'iOS Icon Design Challenges', 'React.js'] const popularList = ['Java', 'Javascript', 'CoffeeScript'] +const handleSelection = (selectedTerm) => { + console.log('Selected term: ' + selectedTerm) +} + const SearchSuggestionsExamples = () => { return (
    - - - + + +
    ) } diff --git a/components/StandardListItem/StandardListItem.jsx b/components/StandardListItem/StandardListItem.jsx index 6e0ff92ed..2e89f2aca 100644 --- a/components/StandardListItem/StandardListItem.jsx +++ b/components/StandardListItem/StandardListItem.jsx @@ -1,4 +1,4 @@ -import {PropTypes, Component } from 'react' +import {PropTypes } from 'react' import React from 'react' require('./StandardListItemStyles.scss') @@ -8,39 +8,45 @@ require('./StandardListItemStyles.scss') // labelText: The text for the label // placeIcon: defines the position of the icon. Either: top | left | right. Default to top -class StandardListItem extends Component { - constructor(props) { - super(props) - } - render() { - const classes = 'StandardListItem transition ' + this.props.placeIcon - let label - let icon +const StandardListItem = ({showIcon, showLabel, imgSrc, labelText, linkUrl, linkTarget='_self', placeIcon='top'}) => { + const classes = 'StandardListItem transition ' + placeIcon + let label + let icon + let item - if (this.props.showLabel){ - label =

    {this.props.labelText}

    - } + if (showLabel) { + label =

    {labelText}

    + } - if (this.props.showIcon){ - icon = - } + if (showIcon) { + icon = + } - return (
    {label}{icon}
    ) + if (linkUrl) { + item = + {label}{icon} + } else { + item =
    {label}{icon}
    } + + return item } StandardListItem.propTypes = { - showIcon : PropTypes.bool, - showLabel : PropTypes.bool, - imgSrc : PropTypes.string, - labelText : PropTypes.node, - placeIcon : PropTypes.string + showIcon : PropTypes.bool, + showLabel : PropTypes.bool, + imgSrc : PropTypes.string, + labelText : PropTypes.node, + linkUrl : PropTypes.string, + linkTarget : PropTypes.string, + placeIcon : PropTypes.string } StandardListItem.defaultProps = { - showIcon: true, - showLabel: true, - placeIcon: 'top' + showIcon : true, + showLabel : true, + linkTarget : '_self', + placeIcon : 'top' } export default StandardListItem diff --git a/components/StandardListItem/StandardListItemExamples.jsx b/components/StandardListItem/StandardListItemExamples.jsx index f3b30c335..499ac6ccf 100644 --- a/components/StandardListItem/StandardListItemExamples.jsx +++ b/components/StandardListItem/StandardListItemExamples.jsx @@ -16,7 +16,7 @@ const StandardListItemExamples = () => (

    Icon on the Right

    - +

    Icon Hidden

    @@ -24,6 +24,18 @@ const StandardListItemExamples = () => (

    Label Hidden

    +

    With Link In Self

    + + +

    With Link In New Tab

    + + +

    Link: Icon on the Right

    + + +

    Link: Icon on the Left

    + +
    ) diff --git a/components/SubNav/SubNav.jsx b/components/SubNav/SubNav.jsx new file mode 100644 index 000000000..68234a9d0 --- /dev/null +++ b/components/SubNav/SubNav.jsx @@ -0,0 +1,48 @@ +require('./SubNav.scss') + +import React from 'react' +import Carousel from '../Carousel/Carousel' +import StandardListItem from '../StandardListItem/StandardListItem' + +const tcSubNav = { + compete : [ + {img: require('./placeholder.svg'), text: 'Design Challenges', link: '/challenges/design/active'}, + {img: require('./placeholder.svg'), text: 'Development Challenges', link: '/challenges/develop/active'}, + {img: require('./placeholder.svg'), text: 'Data Science Challenges', link: '/challenges/data/active'}, + {img: require('./placeholder.svg'), text: 'Competitive Programming', link: process.env.ARENA_URL} + ], + learn : [ + {img: require('./placeholder.svg'), text: 'Getting Started', link: '/getting-started'}, + {img: require('./placeholder.svg'), text: 'Design Challenges', link: '/community/design'}, + {img: require('./placeholder.svg'), text: 'Development Challenges', link: '/community/develop'}, + {img: require('./placeholder.svg'), text: 'Data Science Challenges', link: '/community/data-science'}, + {img: require('./placeholder.svg'), text: 'Competitive Programming', link: '/community/competitive programming/'} + ], + community : [ + {img: require('./placeholder.svg'), text: 'Overview', link: '/community/members'}, + {img: require('./placeholder.svg'), text: 'TCO16', link: process.env.TCO16_URL}, + {img: require('./placeholder.svg'), text: 'Programs', link: '/community/member-overview'}, + {img: require('./placeholder.svg'), text: 'Forums', link: process.env.FORUMS_APP_URL}, + {img: require('./placeholder.svg'), text: 'Statistics', link: '/community/statistics'}, + {img: require('./placeholder.svg'), text: 'Events', link: '/community/events'}, + {img: require('./placeholder.svg'), text: 'Blog', link: '/blog'} + ] +} + +const SubNav = ({ primaryMenu = 'compete' }) => { + const subNav = tcSubNav[primaryMenu] + const subNavMap = (item, idx) => { + return ( + + ) + } + return ( +
    + + { subNav.map(subNavMap) } + +
    + ) +} + +export default SubNav \ No newline at end of file diff --git a/components/SubNav/SubNav.scss b/components/SubNav/SubNav.scss new file mode 100644 index 000000000..78fe29e69 --- /dev/null +++ b/components/SubNav/SubNav.scss @@ -0,0 +1,21 @@ +@import 'topcoder/tc-includes'; + +$subnav-item-bg-color: #B47DD6; +$subnav-item-text-color: #7A7F83; + +.SubNav { + background-color: $accent-gray-dark; + + .StandardListItem { + padding: 20px 0px; + + .label { + color: $subnav-item-text-color; + } + + .label:active, + .label:hover { + color: $white; + } + } +} \ No newline at end of file diff --git a/components/SubNav/SubNavExamples.jsx b/components/SubNav/SubNavExamples.jsx new file mode 100644 index 000000000..ebd0c60ab --- /dev/null +++ b/components/SubNav/SubNavExamples.jsx @@ -0,0 +1,24 @@ +import React from 'react' +import SubNav from './SubNav' + +require('./SubNavExamples.scss') + +const SubNavExamples = () => ( + +
    +

    Compete Sub Navigation

    +
    + +
    +

    Learn Sub Navigation

    +
    + +
    +

    Community Sub Navigation

    +
    + +
    +
    +) + +module.exports = SubNavExamples diff --git a/components/SubNav/SubNavExamples.scss b/components/SubNav/SubNavExamples.scss new file mode 100644 index 000000000..81bc13175 --- /dev/null +++ b/components/SubNav/SubNavExamples.scss @@ -0,0 +1,8 @@ +@import 'topcoder/tc-includes'; + +.SubNavExamples { + > p { + border: 1px solid $accent-gray; + margin: 20px 0px; + } +} \ No newline at end of file diff --git a/components/SubNav/placeholder.svg b/components/SubNav/placeholder.svg new file mode 100644 index 000000000..d3e7220e5 --- /dev/null +++ b/components/SubNav/placeholder.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/components/UserDropdownMenu/UserDropdownMenu.jsx b/components/UserDropdownMenu/UserDropdownMenu.jsx index 0ebf8b393..f26453653 100644 --- a/components/UserDropdownMenu/UserDropdownMenu.jsx +++ b/components/UserDropdownMenu/UserDropdownMenu.jsx @@ -1,62 +1,75 @@ require('./UserDropdownMenu.scss') -import React, { Component } from 'react' +import React, {PropTypes} from 'react' +import Avatar from '../Avatar/Avatar' import Dropdown from '../Dropdown/Dropdown' -const userDropdownLists = [ - [ - { label: 'My Profile', link: 'javascript:;', id: 0 }, - { label: 'Dashboard', link: 'javascript:;', id: 1 }, - { label: 'Settings', link: 'javascript:;', id: 2 }, - { label: 'Payments', link: 'javascript:;', id: 3 } - ], - [ - { label: 'Help', link: 'javascript:;', id: 0 } - ], - [ - { label: 'Log out', link: 'javascript:;', id: 0 } + +const UserDropdownMenu = ({username, userImage, domain, loginUrl, registerUrl}) => { + + const userDropdownLists = [ + [ + { label: 'My Profile', link: '/profile/' + username, id: 0 }, + { label: 'Dashboard', link: '/my-dashbaord', id: 1 }, + { label: 'Settings', link: '/settings/profile', id: 2 }, + { label: 'Payments', link: '//community.' + domain + '/PactsMemberServlet?module=PaymentHistory&full_list=false', id: 3 } + ], + [ + { label: 'Help', link: '//help.' + domain, id: 0 } + ], + [ + { label: 'Log out', link: '/logout', id: 0 } + ] ] -] - -class UserDropdownMenu extends Component { - constructor(props) { - super(props) - - this.state = { isLoggedIn: true } - } - - render() { - const publicDOM =
    - - const loggedInDOM = ( -
    - -
    - - { this.props.username } - -
    + + const publicDOM = ( +
    + Log in + Join +
    + ) -
    - { - userDropdownLists.map((list, i) => { - return ( ) - }) - } - -
    -
    -
    - ) - - return this.state.isLoggedIn ? loggedInDOM : publicDOM - } + const loggedInDOM = ( +
    + +
    + + { username } + +
    + +
    + { + userDropdownLists.map((list, i) => { + return ( ) + }) + } + +
    +
    +
    + ) + + return username ? loggedInDOM : publicDOM +} + +UserDropdownMenu.propTypes = { + username : PropTypes.string, + userImage : PropTypes.string, + domain : PropTypes.string.isRequired, + loginUrl : PropTypes.string, + registerUrl : PropTypes.string +} + +UserDropdownMenu.defaultProps = { + loginUrl : '/login', + registerUrl : '/register' } export default UserDropdownMenu \ No newline at end of file diff --git a/components/UserDropdownMenu/UserDropdownMenu.scss b/components/UserDropdownMenu/UserDropdownMenu.scss index fecdfcdb5..d7b8a02dc 100644 --- a/components/UserDropdownMenu/UserDropdownMenu.scss +++ b/components/UserDropdownMenu/UserDropdownMenu.scss @@ -1,4 +1,5 @@ @import "topcoder/tc-includes"; +@import "topcoder/tc-buttons"; $user-dropdown-bg: #FAFAFB; $placeholder-bg: #B47DD6; @@ -10,14 +11,26 @@ $username-color: #737380; display: inline-block; position: relative; + &.non-logged-in { + .login-button { + display: inline-block; + } + + .join-button { + display: inline-block; + margin-left: 20px; + } + } + .dropdown-menu-header { cursor: pointer; + display: flex; + align-items: center; .user-image { display: inline-block; - width: 30px; - height: 30px; - background-color: $placeholder-bg; + width: 35px; + height: 35px; margin-right: 10px; border-radius: 50%; vertical-align: middle; @@ -28,8 +41,11 @@ $username-color: #737380; font-size: 14px; line-height: 16px; color: $username-color; + @include ellipsis; + margin: 0px; margin-right: 10px; display: inline-block; + overflow: hidden; @media screen and (max-width: 768px) { display: none; @@ -38,7 +54,7 @@ $username-color: #737380; .dropdown-arrow { position: relative; - top: -2px; + top: 2px; display: inline-block; } } diff --git a/components/UserDropdownMenu/UserDropdownMenuExamples.jsx b/components/UserDropdownMenu/UserDropdownMenuExamples.jsx index 0fbc7ea5a..43bf33902 100644 --- a/components/UserDropdownMenu/UserDropdownMenuExamples.jsx +++ b/components/UserDropdownMenu/UserDropdownMenuExamples.jsx @@ -2,7 +2,14 @@ import React from 'react' import UserDropdownMenu from './UserDropdownMenu' const UserDropdownMenuExamples = () => { - return + return ( +
    +

    Logged In state

    + +

    Logged Out state

    + +
    + ) } module.exports = UserDropdownMenuExamples diff --git a/index.coffee b/index.coffee index 187e73af2..ea8f95548 100644 --- a/index.coffee +++ b/index.coffee @@ -1,2 +1,6 @@ module.exports = default: require './components/FileUploader/FileUploaderContainer.cjsx' + NavBar: require './components/Navbar/Navbar.jsx' + SearchBar: require './components/SearchBar/SearchBar.jsx' + UserDropdown: require './components/UserDropdownMenu/UserDropdownMenu.jsx' + TCFooter: require './components/TCFooter/TCFooter.jsx' \ No newline at end of file diff --git a/navbar.webpack.config.coffee b/navbar.webpack.config.coffee new file mode 100644 index 000000000..c5c14027c --- /dev/null +++ b/navbar.webpack.config.coffee @@ -0,0 +1,28 @@ +webpackConfig = require 'appirio-tech-webpack-config' +webpack = require('webpack') +ExtractTextPlugin = require('extract-text-webpack-plugin') + +config = webpackConfig + dirname: __dirname + template: './index.html' + entry: + TCNavComponents: [ + './index.coffee' + ] + +# exports javascript as library +config.output.filename = '[name].js'; +config.output.publicPath = 'https://s3.amazonaws.com/components.topcoder-dev.com/' +config.output.library = '[name]'; +config.output.libraryTarget = 'var' + +# CSS file without hash name +config.plugins.push new ExtractTextPlugin '[name].css' + +# React would be provided externally by the application using the components +config.externals = + "react" : "React" + "react-dom" : "ReactDOM" + "moment" : "moment" + +module.exports = config diff --git a/package.json b/package.json index 5b964ee06..25e65f8b5 100644 --- a/package.json +++ b/package.json @@ -18,15 +18,16 @@ }, "scripts": { "example": "webpack-dev-server -d --progress --inline --colors", - "dev": "webpack-dev-server -d --progress --inline --colors --dev", + "dev": "webpack-dev-server -d --progress --inline --colors --dev --tc", "clean": "rm -r dist", "build": "webpack --config webpack.config.js; cp example/index.html dist/", "lint": "eslint --format table --ext .js,.jsx .", "test": "cross-env NODE_ENV=test mocha --reporter progress --compilers js:babel-core/register --require ignore-styles --recursive \"./components/**/*.spec.js\"", - "test:watch": "npm run test -- --watch" + "test:watch": "npm run test -- --watch", + "build-navbar": "webpack $BUILD --tc --config navbar.webpack.config.coffee;" }, "dependencies": { - "appirio-styles": "0.0.25", + "appirio-styles": "0.0.26", "appirio-tech-api-schemas": "^5.0.69", "appirio-tech-client-app-layer": "^0.1.3", "classnames": "^2.2.3", @@ -37,6 +38,7 @@ "react-dom": "^0.14.7", "react-dropzone": "^3.3.2", "react-redux": "^4.2.1", - "react-select": "^0.9.1" + "react-select": "^0.9.1", + "isomorphic-fetch": "^2.2.1" } } diff --git a/setenv.sh b/setenv.sh new file mode 100644 index 000000000..f8513d0fa --- /dev/null +++ b/setenv.sh @@ -0,0 +1,10 @@ +#!/bin/bash +if [ "$TRAVIS_BRANCH" = "dev" ]; then + export ENV=dev +elif [ "$TRAVIS_BRANCH" = "release" ]; then + export ENV=qa + export BUILD_ARGS=--build +elif [ "$TRAVIS_BRANCH" = "master" ]; then + export ENV=prod + export BUILD_ARGS=--build +fi \ No newline at end of file