├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── deployment ├── azuredeploy.json └── azuredeploy.parameters.json ├── front.js ├── locales ├── en.json └── es.json ├── package-lock.json ├── package.json ├── public ├── assets │ ├── b-barcelona.jpg │ ├── b-footer.jpg │ ├── b-home.jpg │ ├── barcelona.jpg │ ├── cities │ │ ├── barcelona.jpg │ │ ├── barcelona_m.jpg │ │ ├── hawaii.jpg │ │ ├── hawaii_m.jpg │ │ ├── paris.jpg │ │ └── paris_m.jpg │ ├── favicon.ico │ ├── logo_contoso_air.svg │ ├── logo_contoso_air2.svg │ ├── microsoft.svg │ └── plane.png ├── css │ └── main.css ├── fonts │ └── Karla-Regular.ttf └── js │ └── main.js ├── scss ├── main.scss └── styles │ ├── _booking.scss │ ├── _cities.scss │ ├── _cityinfo.scss │ ├── _days.scss │ ├── _deals.scss │ ├── _flights.scss │ ├── _fonts.scss │ ├── _footer.scss │ ├── _icons.scss │ ├── _loading.scss │ ├── _navbar.scss │ ├── _overrides.scss │ ├── _search.scss │ ├── _seats.scss │ ├── _variables.scss │ └── _vendor.scss ├── server.js ├── src ├── config │ ├── i18n.config.js │ └── passport.config.js ├── data │ ├── airports.json │ ├── deals.json │ ├── destinations.json │ └── flights.json ├── repositories │ ├── book.repository.js │ ├── book.repository.model.js │ ├── flights.repository.js │ └── index.js ├── routes │ ├── auth.js │ ├── book.js │ ├── booked.js │ ├── flights.js │ ├── helpers.js │ ├── home.js │ ├── index.js │ ├── purchase.js │ └── receipt.js └── services │ ├── airports.service.ispec.js │ ├── airports.service.js │ ├── airports.service.spec.js │ ├── book.form.service.js │ ├── book.form.service.spec.js │ ├── book.service.js │ ├── book.service.spec.js │ ├── date.service.js │ ├── date.service.spec.js │ ├── deals.service.ispec.js │ ├── deals.service.js │ ├── deals.service.spec.js │ ├── flights.service.js │ ├── flights.service.spec.js │ ├── index.js │ ├── navbar.service.js │ ├── navbar.service.spec.js │ ├── price.service.js │ └── price.service.spec.js ├── views ├── book.hbs ├── booked.hbs ├── flights.hbs ├── home.hbs ├── layouts │ └── main.hbs ├── login.hbs ├── partials │ ├── book │ │ ├── deals-destinations.hbs │ │ ├── deals-price.hbs │ │ ├── deals.hbs │ │ └── form.hbs │ ├── common │ │ ├── footer.hbs │ │ └── navbar.hbs │ ├── flights │ │ ├── filters.hbs │ │ ├── results-picker.hbs │ │ ├── results.hbs │ │ └── slider.hbs │ └── purchase │ │ ├── summary.hbs │ │ └── totalPrice.hbs ├── purchase.hbs └── receipt.hbs ├── web.config └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | 332 | # Logs 333 | logs 334 | *.log 335 | npm-debug.log* 336 | 337 | # Runtime data 338 | pids 339 | *.pid 340 | *.seed 341 | 342 | # Directory for instrumented libs generated by jscoverage/JSCover 343 | lib-cov 344 | 345 | # Coverage directory used by tools like istanbul 346 | coverage 347 | 348 | # nyc test coverage 349 | .nyc_output 350 | 351 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 352 | .grunt 353 | 354 | # node-waf configuration 355 | .lock-wscript 356 | 357 | # Compiled binary addons (http://nodejs.org/api/addons.html) 358 | build/Release 359 | 360 | # Dependency directories 361 | node_modules 362 | jspm_packages 363 | 364 | # Optional npm cache directory 365 | .npm 366 | 367 | # Optional REPL history 368 | .node_repl_history 369 | 370 | # Project specifics 371 | .vscode/ 372 | 373 | # Mac 374 | .DS_Store/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This project is retired, archived, and no longer supported. You are welcome to continue to use and fork the repository. 2 | 3 | 4 | # Microsoft.Github CI Demo 5 | 6 | Demo website application for Contoso Air. 7 | Runs a nodejs server (Express v4.16) that stores customer booked flights in a CosmosDb database. 8 | 9 | ## Requirements 10 | 11 | * Node v8.9.4 or later 12 | * Azure CosmosDb 13 | 14 | ## Local Environment Setup 15 | 16 | This project uses ES6, and has been tested with nodejs v8.9.4 17 | There is almost no front-end logic. Still, the application uses webpack to compile sass styles and bundle third parties js files. If you want to modify any front logic or style run `npm run local:build`. 18 | 19 | In order to launch a local server on port 3000 (can be modified with environment variable PORT) run: 20 | 21 | ```bash 22 | npm install 23 | SET %COSMOS_DB_NAME%= 24 | SET %COSMOS_DB_AUTH_KEY%= 25 | npm start 26 | ``` 27 | 28 | This will run locally the server and attach to the CosmosDb Endpoint using mongodb connection string. 29 | 30 | ## Azure Manual Deployment 31 | 32 | In order to create the Azure deploy there is an ARM template located at deployment folder. 33 | 34 | ARM template parameter | Usage | e.g. 35 | --- | --- | --- 36 | p_environment | set an environment suffix | `dev` 37 | p_site_prefix | common prefix for all resources created | `contoso-air` 38 | p_site_web_name | website specific resource name | `web` 39 | p_comosdb_name | database specific resource name | `db` 40 | 41 | > e.g. previous parameter examples will create resources `contoso-air-db-dev` and `contoso-air-db-dev`. 42 | 43 | Then you run the ARM template with the following commands ([Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) required): 44 | 45 | ```bash 46 | cd deployment 47 | az group deployment create --resource-group --template-file azuredeploy.json --parameters p_environment=dev 48 | ``` 49 | 50 | What's left is to compress the whole folder in a zip file and upload it to Azure. Manually it can be done going to [https://.scm.azurewebsites.net/ZipDeployUI](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-get-started-nodejs#deploy-zip-file) 51 | 52 | > Note: Files under folders `deployment`, `scss` and files `front.js` and `webpack.config.js` are not required to be zipped. 53 | 54 | ## Azure VSTS Deployment 55 | 56 | ### Build process tasks 57 | - [Copy Files](https://go.microsoft.com/fwlink/?LinkID=708389) 58 | - Source Folder: deployment 59 | - Contents: *.json 60 | - Target Folder: $(build.artifactstagingdirectory)/Templates 61 | - [npm](https://go.microsoft.com/fwlink/?LinkID=613746) 62 | - Command: custom 63 | - Command and arguments: install --production 64 | - [Archive Files](http://go.microsoft.com/fwlink/?LinkId=809083) 65 | - Root folder: $(System.DefaultWorkingDirectory) 66 | - Archive type: zip 67 | - [Publish Artifact](https://go.microsoft.com/fwlink/?LinkID=708390) 68 | 69 | ### Release process tasks: 70 | - [Azure Resource Group Deployment](https://aka.ms/argtaskreadme) 71 | - Template: select from the artifact in the Templates folder. 72 | - Template Parameters: same as previous parameter. 73 | - Override template parameters: At least modify the -p_environment variable. 74 | - [Azure App Service Deploy](https://aka.ms/azurermwebdeployreadme) 75 | - Package or folder: Select zip cabinet from the artifact. 76 | 77 | ## Contributing 78 | 79 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 80 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 81 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 82 | 83 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 84 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 85 | provided by the bot. You will only need to do this once across all repos using our CLA. 86 | 87 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 88 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 89 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 90 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const logger = require('morgan'); 4 | const Handlebars = require('handlebars'); 5 | const exphbs = require('express-handlebars'); 6 | const session = require("express-session"); 7 | const flash = require('express-flash'); 8 | const favicon = require('serve-favicon'); 9 | 10 | const passport = require('./src/config/passport.config'); 11 | const configureI18N = require('./src/config/i18n.config'); 12 | 13 | const i18n = configureI18N(__dirname); 14 | const app = express(); 15 | app.use(logger('dev')); 16 | app.use(express.urlencoded({ extended: true })); 17 | app.use(express.json()); 18 | app.use(flash()); 19 | app.use(session({ secret: 'secret', resave: true, saveUninitialized: true })); 20 | app.use(passport.initialize()); 21 | app.use(passport.session()); 22 | app.use(i18n.init); 23 | app.engine('hbs', exphbs({ 24 | extname: '.hbs', 25 | defaultLayout: 'main', 26 | helpers: { 27 | i18n: (s, req) => new Handlebars.SafeString(req.data.root.__(s)) 28 | } 29 | })); 30 | app.set('view engine', 'hbs'); 31 | 32 | app.use(favicon(__dirname + '/public/assets/favicon.ico')); 33 | app.use(express.static(path.join(__dirname, 'public'))); 34 | app.use('/', require('./src/routes')); 35 | 36 | module.exports = app; -------------------------------------------------------------------------------- /deployment/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "p_environment": { 6 | "type": "string" 7 | }, 8 | "p_separator": { 9 | "defaultValue": "-", 10 | "type": "string" 11 | }, 12 | "p_site_prefix": { 13 | "defaultValue": "githubci", 14 | "type": "string" 15 | }, 16 | "p_site_web_name": { 17 | "defaultValue": "web", 18 | "type": "string" 19 | }, 20 | "p_comosdb_name": { 21 | "defaultValue": "db", 22 | "type": "string" 23 | } 24 | }, 25 | "variables": { 26 | "site_web_name": "[concat(parameters('p_site_prefix'), parameters('p_separator'), parameters('p_site_web_name'), parameters('p_separator'), parameters('p_environment'))]", 27 | "comosdb_default_name": "[concat(parameters('p_site_prefix'), parameters('p_separator'), parameters('p_comosdb_name'), parameters('p_separator'), parameters('p_environment'))]" 28 | }, 29 | "resources": [ 30 | { 31 | "type": "Microsoft.DocumentDB/databaseAccounts", 32 | "kind": "MongoDB", 33 | "name": "[variables('comosdb_default_name')]", 34 | "apiVersion": "2015-04-08", 35 | "location": "[resourceGroup().location]", 36 | "tags": { 37 | "defaultExperience": "MongoDB" 38 | }, 39 | "scale": null, 40 | "properties": { 41 | "name": "[variables('comosdb_default_name')]", 42 | "databaseAccountOfferType": "Standard", 43 | "consistencyPolicy": { 44 | "defaultConsistencyLevel": "Session", 45 | "maxIntervalInSeconds": 5, 46 | "maxStalenessPrefix": 100 47 | } 48 | }, 49 | "dependsOn": [] 50 | }, 51 | { 52 | "type": "Microsoft.Web/sites", 53 | "kind": "app", 54 | "name": "[variables('site_web_name')]", 55 | "apiVersion": "2016-08-01", 56 | "location": "[resourceGroup().location]", 57 | "properties": { 58 | "name": "[variables('site_web_name')]", 59 | "reserved": false, 60 | "siteConfig": { 61 | "appSettings": [ 62 | { 63 | "name": "COSMOS_DB_NAME", 64 | "value":"[variables('comosdb_default_name')]" 65 | }, 66 | { 67 | "name": "COSMOS_DB_AUTH_KEY", 68 | "value": "[listKeys(resourceId('Microsoft.DocumentDb/databaseAccounts', variables('comosdb_default_name')), '2015-04-08').primaryMasterKey]" 69 | }, 70 | { 71 | "name": "WEBSITE_NODE_DEFAULT_VERSION", 72 | "value": "8.9.4" 73 | } 74 | ] 75 | } 76 | }, 77 | "dependsOn": [ 78 | "[concat('Microsoft.DocumentDB/databaseAccounts/', variables('comosdb_default_name'))]" 79 | ] 80 | } 81 | ], 82 | "outputs": { 83 | "web": { 84 | "type": "string", 85 | "value": "[variables('site_web_name')]" 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /deployment/azuredeploy.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "p_environment": { 6 | "value": "dev" 7 | }, 8 | "p_site_prefix": { 9 | "value": "githubci" 10 | }, 11 | "p_site_web_name": { 12 | "value": "web" 13 | }, 14 | "p_comosdb_name":{ 15 | "value": "db" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /front.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | require('bootstrap-datepicker'); 3 | require('bootstrap-datepicker/dist/css/bootstrap-datepicker3.standalone.css'); 4 | 5 | require('./scss/main.scss'); 6 | 7 | $(document).ready(function(){ 8 | var today = $('.datepicker').data('start') || new Date(); 9 | var options = { 10 | format: 'yyyy-mm-dd', 11 | startDate: today 12 | }; 13 | 14 | $('.datepicker').datepicker(options); 15 | 16 | $('#dpa').on('changeDate', function(e) { 17 | var datepicker = $('#dpb').data("datepicker"); 18 | datepicker.setStartDate(e.date); 19 | }); 20 | 21 | $('.block-flights-results input[type="radio"]').change(function(){ 22 | $(this).closest('.block-flights-results').find('.big-blue-radio').removeClass('big-blue-radio--active'); 23 | $(this).closest('.block-flights-results-list-item').find('.big-blue-radio').addClass('big-blue-radio--active'); 24 | }) 25 | }) 26 | 27 | 28 | -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Title": "Contoso Air", 3 | "Home": { 4 | "HeroMessage": "Where do you
want to go?" 5 | }, 6 | "Deals": { 7 | "CitiesTitle": "Recommended for you", 8 | "FlightsTitle": "Flight deals", 9 | "FlightsTo": "to ", 10 | "FlightsPrice": "Purchase by ", 11 | "FlightsPriceFrom": "FROM $", 12 | "FlightsOneWay": "ONE WAY", 13 | "FlightsDatesButton": "View Dates" 14 | }, 15 | "NavbarMenu": { 16 | "Book": "Book", 17 | "Booked": "My Booked", 18 | "Login": "Login", 19 | "Greeting": "Hi, %s" 20 | }, 21 | "Footer": { 22 | "About":{ 23 | "Title": "ABOUT CONTOSO", 24 | "Link_1": "Who we are", 25 | "Link_2": "Contact us", 26 | "Link_3": "Travel advisories", 27 | "Link_4": "Customer commitment", 28 | "Link_5": "Feedback", 29 | "Link_6": "Privacy notice" 30 | }, 31 | "Customer":{ 32 | "Title": "CUSTOMER SERVICE", 33 | "Link_1": "Careers", 34 | "Link_2": "Legal", 35 | "Link_3": "Newsroom", 36 | "Link_4": "Investor relations", 37 | "Link_5": "Contract of carriage", 38 | "Link_6": "Tarmac delay plan", 39 | "Link_7": "Site map" 40 | }, 41 | "Products":{ 42 | "Title": "PRODUCTS AND SERVICES", 43 | "Link_1": "Optional services and fees", 44 | "Link_2": "Corporate travel", 45 | "Link_3": "Travel agents", 46 | "Link_4": "Cargo", 47 | "Link_5": "Gift certificates", 48 | "Link_6": "Follow us" 49 | }, 50 | "Copyright": "This site is built by Microsoft for demonstration purposes only." 51 | }, 52 | "Login": { 53 | "Title": "Login", 54 | "Username": "Username", 55 | "Password": "Password", 56 | "Error": { 57 | "MissingCredentials": "Missing Credentials", 58 | "InvalidUsername": "Empty username" 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "Title": "Contoso Air", 3 | "Home": { 4 | "HeroMessage": "Donde
quieres ir?" 5 | }, 6 | "Deals": { 7 | "CitiesTitle": "Recomendado para ti", 8 | "FlightsTitle": "Detalles de vuelos", 9 | "FlightsTo": "a ", 10 | "FlightsPrice": "Compra por ", 11 | "FlightsPriceFrom": "DESDE $", 12 | "FlightsOneWay": "SOLO IDA", 13 | "FlightsDatesButton": "Ver fechas" 14 | }, 15 | "NavbarMenu": { 16 | "Book": "Reserva", 17 | "Booked": "Mis reservas", 18 | "Login": "Login", 19 | "Greeting": "Hola, %s" 20 | }, 21 | "Footer": { 22 | "About": { 23 | "Title": "ACERCA CONTOSO", 24 | "Link_1": "Quien somos", 25 | "Link_2": "Contactanos", 26 | "Link_3": "Consejos de viaje", 27 | "Link_4": "Compromiso del cliente", 28 | "Link_5": "Opiniones del cliente", 29 | "Link_6": "Aviso de privacidad" 30 | }, 31 | "Customer": { 32 | "Title": "SERVICIO AL CLIENTE", 33 | "Link_1": "Carreras profesionales", 34 | "Link_2": "Notas legales", 35 | "Link_3": "Noticias", 36 | "Link_4": "Relaciones con inversores", 37 | "Link_5": "Contrato de transporte", 38 | "Link_6": "Plan de retrasos", 39 | "Link_7": "Mapa web" 40 | }, 41 | "Products": { 42 | "Title": "SERVICIOS Y PRODUCTOS", 43 | "Link_1": "Tarifas de servicios opcionales", 44 | "Link_2": "Viajes de empresa", 45 | "Link_3": "Agentes de viajes", 46 | "Link_4": "Mercancias", 47 | "Link_5": "Vales regalo", 48 | "Link_6": "Síguenos" 49 | }, 50 | "Copyright": "Esta web ha sido creada por Microsoft para propositos de demostración." 51 | }, 52 | "Login": { 53 | "Title": "Login", 54 | "Username": "Nombre de usuario", 55 | "Password": "Contraseña", 56 | "Error": { 57 | "MissingCredentials": "Credenciales invalidas", 58 | "InvalidUsername": "Nombre de usuario vacío" 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contoso-air-dev", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./server.js --production", 7 | "local:build": "webpack --env.NODE_ENV=development --mode=development", 8 | "build": "exit 0", 9 | "unit-test": "jest", 10 | "int-test": "jest --testRegex=.*\\.ispec\\.js$", 11 | "test": "jest --testRegex=.*\\.i?spec\\.js$", 12 | "all-test:watch": "jest --watch --testRegex=.*\\.i?spec\\.js$" 13 | }, 14 | "jest": { 15 | "testResultsProcessor": "jest-junit-reporter", 16 | "collectCoverage": true, 17 | "coverageReporters": [ 18 | "cobertura", 19 | "html" 20 | ] 21 | }, 22 | "dependencies": { 23 | "bootstrap": "^4.1.3", 24 | "bootstrap-datepicker": "^1.8.0", 25 | "debug": "~2.6.9", 26 | "express": "~4.16.0", 27 | "express-flash": "0.0.2", 28 | "express-handlebars": "^3.0.0", 29 | "express-session": "^1.15.6", 30 | "handlebars": "^4.0.12", 31 | "i18n": "^0.8.3", 32 | "moment": "^2.22.2", 33 | "mongoose": "^5.3.7", 34 | "morgan": "~1.9.0", 35 | "npm": "^6.7.0", 36 | "passport": "^0.4.0", 37 | "passport-local": "^1.0.0", 38 | "popper.js": "^1.14.4", 39 | "serve-favicon": "^2.5.0", 40 | "uuid": "^3.3.2" 41 | }, 42 | "devDependencies": { 43 | "autoprefixer": "^9.2.1", 44 | "css-loader": "^1.0.0", 45 | "jest": "^24.0.0", 46 | "jest-junit-reporter": "^1.1.0", 47 | "mini-css-extract-plugin": "^0.4.4", 48 | "node-sass": "^4.9.4", 49 | "postcss-loader": "^3.0.0", 50 | "sass-loader": "^7.1.0", 51 | "webpack": "^4.22.0", 52 | "webpack-cli": "^3.1.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /public/assets/b-barcelona.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ContosoAir/abe54eb03dc768dbc9e6e90bae8c4611b45bb188/public/assets/b-barcelona.jpg -------------------------------------------------------------------------------- /public/assets/b-footer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ContosoAir/abe54eb03dc768dbc9e6e90bae8c4611b45bb188/public/assets/b-footer.jpg -------------------------------------------------------------------------------- /public/assets/b-home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ContosoAir/abe54eb03dc768dbc9e6e90bae8c4611b45bb188/public/assets/b-home.jpg -------------------------------------------------------------------------------- /public/assets/barcelona.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ContosoAir/abe54eb03dc768dbc9e6e90bae8c4611b45bb188/public/assets/barcelona.jpg -------------------------------------------------------------------------------- /public/assets/cities/barcelona.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ContosoAir/abe54eb03dc768dbc9e6e90bae8c4611b45bb188/public/assets/cities/barcelona.jpg -------------------------------------------------------------------------------- /public/assets/cities/barcelona_m.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ContosoAir/abe54eb03dc768dbc9e6e90bae8c4611b45bb188/public/assets/cities/barcelona_m.jpg -------------------------------------------------------------------------------- /public/assets/cities/hawaii.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ContosoAir/abe54eb03dc768dbc9e6e90bae8c4611b45bb188/public/assets/cities/hawaii.jpg -------------------------------------------------------------------------------- /public/assets/cities/hawaii_m.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ContosoAir/abe54eb03dc768dbc9e6e90bae8c4611b45bb188/public/assets/cities/hawaii_m.jpg -------------------------------------------------------------------------------- /public/assets/cities/paris.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ContosoAir/abe54eb03dc768dbc9e6e90bae8c4611b45bb188/public/assets/cities/paris.jpg -------------------------------------------------------------------------------- /public/assets/cities/paris_m.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ContosoAir/abe54eb03dc768dbc9e6e90bae8c4611b45bb188/public/assets/cities/paris_m.jpg -------------------------------------------------------------------------------- /public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ContosoAir/abe54eb03dc768dbc9e6e90bae8c4611b45bb188/public/assets/favicon.ico -------------------------------------------------------------------------------- /public/assets/logo_contoso_air.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 12 | 15 | 16 | 20 | 24 | 28 | 31 | 32 | 35 | 36 | 37 | 39 | 41 | 43 | 45 | 47 | 49 | 51 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /public/assets/logo_contoso_air2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 14 | 17 | 19 | 20 | 23 | 26 | 29 | 31 | 32 | 34 | 35 | 36 | 38 | 39 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /public/assets/microsoft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 21 | 23 | 28 | 33 | 39 | 42 | 44 | 50 | 56 | 57 | 59 | 60 | -------------------------------------------------------------------------------- /public/assets/plane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ContosoAir/abe54eb03dc768dbc9e6e90bae8c4611b45bb188/public/assets/plane.png -------------------------------------------------------------------------------- /public/fonts/Karla-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ContosoAir/abe54eb03dc768dbc9e6e90bae8c4611b45bb188/public/fonts/Karla-Regular.ttf -------------------------------------------------------------------------------- /scss/main.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/variables'; 2 | @import 'styles/fonts'; 3 | @import 'styles/icons'; 4 | @import 'styles/vendor'; 5 | @import 'styles/navbar'; 6 | @import 'styles/footer'; 7 | @import 'styles/search'; 8 | @import 'styles/cityinfo'; 9 | @import 'styles/cities'; 10 | @import 'styles/deals'; 11 | @import 'styles/flights'; 12 | @import 'styles/days'; 13 | @import 'styles/seats'; 14 | @import 'styles/booking'; 15 | @import 'styles/loading'; 16 | 17 | @font-face { 18 | font-family: 'Karla'; 19 | src: url('/fonts/Karla-Regular.ttf'); 20 | } 21 | 22 | body { 23 | font-family: 'Karla', 'Helvetica', 'Arial'; 24 | font-size: 12px; 25 | } 26 | 27 | .btn { 28 | color: $color-white-alpha-9; 29 | cursor: pointer; 30 | font-size: .9rem; 31 | letter-spacing: .05rem; 32 | padding: .2rem 1.2rem; 33 | 34 | &:focus { 35 | border-width: 0; 36 | box-shadow: none; 37 | outline: none; 38 | } 39 | } 40 | 41 | .btn-md { 42 | padding: .6rem 1.5rem; 43 | } 44 | 45 | .btn-lg { 46 | padding: .6rem 4rem; 47 | } 48 | 49 | .btn-cancel { 50 | background-color: $color-grey-medium; 51 | border: 0; 52 | 53 | &:hover { 54 | background-color: $color-grey-dark; 55 | } 56 | } 57 | 58 | .pos-r { 59 | position: relative; 60 | } 61 | 62 | .form-control { 63 | border: .1rem $color-blue-light solid; 64 | border-radius: 0; 65 | line-height: 1; 66 | 67 | &:focus { 68 | border-color: $color-blue; 69 | } 70 | 71 | &::placeholder { 72 | color: $color-grey; 73 | } 74 | } 75 | 76 | .radio-button-square { 77 | border: 1px solid $color-blue-light; 78 | display: inline-block; 79 | height: 14px; 80 | margin-right: .4rem; 81 | position: relative; 82 | top: 2px; 83 | width: 14px; 84 | 85 | &.radio-button-square--active { 86 | 87 | &::after { 88 | background-color: $color-blue-medium; 89 | content: ''; 90 | display: inline-block; 91 | height: 8px; 92 | left: 2px; 93 | position: absolute; 94 | top: 2px; 95 | width: 8px; 96 | } 97 | } 98 | } 99 | 100 | .route-content { 101 | min-height: 20rem; 102 | } 103 | 104 | ul { 105 | list-style: none; 106 | padding-left: 0; 107 | } 108 | 109 | label { 110 | color: $color-blue-medium; 111 | margin-bottom: .1rem; 112 | } 113 | 114 | .form-check-label { 115 | color: $color-grey-medium; 116 | } 117 | 118 | hr { 119 | border-top: 1px solid $color-blue-medium; 120 | margin-bottom: 2rem; 121 | margin-top: 2rem; 122 | } 123 | 124 | p { 125 | line-height: 1.8; 126 | } 127 | 128 | .box { 129 | background-color: $color-white; 130 | border-radius: .7rem; 131 | box-shadow: 0 0 .4rem 0 $color-black-alpha-2; 132 | padding: 1.4rem 1.8rem; 133 | width: 100%; 134 | } 135 | 136 | .content { 137 | margin-bottom: 1.4rem; 138 | min-height: 36rem; 139 | position: relative; 140 | 141 | &::after { 142 | background-image: linear-gradient(to bottom, $color-transparent, $color-white 21rem, $color-white 100%); 143 | content: ''; 144 | display: block; 145 | height: 100%; 146 | left: -1%; 147 | position: absolute; 148 | top: 1.4rem; 149 | width: 102%; 150 | z-index: 0; 151 | } 152 | 153 | .content-wrapper { 154 | position: relative; 155 | z-index: 1; 156 | } 157 | } 158 | 159 | .content--search { 160 | top: -2.5rem; 161 | } 162 | 163 | .redefine-search { 164 | color: $color-grey-medium; 165 | display: block; 166 | font-size: 1.2rem; 167 | margin-top: 2rem; 168 | 169 | &:hover { 170 | text-decoration: none; 171 | } 172 | 173 | .icon { 174 | font-size: .7rem; 175 | } 176 | } 177 | 178 | @include media-breakpoint-up(md) { 179 | body { 180 | font-size: 14px; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /scss/styles/_booking.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .block-booking { 4 | 5 | .cities--indicator { 6 | color: $color-blue-medium; 7 | margin-bottom: 1rem; 8 | text-align: center; 9 | width: 15rem; 10 | 11 | &--line { 12 | background-color: $color-blue-medium; 13 | display: block; 14 | height: 2px; 15 | margin: 0 auto; 16 | position: relative; 17 | top: .8rem; 18 | width: 80%; 19 | 20 | &::before { 21 | border-color: $color-blue-medium; 22 | border-radius: 5px; 23 | border-style: solid; 24 | border-width: 2px; 25 | box-sizing: content-box; 26 | content: ' '; 27 | display: block; 28 | height: 4px; 29 | left: -6px; 30 | position: absolute; 31 | top: -3px; 32 | width: 4px; 33 | } 34 | 35 | &::after { 36 | border-color: $color-blue-medium; 37 | border-radius: 5px; 38 | border-style: solid; 39 | border-width: 2px; 40 | box-sizing: content-box; 41 | content: ' '; 42 | display: block; 43 | height: 4px; 44 | position: absolute; 45 | right: -6px; 46 | top: -3px; 47 | width: 4px; 48 | } 49 | } 50 | 51 | .cell { 52 | font-size: .8rem; 53 | width: 33.33333%; 54 | 55 | &--letters { 56 | font-size: 2.5rem; 57 | font-weight: 600; 58 | line-height: .8; 59 | } 60 | } 61 | } 62 | 63 | .receipt-summary { 64 | line-height: 1; 65 | margin-bottom: 2rem; 66 | 67 | h2 { 68 | color: $color-blue-medium; 69 | font-size: .9rem; 70 | margin-bottom: .3rem; 71 | } 72 | 73 | p { 74 | margin: 0; 75 | } 76 | } 77 | 78 | .block-booking-title { 79 | color: $color-blue-medium; 80 | font-size: 1.9rem; 81 | margin-bottom: 1rem; 82 | } 83 | 84 | .block-booking-title2 { 85 | color: $color-grey-dark; 86 | font-size: 1.4rem; 87 | margin-bottom: 1.5rem; 88 | } 89 | 90 | .block-booking-title3 { 91 | color: $color-blue-medium; 92 | font-size: 1.3rem; 93 | } 94 | 95 | .block-booking-passenger { 96 | color: $color-grey-dark; 97 | font-size: 1rem; 98 | } 99 | 100 | .block-booking-total { 101 | color: $color-grey-dark; 102 | font-size: 1.2rem; 103 | margin-bottom: .5rem; 104 | } 105 | 106 | .block-booking-underline { 107 | border-bottom: 1px solid $color-blue-light; 108 | border-top: 0; 109 | display: block; 110 | margin-bottom: .5rem; 111 | margin-top: 0; 112 | } 113 | 114 | .block-booking-link { 115 | color: $color-blue-medium; 116 | text-decoration: underline; 117 | } 118 | 119 | .block-booking-buttons { 120 | 121 | .btn { 122 | font-size: .8rem; 123 | margin-bottom: .5rem; 124 | margin-left: auto; 125 | margin-right: auto; 126 | padding: .6rem 0; 127 | width: 80%; 128 | } 129 | } 130 | } 131 | 132 | @include media-breakpoint-up(sm) { 133 | .block-booking { 134 | .block-booking-buttons { 135 | 136 | .btn { 137 | font-size: .9rem; 138 | margin-bottom: 0; 139 | margin-left: .5rem; 140 | margin-right: 0; 141 | padding: .6rem 4rem; 142 | width: auto; 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /scss/styles/_cities.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .block-cities { 4 | 5 | .block-cities-title { 6 | color: $color-blue; 7 | font-size: 1.4rem; 8 | padding-bottom: .5rem; 9 | } 10 | 11 | .block-cities-list { 12 | list-style: none; 13 | padding-left: 0; 14 | 15 | .block-cities-list-item { 16 | 17 | .block-cities-list-item-figure { 18 | background-color: $color-grey-light; 19 | display: block; 20 | position: relative; 21 | 22 | .block-cities-list-item-figure-image { 23 | height: auto; 24 | width: 100%; 25 | 26 | &--desktop { 27 | display: none; 28 | } 29 | } 30 | 31 | .block-cities-list-item-figure-caption { 32 | background: $color-white-alpha-8; 33 | bottom: 0; 34 | color: $color-blue-medium; 35 | font-size: 1.2rem; 36 | font-weight: 400; 37 | line-height: 2rem; 38 | position: absolute; 39 | text-align: center; 40 | width: 100%; 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | @include media-breakpoint-up(md) { 48 | .block-cities { 49 | 50 | .block-cities-list { 51 | 52 | .block-cities-list-item { 53 | 54 | .block-cities-list-item-figure { 55 | 56 | .block-cities-list-item-figure-image { 57 | display: none; 58 | 59 | &--desktop { 60 | display: inline; 61 | } 62 | } 63 | 64 | .block-cities-list-item-figure-image { 65 | height: auto; 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | @include media-breakpoint-between(md, lg) { 74 | .block-cities { 75 | 76 | .block-cities-list { 77 | 78 | .block-cities-list-item { 79 | 80 | .block-cities-list-item-figure { 81 | 82 | .block-cities-list-item-figure-caption { 83 | font-size: 1.5rem; 84 | line-height: 3.2rem; 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | @include media-breakpoint-between(lg, xl) { 93 | .block-cities { 94 | 95 | .block-cities-list { 96 | 97 | .block-cities-list-item { 98 | 99 | .block-cities-list-item-figure { 100 | 101 | .block-cities-list-item-figure-caption { 102 | font-size: 1.5rem; 103 | line-height: 4.5rem; 104 | } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | 111 | @include media-breakpoint-up(xl) { 112 | .block-cities { 113 | 114 | .block-cities-list { 115 | 116 | .block-cities-list-item { 117 | 118 | .block-cities-list-item-figure { 119 | 120 | .block-cities-list-item-figure-caption { 121 | font-size: 2rem; 122 | line-height: 5.2rem; 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /scss/styles/_cityinfo.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .block-cityinfo { 4 | font-size: 1.1rem; 5 | margin-bottom: 1rem; 6 | 7 | &-image { 8 | margin-bottom: 1rem; 9 | width: 100%; 10 | } 11 | 12 | &-video { 13 | display: none; 14 | } 15 | 16 | &-title { 17 | color: $color-blue-medium; 18 | font-size: 1.7rem; 19 | margin-bottom: 1rem; 20 | } 21 | 22 | } 23 | 24 | @include media-breakpoint-up(md) { 25 | 26 | .block-cityinfo { 27 | margin-bottom: 1rem; 28 | 29 | &-video { 30 | animation: show-video 2s forwards; 31 | background-color: $color-white; 32 | display: block; 33 | opacity: 0; 34 | position: absolute; 35 | top: -9vw; 36 | width: 90%; 37 | 38 | @keyframes show-video { 39 | 0% { 40 | background-color: $color-white; 41 | opacity: 0; 42 | } 43 | 44 | 100% { 45 | background-color: transparent; 46 | opacity: 1; 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /scss/styles/_days.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .block-flights-days { 4 | margin-bottom: 1rem; 5 | 6 | .block-flights-days-title { 7 | font-size: 1.2rem; 8 | } 9 | 10 | .block-flights-days-slider { 11 | overflow-x: auto; 12 | 13 | .block-flights-days-slider-arrow { 14 | align-self: center; 15 | color: $color-blue-medium; 16 | display: none; 17 | font-size: 2rem; 18 | overflow: hidden; 19 | text-align: center; 20 | } 21 | 22 | .block-flights-days-slider-items { 23 | display: block; 24 | flex-wrap: nowrap; 25 | justify-content: space-around; 26 | list-style: none; 27 | padding: 0; 28 | width: 46rem; 29 | 30 | .block-flights-days-slider-items-item { 31 | align-self: center; 32 | display: inline-block; 33 | padding: .1rem; 34 | 35 | .block-flights-days-slider-items-item-content { 36 | border: .1rem solid $color-blue-medium; 37 | border-radius: .3rem; 38 | display: block; 39 | font-size: .8rem; 40 | line-height: 1rem; 41 | padding: .7rem 0; 42 | text-align: center; 43 | width: 5.5rem; 44 | 45 | .block-flights-days-slider-items-item-content-price { 46 | color: $color-blue-medium; 47 | font-size: 1.5rem; 48 | margin-top: 1rem; 49 | } 50 | 51 | &.active { 52 | background-color: $color-blue-medium; 53 | color: $color-white; 54 | 55 | .block-flights-days-slider-items-item-content-price { 56 | color: $color-white; 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | @include media-breakpoint-between(md, lg) { 66 | .block-flights-days { 67 | 68 | .block-flights-days-slider { 69 | 70 | .block-flights-days-slider-items { 71 | .block-flights-days-slider-items-item { 72 | padding: .1rem; 73 | 74 | .block-flights-days-slider-items-item-content { 75 | border-radius: .3rem; 76 | font-size: .6rem; 77 | line-height: .8rem; 78 | padding: .5rem 0; 79 | width: 3.8rem; 80 | 81 | .block-flights-days-slider-items-item-content-price { 82 | font-size: 1rem; 83 | margin-top: .5rem; 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | @include media-breakpoint-between(lg, xl) { 93 | .block-flights-days { 94 | 95 | .block-flights-days-slider { 96 | 97 | .block-flights-days-slider-items { 98 | .block-flights-days-slider-items-item { 99 | padding: .1rem; 100 | 101 | .block-flights-days-slider-items-item-content { 102 | border-radius: .3rem; 103 | font-size: .8rem; 104 | line-height: 1rem; 105 | padding: .7rem 0; 106 | width: 5rem; 107 | 108 | .block-flights-days-slider-items-item-content-price { 109 | font-size: 1.3rem; 110 | margin-top: 1rem; 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | @include media-breakpoint-up(md) { 120 | .block-flights-days { 121 | 122 | .block-flights-days-slider { 123 | 124 | .block-flights-days-slider-items { 125 | display: flex; 126 | width: auto; 127 | } 128 | 129 | .block-flights-days-slider-arrow { 130 | display: block; 131 | } 132 | } 133 | } 134 | } 135 | 136 | @include media-breakpoint-up(xl) { 137 | .block-flights-days { 138 | 139 | .block-flights-days-slider { 140 | 141 | .block-flights-days-slider-items { 142 | .block-flights-days-slider-items-item { 143 | padding: .1rem; 144 | 145 | .block-flights-days-slider-items-item-content { 146 | border-radius: .3rem; 147 | font-size: .8rem; 148 | line-height: 1rem; 149 | padding: .7rem 0; 150 | width: 5.5rem; 151 | 152 | .block-flights-days-slider-items-item-content-price { 153 | font-size: 1.5rem; 154 | margin-top: 1rem; 155 | } 156 | } 157 | } 158 | } 159 | } 160 | } 161 | } 162 | 163 | -------------------------------------------------------------------------------- /scss/styles/_deals.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .block-deals { 4 | position: relative; 5 | 6 | .block-deals-title { 7 | color: $color-blue; 8 | font-size: 1.4rem; 9 | padding-bottom: .5rem; 10 | } 11 | 12 | .block-deals-list { 13 | list-style: none; 14 | padding: 0; 15 | 16 | .block-deals-list-item { 17 | border: .01rem solid $color-blue-medium; 18 | border-radius: .3rem; 19 | color: $color-grey-dark; 20 | display: block; 21 | margin-bottom: 1rem; 22 | padding: .5rem; 23 | 24 | .block-deals-list-item-col { 25 | 26 | .block-deals-list-item-col-price { 27 | color: $color-blue-medium; 28 | font-size: 2.5rem; 29 | font-weight: 500; 30 | line-height: 2.3rem; 31 | } 32 | 33 | .block-deals-list-item-col-date { 34 | font-size: .5rem; 35 | } 36 | 37 | .block-deals-list-item-col-button { 38 | font-size: .7rem; 39 | margin-top: .5rem; 40 | padding: .4rem 0; 41 | transition: none; 42 | width: 7rem; 43 | } 44 | } 45 | 46 | .block-deals-list-item-col--price { 47 | font-size: .7rem; 48 | } 49 | 50 | .block-deals-list-item-col--button { 51 | align-self: flex-end; 52 | } 53 | } 54 | } 55 | } 56 | 57 | @include media-breakpoint-up(sm) { 58 | .block-deals { 59 | 60 | .block-deals-list { 61 | 62 | .block-deals-list-item { 63 | 64 | .block-deals-list-item-col { 65 | 66 | .block-deals-list-item-col-price { 67 | font-size: 3rem; 68 | } 69 | 70 | .block-deals-list-item-col-date { 71 | font-size: .7rem; 72 | } 73 | 74 | .block-deals-list-item-col-button { 75 | font-size: .8rem; 76 | margin-top: 0; 77 | padding: .6rem 0; 78 | width: 100%; 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /scss/styles/_flights.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .block-flights { 4 | 5 | .block-flights-title { 6 | color: $color-blue-medium; 7 | font-size: 2rem; 8 | margin-bottom: 1.5rem; 9 | } 10 | 11 | .block-flights-title2 { 12 | color: $color-blue-medium; 13 | font-size: 1.5rem; 14 | margin-bottom: 1rem; 15 | } 16 | 17 | .block-flights-title3 { 18 | color: $color-grey-dark; 19 | font-size: 1.2rem; 20 | } 21 | 22 | .block-flights-options { 23 | display: none; 24 | 25 | .block-flights-options-title { 26 | color: $color-blue-medium; 27 | font-size: 1.3rem; 28 | } 29 | 30 | .block-flights-options-label { 31 | padding-left: 0; 32 | } 33 | 34 | } 35 | 36 | .block-flights-results { 37 | margin: 0 .5rem; 38 | padding-left: 0; 39 | 40 | .block-flights-results-list-item { 41 | border-bottom: .1rem solid $color-blue-light; 42 | cursor: pointer; 43 | padding: .5rem 0; 44 | 45 | a { 46 | color: $color-blue-medium; 47 | } 48 | 49 | .block-flights-results-list-item-flights { 50 | padding-left: 0; 51 | } 52 | 53 | &>label { 54 | color:inherit; 55 | cursor: inherit; 56 | } 57 | 58 | &:nth-child(1) { 59 | border-top: .1rem solid $color-blue-light; 60 | } 61 | 62 | .block-flights-results-list-item-price { 63 | color: $color-blue-medium; 64 | display: inline-block; 65 | font-size: 1.5rem; 66 | text-align: center; 67 | width: 4rem; 68 | 69 | input { 70 | display: block; 71 | margin: 0 auto; 72 | } 73 | } 74 | 75 | .block-flights-results-list-item-passengers { 76 | display: inline-block; 77 | font-size: 1.2rem; 78 | text-align: center; 79 | width: 4rem; 80 | } 81 | } 82 | } 83 | 84 | } 85 | 86 | @include media-breakpoint-up(md) { 87 | .block-flights { 88 | margin: 0; 89 | 90 | .block-flights-options { 91 | display: block; 92 | } 93 | } 94 | } 95 | 96 | .big-blue-radio { 97 | border: 1px solid $color-blue-light; 98 | border-radius: 100%; 99 | cursor: pointer; 100 | display: inline-block; 101 | height: 24px; 102 | position: relative; 103 | width: 24px; 104 | 105 | &.big-blue-radio--active { 106 | 107 | &::after { 108 | background-color: $color-blue-medium; 109 | border-radius: 100%; 110 | content: ''; 111 | display: inline-block; 112 | height: 16px; 113 | left: 3px; 114 | position: absolute; 115 | top: 3px; 116 | width: 16px; 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /scss/styles/_fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { font-family: "icons"; font-style: normal; font-weight: normal; src: local("icons"), url("data:font/woff;base64,d09GRgABAAAAAASwAAsAAAAAB5QAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADsAAABUIIslek9TLzIAAAFEAAAAPAAAAFYzpEBMY21hcAAAAYAAAABbAAABmjp67sBnbHlmAAAB3AAAAOgAAAGsOOij32hlYWQAAALEAAAALgAAADYPPX90aGhlYQAAAvQAAAAdAAAAJABuADxobXR4AAADFAAAABMAAAAUAKMAAGxvY2EAAAMoAAAADAAAAAwApAD2bWF4cAAAAzQAAAAfAAAAIAEgAGBuYW1lAAADVAAAAR0AAAHyFNvC+HBvc3QAAAR0AAAAOwAAAFQMc3yzeJxjYGRgYOBiMGCwY2BycfMJYeDLSSzJY5BiYGGAAJA8MpsxJzM9kYEDxgPKsYBpDiBmg4gCACY7BUgAeJxjYGRQZJzAwMrAwKDIoAIk2aG0BAMTAy8DkGBlZsAKAtJcUxgcXjG+YmEwAXJZGEzBwowgAgDEQQVheJztkbsNgDAMRJ+TgwIxCAVDMAQV+1dZIvjDGJz0zvLJcmEDC9Cd0xHYgxG6PbXMO1vm4sgZRT40p7uFe6/05rOxY+XXnn59neJWRV6vFUTtRXxhqEAvYl4PXwB4nIWQzU7DMBCEx+sNK2gh/IVESUtQI+FDVVWuCa7EIQcOHDhw4P3fhXVCEQgh1h57vh3Zkg2DVEv0yIAoLpaytHVtK6K6pgo/8l5TF6lpqNpNMS7GfK/5DCUatOjgsMYWARFPGPCMF7ziDe96fyi6PvShUEmna6esEnWltuVT8Zv/jzez/QkvbCde7ILFe+FUYqea6MDe+9STRO7uTO04i4P5ywM8vjPoOMaV/ocDBvO4a40crbamvLnOjbt/GCaMKRhRA7nMDNOciU3OlAzdJjhN7TwzZq46Z92ZTPsrab9OfwBafx6OeJxjYGRgYADijKjNmvH8Nl8ZuBlMgCIM1z8vZEemGSwYTIEkBwMTiAMAEiwI2gAAeJxjYGRgYDBhAAFzBob//xksGBgZUAErADsFAqkAAAB4nGNgAAIZKDYHYhMGBgAFCQCkAAAAAAAAEAAgAJQA1nicY2BkYGBgZQhhEGQAASYg5gJCBob/YD4DABKMAYEAeJxdjr1OwzAUhU/6h2gQAiExm6ULUvoz9gHamQ7Z08RJWyVx5LiVKjEz8xTMPAXPxYl7JSps6fo75x5fG8ADfhCgWwGGvnarhxuqC/dJd8ID8qPwECGehUdUL8JjvGIiHOIJb5wQDG7pjJEJ93CPWrhP/114QP4QHnL6p/CI/pfwGDG+hUNMgtE+NXW70cWxTKxnX2Jt272p1Tyaeb3WtbaJ05nanlV7KhbO5Sq3plIrUztdlkY11hx06qKdc81yOs3Fj1JTYY8Uhn9usYFGgSNKJLBX/h/FTFjvdFphjgizq/6a/dpnEjieGTNbnFlbnDh7Qdchp86ZMahIK3+3S5fchk7jewc6Kf0IO3+rwRJT7vxfPvKvV78w9VNiAAAAeJxjYGLAD1gZmRiZGVkYWRnZGLgSi4ryy3VzUtNKuCHMosz0jBKO5MSc1LyUxCKWnPz0fAYGAB7gDaYA") format("woff"); } -------------------------------------------------------------------------------- /scss/styles/_footer.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .footer { 4 | background-color: transparent; 5 | background-image: url('#{$assets-root}/b-footer.jpg'); 6 | background-position: center 85%; 7 | background-repeat: no-repeat; 8 | background-size: cover; 9 | padding-bottom: 4rem; 10 | padding-top: 3rem; 11 | 12 | .menu { 13 | display: none; 14 | 15 | h4 { 16 | font-size: .8rem; 17 | font-weight: 600; 18 | } 19 | 20 | ul { 21 | li { 22 | a { 23 | color: $color-black; 24 | } 25 | } 26 | } 27 | } 28 | 29 | &-ms { 30 | height: auto; 31 | margin-bottom: 1rem; 32 | margin-top: 1rem; 33 | width: 10rem; 34 | } 35 | 36 | &-copyright { 37 | font-size: .7rem; 38 | font-weight: 600; 39 | margin-top: .8rem; 40 | } 41 | } 42 | 43 | .route, 44 | .route-barcelona { 45 | 46 | .footer { 47 | background-image: none; 48 | margin-top: -3rem; 49 | padding-bottom: 5rem; 50 | } 51 | } 52 | 53 | @include media-breakpoint-up(sm) { 54 | .footer { 55 | padding-bottom: 10rem; 56 | 57 | .menu { 58 | display: flex; 59 | } 60 | } 61 | } 62 | 63 | @include media-breakpoint-up(xl) { 64 | .footer { 65 | 66 | .footer--col--1 { 67 | flex: 0 0 38%; 68 | max-width: 38%; 69 | } 70 | 71 | .footer--col--2 { 72 | flex: 0 0 38%; 73 | max-width: 38%; 74 | } 75 | 76 | .footer--col--3 { 77 | flex: 0 0 24%; 78 | max-width: 24%; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /scss/styles/_icons.scss: -------------------------------------------------------------------------------- 1 | // sass-lint:disable-all 2 | 3 | .icon { 4 | display: inline-block; 5 | font: normal normal normal 14px/1 icons; 6 | font-size: inherit; 7 | text-rendering: auto; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | } 11 | 12 | .icon-lg { 13 | font-size: 1.3333333333333333em; 14 | line-height: 0.75em; 15 | vertical-align: -15%; 16 | } 17 | .icon-2x { font-size: 2em; } 18 | .icon-3x { font-size: 3em; } 19 | .icon-4x { font-size: 4em; } 20 | .icon-5x { font-size: 5em; } 21 | .icon-fw { 22 | width: 1.2857142857142858em; 23 | text-align: center; 24 | } 25 | 26 | .icon-arrow-left:before { content: "\EA01" } 27 | .icon-arrow-right:before { content: "\EA02" } 28 | .icon-calendar:before { content: "\EA03" } 29 | .icon-logo:before { content: "\EA04" } -------------------------------------------------------------------------------- /scss/styles/_loading.scss: -------------------------------------------------------------------------------- 1 | $t: 4000ms; 2 | $d: 40px; 3 | $r: -14deg; 4 | $m: 30; 5 | $color: #4495d1; 6 | 7 | .loading-wrapper { 8 | background-color: $color-white-alpha-9; 9 | height: 100%; 10 | left: 0; 11 | position: absolute; 12 | top: 0; 13 | width: 100%; 14 | z-index: 10; 15 | 16 | &.loading-fadeout { 17 | opacity: 0; 18 | transition: opacity .5s ease-in-out, width 1s ease-in-out; 19 | } 20 | 21 | .progress-ring { 22 | height: $d; 23 | left: 50%; 24 | padding-top: $d / 5; 25 | position: relative; 26 | top: 50%; 27 | transform: translate(-50%, -50%); 28 | width: $d; 29 | 30 | .progress-ring-wrap { 31 | height: $d - 2; 32 | position: absolute; 33 | width: $d - 2; 34 | 35 | .progress-ring-circle { 36 | animation-duration: $t; 37 | animation-iteration-count: infinite; 38 | animation-name: orbit; 39 | height: $d - 2; 40 | opacity: 0; 41 | transform: rotate(225deg); 42 | width: $d - 2; 43 | 44 | &::after { 45 | background: $color; 46 | border-radius: $d / 8; 47 | box-shadow: 0 0 5% $color; 48 | content: ''; 49 | height: $d / 8; 50 | position: absolute; 51 | width: $d / 8; 52 | } 53 | } 54 | 55 | &:nth-child(2) { 56 | transform: rotate($r); 57 | 58 | .progress-ring-circle { 59 | animation-delay: $t / $m; 60 | } 61 | } 62 | 63 | &:nth-child(3) { 64 | transform: rotate($r * 2); 65 | 66 | .progress-ring-circle { 67 | animation-delay: $t / $m * 2; 68 | } 69 | } 70 | 71 | &:nth-child(4) { 72 | transform: rotate($r * 3); 73 | 74 | .progress-ring-circle { 75 | animation-delay: $t / $m * 3; 76 | } 77 | } 78 | 79 | &:nth-child(5) { 80 | transform: rotate($r * 4); 81 | 82 | .progress-ring-circle { 83 | animation-delay: $t / $m * 4; 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | @keyframes orbit { 91 | 0% { 92 | animation-timing-function: ease-out; 93 | opacity: 1; 94 | transform: rotate(225deg); 95 | } 96 | 97 | 7% { 98 | animation-timing-function: linear; 99 | transform: rotate(345deg); 100 | } 101 | 102 | 35% { 103 | animation-timing-function: ease-out; 104 | transform: rotate(495deg); 105 | } 106 | 107 | 42% { 108 | animation-timing-function: linear; 109 | transform: rotate(690deg); 110 | } 111 | 112 | 70% { 113 | animation-timing-function: linear; 114 | opacity: 1; 115 | transform: rotate(835deg); 116 | } 117 | 118 | 76% { 119 | opacity: 1; 120 | } 121 | 122 | 77% { 123 | animation-timing-function: ease-in; 124 | transform: rotate(955deg); 125 | } 126 | 127 | 78% { 128 | opacity: 0; 129 | transform: rotate(955deg); 130 | } 131 | 132 | 100% { 133 | opacity: 0; 134 | transform: rotate(955deg); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /scss/styles/_navbar.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .block-navbar { 4 | background: $color-white; 5 | box-shadow: 0 .2rem .5rem 0 $color-black-alpha-2; 6 | color: $color-grey; 7 | font-size: .9rem; 8 | height: 5.5rem; 9 | 10 | .block-navbar-username { 11 | font-size: .8rem; 12 | position: absolute; 13 | right: .5rem; 14 | top: -.5rem; 15 | z-index: 10; 16 | 17 | &--name { 18 | display: none; 19 | } 20 | 21 | &--logout { 22 | 23 | a { 24 | color: $color-blue-medium; 25 | 26 | &:hover { 27 | color: $color-blue-medium; 28 | } 29 | } 30 | } 31 | } 32 | 33 | .block-navbar-left { 34 | text-align: center; 35 | 36 | .block-navbar-left-logo { 37 | height: auto; 38 | width: 11rem; 39 | } 40 | } 41 | 42 | .block-navbar-right { 43 | justify-content: center; 44 | 45 | .block-navbar-right-menu { 46 | flex-direction: row; 47 | justify-content: center; 48 | 49 | .block-navbar-right-menu-item { 50 | margin: 0 .3rem; 51 | 52 | &__logout { 53 | display: none; 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | @include media-breakpoint-up(md) { 61 | .block-navbar { 62 | font-size: 1.1rem; 63 | height: 4.5rem; 64 | padding-top: .7rem; 65 | 66 | .block-navbar-username { 67 | left: auto; 68 | right: 1.7rem; 69 | top: -.5rem; 70 | transform: none; 71 | 72 | &--name { 73 | color: $color-black; 74 | display: inline-block; 75 | } 76 | 77 | &--logout { 78 | display: none; 79 | } 80 | } 81 | 82 | .block-navbar-left { 83 | text-align: left; 84 | 85 | .block-navbar-left-logo { 86 | height: auto; 87 | width: 14rem; 88 | } 89 | } 90 | 91 | .block-navbar-right { 92 | justify-content: flex-end; 93 | 94 | .block-navbar-right-menu { 95 | justify-content: flex-end; 96 | 97 | .block-navbar-right-menu-item { 98 | margin: 0 .5rem; 99 | 100 | &__logout { 101 | cursor: pointer; 102 | display: inline-block; 103 | font-size: .9rem; 104 | line-height: 2.05rem; 105 | overflow: hidden; 106 | text-overflow: ellipsis; 107 | white-space: nowrap; 108 | width: 5rem; 109 | 110 | a { 111 | color: $color-blue-medium; 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | @include media-breakpoint-up(xl) { 121 | .block-navbar { 122 | 123 | .block-navbar-right { 124 | justify-content: flex-end; 125 | 126 | .block-navbar-right-menu { 127 | 128 | .block-navbar-right-menu-item { 129 | font-size: 1.2rem; 130 | margin-left: 1.2rem; 131 | margin-right: 1.2rem; 132 | 133 | &__logout { 134 | font-size: 1rem; 135 | line-height: 2.1rem; 136 | } 137 | } 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /scss/styles/_overrides.scss: -------------------------------------------------------------------------------- 1 | $primary: $color-red -------------------------------------------------------------------------------- /scss/styles/_search.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .block-search { 4 | background-color: $color-grey-light; 5 | background-image: url('#{$assets-root}/b-home.jpg'); 6 | background-position: center bottom; 7 | background-repeat: repeat-x; 8 | 9 | padding: 2rem 0 4rem; 10 | position: relative; 11 | 12 | .loading-wrapper { 13 | left: 5%; 14 | width: 90%; 15 | } 16 | 17 | .block-search-row { 18 | position: relative; 19 | 20 | .block-search-row-logo { 21 | color: $color-blue-alpha-2; 22 | font-size: 8rem; 23 | left: 40%; 24 | line-height: 7rem; 25 | position: absolute; 26 | top: 0; 27 | } 28 | } 29 | 30 | .block-search-message { 31 | border-bottom: .4rem solid $color-white; 32 | color: $color-blue; 33 | display: inline-block; 34 | margin-bottom: 2rem; 35 | position: relative; 36 | 37 | .block-search-message-title { 38 | font-size: 2rem; 39 | font-weight: 400; 40 | line-height: 2.2rem; 41 | } 42 | 43 | &--video { 44 | border-bottom: 0; 45 | width: 100%; 46 | } 47 | } 48 | 49 | .block-search-form { 50 | 51 | .block-search-form-title { 52 | color: $color-blue-medium; 53 | font-size: 1.4rem; 54 | margin-bottom: .9rem; 55 | } 56 | 57 | .block-search-form-options { 58 | display: flex; 59 | flex-direction: column; 60 | margin-bottom: .5rem; 61 | 62 | .block-search-form-options-option { 63 | border: 1px $color-grey solid; 64 | border-radius: 5px; 65 | color: $color-grey; 66 | display: inline-block; 67 | margin-bottom: .5rem; 68 | padding: .2rem 0; 69 | text-align: center; 70 | width: 100%; 71 | } 72 | 73 | .block-search-form-options-option--active { 74 | border-color: $color-blue-medium; 75 | color: $color-blue-medium; 76 | } 77 | } 78 | 79 | .block-search-form-date { 80 | position: relative; 81 | 82 | .block-search-form-date-button { 83 | bottom: 0; 84 | cursor: pointer; 85 | display: block; 86 | font-size: 1.2rem; 87 | position: absolute; 88 | right: .6rem; 89 | } 90 | } 91 | 92 | .block-search-form-button { 93 | width: 100%; 94 | } 95 | } 96 | } 97 | 98 | .block-search--city { 99 | background-image: url('#{$assets-root}/b-barcelona.jpg'); 100 | background-position: center center; 101 | 102 | .block-search-row { 103 | 104 | .block-search-row-logo { 105 | color: $color-white-alpha-2; 106 | } 107 | } 108 | 109 | .block-search-message { 110 | border-color: $color-blue; 111 | color: $color-white; 112 | } 113 | } 114 | 115 | @include media-breakpoint-up(sm) { 116 | .block-search { 117 | 118 | .block-search-form { 119 | 120 | .block-search-form-options { 121 | flex-direction: row; 122 | justify-content: space-between; 123 | 124 | .block-search-form-options-option { 125 | width: 32%; 126 | } 127 | } 128 | } 129 | } 130 | } 131 | 132 | @include media-breakpoint-up(md) { 133 | .block-search { 134 | background-size: cover; 135 | padding: 6rem 0 8rem; 136 | 137 | .block-search-message { 138 | 139 | .block-search-message-title { 140 | font-size: 3.2rem; 141 | line-height: 3.4rem; 142 | } 143 | } 144 | 145 | .block-search-row { 146 | 147 | .block-search-row-logo { 148 | font-size: 20rem; 149 | left: 25%; 150 | line-height: 20rem; 151 | top: 20%; 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /scss/styles/_seats.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .block-seats { 4 | 5 | .col-seats { 6 | display: none; 7 | } 8 | 9 | .row-seats { 10 | margin-top: 1rem; 11 | 12 | button { 13 | margin-left: .5rem; 14 | } 15 | } 16 | 17 | .block-seats-flight { 18 | 19 | .block-seats-flight-headings { 20 | color: $color-blue-medium; 21 | font-size: 1rem; 22 | 23 | .block-seats-flight-headings-title { 24 | font-size: 1rem; 25 | } 26 | } 27 | 28 | .icon-seat { 29 | background-color: $color-white; 30 | border-color: $color-blue-medium; 31 | border-radius: .2rem; 32 | border-style: solid; 33 | border-width: 1px; 34 | display: inline-block; 35 | height: 1.4rem; 36 | overflow: hidden; 37 | width: 1.2rem; 38 | 39 | &::after { 40 | border-color: $color-blue-medium; 41 | border-radius: 3rem; 42 | border-style: solid; 43 | border-width: 1px; 44 | content: ''; 45 | display: block; 46 | height: 3rem; 47 | transform: translate(-.9rem, .85rem); 48 | width: 3rem; 49 | } 50 | } 51 | 52 | .icon-seat-sm { 53 | height: 1.5rem; 54 | width: 1.3rem; 55 | 56 | &::after { 57 | height: 3rem; 58 | transform: translate(-.9rem, .85rem); 59 | width: 3rem; 60 | } 61 | } 62 | 63 | .icon-seat-active { 64 | background-color: $color-red; 65 | } 66 | 67 | .icon-seat-available { 68 | cursor: pointer; 69 | } 70 | 71 | .icon-seat-unavailable { 72 | background-color: $color-grey; 73 | } 74 | 75 | .icon-seat-preferred { 76 | background-color: $color-red; 77 | } 78 | 79 | .icon-seat-company { 80 | background-color: $color-blue-light; 81 | } 82 | 83 | .block-seats-flight-select { 84 | background-color: $color-blue-light; 85 | background-image: url('#{$assets-root}/plane.png'); 86 | background-position: 51.5% 43.5%; 87 | background-repeat: no-repeat; 88 | background-size: 270% 500%; 89 | height: 16.7rem; 90 | margin-top: 2rem; 91 | overflow-y: scroll; 92 | position: relative; 93 | 94 | .block-seats-flight-select-legend { 95 | background: $color-white; 96 | box-shadow: 0 0 5px $color-black-alpha-2; 97 | display: none; 98 | font-size: .7rem; 99 | padding: 1rem; 100 | position: absolute; 101 | right: 1rem; 102 | top: 1rem; 103 | 104 | .block-seats-flight-select-legend-items { 105 | 106 | .block-seats-flight-select-legend-items-item { 107 | line-height: 1.5rem; 108 | vertical-align: middle; 109 | 110 | .icon-seat { 111 | margin-right: .3rem; 112 | position: relative; 113 | top: .5rem; 114 | } 115 | } 116 | } 117 | } 118 | 119 | .block-seats-flight-select-seats { 120 | color: $color-white; 121 | height: 40rem; 122 | margin: 0 auto; 123 | position: relative; 124 | text-align: center; 125 | 126 | .column { 127 | display: inline-block; 128 | vertical-align: middle; 129 | width: 8%; 130 | } 131 | 132 | .column-letter { 133 | width: 7.5%; 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | @include media-breakpoint-up(sm) { 141 | .block-seats { 142 | 143 | .col-seats { 144 | display: block; 145 | } 146 | 147 | .row-seats { 148 | margin-top: 0; 149 | 150 | button { 151 | display: block; 152 | margin-left: 0; 153 | } 154 | } 155 | 156 | .block-seats-flight { 157 | 158 | .icon-seat { 159 | height: 1.8rem; 160 | width: 1.6rem; 161 | 162 | &::after { 163 | border-radius: 3rem; 164 | height: 5rem; 165 | transform: translate(-1.75rem, 1rem); 166 | width: 5rem; 167 | } 168 | } 169 | 170 | .icon-seat-sm { 171 | height: 1.5rem; 172 | width: 1.3rem; 173 | 174 | &::after { 175 | height: 3rem; 176 | transform: translate(-.9rem, .85rem); 177 | width: 3rem; 178 | } 179 | } 180 | 181 | .block-seats-flight-select { 182 | height: 20rem; 183 | 184 | .block-seats-flight-select-seats { 185 | height: 40rem; 186 | width: 20rem; 187 | 188 | .column { 189 | width: 13%; 190 | } 191 | 192 | .column-letter { 193 | width: 12.4%; 194 | } 195 | } 196 | } 197 | } 198 | } 199 | } 200 | 201 | @include media-breakpoint-between(sm, md) { 202 | .block-seats { 203 | 204 | .block-seats-flight { 205 | 206 | .block-seats-flight-select { 207 | background-position: 51.5% 43%; 208 | background-size: 240%; 209 | height: 20rem; 210 | } 211 | } 212 | } 213 | } 214 | 215 | @include media-breakpoint-up(md) { 216 | .block-seats { 217 | 218 | .block-seats-flight { 219 | 220 | .block-seats-flight-headings { 221 | font-size: 1.2rem; 222 | 223 | .block-seats-flight-headings-title { 224 | font-size: 1.5rem; 225 | } 226 | } 227 | } 228 | } 229 | } 230 | 231 | @include media-breakpoint-between(md, lg) { 232 | .block-seats { 233 | 234 | .block-seats-flight { 235 | 236 | .block-seats-flight-select { 237 | background-position: 58.7% 43%; 238 | background-size: 185%; 239 | height: 20.7rem; 240 | 241 | .block-seats-flight-select-seats { 242 | margin-left: 20%; 243 | } 244 | 245 | .block-seats-flight-select-legend { 246 | display: block; 247 | width: 8rem; 248 | 249 | .block-seats-flight-select-legend-title { 250 | color: $color-blue-medium; 251 | font-size: 1.2rem; 252 | } 253 | } 254 | } 255 | } 256 | } 257 | } 258 | 259 | @include media-breakpoint-between(lg, xl) { 260 | .block-seats { 261 | 262 | .block-seats-flight { 263 | 264 | .block-seats-flight-select { 265 | background-position: 74% 43%; 266 | background-size: 135%; 267 | height: 20.7rem; 268 | 269 | .block-seats-flight-select-seats { 270 | margin-left: 25%; 271 | } 272 | 273 | .block-seats-flight-select-legend { 274 | display: block; 275 | width: 17rem; 276 | 277 | .block-seats-flight-select-legend-title { 278 | font-size: 1.5rem; 279 | } 280 | } 281 | } 282 | } 283 | } 284 | } 285 | 286 | @include media-breakpoint-up(xl) { 287 | .block-seats { 288 | 289 | .block-seats-flight { 290 | 291 | .block-seats-flight-select { 292 | background-position: 107% 43%; 293 | background-size: 115%; 294 | height: 20.7rem; 295 | 296 | .block-seats-flight-select-seats { 297 | margin-left: 27.6%; 298 | } 299 | } 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /scss/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $color-white: #fff; 2 | $color-red: rgb(223, 29, 89); 3 | $color-blue: rgb(68, 149, 209); 4 | $color-blue-medium: rgb(40, 170, 225); 5 | $color-grey: #ccc; 6 | $color-grey-light: #eee; 7 | $color-grey-medium: rgb(87, 89, 94); 8 | $color-grey-dark: rgb(51, 51, 51); 9 | $color-blue-light: #cde7f0; 10 | $color-black: #000; 11 | $color-transparent: rgba(255, 255, 255, 0); 12 | 13 | $color-black-alpha-2: rgba($color-black, .2); 14 | $color-blue-alpha-2: rgba($color-blue-medium, .2); 15 | $color-white-alpha-2: rgba($color-white, .2); 16 | $color-white-alpha-5: rgba($color-white, .5); 17 | $color-white-alpha-7: rgba($color-white, .7); 18 | $color-white-alpha-8: rgba($color-white, .8); 19 | $color-white-alpha-9: rgba($color-white, .9); 20 | 21 | $assets-root: '/assets'; -------------------------------------------------------------------------------- /scss/styles/_vendor.scss: -------------------------------------------------------------------------------- 1 | @import 'overrides'; 2 | 3 | /*! 4 | * Bootstrap v4.1.3 (https://getbootstrap.com/) 5 | * Copyright 2011-2018 The Bootstrap Authors 6 | * Copyright 2011-2018 Twitter, Inc. 7 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 8 | */ 9 | 10 | @import '~bootstrap/scss/functions'; 11 | @import '~bootstrap/scss/variables'; 12 | @import '~bootstrap/scss/mixins'; 13 | @import '~bootstrap/scss/root'; 14 | @import '~bootstrap/scss/reboot'; 15 | @import '~bootstrap/scss/type'; 16 | @import '~bootstrap/scss/images'; 17 | @import '~bootstrap/scss/code'; 18 | @import '~bootstrap/scss/grid'; 19 | @import '~bootstrap/scss/tables'; 20 | @import '~bootstrap/scss/forms'; 21 | @import '~bootstrap/scss/buttons'; 22 | @import '~bootstrap/scss/transitions'; 23 | @import '~bootstrap/scss/dropdown'; 24 | @import '~bootstrap/scss/button-group'; 25 | @import '~bootstrap/scss/input-group'; 26 | @import '~bootstrap/scss/custom-forms'; 27 | @import '~bootstrap/scss/nav'; 28 | @import '~bootstrap/scss/navbar'; 29 | @import '~bootstrap/scss/card'; 30 | @import '~bootstrap/scss/breadcrumb'; 31 | @import '~bootstrap/scss/pagination'; 32 | @import '~bootstrap/scss/badge'; 33 | @import '~bootstrap/scss/jumbotron'; 34 | @import '~bootstrap/scss/alert'; 35 | @import '~bootstrap/scss/progress'; 36 | @import '~bootstrap/scss/media'; 37 | @import '~bootstrap/scss/list-group'; 38 | @import '~bootstrap/scss/close'; 39 | @import '~bootstrap/scss/modal'; 40 | @import '~bootstrap/scss/tooltip'; 41 | @import '~bootstrap/scss/popover'; 42 | @import '~bootstrap/scss/carousel'; 43 | @import '~bootstrap/scss/utilities'; 44 | @import '~bootstrap/scss/print'; 45 | 46 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const app = require('./app'); 8 | const debug = require('debug')('contoso-air-dev:server'); 9 | const http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | const port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | const server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | const port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | const bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | const addr = server.address(); 86 | const bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /src/config/i18n.config.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n'); 2 | 3 | const configureI18N = (root_path) => { 4 | i18n.configure({ 5 | objectNotation: true, 6 | locales: ['en', 'es'], 7 | directory: root_path + '/locales' 8 | }); 9 | 10 | return i18n; 11 | } 12 | 13 | module.exports = configureI18N; -------------------------------------------------------------------------------- /src/config/passport.config.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const LocalStrategy = require('passport-local').Strategy; 3 | 4 | passport.use( 5 | new LocalStrategy({ 6 | usernameField: 'username', 7 | passwordField: 'password', 8 | session: true, 9 | passReqToCallback: true 10 | }, 11 | function(req, username, password, done) { 12 | if (!username) { 13 | req.flash('error', "Login.Error.MissingCredentials"); 14 | return done(null, false); 15 | } 16 | 17 | return done(null, { 18 | name: username 19 | }); 20 | } 21 | ) 22 | ); 23 | 24 | passport.serializeUser(function(user, done) { 25 | done(null, user.name); 26 | }); 27 | 28 | passport.deserializeUser(function(name, done) { 29 | done(null, { name }); 30 | }); 31 | 32 | module.exports = passport; -------------------------------------------------------------------------------- /src/data/deals.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "0", 4 | "fromName": "Seattle", 5 | "fromCode": "SEA", 6 | "toName": "Hawaii", 7 | "toCode": "HNL", 8 | "price": "1100", 9 | "departTime": "2018-03-04T08:45:00Z", 10 | "arrivalTime": "2018-03-05T14:55:00Z", 11 | "hours": "10h 30m", 12 | "stops": "1", 13 | "since": "2018-03-04T08:45:00Z" 14 | }, 15 | { 16 | "id": "1", 17 | "fromName": "Seattle", 18 | "fromCode": "SEA", 19 | "toName": "Paris", 20 | "toCode": "CDG", 21 | "price": "750", 22 | "departTime": "2018-03-04T08:45:00Z", 23 | "arrivalTime": "2018-03-05T12:20:00Z", 24 | "hours": "09h 15m", 25 | "stops": "1", 26 | "since": "2018-03-04T08:45:00Z" 27 | }, 28 | { 29 | "id": "2", 30 | "fromName": "Seattle", 31 | "fromCode": "SEA", 32 | "toName": "Tokyo", 33 | "toCode": "NAR", 34 | "price": "1070", 35 | "departTime": "2018-03-04T08:45:00Z", 36 | "arrivalTime": "2018-03-05T14:25:00Z", 37 | "hours": "10h 0m", 38 | "stops": "1", 39 | "since": "2018-03-04T08:45:00Z" 40 | }, 41 | { 42 | "id": "3", 43 | "fromName": "Seattle", 44 | "fromCode": "SEA", 45 | "toName": "Barcelona", 46 | "toCode": "BCN", 47 | "price": "750", 48 | "departTime": "2018-03-04T08:45:00Z", 49 | "arrivalTime": "2018-03-05T14:25:00Z", 50 | "hours": "11h 20m", 51 | "stops": "1", 52 | "since": "2018-03-04T08:45:00Z" 53 | }, 54 | { 55 | "id": "4", 56 | "fromName": "Seattle", 57 | "fromCode": "SEA", 58 | "toName": "Barcelona", 59 | "toCode": "BCN", 60 | "price": "479", 61 | "departTime": null, 62 | "arrivalTime": null, 63 | "hours": null, 64 | "stops": null, 65 | "since": "2018-02-13T00:00:01Z" 66 | }, 67 | { 68 | "id": "5", 69 | "fromName": "Seattle", 70 | "fromCode": "SEA", 71 | "toName": "Tokyo", 72 | "toCode": "NAR", 73 | "price": "535", 74 | "departTime": null, 75 | "arrivalTime": null, 76 | "hours": null, 77 | "stops": null, 78 | "since": "2017-02-13T00:00:01Z" 79 | }, 80 | { 81 | "id": "6", 82 | "fromName": "Seattle", 83 | "fromCode": "SEA", 84 | "toName": "London", 85 | "toCode": "LHR", 86 | "price": "626", 87 | "departTime": null, 88 | "arrivalTime": null, 89 | "hours": null, 90 | "stops": null, 91 | "since": "2017-02-13T00:00:01Z" 92 | }, 93 | { 94 | "id": "7", 95 | "fromName": "Seattle", 96 | "fromCode": "SEA", 97 | "toName": "Singapore", 98 | "toCode": "SIN", 99 | "price": "528", 100 | "departTime": null, 101 | "arrivalTime": null, 102 | "hours": null, 103 | "stops": null, 104 | "since": "2017-02-13T00:00:01Z" 105 | } 106 | ] 107 | -------------------------------------------------------------------------------- /src/data/destinations.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 0, 4 | "title": "Hawaii", 5 | "desktop-image": "hawaii.jpg", 6 | "mobile-image": "hawaii_m.jpg" 7 | }, 8 | { 9 | "id": 2, 10 | "title": "Paris", 11 | "desktop-image": "paris.jpg", 12 | "mobile-image": "paris_m.jpg" 13 | }, 14 | { 15 | "id": 3, 16 | "title": "Barcelona", 17 | "desktop-image": "barcelona.jpg", 18 | "mobile-image": "barcelona_m.jpg" 19 | } 20 | ] -------------------------------------------------------------------------------- /src/data/flights.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "0", 4 | "duration": "13h 22m", 5 | "price": "479", 6 | "fromCode": "FROMCODE", 7 | "toCode": "TOCODE", 8 | "distance": "3750", 9 | "segments": [ 10 | { 11 | "flight": "935", 12 | "fromCode": "FROMCODE", 13 | "departTime": "2017-03-04T08:05:00Z", 14 | "toCode": "CDG", 15 | "arrivalTime": "2017-03-04T12:15:00Z" 16 | }, 17 | { 18 | "flight": "851", 19 | "fromCode": "CDG", 20 | "departTime": "2017-03-04T15:00:00Z", 21 | "toCode": "TOCODE", 22 | "arrivalTime": "2017-03-05T08:00:00Z" 23 | } 24 | ] 25 | }, 26 | { 27 | "id": "1", 28 | "duration": "10h 15m", 29 | "price": "543", 30 | "fromCode": "FROMCODE", 31 | "toCode": "TOCODE", 32 | "distance": "3750", 33 | "segments": [ 34 | { 35 | "flight": "335", 36 | "fromCode": "FROMCODE", 37 | "departTime": "2017-03-04T09:05:00Z", 38 | "toCode": "JFK", 39 | "arrivalTime": "2017-03-04T14:15:00Z" 40 | }, 41 | { 42 | "flight": "251", 43 | "fromCode": "JFK", 44 | "departTime": "2017-03-04T17:00:00Z", 45 | "toCode": "TOCODE", 46 | "arrivalTime": "2017-03-05T09:00:00Z" 47 | } 48 | ] 49 | }, 50 | { 51 | "id": "2", 52 | "duration": "08h 15m", 53 | "price": "498", 54 | "fromCode": "FROMCODE", 55 | "toCode": "TOCODE", 56 | "distance": "4150", 57 | "segments": [ 58 | { 59 | "flight": "125", 60 | "fromCode": "FROMCODE", 61 | "departTime": "2017-03-04T08:05:00Z", 62 | "toCode": "TOR", 63 | "arrivalTime": "2017-03-04T13:15:00Z" 64 | }, 65 | { 66 | "flight": "121", 67 | "fromCode": "TOR", 68 | "departTime": "2017-03-04T15:00:00Z", 69 | "toCode": "TOCODE", 70 | "arrivalTime": "2017-03-05T08:00:00Z" 71 | } 72 | ] 73 | }, 74 | { 75 | "id": "3", 76 | "duration": "09h 45m", 77 | "price": "567", 78 | "fromCode": "FROMCODE", 79 | "toCode": "TOCODE", 80 | "distance": "3750", 81 | "segments": [ 82 | { 83 | "flight": "11", 84 | "fromCode": "FROMCODE", 85 | "departTime": "2017-03-04T08:05:00Z", 86 | "toCode": "BOS", 87 | "arrivalTime": "2017-03-04T13:15:00Z" 88 | }, 89 | { 90 | "flight": "851", 91 | "fromCode": "BOS", 92 | "departTime": "2017-03-04T15:00:00Z", 93 | "toCode": "TOCODE", 94 | "arrivalTime": "2017-03-05T08:00:00Z" 95 | } 96 | ] 97 | }, 98 | { 99 | "id": "4", 100 | "duration": "11h 20m", 101 | "price": "608", 102 | "fromCode": "FROMCODE", 103 | "toCode": "TOCODE", 104 | "distance": "4150", 105 | "segments": [ 106 | { 107 | "flight": "233", 108 | "fromCode": "FROMCODE", 109 | "departTime": "2017-03-04T08:05:00Z", 110 | "toCode": "JFK", 111 | "arrivalTime": "2017-03-04T14:15:00Z" 112 | }, 113 | { 114 | "flight": "844", 115 | "fromCode": "JFK", 116 | "departTime": "2017-03-04T15:00:00Z", 117 | "toCode": "TOCODE", 118 | "arrivalTime": "2017-03-05T08:00:00Z" 119 | } 120 | ] 121 | }, 122 | { 123 | "id": "5", 124 | "duration": "11h 25m", 125 | "price": "577", 126 | "fromCode": "FROMCODE", 127 | "toCode": "TOCODE", 128 | "distance": "3750", 129 | "segments": [ 130 | { 131 | "flight": "935", 132 | "fromCode": "FROMCODE", 133 | "departTime": "2017-03-04T08:05:00Z", 134 | "toCode": "JFK", 135 | "arrivalTime": "2017-03-04T13:15:00Z" 136 | }, 137 | { 138 | "flight": "851", 139 | "fromCode": "JFK", 140 | "departTime": "2017-03-04T15:00:00Z", 141 | "toCode": "TOCODE", 142 | "arrivalTime": "2017-03-05T08:00:00Z" 143 | } 144 | ] 145 | }, 146 | { 147 | "id": "6", 148 | "duration": "15h 45m", 149 | "price": "560", 150 | "fromCode": "FROMCODE", 151 | "toCode": "TOCODE", 152 | "distance": "3450", 153 | "segments": [ 154 | { 155 | "flight": "698", 156 | "fromCode": "FROMCODE", 157 | "departTime": "2017-03-04T02:30:00Z", 158 | "toCode": "FRA", 159 | "arrivalTime": "2017-03-04T04:40:00Z" 160 | }, 161 | { 162 | "flight": "851", 163 | "fromCode": "FRA", 164 | "departTime": "2017-03-04T07:30:00Z", 165 | "toCode": "TOCODE", 166 | "arrivalTime": "2017-03-04T09:15:00Z" 167 | } 168 | ] 169 | }, 170 | { 171 | "id": "7", 172 | "duration": "10h 15m", 173 | "price": "701", 174 | "fromCode": "FROMCODE", 175 | "toCode": "TOCODE", 176 | "distance": "3450", 177 | "segments": [ 178 | { 179 | "flight": "335", 180 | "fromCode": "FROMCODE", 181 | "departTime": "2017-03-04T09:05:00Z", 182 | "toCode": "FRA", 183 | "arrivalTime": "2017-03-04T14:15:00Z" 184 | }, 185 | { 186 | "flight": "251", 187 | "fromCode": "FRA", 188 | "departTime": "2017-03-04T17:00:00Z", 189 | "toCode": "TOCODE", 190 | "arrivalTime": "2017-03-04T09:00:00Z" 191 | } 192 | ] 193 | } 194 | ] 195 | -------------------------------------------------------------------------------- /src/repositories/book.repository.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const UserInfoModelSchema = require('./book.repository.model'); 3 | 4 | class BookFlightsRepository { 5 | constructor(options) { 6 | let { cosmosdb_name, cosmosdb_key, cosmosdb_url, database_name } = options; 7 | cosmosdb_url = cosmosdb_url || `${cosmosdb_name}.documents.azure.com:10255`; 8 | database_name = database_name || 'admin'; 9 | const connectionString = `mongodb://${cosmosdb_name}:${encodeURIComponent(cosmosdb_key)}@${cosmosdb_url}/${database_name}?ssl=true&replicaSet=globaldb`; 10 | mongoose.connect(connectionString, { useNewUrlParser: true }); 11 | mongoose.Promise = global.Promise; 12 | } 13 | async getUserInfo(username) { 14 | const UserInfoModel = await mongoose.model('UserInfoModel', UserInfoModelSchema); 15 | const result = await UserInfoModel.findOne({ user: username }).lean().exec(); 16 | return result || { user: username, booked: null, purchased: [] }; 17 | } 18 | 19 | async createOrUpdateUserInfo(userInfo) { 20 | const UserInfoModel = await mongoose.model('UserInfoModel', UserInfoModelSchema); 21 | await UserInfoModel.findOneAndUpdate({ user: userInfo.user }, userInfo, { upsert: true }); 22 | } 23 | } 24 | 25 | module.exports = BookFlightsRepository; -------------------------------------------------------------------------------- /src/repositories/book.repository.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const SegmentObject = { 5 | flight: String, 6 | fromCode: String, 7 | toCode: String, 8 | seats: [String], 9 | departTime: String, 10 | arrivalTime: String 11 | }; 12 | 13 | const FlightObject = { 14 | duration: String, 15 | price: Number, 16 | segments: [SegmentObject] 17 | }; 18 | 19 | const TravelObject = { 20 | id: String, 21 | passengers: Number, 22 | parting: FlightObject, 23 | returning: FlightObject 24 | }; 25 | 26 | const UserInfoModelSchema = new Schema({ 27 | user: String, 28 | booked: TravelObject, 29 | purchased: [TravelObject] 30 | }); 31 | 32 | module.exports = UserInfoModelSchema; -------------------------------------------------------------------------------- /src/repositories/flights.repository.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | const segmentsParser = function(fromCode, toCode, diff) { 4 | return s => ({ 5 | flight: s.flight, 6 | fromCode: s.fromCode == 'FROMCODE' ? fromCode : s.fromCode, 7 | toCode: s.toCode == 'TOCODE' ? toCode : s.toCode, 8 | departTime: moment(s.departTime).add(diff, 'days'), 9 | arrivalTime: moment(s.departTime).add(diff, 'days') 10 | }); 11 | } 12 | 13 | const flightParser = function(fromCode, toCode, day){ 14 | return f => { 15 | const diff = moment(f.segments[0].departTime).startOf('day').diff(moment(day), 'days'); 16 | const segments = f.segments.map(segmentsParser(fromCode, toCode, diff)); 17 | const price = parseInt(f.price); 18 | return Object.assign({}, f, { fromCode, toCode, segments, price }); 19 | } 20 | } 21 | 22 | class FlightsRepository { 23 | constructor(flights) { 24 | this._flights = flights; 25 | } 26 | 27 | findFlights(fromCode, toCode, day) { 28 | return this._flights.map(flightParser(fromCode, toCode, day)); 29 | } 30 | 31 | findFlightById(fromCode, toCode, day, id) { 32 | const result = this._flights.find(f => f.id == id); 33 | if (!result) return null; 34 | return flightParser(fromCode, toCode, day)(result); 35 | } 36 | } 37 | 38 | module.exports = FlightsRepository; -------------------------------------------------------------------------------- /src/repositories/index.js: -------------------------------------------------------------------------------- 1 | const airportsJSON = require('../data/airports'); 2 | const destinationsJSON = require('../data/destinations'); 3 | const dealsJSON = require('../data/deals'); 4 | const flightsJSON = require('../data/flights'); 5 | const _BookRepository = require('./book.repository'); 6 | const _FlightsRepository = require('./flights.repository'); 7 | 8 | let bookRepository = null; 9 | 10 | const AirportsRepository = () => airportsJSON; 11 | const DestinationsRepository = () => destinationsJSON; 12 | const DealsRepository = () => dealsJSON; 13 | const BookRepository = () => { 14 | if (!bookRepository) { 15 | const cosmosdb_name = process.env.COSMOS_DB_NAME; 16 | const cosmosdb_key = process.env.COSMOS_DB_AUTH_KEY; 17 | const cosmosdb_url = process.env.COSMOS_DB_URL; 18 | const database_name = process.env.COSMOS_DB_DATABASE; 19 | bookRepository = new _BookRepository({ cosmosdb_name, cosmosdb_key, cosmosdb_url, database_name }); 20 | } 21 | 22 | return bookRepository; 23 | } 24 | const FlightsRepository = () => new _FlightsRepository(flightsJSON); 25 | 26 | module.exports = { 27 | AirportsRepository, 28 | DestinationsRepository, 29 | DealsRepository, 30 | BookRepository, 31 | FlightsRepository 32 | }; -------------------------------------------------------------------------------- /src/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const passport = require('passport'); 3 | 4 | const navbarService = require('../services').NavbarService(); 5 | 6 | const router = express.Router(); 7 | 8 | router.get('/', function (req, res, next) { 9 | if (req.isAuthenticated()) { 10 | req.logout(); 11 | res.redirect('/'); 12 | } 13 | else { 14 | const errors = req.flash('error'); 15 | const vm = { 16 | nav: navbarService.getData(req), 17 | errors 18 | }; 19 | res.render('login', vm); 20 | } 21 | }); 22 | 23 | router.post('/', passport.authenticate('local', { 24 | successRedirect: '/', 25 | failureRedirect: '/login', 26 | failureFlash: true, 27 | badRequestMessage: "Login.Error.MissingCredentials" 28 | })); 29 | 30 | module.exports = router; -------------------------------------------------------------------------------- /src/routes/book.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const moment = require('moment'); 3 | 4 | const { encodeData, secured } = require('./helpers'); 5 | const navbarService = require('../services').NavbarService(); 6 | const bookFormService = require('../services').BookFormService(); 7 | const dealsService = require('../services').DealsService(); 8 | 9 | const dateFormat = 'YYYY-MM-DD'; 10 | const router = express.Router(); 11 | 12 | router.get('/', secured, function(req, res, next) { 13 | const vm = { 14 | nav: navbarService.getData(req), 15 | form: bookFormService.getForm(dateFormat), 16 | deals: { 17 | destinations: dealsService.getBestDestinations(3), 18 | flights: dealsService.getFlightDeals(4) 19 | } 20 | }; 21 | res.render('book', vm); 22 | }); 23 | 24 | router.post('/', secured, function(req, res, next) { 25 | const dpa = req.body.dpa || moment(); 26 | const dpb = req.body.dpb || moment().add(7, 'days'); 27 | const fromCode = req.body.fromCode || 'BCN'; 28 | const toCode = req.body.toCode || 'SEA'; 29 | const passengers = req.body.passengers || 1; 30 | res.redirect('/book/flights?' + encodeData({ 31 | fromCode, toCode, passengers, 32 | dpa: moment(dpa, dateFormat).toJSON(), 33 | dpb: moment(dpb, dateFormat).toJSON() 34 | })); 35 | }); 36 | 37 | module.exports = router; 38 | -------------------------------------------------------------------------------- /src/routes/booked.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const { secured } = require('./helpers'); 4 | const navbarService = require('../services').NavbarService(); 5 | const bookService = require('../services').BookService(); 6 | 7 | const router = express.Router(); 8 | 9 | router.get('/', secured, async function (req, res, next) { 10 | const flights = await bookService.getFlights(req.user.name); 11 | const vm = { 12 | nav: navbarService.getData(req), 13 | flights 14 | }; 15 | res.render('booked', vm); 16 | }); 17 | 18 | module.exports = router; -------------------------------------------------------------------------------- /src/routes/flights.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const moment = require('moment'); 3 | 4 | const { secured } = require('./helpers'); 5 | const navbarService = require('../services').NavbarService(); 6 | const dateService = require('../services').DateService(); 7 | const flightsService = require('../services').FlightsService(); 8 | const airportsService = require('../services').AirportsService(); 9 | const bookService = require('../services').BookService(); 10 | 11 | const router = express.Router(); 12 | const fromFlightPicker = 'fromFlight'; 13 | const toFlightPicker = 'toFlight'; 14 | 15 | router.get('/', secured, function (req, res, next) { 16 | const { fromCode, toCode, dpa, dpb, passengers } = req.query; 17 | 18 | const departureDate = moment(dpa); 19 | const returnDate = moment(dpb); 20 | const departureFlights = flightsService.getFlights(fromCode, toCode, departureDate); 21 | const returningFlights = flightsService.getFlights(toCode, fromCode, returnDate); 22 | const departureSlider = dateService.getDaysSequence(departureDate, Math.min(...departureFlights.map(f => f.price)), 8); 23 | const returningSlider = dateService.getDaysSequence(returnDate, Math.min(...returningFlights.map(f => f.price)), 8); 24 | 25 | const vm = { 26 | nav: navbarService.getData(req), 27 | departure: { 28 | name: airportsService.getByCode(fromCode).city, 29 | days: departureSlider, 30 | flights: { 31 | name: fromFlightPicker, 32 | list: departureFlights 33 | } 34 | }, 35 | returning: { 36 | name: airportsService.getByCode(toCode).city, 37 | days: returningSlider, 38 | flights: { 39 | name: toFlightPicker, 40 | list: returningFlights 41 | } 42 | }, 43 | query: { fromCode, toCode, dpa, dpb, passengers } 44 | } 45 | 46 | res.render('flights', vm); 47 | }); 48 | 49 | router.post('/', secured, async function (req, res, next) { 50 | const { passengers, fromCode, toCode, dpa, dpb } = req.body; 51 | const depFlight = req.body[fromFlightPicker]; 52 | const retFlight = req.body[toFlightPicker]; 53 | 54 | const partingFlight = flightsService.getFlightById(fromCode, toCode, moment(dpa), depFlight); 55 | const returningFlight = flightsService.getFlightById(toCode, fromCode, moment(dpb), retFlight); 56 | await bookService.bookFlight(req.user.name, partingFlight, returningFlight, passengers); 57 | res.redirect('/book/purchase'); 58 | }); 59 | 60 | module.exports = router; 61 | -------------------------------------------------------------------------------- /src/routes/helpers.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | 3 | const encodeData = function (data) { 4 | return Object.keys(data).map(function (key) { 5 | return [key, data[key]].map(encodeURIComponent).join("="); 6 | }).join("&"); 7 | } 8 | 9 | const secured = function (req, res, next) { 10 | if (req.isAuthenticated()) { 11 | return next(); 12 | } 13 | 14 | return res.redirect('/login'); 15 | } 16 | 17 | module.exports = { 18 | encodeData, 19 | secured 20 | }; -------------------------------------------------------------------------------- /src/routes/home.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const navbarService = require('../services').NavbarService(); 4 | const dealsService = require('../services').DealsService(); 5 | 6 | const router = express.Router(); 7 | 8 | router.get('/', function (req, res, next) { 9 | const vm = { 10 | nav: navbarService.getData(req), 11 | deals: { 12 | destinations: dealsService.getBestDestinations(3), 13 | flights: dealsService.getFlightDeals(4) 14 | } 15 | }; 16 | res.render('home', vm); 17 | }); 18 | 19 | module.exports = router; -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | 3 | const flightsRouter = require('./flights'); 4 | const purchaseRouter = require('./purchase'); 5 | const receiptRouter = require('./receipt'); 6 | const bookRouter = require('./book'); 7 | const bookedRouter = require('./booked'); 8 | const authRouter = require('./auth'); 9 | const homeRouter = require('./home'); 10 | 11 | router.use('/book/flights', flightsRouter); 12 | router.use('/book/purchase', purchaseRouter); 13 | router.use('/book/receipt', receiptRouter); 14 | router.use('/book', bookRouter); 15 | router.use('/booked', bookedRouter); 16 | router.use('/login', authRouter); 17 | router.use('/logout', authRouter); 18 | router.use('/', homeRouter); 19 | 20 | module.exports = router; -------------------------------------------------------------------------------- /src/routes/purchase.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const { encodeData, secured } = require('./helpers'); 4 | const navbarService = require('../services').NavbarService(); 5 | const bookService = require('../services').BookService(); 6 | 7 | const router = express.Router(); 8 | 9 | router.get('/', secured, async function (req, res, next) { 10 | const booked = await bookService.getBooked(req.user.name); 11 | const { parting, returning } = booked; 12 | const price = (parting.price + returning.price) * booked.passengers; 13 | const vm = { 14 | nav: navbarService.getData(req), 15 | name: req.user.name, 16 | summary: { 17 | parting, 18 | returning 19 | }, 20 | totals: { price, passengers: booked.passengers } 21 | }; 22 | res.render('purchase', vm); 23 | }); 24 | 25 | router.post('/', secured, async function (req, res, next) { 26 | const id = await bookService.purchase(req.user.name); 27 | res.redirect('/book/receipt?' + encodeData({ id })); 28 | }); 29 | 30 | module.exports = router; -------------------------------------------------------------------------------- /src/routes/receipt.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const { secured } = require('./helpers'); 4 | const navbarService = require('../services').NavbarService(); 5 | const bookService = require('../services').BookService(); 6 | 7 | const router = express.Router(); 8 | 9 | router.get('/', secured, async function (req, res, next) { 10 | const { id } = req.query; 11 | const flight = await bookService.getFlightById(req.user.name, id); 12 | const { parting, returning, passengers } = flight; 13 | const price = (parting.price + returning.price) * passengers; 14 | const vm = { 15 | nav: navbarService.getData(req), 16 | name: req.user.name, 17 | fromCity: parting.segments[0].fromCity, 18 | fromCode: parting.segments[0].fromCode, 19 | toCity: returning.segments[0].fromCity, 20 | toCode: returning.segments[0].fromCode, 21 | summary: { 22 | parting, 23 | returning 24 | }, 25 | totals: { price, passengers } 26 | } 27 | res.render('receipt', vm); 28 | }); 29 | 30 | module.exports = router; -------------------------------------------------------------------------------- /src/services/airports.service.ispec.js: -------------------------------------------------------------------------------- 1 | const AirportsService = require('./index').AirportsService; 2 | 3 | describe('[Int] That Airports Service', () => { 4 | it('has that all airports have a city', () => { 5 | const airports = AirportsService(); 6 | const all = airports.getAll(); 7 | expect(all.every(a => a.city)).toBe(true); 8 | }); 9 | 10 | it.each(['CDG', 'JFK', 'TOR', 'BOS', 'JFK', 'FRA'])('exists airport with code %s (used by flights)', (code) => { 11 | const airports = AirportsService(); 12 | const airport = airports.getByCode(code); 13 | expect(airport.code).toBeTruthy(); 14 | expect(airport.city).toBeTruthy(); 15 | }); 16 | 17 | it.each(['SEA', 'BCN', 'HNL', 'CDG', 'NAR', 'LHR', 'SIN'])('exists airport with code %s (used by deals)', (code) => { 18 | const airports = AirportsService(); 19 | const airport = airports.getByCode(code); 20 | expect(airport.code).toBeTruthy(); 21 | expect(airport.city).toBeTruthy(); 22 | }); 23 | }); -------------------------------------------------------------------------------- /src/services/airports.service.js: -------------------------------------------------------------------------------- 1 | const avoidEmptyCity = function(a) { 2 | return Object.assign({}, a, { city: a.city || '-'}); 3 | } 4 | 5 | class AirportsService { 6 | constructor(airports){ 7 | this._airports = airports; 8 | } 9 | 10 | getAll(){ 11 | return this._airports.filter(a => a.code).map(avoidEmptyCity); 12 | } 13 | 14 | getByCode(code) { 15 | return avoidEmptyCity(this._airports.find(a => a.code == code)); 16 | } 17 | } 18 | 19 | module.exports = AirportsService; -------------------------------------------------------------------------------- /src/services/airports.service.spec.js: -------------------------------------------------------------------------------- 1 | const AirportsService = require('./airports.service'); 2 | 3 | const dummyAirportsJSON = [{ 4 | "code": "BCN", 5 | "city": "Barcelona", 6 | "country": "Spain" 7 | }, 8 | { 9 | "code": "SEA", 10 | "name": "Tacoma International Airport", 11 | "city": "Seattle" 12 | } 13 | ]; 14 | 15 | describe('[Unit] That Airports Service', () => { 16 | it('gets all listed airpots', () => { 17 | const airports = new AirportsService(dummyAirportsJSON); 18 | const all = airports.getAll(); 19 | expect(all.length).toBe(2); 20 | }); 21 | 22 | it('gets specific airport', () => { 23 | const airports = new AirportsService(dummyAirportsJSON); 24 | 25 | const all = airports.getAll(); 26 | expect(all.length).toBe(2); 27 | }); 28 | 29 | it('never has an empty city', () => { 30 | const airports = new AirportsService([{ code: 'A'}, { code: 'B', city: 'B City'}]); 31 | 32 | const all = airports.getAll(); 33 | expect(all.every(a => a.city)).toBe(true); 34 | 35 | const cityA = airports.getByCode('A'); 36 | expect(cityA.city).toBeTruthy(); 37 | 38 | const cityB = airports.getByCode('B'); 39 | expect(cityB.city).toBe('B City'); 40 | }); 41 | }); -------------------------------------------------------------------------------- /src/services/book.form.service.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | class BookFormService { 4 | constructor(airports) { 5 | this._airports = airports; 6 | } 7 | 8 | getForm() { 9 | return { 10 | kinds: [ 11 | { text:'Round trip', active: true}, 12 | { text: 'One way' }, 13 | { text: 'Multi-city' } 14 | ], 15 | today: moment().toDate(), 16 | passengers: [1, 2, 3, 4, 5], 17 | airports: this._airports.getAll() 18 | }; 19 | } 20 | } 21 | 22 | module.exports = BookFormService; 23 | -------------------------------------------------------------------------------- /src/services/book.form.service.spec.js: -------------------------------------------------------------------------------- 1 | const BookFormService = require('./book.form.service'); 2 | 3 | const AirportsService = require('./airports.service'); 4 | jest.mock('./airports.service'); 5 | 6 | describe('[Unit] That Book Form Service', () => { 7 | it('all required form fields on view exist', () => { 8 | AirportsService.mockClear(); 9 | const airports = new AirportsService(); 10 | const bookForm = new BookFormService(airports); 11 | const formData = bookForm.getForm(); 12 | expect(Array.isArray(formData.kinds)).toBe(true); 13 | formData.kinds.every(k => 14 | expect(typeof k.text).toBe('string') 15 | ); 16 | 17 | expect(formData.today).toBeInstanceOf(Date); 18 | expect(Array.isArray(formData.passengers)).toBe(true); 19 | expect(formData.passengers).toEqual(expect.arrayContaining([1])); 20 | const airportsGetAll = AirportsService.mock.instances[0].getAll 21 | expect(airportsGetAll).toBeCalledTimes(1); 22 | }); 23 | }); -------------------------------------------------------------------------------- /src/services/book.service.js: -------------------------------------------------------------------------------- 1 | const uuidv4 = require('uuid/v4'); 2 | 3 | const getRandomSeat = function(cols = 'ABCDEF', rows = 32) { 4 | var col = cols[Math.floor(Math.random() * cols.length)]; 5 | var row = Math.floor(Math.random() * rows) + 1; 6 | return row + '' + col; 7 | } 8 | 9 | const getNextSeat = function(seat, cols = 'ABCDEF') { 10 | const col = (cols.indexOf(seat.substr(-1)) + 1) % cols.length; 11 | if (col != 0) return seat.slice(0, -1) + cols[col]; 12 | const row = parseInt(seat.slice(0, -1)) + 1; 13 | return row + cols[col]; 14 | } 15 | 16 | const segmentParser = function(s, airports, passengers) { 17 | const seats = [getRandomSeat()]; 18 | for (let i = 1; i < passengers; i++){ 19 | seats.push(getNextSeat(seats[i-1])); 20 | } 21 | 22 | return { 23 | flight: s.flight, 24 | fromCode: s.fromCode, 25 | fromCity: airports.getByCode(s.fromCode).city, 26 | toCode: s.toCode, 27 | toCity: airports.getByCode(s.toCode).city, 28 | seats, 29 | departTime: s.departTime, 30 | arrivalTime: s.arrivalTime 31 | } 32 | } 33 | 34 | const flightParser = function(flight, airports, passengers) { 35 | return { 36 | duration: flight.duration, 37 | price: flight.price, 38 | fromCode: flight.fromCode, 39 | toCode: flight.toCode, 40 | segments: flight.segments.map(s => segmentParser(s, airports, passengers)) 41 | }; 42 | } 43 | 44 | class BookService { 45 | constructor(bookRepository, airports) { 46 | this._repo = bookRepository; 47 | this._airports = airports; 48 | } 49 | 50 | async getFlights(username) { 51 | const userInfo = await this._repo.getUserInfo(username); 52 | return userInfo.purchased.map(p => Object.assign({}, p, { 53 | total: (p.parting.price + p.returning.price) * p.passengers 54 | })); 55 | } 56 | 57 | async getBooked(username) { 58 | const userInfo = await this._repo.getUserInfo(username); 59 | return userInfo.booked; 60 | } 61 | 62 | async getFlightById(username, id) { 63 | const purchased = await this.getFlights(username); 64 | return purchased.find(f => f.id == id); 65 | } 66 | 67 | async bookFlight(username, parting, returning, passengers) { 68 | const userInfo = await this._repo.getUserInfo(username); 69 | userInfo.booked = { 70 | id: uuidv4(), 71 | passengers, 72 | parting: flightParser(parting, this._airports, passengers), 73 | returning: flightParser(returning, this._airports, passengers) 74 | }; 75 | 76 | await this._repo.createOrUpdateUserInfo(userInfo); 77 | return userInfo.booked.id; 78 | } 79 | 80 | async purchase(username) { 81 | const userInfo = await this._repo.getUserInfo(username); 82 | if (!userInfo.booked) return null; 83 | 84 | const id = userInfo.booked.id; 85 | userInfo.purchased.push(userInfo.booked); 86 | userInfo.booked = null; 87 | await this._repo.createOrUpdateUserInfo(userInfo); 88 | return id; 89 | } 90 | } 91 | 92 | module.exports = BookService; -------------------------------------------------------------------------------- /src/services/book.service.spec.js: -------------------------------------------------------------------------------- 1 | const BookService = require('./book.service'); 2 | 3 | const BookRepository = require('../repositories/book.repository'); 4 | const AirportsService = require('./airports.service'); 5 | jest.mock('../repositories/book.repository'); 6 | jest.mock('./airports.service'); 7 | 8 | describe('[Unit] That Book Service', () => { 9 | beforeEach(() => { 10 | BookRepository.mockClear(); 11 | AirportsService.mockClear(); 12 | }) 13 | 14 | it('calls repository on getFlights', async () => { 15 | BookRepository.mockImplementation(function() { 16 | return { 17 | getUserInfo: async (username) => { 18 | this.getUserInfo(username); 19 | return { purchased: [] }; 20 | } 21 | } 22 | }); 23 | 24 | const bookRepository = new BookRepository(); 25 | const bookService = new BookService(bookRepository, null); 26 | 27 | const result = await bookService.getFlights('me'); 28 | expect(result).toEqual([]); 29 | const bookRepositoryGetUserInfo = BookRepository.mock.instances[0].getUserInfo; 30 | expect(bookRepositoryGetUserInfo).toBeCalledTimes(1); 31 | expect(bookRepositoryGetUserInfo).toBeCalledWith('me'); 32 | }); 33 | 34 | it('adds up the price on ', async () => { 35 | BookRepository.mockImplementation(function() { 36 | return { 37 | getUserInfo: async (username) => ({ 38 | id: username, 39 | purchased: [ 40 | { passengers: 2, parting: { price: 10 }, returning: { price: 20 }}, 41 | { passengers: 1, parting: { price: 10 }, returning: { price: 10 }}, 42 | ] 43 | }) 44 | }; 45 | }); 46 | 47 | const bookRepository = new BookRepository(); 48 | const bookService = new BookService(bookRepository, null); 49 | 50 | const result = await bookService.getFlights('me'); 51 | expect(result).toHaveLength(2); 52 | expect(result[0].total).toBe(60); 53 | expect(result[1].total).toBe(20); 54 | }); 55 | 56 | it('can book a flight', async () => { 57 | BookRepository.mockImplementation(function() { 58 | return { 59 | getUserInfo: async (username) => ({ id: 'me', booked: null, purchased: [] }), 60 | createOrUpdateUserInfo: this.createOrUpdateUserInfo 61 | } 62 | }); 63 | 64 | const bookRepository = new BookRepository(); 65 | const airportsService = new AirportsService(); 66 | const bookService = new BookService(bookRepository, airportsService); 67 | const id = await bookService.bookFlight('me', 68 | { fromCode: 'BCN', toCode: 'SEA', price: 10, segments: [] }, 69 | { fromCode: 'SEA', toCode: 'BCN', price: 20, segments: [] }, 70 | 3); 71 | 72 | expect(id).toBeTruthy(); 73 | const bookRepositoryCreateOrUpdateUserInfo = BookRepository.mock.instances[0].createOrUpdateUserInfo; 74 | expect(bookRepositoryCreateOrUpdateUserInfo).toBeCalled(); 75 | expect(bookRepositoryCreateOrUpdateUserInfo).toBeCalledWith( 76 | expect.objectContaining({ 77 | id: expect.stringMatching('me'), 78 | booked: expect.objectContaining({ 79 | id: expect.stringMatching(id), 80 | }) 81 | }) 82 | ); 83 | }); 84 | 85 | it('gets no booked flight if there wasnt any', async () => { 86 | BookRepository.mockImplementation(function() { 87 | return { 88 | getUserInfo: async (username) => ({ id: username, user: username, booked: null }) 89 | }; 90 | }); 91 | 92 | const bookRepository = new BookRepository(); 93 | const bookService = new BookService(bookRepository, null); 94 | const booked = await bookService.getBooked('me'); 95 | expect(booked).toBeFalsy(); 96 | }); 97 | 98 | it('doesnt purchase if there is no booked', async () => { 99 | BookRepository.mockImplementation(function() { 100 | return { 101 | getUserInfo: async (username) => ({ id: username, user: username, booked: null, purchased: [] }) 102 | }; 103 | }); 104 | 105 | const bookRepository = new BookRepository(); 106 | const bookService = new BookService(bookRepository, null); 107 | const id = await bookService.purchase('me'); 108 | expect(id).toBeFalsy(); 109 | }); 110 | 111 | it('adds booked to purchased and removes it from booked storage', async () => { 112 | BookRepository.mockImplementation(function() { 113 | return { 114 | getUserInfo: async (username) => ({ id: username, user: username, booked: { id: '87ffc07c-6d9e-47f7-8ce1-f4d23d678b85' }, purchased: [] }), 115 | createOrUpdateUserInfo: this.createOrUpdateUserInfo 116 | }; 117 | }); 118 | 119 | const bookRepository = new BookRepository(); 120 | const bookService = new BookService(bookRepository, null); 121 | const id = await bookService.purchase('me'); 122 | expect(id).toBe('87ffc07c-6d9e-47f7-8ce1-f4d23d678b85'); 123 | const bookRepositoryCreateOrUpdateUserInfo = BookRepository.mock.instances[0].createOrUpdateUserInfo; 124 | expect(bookRepositoryCreateOrUpdateUserInfo).toBeCalled(); 125 | expect(bookRepositoryCreateOrUpdateUserInfo).toBeCalledWith({ 126 | id: expect.stringMatching('me'), 127 | user: expect.anything(), 128 | booked: null, 129 | purchased: expect.arrayContaining([ 130 | expect.objectContaining({ 131 | id: expect.stringMatching('87ffc07c-6d9e-47f7-8ce1-f4d23d678b85') 132 | }) 133 | ]) 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /src/services/date.service.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | class DateService { 4 | constructor(price) { 5 | this._price = price; 6 | } 7 | 8 | getDaysSequence(middle, priceBase, sequenceDays = 8) { 9 | const middleDay = (sequenceDays - 1) >> 1; 10 | let current = moment(middle).subtract(middleDay, 'day'); 11 | let result = []; 12 | for (let i = 0; i < sequenceDays; i++) { 13 | result[i] = { 14 | active: i == middleDay, 15 | dayWeek: current.format('dddd'), 16 | dayText: current.format('MMM D'), 17 | price: i == middleDay ? priceBase : this._price.getPrice(priceBase, current) 18 | }; 19 | 20 | current.add(1, 'day'); 21 | } 22 | 23 | return result; 24 | } 25 | } 26 | 27 | module.exports = DateService; -------------------------------------------------------------------------------- /src/services/date.service.spec.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const DateService = require('./date.service'); 3 | 4 | const PriceService = require('./price.service'); 5 | jest.mock('./price.service'); 6 | 7 | describe('[Unit] That Date Service', () => { 8 | beforeEach(() => { 9 | PriceService.mockClear(); 10 | }) 11 | 12 | it('returns a valid list of days for set of 8 days', () => { 13 | const priceService = new PriceService(); 14 | const dateService = new DateService(priceService); 15 | 16 | moment.locale('en'); 17 | const middleDay = new Date(2018, 9, 30); 18 | const result = dateService.getDaysSequence(middleDay, 10, 8); 19 | expect(result).toHaveLength(8); 20 | expect(result[0].active).toBeFalsy(); 21 | expect(result[0].dayWeek).toBe('Saturday'); 22 | expect(result[0].dayText).toBe('Oct 27'); 23 | expect(result[3].active).toBeTruthy(); 24 | expect(result[7].active).toBeFalsy(); 25 | expect(result[7].dayWeek).toBe('Saturday'); 26 | expect(result[7].dayText).toBe('Nov 3'); 27 | }); 28 | 29 | 30 | it('returns a valid list of days for a set of 5 days', () => { 31 | const priceService = new PriceService(); 32 | const dateService = new DateService(priceService); 33 | 34 | moment.locale('en'); 35 | const middleDay = new Date(2018, 10, 5); 36 | const result = dateService.getDaysSequence(middleDay, 10, 5); 37 | expect(result).toHaveLength(5); 38 | expect(result[0].active).toBeFalsy(); 39 | expect(result[0].dayWeek).toBe('Saturday'); 40 | expect(result[0].dayText).toBe('Nov 3'); 41 | expect(result[2].active).toBeTruthy(); 42 | expect(result[4].active).toBeFalsy(); 43 | expect(result[4].dayWeek).toBe('Wednesday'); 44 | expect(result[4].dayText).toBe('Nov 7'); 45 | }); 46 | }); -------------------------------------------------------------------------------- /src/services/deals.service.ispec.js: -------------------------------------------------------------------------------- 1 | const DealsService = require('./deals.service'); 2 | 3 | const DestinationsJSON = require('../data/destinations'); 4 | const DealsJSON = require('../data/deals'); 5 | const AirportsService = require('./airports.service'); 6 | jest.mock('./airports.service'); 7 | 8 | describe('[Int] That Deals Service', () => { 9 | it.each([1,3])('returns always %s destinations', (n) => { 10 | const airports = new AirportsService(); 11 | const dealsService = new DealsService(DestinationsJSON, DealsJSON, airports); 12 | const destinations = dealsService.getBestDestinations(n); 13 | 14 | expect(destinations).toHaveLength(n); 15 | expect(destinations).toContainEqual({ 16 | id: expect.anything(), 17 | title: expect.any(String), 18 | "desktop-image": expect.any(String), 19 | "mobile-image": expect.any(String) 20 | }); 21 | }); 22 | 23 | it.each([2,4])('returns always %s deals', (n) => { 24 | AirportsService.mockImplementation(function() { 25 | return { getByCode: () => ({ city: 'city' })}; 26 | }); 27 | const airports = new AirportsService(); 28 | const dealsService = new DealsService(DestinationsJSON, DealsJSON, airports); 29 | const deals = dealsService.getFlightDeals(n); 30 | 31 | expect(deals).toHaveLength(n); 32 | expect(deals).toContainEqual( 33 | expect.objectContaining({ 34 | fromCode: expect.any(String), 35 | fromName: expect.anything(), 36 | toCode: expect.any(String), 37 | toName: expect.anything(), 38 | price: expect.any(Number), 39 | since: expect.any(String) 40 | }) 41 | ); 42 | }); 43 | }); -------------------------------------------------------------------------------- /src/services/deals.service.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | const dealsParser = function(airports) { 4 | return deal => ({ 5 | fromCode: deal.fromCode, 6 | fromName: airports.getByCode(deal.fromCode).city, 7 | toCode: deal.toCode, 8 | toName: airports.getByCode(deal.toCode).city, 9 | price: parseInt(deal.price), 10 | since: moment(deal.since).format('MMM Do YYYY') 11 | }); 12 | } 13 | 14 | class DealsService { 15 | constructor(destinations, deals, airports){ 16 | this._destinations = destinations; 17 | this._deals = deals; 18 | this._airports = airports; 19 | } 20 | 21 | getBestDestinations(n) { 22 | return this._destinations.slice(0, n); 23 | } 24 | 25 | getFlightDeals(n) { 26 | return this._deals 27 | .sort((a, b) => a.price - b.price) 28 | .slice(0, n) 29 | .map(dealsParser(this._airports)); 30 | } 31 | } 32 | 33 | module.exports = DealsService; -------------------------------------------------------------------------------- /src/services/deals.service.spec.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const DealsService = require('./deals.service'); 3 | 4 | const AirportsService = require('./airports.service'); 5 | jest.mock('./airports.service'); 6 | 7 | describe('[Unit] That Deals Service', () => { 8 | it('returns correct destinations', () => { 9 | const _destinations = [{ id: 0, title: 'Hawaii'}, { id: 1, title: 'Paris'}, { id: 2, title: 'Barcelona'}] 10 | const dealsService = new DealsService(_destinations, null, null); 11 | const destinations = dealsService.getBestDestinations(2); 12 | 13 | expect(destinations).toHaveLength(2); 14 | expect(destinations).toContainEqual(expect.objectContaining({ title: 'Hawaii'})); 15 | expect(destinations).toContainEqual(expect.objectContaining({ title: 'Paris'})); 16 | expect(destinations).not.toContainEqual(expect.objectContaining({ title: 'Barcelona'})); 17 | }); 18 | 19 | it('returns correct deal', () => { 20 | AirportsService.mockImplementation(function() { 21 | return { getByCode: () => ({ city: 'city' })}; 22 | }); 23 | const _deals = [{ 24 | "id": "0", 25 | "fromCode": "SEA", 26 | "toCode": "HNL", 27 | "price": "1100", 28 | "since": "2018-03-04T08:45:00Z" 29 | }]; 30 | 31 | moment.locale('en'); 32 | const airports = new AirportsService(); 33 | const dealsService = new DealsService(null, _deals, airports); 34 | const deals = dealsService.getFlightDeals(1); 35 | 36 | expect(deals).toHaveLength(1); 37 | expect(deals).toContainEqual( 38 | expect.objectContaining({ 39 | fromCode: 'SEA', 40 | toCode: 'HNL', 41 | price: 1100, 42 | since: expect.stringMatching(/\w{2,3} \d+\w{2} \d{4}/i) 43 | }) 44 | ); 45 | }); 46 | }); -------------------------------------------------------------------------------- /src/services/flights.service.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | const segmentParser = function(airports) { 4 | return (s) => ({ 5 | flight: s.flight, 6 | fromCode: s.fromCode, 7 | fromCity: airports.getByCode(s.fromCode).city, 8 | toCode: s.toCode, 9 | toCity: airports.getByCode(s.toCode).city, 10 | departTime: moment(s.departTime).format('hh:mm A'), 11 | arrivalTime: moment(s.arrivalTime).format('hh:mm A') 12 | }); 13 | } 14 | 15 | const flightParser = function(airports, prices) { 16 | return f => ({ 17 | id: parseInt(f.id), 18 | segments: f.segments.map(segmentParser(airports)), 19 | departDate: moment(f.segments[0].departTime).format('MMMM Do YYYY'), 20 | duration: f.duration, 21 | distance: f.distance, 22 | stops: f.segments.length - 1, 23 | price: prices.getPrice(f.price, moment(f.segments[0].departTime)) 24 | }); 25 | } 26 | 27 | class FlightsService { 28 | constructor(flightsRepository, airports, prices){ 29 | this._repo = flightsRepository; 30 | this._airports = airports; 31 | this._prices = prices; 32 | } 33 | 34 | getFlights(fromCode, toCode, day) { 35 | return this._repo.findFlights(fromCode, toCode, day) 36 | .map(flightParser(this._airports, this._prices)); 37 | } 38 | 39 | getFlightById(fromCode, toCode, day, id) { 40 | const result = this._repo.findFlightById(fromCode, toCode, day, id); 41 | if (!result) return null; 42 | return flightParser(this._airports, this._prices)(result); 43 | } 44 | } 45 | 46 | module.exports = FlightsService; -------------------------------------------------------------------------------- /src/services/flights.service.spec.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const FlightsService = require('./flights.service'); 3 | 4 | const FlightsRepository = require('../repositories/flights.repository'); 5 | const AirportsService = require('./airports.service'); 6 | const PriceService = require('./price.service'); 7 | jest.mock('../repositories/flights.repository'); 8 | jest.mock('./airports.service'); 9 | jest.mock('./price.service'); 10 | 11 | describe('[Unit] That Flights Service', () => { 12 | it('returns empty if no flights in repository', () => { 13 | FlightsRepository.mockImplementation(function() { 14 | return { findFlights: () => [] } 15 | }); 16 | const repo = new FlightsRepository(); 17 | const airports = new AirportsService(); 18 | const prices = new PriceService(); 19 | const flightsService = new FlightsService(repo, airports, prices); 20 | 21 | const flights = flightsService.getFlights(); 22 | expect(flights).toHaveLength(0); 23 | }); 24 | 25 | it('returns all flights', () => { 26 | AirportsService.mockImplementation(function(){ 27 | return { getByCode: () => ({ city: '-'})}; 28 | }); 29 | FlightsRepository.mockImplementation(function() { 30 | return { findFlights: () => [ 31 | { segments: ['1st flight', '2nd flight']}, 32 | { segments: ['1st flight', 'another flight']} 33 | ]}; 34 | }); 35 | const repo = new FlightsRepository(); 36 | const airports = new AirportsService(); 37 | const prices = new PriceService(); 38 | const flightsService = new FlightsService(repo, airports, prices); 39 | 40 | const flights = flightsService.getFlights('FROM', 'TO', new Date(2018, 9, 30)); 41 | expect(flights).toHaveLength(2); 42 | }); 43 | 44 | it('get correct flight by id with correct parsers', () => { 45 | AirportsService.mockImplementation(function(){ 46 | return { getByCode: () => ({ city: '-'})}; 47 | }); 48 | FlightsRepository.mockImplementation(function() { 49 | return { findFlightById: () => ({ 50 | id: "1", 51 | duration: "7h 10m", 52 | distance: "1234", 53 | segments: [{ 54 | flight: 987, 55 | fromCode: 'SEA', 56 | toCode: 'HNL', 57 | departTime: '2018-10-30T08:00:00Z', 58 | arrivalTime: '2018-10-30T15:10:00Z' 59 | },{ 60 | flight: 123, 61 | fromCode: 'BCN', 62 | toCode: 'TO', 63 | departTime: '2018-11-06T08:00:00Z', 64 | arrivalTime: '2018-11-06T15:00:00Z' 65 | }] 66 | })}; 67 | }); 68 | 69 | moment.locale('en'); 70 | const repo = new FlightsRepository(); 71 | const airports = new AirportsService(); 72 | const prices = new PriceService(); 73 | const flightsService = new FlightsService(repo, airports, prices); 74 | 75 | const flight = flightsService.getFlightById('FROM', 'TO', new Date(2018, 9, 30), 1); 76 | expect(flight).toHaveProperty('id', 1); 77 | expect(flight).toHaveProperty('stops', 1); 78 | expect(flight).toHaveProperty('departDate', expect.stringMatching(/\w+ \d{1,2}\w{2} \d{4}/)); 79 | expect(flight).toHaveProperty('duration', '7h 10m'); 80 | expect(flight.segments).toHaveLength(2); 81 | expect(flight.segments[0]).toHaveProperty('flight', 987); 82 | expect(flight.segments[0]).toHaveProperty('departTime', expect.stringMatching(/\d{2}:\d{2} [AP]M/)); 83 | expect(flight.segments[0]).toHaveProperty('arrivalTime', expect.stringMatching(/\d{2}:\d{2} [AP]M/)); 84 | }); 85 | }); 86 | 87 | -------------------------------------------------------------------------------- /src/services/index.js: -------------------------------------------------------------------------------- 1 | const AirportsRepository = require('../repositories').AirportsRepository; 2 | const DestinationsRepository = require('../repositories').DestinationsRepository; 3 | const DealsRepository = require('../repositories').DealsRepository; 4 | const BookRepository = require('../repositories').BookRepository; 5 | const FlightsRepository = require('../repositories').FlightsRepository; 6 | 7 | const _PriceService = require('./price.service'); 8 | const _NavbarService = require('./navbar.service'); 9 | const _AirportsService = require('./airports.service'); 10 | const _DateService = require('./date.service'); 11 | const _BookFormService = require('./book.form.service'); 12 | const _BookService = require('./book.service'); 13 | const _DealsService = require('./deals.service'); 14 | const _FlightsService = require('./flights.service'); 15 | 16 | const PriceService = () => new _PriceService(); 17 | const NavbarService = () => new _NavbarService(); 18 | const AirportsService = () => new _AirportsService(AirportsRepository()); 19 | const DateService = () => new _DateService(PriceService()); 20 | const BookFormService = () => new _BookFormService(AirportsService()); 21 | const BookService = () => new _BookService(BookRepository(), AirportsService()); 22 | const DealsService = () => new _DealsService(DestinationsRepository(), DealsRepository(), AirportsService()); 23 | const FlightsService = () => new _FlightsService(FlightsRepository(), AirportsService(), PriceService()); 24 | 25 | module.exports = { 26 | PriceService, 27 | NavbarService, 28 | AirportsService, 29 | DateService, 30 | BookFormService, 31 | BookService, 32 | DealsService, 33 | FlightsService 34 | }; -------------------------------------------------------------------------------- /src/services/navbar.service.js: -------------------------------------------------------------------------------- 1 | const publicMenu = [ 2 | { url: '/book', text: 'NavbarMenu.Book' } 3 | ]; 4 | 5 | const securedMenu = [ 6 | { url: '/booked', text: 'NavbarMenu.Booked'} 7 | ]; 8 | 9 | const onlyPublicMenu = [ 10 | { url: '/login', text: 'NavbarMenu.Login'} 11 | ]; 12 | 13 | class NavbarService { 14 | getData(req) { 15 | const { baseUrl, user } = req; 16 | const greeting = user ? req.__('NavbarMenu.Greeting', user.name) : ''; 17 | const mapMenus = m => Object.assign({}, m, { 18 | active: m.url == baseUrl, 19 | text: req.__(m.text) 20 | }); 21 | return { 22 | greeting, 23 | publicMenu: publicMenu.map(mapMenus), 24 | securedMenu: securedMenu.map(mapMenus), 25 | onlyPublicMenu: onlyPublicMenu.map(mapMenus) 26 | }; 27 | } 28 | } 29 | 30 | module.exports = NavbarService; 31 | -------------------------------------------------------------------------------- /src/services/navbar.service.spec.js: -------------------------------------------------------------------------------- 1 | const NavbarService = require('./navbar.service'); 2 | 3 | describe('[Unit] That Navbar Service', () => { 4 | it('returns always three arrays', () => { 5 | const req = { baseUrl: '/login', user: { name: 'me'}, __: jest.fn(() => '-') }; 6 | 7 | const navbarService = new NavbarService(); 8 | const nav = navbarService.getData(req); 9 | expect(nav).toMatchObject(expect.objectContaining({ 10 | publicMenu: expect.any(Array), 11 | securedMenu: expect.any(Array), 12 | onlyPublicMenu: expect.any(Array) 13 | })); 14 | expect(nav.publicMenu).toContainEqual( 15 | expect.objectContaining( 16 | { text: expect.any(String), url: expect.any(String)} 17 | ) 18 | ); 19 | expect(nav.securedMenu).toContainEqual( 20 | expect.objectContaining( 21 | { text: expect.any(String), url: expect.any(String)} 22 | ) 23 | ); 24 | expect(nav.onlyPublicMenu).toContainEqual( 25 | expect.objectContaining( 26 | { text: expect.any(String), url: expect.any(String)} 27 | ) 28 | ); 29 | expect(req.__).toHaveBeenCalled(); 30 | const n = nav.publicMenu.length + nav.securedMenu.length + nav.onlyPublicMenu.length; 31 | expect(req.__).toHaveBeenCalledTimes(n + 1); 32 | }); 33 | 34 | it('dont have greeting if not logged in', () => { 35 | const req = { baseUrl: '/', user: null, __: jest.fn(() => '-') }; 36 | const navbarService = new NavbarService(); 37 | const nav = navbarService.getData(req); 38 | expect(nav).toMatchObject({ greeting: ''}); 39 | expect(req.__).toHaveBeenCalled(); 40 | const n = nav.publicMenu.length + nav.securedMenu.length + nav.onlyPublicMenu.length; 41 | expect(req.__).toHaveBeenCalledTimes(n); 42 | }); 43 | 44 | 45 | it('to have the correct active flag', () => { 46 | const req = { baseUrl: '/login', user: null, __: jest.fn(() => '-') }; 47 | const navbarService = new NavbarService(); 48 | const nav = navbarService.getData(req); 49 | 50 | expect(nav.publicMenu).not.toContainEqual( 51 | expect.objectContaining( { active: true}) 52 | ); 53 | 54 | expect(nav.securedMenu).not.toContainEqual( 55 | expect.objectContaining( { active: true}) 56 | ); 57 | 58 | expect(nav.onlyPublicMenu).toContainEqual( 59 | expect.objectContaining( { active: true, url: '/login'}) 60 | ); 61 | }); 62 | }); -------------------------------------------------------------------------------- /src/services/price.service.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | const pricePerWeek = [68, 40, 45, 38, 75, 90, 85]; 4 | 5 | class PriceService { 6 | getPrice(priceBase, day) { 7 | return priceBase + pricePerWeek[moment(day).isoWeekday() - 1]; 8 | } 9 | } 10 | 11 | module.exports = PriceService; -------------------------------------------------------------------------------- /src/services/price.service.spec.js: -------------------------------------------------------------------------------- 1 | const PriceService = require('./price.service'); 2 | 3 | describe('[Unit] That Price Service', () => { 4 | it('returns always higher prices on weekend', () => { 5 | const priceService = new PriceService(); 6 | 7 | const saturdayPrice = priceService.getPrice(10, new Date(2018, 9, 27)); 8 | const sundayPrice = priceService.getPrice(10, new Date(2018, 9, 28)); 9 | const mondayPrice = priceService.getPrice(10, new Date(2018, 9, 29)); 10 | const thursdayPrice = priceService.getPrice(10, new Date(2018, 10, 1)); 11 | 12 | expect(saturdayPrice).toBeGreaterThan(mondayPrice); 13 | expect(saturdayPrice).toBeGreaterThan(thursdayPrice); 14 | expect(sundayPrice).toBeGreaterThan(mondayPrice); 15 | expect(sundayPrice).toBeGreaterThan(thursdayPrice); 16 | }); 17 | 18 | 19 | it('always return more than base price', () => { 20 | const priceService = new PriceService(); 21 | 22 | const saturdayPrice = priceService.getPrice(10, new Date(2018, 9, 27)); 23 | const sundayPrice = priceService.getPrice(10, new Date(2018, 9, 28)); 24 | const mondayPrice = priceService.getPrice(10, new Date(2018, 9, 29)); 25 | const thursdayPrice = priceService.getPrice(10, new Date(2018, 10, 1)); 26 | 27 | expect(saturdayPrice).toBeGreaterThan(10); 28 | expect(sundayPrice).toBeGreaterThan(10); 29 | expect(mondayPrice).toBeGreaterThan(10); 30 | expect(thursdayPrice).toBeGreaterThan(10); 31 | }); 32 | }); -------------------------------------------------------------------------------- /views/book.hbs: -------------------------------------------------------------------------------- 1 | 19 |
20 | {{> book/deals deals}} 21 |
-------------------------------------------------------------------------------- /views/booked.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/flights.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/home.hbs: -------------------------------------------------------------------------------- 1 | 16 |
17 | {{> book/deals deals}} 18 |
-------------------------------------------------------------------------------- /views/layouts/main.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{i18n "Title"}} 6 | 7 | 8 | 9 | 10 | {{> common/navbar nav}} 11 |
12 | {{{body}}} 13 |
14 | {{> common/footer}} 15 | 16 | 17 | -------------------------------------------------------------------------------- /views/login.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/partials/book/deals-destinations.hbs: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | {{title}} 4 | {{title}} 5 |
    {{title}}
    6 |
    7 |
  • -------------------------------------------------------------------------------- /views/partials/book/deals-price.hbs: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | 4 | 5 | {{fromName}} ({{fromCode}}) 6 | {{i18n "Deals.FlightsTo"}}{{toName}} ({{toCode}}) 7 | {{i18n "Deals.FlightsPrice"}}{{since}} 8 | 9 | 10 | {{i18n "Deals.FlightsPriceFrom"}} 11 | {{price}} 12 | {{i18n "Deals.FlightsOneWay"}} 13 | 14 | 15 | 16 | 17 | 18 | 19 |
  • -------------------------------------------------------------------------------- /views/partials/book/deals.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/partials/book/form.hbs: -------------------------------------------------------------------------------- 1 |
    2 |

    Book a trip

    3 |
    4 | {{#each kinds}} 5 |
    6 | {{text}} 7 |
    8 | {{/each}} 9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 | 16 | 22 |
    23 |
    24 |
    25 |
    26 | 27 | 33 |
    34 |
    35 |
    36 | 37 |
    38 |
    39 |
    40 | 41 | 42 |
    43 |
    44 |
    45 |
    46 | 47 | 48 |
    49 |
    50 |
    51 | 52 |
    53 |
    54 |
    55 | 56 | 61 |
    62 |
    63 |
    64 | 65 |
    66 |
    67 |
    -------------------------------------------------------------------------------- /views/partials/common/footer.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/partials/common/navbar.hbs: -------------------------------------------------------------------------------- 1 | 2 | 40 | -------------------------------------------------------------------------------- /views/partials/flights/filters.hbs: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 |
    5 |

    View by

    6 | 7 |
    8 | 12 |
    13 |
    14 | 18 |
    19 |
    20 | 24 |
    25 |
    26 |
    27 | 28 |
    29 | 30 |

    Filter results

    31 |
    32 | 36 |
    37 |
    38 | 42 |
    43 |
    44 | 48 |
    49 |
    50 |
    51 |
    52 |
    -------------------------------------------------------------------------------- /views/partials/flights/results-picker.hbs: -------------------------------------------------------------------------------- 1 |
      2 | {{#each list}} 3 |
    • 4 | 5 | 6 | 40 |
    • 41 | {{/each}} 42 |
    -------------------------------------------------------------------------------- /views/partials/flights/results.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
      4 | {{#each this}} 5 |
    • 6 | {{#with parting}} 7 |
      8 | 9 |
        10 | {{#each segments}} 11 |
      • 12 | 13 | Flight {{flight}} 14 | 15 | 16 | 17 | {{fromCode}} 18 | {{departTime}} 19 | 20 | 21 | {{toCode}} 22 | {{arrivalTime}} 23 | 24 | 25 | {{#each seats}} 26 | {{this}}{{#unless @last}} | {{/unless}} 27 | {{/each}} 28 | 29 |
      • 30 | {{/each}} 31 |
      32 | 33 | 34 | {{duration}} | {{stops}} stop/s 35 | 36 | 37 |
      38 | 39 |
      40 | ${{../total}} 41 |
      42 |
      43 | {{../passengers}} passenger/s 44 |
      45 |
      46 |
      47 | {{/with}} 48 | {{#with returning}} 49 |
      50 | 51 |
        52 | {{#each segments}} 53 |
      • 54 | 55 | Flight {{flight}} 56 | 57 | 58 | 59 | {{fromCode}} 60 | {{departTime}} 61 | 62 | 63 | {{toCode}} 64 | {{arrivalTime}} 65 | 66 | 67 | {{#each seats}} 68 | {{this}}{{#unless @last}} | {{/unless}} 69 | {{/each}} 70 | 71 |
      • 72 | {{/each}} 73 |
      74 | 75 | 76 | {{duration}} | {{stops}} stop/s 77 | 78 | 79 |
      80 |
      81 | {{/with}} 82 |
    • 83 | {{/each}} 84 |
    -------------------------------------------------------------------------------- /views/partials/flights/slider.hbs: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 |
    5 |
    6 |
    7 | 8 |
    9 |
    10 |
      11 | {{#each this}} 12 |
    • 13 | 14 | {{dayWeek}} 15 | {{dayText}} 16 | ${{price}} 17 | 18 |
    • 19 | {{/each}} 20 |
    21 |
    22 |
    23 | 24 |
    25 |
    26 |
    27 |
    28 |
    29 |
    -------------------------------------------------------------------------------- /views/partials/purchase/summary.hbs: -------------------------------------------------------------------------------- 1 | {{#with parting}} 2 |

    {{departDate}}

    3 |
      4 |
    • 5 | FLIGHT 6 | DEPARTS 7 | ARRIVES 8 | SEAT 9 | 10 |
      11 |
    • 12 | 13 | {{#each segments}} 14 |
    • 15 | Flight {{flight}} 16 | 17 | {{fromCity}} 18 | {{departTime}} 19 | 20 | 21 | {{toCity}} 22 | {{arrivalTime}} 23 | 24 | 25 | {{#each seats}} 26 | {{this}}{{#unless @last}} | {{/unless}} 27 | {{/each}} 28 | 29 |
    • 30 | {{/each}} 31 |
    32 |
    33 | Distance: 34 | {{distance}} mi | 35 | Duration: 36 | {{duration}} 37 |
    38 | 39 | 40 |
    41 |
    42 |
    43 | {{/with}} 44 | 45 | {{#with returning}} 46 |

    {{departDate}}

    47 |
      48 | {{#each segments}} 49 |
    • 50 | Flight {{flight}} 51 | 52 | {{fromCity}} 53 | {{departTime}} 54 | 55 | 56 | {{toCity}} 57 | {{arrivalTime}} 58 | 59 | 60 | {{#each seats}} 61 | {{this}}{{#unless @last}} | {{/unless}} 62 | {{/each}} 63 | 64 |
    • 65 | {{/each}} 66 |
    67 |
    68 | Distance: 69 | {{distance}} mi | 70 | Duration: 71 | {{duration}} 72 |
    73 | {{/with}} -------------------------------------------------------------------------------- /views/partials/purchase/totalPrice.hbs: -------------------------------------------------------------------------------- 1 |
    2 |

    Total price

    3 |
    4 | 5 | 6 | 7 | 8 |

    ${{price}}

    9 |

    for {{passengers}} passenger/s

    10 |
    11 | Taxes, fees and charges 12 |
    13 | Low-price guarantee -------------------------------------------------------------------------------- /views/purchase.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 5 | Shop for another flight 6 | 7 |
    8 |

    Flight Summary

    9 |

    Please review the information below before confirming your booking.

    10 | 11 |

    Passenger Name

    12 |

    {{name}}

    13 |
    14 | 15 |
    16 |
    17 | {{> purchase/summary summary}} 18 |
    19 |
    20 | {{> purchase/totalPrice totals}} 21 |
    22 | 23 |
    24 |
    25 |
    26 | 27 |
    28 |
    29 | 30 | 31 | 32 | 33 | Cancel 34 |
    35 |
    36 |
    37 |
    -------------------------------------------------------------------------------- /views/receipt.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | Shop for another flight 5 | 6 |
    7 |

    Flight booked!

    8 |
    9 |
    10 |
    {{fromCity}}
    11 |
    12 |
    {{toCity}}
    13 |
    14 |
    15 |
    {{fromCode}}
    16 |
    17 |
    {{toCode}}
    18 |
    19 |
    20 |
    21 |

    Your receipt has been sent to you via email.

    22 |

    Please note that this is not a boarding pass.

    23 |

    You may check in for your flight up to 24 hours in advance of your departure.

    24 |
    25 | 26 |

    Passenger Name

    27 |

    {{name}}

    28 |
    29 | 30 |
    31 |
    32 | {{> purchase/summary summary}} 33 |
    34 |
    35 | {{> purchase/totalPrice totals}} 36 |
    37 | 38 |
    39 |
    40 |
    41 |
    42 |
    -------------------------------------------------------------------------------- /web.config: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 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 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | 4 | module.exports = { 5 | devtool: false, 6 | entry: './front.js', 7 | output: { 8 | filename: 'js/main.js', 9 | path: path.resolve(__dirname, 'public'), 10 | }, 11 | resolve: { 12 | extensions: ['.css', '.scss'], 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.s?css$/, 18 | use: [ 19 | MiniCssExtractPlugin.loader, 20 | { loader: 'css-loader', options: { importLoaders: 1 } }, 21 | { 22 | loader: 'postcss-loader', 23 | options: { 24 | ident: 'postcss', 25 | plugins: [ 26 | require('autoprefixer')({add: true }) 27 | ] 28 | } 29 | }, 30 | 'sass-loader', 31 | ], 32 | } 33 | ] 34 | }, 35 | plugins: [ 36 | new MiniCssExtractPlugin({ 37 | filename: 'css/main.css', 38 | }) 39 | ] 40 | }; --------------------------------------------------------------------------------