├── .gitignore ├── Dockerfile ├── angular-cli.json ├── buildContainer.sh ├── documentation └── try-cb-api-spec-v2.adoc ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── readme.md ├── src ├── app │ ├── +cart │ │ ├── cart.component.html │ │ ├── cart.component.ts │ │ ├── index.ts │ │ └── shared │ │ │ └── index.ts │ ├── +home │ │ ├── home.component.html │ │ ├── home.component.ts │ │ ├── index.ts │ │ └── shared │ │ │ └── index.ts │ ├── +hotels │ │ ├── hotels.component.html │ │ ├── hotels.component.ts │ │ ├── index.ts │ │ └── shared │ │ │ └── index.ts │ ├── +login │ │ ├── index.ts │ │ ├── login.component.html │ │ ├── login.component.ts │ │ └── shared │ │ │ └── index.ts │ ├── +user │ │ ├── index.ts │ │ ├── shared │ │ │ └── index.ts │ │ ├── user.component.html │ │ └── user.component.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.guards.auth.ts │ ├── app.guards.notlogged.ts │ ├── app.module.ts │ ├── app.routes.ts │ ├── index.ts │ └── shared │ │ ├── angular2-jwt.ts │ │ ├── auth.service.ts │ │ ├── index.ts │ │ ├── interfaces.ts │ │ ├── md5.ts │ │ ├── narration.component.css │ │ ├── narration.component.html │ │ ├── narration.component.ts │ │ ├── narration.service.ts │ │ ├── navbar.component.html │ │ ├── navbar.component.ts │ │ └── utility.service.ts ├── assets │ ├── .npmignore │ ├── CBTravel.LOGO.png │ └── poweredBy.01.png ├── environments │ ├── environment.prod.ts │ ├── environment.test.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.json └── typings.d.ts └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | *.launch 16 | .settings/ 17 | 18 | # misc 19 | /.sass-cache 20 | /connect.lock 21 | /coverage/* 22 | /libpeerconnection.log 23 | npm-debug.log 24 | testem.log 25 | /typings 26 | 27 | # e2e 28 | /e2e/*.js 29 | /e2e/*.map 30 | 31 | #System Files 32 | .DS_Store 33 | Thumbs.db 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | COPY dist/ /usr/share/nginx/html 3 | -------------------------------------------------------------------------------- /angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "version": "1.0.0-beta.14", 4 | "name": "try-cb" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": "assets", 11 | "index": "index.html", 12 | "main": "main.ts", 13 | "test": "test.ts", 14 | "tsconfig": "tsconfig.json", 15 | "prefix": "app", 16 | "mobile": false, 17 | "styles": [ 18 | "styles.css", 19 | "../node_modules/bootstrap/dist/css/bootstrap.css" 20 | ], 21 | "scripts": [ 22 | "../node_modules/jquery/dist/jquery.js", 23 | "../node_modules/bootstrap/dist/js/bootstrap.js" 24 | ], 25 | "environments": { 26 | "source": "environments/environment.ts", 27 | "dev": "environments/environment.ts", 28 | "prod": "environments/environment.prod.ts", 29 | "test": "environments/environment.test.ts" 30 | } 31 | } 32 | ], 33 | "addons": [], 34 | "packages": [], 35 | "e2e": { 36 | "protractor": { 37 | "config": "./protractor.conf.js" 38 | } 39 | }, 40 | "test": { 41 | "karma": { 42 | "config": "./karma.conf.js" 43 | } 44 | }, 45 | "defaults": { 46 | "styleExt": "css", 47 | "prefixInterfaces": false 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /buildContainer.sh: -------------------------------------------------------------------------------- 1 | docker run --rm -it -v $(pwd)/:/project -u $(id -u):$(id -g) metal3d/ng build --environment=test 2 | docker build -t trycb/front . 3 | -------------------------------------------------------------------------------- /documentation/try-cb-api-spec-v2.adoc: -------------------------------------------------------------------------------- 1 | = Try-Cb Sample App v2 REST API specification 2 | :toc: left 3 | :sectnums: 4 | 5 | == Overall Design 6 | The v2 of the API is designed around the following guidelines: 7 | 8 | - the return type is `application/json` (unless otherwise noted). 9 | - in case of success, the response body structure is the `Result` structure described below. The `data` entry contains JSON data to be directly displayed to the user. The `context` array contains values like N1QL queries that can be displayed in a console-like panel in the frontend ("narration"). 10 | - in case of application errors, the response body structure is the `Error` structure described below. It only contains a `failure` entry, a message that can be displayed to the user. 11 | 12 | === Data Models 13 | 14 | .Result JSON structure 15 | [source,json] 16 | ---- 17 | { 18 | "data": "any JSON object/array/value", 19 | "context": [ "an array of strings", "can be omitted" ] 20 | } 21 | ---- 22 | 23 | .Error JSON structure 24 | [source,json] 25 | ---- 26 | { 27 | "failure": "a single string message displayable to the user" 28 | } 29 | ---- 30 | 31 | .Flight (when booking) 32 | [source,json] 33 | ---- 34 | { 35 | "name": "Fake Flight", 36 | "fligh": "AF885", 37 | "date": "6/23/2016 16:20:00", 38 | "price": 200, 39 | "sourceairport": "CDG", 40 | "destinationairport": "SFO", 41 | "bookedon": null 42 | } 43 | ---- 44 | Note: the `bookedon` field should be set by the backend, eg `try-cb-java` for Java. `date` time part is in utc. The `price` is considered to be expressed in dollars. 45 | 46 | === What to store in Couchbase 47 | 48 | .User object stored in Couchbase 49 | [source,json] 50 | ---- 51 | { 52 | "name": "username", 53 | "password": "password hash", 54 | "flights": [ 55 | { 56 | "name": "Fake Flight", 57 | "fligh": "AF885", 58 | "date": "6/23/2016 16:20:00", 59 | "price": 200, 60 | "sourceairport": "CDG", 61 | "destinationairport": "SFO", 62 | "bookedon": "try-cb-xxx" 63 | } 64 | ] 65 | } 66 | ---- 67 | 68 | The flights is only initialized once user starts making bookings. 69 | 70 | === What is stored in Browser's LocalStorage 71 | 72 | 1. A `user` entry with the connected user's name. 73 | 2. A `token` entry with the token (usually a JWT token, but can be a simpler base64 encoded user name if JWT is disabled). 74 | 3. A `cart` entry with an array of flights (collected and ready for later booking). 75 | 76 | == Airport API 77 | 78 | === findAll 79 | 80 | [source,javascript] 81 | ---- 82 | GET /api/airports?search=xxx 83 | ---- 84 | 85 | ==== Parameters 86 | Url parameters. 87 | 88 | |=== 89 | |Param |Description 90 | 91 | |`search` 92 | |The search parameter drives the airport search according to the value length and case. 93 | 94 | If `search` length = 3: `faa` code (search in uppercase) <1> 95 | 96 | If `search` length = 4 and fully uppercase / fully lowercase: `icao` code (search in uppercase) <2> 97 | 98 | Otherwise: `airportname` search using LIKE. <3> 99 | 100 | See each corresponding query below. 101 | |=== 102 | 103 | ==== Execution 104 | 105 | .(1) 106 | [source,sql] 107 | ---- 108 | SELECT airportname from `travel-sample` WHERE faa = UPPER('XXX'); 109 | ---- 110 | 111 | .(2) 112 | [source,sql] 113 | ---- 114 | SELECT airportname from `travel-sample` WHERE icao = UPPER('YYYY'); 115 | ---- 116 | 117 | .(3) 118 | [source,sql] 119 | ---- 120 | SELECT airportname from `travel-sample` WHERE LOWER(airportname) LIKE LOWER('%ZZZZZ%'); 121 | ---- 122 | 123 | ==== Return Codes and Values 124 | `200`: Successfully searched airports (`application/json`) 125 | 126 | .200 body 127 | [source,json] 128 | ---- 129 | { 130 | "data": [ 131 | { "airportname": "Some Airport Name" }, 132 | { "airportname": "Another Matching Airport" } 133 | ], 134 | "context": [ 135 | "THE N1QL QUERY THAT WAS EXECUTED" 136 | ] 137 | } 138 | ---- 139 | 140 | `500`: Something went wrong. 141 | 142 | .500 body 143 | [source,json] 144 | ---- 145 | { 146 | "failure": "message from the service/exception" 147 | } 148 | ---- 149 | 150 | 151 | 152 | == FlightPath API 153 | === findAll 154 | 155 | [source,javascript] 156 | ---- 157 | GET /api/flightPaths/{from}/{to}?leave=mm/dd/YYYY 158 | ---- 159 | 160 | ==== Parameters 161 | Path Variables & Url Parameters 162 | 163 | |=== 164 | |Param |Description 165 | 166 | |`from` 167 | |Path variable. The *airport name* for the beginning of the route. 168 | 169 | |`to` 170 | |Path variable. The *airport name* for the end of the route. 171 | 172 | |`leave` 173 | |A `dd/mm/YYYY` formatted date for the trip, as an url query parameter. 174 | |=== 175 | 176 | ==== Execution 177 | `{from}`, `{to}`, `{fromAirport}` and `{toAirport}` are to be replaced by their corresponding values from the query parameters / code: 178 | 179 | The first query extracts airport faa codes to use in a second query: 180 | 181 | .Extract faa codes 182 | [source,sql] 183 | ---- 184 | #Retrieve and extract the fromAirport and toAirport faa codes 185 | SELECT faa AS fromAirport 186 | FROM `travel-sample` 187 | WHERE airportname = '{from}' 188 | UNION 189 | SELECT faa AS toAirport 190 | FROM `travel-sample` 191 | WHERE airportname = '{to}'; 192 | ---- 193 | 194 | These faa codes are then used to find a route. Note that the `leave` date is only used to get a day of the week `dayOfWeek`, an int between `1` (Sunday) and `7` (Saturday). 195 | 196 | .Use faa codes to find routes 197 | [source,sql] 198 | ---- 199 | SELECT a.name, s.flight, s.utc, r.sourceairport, r.destinationairport, r.equipment 200 | FROM `travel-sample` AS r 201 | UNNEST r.schedule AS s 202 | JOIN `travel-sample` AS a ON KEYS r.airlineid 203 | WHERE r.sourceairport = '{fromAirport}' 204 | AND r.destinationairport = '{toAirport}' 205 | AND s.day = {dayOfWeek} 206 | ORDER BY a.name ASC; 207 | ---- 208 | 209 | The returned `data` payload corresponds to the rows of the second query (name, flight, utc, sourceairport, destinationairport, equipment) with an additional `price` column. 210 | 211 | The `price` can be generated randomly, or with a more consistent algorithm. 212 | 213 | WARNING: The NodeJS backend estimates the flight time and computes the `price` accordingly, enriching the response with an id, a price and a flight time: 214 | 215 | .NodeJS computing distance, flight time and price 216 | [source,javascript] 217 | ---- 218 | for (i = 0; i < res.length; i++) { 219 | if (res[i].toAirport) { 220 | queryTo = res[i].toAirport; 221 | geoEnd = {longitude: res[i].geo.lon, latitude: res[i].geo.lat}; 222 | } 223 | if (res[i].fromAirport) { 224 | queryFrom = res[i].fromAirport; 225 | geoStart = {longitude: res[i].geo.lon, latitude: res[i].geo.lat}; 226 | } 227 | } 228 | 229 | distance = haversine(geoStart, geoEnd); 230 | flightTime = Math.round(distance / config.application.avgKmHr); 231 | price = Math.round(distance * config.application.distanceCostMultiplier); 232 | 233 | queryPrep = "SELECT r.id, a.name, s.flight, s.utc, r.sourceairport, r.destinationairport, r.equipment " + 234 | "FROM `" + config.couchbase.bucket + "` r UNNEST r.schedule s JOIN `" + 235 | config.couchbase.bucket + "` a ON KEYS r.airlineid WHERE r.sourceairport='" + queryFrom + 236 | "' AND r.destinationairport='" + queryTo + "' AND s.day=" + convDate(leave) + " ORDER BY a.name"; 237 | 238 | //... 239 | //for each result 240 | var resCount = flightPaths.length; 241 | for (r = 0; r < flightPaths.length; r++) { 242 | resCount--; 243 | flightPaths[r].flighttime = flightTime; 244 | flightPaths[r].price = Math.round(price * ((100 - (Math.floor(Math.random() * (20) + 1))) / 100)); 245 | ---- 246 | 247 | ==== Return Codes and Values 248 | `200`: Successfully searched a route. 249 | 250 | .200 body 251 | [source,json] 252 | ---- 253 | { 254 | "data": [ 255 | { 256 | "name": "Air France", 257 | "flight": "AF479", 258 | "equipment": "388", 259 | "utc": "11:10:00", 260 | "sourceairport": "SFO", 261 | "destinationairport": "CDG", 262 | "price": 902 263 | }, 264 | { 265 | "name": "Delta Air Lines", 266 | "flight": "DL783", 267 | "equipment": "388", 268 | "utc": "13:33:00", 269 | "sourceairport": "SFO", 270 | "destinationairport": "CDG", 271 | "price": 1025 272 | } 273 | ], 274 | "context": [ 275 | "1st N1QL query", 276 | "2nd N1QL query" 277 | ] 278 | } 279 | ---- 280 | 281 | `500`: Something went wrong. 282 | 283 | .500 body 284 | [source,json] 285 | ---- 286 | { 287 | "failure": "message from the service/exception, eg. 'bad request'" 288 | } 289 | ---- 290 | 291 | 292 | 293 | == User API 294 | === login 295 | 296 | [source,javascript] 297 | ---- 298 | POST /api/user/login 299 | ---- 300 | 301 | ==== Parameters 302 | Body (`application/json`) provided by the client: 303 | 304 | [source,json] 305 | ---- 306 | { 307 | "user": "{username}", 308 | "password": "{md5_password}" 309 | } 310 | ---- 311 | 312 | ==== Execution 313 | The code tries to authenticate the user by checking there is a `user::{username}` document in store, and that the password field matches. 314 | In case of success, if JWT is implemented a JWT token is constructed and returned. It will be required for further user-specific interactions: booking flights and retrieving list of bookings. 315 | 316 | IMPORTANT: The JWT content must include a `user` "claim" with the username as value. 317 | 318 | WARNING: If JWT (JSON Web Token) are not supported by a backend, the token must be the username, encoded in base64. The fronted will need to be configured accordingly, in order to correctly parse the simpler "token". 319 | 320 | ==== Return Codes and Values 321 | `200`: User was authenticated (`application/json`). Note these objects have no context. 322 | 323 | .200 payload 324 | [source,json] 325 | ---- 326 | { 327 | "data": { "token": "JWT Token in base64 form" } 328 | } 329 | ---- 330 | 331 | .200 alternative payload (if JWT not implemented) 332 | [source,json] 333 | ---- 334 | { 335 | "data": { "token": "BASE64-ENCODED-USERNAME" } 336 | } 337 | ---- 338 | 339 | `401`: Authentication failure. 340 | 341 | === create user 342 | 343 | [source,javascript] 344 | ---- 345 | POST /api/user/signup 346 | ---- 347 | 348 | ==== Parameters 349 | Body (`application/json`) provided by the client: 350 | 351 | [source,json] 352 | ---- 353 | { 354 | "user": "{user}", 355 | "password": "{md5_password}" 356 | } 357 | ---- 358 | 359 | ==== Execution 360 | A document with a key of `user::USERNAME` is created. If it already exists, 409 error is triggered. 361 | 362 | **Note**: The name and destination bucket can vary and interoperability between SDKs is not required. For instance, the NodeJS backend uses `Ottoman`, which implies its own key pattern, and in the Java SDK the bucket can be configured ("default" bucket is otherwise used) and the document can optionally be created with an expiry (configurable as well). 363 | 364 | The content of the document is an object with at least the following structure: 365 | 366 | [source,json] 367 | ---- 368 | { 369 | "name": "username", 370 | "password": "password hash" 371 | } 372 | ---- 373 | 374 | Additionally, a JSON Web Token (JWT) is created and returned to the client. 375 | 376 | IMPORTANT: The JWT content must include a `user` "claim" with the username as value. 377 | 378 | WARNING: If JWT (JSON Web Token) are not supported by a backend, the token must be the username, encoded in base64. The fronted will need to be configured accordingly, in order to correctly parse the simpler "token". 379 | 380 | ==== Return Codes and Values 381 | `202`: The user was created successfully. "data" contains a generated JWT token under the `token` entry: 382 | 383 | .202 body 384 | [source,json] 385 | ---- 386 | { 387 | "data": { "token": "JWT token" }, 388 | "context": [ "message indicating what document was created (key, bucket, expiry)" ] 389 | } 390 | ---- 391 | 392 | `409`: The user already exists. 393 | 394 | === Get user cart 395 | 396 | [source,javascript] 397 | ---- 398 | GET /api/user/{username}/flights 399 | ---- 400 | 401 | ==== Parameters 402 | Url parameters. 403 | 404 | |=== 405 | |Param 406 | |Description 407 | 408 | |`username` 409 | |the username for which to display booked flights. 410 | |=== 411 | 412 | ==== Authentication 413 | This operation is subject to authentication. To that end, the JWT token that was returned by the server when logging in / signing up is to be passed through the `Authorization` header, prefixed with `"Bearer "`. 414 | 415 | This will be used by the backend to verify the `user` claim inside the token matches the `username` in the URL. 416 | 417 | ==== Execution 418 | If there is a JWT token, it is verified and an username extracted. Otherwise the username is directly provided, base64 encoded. 419 | 420 | The code checks for a document stored for that particular user (eg. a document with a key of `user::USERNAME`) . If it doesn't exist, 401 is returned. 421 | 422 | Inside the document is an array of `flights`, that is returned as this response's "data". 423 | 424 | ==== Return Codes and Values 425 | `200`: The booked flights were found. Booked flights that are stored in db for the user are returned in "data" 426 | 427 | .200 body 428 | [source,json] 429 | ---- 430 | { 431 | "data": [ 432 | { 433 | "name": "string", 434 | "flight": "string", 435 | "price": "integer", 436 | "date": "string", 437 | "sourceairport":"string", 438 | "destinationairport":"string", 439 | "bookedon":"string" 440 | } 441 | ] 442 | } 443 | ---- 444 | 445 | `401`: If no token/username was provided 446 | 447 | `403`: If the token/username couldn't be verified, or document stored that correspond to this user. 448 | 449 | 450 | === book a flight 451 | 452 | [source,javascript] 453 | ---- 454 | POST /api/user/{username}/flights 455 | ---- 456 | 457 | ==== Parameters 458 | Url Parameters: 459 | 460 | |=== 461 | |Param 462 | |Description 463 | 464 | |`username` 465 | |the username for which to display booked flights. 466 | |=== 467 | 468 | Body payload (`application/json`): 469 | 470 | [source,json] 471 | ---- 472 | { 473 | "flights": [ 474 | { 475 | "name": "string", 476 | "flight": "string", 477 | "price": "integer", 478 | "date": "string", 479 | "sourceairport": "string", 480 | "destinationairport": "string" 481 | } 482 | ] 483 | } 484 | ---- 485 | 486 | `name` is the airline's name, while `flight` is the flight's code. `date` is a concatenation of the departure date (in mm/dd/yyyy format) and the flight's departure time in UTC, and the airport entries contain airport codes. 487 | 488 | WARNING: The `flights` entries are not directly the same as what is returned during a flight search. Instead of `utc` (the route's departure time), a `date` entry should be constructed, concatenating the leg's date, a space and the `utc` field from the initial response. 489 | 490 | ==== Authentication 491 | This operation is subject to authentication. To that end, the JWT token that was returned by the server when logging in / signing up is to be passed through the `Authorization` header, prefixed with `"Bearer "`. 492 | 493 | This will be used by the backend to verify the `user` claim inside the token matches the `username` in the URL. 494 | 495 | ==== Execution 496 | The code just check for existence of the user document by using the 497 | username from the token (as `user` claim for JWT, or simply base64 decoded otherwise). Key should be in the form 498 | `user::USERNAME` for most SDKs. 499 | 500 | Inside the document, a `flights` array is added or appended with each 501 | element of the `flights` array in the payload. Each flight element has 502 | its `bookedon` field set to a string specific to the used backend (eg. 503 | "`try-cb-java`" for the Java backend). 504 | 505 | ==== Return Codes and Values 506 | `202`: Flights booking were added. Data is each flight booking that was 507 | added. 508 | 509 | .202 body (added flights) 510 | [source,json] 511 | ---- 512 | { 513 | "data": { 514 | "added": [ 515 | { 516 | "name": "string", 517 | "flight": "string", 518 | "price": "integer", 519 | "date": "string", 520 | "sourceairport": "string", 521 | "destinationairport": "string", 522 | "bookedon": "string" 523 | } 524 | ] 525 | }, 526 | "context": [ "message indicating in which document key the flight was added"] 527 | } 528 | ---- 529 | 530 | `401`: There is no token/username. 531 | 532 | `403`: The token or username can't be verified / doesn't have a corresponding document stored. 533 | 534 | `400`: the payload doesn't have a flights array or it is malformed. 535 | 536 | == Hotels API (new) 537 | 538 | === The Hotel FTS index 539 | The FTS index that indexes hotels is named `hotels`. It has the following mappings (at a minimum): 540 | 541 | - name 542 | - description 543 | - city 544 | - country 545 | - address 546 | - price 547 | 548 | === Find hotel POIs 549 | 550 | [source,javascript] 551 | ---- 552 | GET /api/hotels/{description}/{location} 553 | ---- 554 | 555 | ==== Parameters 556 | URL path parameters. 557 | 558 | |=== 559 | |URL part |Description 560 | 561 | |`description` 562 | |First variable in the URL path, the description is a keyword that will be searched for in the content and name fields of the hotels. Special value "`*`" will deactivate this criteria. 563 | 564 | |`location` 565 | |Second variable in the URL path, the location is a keyword that will be searched for in all the address-related fields of the hotels. Special value "`*`" will deactivate this criteria (and if both are deactivated, all hotels are searched for). 566 | |=== 567 | 568 | ==== Execution 569 | First one of three FTS queries is executed (depending on the description and location criterias): 570 | 571 | .Without location nor description 572 | [source,json] 573 | ---- 574 | { 575 | "size":100, 576 | "query": { 577 | "field":"type", 578 | "term":"hotel" 579 | } 580 | } 581 | ---- 582 | 583 | .Adding description criteria 584 | [source,json] 585 | ---- 586 | { 587 | "size":100, 588 | "query": { 589 | "conjuncts": [ 590 | {"field":"type","term":"hotel"}, 591 | {"disjuncts":[ 592 | {"field":"name","match_phrase":"foo"}, 593 | {"field":"description","match_phrase":"foo"} 594 | ]} 595 | ] 596 | } 597 | } 598 | ---- 599 | 600 | .Adding also location criteria 601 | [source,json] 602 | ---- 603 | { 604 | "size":100, 605 | "query": { 606 | "conjuncts": [ 607 | {"field":"type","term":"hotel"}, 608 | {"disjuncts":[ 609 | {"field":"name","match_phrase":"foo"}, 610 | {"field":"description","match_phrase":"foo"} 611 | ]}, 612 | 613 | {"disjuncts":[ 614 | {"field":"country","match_phrase":"France"}, 615 | {"field":"city","match_phrase":"France"}, 616 | {"field":"state","match_phrase":"France"}, 617 | {"field":"address","match_phrase":"France"} 618 | ]} 619 | ] 620 | } 621 | } 622 | ---- 623 | 624 | Then use subdoc to retrieve complete data of each relevant field (even if they are not stored by FTS index): 625 | 626 | .subdoc retrieval of stored data in Java 627 | [source,java] 628 | ---- 629 | for (SearchQueryRow row : result) { 630 | DocumentFragment fragment = bucket 631 | .lookupIn(row.id()) 632 | .get("country") 633 | .get("city") 634 | .get("state") 635 | .get("address") 636 | .get("name") 637 | .get("description") 638 | .execute(); 639 | 640 | Map map = new HashMap(); 641 | 642 | String country = (String) fragment.content("country"); 643 | String city = (String) fragment.content("city"); 644 | String state = (String) fragment.content("state"); 645 | String address = (String) fragment.content("address"); 646 | 647 | //... 648 | } 649 | ---- 650 | 651 | The country/city/state/address are aggregated into an "address". 652 | 653 | The final response payload for a given hotel has "name", "description" and "address" fields. 654 | 655 | ==== Return Codes and Values 656 | `200`: Hotel searched without failure. 657 | 658 | .200 body 659 | [source,json] 660 | ---- 661 | { 662 | "data": [ 663 | { 664 | "name": "nitenite", 665 | "address": "18 Holliday Street, Birmingham, United Kingdom", 666 | "description": "The property brands itself as a boutique hotel, where postmodern common space appointments are meant to make up for the ultrasmall (7 sqm) cabins that serve as ensuite rooms." 667 | } 668 | ], 669 | "context": [ 670 | "the FTS request executed (pretty printed if possible)", 671 | "a plain text snippet of subdoc specs (hard wrapped)" 672 | ] 673 | } 674 | ---- 675 | 676 | `500`: Something went wrong 677 | 678 | .500 body 679 | [source,json] 680 | ---- 681 | { 682 | "failure": "error message describing what went wrong" 683 | } 684 | ---- 685 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { TryCbPage } from './app.po'; 2 | 3 | describe('try-cb App', function() { 4 | let page: TryCbPage; 5 | 6 | beforeEach(() => { 7 | page = new TryCbPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('app works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor/globals'; 2 | 3 | export class TryCbPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "outDir": "../dist/out-tsc-e2e", 10 | "sourceMap": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "../node_modules/@types" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', 'angular-cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-remap-istanbul'), 12 | require('angular-cli/plugins/karma') 13 | ], 14 | files: [ 15 | { pattern: './src/test.ts', watched: false } 16 | ], 17 | preprocessors: { 18 | './src/test.ts': ['angular-cli'] 19 | }, 20 | remapIstanbulReporter: { 21 | reports: { 22 | html: 'coverage', 23 | lcovonly: './coverage/coverage.lcov' 24 | } 25 | }, 26 | angularCli: { 27 | config: './angular-cli.json', 28 | environment: 'dev' 29 | }, 30 | reporters: ['progress', 'karma-remap-istanbul'], 31 | port: 9876, 32 | colors: true, 33 | logLevel: config.LOG_INFO, 34 | autoWatch: true, 35 | browsers: ['Chrome'], 36 | singleRun: false 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "try-cb", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "angular-cli": {}, 6 | "scripts": { 7 | "start": "ng serve", 8 | "lint": "tslint \"src/**/*.ts\"", 9 | "test": "ng test", 10 | "pree2e": "webdriver-manager update", 11 | "e2e": "protractor" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/common": "2.0.0", 16 | "@angular/compiler": "2.0.0", 17 | "@angular/core": "2.0.0", 18 | "@angular/forms": "2.0.0", 19 | "@angular/http": "2.0.0", 20 | "@angular/platform-browser": "2.0.0", 21 | "@angular/platform-browser-dynamic": "2.0.0", 22 | "@angular/router": "3.0.0", 23 | "bootstrap": "3.3.7", 24 | "jquery": "3.1.0", 25 | "core-js": "2.4.1", 26 | "rxjs": "5.0.0-beta.12", 27 | "ts-helpers": "1.1.1", 28 | "zone.js": "0.6.23", 29 | "ng2-bootstrap": "1.1.5" 30 | }, 31 | "devDependencies": { 32 | "@types/jasmine": "2.2.30", 33 | "angular-cli": "1.0.0-beta.14", 34 | "codelyzer": "0.0.26", 35 | "jasmine-core": "2.4.1", 36 | "jasmine-spec-reporter": "2.5.0", 37 | "karma": "1.2.0", 38 | "karma-chrome-launcher": "2.0.0", 39 | "karma-cli": "1.0.1", 40 | "karma-jasmine": "1.0.2", 41 | "karma-remap-istanbul": "0.2.1", 42 | "protractor": "4.0.5", 43 | "ts-node": "1.2.1", 44 | "tslint": "3.13.0", 45 | "typescript": "2.0.2", 46 | "webpack": "2.1.0-beta.22" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/docs/referenceConf.js 3 | 4 | /*global jasmine */ 5 | var SpecReporter = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './e2e/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | useAllAngular2AppRoots: true, 24 | beforeLaunch: function() { 25 | require('ts-node').register({ 26 | project: 'e2e' 27 | }); 28 | }, 29 | onPrepare: function() { 30 | jasmine.getEnv().addReporter(new SpecReporter()); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | > :warning: *IMPORTANT* this repository is out of date. 2 | > From Couchbase Server 7.0, please see https://github.com/couchbaselabs/try-cb-frontend-v2 3 | 4 |
5 | 6 | Angular 2 Front End for CB Reference Application 7 | =============== 8 | 9 | Travel-Sample Application for CB 4.5 including support for new features : 10 | - CBFT 11 | - Subdoc 12 | - Array Indexing 13 | 14 | This application uses Angular 2.0 and typescript. The developer environment requires Angular-CLI to build. 15 | 16 | ## Versioning 17 | 18 | **This is version `2.0.1`** 19 | 20 | The aim is to build a distributable package that can be embedded in the various `try-cb` backend implementations. 21 | 22 | Each time a new stabilized version is made, the version minor or major number should be increased and the build should be copied over to all try-cb backends. 23 | 24 | The distribution's version number should be reflected in a tag and be also visible in the app's footer (see `app/app.component.html`). 25 | 26 | ## Steps to Build 27 | - [1] Install NPM 6.0.0 or later 28 | 29 | - [2] Clone this repo. 30 | 31 | - [3] Make sure you have `angular-cli` installed (**this was built using angular-cli `1.0.0-beta.14`**): 32 | 33 | ``` 34 | npm install -g angular-cli@1.0.0-beta.14 35 | ``` 36 | 37 | - [3] Install node dependencies, build and serve using angular-cli: 38 | 39 | ``` 40 | npm install 41 | ng serve 42 | ``` 43 | 44 | To build a distributable version that can be embedded in a try-cb backend implementation, you can also use production mode: 45 | 46 | ``` 47 | rm -rf dist/* 48 | ng build --prod 49 | ``` 50 | 51 | Then copy the content of `dist/` over to the backend application. 52 | 53 | **If you have globally installed an different angular-cli**: 54 | 55 | ``` 56 | npm uninstall -g angular-cli 57 | npm install -g angular-cli@1.0.0-beta.14 58 | ``` 59 | 60 | Note the `latest` version of anglular-cli doesn't work with the version that was used to build this example (1.0.0-beta14). 61 | -------------------------------------------------------------------------------- /src/app/+cart/cart.component.html: -------------------------------------------------------------------------------- 1 |
2 | 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 |
AirlineFlightDepartureFromToPriceReservation
{{row.name}}{{row.flight}}{{row.date}}{{row.sourceairport | uppercase}}{{row.destinationairport | uppercase}}${{row.price}} 27 | 28 | 29 |
33 |
34 |
Error: {{error | json}}
35 |
Added: {{added | json }}
36 | -------------------------------------------------------------------------------- /src/app/+cart/cart.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Response } from '@angular/http'; 3 | import { AuthService, IUser, UtilityService, NarrationService } from '../shared'; 4 | import { environment } from '../../environments/environment'; 5 | @Component({ 6 | selector: 'app-cart', 7 | templateUrl: 'cart.component.html' 8 | }) 9 | export class CartComponent implements OnInit { 10 | 11 | authService: AuthService; 12 | utility: UtilityService; 13 | narrationService: NarrationService; 14 | error: string; 15 | added: Array; 16 | cart: Array; 17 | 18 | constructor(authService: AuthService, utility: UtilityService, narrationService: NarrationService) { 19 | this.authService = authService; 20 | this.narrationService = narrationService; 21 | this.utility = utility; 22 | this.cart = utility.getCart(); 23 | } 24 | 25 | ngOnInit() {} 26 | 27 | createFakeBooking() { 28 | let fake = { 29 | "name": "Fake Flight", 30 | "date": "6/23/2016 11:11:11", 31 | "sourceairport": "CDG", 32 | "destinationairport": "SFO" 33 | }; 34 | this.cart = this.utility.getCart(); 35 | this.cart.push(fake); 36 | localStorage.setItem("cart", JSON.stringify(this.cart)); 37 | 38 | this.narrationService.addSeparator("CART"); 39 | this.narrationService.addPre("Added fake element to cart", "Nothing was transmitted to the server", JSON.stringify(fake)); 40 | } 41 | 42 | book(flight: any) { 43 | this.narrationService.addSeparator("CART: Booking"); 44 | let flights = { 45 | "flights": [ flight ] 46 | }; 47 | let user:string = this.utility.getUser(); 48 | let url:string = "/api/user/" + user + "/flights"; 49 | this.narrationService.addPre("Authenticated POST to " + url, "The payload was:", JSON.stringify(flights)); 50 | 51 | return this.utility.makePostRequest(url, [], flights, true).then((response: Response) => { 52 | let data = UtilityService.extractData(response); 53 | let narration = UtilityService.extractNarration(response); 54 | this.remove(flight); 55 | this.added = data.added; 56 | this.error = null; 57 | 58 | this.narrationService.add("Booking stored in the backend", narration); 59 | this.narrationService.add("SUCCESS", ""); 60 | }, (error) => { 61 | this.added = null; 62 | this.error = error; 63 | this.narrationService.addPre("ERROR", "", error); 64 | }); 65 | } 66 | 67 | remove(flight: any) { 68 | this.cart.splice(this.cart.indexOf(flight), 1); 69 | localStorage.setItem("cart", JSON.stringify(this.cart)); 70 | } 71 | 72 | clearCart() { 73 | this.cart = []; 74 | localStorage.setItem("cart", JSON.stringify(this.cart)); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/app/+cart/index.ts: -------------------------------------------------------------------------------- 1 | export { CartComponent } from './cart.component'; 2 | -------------------------------------------------------------------------------- /src/app/+cart/shared/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/try-cb-frontend/cc9a02c5fd0454933f2a11095203163de40d054b/src/app/+cart/shared/index.ts -------------------------------------------------------------------------------- /src/app/+home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Find a Flight 14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 |
25 |
26 | From 27 | 38 | To 39 | 50 |
51 |
52 | 53 |
54 |
55 | No Results Found 56 |
57 |
58 |
59 |
60 | 61 |
62 |
63 |
64 | Leave 65 | 68 | Return 69 | 71 |
72 |
73 |
74 |
75 |
76 | 79 |
80 |
81 |
82 |
83 | 86 |
87 |
88 |
89 |
90 | 91 |
92 |
93 |
94 |
95 |

Available Outbound Leg Flights

96 |
97 |
98 |

{{from}} -> {{to}}

99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 127 | 128 | 129 |
AirlineFlightDepartureFromToAircraftPriceReservation
{{row.name}}{{row.flight}}{{row.utc}}{{row.sourceairport | uppercase}}{{row.destinationairport | uppercase}}{{row.equipment}}${{row.price}} 122 | 125 | 126 |
130 |
131 | 133 |
134 |
135 |
136 |

Available Return Leg Flights

137 |
138 |
139 |

{{to}} -> {{from}}

140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 168 | 169 | 170 |
AirlineFlightDepartureFromToAircraftPriceReservation
{{row.name}}{{row.flight}}{{row.utc}}{{row.sourceairport | uppercase}}{{row.destinationairport | uppercase}}{{row.equipment}}${{row.price}} 163 | 166 | 167 |
171 |
172 | 174 |
175 |
176 |
177 | -------------------------------------------------------------------------------- /src/app/+home/home.component.ts: -------------------------------------------------------------------------------- 1 | import {Observable} from "rxjs/Observable"; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { Response } from "@angular/http"; 4 | import { UtilityService } from '../shared'; 5 | import { environment } from '../../environments/environment'; 6 | import { AuthService, NarrationService } from '../shared'; 7 | 8 | @Component({ 9 | selector: 'app-home', 10 | templateUrl: 'home.component.html' 11 | }) 12 | 13 | export class HomeComponent implements OnInit { 14 | public auth:AuthService; 15 | public utility:UtilityService; 16 | narrationService: NarrationService; 17 | 18 | public to:string = ''; 19 | public from:string = ''; 20 | public leaves:string = ''; 21 | public returns:string = null; 22 | public typeaheadLoading:boolean = false; 23 | public typeaheadNoResults:boolean = false; 24 | public outboundData: any[]; 25 | public inboundData: any[]; 26 | public choosen: any[]; 27 | 28 | constructor(auth:AuthService,utility:UtilityService, narrationService: NarrationService) { 29 | this.narrationService = narrationService; 30 | this.auth=auth; 31 | this.utility=utility; 32 | this.choosen=[]; 33 | } 34 | 35 | public getContext():any { 36 | return this; 37 | } 38 | 39 | public getToAirport(): Observable { 40 | return this.utility.makeGetRequestObs("/api/airports?search="+ this.to,[]) 41 | .map((result: Response) => { 42 | let data = UtilityService.extractData(result) as string[]; 43 | let narration = UtilityService.extractNarration(result); 44 | this.narrationService.addPre("N1QL typeahead for To = " + this.to, "The following N1QL query was executed on the server:" , narration[0]); 45 | return data; 46 | }) 47 | .timeout(500); 48 | } 49 | 50 | public getFromAirport():Observable { 51 | return this.utility.makeGetRequestObs("/api/airports?search="+ this.from,[]) 52 | .map((result: Response) => { 53 | let data = UtilityService.extractData(result) as string[]; 54 | let narration = UtilityService.extractNarration(result); 55 | this.narrationService.addPre("N1QL typeahead for From = " + this.from, "The following N1QL query was executed on the server:" , narration[0]); 56 | return data; 57 | }) 58 | .timeout(500); 59 | } 60 | 61 | public changeTypeaheadLoading(e:boolean):void { 62 | this.typeaheadLoading = e; 63 | } 64 | 65 | public changeTypeaheadNoResults(e:boolean):void { 66 | this.typeaheadNoResults = e; 67 | } 68 | 69 | public typeaheadOnSelect(e:any):void { 70 | console.log(`Selected value: ${e.item}`); 71 | } 72 | 73 | public findFlights(from:string, to:string, leaves:string, returns:string):void { 74 | let narrationUrl = "/api/flightPaths/" + from + "/" + to + "?leave=" + leaves; 75 | this.narrationService.addSeparator("HOME: Find Flight"); 76 | this.narrationService.add("Outbound leg GET to " + narrationUrl, ""); 77 | 78 | this.utility.makeGetRequestObs("/api/flightPaths",[from, to],"leave="+leaves) 79 | .map((response: Response) => response.json()) 80 | .subscribe( 81 | (val: any) => { 82 | this.outboundData = val.data; 83 | //we expect 2 context requests 84 | if (val.context.length == 2) { 85 | this.narrationService.addPre("Outbound leg N1QL query 1 executed in the backend", "The following N1QL query was executed in the backend:", val.context[0]); 86 | this.narrationService.addPre("Outbound leg N1QL query 2 executed in the backend", "The following N1QL query was executed in the backend:", val.context[1]); 87 | this.narrationService.add("Outbound leg SUCCESS", "Found " + val.data.length + " matching outbound flights"); 88 | } else { 89 | this.narrationService.fallbackPre(2, "Outbound leg SUCCESS (found " + val.data.length + " matching outbound flights)", val.context); 90 | } 91 | }, 92 | (error: any) => { 93 | this.outboundData = null; 94 | this.narrationService.addPre("Outbound leg ERROR", "finding outbound flights: ", JSON.stringify(error)); 95 | } 96 | ); 97 | 98 | if (returns) { 99 | let narrationUrl = "/api/flightPaths/" + to + "/" + from + "?leave=" + returns; 100 | this.narrationService.add("Return leg GET to " + narrationUrl, ""); 101 | 102 | this.utility.makeGetRequestObs("/api/flightPaths",[to, from],"leave=" + returns) 103 | .map((response: Response) => response.json()) 104 | .subscribe( 105 | (val: any) => { 106 | this.inboundData = val.data; 107 | //we expect 2 context requests 108 | if (val.context.length == 2) { 109 | this.narrationService.addPre("Return leg N1QL query 1 executed in the backend", "The following N1QL query was executed in the backend:", val.context[0]); 110 | this.narrationService.addPre("Return leg N1QL query 2 executed in the backend", "The following N1QL query was executed in the backend:", val.context[1]); 111 | this.narrationService.add("Return leg SUCCESS", "Found " + val.data.length + " matching return flights"); 112 | } else { 113 | this.narrationService.fallbackPre(2, "Return legSUCCESS (found " + val.data.length + " matching return flights)", val.context); 114 | } 115 | }, 116 | (error: any) => { 117 | this.inboundData = null; 118 | this.narrationService.addPre("Return leg ERROR", "finding return flights: ", JSON.stringify(error)); 119 | } 120 | ); 121 | } 122 | } 123 | 124 | public choose(row: any, date: string): void { 125 | this.choosen.push(row); 126 | let cart = this.utility.getCart(); 127 | cart.push({ 128 | "name": row.name, 129 | "flight": row.flight, 130 | "price": row.price, 131 | "date": date + " " + row.utc, 132 | "sourceairport": row.sourceairport, 133 | "destinationairport": row.destinationairport 134 | }); 135 | localStorage.setItem("cart", JSON.stringify(cart)); 136 | } 137 | 138 | public inCart(row: any): boolean { 139 | return this.choosen.indexOf(row) != -1; 140 | } 141 | 142 | ngOnInit() { 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/app/+home/index.ts: -------------------------------------------------------------------------------- 1 | export { HomeComponent } from './home.component'; 2 | -------------------------------------------------------------------------------- /src/app/+home/shared/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/try-cb-frontend/cc9a02c5fd0454933f2a11095203163de40d054b/src/app/+home/shared/index.ts -------------------------------------------------------------------------------- /src/app/+hotels/hotels.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Find Hotels

4 |
5 |
6 |
10 | 11 |
12 | 13 | 18 |
19 | 20 |
21 | 22 | 27 |
28 | 29 | 30 |
31 |
32 |
33 | 34 |
35 |
36 |

Matching Hotels

37 |
38 |
39 |
40 | No matching Hotels 41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
NameAddressDescription
{{line.name}}{{line.address}}{{line.description}}
58 |
59 | 63 |
64 | 65 |
66 |
There was an error (click for details) 67 |
68 | 69 |
{{error | json}}
70 |
-------------------------------------------------------------------------------- /src/app/+hotels/hotels.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy, Injectable, Inject } from '@angular/core'; 2 | import { FormBuilder, FormGroup } from '@angular/forms'; 3 | import { Response } from "@angular/http"; 4 | import { UtilityService } from "../shared/utility.service"; 5 | import { Narration, NarrationService } from "../shared/narration.service"; 6 | import { environment } from '../../environments/environment'; 7 | import { Observable } from 'rxjs/Rx'; 8 | 9 | @Component({ 10 | selector: 'app-hotels', 11 | templateUrl: 'hotels.component.html' 12 | }) 13 | 14 | @Injectable() 15 | 16 | export class HotelsComponent implements OnInit { 17 | 18 | hotelForm: FormGroup; 19 | utility: UtilityService; 20 | private _narrations: NarrationService; 21 | data; 22 | error; 23 | 24 | constructor(fb: FormBuilder, utility: UtilityService, narrationService: NarrationService) { 25 | this.hotelForm = fb.group({ 26 | 'description': [''], 27 | 'location': [''] 28 | }); 29 | this.utility = utility; 30 | this._narrations = narrationService; 31 | } 32 | 33 | ngOnInit() { } 34 | 35 | findHotels(value: any): void { 36 | var location = value.location; 37 | var description = value.description; 38 | 39 | var url = "/api/hotel/"; 40 | var hasDescription = (description != null && description != ""); 41 | var hasLocation = (location != null && location != ""); 42 | if (hasDescription && hasLocation) { 43 | url = url + description + "/" + location + "/"; 44 | } else if (hasLocation) { 45 | url = url + "*/" + location + "/"; 46 | } else if (hasDescription) { 47 | url = url + description + "/" 48 | } 49 | 50 | this._narrations.addSeparator("HOTEL: Find Hotels"); 51 | this._narrations.add("GET to " + url, ""); 52 | 53 | this.utility.makeGetRequestObs(url, []) 54 | .map((response: Response) => response.json()) 55 | .subscribe( 56 | (val: any) => { 57 | this.data = val.data; 58 | this.error = null; 59 | 60 | //we expect 2 context requests 61 | if (val.context.length == 2) { 62 | this._narrations.addPre("FTS query executed in the backend", "The following FTS query was executed in the backend:", val.context[0]); 63 | this._narrations.addPre("Subdocument query executed in the backend", "The following subdocument fetch was executed in the backend:", val.context[1]); 64 | this._narrations.add("SUCCESS", "Found " + this.data.length + " matching hotels"); 65 | } else { 66 | this._narrations.fallbackPre(2, "SUCCESS (found " + this.data.length + " matching hotels)", val.context); 67 | } 68 | 69 | 70 | }, 71 | (error: any) => { 72 | this.data = null; 73 | this.error = error; 74 | this._narrations.addPre("ERROR", "There was an error:", JSON.stringify(this.error, null, 2)); 75 | } 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/+hotels/index.ts: -------------------------------------------------------------------------------- 1 | export { HotelsComponent } from './hotels.component'; 2 | -------------------------------------------------------------------------------- /src/app/+hotels/shared/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/try-cb-frontend/cc9a02c5fd0454933f2a11095203163de40d054b/src/app/+hotels/shared/index.ts -------------------------------------------------------------------------------- /src/app/+login/index.ts: -------------------------------------------------------------------------------- 1 | export { LoginComponent } from './login.component'; 2 | -------------------------------------------------------------------------------- /src/app/+login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | 18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /src/app/+login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AuthService } from '../shared'; 3 | import { Router } from '@angular/router' 4 | 5 | @Component({ 6 | selector: 'app-login', 7 | templateUrl: 'login.component.html' 8 | }) 9 | export class LoginComponent implements OnInit { 10 | 11 | authService:AuthService; 12 | isNew:boolean; 13 | loginError:string; 14 | username:string; 15 | password:string; 16 | router:Router; 17 | 18 | constructor(authService:AuthService,router:Router) { 19 | this.authService=authService; 20 | this.isNew=true; 21 | this.router=router 22 | } 23 | 24 | login(email:string,password:string,isNew:boolean){ 25 | if(isNew){ 26 | this.authService.register(email,password).then((result) => { 27 | this.router.navigateByUrl("/home"); 28 | }, (error) => { 29 | this.loginError=error; 30 | this.password=null; 31 | }); 32 | }else{ 33 | this.authService.login(email, password).then((result) => { 34 | this.router.navigateByUrl("/home"); 35 | }, (error) => { 36 | this.loginError=error; 37 | this.username=null; 38 | this.password=null; 39 | }); 40 | } 41 | } 42 | 43 | ngOnInit() { 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/app/+login/shared/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/try-cb-frontend/cc9a02c5fd0454933f2a11095203163de40d054b/src/app/+login/shared/index.ts -------------------------------------------------------------------------------- /src/app/+user/index.ts: -------------------------------------------------------------------------------- 1 | export { UserComponent } from './user.component'; 2 | -------------------------------------------------------------------------------- /src/app/+user/shared/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/try-cb-frontend/cc9a02c5fd0454933f2a11095203163de40d054b/src/app/+user/shared/index.ts -------------------------------------------------------------------------------- /src/app/+user/user.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Flights booked for {{user}}:

5 |
6 |
7 |
8 |

{{user}}, you have no booked flights

9 |

10 |  search flights 11 | or 12 |  purchase flights in the cart 13 | . 14 |

15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
DescriptionDateFromToPriceBooked Via
{{b.name}} {{b.flight}}{{b.date}}{{b.sourceairport}}{{b.destinationairport}}${{b.price}}{{b.bookedon}}
38 |

Error:

{{error}}
39 |
40 | 41 |
42 | -------------------------------------------------------------------------------- /src/app/+user/user.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Injectable } from '@angular/core'; 2 | import { Response } from '@angular/http'; 3 | import { AuthService, IUser, UtilityService } from '../shared'; 4 | import { environment } from '../../environments/environment'; 5 | 6 | @Injectable() 7 | @Component({ 8 | selector: 'app-user', 9 | templateUrl: 'user.component.html' 10 | }) 11 | export class UserComponent implements OnInit { 12 | authService: AuthService; 13 | user: string; 14 | error: any; 15 | booked: Array; 16 | utility: UtilityService; 17 | 18 | constructor(authService: AuthService, utilityService: UtilityService) { 19 | this.authService = authService; 20 | this.utility = utilityService; 21 | } 22 | 23 | ngOnInit() { 24 | this.user = this.utility.getUser(); 25 | 26 | this.utility.makeGetRequestObs("/api/user", [this.user, "flights"], null, true) 27 | .map((response: Response) => response.json()) 28 | .subscribe( 29 | (val) => { 30 | this.booked = val.data; 31 | console.log("found booked flights: " + val.data); 32 | }, 33 | (error: any) => { 34 | this.booked = null; 35 | this.error = error; 36 | } 37 | ); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/try-cb-frontend/cc9a02c5fd0454933f2a11095203163de40d054b/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 | 23 |
24 |
25 |
26 | 27 |
28 | 32 |
33 |
34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { AuthService } from './shared'; 4 | import { Http, Request, RequestMethod, Headers } from "@angular/http"; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.css'] 10 | }) 11 | export class AppComponent { 12 | router: Router; 13 | authService: AuthService; 14 | constructor(router:Router,authService:AuthService){ 15 | this.authService=authService; 16 | this.router=router; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/app.guards.auth.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, Router } from '@angular/router'; 3 | import { AuthService } from './shared/auth.service'; 4 | 5 | @Injectable() 6 | export class OnlyLoggedInGuard implements CanActivate { 7 | 8 | constructor(private authService: AuthService, private router: Router) { } 9 | 10 | canActivate(): boolean { 11 | if (this.authService.isAuthenticated()) { 12 | return true; 13 | } else { 14 | this.router.navigateByUrl('/login'); 15 | return false; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/app/app.guards.notlogged.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, Router } from '@angular/router'; 3 | import { AuthService } from './shared/auth.service'; 4 | 5 | @Injectable() 6 | export class OnlyNotLoggedInGuard implements CanActivate { 7 | 8 | constructor(private authService: AuthService, private router: Router) { } 9 | 10 | canActivate(): boolean { 11 | if (!this.authService.isAuthenticated()) { 12 | return true; 13 | } else { 14 | this.router.navigateByUrl('/home'); 15 | return false; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { HttpModule } from '@angular/http'; 5 | 6 | import { TypeaheadModule } from 'ng2-bootstrap/ng2-bootstrap'; 7 | import { routing } from './app.routes'; 8 | 9 | import { AppComponent } from './app.component'; 10 | import { AuthService,UtilityService, NavbarComponent, NarrationComponent, Narration, NarrationService } from './shared'; 11 | import { OnlyLoggedInGuard } from './app.guards.auth.ts'; 12 | import { OnlyNotLoggedInGuard } from './app.guards.notlogged.ts'; 13 | import { HomeComponent } from './+home'; 14 | import { LoginComponent } from './+login'; 15 | import { UserComponent } from './+user'; 16 | import { CartComponent } from './+cart'; 17 | import { HotelsComponent } from './+hotels'; 18 | 19 | @NgModule({ 20 | declarations: [ 21 | AppComponent, 22 | NavbarComponent, 23 | NarrationComponent, 24 | HomeComponent, 25 | LoginComponent, 26 | UserComponent, 27 | CartComponent, 28 | HotelsComponent 29 | ], 30 | imports: [ 31 | routing, 32 | BrowserModule, 33 | FormsModule, 34 | ReactiveFormsModule, 35 | HttpModule, 36 | TypeaheadModule 37 | ], 38 | providers: [ 39 | AuthService, 40 | UtilityService, 41 | NarrationService, 42 | OnlyLoggedInGuard, 43 | OnlyNotLoggedInGuard 44 | ], 45 | bootstrap: [ AppComponent ] 46 | }) 47 | export class AppModule { } 48 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes, RouterModule } from '@angular/router'; 2 | 3 | import { HomeComponent } from './+home'; 4 | import { LoginComponent } from './+login'; 5 | import { UserComponent } from './+user'; 6 | import { CartComponent } from './+cart'; 7 | import { HotelsComponent } from './+hotels'; 8 | 9 | import { OnlyLoggedInGuard } from './app.guards.auth'; 10 | import { OnlyNotLoggedInGuard } from './app.guards.notlogged'; 11 | 12 | const routes: Routes = [ 13 | {path: '', component: HomeComponent, canActivate: [ OnlyLoggedInGuard]}, 14 | {path: 'home', component: HomeComponent, canActivate: [ OnlyLoggedInGuard]}, 15 | {path: 'login', component: LoginComponent, canActivate: [ OnlyNotLoggedInGuard ]}, 16 | {path: 'user', component: UserComponent, canActivate: [ OnlyLoggedInGuard]}, 17 | {path: 'cart', component: CartComponent, canActivate: [ OnlyLoggedInGuard]}, 18 | {path: 'hotels', component: HotelsComponent, canActivate: [ OnlyLoggedInGuard]} 19 | ]; 20 | 21 | export const routing = RouterModule.forRoot(routes); -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.component'; 2 | export * from './app.module'; 3 | -------------------------------------------------------------------------------- /src/app/shared/angular2-jwt.ts: -------------------------------------------------------------------------------- 1 | import { Injectable} from '@angular/core'; 2 | import {Http, Headers, Request, RequestOptions, RequestOptionsArgs, RequestMethod, Response} from '@angular/http'; 3 | import {Observable} from 'rxjs/Observable'; 4 | 5 | // Avoid TS error "cannot find name escape" 6 | declare var escape: any; 7 | 8 | export interface IAuthConfig { 9 | headerName: string; 10 | headerPrefix: string; 11 | tokenName: string; 12 | tokenGetter: any; 13 | noJwtError: boolean; 14 | globalHeaders: Array; 15 | noTokenScheme?:boolean; 16 | } 17 | 18 | /** 19 | * Sets up the authentication configuration. 20 | */ 21 | 22 | export class AuthConfig { 23 | 24 | headerName: string; 25 | headerPrefix: string; 26 | tokenName: string; 27 | tokenGetter: any; 28 | noJwtError: boolean; 29 | noTokenScheme: boolean; 30 | globalHeaders: Array; 31 | 32 | constructor(config:any={}) { 33 | this.headerName = config.headerName || 'Authorization'; 34 | if (config.headerPrefix) { 35 | this.headerPrefix = config.headerPrefix + ' '; 36 | } else if (config.noTokenScheme) { 37 | this.headerPrefix = ''; 38 | } else { 39 | this.headerPrefix = 'Bearer '; 40 | } 41 | this.tokenName = config.tokenName || 'id_token'; 42 | this.noJwtError = config.noJwtError || false; 43 | this.tokenGetter = config.tokenGetter || (() => localStorage.getItem(this.tokenName)); 44 | this.globalHeaders = config.globalHeaders || []; 45 | this.noTokenScheme=config.noTokenScheme||false; 46 | } 47 | 48 | getConfig():IAuthConfig { 49 | return { 50 | headerName: this.headerName, 51 | headerPrefix: this.headerPrefix, 52 | tokenName: this.tokenName, 53 | tokenGetter: this.tokenGetter, 54 | noJwtError: this.noJwtError, 55 | noTokenScheme:this.noTokenScheme, 56 | globalHeaders: this.globalHeaders 57 | } 58 | } 59 | 60 | } 61 | 62 | /** 63 | * Allows for explicit authenticated HTTP requests. 64 | */ 65 | 66 | @Injectable() 67 | export class AuthHttp { 68 | 69 | private _config: IAuthConfig; 70 | public tokenStream: Observable; 71 | 72 | constructor(options: AuthConfig, private http: Http) { 73 | this._config = options.getConfig(); 74 | 75 | this.tokenStream = new Observable((obs: any) => { 76 | obs.next(this._config.tokenGetter()); 77 | }); 78 | } 79 | 80 | setGlobalHeaders(headers: Array, request: Request | RequestOptionsArgs) { 81 | headers.forEach((header: Object) => { 82 | let key: string = Object.keys(header)[0]; 83 | let headerValue: string = (header)[key]; 84 | request.headers.set(key, headerValue); 85 | }); 86 | } 87 | 88 | request(url: string | Request, options?: RequestOptionsArgs) : Observable { 89 | 90 | let request: any; 91 | let globalHeaders = this._config.globalHeaders; 92 | 93 | if (!tokenNotExpired(null, this._config.tokenGetter())) { 94 | if (!this._config.noJwtError) { 95 | return new Observable((obs: any) => { 96 | obs.error(new Error('No JWT present')); 97 | }); 98 | } else { 99 | request = this.http.request(url, options); 100 | } 101 | 102 | } else if (typeof url === 'string') { 103 | let reqOpts: RequestOptionsArgs = options || {}; 104 | 105 | if (!reqOpts.headers) { 106 | reqOpts.headers = new Headers(); 107 | } 108 | 109 | if (globalHeaders) { 110 | this.setGlobalHeaders(globalHeaders, reqOpts); 111 | } 112 | 113 | reqOpts.headers.set(this._config.headerName, this._config.headerPrefix + this._config.tokenGetter()); 114 | request = this.http.request(url, reqOpts); 115 | 116 | } else { 117 | let req: Request = url; 118 | 119 | if (!req.headers) { 120 | req.headers = new Headers(); 121 | } 122 | 123 | if (globalHeaders) { 124 | this.setGlobalHeaders(globalHeaders, req); 125 | } 126 | 127 | req.headers.set(this._config.headerName, this._config.headerPrefix + this._config.tokenGetter()); 128 | request = this.http.request(req); 129 | } 130 | 131 | return request; 132 | } 133 | 134 | private requestHelper(requestArgs: RequestOptionsArgs, additionalOptions: RequestOptionsArgs) : Observable { 135 | let options: RequestOptions = new RequestOptions(requestArgs); 136 | 137 | if (additionalOptions) { 138 | options = options.merge(additionalOptions) 139 | } 140 | 141 | return this.request(new Request(options)) 142 | } 143 | 144 | get(url: string, options?: RequestOptionsArgs) : Observable { 145 | return this.requestHelper({ url: url, method: RequestMethod.Get }, options); 146 | } 147 | 148 | post(url: string, body: string, options?: RequestOptionsArgs) : Observable { 149 | return this.requestHelper({ url: url, body: body, method: RequestMethod.Post }, options); 150 | } 151 | 152 | put(url: string, body: string, options ?: RequestOptionsArgs) : Observable { 153 | return this.requestHelper({ url: url, body: body, method: RequestMethod.Put }, options); 154 | } 155 | 156 | delete(url: string, options ?: RequestOptionsArgs) : Observable { 157 | return this.requestHelper({ url: url, method: RequestMethod.Delete }, options); 158 | } 159 | 160 | patch(url: string, body:string, options?: RequestOptionsArgs) : Observable { 161 | return this.requestHelper({ url: url, body: body, method: RequestMethod.Patch }, options); 162 | } 163 | 164 | head(url: string, options?: RequestOptionsArgs) : Observable { 165 | return this.requestHelper({ url: url, method: RequestMethod.Head }, options); 166 | } 167 | 168 | } 169 | 170 | /** 171 | * Helper class to decode and find JWT expiration. 172 | */ 173 | @Injectable() 174 | export class JwtHelper { 175 | 176 | public urlBase64Decode(str:string) { 177 | var output = str.replace(/-/g, '+').replace(/_/g, '/'); 178 | switch (output.length % 4) { 179 | case 0: { break; } 180 | case 2: { output += '=='; break; } 181 | case 3: { output += '='; break; } 182 | default: { 183 | throw 'Illegal base64url string!'; 184 | } 185 | } 186 | 187 | return decodeURIComponent(escape(window.atob(output))); //polyfill https://github.com/davidchambers/Base64.js 188 | } 189 | 190 | public decodeToken(token:string) { 191 | console.log("DEBUG JWT:",token); 192 | var parts = token.split('.'); 193 | 194 | if (parts.length !== 3) { 195 | throw new Error('JWT must have 3 parts'); 196 | } 197 | 198 | var decoded = this.urlBase64Decode(parts[1]); 199 | if (!decoded) { 200 | throw new Error('Cannot decode the token'); 201 | } 202 | 203 | return JSON.parse(decoded); 204 | } 205 | 206 | public getTokenExpirationDate(token:string) { 207 | var decoded: any; 208 | decoded = this.decodeToken(token); 209 | 210 | if(typeof decoded.exp === "undefined") { 211 | return null; 212 | } 213 | 214 | var date = new Date(0); // The 0 here is the key, which sets the date to the epoch 215 | date.setUTCSeconds(decoded.exp); 216 | 217 | return date; 218 | } 219 | 220 | public isTokenExpired(token:string, offsetSeconds?:number) { 221 | var date = this.getTokenExpirationDate(token); 222 | offsetSeconds = offsetSeconds || 0; 223 | if (date === null) { 224 | return false; 225 | } 226 | 227 | // Token expired? 228 | return !(date.valueOf() > (new Date().valueOf() + (offsetSeconds * 1000))); 229 | } 230 | } 231 | 232 | /** 233 | * Checks for presence of token and that token hasn't expired. 234 | * For use with the @CanActivate router decorator and NgIf 235 | */ 236 | 237 | export function tokenNotExpired(tokenName = 'id_token', jwt?:string):boolean { 238 | 239 | const token:string = jwt || localStorage.getItem(tokenName); 240 | 241 | const jwtHelper = new JwtHelper(); 242 | 243 | return token && !jwtHelper.isTokenExpired(token, null); 244 | } 245 | -------------------------------------------------------------------------------- /src/app/shared/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from "@angular/core"; 2 | import { Response } from "@angular/http"; 3 | import { IUser,IToken } from "./interfaces"; 4 | import { UtilityService } from "./utility.service"; 5 | import { NarrationService } from "./narration.service"; 6 | import { environment } from '../../environments/environment'; 7 | import { md5 } from './md5'; 8 | import { JwtHelper } from './angular2-jwt'; 9 | import { Router } from '@angular/router' 10 | 11 | 12 | @Injectable() 13 | export class AuthService { 14 | 15 | utility: UtilityService; 16 | narrationService:NarrationService; 17 | jwt: JwtHelper; 18 | 19 | constructor(utility: UtilityService, narrationService:NarrationService) { 20 | this.utility = utility; 21 | this.narrationService = narrationService; 22 | this.jwt = new JwtHelper(); 23 | } 24 | 25 | isAuthenticated() { 26 | if (!localStorage.getItem("user") || localStorage.getItem("user") == "") { 27 | return false; 28 | } else { 29 | return true; 30 | } 31 | } 32 | 33 | login(email: string, password: string) { 34 | return new Promise((resolve, reject) => { 35 | this.utility.makePostRequest("/api/user/login", [], {"user": email, "password": md5(password)}).then((res: Response) => { 36 | let result = UtilityService.extractData(res); 37 | if (result.token && environment.jwtEnabled) { 38 | try { 39 | let decodedToken = this.jwt.decodeToken(result.token); 40 | localStorage.setItem("user", decodedToken.user); 41 | localStorage.setItem("token", result.token); 42 | resolve(); 43 | } catch (e) { 44 | reject("Backend created account but returned a malformed token: " + e); 45 | } 46 | } else if (result.token) { 47 | let user = this.jwt.urlBase64Decode(result.token); 48 | localStorage.setItem("user", user); 49 | localStorage.setItem("token", result.token); 50 | resolve(); 51 | } else { 52 | console.log("DEBUG: login failure, got " + JSON.stringify(result)); 53 | reject("No token found in login response"); 54 | } 55 | }, (error) => { 56 | reject(error); 57 | }); 58 | }); 59 | } 60 | 61 | register(email: string, password:string) { 62 | let cUser: IUser = { user: email, password: md5(password) }; 63 | return new Promise((resolve, reject) => { 64 | this.utility.makePostRequest("/api/user/signup", [], cUser).then((res: Response) => { 65 | let result = UtilityService.extractData(res); 66 | let narration = UtilityService.extractNarration(res); 67 | this.narrationService.addPre("Account Created", "The account for " + email + " was created on the server", narration[0]); 68 | if (environment.jwtEnabled && result && result.token) { 69 | try { 70 | let decodedToken = this.jwt.decodeToken(result.token); 71 | localStorage.setItem("user", decodedToken.user); 72 | localStorage.setItem("token", result.token); 73 | resolve(); 74 | } catch (e) { 75 | reject("Backend created account but returned a malformed token: " + e); 76 | } 77 | } else if (result && result.token) { 78 | let user = this.jwt.urlBase64Decode(result.token); 79 | localStorage.setItem("user", user); 80 | localStorage.setItem("token", result.token); 81 | resolve(); 82 | } else { 83 | console.log("DEBUG: registration failure, got " + JSON.stringify(result)); 84 | reject("Registration Failure"); 85 | } 86 | }, (error) => { 87 | reject(error); 88 | }); 89 | }); 90 | } 91 | 92 | deAuthenticate() { 93 | localStorage.clear(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthService } from './auth.service'; 2 | export { UtilityService } from './utility.service'; 3 | export { IUser } from './interfaces' 4 | export { NavbarComponent } from './navbar.component'; 5 | export { NarrationComponent } from './narration.component'; 6 | export { Narration, NarrationService } from './narration.service'; 7 | -------------------------------------------------------------------------------- /src/app/shared/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | _id?: string, 3 | user: string, 4 | password: string, 5 | token?: string, 6 | flights?: Array 7 | } 8 | 9 | export interface IToken { 10 | status: string 11 | token?: string 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/md5.ts: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/killmenot/webtoolkit.md5 2 | 3 | export let md5 = (string) => { 4 | 5 | function RotateLeft(lValue, iShiftBits) { 6 | return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)); 7 | } 8 | 9 | function AddUnsigned(lX, lY) { 10 | var lX4, lY4, lX8, lY8, lResult; 11 | lX8 = (lX & 0x80000000); 12 | lY8 = (lY & 0x80000000); 13 | lX4 = (lX & 0x40000000); 14 | lY4 = (lY & 0x40000000); 15 | lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF); 16 | if (lX4 & lY4) { 17 | return (lResult ^ 0x80000000 ^ lX8 ^ lY8); 18 | } 19 | if (lX4 | lY4) { 20 | if (lResult & 0x40000000) { 21 | return (lResult ^ 0xC0000000 ^ lX8 ^ lY8); 22 | } else { 23 | return (lResult ^ 0x40000000 ^ lX8 ^ lY8); 24 | } 25 | } else { 26 | return (lResult ^ lX8 ^ lY8); 27 | } 28 | } 29 | 30 | function F(x, y, z) { 31 | return (x & y) | ((~x) & z); 32 | } 33 | 34 | function G(x, y, z) { 35 | return (x & z) | (y & (~z)); 36 | } 37 | 38 | function H(x, y, z) { 39 | return (x ^ y ^ z); 40 | } 41 | 42 | function I(x, y, z) { 43 | return (y ^ (x | (~z))); 44 | } 45 | 46 | function FF(a, b, c, d, x, s, ac) { 47 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac)); 48 | return AddUnsigned(RotateLeft(a, s), b); 49 | }; 50 | 51 | function GG(a, b, c, d, x, s, ac) { 52 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac)); 53 | return AddUnsigned(RotateLeft(a, s), b); 54 | }; 55 | 56 | function HH(a, b, c, d, x, s, ac) { 57 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac)); 58 | return AddUnsigned(RotateLeft(a, s), b); 59 | }; 60 | 61 | function II(a, b, c, d, x, s, ac) { 62 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac)); 63 | return AddUnsigned(RotateLeft(a, s), b); 64 | }; 65 | 66 | function ConvertToWordArray(string) { 67 | var lWordCount; 68 | var lMessageLength = string.length; 69 | var lNumberOfWords_temp1 = lMessageLength + 8; 70 | var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64; 71 | var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16; 72 | var lWordArray = Array(lNumberOfWords - 1); 73 | var lBytePosition = 0; 74 | var lByteCount = 0; 75 | while (lByteCount < lMessageLength) { 76 | lWordCount = (lByteCount - (lByteCount % 4)) / 4; 77 | lBytePosition = (lByteCount % 4) * 8; 78 | lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount) << lBytePosition)); 79 | lByteCount++; 80 | } 81 | lWordCount = (lByteCount - (lByteCount % 4)) / 4; 82 | lBytePosition = (lByteCount % 4) * 8; 83 | lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition); 84 | lWordArray[lNumberOfWords - 2] = lMessageLength << 3; 85 | lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29; 86 | return lWordArray; 87 | }; 88 | 89 | function WordToHex(lValue) { 90 | var WordToHexValue = "", 91 | WordToHexValue_temp = "", 92 | lByte, lCount; 93 | for (lCount = 0; lCount <= 3; lCount++) { 94 | lByte = (lValue >>> (lCount * 8)) & 255; 95 | WordToHexValue_temp = "0" + lByte.toString(16); 96 | WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length - 2, 2); 97 | } 98 | return WordToHexValue; 99 | }; 100 | 101 | function Utf8Encode(string) { 102 | string = string.replace(/\r\n/g, "\n"); 103 | var utftext = ""; 104 | 105 | for (var n = 0; n < string.length; n++) { 106 | 107 | var c = string.charCodeAt(n); 108 | 109 | if (c < 128) { 110 | utftext += String.fromCharCode(c); 111 | } else if ((c > 127) && (c < 2048)) { 112 | utftext += String.fromCharCode((c >> 6) | 192); 113 | utftext += String.fromCharCode((c & 63) | 128); 114 | } else { 115 | utftext += String.fromCharCode((c >> 12) | 224); 116 | utftext += String.fromCharCode(((c >> 6) & 63) | 128); 117 | utftext += String.fromCharCode((c & 63) | 128); 118 | } 119 | 120 | } 121 | 122 | return utftext; 123 | }; 124 | 125 | var x = Array(); 126 | var k, AA, BB, CC, DD, a, b, c, d; 127 | var S11 = 7, 128 | S12 = 12, 129 | S13 = 17, 130 | S14 = 22; 131 | var S21 = 5, 132 | S22 = 9, 133 | S23 = 14, 134 | S24 = 20; 135 | var S31 = 4, 136 | S32 = 11, 137 | S33 = 16, 138 | S34 = 23; 139 | var S41 = 6, 140 | S42 = 10, 141 | S43 = 15, 142 | S44 = 21; 143 | 144 | string = Utf8Encode(string); 145 | 146 | x = ConvertToWordArray(string); 147 | 148 | a = 0x67452301; 149 | b = 0xEFCDAB89; 150 | c = 0x98BADCFE; 151 | d = 0x10325476; 152 | 153 | for (k = 0; k < x.length; k += 16) { 154 | AA = a; 155 | BB = b; 156 | CC = c; 157 | DD = d; 158 | a = FF(a, b, c, d, x[k + 0], S11, 0xD76AA478); 159 | d = FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756); 160 | c = FF(c, d, a, b, x[k + 2], S13, 0x242070DB); 161 | b = FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE); 162 | a = FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF); 163 | d = FF(d, a, b, c, x[k + 5], S12, 0x4787C62A); 164 | c = FF(c, d, a, b, x[k + 6], S13, 0xA8304613); 165 | b = FF(b, c, d, a, x[k + 7], S14, 0xFD469501); 166 | a = FF(a, b, c, d, x[k + 8], S11, 0x698098D8); 167 | d = FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF); 168 | c = FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1); 169 | b = FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE); 170 | a = FF(a, b, c, d, x[k + 12], S11, 0x6B901122); 171 | d = FF(d, a, b, c, x[k + 13], S12, 0xFD987193); 172 | c = FF(c, d, a, b, x[k + 14], S13, 0xA679438E); 173 | b = FF(b, c, d, a, x[k + 15], S14, 0x49B40821); 174 | a = GG(a, b, c, d, x[k + 1], S21, 0xF61E2562); 175 | d = GG(d, a, b, c, x[k + 6], S22, 0xC040B340); 176 | c = GG(c, d, a, b, x[k + 11], S23, 0x265E5A51); 177 | b = GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA); 178 | a = GG(a, b, c, d, x[k + 5], S21, 0xD62F105D); 179 | d = GG(d, a, b, c, x[k + 10], S22, 0x2441453); 180 | c = GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681); 181 | b = GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8); 182 | a = GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6); 183 | d = GG(d, a, b, c, x[k + 14], S22, 0xC33707D6); 184 | c = GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87); 185 | b = GG(b, c, d, a, x[k + 8], S24, 0x455A14ED); 186 | a = GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905); 187 | d = GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8); 188 | c = GG(c, d, a, b, x[k + 7], S23, 0x676F02D9); 189 | b = GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A); 190 | a = HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942); 191 | d = HH(d, a, b, c, x[k + 8], S32, 0x8771F681); 192 | c = HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122); 193 | b = HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C); 194 | a = HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44); 195 | d = HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9); 196 | c = HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60); 197 | b = HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70); 198 | a = HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6); 199 | d = HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA); 200 | c = HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085); 201 | b = HH(b, c, d, a, x[k + 6], S34, 0x4881D05); 202 | a = HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039); 203 | d = HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5); 204 | c = HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8); 205 | b = HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665); 206 | a = II(a, b, c, d, x[k + 0], S41, 0xF4292244); 207 | d = II(d, a, b, c, x[k + 7], S42, 0x432AFF97); 208 | c = II(c, d, a, b, x[k + 14], S43, 0xAB9423A7); 209 | b = II(b, c, d, a, x[k + 5], S44, 0xFC93A039); 210 | a = II(a, b, c, d, x[k + 12], S41, 0x655B59C3); 211 | d = II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92); 212 | c = II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D); 213 | b = II(b, c, d, a, x[k + 1], S44, 0x85845DD1); 214 | a = II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F); 215 | d = II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0); 216 | c = II(c, d, a, b, x[k + 6], S43, 0xA3014314); 217 | b = II(b, c, d, a, x[k + 13], S44, 0x4E0811A1); 218 | a = II(a, b, c, d, x[k + 4], S41, 0xF7537E82); 219 | d = II(d, a, b, c, x[k + 11], S42, 0xBD3AF235); 220 | c = II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB); 221 | b = II(b, c, d, a, x[k + 9], S44, 0xEB86D391); 222 | a = AddUnsigned(a, AA); 223 | b = AddUnsigned(b, BB); 224 | c = AddUnsigned(c, CC); 225 | d = AddUnsigned(d, DD); 226 | } 227 | 228 | var temp = WordToHex(a) + WordToHex(b) + WordToHex(c) + WordToHex(d); 229 | 230 | return temp.toLowerCase(); 231 | }; 232 | -------------------------------------------------------------------------------- /src/app/shared/narration.component.css: -------------------------------------------------------------------------------- 1 | a { 2 | cursor: pointer; 3 | color: blue; 4 | font-size: small; 5 | font-family: monospace; 6 | } 7 | 8 | div.narration { 9 | background-color: lightyellow; 10 | color: black; 11 | } 12 | 13 | div.narration pre { 14 | max-height: 100px; 15 | overflow: auto; 16 | } 17 | 18 | div.narration pre.expanded { 19 | max-height: none; 20 | } 21 | 22 | div.console { 23 | background-color: black; 24 | font-family: monospace; 25 | color: white; 26 | height: 110px; 27 | overflow-y: scroll; 28 | } 29 | 30 | div.console li { 31 | list-style-type: none; 32 | } 33 | 34 | div.console div.clickable { 35 | cursor: pointer; 36 | } 37 | 38 | div.console div.clickable:hover { 39 | color: yellow; 40 | } 41 | 42 | div.console li span.bullet { 43 | font-weight: bold; 44 | } 45 | 46 | div.console li span.bullet.selected { 47 | color: yellow; 48 | text-decoration: none; 49 | } -------------------------------------------------------------------------------- /src/app/shared/narration.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 | Narration (hide/clear) 7 |
8 |
9 |
    10 |
  • 11 |
    12 | >>  {{narration.step}}
    13 | 14 |

    --[ {{narration.narration}} ]--
    15 |
  • 16 |
17 |
18 |
19 |
{{selected.narration}} 20 | (-) 21 | (+) 22 |
{{selected.pre}}
23 |
24 |
Click on a step above for an explanation
25 |
26 |
27 |
28 |
-------------------------------------------------------------------------------- /src/app/shared/narration.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Injectable, Inject, OnInit } from '@angular/core'; 2 | import { Narration, NarrationService } from './narration.service' 3 | 4 | @Injectable() 5 | @Component({ 6 | selector: 'app-narration', 7 | templateUrl: 'narration.component.html', 8 | styleUrls: ['narration.component.css'] 9 | }) 10 | export class NarrationComponent implements OnInit { 11 | model: Narration[]; 12 | selected: Narration; 13 | separator: string; 14 | collapsed: boolean = true; 15 | expandedPre: boolean; 16 | showExpandPre: boolean; 17 | private _sharedService: NarrationService; 18 | 19 | constructor(sharedService: NarrationService) { 20 | this._sharedService = sharedService; 21 | } 22 | 23 | ngOnInit() { 24 | this.model = this._sharedService.dataArray; 25 | this.separator = this._sharedService.separatorKeyword; 26 | this.selected = this.model[0]; 27 | } 28 | 29 | select(n: Narration) { 30 | this.selected = n; 31 | this.expandedPre = false; 32 | this.showExpandPre = n.pre && n.pre.split(/\r\n|\r|\n/).length > 4; 33 | } 34 | 35 | toggleCollapse() { 36 | this.collapsed = !this.collapsed; 37 | } 38 | 39 | expandPre() { 40 | this.expandedPre = !this.expandedPre; 41 | } 42 | 43 | clear() { 44 | this._sharedService.clear(); 45 | this.expandedPre = false; 46 | this.selected = null; 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/app/shared/narration.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Input } from '@angular/core'; 2 | 3 | export class Narration { 4 | public step: string; 5 | public narration: string; 6 | public pre: string; 7 | 8 | constructor(s: string, n: string, pre?: string) { 9 | this.step = s; 10 | this.narration = n; 11 | this.pre = pre ? pre : null; 12 | } 13 | }; 14 | 15 | @Injectable() 16 | export class NarrationService { 17 | dataArray: Narration[] = []; 18 | separatorKeyword: string = "SEPARATOR"; 19 | 20 | clear() { 21 | this.dataArray.length = 0; 22 | } 23 | 24 | add(step: string, details: string) { 25 | this.dataArray.push(new Narration(step, details)); 26 | } 27 | 28 | addPre(step: string, details: string, pre: string) { 29 | this.dataArray.push(new Narration(step, details, pre)); 30 | } 31 | 32 | addSeparator(transactionTitle: string) { 33 | this.dataArray.push(new Narration(this.separatorKeyword, transactionTitle)); 34 | } 35 | 36 | fallbackPre(expectedPreCount: number, genericMessage: string, realPre: string[]) { 37 | var i = 1; 38 | for (var ctx of realPre) { 39 | this.addPre(genericMessage + 40 | " (note: expected " + expectedPreCount + " backend queries but got " + realPre.length 41 | + ", " + i++ + "/" + realPre.length + ")", 42 | "A query was executed in the backend:", 43 | ctx); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/app/shared/navbar.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | LOGGED IN AS: 5 | {{utilityService.getUser()}} 6 | | LOGOUT 7 |
8 |
9 |
10 |
11 | SHOPPING CART: {{utilityService.getCart().length}} items 12 | ($ {{getCost()}}) 13 |
14 |
15 | 36 |
37 | -------------------------------------------------------------------------------- /src/app/shared/navbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Injectable, Inject } from '@angular/core'; 2 | import { AuthService } from './auth.service'; 3 | import { UtilityService } from './utility.service'; 4 | import { Router } from '@angular/router'; 5 | 6 | @Component({ 7 | selector: 'app-navbar', 8 | templateUrl: 'navbar.component.html' 9 | }) 10 | 11 | export class NavbarComponent { 12 | authService:AuthService; 13 | utilityService:UtilityService; 14 | router:Router; 15 | 16 | constructor(authService:AuthService, utilityService:UtilityService,router:Router) { 17 | this.authService=authService; 18 | this.utilityService=utilityService; 19 | this.router=router; 20 | } 21 | 22 | getCost() { 23 | let cost = 0; 24 | for (var flight of this.utilityService.getCart()) { 25 | if (flight.price) 26 | cost = cost + flight.price; 27 | } 28 | return cost; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shared/utility.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from "@angular/core"; 2 | import { Http, Request, RequestOptions, RequestMethod, Headers, URLSearchParams, Response } from "@angular/http"; 3 | import { Observable } from 'rxjs/Rx'; 4 | import 'rxjs/add/operator/do' 5 | import 'rxjs/add/operator/map' 6 | 7 | @Injectable() 8 | export class UtilityService { 9 | 10 | http: Http; 11 | 12 | constructor(http: Http) { 13 | this.http = http; 14 | } 15 | 16 | makePostRequest(url: string, params: Array, body: Object, authenticate: boolean=false) { 17 | var fullUrl: string = window.location.protocol + "//" + window.location.host + url; 18 | if(params && params.length > 0) { 19 | fullUrl = fullUrl + "/" + params.join("/"); 20 | } 21 | console.log("DEBUG: POST FULL URL:",fullUrl, " BODY:",JSON.stringify(body)); 22 | return new Promise((resolve, reject) => { 23 | var requestHeaders = new Headers(); 24 | requestHeaders.append("Content-Type", "application/json"); 25 | if (authenticate) { 26 | let token = this.getToken(); 27 | if (token) requestHeaders.append('Authorization', 'Bearer ' + token); 28 | } 29 | this.http.request(new Request({ 30 | method: RequestMethod.Post, 31 | url: fullUrl, 32 | body: JSON.stringify(body), 33 | headers: requestHeaders 34 | })) 35 | .subscribe((success) => { 36 | console.log("DEBUG: POST RESPONSE:",fullUrl,":",success.json()); 37 | resolve(success); 38 | }, (error) => { 39 | reject(error); 40 | }); 41 | }); 42 | } 43 | 44 | makeGetRequestObs(url: string, params: Array, searchParams?: string, authenticate?: boolean) { 45 | let headers = new Headers({ 'Content-Type': 'application/json'}); 46 | if (authenticate) { 47 | let token = this.getToken(); 48 | if (token) headers.append('Authorization', 'Bearer ' + token); 49 | } 50 | let options = new RequestOptions({headers: headers}); 51 | 52 | var fullUrl: string = url; 53 | if(params && params.length > 0) { 54 | fullUrl = fullUrl + "/" + params.join("/"); 55 | } 56 | 57 | if (searchParams) { 58 | let urlSearchParams: URLSearchParams = new URLSearchParams(searchParams); 59 | options.search = urlSearchParams; 60 | } 61 | 62 | console.log("DEBUG: GET FULL URL:",fullUrl); 63 | return this.http.get(fullUrl, options) 64 | .do((success) => { 65 | console.log("DEBUG: GET RESPONSE:",fullUrl,":",success.json()); 66 | }, 67 | (error) => { 68 | console.log("DEBUG: GET ERROR:",fullUrl,":",error); 69 | }) 70 | .catch(UtilityService.extractError); 71 | } 72 | 73 | public static extractData(res: Response): any { 74 | let body = res.json(); 75 | return body.data || { }; 76 | } 77 | 78 | public static extractNarration(res: Response): any { 79 | let body = res.json(); 80 | return body.context || []; 81 | } 82 | 83 | public static extractError(res: Response): Observable { 84 | let body = res.json(); 85 | 86 | if (body.failure) { 87 | return Observable.throw(body.failure); 88 | } 89 | if (body.message) { 90 | return Observable.throw(body.message); 91 | } 92 | 93 | return Observable.throw(body); 94 | } 95 | 96 | makeGetRequest(url: string, params: Array) { 97 | let obs = this.makeGetRequestObs(url, params); 98 | return new Promise((resolve, reject) => { 99 | obs 100 | .subscribe((success) => { 101 | resolve(success); 102 | }, (error) => { 103 | reject(error); 104 | }); 105 | }); 106 | } 107 | 108 | getUser(){ 109 | let user = localStorage.getItem("user"); 110 | if (user) { 111 | return user; 112 | } 113 | return null; 114 | } 115 | 116 | getToken() { 117 | let token = localStorage.getItem("token"); 118 | if (token) { 119 | return token; 120 | } 121 | return null; 122 | } 123 | 124 | getCart() { 125 | let cart = localStorage.getItem("cart"); 126 | if (cart) { 127 | return JSON.parse(cart); 128 | } 129 | return []; 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/assets/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/try-cb-frontend/cc9a02c5fd0454933f2a11095203163de40d054b/src/assets/.npmignore -------------------------------------------------------------------------------- /src/assets/CBTravel.LOGO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/try-cb-frontend/cc9a02c5fd0454933f2a11095203163de40d054b/src/assets/CBTravel.LOGO.png -------------------------------------------------------------------------------- /src/assets/poweredBy.01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/try-cb-frontend/cc9a02c5fd0454933f2a11095203163de40d054b/src/assets/poweredBy.01.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | jwtEnabled: true 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.test.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | jwtEnabled: true 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | jwtEnabled: true 4 | }; 5 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/try-cb-frontend/cc9a02c5fd0454933f2a11095203163de40d054b/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TryCb 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Loading... 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | import { enableProdMode } from '@angular/core'; 5 | import { environment } from './environments/environment'; 6 | import { AppModule } from './app/'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | // This file includes polyfills needed by Angular 2 and is loaded before 2 | // the app. You can add your own extra polyfills to this file. 3 | import 'core-js/es6/symbol'; 4 | import 'core-js/es6/object'; 5 | import 'core-js/es6/function'; 6 | import 'core-js/es6/parse-int'; 7 | import 'core-js/es6/parse-float'; 8 | import 'core-js/es6/number'; 9 | import 'core-js/es6/math'; 10 | import 'core-js/es6/string'; 11 | import 'core-js/es6/date'; 12 | import 'core-js/es6/array'; 13 | import 'core-js/es6/regexp'; 14 | import 'core-js/es6/map'; 15 | import 'core-js/es6/set'; 16 | import 'core-js/es6/reflect'; 17 | 18 | import 'core-js/es7/reflect'; 19 | import 'zone.js/dist/zone'; 20 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | .checkbox { margin-left: 30px; } 3 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | 10 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 11 | declare var __karma__: any; 12 | declare var require: any; 13 | 14 | // Prevent Karma from running prematurely. 15 | __karma__.loaded = function () {}; 16 | 17 | 18 | Promise.all([ 19 | System.import('@angular/core/testing'), 20 | System.import('@angular/platform-browser-dynamic/testing') 21 | ]) 22 | // First, initialize the Angular testing environment. 23 | .then(([testing, testingBrowser]) => { 24 | testing.getTestBed().initTestEnvironment( 25 | testingBrowser.BrowserDynamicTestingModule, 26 | testingBrowser.platformBrowserDynamicTesting() 27 | ); 28 | }) 29 | // Then we find all the tests. 30 | .then(() => require.context('./', true, /\.spec\.ts/)) 31 | // And load the modules. 32 | .then(context => context.keys().map(context)) 33 | // Finally, start Karma to run the tests. 34 | .then(__karma__.start, __karma__.error); 35 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "lib": ["es6", "dom"], 7 | "mapRoot": "./", 8 | "module": "es6", 9 | "moduleResolution": "node", 10 | "outDir": "../dist/out-tsc", 11 | "sourceMap": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "../node_modules/@types" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | // Typings reference file, see links for more information 2 | // https://github.com/typings/typings 3 | // https://www.typescriptlang.org/docs/handbook/writing-declaration-files.html 4 | 5 | declare var System: any; 6 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "label-position": true, 19 | "label-undefined": true, 20 | "max-line-length": [ 21 | true, 22 | 140 23 | ], 24 | "member-access": false, 25 | "member-ordering": [ 26 | true, 27 | "static-before-instance", 28 | "variables-before-functions" 29 | ], 30 | "no-arg": true, 31 | "no-bitwise": true, 32 | "no-console": [ 33 | true, 34 | "debug", 35 | "info", 36 | "time", 37 | "timeEnd", 38 | "trace" 39 | ], 40 | "no-construct": true, 41 | "no-debugger": true, 42 | "no-duplicate-key": true, 43 | "no-duplicate-variable": true, 44 | "no-empty": false, 45 | "no-eval": true, 46 | "no-inferrable-types": true, 47 | "no-shadowed-variable": true, 48 | "no-string-literal": false, 49 | "no-switch-case-fall-through": true, 50 | "no-trailing-whitespace": true, 51 | "no-unused-expression": true, 52 | "no-unused-variable": true, 53 | "no-unreachable": true, 54 | "no-use-before-declare": true, 55 | "no-var-keyword": true, 56 | "object-literal-sort-keys": false, 57 | "one-line": [ 58 | true, 59 | "check-open-brace", 60 | "check-catch", 61 | "check-else", 62 | "check-whitespace" 63 | ], 64 | "quotemark": [ 65 | true, 66 | "single" 67 | ], 68 | "radix": true, 69 | "semicolon": [ 70 | "always" 71 | ], 72 | "triple-equals": [ 73 | true, 74 | "allow-null-check" 75 | ], 76 | "typedef-whitespace": [ 77 | true, 78 | { 79 | "call-signature": "nospace", 80 | "index-signature": "nospace", 81 | "parameter": "nospace", 82 | "property-declaration": "nospace", 83 | "variable-declaration": "nospace" 84 | } 85 | ], 86 | "variable-name": false, 87 | "whitespace": [ 88 | true, 89 | "check-branch", 90 | "check-decl", 91 | "check-operator", 92 | "check-separator", 93 | "check-type" 94 | ], 95 | 96 | "directive-selector-prefix": [true, "app"], 97 | "component-selector-prefix": [true, "app"], 98 | "directive-selector-name": [true, "camelCase"], 99 | "component-selector-name": [true, "kebab-case"], 100 | "directive-selector-type": [true, "attribute"], 101 | "component-selector-type": [true, "element"], 102 | "use-input-property-decorator": true, 103 | "use-output-property-decorator": true, 104 | "use-host-property-decorator": true, 105 | "no-input-rename": true, 106 | "no-output-rename": true, 107 | "use-life-cycle-interface": true, 108 | "use-pipe-transform-interface": true, 109 | "component-class-suffix": true, 110 | "directive-class-suffix": true 111 | } 112 | } 113 | --------------------------------------------------------------------------------