├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── react-banner.min.js └── style.css ├── docs ├── 0.root.bundle.js ├── 0.root.bundle.js.map ├── 1.root.bundle.js ├── 1.root.bundle.js.map ├── 2.root.bundle.js ├── 2.root.bundle.js.map ├── 3.root.bundle.js ├── 3.root.bundle.js.map ├── 4.root.bundle.js ├── 4.root.bundle.js.map ├── 404.html ├── 5.root.bundle.js ├── 5.root.bundle.js.map ├── 6.root.bundle.js ├── 6.root.bundle.js.map ├── favicon.ico ├── index.html ├── root.bundle.js ├── root.bundle.js.map └── spa-redirect.js ├── package-lock.json ├── package.json ├── src ├── 404.html ├── banner │ ├── banner-item.jsx │ ├── banner-search-style.css │ ├── banner-search.jsx │ ├── banner-style.css │ ├── banner-sub-style.css │ ├── banner-sub.jsx │ ├── banner.jsx │ └── utils.js ├── docs.jsx ├── favicon.ico ├── icons │ ├── cross-icon.jsx │ ├── hamburger-icon.jsx │ └── search-icon.jsx ├── links │ ├── spa-link.jsx │ └── standard-link.jsx ├── logo │ ├── logo-style.css │ └── logo.jsx ├── search-results │ ├── search-results-style.css │ └── search-results.jsx ├── sidebar │ ├── sidebar-style.css │ └── sidebar.jsx ├── site │ ├── content │ │ ├── customization.md │ │ ├── headroom.md │ │ ├── index.md │ │ ├── router.md │ │ ├── search.md │ │ └── sidebar.md │ ├── items.json │ ├── site-style.css │ └── site.jsx └── spa-redirect.js ├── webpack.dist.js └── webpack.docs.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-syntax-dynamic-import", 9 | "react-hot-loader/babel" 10 | ] 11 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Top-most EditorConfig file 2 | root = true 3 | 4 | # Set default charset 5 | [*.{js}] 6 | charset = utf-8 7 | 8 | # 4 space indentation 9 | [*.{html,css,js,jsx,json,md}] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | # Format Config 14 | [{package.json}] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "eslint:recommended", 4 | "parser": "babel-eslint", 5 | "env": { 6 | "browser": true, 7 | "es6": true, 8 | "node": true 9 | }, 10 | "plugins": [ 11 | "react" 12 | ], 13 | "globals": { 14 | "PRODUCTION": true 15 | }, 16 | "rules": { 17 | "no-undef": 2, 18 | "no-unreachable": 2, 19 | "no-unused-vars": 0, 20 | "no-console": 0, 21 | "semi": [ "error", "never" ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | npm-debug.log* 3 | 4 | # Dependency Directories 5 | node_modules 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | npm-debug.log 3 | 4 | # Dependency Directories 5 | node_modules 6 | 7 | # Docs Related 8 | src 9 | !src/banner 10 | !src/icons 11 | !src/links 12 | docs 13 | .babelrc 14 | .eslintrc 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [1.0.3](https://github.com/skipjack/react-banner/compare/v1.0.2...v1.0.3) (2020-03-27) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * **deps:** fix a bunch of audit warnings ([fbe61b9](https://github.com/skipjack/react-banner/commit/fbe61b9)) 11 | * improve button accessibility ([#17](https://github.com/skipjack/react-banner/issues/17)) ([a919af0](https://github.com/skipjack/react-banner/commit/a919af0)) 12 | 13 | 14 | 15 | ### [1.0.2](https://github.com/skipjack/react-banner/compare/v1.0.1...v1.0.2) (2020-01-09) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * fix broken active class name ([4790380](https://github.com/skipjack/react-banner/commit/4790380)) 21 | 22 | 23 | 24 | ### [1.0.1](https://github.com/skipjack/react-banner/compare/v1.0.0...v1.0.1) (2019-12-05) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * **deps:** pick up updates and fix audit warnings ([816bbab](https://github.com/skipjack/react-banner/commit/816bbab)) 30 | * apply custom classes on items again ([2d87132](https://github.com/skipjack/react-banner/commit/2d87132)) 31 | 32 | 33 | 34 | ## [1.0.0](https://github.com/skipjack/react-banner/compare/v1.0.0-rc.3...v1.0.0) (2019-06-10) 35 | 36 | 37 | 38 | ## [1.0.0-rc.3](https://github.com/skipjack/react-banner/compare/v1.0.0-rc.2...v1.0.0-rc.3) (2019-06-09) 39 | 40 | 41 | 42 | ## [1.0.0-rc.2](https://github.com/skipjack/react-banner/compare/v1.0.0-rc.1...v1.0.0-rc.2) (2019-06-07) 43 | 44 | 45 | ### Bug Fixes 46 | 47 | * **deps:** fix `extend@3.0.1` vulnerability ([14746a3](https://github.com/skipjack/react-banner/commit/14746a3)) 48 | 49 | 50 | ### refactor 51 | 52 | * **banner:** migrate to stateless components ([b658b50](https://github.com/skipjack/react-banner/commit/b658b50)) 53 | 54 | 55 | ### BREAKING CHANGES 56 | 57 | * **banner:** `onSearchTyping` is replaced by `onSearch` which 58 | existed previously but wasn't used at all. 59 | 60 | 61 | 62 | 63 | ## 1.0.0-rc.1 (2018-08-25) 64 | 65 | * chore(deps): update outdated packages and lock file ([96661de](https://github.com/skipjack/react-banner/commit/96661de)) 66 | * fix: fix bug with some links rendering as basic `span` elements ([6c8d069](https://github.com/skipjack/react-banner/commit/6c8d069)) 67 | * docs(changelog): add missing breaking changes ([01a56e6](https://github.com/skipjack/react-banner/commit/01a56e6)) 68 | 69 | 70 | 71 | 72 | # [1.0.0-rc.0](https://github.com/skipjack/react-banner/compare/v0.5.0...v1.0.0-rc.0) (2018-04-11) 73 | 74 | 75 | ### Code Refactoring 76 | 77 | * rename `links` prop to `items` and change object structure ([d415872](https://github.com/skipjack/react-banner/commit/d415872)) 78 | 79 | 80 | ### Features 81 | 82 | * support `overlay` prop to easily overlay the banner on top of other content ([7f16565](https://github.com/skipjack/react-banner/commit/7f16565)) 83 | 84 | 85 | ### BREAKING CHANGES 86 | 87 | * The `links` prop is now `items`. A `content` field is now used for 88 | each object in `items` to determine what's displayed rather than the previous 89 | `title` property. The `title` field now only determines the normal HTML 90 | tooltip-like `title` attribute. 91 | 92 | 93 | 94 | 95 | # [0.5.0](https://github.com/skipjack/react-banner/compare/v0.4.0...v0.5.0) (2018-01-25) 96 | 97 | 98 | ### Bug Fixes 99 | 100 | * ignore unnecessary configs from `publish` ([ca8b6fa](https://github.com/skipjack/react-banner/commit/ca8b6fa)) 101 | 102 | 103 | ### Features 104 | 105 | * allow link objects to use a custom `isActive` function ([b025d88](https://github.com/skipjack/react-banner/commit/b025d88)) 106 | 107 | 108 | 109 | 110 | # [0.4.0](https://github.com/skipjack/react-banner/compare/v0.3.1...v0.4.0) (2017-10-19) 111 | 112 | 113 | ### Features 114 | 115 | * **search:** implement `onSearchTyping` and add `searchResults` props ([636ae07](https://github.com/skipjack/react-banner/commit/636ae07)) 116 | 117 | 118 | 119 | 120 | ## [0.3.1](https://github.com/skipjack/react-banner/compare/v0.3.0...v0.3.1) (2017-05-11) 121 | 122 | 123 | ### Bug Fixes 124 | 125 | * simplify `_isActive` test and fix a bug in the test ([84834ba](https://github.com/skipjack/react-banner/commit/84834ba)) 126 | 127 | 128 | 129 | 130 | # [0.3.0](https://github.com/skipjack/react-banner/compare/v0.2.0...v0.3.0) (2017-05-01) 131 | 132 | 133 | ### Bug Fixes 134 | 135 | * fix issue with duplicate/incorrect link ids ([73b08af](https://github.com/skipjack/react-banner/commit/73b08af)) 136 | * force link objects with no `url` to always fail `_isActive` test ([59dc0fd](https://github.com/skipjack/react-banner/commit/59dc0fd)) 137 | * ignore all non library related files ([4e6545a](https://github.com/skipjack/react-banner/commit/4e6545a)) 138 | * remove unnecessary `activeClassName` in `spa-link.jsx` ([0bef146](https://github.com/skipjack/react-banner/commit/0bef146)) 139 | 140 | 141 | ### Features 142 | 143 | * add support for custom `className` in link objects ([edfcfa1](https://github.com/skipjack/react-banner/commit/edfcfa1)) 144 | 145 | 146 | 147 | 148 | # [0.2.0](https://github.com/skipjack/react-banner/compare/v0.1.7...v0.2.0) (2017-04-27) 149 | 150 | 151 | ### Bug Fixes 152 | 153 | * remove react-dom peer dep ([364b3e9](https://github.com/skipjack/react-banner/commit/364b3e9)) 154 | 155 | 156 | ### Features 157 | 158 | * expose `icons` prop for icon modification ([69ece75](https://github.com/skipjack/react-banner/commit/69ece75)) 159 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Greg Venech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM Version][7]][5] 2 | [![Standard Version][8]][6] 3 | 4 | react-banner 5 | ============ 6 | 7 | A flexible banner component, available as a react plugin. 8 | 9 | 10 | ## Installation 11 | 12 | This component can be installed from npm: 13 | 14 | ``` bash 15 | npm install react-banner 16 | ``` 17 | 18 | You can also grab the minified JavaScript and CSS straight from `/dist` and 19 | include it with a ` 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /docs/5.root.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[5],{96:function(s,n,a){"use strict";a.r(n),n.default='

While the Banner does provide a dynamic search bar, you still have to handle\ngetting and displaying results. This typically consists of hitting some backend\nonSearch and displaying those results via the searchResults prop. Let\'s\ntake a look at a simple example...

\n

First, we\'ll add a static list of possible search results and some state to\ntrack what\'s visible.

\n
+ const options = [\n+     { title: \'Foo\', description: \'Lorem ipsum dolor...\' },\n+     { title: \'Bar\', description: \'Sed tortor eu arcu...\' },\n+     { title: \'Baz\', description: \'Curabitur velit dolor...\' },\n+     { title: \'Qux\', description: \'Donec imperdiet urna...\' },\n+     { title: \'Quz\', description: \'Fusce eu tellus biben...\' }\n+ ] \n+ \n  export default props => {\n+     const [results, setResults] = useState(options)\n+\n      return (\n          <Banner\n              logo="My Logo"\n              url={ window.location.pathname }\n              items={[\n                  { "content": "Example Link", "url": "/example" },\n                  { "content": "Another", "url": "/another" },\n                  { "content": "Link w/ Children", "url": "/children", "children": [\n                      { "content": "John", "url": "/children/john" },\n                      { "content": "Jill", "url": "/children/jill" },\n                      { "content": "Jack", "url": "/children/jack" }\n                  ]}\n-             ]} />\n+             ]}\n+             onSearch={ input => {\n+                 setResults(options.filter(option => (\n+                     option.title.includes(input) ||\n+                     option.description.includes(input)\n+                 )))\n+             }} />\n      )\n  }
\n

You can add a console.log statement before return to see the how the\nresults state updates as you type. Next, you\'ll need to render the search\nresults.

\n
              onSearch={ input => {\n                  setResults(options.filter(option => (\n                      option.title.includes(input) ||\n                      option.description.includes(input)\n                  )))\n-             }} />\n+             }}\n+             searchResults={(\n+                 <div style={{ background: \'white\', borderRadius: 5 }}>\n+                     { results.map(option => (\n+                         <div>{ option.title }: { option.description }</div>\n+                     ))}\n+                 </div>\n+             )} />
\n

We\'ve left out any fancy styling for brevity but you should get the idea at\nthis point. In the real world, you\'d likely hit a RESTful API or some other\nbackend to get results. For example, this site uses Algolia and their\nDocSearch service for the search implementation. It then renders\nresults via this SearchResults component which pulls some code from\nsome other Algolia SDKs.

\n'}}]); 2 | //# sourceMappingURL=5.root.bundle.js.map -------------------------------------------------------------------------------- /docs/5.root.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///./site/content/search.md"],"names":[],"mappings":"yFAAA,OAAe","file":"5.root.bundle.js","sourcesContent":["export default \"

While the Banner does provide a dynamic search bar, you still have to handle\\ngetting and displaying results. This typically consists of hitting some backend\\nonSearch and displaying those results via the searchResults prop. Let's\\ntake a look at a simple example...

\\n

First, we'll add a static list of possible search results and some state to\\ntrack what's visible.

\\n
+ const options = [\\n+     { title: 'Foo', description: 'Lorem ipsum dolor...' },\\n+     { title: 'Bar', description: 'Sed tortor eu arcu...' },\\n+     { title: 'Baz', description: 'Curabitur velit dolor...' },\\n+     { title: 'Qux', description: 'Donec imperdiet urna...' },\\n+     { title: 'Quz', description: 'Fusce eu tellus biben...' }\\n+ ] \\n+ \\n  export default props => {\\n+     const [results, setResults] = useState(options)\\n+\\n      return (\\n          <Banner\\n              logo=\\\"My Logo\\\"\\n              url={ window.location.pathname }\\n              items={[\\n                  { \\\"content\\\": \\\"Example Link\\\", \\\"url\\\": \\\"/example\\\" },\\n                  { \\\"content\\\": \\\"Another\\\", \\\"url\\\": \\\"/another\\\" },\\n                  { \\\"content\\\": \\\"Link w/ Children\\\", \\\"url\\\": \\\"/children\\\", \\\"children\\\": [\\n                      { \\\"content\\\": \\\"John\\\", \\\"url\\\": \\\"/children/john\\\" },\\n                      { \\\"content\\\": \\\"Jill\\\", \\\"url\\\": \\\"/children/jill\\\" },\\n                      { \\\"content\\\": \\\"Jack\\\", \\\"url\\\": \\\"/children/jack\\\" }\\n                  ]}\\n-             ]} />\\n+             ]}\\n+             onSearch={ input => {\\n+                 setResults(options.filter(option => (\\n+                     option.title.includes(input) ||\\n+                     option.description.includes(input)\\n+                 )))\\n+             }} />\\n      )\\n  }
\\n

You can add a console.log statement before return to see the how the\\nresults state updates as you type. Next, you'll need to render the search\\nresults.

\\n
              onSearch={ input => {\\n                  setResults(options.filter(option => (\\n                      option.title.includes(input) ||\\n                      option.description.includes(input)\\n                  )))\\n-             }} />\\n+             }}\\n+             searchResults={(\\n+                 <div style={{ background: 'white', borderRadius: 5 }}>\\n+                     { results.map(option => (\\n+                         <div>{ option.title }: { option.description }</div>\\n+                     ))}\\n+                 </div>\\n+             )} />
\\n

We've left out any fancy styling for brevity but you should get the idea at\\nthis point. In the real world, you'd likely hit a RESTful API or some other\\nbackend to get results. For example, this site uses Algolia and their\\nDocSearch service for the search implementation. It then renders\\nresults via this SearchResults component which pulls some code from\\nsome other Algolia SDKs.

\\n\";"],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/6.root.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[6],{97:function(e,n,s){"use strict";s.r(n),n.default='

Top navigation bars are great when your site is being viewed on larger screens\nlike desktops and laptops, however when users visit your site from a mobile\nphone you need to offer a more friendly alternative. There\'s a lot of options\nout there: top-down menus, full-screen overlay menus, panels\nand many more. Personally I think side panels are the most intuitive and, with\nReact Sidebar, very easy to configure.

\n
\n

Tip: View this page on a mobile screen (or with mobile dev tools) to see\nthis in action. Clicking the hamburger menu in the top right corner will\ndisplay the sidebar.

\n
\n

Basic Setup

\n

Luckily, this package provides a built-in hamburger menu displayed on smaller\nscreens that you can hook into to display the menu. Going forward with our\noriginal example, you would npm install react-sidebar --save and update the\ncode as such:

\n
import React from \'react\'\nimport Banner from \'react-banner\'\nimport Sidebar from \'react-sidebar\'\nimport \'react-banner/dist/style.css\'\n\nconst SidebarContent = props => {\n    return (\n        <div style={{\n            width: \'80vw\',\n            height: \'100vh\',\n            background: \'white\'\n        }} />\n    )\n}\n\nconst Site = props => {\n    const [sidebar, setSidebar] = useState(false)\n    \n    return (\n        <Sidebar\n            sidebar={ <}\n            open={ sidebar }\n            onSetOpen={ () => setSidebar(true) }>\n            <Banner\n                logo="My Logo"\n                url={ window.location.pathname }\n                onMenuClick={ () => setSidebar(!sidebar) }\n                items={[\n                    { "content": "Example Link", "url": "/example" },\n                    { "content": "Another", "url": "/another" }\n                ]} />\n            <main>\n                <h2>Hey, I\'m some content</h2>\n            </main>\n        </Sidebar>\n    )\n}
\n
\n

Note: The above example just shows an empty <div> for brevity, however in\na real site you\'d likely use the same link array passed to the Banner to\ngenerate a vertical navigation menu displayed within SidebarContent.

\n
\n

Using React Sidebar is just one of many possibilities. Using the\nonMenuClick property you could pass a callback that triggers any kind of\nmenu or behavior to occur.

\n'}}]); 2 | //# sourceMappingURL=6.root.bundle.js.map -------------------------------------------------------------------------------- /docs/6.root.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///./site/content/sidebar.md"],"names":[],"mappings":"yFAAA,OAAe","file":"6.root.bundle.js","sourcesContent":["export default \"

Top navigation bars are great when your site is being viewed on larger screens\\nlike desktops and laptops, however when users visit your site from a mobile\\nphone you need to offer a more friendly alternative. There's a lot of options\\nout there: top-down menus, full-screen overlay menus, panels\\nand many more. Personally I think side panels are the most intuitive and, with\\nReact Sidebar, very easy to configure.

\\n
\\n

Tip: View this page on a mobile screen (or with mobile dev tools) to see\\nthis in action. Clicking the hamburger menu in the top right corner will\\ndisplay the sidebar.

\\n
\\n

Basic Setup

\\n

Luckily, this package provides a built-in hamburger menu displayed on smaller\\nscreens that you can hook into to display the menu. Going forward with our\\noriginal example, you would npm install react-sidebar --save and update the\\ncode as such:

\\n
import React from 'react'\\nimport Banner from 'react-banner'\\nimport Sidebar from 'react-sidebar'\\nimport 'react-banner/dist/style.css'\\n\\nconst SidebarContent = props => {\\n    return (\\n        <div style={{\\n            width: '80vw',\\n            height: '100vh',\\n            background: 'white'\\n        }} />\\n    )\\n}\\n\\nconst Site = props => {\\n    const [sidebar, setSidebar] = useState(false)\\n    \\n    return (\\n        <Sidebar\\n            sidebar={ <}\\n            open={ sidebar }\\n            onSetOpen={ () => setSidebar(true) }>\\n            <Banner\\n                logo=\\\"My Logo\\\"\\n                url={ window.location.pathname }\\n                onMenuClick={ () => setSidebar(!sidebar) }\\n                items={[\\n                    { \\\"content\\\": \\\"Example Link\\\", \\\"url\\\": \\\"/example\\\" },\\n                    { \\\"content\\\": \\\"Another\\\", \\\"url\\\": \\\"/another\\\" }\\n                ]} />\\n            <main>\\n                <h2>Hey, I'm some content</h2>\\n            </main>\\n        </Sidebar>\\n    )\\n}
\\n
\\n

Note: The above example just shows an empty <div> for brevity, however in\\na real site you'd likely use the same link array passed to the Banner to\\ngenerate a vertical navigation menu displayed within SidebarContent.

\\n
\\n

Using React Sidebar is just one of many possibilities. Using the\\nonMenuClick property you could pass a callback that triggers any kind of\\nmenu or behavior to occur.

\\n\";"],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skipjack/react-banner/6831282cf28aabf090799ae8532872ad8fd97439/docs/favicon.ico -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Banner | A flexible banner component 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/spa-redirect.js: -------------------------------------------------------------------------------- 1 | // Single Page Apps for GitHub Pages 2 | // https://github.com/rafrex/spa-github-pages 3 | // Copyright (c) 2016 Rafael Pedicini, licensed under the MIT License 4 | // ---------------------------------------------------------------------- 5 | // This script checks to see if a redirect is present in the query string 6 | // and converts it back into the correct url and adds it to the 7 | // browser's history using window.history.replaceState(...), 8 | // which won't cause the browser to attempt to load the new url. 9 | // When the single page app is loaded further down in this file, 10 | // the correct url will be waiting in the browser's history for 11 | // the single page app to route accordingly. 12 | 13 | (function(l) { 14 | if (l.search) { 15 | var q = {}; 16 | 17 | l.search.slice(1).split('&').forEach(function(v) { 18 | var a = v.split('='); 19 | q[a[0]] = a.slice(1).join('=').replace(/~and~/g, '&'); 20 | }); 21 | 22 | if (q.p !== undefined) { 23 | window.history.replaceState(null, null, 24 | l.pathname.slice(0, -1) + (q.p || '') + 25 | (q.q ? ('?' + q.q) : '') + 26 | l.hash 27 | ); 28 | } 29 | } 30 | }(window.location)) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-banner", 3 | "version": "1.0.3", 4 | "description": "A flexible banner component.", 5 | "main": "dist/react-banner.min.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --config webpack.docs.js --env.dev", 8 | "watch": "webpack --watch --config webpack.dist.js", 9 | "build": "npm run build:lib && npm run build:docs", 10 | "build:lib": "webpack --config webpack.dist.js", 11 | "build:docs": "webpack --config webpack.docs.js", 12 | "release": "standard-version", 13 | "test": "npm prune && npm install && npm start" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/skipjack/react-banner.git" 18 | }, 19 | "keywords": [ 20 | "react", 21 | "component", 22 | "react-banner", 23 | "banner" 24 | ], 25 | "author": "Greg Venech", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/skipjack/react-banner/issues" 29 | }, 30 | "homepage": "https://github.com/skipjack/react-banner#readme", 31 | "peerDependencies": { 32 | "react": "^15.0.0 || ^16.0.0", 33 | "prop-types": "^15.5.8" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.9.0", 37 | "@babel/plugin-proposal-class-properties": "^7.8.3", 38 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 39 | "@babel/preset-env": "^7.9.0", 40 | "@babel/preset-react": "^7.9.4", 41 | "algoliasearch": "^3.35.1", 42 | "babel-core": "^6.26.3", 43 | "babel-eslint": "^10.1.0", 44 | "babel-loader": "^8.1.0", 45 | "copy-webpack-plugin": "^5.1.1", 46 | "css-loader": "^2.1.1", 47 | "docsearch.js": "^2.6.3", 48 | "eslint": "^5.16.0", 49 | "eslint-loader": "^2.2.1", 50 | "eslint-plugin-react": "^7.19.0", 51 | "file-loader": "^3.0.1", 52 | "html-webpack-plugin": "^3.2.0", 53 | "html-webpack-template": "^6.2.0", 54 | "json-loader": "^0.5.4", 55 | "mini-css-extract-plugin": "^0.7.0", 56 | "prop-types": "^15.7.2", 57 | "raw-loader": "^2.0.0", 58 | "react": "^16.13.1", 59 | "react-dom": "^16.13.1", 60 | "react-headroom": "^2.2.8", 61 | "react-hot-loader": "^3.0.0", 62 | "react-pirate": "^1.3.0", 63 | "react-router-dom": "^5.1.2", 64 | "react-sidebar": "^3.0.2", 65 | "remark-highlight.js": "^5.2.0", 66 | "remark-loader": "^0.3.0", 67 | "standard-version": "^8.0.1", 68 | "style-loader": "^0.23.1", 69 | "terser-webpack-plugin": "^1.4.1", 70 | "uglifyjs-webpack-plugin": "^2.2.0", 71 | "webpack": "^4.42.1", 72 | "webpack-cli": "^3.3.11", 73 | "webpack-dev-server": "^3.10.3" 74 | }, 75 | "dependencies": {} 76 | } 77 | -------------------------------------------------------------------------------- /src/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Banner | A flexible banner component 6 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/banner/banner-item.jsx: -------------------------------------------------------------------------------- 1 | // Foundational 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | 5 | 6 | const BannerItem = ({ 7 | className = '', 8 | activeClassName = '', 9 | content, 10 | active, 11 | url, 12 | link: Link, 13 | ...rest 14 | }) => { 15 | const activeMod = active ? activeClassName : '' 16 | 17 | if ( !url ) return ( 18 | 19 | { content } 20 | 21 | ) 22 | 23 | return ( 24 | 28 | { content } 29 | 30 | ) 31 | } 32 | 33 | // Validation 34 | BannerItem.propTypes = { 35 | className: PropTypes.string, 36 | content: PropTypes.node, 37 | active: PropTypes.bool, 38 | url: PropTypes.string, 39 | link: PropTypes.func 40 | } 41 | 42 | // Exposure 43 | export default BannerItem -------------------------------------------------------------------------------- /src/banner/banner-search-style.css: -------------------------------------------------------------------------------- 1 | .banner-search { 2 | flex: 0 0 auto; 3 | position: relative; 4 | display: flex; 5 | justify-content: flex-end; 6 | } 7 | 8 | .banner-search__input { 9 | font-size: 14px; 10 | width: 0; 11 | max-width: calc(100vw - 8.5em); 12 | padding: 0; 13 | border: none; 14 | background: transparent; 15 | text-indent: 0.5em; 16 | margin-right: 0; 17 | color: white; 18 | text-shadow: 0 0 0 #666; 19 | transition: all 250ms; 20 | } 21 | 22 | .banner-search__input::-webkit-input-placeholder{ 23 | color: #666; 24 | text-shadow: none; 25 | -webkit-text-fill-color: initial; 26 | } 27 | 28 | .banner-search__icon { 29 | font-size: 1em; 30 | width: 1em; 31 | height: 1em; 32 | padding: 0; 33 | border: none; 34 | cursor: pointer; 35 | fill: lightgrey; 36 | background: transparent; 37 | transition: color 250ms; 38 | } 39 | 40 | .banner-search__input:focus, 41 | .banner-search__icon:focus { 42 | outline: none; 43 | } 44 | 45 | .banner-search__clear { 46 | display: none; 47 | margin-right: 0.25em; 48 | margin-bottom: -1px; 49 | } 50 | 51 | .banner-search--active .banner-search__input { 52 | margin-right: 0.5em; 53 | width: 400px; 54 | } 55 | 56 | .banner-search--active .banner-search__clear { 57 | display: block; 58 | } 59 | 60 | .banner-search__results { 61 | position: absolute; 62 | top: 100%; 63 | right: 0; 64 | width: 100%; 65 | margin-top: 5px; 66 | } 67 | -------------------------------------------------------------------------------- /src/banner/banner-search.jsx: -------------------------------------------------------------------------------- 1 | // Foundational 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | 5 | // Utilities 6 | import { useState, useEffect, useRef } from 'react' 7 | import { useMount, useUnmount, usePrevious } from 'react-pirate' 8 | 9 | // Styling 10 | import './banner-search-style' 11 | 12 | 13 | const BannerSearch = ({ 14 | blockName, 15 | active, 16 | placeholder = 'Search this site...', 17 | searchResults, 18 | icons, 19 | onToggle, 20 | onSearch 21 | }) => { 22 | const [input, setInput] = useState('') 23 | const [showResults, toggleResults] = useState(false) 24 | const inputElement = useRef(null) 25 | const container = useRef(null) 26 | const prevActive = usePrevious(active) 27 | const activeMod = active ? `${blockName}--active` : '' 28 | 29 | const handler = e => { 30 | const outsideContainer = container.current && !container.current.contains(e.target) 31 | const notSearch = !Array.from(e.target.classList).includes(`${blockName}__input`) 32 | 33 | if (outsideContainer && notSearch) { 34 | toggleResults(false) 35 | } 36 | } 37 | 38 | useMount(() => window.addEventListener( 'click', handler)) 39 | useUnmount(() => window.removeEventListener('click', handler)) 40 | useEffect(() => { 41 | if (active && !prevActive) inputElement.current.focus() 42 | }) 43 | 44 | return ( 45 |
46 | toggleResults(true) } 53 | onChange={ e => { 54 | setInput(e.target.value) 55 | onSearch(e.target.value) 56 | }} /> 57 | 63 | 69 | { searchResults && showResults ? ( 70 |
73 | { searchResults } 74 |
75 | ) : null } 76 |
77 | ) 78 | } 79 | 80 | // Validation 81 | BannerSearch.propTypes = { 82 | blockName: PropTypes.string.isRequired, 83 | active: PropTypes.bool.isRequired, 84 | placeholder: PropTypes.string, 85 | icons: PropTypes.shape({ 86 | clear: PropTypes.node.isRequired, 87 | search: PropTypes.node.isRequired 88 | }), 89 | searchResults: PropTypes.node, 90 | onToggle: PropTypes.func.isRequired, 91 | onSearch: PropTypes.func 92 | } 93 | 94 | // Exposure 95 | export default BannerSearch -------------------------------------------------------------------------------- /src/banner/banner-style.css: -------------------------------------------------------------------------------- 1 | .banner { 2 | background: #333; 3 | } 4 | 5 | .banner__inner { 6 | display: flex; 7 | width: 100%; 8 | min-width: 320px; 9 | max-width: 960px; 10 | min-height: 54px; 11 | margin: 0 auto; 12 | padding: 0 1.5em; 13 | align-items: center; 14 | } 15 | 16 | .banner__mobile { 17 | display: none; 18 | fill: white; 19 | padding: 0; 20 | margin: 0; 21 | line-height: 0; 22 | outline: none; 23 | border: none; 24 | cursor: pointer; 25 | background: transparent; 26 | transition: color 250ms; 27 | } 28 | 29 | .banner__mobile svg { 30 | width: 20px; 31 | height: 20px; 32 | } 33 | 34 | .banner__logo { 35 | margin: auto; 36 | text-decoration: none; 37 | text-transform: uppercase; 38 | font-weight: 500; 39 | letter-spacing: 0.5px; 40 | color: white; 41 | transition: all 250ms; 42 | } 43 | 44 | .banner__items { 45 | flex: 1 1 auto; 46 | display: flex; 47 | align-items: center; 48 | justify-content: flex-end; 49 | } 50 | 51 | .banner__item { 52 | display: inline-block; 53 | font-size: 0.8em; 54 | padding: 1.5em; 55 | cursor: pointer; 56 | color: lightgrey; 57 | text-transform: uppercase; 58 | text-decoration: none; 59 | transition: all 250ms; 60 | } 61 | 62 | .banner__item--active { 63 | background: #535353; 64 | color: #fff; 65 | } 66 | 67 | .banner__item--offset { 68 | margin-right: -1.5em; 69 | } 70 | 71 | .banner--search .banner__item { 72 | pointer-events: none; 73 | overflow: hidden; 74 | white-space: nowrap; 75 | padding: 1.5em 0; 76 | margin-right: -35px; 77 | opacity: 0; 78 | } 79 | 80 | .banner--overlay { 81 | position: absolute; 82 | top: 0; 83 | left: 0; 84 | width: 100%; 85 | z-index: 50; 86 | background: transparent; 87 | } 88 | 89 | @media (max-width: 720px) { 90 | .banner__mobile { display: block; } 91 | .banner__items { display: none; } 92 | .banner--search .banner__logo { 93 | pointer-events: none; 94 | overflow: hidden; 95 | white-space: nowrap; 96 | text-overflow: ellipsis; 97 | margin-right: -35px; 98 | letter-spacing: -1em; 99 | opacity: 0; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/banner/banner-sub-style.css: -------------------------------------------------------------------------------- 1 | .banner-sub { 2 | display: block; 3 | background: #535353; 4 | } 5 | 6 | .banner-sub__inner { 7 | min-width: 320px; 8 | max-width: 960px; 9 | margin: 0 auto; 10 | padding: 0 1.5em; 11 | text-align: right; 12 | } 13 | 14 | .banner-sub__link { 15 | display: inline-block; 16 | font-size: 0.8em; 17 | padding: 0.4em 0; 18 | color: whitesmoke; 19 | text-transform: uppercase; 20 | text-decoration: none; 21 | } 22 | 23 | .banner-sub__link:not(:last-child) { 24 | margin-right: 1.5em; 25 | } 26 | 27 | .banner-sub__link:first-of-type { 28 | margin-left: 0; 29 | } 30 | 31 | @media (max-width: 720px) { 32 | .banner-sub { 33 | display: none; 34 | } 35 | } -------------------------------------------------------------------------------- /src/banner/banner-sub.jsx: -------------------------------------------------------------------------------- 1 | // Foundational 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | 5 | // Styling 6 | import './banner-sub-style' 7 | 8 | 9 | const BannerSub = ({ 10 | blockName, 11 | link: Link, 12 | sublinks = [], 13 | url, 14 | isActive 15 | }) => { 16 | return ( 17 |
18 |
19 | { 20 | sublinks.map((sublink, index) => { 21 | let activeMod = isActive(sublink, url) ? `${blockName}__link--active` : '' 22 | 23 | return ( 24 | 28 | { sublink.content } 29 | 30 | ) 31 | }) 32 | } 33 |
34 |
35 | ) 36 | } 37 | 38 | // Validation 39 | BannerSub.propTypes = { 40 | blockName: PropTypes.string, 41 | link: PropTypes.func, 42 | sublinks: PropTypes.array, 43 | url: PropTypes.string, 44 | isActive: PropTypes.func 45 | } 46 | 47 | // Exposure 48 | export default BannerSub -------------------------------------------------------------------------------- /src/banner/banner.jsx: -------------------------------------------------------------------------------- 1 | // Foundational 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | 5 | // Utilities 6 | import { useState } from 'react' 7 | import { useMount, useUnmount } from 'react-pirate' 8 | import { isActive } from './utils' 9 | 10 | // Components 11 | import BannerSearch from './banner-search' 12 | import BannerSub from './banner-sub' 13 | import BannerItem from './banner-item' 14 | import StandardLink from '../links/standard-link' 15 | 16 | // Images 17 | import HamburgerIcon from '../icons/hamburger-icon' 18 | import CrossIcon from '../icons/cross-icon' 19 | import SearchIcon from '../icons/search-icon' 20 | 21 | // Styles 22 | import './banner-style' 23 | 24 | 25 | const Banner = ({ 26 | blockName = 'banner', 27 | className = '', 28 | logo, 29 | overlay = false, 30 | searchBar = true, 31 | searchResults, 32 | items = [], 33 | icons = { 34 | menu: , 35 | clear: , 36 | search: 37 | }, 38 | url, 39 | link: Link = StandardLink, 40 | onMenuClick, 41 | onSearch 42 | }) => { 43 | const [search, setSearch] = useState(false) 44 | const browser = window !== undefined 45 | const activeItem = items.find(obj => isActive(obj, url)) || {} 46 | const sublinks = activeItem.children || [] 47 | const searchMod = search ? `${blockName}--search` : '' 48 | const overlayMod = overlay ? `${blockName}--overlay` : '' 49 | 50 | // @todo why not implement in `BannerSearch` 51 | const handler = e => { 52 | let isSearchInput = e.target.classList.contains(`${blockName}-search__input`) 53 | 54 | if (e.which === 9 && isSearchInput) { 55 | setSearch(true) 56 | } 57 | } 58 | 59 | if ( browser ) { 60 | useMount(() => window.addEventListener('keyup', handler)) 61 | useUnmount(() => window.removeEventListener('keyup', handler)) 62 | } 63 | 64 | return ( 65 |
66 |
67 | 73 | 74 | { logo } 75 | 76 | 87 | { searchBar ? ( 88 | setSearch(!search) } 94 | onSearch={ onSearch } /> 95 | ) : null } 96 |
97 | 103 |
104 | ) 105 | } 106 | 107 | // Validation 108 | Banner.propTypes = { 109 | blockName: PropTypes.string, 110 | className: PropTypes.string, 111 | logo: PropTypes.node, 112 | url: PropTypes.string, 113 | overlay: PropTypes.bool, 114 | icons: PropTypes.shape({ 115 | menu: PropTypes.node, 116 | clear: PropTypes.node, 117 | search: PropTypes.node 118 | }), 119 | link: PropTypes.oneOfType([ 120 | PropTypes.func, 121 | PropTypes.instanceOf(React.Component) 122 | ]), 123 | items: PropTypes.arrayOf( 124 | PropTypes.object 125 | ), 126 | searchBar: PropTypes.bool, 127 | searchResults: PropTypes.node, 128 | onMenuClick: PropTypes.func, 129 | onSearch: PropTypes.func, 130 | onSearchTyping: PropTypes.func 131 | } 132 | 133 | // Exposure 134 | export default Banner -------------------------------------------------------------------------------- /src/banner/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if the given link `item` is active 3 | * 4 | * @param {object} item - An item object describing a link 5 | * @param {string} url - The URL to test against 6 | * @return {bool} - Whether or not the given item is active 7 | */ 8 | export const isActive = (item = {}, url = '') => { 9 | if ( typeof item.isActive === 'function' ) { 10 | return item.isActive(url) 11 | 12 | } else { 13 | let testUrl = item.url, 14 | regex = new RegExp(`^${testUrl}/?`) 15 | 16 | return ( 17 | testUrl === url || 18 | testUrl !== '/' ? regex.test(url) : false 19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /src/docs.jsx: -------------------------------------------------------------------------------- 1 | // Import External Dependencies 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import { BrowserRouter, Route } from 'react-router-dom' 5 | import { AppContainer } from 'react-hot-loader' 6 | 7 | // Import Local Dependencies 8 | import Site from './site/site' 9 | 10 | // Retrieve element and create render method 11 | let element = document.querySelector('#root'), 12 | render = Root => ReactDOM.render(( 13 | 14 | 15 | 16 | 17 | 18 | ), element) 19 | 20 | // Initial Render 21 | render(Site) 22 | 23 | // Subsequent renders with HMR 24 | if ( module.hot ) { 25 | module.hot.accept('./site/site', () => { 26 | render(Site) 27 | }) 28 | } -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skipjack/react-banner/6831282cf28aabf090799ae8532872ad8fd97439/src/favicon.ico -------------------------------------------------------------------------------- /src/icons/cross-icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default props => ( 4 | 5 | 6 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /src/icons/hamburger-icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default props => ( 4 | 5 | 6 | 7 | 9 | 11 | 13 | 14 | 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /src/icons/search-icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default props => ( 4 | 5 | 6 | 8 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /src/links/spa-link.jsx: -------------------------------------------------------------------------------- 1 | // Foundational 2 | import React from 'react' 3 | 4 | // Components 5 | import { NavLink } from 'react-router-dom' 6 | 7 | /** 8 | * React router stateless link component 9 | * 10 | * @param {object} props - Link options containing at least a `url` 11 | * @return {object} - Markup for the link 12 | */ 13 | const SPALink = ({ 14 | index = '', 15 | url = '', 16 | reload = false, 17 | ...props 18 | }) => { 19 | props.content = typeof props.content === 'string' ? props.content : null 20 | 21 | if ( reload || url.match(/^https?:/) ) { 22 | return ( 23 | 24 | { props.children } 25 | 26 | ) 27 | } 28 | 29 | return ( 30 | 34 | { props.children } 35 | 36 | ) 37 | } 38 | 39 | // Exposure 40 | export default SPALink -------------------------------------------------------------------------------- /src/links/standard-link.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | /** 4 | * Standard stateless link component 5 | * 6 | * @param {object} props - Link options containing at least a `url` 7 | * @return {object} - Markup for the link 8 | */ 9 | export default ({ index, url, ...props }) => ( 10 | 11 | { props.children } 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /src/logo/logo-style.css: -------------------------------------------------------------------------------- 1 | .logo__inner { 2 | fill: #16A085; 3 | } -------------------------------------------------------------------------------- /src/logo/logo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './logo-style' 3 | 4 | const block = 'logo' 5 | 6 | export default props => { 7 | return ( 8 | 15 | 18 | 20 | 21 | ) 22 | } -------------------------------------------------------------------------------- /src/search-results/search-results-style.css: -------------------------------------------------------------------------------- 1 | .search-results { 2 | margin-top: 5px; 3 | border-radius: 5px; 4 | box-shadow: 0 0 3px #999; 5 | background: white; 6 | overflow: hidden; 7 | } 8 | 9 | .search-results__link { 10 | background: white; 11 | transition: background 250ms; 12 | } 13 | 14 | .search-results__link:hover { 15 | background: rgba(141, 214, 249, 0.1); 16 | } 17 | 18 | .search-results__footer { 19 | margin-right: 10.66667px; 20 | margin-bottom: 8px; 21 | } -------------------------------------------------------------------------------- /src/search-results/search-results.jsx: -------------------------------------------------------------------------------- 1 | // Foundational 2 | import React from 'react' 3 | import Utils from 'docsearch.js/dist/npm/src/lib/utils' 4 | 5 | // Styling 6 | import 'docsearch.js/dist/cdn/docsearch.css' 7 | import './search-results-style' 8 | 9 | // BEM block name 10 | const block = 'search-results' 11 | 12 | 13 | export default class SearchResults extends React.Component { 14 | render() { 15 | let { results } = this.props, 16 | suggestions = this._formatHits(results) 17 | 18 | return ( 19 |
20 | { suggestions.map((suggestion, index) => { 21 | let suggestionPrefix = 'algolia-docsearch-suggestion', 22 | categoryMod = suggestion.isCategoryHeader ? `${suggestionPrefix}__main` : '', 23 | subcategoryMod = suggestion.isSubCategoryHeader ? `${suggestionPrefix}__secondary` : '' 24 | 25 | return ( 26 |
29 |
30 | 35 |
36 |
37 |
38 | 43 |
44 | 45 | { suggestion.isTextOrSubcatoryNonEmpty ? ( 46 | 49 |
50 | { suggestion.subcategory } 51 |
52 |
68 | ) 69 | })} 70 | 71 | { suggestions.length === 0 ? ( 72 |
73 |
74 |
75 |
76 |
77 | No results found for query "{ this.props.query }" 78 |
79 |
80 |
81 |
82 |
83 | ) : null } 84 | 85 |
86 | Search by 87 | 90 | Algolia 91 | 92 |
93 |
94 | ) 95 | } 96 | 97 | _formatHits(receivedHits) { 98 | const clonedHits = Utils.deepClone(receivedHits) 99 | const hits = clonedHits.map(hit => { 100 | if (hit._highlightResult) { 101 | hit._highlightResult = Utils.mergeKeyWithParent( 102 | hit._highlightResult, 103 | 'hierarchy' 104 | ) 105 | } 106 | 107 | return Utils.mergeKeyWithParent(hit, 'hierarchy') 108 | }) 109 | 110 | // Group hits by category / subcategory 111 | let groupedHits = Utils.groupBy(hits, 'lvl0') 112 | 113 | for ( let level in groupedHits ) { 114 | const collection = groupedHits[level] 115 | const groupedHitsByLvl1 = Utils.groupBy(collection, 'lvl1') 116 | const flattenedHits = Utils.flattenAndFlagFirst( 117 | groupedHitsByLvl1, 118 | 'isSubCategoryHeader' 119 | ) 120 | 121 | groupedHits[level] = flattenedHits 122 | } 123 | 124 | groupedHits = Utils.flattenAndFlagFirst(groupedHits, 'isCategoryHeader') 125 | 126 | // Translate hits into smaller objects to be send to the template 127 | return groupedHits.map(hit => { 128 | const url = this._formatURL(hit) 129 | const category = Utils.getHighlightedValue(hit, 'lvl0') 130 | const subcategory = Utils.getHighlightedValue(hit, 'lvl1') || category 131 | const displayTitle = Utils 132 | .compact([ 133 | Utils.getHighlightedValue(hit, 'lvl2') || subcategory, 134 | Utils.getHighlightedValue(hit, 'lvl3'), 135 | Utils.getHighlightedValue(hit, 'lvl4'), 136 | Utils.getHighlightedValue(hit, 'lvl5'), 137 | Utils.getHighlightedValue(hit, 'lvl6') 138 | ]) 139 | .join( 140 | '' 141 | ) 142 | 143 | const text = Utils.getSnippetedValue(hit, 'content') 144 | const isTextOrSubcatoryNonEmpty = (subcategory && subcategory !== '') || 145 | (displayTitle && displayTitle !== '') 146 | const isLvl1EmptyOrDuplicate = !subcategory || 147 | subcategory === '' || 148 | subcategory === category 149 | const isLvl2 = displayTitle && 150 | displayTitle !== '' && 151 | displayTitle !== subcategory 152 | const isLvl1 = !isLvl2 && 153 | (subcategory && subcategory !== '' && subcategory !== category) 154 | const isLvl0 = !isLvl1 && !isLvl2 155 | 156 | return { 157 | isLvl0, 158 | isLvl1, 159 | isLvl2, 160 | isLvl1EmptyOrDuplicate, 161 | isCategoryHeader: hit.isCategoryHeader, 162 | isSubCategoryHeader: hit.isSubCategoryHeader, 163 | isTextOrSubcatoryNonEmpty, 164 | category, 165 | subcategory, 166 | title: displayTitle, 167 | text, 168 | url 169 | } 170 | }) 171 | } 172 | 173 | _formatURL(hit) { 174 | const { url, anchor } = hit 175 | 176 | if (url) { 177 | const containsAnchor = url.indexOf('#') !== -1 178 | 179 | if (containsAnchor) return url 180 | else if (anchor) return `${hit.url}#${hit.anchor}` 181 | return url 182 | } else if (anchor) return `#${hit.anchor}` 183 | 184 | /* eslint-disable */ 185 | console.warn('no anchor nor url for : ', JSON.stringify(hit)) 186 | /* eslint-enable */ 187 | 188 | return null 189 | } 190 | } -------------------------------------------------------------------------------- /src/sidebar/sidebar-style.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | width: 75vw; 3 | height: 100vh; 4 | background: white; 5 | padding-top: 1em; 6 | } 7 | 8 | .sidebar__title { 9 | margin-top: 0; 10 | margin-left: 16px; 11 | margin-bottom: 8px; 12 | } 13 | 14 | .sidebar__link { 15 | display: block; 16 | padding: 0.5em 1em; 17 | border-top: 1px solid #dedede; 18 | transition: background 250ms; 19 | } 20 | 21 | .sidebar__link:hover { 22 | background: lightgrey; 23 | } -------------------------------------------------------------------------------- /src/sidebar/sidebar.jsx: -------------------------------------------------------------------------------- 1 | // Foundational 2 | import React from 'react' 3 | 4 | // Components 5 | import { Link } from 'react-router-dom' 6 | 7 | // Styling 8 | import './sidebar-style' 9 | 10 | // BEM Block Name 11 | const block = 'sidebar' 12 | 13 | 14 | const Sidebar = ({ 15 | items, 16 | ...props 17 | }) => ( 18 |
19 |

20 | React Banner 21 |

22 | { 23 | items 24 | .reduce((arr, item) => arr.concat(item.children ? item.children : [ item ]), []) 25 | .map((item, index) => ( 26 | 30 | { item.content } 31 | 32 | )) 33 | } 34 |
35 | ) 36 | 37 | // Exposure 38 | export default Sidebar -------------------------------------------------------------------------------- /src/site/content/customization.md: -------------------------------------------------------------------------------- 1 | Customizing React Banner can be done in two ways. The props listed below can 2 | be used to tweak, change, and respond to the __functionality__ offered by this 3 | component. To change the __layout__ and __color scheme__ we recommend 4 | extending or forking the base stylesheet. 5 | 6 | 7 | ## Supported Props 8 | 9 | #### blockName (string) 10 | 11 | This package follows [BEM naming conventions][1]. If you are doing a lot of 12 | custom styling or otherwise require control of the base name for all css 13 | classes, go ahead and use this option to alter the base class. 14 | 15 | > __Warning:__ This requires that you fork the stylesheet and update all 16 | > classes there as well. 17 | 18 | #### className (string) 19 | 20 | Adds an additional class to the root `
` element. 21 | 22 | #### logo (node) 23 | 24 | The image, text, or whatever else you may want to display in the left section 25 | of the banner. 26 | 27 | #### url (string) 28 | 29 | The current url, used to determine which link (or links) is active. If you're 30 | building a normal site it's enough to just pass `window.location.pathname`, 31 | with [react-router][2] `props.location.pathname` should be used. 32 | 33 | #### link (component) 34 | 35 | The component used for routing and displaying links. See the __Custom Links__ 36 | section below for more details. 37 | 38 | #### items (array) 39 | 40 | The data used to generate the navigation items. Pass an array of objects that 41 | conform to the spec in __Item Data__ below. 42 | 43 | #### searchBar (boolean) 44 | 45 | Pass `false` to remove the search bar. 46 | 47 | #### onMenuClick (function) 48 | 49 | A callback fired whenever the mobile ("hamburger") menu button is clicked. 50 | 51 | #### onSearch (function) 52 | 53 | A callback fired whenever the user changes the search input. 54 | 55 | 56 | ## Item Data 57 | 58 | Each link object __MUST__ contain `title` and `url` properties. This component 59 | will also handle the first level of `children` link objects by displaying a 60 | secondary navigation menu under the main banner. Any more levels of nested 61 | `children` would have to be handled using a custom link component (described 62 | above) or in another part of your site. Here's an example of a valid link 63 | object: 64 | 65 | ``` js 66 | { 67 | content: 'Example', 68 | url: '/example', 69 | children: [ ... ] // Optional 70 | } 71 | ``` 72 | 73 | Note that anything renderable is allowed in the link's `title` prop, e.g. the 74 | following code would allow you to render an icon in place of text (using JSX): 75 | 76 | ``` js 77 | { 78 | title: , 79 | url: 'https://github.com' 80 | } 81 | ``` 82 | 83 | > __Note:__ The icon code shown above is dependent on having an icon font 84 | > available (e.g. [font awesome][6]). However you could also render a full 85 | > SVG, component, or anything else in the same manner. 86 | 87 | 88 | ## Custom Links 89 | 90 | Passing a custom link component can be an easy way to extend the navigation 91 | section of the banner. For example, you could pass a custom component that 92 | acts as a dropdown to render `children` if your site contains a large amount 93 | of pages. 94 | 95 | There are two pre-built link components provided in the `/src/links` 96 | directory: `StandardLink` being the default while `SPALink` can be used for 97 | single page applications using react-router (like this site, for example). The 98 | data from each link object, described below, is spread onto this component as 99 | props as well as a BEM element class name and active modifier class (if the 100 | link is active). 101 | 102 | The two components above, however, are more for demonstration purposes and not 103 | very customizable. We recommend creating your own link component, using the [ 104 | two defaults][3] for inspiration. For example, you could use HTML5 history API 105 | manually: 106 | 107 | ``` js 108 | // ... import dependencies and such 109 | 110 | class CustomLink extends React.Component { 111 | render() { 112 | let { index, url, ...otherProps } = this.props 113 | 114 | return ( 115 | 119 | { props.children } 120 | 121 | ) 122 | } 123 | 124 | _navigate(url = '', event) { 125 | if ( !url.match(/^https?:/) ) { 126 | event.preventDefault() 127 | history.pushState({ 128 | some: 'state' 129 | }, 'MyTitle', url) 130 | } 131 | } 132 | } 133 | 134 | export default props => { 135 | return ( 136 | 149 | ) 150 | } 151 | ``` 152 | 153 | > __Note:__ If all you'd like to customize is the styling of items, there is 154 | > no need to pass a custom component. Simply fork and edit the stylesheet as 155 | > you wish. 156 | 157 | 158 | ## The Stylesheet 159 | 160 | This component comes with a [base stylesheet][4] that is meant to be forked 161 | and extended. Most people will at least need to change the color scheme to 162 | match their own branding. We encourage you to tweak the layout to suit your 163 | needs as well but will only officially support the base stylesheet provided. 164 | Most minor layout changes should be fine, but changing the layout of bits like 165 | the search bar could cause some funkiness. 166 | 167 | Below are some design variations people have come up with. If you work some 168 | magic of your own please [ping me][5] and I'll add it to the list! 169 | 170 | > @todo add screenshots 171 | 172 | 173 | [1]: http://getbem.com/naming/ 174 | [2]: https://github.com/ReactTraining/react-router 175 | [3]: https://github.com/skipjack/react-banner/tree/master/src/links 176 | [4]: https://github.com/skipjack/react-banner/blob/master/dist/style.css 177 | [5]: mailto:greg.venech@gmail.com 178 | [6]: http://fontawesome.io/ -------------------------------------------------------------------------------- /src/site/content/headroom.md: -------------------------------------------------------------------------------- 1 | React Banner does not provide any built-in "sticky-ness" but can easily be 2 | integrated with things like [React Headroom][1] to achieve this behavior. 3 | 4 | 5 | ## Basic Setup 6 | 7 | Going forward with our original example, you would `npm install react-headroom 8 | --save` and update the code as such: 9 | 10 | ``` diff 11 | import React from 'react' 12 | import Banner from 'react-banner' 13 | + import Headroom from 'react-headroom' 14 | import 'react-banner/dist/style.css' 15 | 16 | export default props => { 17 | return ( 18 | + 19 | 26 | + 27 | ) 28 | } 29 | ``` 30 | 31 | 32 | ## Configuration with React Sidebar 33 | 34 | Making this component play nicely with [react-sidebar][2] is a bit trickier 35 | but can still be done. You'll need to store your main content element and pass 36 | it as the `parent` prop to ``. See the [main component][3] used to 37 | generate this documentation for an example. 38 | 39 | For more details please see the headroom component's [documentation][1]. 40 | 41 | 42 | [1]: https://github.com/KyleAMathews/react-headroom 43 | [2]: ./integration/sidebar 44 | [3]: https://github.com/skipjack/react-banner/blob/master/src/site/site.jsx -------------------------------------------------------------------------------- /src/site/content/index.md: -------------------------------------------------------------------------------- 1 | **React Banner** is a react component for generating banners (or "navigation bars") like one displayed above. This component is easy to use, customize, and integrate with other open source react components. 2 | 3 | Banners of one kind or another are used on a wide variety of sites and often provide similar functionality such as _navigation_ and _searching_. Using this component lets you to quickly get this key part of your site up and running with very little code. Then easily customize and tweak the styling as your requirements or design evolves. 4 | 5 | 6 | ## Installation 7 | 8 | This component can be installed from npm: 9 | 10 | ``` bash 11 | npm install react-banner 12 | ``` 13 | 14 | You can also grab the minified JavaScript and CSS straight from `/dist` and include it with a `