├── .eslintrc
├── .gitignore
├── GETTING-STARTED.md
├── README.md
├── index.html
├── package.json
├── samples
├── blog
│ ├── css
│ │ └── algolia.css
│ └── index.html
└── ecommerce
│ ├── css
│ ├── index.css
│ ├── material.css
│ └── normalize.css
│ └── index.html
├── src
├── components
│ ├── BemHelper
│ │ └── index.js
│ ├── Facets
│ │ ├── Conjunctive
│ │ │ └── index.js
│ │ ├── Disjunctive
│ │ │ └── index.js
│ │ ├── Slider
│ │ │ └── index.js
│ │ ├── TabMenu
│ │ │ └── index.js
│ │ ├── index.js
│ │ └── order
│ │ │ └── index.js
│ ├── HitsSelector
│ │ └── index.js
│ ├── Hogan
│ │ └── index.js
│ ├── IndexSelector
│ │ └── index.js
│ ├── Intro
│ │ └── index.js
│ ├── Page
│ │ └── index.js
│ ├── Pagination
│ │ ├── PaginationLink
│ │ │ └── index.js
│ │ ├── Paginator
│ │ │ └── index.js
│ │ └── index.js
│ ├── Results
│ │ ├── Result
│ │ │ └── index.js
│ │ └── index.js
│ ├── SearchBox
│ │ └── index.js
│ └── SearchStats
│ │ └── index.js
├── index.js
└── setup
│ ├── index.js
│ └── url.js
└── todo.md
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "ecmaFeatures" : {
3 | "jsx" : true
4 | },
5 | "plugins" : [
6 | "react"
7 | ],
8 | "rules" : {
9 | "brace-style": [2, "stroustrup", { "allowSingleLine": true }],
10 | "camelcase" : 1,
11 | "comma-spacing": [2, {"before": false, "after": true}],
12 | "comma-style": [2, "last"],
13 | "consistent-return" : 2,
14 | "consistent-this": [1, "self"],
15 | "curly" : [2,"multi-line"],
16 | "default-case": 1,
17 | "eqeqeq": [2, "smart"],
18 | "global-strict" : 0,
19 | "indent": [2, 2, {"indentSwitchCase": true}],
20 | "key-spacing" : [ 1, { "beforeColon" : true, "afterColon" : true } ],
21 | "new-cap" : 2,
22 | "no-extra-bind" : 1,
23 | "no-multiple-empty-lines" : 1,
24 | "no-multi-spaces": 1,
25 | "no-else-return": 1,
26 | "no-spaced-func": 1,
27 | "no-trailing-spaces" : 2,
28 | "no-underscore-dangle": 0,
29 | "no-use-before-define": "nofunc",
30 | "quotes" : [1, "double"],
31 | "quotes" : [2, "double", "avoid-escape"],
32 | "semi": 2,
33 | "space-before-blocks": 1,
34 | "space-before-function-parentheses": [1, "never"],
35 | "space-infix-ops": 1,
36 | "space-in-parens": [1, "always"],
37 | "strict" : [2, "global"]
38 | },
39 | "globals" : {
40 | "module" : true,
41 | "require" : true,
42 | "document" : true
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Commenting this out is preferred by some people, see
24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
25 | node_modules
26 |
27 | # Users Environment Variables
28 | .lock-wscript
29 |
30 | docs
31 |
32 | # Build files
33 | dist/
34 |
--------------------------------------------------------------------------------
/GETTING-STARTED.md:
--------------------------------------------------------------------------------
1 | # Getting started with algolia UI kit
2 |
3 | The algolia UI kit is intended to be used with Algolia and Algolia only. You
4 | can create an account on https://www.algolia.com/users/sign_up
5 |
6 | ## Build the project (real distribution is coming)
7 |
8 | - clone repo : `git clone https://github.com/algolia/algolia-ui-kit.git`
9 | - move into the repo : `cd algolia-ui-kit`
10 | - download the dependencies : `npm install`
11 | - build the minified version : `npm run build:js`
12 |
13 | The minified version is in dist/algolia-ui-kit.min.js.
14 |
15 | ## Bootstrap page
16 |
17 | ### Starting from scratch
18 |
19 | - Create a new HTML page
20 | - Import the minified algolia-ui-kit file (see dist/algolia-ui-kit.min.js)
21 |
22 | ### Starting from an existing project
23 |
24 | - Import the minified algolia-ui-kit file (see dist/algolia-ui-kit.min.js)
25 |
26 | ## Configure index
27 |
28 | In order for the UI-kit to work, you need to provide informations about where
29 | it should fetch the data from (eg. the index you want to use for your application)
30 |
31 | This configuration needs to set on the body of the document with those keys :
32 | - data-algolia-key : the public search key of your index ( see https://www.algolia.com/licensing )
33 | - data-algolia-app-id : the id of your application ( see https://www.algolia.com/licensing )
34 | - data-algolia-index : the name of the index you want to target ( see https://www.algolia.com/explorer )
35 |
36 | The markup should look like :
37 |
38 | ```html
39 |
40 | ...
41 |
44 | ...
45 |
46 |
47 | ```
48 |
49 | ## The search results
50 |
51 | The results component let you specify how the search results will be displayed.
52 | This is a mandatory component.
53 |
54 | ### Configuration
55 |
56 | This component is configured by creating an empty div element that will hold
57 | the rendered results.
58 |
59 | There are 3 parameters to configure on this component :
60 | - the number of items to retrieve : data-hits-per-page
61 | - the template used to render a single result : data-hit-template
62 | - the template used when there is no results to display : data-no-results-template
63 |
64 | How the results are displayed can be customized by using templates (attribute
65 | data-hits-template on the div). This template will receive a single "hit" or
66 | "result" from the list of results retrieved from Algolia. The template is
67 | specified by giving the css selectors to the element that contains the template.
68 | The template should be written using Hogan implementation of Mustache.
69 |
70 | If no results are returned from the query, the content displayed can also be
71 | specified with a template (attribute data-no-results-template). Similarly configured
72 | this template will be rendered once and will receive the whole algolia results
73 | object.
74 |
75 | ```html
76 |
80 | ```
81 |
82 | ### Template
83 |
84 | The templating system behind is Hogan.js. It's an implementation of Mustache
85 | built by Twitter.
86 |
87 | ## Search box
88 |
89 | The search box is the main entry for your users in the search. When a key is hit
90 | then the query is updated and sent to the server. The results of this query is
91 | then feed back to the results component.
92 |
93 | ### Configuration
94 |
95 | The search box is defined with a div element with the classes "algolia-magic" and
96 | "search-box". It has option only one option data-placeholder which defines the
97 | value indicating to your user what they should search for.
98 |
99 | ```html
100 |
101 | ```
102 |
103 | ## Pagination
104 |
105 | Results are returned by page with respect to the hits per page parameter from
106 | the results component. In order to browse those pages, a search needs a pager
107 | or paginator.
108 |
109 | ### Configuration
110 |
111 | The paginator is described by a div element with the classes "algolia-magic" and
112 | "pagination". The only parameter of this component is the number of items "around"
113 | the selected page : **data-padding**.
114 |
115 | ```html
116 |
117 | ```
118 |
119 | ## Facets
120 |
121 | Facets are the attributes we can use to do some filtering over the results. By
122 | default, none are provided on an index. The good news is that they can be easily
123 | added from the administration website https://www.algolia.com/explorer#?tab=display
124 |
125 | Once configured those facets can be used to display some filtering components.
126 | Algolia UI kit currently supports :
127 | - list of facets
128 | - list of disjunctive facets (advanced facets that can be combine)
129 | - tab list (used for facetting sub categories)
130 | - slider (for filtering on numeric values)
131 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > Thanks for your interest in this project. We decided that the approach used in `algolia-ui-kit` (html markup based with magic) is not the best suited for our users, therefore we created [instantsearch.js](https://github.com/algolia/instantsearch.js). Let us know if you like the new project.
2 |
3 |
4 | UI Design Kit (Figma)
5 | ---------------------------
6 | You've been looking the the UI Design Kit (Figma) and landed on this page, just follow this link to get back on the right track.
7 | https://www.algolia.com/doc/guides/building-search-ui/resources/ui-kit/js/
8 |
9 | 
10 |
11 |
12 |
13 |
14 | DEPRECATED - Algolia Kite
15 | =============================
16 |
17 | Algolia UI-kit Kite is a UI framework to build in minutes a fresh user experience.
18 |
19 | This project is NOT FOR PROD and also not maintained anymore :) Have a look at [instantsearch.js](https://github.com/algolia/instantsearch.js) if you want javascript widgets for Algolia.
20 |
21 | What is the algolia UI-kit?
22 | ---------------------------
23 |
24 | It is UI framework based on a state of the art architecture with ease of use as
25 | the key feature.
26 |
27 | It uses DOM tagging features (classes and data attributes) for the configuration
28 | and leverages React for displaying the UI. You are not forced to use React
29 | yourself to use this UI kit. In fact, since the whole glue is made internally,
30 | you should consider using the component raw if you are already using React.
31 |
32 | Why should I bother?
33 | --------------------
34 |
35 | ### Easy
36 |
37 | After importing your data in Algolia, you can configure a whole new algolia
38 | search experience in only 3 steps :
39 | - add the script at the bottom of your page
40 | - add components and configurations directly in your HTML with only DOM classes
41 | and data attributes
42 | - reload your page!
43 |
44 | The kit is auto loaded and will automatically read the configuration.
45 |
46 | ### Fast
47 |
48 | Algolia UI-kit is built around two main components :
49 | - React, the blazing fast view layer built by Facebook
50 | - Algolia JS Helper v2, the search state managing layer on top of our raw API
51 |
52 | It's not only fast during the execution time, we've made it so it is very easy
53 | to get and deploy.
54 |
55 | How does it look?
56 | -----------------
57 |
58 | ```html
59 |
60 |
61 |
62 |
63 |
64 |
65 |
70 |
79 |
80 |
81 | ```
82 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Algolia UI-Kit
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "AlgoliasearchUIKit",
3 | "version": "1.0.0",
4 | "description": "Automagic kit for Algolia search",
5 | "main": "index.js",
6 | "author": "Algolia unicorn factory ",
7 | "scripts": {
8 | "postinstall": "mkdir -p ./dist",
9 | "build:js": "NODE_ENV=production browserify -d src/index.js | uglifyjs -c warnings=false -m > dist/algoliasearch-ui-kit.min.js && gzip -k -f -9 dist/algoliasearch-ui-kit.min.js",
10 | "watch:js": "watchify -v -d src/index.js -o dist/algoliasearch-ui-kit.js",
11 | "serve": "static -z -a 0.0.0.0 ."
12 | },
13 | "browserify": {
14 | "transform": [
15 | "babelify",
16 | "envify"
17 | ]
18 | },
19 | "devDependencies": {
20 | "babelify": "5.0.4",
21 | "browserify": "9.0.3",
22 | "envify": "3.4.0",
23 | "eslint-plugin-react": "2.2.0",
24 | "minifyify": "6.4.0",
25 | "node-static": "0.7.6",
26 | "uglifyjs": "2.4.10",
27 | "watchify": "2.4.0"
28 | },
29 | "dependencies": {
30 | "algoliasearch": "3.1.x",
31 | "algoliasearch-helper": "2.0.4",
32 | "classnames": "2.1.1",
33 | "hogan.js": "3.0.2",
34 | "lodash": "3.5.0",
35 | "qs": "2.4.2",
36 | "react": "0.13.1",
37 | "react-dropdown-button": "1.0.11",
38 | "react-slider": "0.4.2"
39 | },
40 | "license": "MIT"
41 | }
42 |
--------------------------------------------------------------------------------
/samples/blog/css/algolia.css:
--------------------------------------------------------------------------------
1 | input[type="text"].algolia-magic-search-box--input,
2 | .algolia-magic-search-box--input {
3 | padding: 10px 42px;
4 | display: inline-block;
5 | border-radius: 20px;
6 | border: solid 1px #ccc;
7 | background: #FFF;
8 | }
9 | input[type="text"].algolia-magic-search-box--input:focus,
10 | .algolia-magic-search-box--input:focus {
11 | border: solid 1px #2897C5;
12 | outline: 0;
13 | box-shadow: none;
14 | background:none;
15 | }
16 |
17 |
18 | .algolia-magic-tab-menu {
19 | width:100%;
20 | }
21 | .algolia-magic-tab-menu--item {
22 | font-size: 1.1em;
23 | margin-right: 14px;
24 | display: inline-block;
25 | cursor:pointer;
26 | }
27 | .algolia-magic-tab-menu--item__active {
28 | font-weight: bold;
29 | border-bottom: solid 2px #2897C5;
30 | line-height: 24px;
31 | }
32 | .algolia-magic-tab-menu--count {
33 | padding-left: 4px;
34 | font-size: .9em;
35 | color: #aaa;
36 | font-weight: 400;
37 | }
38 |
--------------------------------------------------------------------------------
/samples/blog/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Milliseconds Matter The Official Algolia Blog
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
66 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
89 |
90 |
91 |
92 |
97 |
98 |
103 |
104 |
109 |
110 |
115 |
116 |
181 |
182 |
187 |
188 |
189 |
219 |
224 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
273 |
274 |
275 |
276 |
277 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
297 |
298 |
299 |
300 |
301 |
302 |
325 |
326 |
327 |
328 |
329 |
330 |
355 |
356 |
357 |
358 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
--------------------------------------------------------------------------------
/samples/ecommerce/css/index.css:
--------------------------------------------------------------------------------
1 | html, body, .app{ height: 100%}
2 | hr {
3 | margin: 5px;
4 | }
5 |
6 | em {
7 | font-style : normal;
8 | text-decoration : underline;
9 | }
10 |
11 | a{
12 | color: #46AEDA;
13 | }
14 |
15 | .algolia-magic {
16 | opacity : 0;
17 | transition : opacity 500ms;
18 | }
19 |
20 | .algolia-magic.show{
21 | opacity : 1;
22 | }
23 |
24 | .header{
25 | display: block;
26 | background: rgb( 10, 24, 35 );
27 | color: #ECECE6;
28 | text-align: center;
29 | padding: 20px 0;
30 | }
31 |
32 | .app-name {
33 | font-size: 5em;
34 | }
35 |
36 | .tag-line{
37 | margin: -10px 0 10px;
38 | font-weight: lighter;
39 | color: #46AEDA;
40 | font-size: 2em;
41 | }
42 |
43 | .algolia{
44 | font-weight:normal;
45 | }
46 |
47 | .powered-by{
48 | font-size:small;
49 | font-weight:lighter;
50 | color:#558FE5;
51 | }
52 |
53 | .autocomplete-form {
54 | margin-top: 1em;
55 | margin-bottom: 1em;
56 | }
57 |
58 | .search-container {
59 | background : #12314A;
60 | margin: 0 0 2em 0;
61 | }
62 | .search{
63 | text-align:center;
64 | }
65 |
66 | .algolia-magic-search-box {
67 | margin : 0 auto;
68 | width: 100%;
69 | max-width : 600px;
70 | height: 2.4em;
71 | }
72 |
73 | .right{
74 | text-align: right;
75 | }
76 |
77 | .list-group-item.active, .list-group-item.active:focus, .list-group-item.active:hover {
78 | color:#558FE5;
79 | }
80 |
81 | .list-group .list-group-item.active:hover, .list-group .list-group-item.active:focus{
82 | outline:none;
83 | }
84 |
85 | .pagination * {
86 | background: transparent !important;
87 | border: none !important;
88 | }
89 |
90 | .pagination a {
91 | color: black !important;
92 | }
93 |
94 | .pagination > .active > a {
95 | color : #46AED5 !important;
96 | font-weight : bold;
97 | }
98 |
99 | .disabled span{
100 | color: lightgrey;
101 | }
102 |
103 | .results-container{
104 | height: 100%;
105 | }
106 |
107 | .results nav {
108 | text-align:center;
109 | }
110 | .result-items nav .pagination{
111 | margin : 20px auto;
112 | }
113 |
114 | .search-stats{
115 | position: relative;
116 | margin-bottom : 2em;
117 | }
118 | .search_stats_container > nav {
119 | position: absolute;
120 | top: 0;
121 | right: 5px;
122 | }
123 |
124 | .top_search_options{
125 | margin-bottom: 2em;
126 | }
127 | .tag{
128 | margin-right: 4px;
129 | display : inline-block;
130 | }
131 |
132 | .result{
133 | }
134 | .speaker-name{
135 | font-size:2em;
136 | margin-top : 0;
137 | }
138 |
139 | .product-image{
140 | max-width : 75px;
141 | max-height: 90px;
142 | margin-bottom : 15px;
143 | }
144 |
145 | .speaker-session-title{
146 | font-size:1.5em;
147 | margin: 0;
148 | }
149 |
150 | .speaker-sessions{
151 | padding: 5px 20px;
152 | }
153 |
154 | .form-control, .form-control:focus{
155 | color:lightgrey;
156 | background-image: linear-gradient(#46AEDA, #46AEDA), linear-gradient(#ddd, #ddd);
157 | }
158 |
159 | .no-results {
160 | text-align: center;
161 | font-size : 20px;
162 | }
163 |
164 | ::-webkit-input-placeholder { /* WebKit browsers */
165 | color : #eee!important;
166 | }
167 | :-moz-placeholder { /* Mozilla Firefox 4 to 18 */
168 | color : #eee!important;
169 | opacity : 1;
170 | }
171 | ::-moz-placeholder { /* Mozilla Firefox 19+ */
172 | color : #eee!important;
173 | opacity : 1;
174 | }
175 | :-ms-input-placeholder { /* Internet Explorer 10+ */
176 | color : #eee!important;
177 | }
178 |
179 | .horizontal-slider {
180 | height: 50px;
181 | }
182 |
183 | .horizontal-slider .bar {
184 | top: 20px;
185 | height: 10px;
186 | background: #DAECFA;
187 | }
188 |
189 | .horizontal-slider .bar-1 {
190 | background : #246396;
191 | }
192 |
193 | .horizontal-slider .handle {
194 | top: 15px;
195 | width: 20px;
196 | height: 20px;
197 | line-height: 30px;
198 | background: #224C6E;
199 | color: white;
200 | border-radius: 7px;
201 | }
202 |
203 | .statistics {
204 | text-align : left;
205 | }
206 |
207 | .results-header {
208 | display : flex;
209 | margin-bottom: 1em;
210 | align-items: flex-end;
211 | }
212 |
213 | .selectors {
214 | text-align : right;
215 | }
216 | .selectors > .algolia-magic.show {
217 | display: inline-block !important;
218 | }
219 |
--------------------------------------------------------------------------------
/samples/ecommerce/css/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */
2 |
3 | /**
4 | * 1. Set default font family to sans-serif.
5 | * 2. Prevent iOS text size adjust after orientation change, without disabling
6 | * user zoom.
7 | */
8 |
9 | html {
10 | font-family: sans-serif; /* 1 */
11 | -ms-text-size-adjust: 100%; /* 2 */
12 | -webkit-text-size-adjust: 100%; /* 2 */
13 | }
14 |
15 | /**
16 | * Remove default margin.
17 | */
18 |
19 | body {
20 | margin: 0;
21 | }
22 |
23 | /* HTML5 display definitions
24 | ========================================================================== */
25 |
26 | /**
27 | * Correct `block` display not defined for any HTML5 element in IE 8/9.
28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11
29 | * and Firefox.
30 | * Correct `block` display not defined for `main` in IE 11.
31 | */
32 |
33 | article,
34 | aside,
35 | details,
36 | figcaption,
37 | figure,
38 | footer,
39 | header,
40 | hgroup,
41 | main,
42 | menu,
43 | nav,
44 | section,
45 | summary {
46 | display: block;
47 | }
48 |
49 | /**
50 | * 1. Correct `inline-block` display not defined in IE 8/9.
51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
52 | */
53 |
54 | audio,
55 | canvas,
56 | progress,
57 | video {
58 | display: inline-block; /* 1 */
59 | vertical-align: baseline; /* 2 */
60 | }
61 |
62 | /**
63 | * Prevent modern browsers from displaying `audio` without controls.
64 | * Remove excess height in iOS 5 devices.
65 | */
66 |
67 | audio:not([controls]) {
68 | display: none;
69 | height: 0;
70 | }
71 |
72 | /**
73 | * Address `[hidden]` styling not present in IE 8/9/10.
74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
75 | */
76 |
77 | [hidden],
78 | template {
79 | display: none;
80 | }
81 |
82 | /* Links
83 | ========================================================================== */
84 |
85 | /**
86 | * Remove the gray background color from active links in IE 10.
87 | */
88 |
89 | a {
90 | background-color: transparent;
91 | }
92 |
93 | /**
94 | * Improve readability when focused and also mouse hovered in all browsers.
95 | */
96 |
97 | a:active,
98 | a:hover {
99 | outline: 0;
100 | }
101 |
102 | /* Text-level semantics
103 | ========================================================================== */
104 |
105 | /**
106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
107 | */
108 |
109 | abbr[title] {
110 | border-bottom: 1px dotted;
111 | }
112 |
113 | /**
114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
115 | */
116 |
117 | b,
118 | strong {
119 | font-weight: bold;
120 | }
121 |
122 | /**
123 | * Address styling not present in Safari and Chrome.
124 | */
125 |
126 | dfn {
127 | font-style: italic;
128 | }
129 |
130 | /**
131 | * Address variable `h1` font-size and margin within `section` and `article`
132 | * contexts in Firefox 4+, Safari, and Chrome.
133 | */
134 |
135 | h1 {
136 | font-size: 2em;
137 | margin: 0.67em 0;
138 | }
139 |
140 | /**
141 | * Address styling not present in IE 8/9.
142 | */
143 |
144 | mark {
145 | background: #ff0;
146 | color: #000;
147 | }
148 |
149 | /**
150 | * Address inconsistent and variable font size in all browsers.
151 | */
152 |
153 | small {
154 | font-size: 80%;
155 | }
156 |
157 | /**
158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers.
159 | */
160 |
161 | sub,
162 | sup {
163 | font-size: 75%;
164 | line-height: 0;
165 | position: relative;
166 | vertical-align: baseline;
167 | }
168 |
169 | sup {
170 | top: -0.5em;
171 | }
172 |
173 | sub {
174 | bottom: -0.25em;
175 | }
176 |
177 | /* Embedded content
178 | ========================================================================== */
179 |
180 | /**
181 | * Remove border when inside `a` element in IE 8/9/10.
182 | */
183 |
184 | img {
185 | border: 0;
186 | }
187 |
188 | /**
189 | * Correct overflow not hidden in IE 9/10/11.
190 | */
191 |
192 | svg:not(:root) {
193 | overflow: hidden;
194 | }
195 |
196 | /* Grouping content
197 | ========================================================================== */
198 |
199 | /**
200 | * Address margin not present in IE 8/9 and Safari.
201 | */
202 |
203 | figure {
204 | margin: 1em 40px;
205 | }
206 |
207 | /**
208 | * Address differences between Firefox and other browsers.
209 | */
210 |
211 | hr {
212 | -moz-box-sizing: content-box;
213 | box-sizing: content-box;
214 | height: 0;
215 | }
216 |
217 | /**
218 | * Contain overflow in all browsers.
219 | */
220 |
221 | pre {
222 | overflow: auto;
223 | }
224 |
225 | /**
226 | * Address odd `em`-unit font size rendering in all browsers.
227 | */
228 |
229 | code,
230 | kbd,
231 | pre,
232 | samp {
233 | font-family: monospace, monospace;
234 | font-size: 1em;
235 | }
236 |
237 | /* Forms
238 | ========================================================================== */
239 |
240 | /**
241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited
242 | * styling of `select`, unless a `border` property is set.
243 | */
244 |
245 | /**
246 | * 1. Correct color not being inherited.
247 | * Known issue: affects color of disabled elements.
248 | * 2. Correct font properties not being inherited.
249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
250 | */
251 |
252 | button,
253 | input,
254 | optgroup,
255 | select,
256 | textarea {
257 | color: inherit; /* 1 */
258 | font: inherit; /* 2 */
259 | margin: 0; /* 3 */
260 | }
261 |
262 | /**
263 | * Address `overflow` set to `hidden` in IE 8/9/10/11.
264 | */
265 |
266 | button {
267 | overflow: visible;
268 | }
269 |
270 | /**
271 | * Address inconsistent `text-transform` inheritance for `button` and `select`.
272 | * All other form control elements do not inherit `text-transform` values.
273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
274 | * Correct `select` style inheritance in Firefox.
275 | */
276 |
277 | button,
278 | select {
279 | text-transform: none;
280 | }
281 |
282 | /**
283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
284 | * and `video` controls.
285 | * 2. Correct inability to style clickable `input` types in iOS.
286 | * 3. Improve usability and consistency of cursor style between image-type
287 | * `input` and others.
288 | */
289 |
290 | button,
291 | html input[type="button"], /* 1 */
292 | input[type="reset"],
293 | input[type="submit"] {
294 | -webkit-appearance: button; /* 2 */
295 | cursor: pointer; /* 3 */
296 | }
297 |
298 | /**
299 | * Re-set default cursor for disabled elements.
300 | */
301 |
302 | button[disabled],
303 | html input[disabled] {
304 | cursor: default;
305 | }
306 |
307 | /**
308 | * Remove inner padding and border in Firefox 4+.
309 | */
310 |
311 | button::-moz-focus-inner,
312 | input::-moz-focus-inner {
313 | border: 0;
314 | padding: 0;
315 | }
316 |
317 | /**
318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in
319 | * the UA stylesheet.
320 | */
321 |
322 | input {
323 | line-height: normal;
324 | }
325 |
326 | /**
327 | * It's recommended that you don't attempt to style these elements.
328 | * Firefox's implementation doesn't respect box-sizing, padding, or width.
329 | *
330 | * 1. Address box sizing set to `content-box` in IE 8/9/10.
331 | * 2. Remove excess padding in IE 8/9/10.
332 | */
333 |
334 | input[type="checkbox"],
335 | input[type="radio"] {
336 | box-sizing: border-box; /* 1 */
337 | padding: 0; /* 2 */
338 | }
339 |
340 | /**
341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain
342 | * `font-size` values of the `input`, it causes the cursor style of the
343 | * decrement button to change from `default` to `text`.
344 | */
345 |
346 | input[type="number"]::-webkit-inner-spin-button,
347 | input[type="number"]::-webkit-outer-spin-button {
348 | height: auto;
349 | }
350 |
351 | /**
352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
354 | * (include `-moz` to future-proof).
355 | */
356 |
357 | input[type="search"] {
358 | -webkit-appearance: textfield; /* 1 */
359 | -moz-box-sizing: content-box;
360 | -webkit-box-sizing: content-box; /* 2 */
361 | box-sizing: content-box;
362 | }
363 |
364 | /**
365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X.
366 | * Safari (but not Chrome) clips the cancel button when the search input has
367 | * padding (and `textfield` appearance).
368 | */
369 |
370 | input[type="search"]::-webkit-search-cancel-button,
371 | input[type="search"]::-webkit-search-decoration {
372 | -webkit-appearance: none;
373 | }
374 |
375 | /**
376 | * Define consistent border, margin, and padding.
377 | */
378 |
379 | fieldset {
380 | border: 1px solid #c0c0c0;
381 | margin: 0 2px;
382 | padding: 0.35em 0.625em 0.75em;
383 | }
384 |
385 | /**
386 | * 1. Correct `color` not being inherited in IE 8/9/10/11.
387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets.
388 | */
389 |
390 | legend {
391 | border: 0; /* 1 */
392 | padding: 0; /* 2 */
393 | }
394 |
395 | /**
396 | * Remove default vertical scrollbar in IE 8/9/10/11.
397 | */
398 |
399 | textarea {
400 | overflow: auto;
401 | }
402 |
403 | /**
404 | * Don't inherit the `font-weight` (applied by a rule above).
405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
406 | */
407 |
408 | optgroup {
409 | font-weight: bold;
410 | }
411 |
412 | /* Tables
413 | ========================================================================== */
414 |
415 | /**
416 | * Remove most spacing between table cells.
417 | */
418 |
419 | table {
420 | border-collapse: collapse;
421 | border-spacing: 0;
422 | }
423 |
424 | td,
425 | th {
426 | padding: 0;
427 | }
428 |
--------------------------------------------------------------------------------
/samples/ecommerce/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
21 |
26 |
27 |
28 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
63 |
66 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/src/components/BemHelper/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var BemHelper = function BemHelper( block ) {
4 | return function( element, modifier ) {
5 | if ( !element ) {
6 | return block;
7 | }
8 | if ( !modifier ) {
9 | return block + "--" + element;
10 | }
11 | return block + "--" + element + "__" + modifier;
12 | };
13 | };
14 |
15 | module.exports = BemHelper;
16 |
--------------------------------------------------------------------------------
/src/components/Facets/Conjunctive/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var React = require( "react" );
3 | var map = require( "lodash/collection/map" );
4 | var order = require( "../order" );
5 |
6 | var Facet = React.createClass( {
7 | render : function() {
8 | var facet = this.props.facet;
9 | var helper = this.props.helper;
10 | var sort = this.props.sort;
11 | var titleLabel = this.titleLabel || facet.name;
12 |
13 | var title = { titleLabel } ;
14 |
15 | var facetValues = map( facet.data, function( nb, facetFilter ) {
16 | return {
17 | name : facetFilter,
18 | isRefined : helper.isRefined( facet.name, facetFilter ),
19 | value : nb
20 | };
21 | } );
22 |
23 | if( sort && order[ sort ] ){
24 | facetValues.sort( order[ sort ] );
25 | }
26 |
27 | var values = map( facetValues, function( facetValue ) {
28 | var activeClass = helper.isRefined( facet.name, facetValue.name ) ? " active" : "";
29 | var className = "list-group-item" + activeClass;
30 | return
34 | { facetValue.name } ({ facetValue.value })
35 | ;
36 | }, this );
37 |
38 | return
39 |
{ title }
40 |
{values}
41 |
;
42 | },
43 | toggleSelect : function( facetName, facetValue, event ) {
44 | event.preventDefault();
45 | this.props.helper.toggleRefine( facetName, facetValue ).search();
46 | }
47 | } );
48 |
49 | module.exports = Facet;
50 |
--------------------------------------------------------------------------------
/src/components/Facets/Disjunctive/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var React = require( "react" );
3 | var map = require( "lodash/collection/map" );
4 | var order = require( "../order" );
5 |
6 | var DisjunctiveFacet = React.createClass( {
7 | render : function() {
8 | var facet = this.props.facet;
9 | var helper = this.props.helper;
10 | var sort = this.props.sort;
11 | var titleLabel = this.props.titleLabel || facet.name;
12 |
13 | var title = { titleLabel } ;
14 |
15 | var facetValues = map( facet.data, function( nb, facetFilter ) {
16 | return {
17 | name : facetFilter,
18 | isRefined : helper.isRefined( facet.name, facetFilter ),
19 | value : nb
20 | };
21 | } );
22 |
23 | if( sort && order[ sort ] ){
24 | facetValues.sort( order[ sort ] );
25 | }
26 |
27 | var values = map( facetValues, function( facetValue ) {
28 | var activeClass = facetValue.isRefined ? " active" : "";
29 | var className = "list-group-item" + activeClass;
30 | return
34 |
38 | { facetValue.name } ({ facetValue.value })
39 | ;
40 | }, this );
41 |
42 | return
43 |
{ title }
44 |
{values}
45 |
;
46 | },
47 | toggleSelect : function( facetName, facetValue, event ) {
48 | event.preventDefault();
49 | this.props.helper.toggleRefine( facetName, facetValue ).search();
50 | }
51 | } );
52 |
53 | module.exports = DisjunctiveFacet;
54 |
--------------------------------------------------------------------------------
/src/components/Facets/Slider/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var React = require( "react" );
3 |
4 | var Slider = require( "react-slider" );
5 |
6 | var AlgoliaSlider = React.createClass( {
7 | render : function() {
8 | var helper = this.props.helper;
9 | var facet = this.props.facet;
10 | var titleLabel = this.props.titleLabel || facet.name;
11 |
12 | var title = { titleLabel } ;
13 |
14 | var min = ( facet.stats && facet.stats.min ) || 0;
15 | var max = ( facet.stats && facet.stats.max ) || 0;
16 | var values = [
17 | helper.state.getNumericRefinement( facet.name, ">=" ) || min,
18 | helper.state.getNumericRefinement( facet.name, "<=" ) || max
19 | ];
20 |
21 | return
22 |
{ title }
23 |
24 |
{ values[0] } - { values[1] }
25 |
34 |
35 |
36 |
37 |
38 |
;
39 | },
40 | change : function( values ) {
41 | this.props.helper.addNumericRefinement( this.props.facet.name, ">=", values[0] )
42 | .addNumericRefinement( this.props.facet.name, "<=", values[1] );
43 | },
44 | search : function() {
45 | this.props.helper.search();
46 | }
47 | } );
48 |
49 | module.exports = AlgoliaSlider;
50 |
--------------------------------------------------------------------------------
/src/components/Facets/TabMenu/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var React = require( "react" );
3 | var cx = require( "classnames" );
4 | var forEach = require( "lodash/collection/forEach" );
5 | var sortBy = require( "lodash/collection/sortBy" );
6 | var map = require( "lodash/collection/map" );
7 | var bem = require( "../../BemHelper" )( "algolia-magic-tab-menu" );
8 |
9 | // TODO: We should handle i18n in a much better way.
10 | var facetAllValue = "All";
11 |
12 | var TabMenu = React.createClass( {
13 | _getKey : function _getKey( facet ) {
14 | return [
15 | "algolia-tab-menu",
16 | this.props.facet.name,
17 | facet.value
18 | ].join( "-" );
19 | },
20 | _getCategories : function _getCategories( facets ) {
21 | var results = [];
22 | var allCount = 0;
23 | forEach( facets, function( count, value ) {
24 | results.push( {value : value, count : count} );
25 | allCount += count;
26 | } );
27 | results = sortBy( results, "count" ).reverse();
28 | results.unshift( {value : facetAllValue, count : allCount} );
29 | return results;
30 | },
31 | _getActiveTab : function _getActiveTab() {
32 | var facetName = this.props.facet.name;
33 | var refinements = this.props.helper.state.disjunctiveFacetsRefinements[facetName];
34 | if ( !refinements ) {
35 | return facetAllValue;
36 | }
37 | return refinements[0];
38 | },
39 | render : function() {
40 | var self = this;
41 | var facets = self._getCategories( this.props.facet.data );
42 |
43 | var lis = map( facets, function( facet ) {
44 | var uniqueKey = self._getKey( facet );
45 | var onChange = self.onChange.bind( self, facet.value );
46 |
47 | var className = {};
48 | className[bem( "item" )] = true;
49 | className[bem( "item", "active" )] = ( facet.value === self._getActiveTab() );
50 | className = cx( className );
51 |
52 | return (
53 |
54 | {facet.value}
55 | {facet.count}
56 |
57 | );
58 | } );
59 |
60 | return ;
61 | },
62 | onChange : function onChange( facetValue ) {
63 | var facetName = this.props.facet.name;
64 | this.props.helper.clearRefinements( facetName );
65 | if ( facetValue !== facetAllValue ) {
66 | this.props.helper.toggleRefine( facetName, facetValue );
67 | }
68 | this.props.helper.search();
69 | }
70 | } );
71 |
72 | module.exports = TabMenu;
73 |
--------------------------------------------------------------------------------
/src/components/Facets/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var React = require( "react" );
3 | var map = require( "lodash/collection/map" );
4 |
5 | var Facets = React.createClass( {
6 | render : function() {
7 | var results = this.props.results;
8 | var helper = this.props.helper;
9 | var facets = map( results.facets, function( facet ) {
10 |
11 | }, this );
12 |
13 | return { facets }
;
14 | },
15 | } );
16 |
17 | module.exports = Facets;
18 |
--------------------------------------------------------------------------------
/src/components/Facets/order/index.js:
--------------------------------------------------------------------------------
1 | var order = {
2 | lexicographic : function( facetValueA, facetValueB ){
3 | return facetValueA.name.localeCompare( facetValueB.name );
4 | },
5 | lexicographicWithSelectedFirst : function( facetValueA, facetValueB ){
6 | if( facetValueB.isRefined === facetValueA.isRefined ){
7 | return order.lexicographic( facetValueA, facetValueB );
8 | }
9 | if( facetValueA.isRefined ){
10 | return -1;
11 | }
12 | else {
13 | return 1;
14 | }
15 | }
16 | };
17 |
18 | module.exports = order;
19 |
--------------------------------------------------------------------------------
/src/components/HitsSelector/index.js:
--------------------------------------------------------------------------------
1 | var React = require( "react" );
2 | var DDButton = require( "react-dropdown-button" )
3 |
4 | var AlgoliasearchHelper = require( "algoliasearch-helper/src/algoliasearch.helper" );
5 | var SearchResults = require( "algoliasearch-helper/src/SearchResults" );
6 | var SearchParameters = require( "algoliasearch-helper/src/SearchParameters" );
7 |
8 | var map = require( "lodash/collection/map" );
9 | var without = require( "lodash/array/without" );
10 |
11 | var HitsSelector = React.createClass( {
12 | propTypes : {
13 | helper : React.PropTypes.instanceOf( AlgoliasearchHelper ),
14 | results : React.PropTypes.instanceOf( SearchResults ),
15 | searchState : React.PropTypes.instanceOf( SearchParameters ),
16 | displayOptions : React.PropTypes.arrayOf( React.PropTypes.number )
17 | },
18 | render : function() {
19 | var selectedOption = parseInt( this.props.searchState.hitsPerPage, 10 );
20 | var selectableOptions = map(
21 | without( this.props.displayOptions, selectedOption ).sort( function(a, b){
22 | if( a > b ) return 1;
23 | return a < b ? -1 : 0;
24 | } ),
25 | function toDisplayObject( opt ) {
26 | return { label : opt, value : opt };
27 | }
28 | );
29 | return
30 | Display : { selectedOption }
31 | ;
32 | },
33 | changeIndex : function( e, itemProps ){
34 | this.props.helper.setState(
35 | this.props.searchState.setHitsPerPage( itemProps.data.value )
36 | ).search();
37 | }
38 | } );
39 |
40 | module.exports = HitsSelector;
41 |
--------------------------------------------------------------------------------
/src/components/Hogan/index.js:
--------------------------------------------------------------------------------
1 | var React = require( "react" );
2 |
3 | var hogan = require( "hogan.js" );
4 | var memoize = require( "lodash/function/memoize" );
5 |
6 | var memoHogan = memoize( hogan.compile.bind( hogan ) );
7 |
8 | var HoganResult = React.createClass( {
9 | componentWillMount : function(){
10 | this.setState( {
11 | template : memoHogan( this.props.template )
12 | } );
13 | },
14 | render : function(){
15 | var content = this.state.template.render( this.props.data );
16 | return
;
17 | }
18 | } );
19 |
20 | module.exports = HoganResult;
21 |
--------------------------------------------------------------------------------
/src/components/IndexSelector/index.js:
--------------------------------------------------------------------------------
1 | var React = require( "react" );
2 | var DDButton = require( "react-dropdown-button" )
3 |
4 | var AlgoliasearchHelper = require( "algoliasearch-helper/src/algoliasearch.helper" );
5 | var SearchResults = require( "algoliasearch-helper/src/SearchResults" );
6 | var SearchParameters = require( "algoliasearch-helper/src/SearchParameters" );
7 |
8 | var find = require( "lodash/collection/find" );
9 | var reject = require( "lodash/collection/reject" );
10 |
11 | var IndexSelector = React.createClass( {
12 | propTypes : {
13 | helper : React.PropTypes.instanceOf( AlgoliasearchHelper ),
14 | results : React.PropTypes.instanceOf( SearchResults ),
15 | searchState : React.PropTypes.instanceOf( SearchParameters ),
16 | indices : React.PropTypes.arrayOf( React.PropTypes.shape( {
17 | label : React.PropTypes.string,
18 | index : React.PropTypes.string
19 | } ) ),
20 | selectedIndex : React.PropTypes.string
21 | },
22 | render : function() {
23 | var isSelectedIndex = function( i ) {
24 | return this.props.selectedIndex === i.index
25 | };
26 | var selectedIndex = find( this.props.indices, isSelectedIndex, this );
27 | var displayableIndices = reject( this.props.indices, isSelectedIndex, this );
28 | return
29 | Sort by : { selectedIndex.label }
30 | ;
31 | },
32 | changeIndex : function( e, itemProps ){
33 | this.props.helper.setIndex( itemProps.data.index ).search();
34 | }
35 | } );
36 |
37 | module.exports = IndexSelector;
38 |
--------------------------------------------------------------------------------
/src/components/Intro/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var React = require( "react" );
3 |
4 | class Intro extends React.Component {
5 | render() {
6 | return
7 |
Find the speaker!
8 |
An app powered by
9 |
;
10 | }
11 | }
12 |
13 | module.exports = Intro;
14 |
--------------------------------------------------------------------------------
/src/components/Page/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var React = require( "react" );
3 | var SearchBox = require( "../SearchBox" );
4 | var Results = require( "../Results" );
5 | var Intro = require( "../Intro" );
6 |
7 | class Page extends React.Component {
8 | render() {
9 | var result = this.props.result;
10 | var searchState = this.props.searchState;
11 | var helper = this.props.helper;
12 | var resultRendered = result.nbHits > 0 ?
13 | :
14 | No results could be found :(
;
15 |
16 | return
17 |
18 |
19 |
20 |
27 |
28 |
29 | { resultRendered }
30 |
31 |
32 | ;
33 | }
34 | componentDidMount() {
35 | this.searchContainerPosition = React.findDOMNode( this.refs.searchContainer ).getBoundingClientRect().top;
36 | }
37 | moveToRef() {
38 | window.scrollTo( 0, this.searchContainerPosition );
39 | }
40 | }
41 |
42 | module.exports = Page;
43 |
--------------------------------------------------------------------------------
/src/components/Pagination/PaginationLink/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var React = require( "react" );
3 |
4 | class PaginationLink extends React.Component {
5 | render() {
6 | var label = this.props.label;
7 | var ariaLabel = this.props.ariaLabel;
8 | var href = this.props.href;
9 | var page = this.props.page;
10 | var className = this.props.className;
11 |
12 | var disabled = ( page === undefined );
13 |
14 | if( disabled ) {
15 | return
16 |
17 | { label }
18 |
19 | ;
20 | }
21 |
22 | return
23 |
24 | { label }
25 |
26 | ;
27 | }
28 | clickDisabled( e ){
29 | e.preventDefault();
30 | }
31 | click( page, e ){
32 | e.preventDefault();
33 | this.props.helper.setCurrentPage( page ).search();
34 | }
35 | }
36 |
37 | module.exports = PaginationLink;
38 |
--------------------------------------------------------------------------------
/src/components/Pagination/Paginator/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var range = require( "lodash/utility/range" );
3 | var Paginator = function Paginator( params ) {
4 | if( params.currentPage < 0 || params.currentPage > params.total ) {
5 | throw new Error( "First page is out of bounds. Was " +
6 | params.currentPage + ", expected between 0 and " + params.total );
7 | }
8 | this.currentPage = params.currentPage;
9 | this.total = params.total;
10 | this.padding = params.padding || 5;
11 | };
12 |
13 |
14 | Paginator.prototype = {
15 | constructor : Paginator,
16 | pages : function() {
17 | var current = this.currentPage;
18 | var padding = this.padding;
19 | var paddingLeft = this.calculatePaddingLeft( current, padding, this.total );
20 | var paddingRight = Math.min( 2 * padding + 1, this.total ) - paddingLeft;
21 | var first = current - paddingLeft;
22 | var last = current + paddingRight;
23 | return range( first, last );
24 | },
25 | calculatePaddingLeft : function( current, padding, total ) {
26 | if( current <= padding ) {
27 | return current;
28 | }
29 | else {
30 | if( current >= ( total - padding ) ) {
31 | return 2 * padding + 1 - ( total - current );
32 | }
33 | else {
34 | return padding;
35 | }
36 | }
37 | },
38 | showFirstEllipsis : function() {
39 | return this.currentPage > this.padding;
40 | },
41 | showLastEllipsis : function() {
42 | return this.currentPage < ( this.total - this.padding );
43 | },
44 | isLastPage : function() {
45 | return this.currentPage === this.total - 1;
46 | },
47 | isFirstPage : function() {
48 | return this.currentPage === 0;
49 | }
50 | };
51 |
52 | module.exports = Paginator;
53 |
--------------------------------------------------------------------------------
/src/components/Pagination/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var React = require( "react" );
3 | var forEach = require( "lodash/collection/forEach" );
4 |
5 | var Paginator = require( "./Paginator" );
6 | var PaginationLink = require( "./PaginationLink" );
7 |
8 | class Pagination extends React.Component {
9 | render() {
10 | var results = this.props.results;
11 |
12 | if( !results || !results.hits || results.hits.length === 0 ) return ;
13 |
14 | var pager = new Paginator( {
15 | currentPage : results.page,
16 | total : results.nbPages,
17 | padding : this.props.padding
18 | } );
19 |
20 | if( pager ) {
21 | return
22 |
23 | { this.props.showFirstLast ? this.firstPageLink( pager ) : undefined }
24 | { this.previousPageLink( pager ) }
25 | { this.pages( pager ) }
26 | { this.nextPageLink( pager ) }
27 | { this.props.showFirstLast ? this.lastPageLink( pager ) : undefined }
28 |
29 | ;
30 | }
31 | return ;
32 | }
33 | previousPageLink( pager ) {
34 | var label = this.props.labels.previous;
35 | if( !label ) return undefined;
36 |
37 | var page = pager.isFirstPage() ? undefined : pager.currentPage - 1;
38 | return ;
40 | }
41 | nextPageLink( pager ) {
42 | var label = this.props.labels.next;
43 | if( !label ) return undefined;
44 |
45 | var page = pager.isLastPage() ? undefined : pager.currentPage + 1;
46 | return
48 | }
49 | firstPageLink( pager ) {
50 | var label = this.props.labels.first;
51 | if( !label ) return undefined;
52 |
53 | var page = pager.isFirstPage() ? undefined : 0;
54 | return ;
56 | }
57 | lastPageLink( pager ) {
58 | var label = this.props.labels.last;
59 | if( !label ) return undefined;
60 |
61 | var page = pager.isLastPage() ? undefined : pager.total - 1;
62 | return ;
64 | }
65 | pages( pager ) {
66 | var pages = pager.pages();
67 | var elements = [];
68 |
69 | forEach( pages, function( pageNumber ) {
70 | var className = pageNumber === pager.currentPage ? "active" : "";
71 | elements.push(
72 |
76 | );
77 | }, this );
78 |
79 | return elements;
80 | }
81 | }
82 |
83 | Pagination.defaultProps = {
84 | labels : {
85 | previous : ‹ ,
86 | next : › ,
87 | first : « ,
88 | last : »
89 | },
90 | showFirstLast : true
91 | };
92 |
93 | module.exports = Pagination;
94 |
--------------------------------------------------------------------------------
/src/components/Results/Result/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var React = require( "react" );
3 | var map = require( "lodash/collection/map" );
4 |
5 | var baseURL = "http://www.irce.com";
6 |
7 | class Result extends React.Component{
8 | render() {
9 | var result = this.props.result;
10 | var sessions = map( result.sessions, function( session ) {
11 | return { session.name } ;
12 | } );
13 | var style = {
14 | float : "left",
15 | background : "#aaa"
16 | };
17 | return
18 |
19 |
20 |
21 |
25 |
26 |
Sessions
27 |
30 |
31 |
32 |
;
33 | }
34 | }
35 |
36 | module.exports = Result;
37 |
--------------------------------------------------------------------------------
/src/components/Results/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var React = require( "react" );
3 | var map = require( "lodash/collection/map" );
4 |
5 | var Hogan = require( "../Hogan" );
6 |
7 | class Results extends React.Component {
8 | render() {
9 | var results = this.props.results;
10 | if( !results || !results.hits || results.hits.length === 0 ) {
11 | return this.renderNoResults( results, this.props.noResultsTemplate );
12 | }
13 | return this.renderWithResults( results.hits, this.props.hitTemplate );
14 | }
15 | renderWithResults( hits, hitTemplate ) {
16 | var renderedHits = map( hits, function( hit ) {
17 | return ;
18 | } );
19 | return { renderedHits }
20 | }
21 | renderNoResults( results, noResultsTemplate ) {
22 | return
23 |
24 |
;
25 | }
26 | }
27 |
28 | module.exports = Results;
29 |
--------------------------------------------------------------------------------
/src/components/SearchBox/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var React = require( "react" );
3 | var bem = require( "../BemHelper" )( "algolia-magic-search-box" );
4 | var cx = require( "classnames" );
5 |
6 | var AlgoliasearchHelper = require( "algoliasearch-helper/src/algoliasearch.helper" );
7 | var SearchResults = require( "algoliasearch-helper/src/SearchResults" );
8 |
9 | var SearchBox = React.createClass( {
10 | propTypes : {
11 | helper : React.PropTypes.instanceOf( AlgoliasearchHelper ),
12 | results : React.PropTypes.instanceOf( SearchResults ),
13 | onFocus : React.PropTypes.func,
14 | placeholder : React.PropTypes.string
15 | },
16 | render : function() {
17 | var onFocus = this.props.onFocus;
18 | var classNames = cx( bem( "input" ), this.props.inputClass );
19 |
20 | return ;
31 | },
32 | change : function( e ) {
33 | var value = e.target.value;
34 | var helper = this.props.helper;
35 |
36 | helper.setQuery( value ).search();
37 | }
38 | } );
39 |
40 | module.exports = SearchBox;
41 |
42 |
--------------------------------------------------------------------------------
/src/components/SearchStats/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var React = require( "react" );
3 |
4 | var Pagination = require( "../Pagination" );
5 |
6 | var paginationLabels = {
7 | next : "suivant",
8 | previous : "précédent"
9 | };
10 |
11 | class SearchStats extends React.Component {
12 | render() {
13 | return
14 |
15 | { this.props.results.nbHits } jeux de données
16 | trouvés
17 |
18 |
22 |
;
23 | }
24 | }
25 |
26 | module.exports = SearchStats;
27 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var React = require( "react" );
3 | var algoliasearch = require( "algoliasearch" );
4 | var algoliasearchHelper = require( "algoliasearch-helper" );
5 |
6 | var forEach = require( "lodash/collection/forEach" );
7 | var map = require( "lodash/collection/map" );
8 |
9 | var setup = require( "./setup" );
10 |
11 | var Slider = require( "./components/Facets/Slider" );
12 | var TabMenu = require( "./components/Facets/TabMenu" );
13 | var ConjunctiveF = require( "./components/Facets/Conjunctive" );
14 | var DisjunctiveF = require( "./components/Facets/Disjunctive" );
15 | var Results = require( "./components/Results" );
16 | var SearchBox = require( "./components/SearchBox" );
17 | var Pagination = require( "./components/Pagination" );
18 | var Hogan = require( "./components/Hogan" );
19 | var IndexSelector = require( "./components/IndexSelector" );
20 | var HitsSelector = require( "./components/HitsSelector" );
21 |
22 | var URLManager = require( "./setup/url.js" );
23 |
24 | ( function setupAll() {
25 | var firstRendering = true;
26 | var appConfig = setup.readAlgoliaConfig( document );
27 | var containers = setup.readContainersConfig( document );
28 |
29 | //check configuration
30 | if( !containers || !containers.results ) {
31 | throw new Error( "The result-items containers must be defined in order to correctly initialize the UI-kit. Please check GETTING-STARTED.md chapter called 'The search results'" );
32 | }
33 |
34 | var result = {};
35 | var state = {};
36 |
37 | var facets = map( containers.facets, "name" );
38 | var disjunctiveFacets = map( containers.disjunctiveFacets, "name" )
39 | .concat( map( containers.sliders, "name" ) )
40 | .concat( map( containers.tabMenu, "name" ) )
41 | var defaultUrlState = {
42 | page : 0,
43 | query : "",
44 | disjunctiveFacetsRefinements : {},
45 | facetsRefinements : {},
46 | numericRefinements : {}
47 | };
48 | var urlManager = new URLManager(
49 | defaultUrlState,
50 | {
51 | hitsPerPage : containers.results.hitsPerPage,
52 | facets : facets,
53 | disjunctiveFacets : disjunctiveFacets
54 | }
55 | );
56 |
57 | var initialState = urlManager.init();
58 |
59 | var client = algoliasearch( appConfig.appID, appConfig.key );
60 | var helper = algoliasearchHelper( client, appConfig.index, initialState );
61 |
62 | helper.on( "result", function( newResult, newState ) {
63 | result = newResult;
64 | render( helper, newState, result );
65 | } );
66 |
67 | helper.on( "change", function( newState ) {
68 | state = newState;
69 | urlManager.update( state );
70 | render( helper, state, result );
71 | } );
72 |
73 |
74 | window.addEventListener( "popstate", function( event ){
75 | helper.overrideStateWithoutTriggeringChangeEvent( event.state )
76 | .search();
77 | } );
78 |
79 | helper.search();
80 |
81 | function render( h, s, r ) {
82 | if( containers.results ) {
83 | React.render( ,
88 | containers.results.node );
89 | }
90 |
91 | if( containers.searchBox ) {
92 | React.render( ,
95 | containers.searchBox.node );
96 | }
97 |
98 | if( containers.pagination ) {
99 | React.render( ,
103 | containers.pagination.node );
104 | }
105 |
106 | if( containers.statistics ) {
107 | React.render( ,
109 | containers.statistics.node );
110 | }
111 |
112 | if( containers.indexSelector ) {
113 |
114 | React.render( ,
119 | containers.indexSelector.node );
120 | }
121 |
122 | if( containers.hitsSelector ) {
123 | React.render( ,
125 | containers.hitsSelector.node );
126 | }
127 |
128 | forEach( containers.tabMenu, function( f ) {
129 | React.render( ,
133 | f.node );
134 | } );
135 |
136 | forEach( containers.facets, function( f ) {
137 | React.render( ,
142 | f.node );
143 | } );
144 |
145 | forEach( containers.disjunctiveFacets, function( f ) {
146 | React.render( ,
151 | f.node );
152 | } );
153 |
154 | forEach( containers.sliders, function( slider ) {
155 | React.render( ,
159 | slider.node );
160 | } );
161 |
162 | if( firstRendering ) {
163 | forEach( document.querySelectorAll( ".algolia-magic" ), function( d ) {
164 | d.classList.add( "show" );
165 | } );
166 | firstRendering = false;
167 | }
168 | }
169 |
170 | function getFacetOrDefaults( results, facetName ) {
171 | return results.getFacetByName( facetName ) || {name : facetName, data : [] };
172 | }
173 | } )();
174 |
--------------------------------------------------------------------------------
/src/setup/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var map = require( "lodash/collection/map" );
3 |
4 | module.exports = {
5 | /**
6 | * Read the algolia configuration from a given dom node.
7 | * @param {DOMNode} dom node holding the configuration
8 | * @return {Object} configuration for the algolia search client
9 | */
10 | readAlgoliaConfig : function readAlgoliaConfig( dom ) {
11 | var d = dom.body.dataset;
12 |
13 | var appID = d.algoliaAppId;
14 | var key = d.algoliaKey;
15 | var index = d.algoliaIndex;
16 |
17 | if( !appID || !key || !index ){
18 | throw new Error( "algolia-app-id, algolia-key and algolia-index are mandatory parameters on the body. Check out the GETTING_STARTED.md chapter called 'configure index'." );
19 | }
20 |
21 | return {
22 | appID : d.algoliaAppId,
23 | key : d.algoliaKey,
24 | index : d.algoliaIndex
25 | };
26 | },
27 | /**
28 | * Read the current DOM node for component configurations. Return a list
29 | * of components to render. Those components should follow the same props
30 | * signature : { Helper, SearchResults, SearchState }
31 | * @param {DOMNode} dom the root dom node in which to look for components
32 | * @return {Array} preconfigured react components
33 | */
34 | readContainersConfig : function lookForContainers( dom ) {
35 | var containersConfig = {};
36 | containersConfig.indexSelector = ( function( d ) {
37 | if( d === null ) return undefined;
38 |
39 | var options = d.querySelectorAll( "[data-index-name]" );
40 | var indices = map( options, function( d0 ) {
41 | return {
42 | label : d0.innerHTML,
43 | index : d0.dataset.indexName
44 | };
45 | } );
46 |
47 | return {
48 | node : d,
49 | indices : indices
50 | };
51 | } )( dom.querySelector( ".algolia-magic.index-selector" ) );
52 |
53 | containersConfig.hitsSelector = ( function( d ) {
54 | if( d === null ) return undefined;
55 |
56 | var options = d.querySelectorAll( "[data-hits-per-page]" );
57 | var displayOptions = map( options, function( d0 ) {
58 | return parseInt( d0.dataset.hitsPerPage, 10 );
59 | } );
60 |
61 | return {
62 | node : d,
63 | displayOptions : displayOptions
64 | };
65 | } )( dom.querySelector( ".algolia-magic.hits-selector" ) );
66 |
67 | containersConfig.sliders = map(
68 | dom.querySelectorAll( ".algolia-magic.slider" ),
69 | function domToSlider( d ) {
70 | return {
71 | node : d,
72 | name : d.dataset.facetName,
73 | titleLabel : d.dataset.titleLabel
74 | };
75 | } );
76 |
77 | containersConfig.tabMenu = map(
78 | dom.querySelectorAll( ".algolia-magic-tab-menu" ),
79 | function domToBelongsToTabMenu( d ) {
80 | return {
81 | node : d,
82 | name : d.dataset.algoliaFacetName
83 | };
84 | } );
85 |
86 | containersConfig.facets = map(
87 | dom.querySelectorAll( ".algolia-magic.facet" ),
88 | function domToFacet( d ) {
89 | return {
90 | node : d,
91 | name : d.dataset.facetName,
92 | sort : d.dataset.sort,
93 | titleLabel : d.dataset.titleLabel
94 | };
95 | } );
96 |
97 | containersConfig.disjunctiveFacets = map(
98 | dom.querySelectorAll( ".algolia-magic.disjunctive-facet" ),
99 | function domToFacet( d ) {
100 | return {
101 | node : d,
102 | name : d.dataset.facetName,
103 | sort : d.dataset.sort,
104 | titleLabel : d.dataset.titleLabel
105 | };
106 | } );
107 |
108 | containersConfig.searchBox = ( function domToSearchBox( d ) {
109 | if( d === null ) return undefined;
110 |
111 | return {
112 | node : d,
113 | placeholder : d.dataset.placeholder,
114 | inputClass : d.dataset.inputClass
115 | };
116 | } )( dom.querySelector( ".algolia-magic-search-box" ) );
117 |
118 | containersConfig.results = ( function domToResult( d ) {
119 | if( d === null ) return undefined;
120 |
121 | var noResSelector = d.dataset.noResultsTemplate;
122 | var noResTmplate = ( function( sel ){
123 | if( !sel ) return "";
124 | var templateDom = dom.querySelector( noResSelector );
125 | return templateDom ? templateDom.innerHTML : "";
126 | } )( noResSelector )
127 |
128 | var hitTemplateSelector = d.dataset.hitTemplate;
129 | if( !hitTemplateSelector ) throw new Error(
130 | "hit-template is a mandatory field of the results-items component and should be a selector to a valid template" );
131 | var hitTemplate = dom.querySelector( hitTemplateSelector );
132 | if( !hitTemplate ) throw new Error( "hit-template should be a selector to a valid template" );
133 |
134 | return {
135 | node : d,
136 | hitsPerPage : ( d.dataset && d.dataset.hitsPerPage ) || 12,
137 | hitTemplate : hitTemplate.innerHTML,
138 | noResultsTemplate : noResTmplate
139 | };
140 | } )( dom.querySelector( ".algolia-magic.result-items" ) );
141 |
142 | containersConfig.pagination = ( function domToPagination( d ) {
143 | if( d === null ) return undefined;
144 |
145 | var labels = ( function() {
146 | var next = d.dataset.labelNext;
147 | var previous = d.dataset.labelPrevious;
148 | var last = d.dataset.labelLast;
149 | var first = d.dataset.labelFirst;
150 |
151 | if( next || previous || last || first ) {
152 | return {
153 | next : next, previous : previous, last : last, first : first
154 | };
155 | }
156 |
157 | return undefined;
158 | } )();
159 |
160 | return {
161 | node : d,
162 | padding : d.dataset.padding || 2,
163 | labels : labels
164 | };
165 | } )( dom.querySelector( ".algolia-magic.pagination" ) );
166 |
167 | containersConfig.statistics = ( function domToStats( d ) {
168 | if( d === null ) return undefined;
169 |
170 | return {
171 | node : d,
172 | template : dom.querySelector( d.dataset.template ).innerHTML
173 | };
174 | } )( dom.querySelector( ".algolia-magic.statistics" ) );
175 |
176 | return containersConfig;
177 | }
178 | };
179 |
--------------------------------------------------------------------------------
/src/setup/url.js:
--------------------------------------------------------------------------------
1 | var qs = require( "qs" );
2 |
3 | var defaults = require( "lodash/object/defaults" );
4 | var pick = require( "lodash/object/pick" );
5 | var keys = require( "lodash/object/keys" );
6 | var isEqual = require( "lodash/lang/isEqual" );
7 | var mapValues = require( "lodash/object/mapValues" );
8 |
9 | var SearchParameters = require( "algoliasearch-helper/src/SearchParameters" );
10 |
11 | var timerMaker = function( t0 ){
12 | var t = t0;
13 | return function(){
14 | var now = Date.now();
15 | var delta = now - t;
16 | t = now;
17 | return delta;
18 | };
19 | };
20 |
21 | var URLManager = function( urlStateDefaults, stateDefaults, threshold, timer ){
22 | this.urlStateDefaults = urlStateDefaults || {};
23 | this.stateDefaults = stateDefaults || {};
24 | this.threshold = threshold || 1000;
25 | this.timer = timer || timerMaker( Date.now() );
26 | this.urlStateKeys = keys( urlStateDefaults );
27 | }
28 |
29 | URLManager.prototype.init = function() {
30 | // Read the URL
31 | var urlParams = pick( this.read(), keys( this.urlStateDefaults ) );
32 |
33 | // Create an updated state with the parameters given by the user
34 | var updatedURLState = defaults( urlParams, this.urlStateDefaults );
35 | var updatedStateDef = defaults( {}, updatedURLState, this.stateDefaults );
36 | var searchState = new SearchParameters( updatedStateDef );
37 |
38 | // Update the url with the computed state
39 | window.history.replaceState( searchState, "", "?" + qs.stringify( updatedURLState ) )
40 |
41 | // Return the new state
42 | return searchState;
43 | };
44 |
45 | URLManager.prototype.update = function( state ) {
46 | var newUrlState = pick( state, this.urlStateKeys );
47 | var newUrlStateStringified = qs.stringify( newUrlState );
48 | var currentUrlSearch = window.location.search.slice( 1 );
49 |
50 | if( newUrlStateStringified === currentUrlSearch ) {
51 | return;
52 | }
53 |
54 | if( this.timer() < this.threshold ) {
55 | window.history.replaceState( state, "", "?" + newUrlStateStringified );
56 | }
57 | else {
58 | window.history.pushState( state, "", "?" + newUrlStateStringified );
59 | }
60 | };
61 |
62 | URLManager.prototype.read = function(){
63 | var search = window.location.search.slice( 1 );
64 | var parameters = qs.parse( search );
65 | return parameters;
66 | };
67 |
68 | module.exports = URLManager;
69 |
--------------------------------------------------------------------------------
/todo.md:
--------------------------------------------------------------------------------
1 | Algolia Magic Kite
2 | ==================
3 |
4 | - [x] Template in hits
5 | - [x] Pagination component
6 | - [x] SearchBox placeHolder
7 | - [x] Change dataset
8 | - [x] Disjunctive facets
9 | - [x] Slider
10 | - [ ] Configuration of the search
11 | - [ ] Refactor component definition
12 | - [ ] Multi index selection
13 | - [x] Sort on facet list
14 | - [ ] History / URL
15 |
--------------------------------------------------------------------------------