├── .README.md.swp ├── .browserslistrc ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── api └── products │ └── [id].ts ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── core │ │ ├── flavor.ts │ │ ├── product.ts │ │ ├── selectedProductAttributes.ts │ │ └── size.ts │ ├── homepage │ │ ├── homepage.component.html │ │ ├── homepage.component.scss │ │ ├── homepage.component.spec.ts │ │ └── homepage.component.ts │ ├── mock-products.ts │ ├── navbar │ │ ├── navbar.component.html │ │ ├── navbar.component.scss │ │ ├── navbar.component.spec.ts │ │ └── navbar.component.ts │ ├── product-page │ │ ├── product-page.component.html │ │ ├── product-page.component.scss │ │ ├── product-page.component.spec.ts │ │ └── product-page.component.ts │ ├── product.service.spec.ts │ ├── product.service.ts │ ├── product │ │ ├── product.component.html │ │ ├── product.component.scss │ │ ├── product.component.spec.ts │ │ └── product.component.ts │ └── products │ │ ├── products.component.html │ │ ├── products.component.scss │ │ ├── products.component.spec.ts │ │ └── products.component.ts ├── assets │ ├── .gitkeep │ ├── ice-cream-cherry.svg │ ├── ice-cream-prune.svg │ ├── ice-cream-squash.svg │ ├── popsicle-cherry.svg │ ├── popsicle-lettuce.svg │ └── popsicle-lime.svg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.README.md.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipcart/snipcart-angular-tutorial/a49440b61ac32aa4d1f04993c6587733b23043dc/.README.md.swp -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | 48 | .vercel 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Snipcart Angular Tutorial 2 | Because summer is coming in Canada and its been quite a long year, I wanted to replenish my youth memories and make an **REPLENISHING ICE CREAM STORE**! 3 | 4 | ## Angular setup 5 | 6 | Let's first install the Angular CLI. It's a neat tool Angular provides that automates many development tasks. To install it, open a terminal and type the following command: 7 | 8 | `npm install -g @angular/cli` 9 | 10 | Once installed, create a new project with the following command: 11 | `ng new snipcart-angular` 12 | 13 | A prompt will appear asking you if you want to enable strict mode. Select yes. This will enable a few other settings that will help to catch bugs ahead of time. 14 | 15 | In your directory you now see your project repo. Type the following commands to go in it: 16 | ``` 17 | cd snipcart-angular 18 | ng serve --open 19 | ``` 20 | ng serve will build the app, while the --open option will open up a browser to `http://localhost:4200/`. You should now see Angular generic template page. 21 | 22 | ### Customizing the html template 23 | Now that the general project setup is done. Let's start customizing it! 24 | 25 | First of all, open the `app.component.ts` file and change the value of the title property for 'Ice cream store'. 26 | 27 | Afterwards, open the `app.component.html` file and remove all of the template section, replacing it by `

{{title}}

`. The final result should look like this: 28 | 29 | ```html 30 |

{{title}}

31 | 32 | ``` 33 | 34 | Afterwards, the browser should reload instantly, displaying only the new title. 35 | 36 | #### Customizing the stylesheet 37 | 38 | For help with our styling, we will use the angular material components. These components made by the material team are an implementation of [material design](https://material.io/). They will allow us to quickly create a fast, design tested e-commerce. 39 | 40 | Let's use the following command to install material UI: `ng add @angular/material` 41 | 42 | 43 | You can then replace `src/app.component.scss`. 44 | 45 | ### Creating mock products 46 | Later in the tutorial, we will use a full InMemoryDbService. For now, let's do something simple and simply create a `mock-products.ts` file in our root component: 47 | ```ts 48 | import { Product } from './core/product'; 49 | import { Size } from './core/size'; 50 | 51 | export const PRODUCTS: Product[] = [ 52 | { 53 | id: 1, 54 | name: 'Ice Cream', 55 | imageUrls: ['../assets/ice-cream-prune.svg', '../assets/ice-cream-cherry.svg', '../assets/ice-cream-squash.svg'], 56 | price: 10, 57 | flavors: [ 58 | { name: 'prune', color: '#5A188E' }, 59 | { name: 'squash', color: '#F88532' }, 60 | { name: 'cherry', color: '#E91E63' }, 61 | ], 62 | sizes: [Size.SMALL, Size.MEDIUM, Size.LARGE], 63 | }, 64 | { 65 | id: 2, 66 | name: 'Popsicle', 67 | imageUrls: ['../assets/popsicle-lime.svg', '../assets/popsicle-lettuce.svg', '../assets/popsicle-cherry.svg'], 68 | price: 8, 69 | flavors: [ 70 | { name: 'lime', color: '#00CACA' }, 71 | { name: 'lettuce', color: '#80DC0B' }, 72 | { name: 'cherry', color: '#E91E63' }, 73 | ], 74 | sizes: [Size.SMALL, Size.LARGE], 75 | }, 76 | ]; 77 | ``` 78 | 79 | You will also need to create a `core` folder that will contain our typescript product interface as well as a flavor and size interface we will use to better fulfill our customer's wishes! 80 | 81 | ```ts 82 | // core/product.ts 83 | 84 | import { Flavor } from "./flavor"; 85 | import { Size } from "./size"; 86 | 87 | export interface Product { 88 | id: number; 89 | name: string; 90 | imageUrls: string[]; 91 | price: number; 92 | flavors: Flavor[]; 93 | sizes: Size[]; 94 | } 95 | ``` 96 | 97 | ```ts 98 | // core/flavor.ts 99 | export interface Flavor { 100 | name: string; 101 | color: string; 102 | } 103 | ``` 104 | 105 | ```ts 106 | // core/size.ts 107 | export enum Size { 108 | SMALL = "small", 109 | MEDIUM = "medium", 110 | LARGE = "large", 111 | } 112 | ``` 113 | ## Create a homepage component 114 | 115 | Let's now create our website's homepage, where we will display our product header and options. 116 | 117 | In your terminal type the following command: `ng generate component homepage`. 118 | 119 | Then, let's start by adding props to the homepage. It will be used to display our app title to our website, let's create 2 properties for our app title and subtitle. 120 | 121 | ```ts 122 | // homepage.component.ts 123 | 124 | @Component({ 125 | selector: 'app-homepage', 126 | templateUrl: './homepage.component.html', 127 | styleUrls: ['./homepage.component.scss'] 128 | }) 129 | export class HomepageComponent { 130 | title = 'Infinite summer ice cream store'; 131 | subtitle = 'Which one do you want?'; 132 | } 133 | ``` 134 | 135 | Then let's add the html templating in the corresponding file: 136 | 137 | ```html 138 | 139 |
140 |

{{ title }}

141 |

{{ subtitle }}

142 |
143 | ``` 144 | 145 | In order to display the homepage view, add the following line to the component: 146 | 147 | ```html 148 | 149 | 150 | 151 | 152 | ``` 153 | 154 | You should now see the title and subtitle displayed! But what's a e-commerce homepage header without some product to show? 155 | ## Display products: introducing directives 156 | Now that we have a cool header and some nice products, let's display them on our website! We will do it in a separate component that will improve reusability. 157 | 158 | In your terminal, create the product-display component by typing the following Angular CLI command: `ng generate component products`. 159 | 160 | Now that we have our component, let's first import the products that we need. Angular allows us to do so easily. Simply add the following lines in `product-display-component.ts`: 161 | 162 | ```ts 163 | import { PRODUCTS } from '../mock-products'; 164 | ``` 165 | 166 | And then define the attributes in the component class with the following lines: 167 | 168 | ```ts 169 | export class ProductsComponent implements OnInit { 170 | 171 | products = PRODUCTS; 172 | } 173 | ``` 174 | 175 | We are now ready to create a product component to give further information about them to our customers. 176 | 177 | `ng generate component product`. 178 | 179 | Open the newly created `product.component.ts` file and replace it with the following content: 180 | 181 | ```ts 182 | import { Component, Input, OnInit } from '@angular/core'; 183 | import { Product } from '../core/product'; 184 | 185 | @Component({ 186 | selector: 'app-product', 187 | templateUrl: './product.component.html', 188 | styleUrls: ['./product.component.scss'] 189 | }) 190 | export class ProductComponent implements OnInit { 191 | @Input() product: Product | undefined; 192 | imageUrl :string = ""; 193 | 194 | ngOnInit() { 195 | this.imageUrl = this.product?.imageUrls[0] ?? ''; 196 | } 197 | } 198 | ``` 199 | 200 | The `@Input` decorator we added allow us to declare *input properties*. This means that the component can now receive its value directly from its parent component. 201 | 202 | Let's look at what we just created: first, an input property product which binds to the Product object we created in core, and secondly a imageUrl property which we will use to display our product's image. With the line `this.imageUrl = this.product?.imageUrls[0] ?? '';` we then assign to imageUrl the value of the first image in the imageUrls array if it exists, and do so within the ngOnInit method. 203 | 204 | ngOnInit is an Angular lifecycle method that gets called after component initialization. Since the component is initialized, the input props, in our case the product prop, is populated and we can access it. This is what allows us to populate the property. 205 | 206 | Now that our component properties are defined, we can add them to our html (`product.component.html`): 207 | 208 | ```html 209 | 212 | ``` 213 | 214 | Notice the `*ngFor` directive we are using. It allows us to modify the DOM structure by - you guessed it - looping over the elements of the `products` list and creating the specified html node for it (in our case, the app-products node). 215 | 216 | [Directives] such as `*ngFor` are defined in [Angular's official documentation](https://angular.io/guide/built-in- 217 | directives#built-in-structural-directives) as "classes that add additional behavior to elements". In that sense, components are also directives, because they define additional behavior to a standard html template. 218 | 219 | ### Create a product page: introducing angular routing 220 | 221 | Open `app-routing.module.ts` and insert the following into routes: 222 | 223 | ```ts 224 | const routes: Routes = [ 225 | {path: "**", component: HomepageComponent}, 226 | ]; 227 | ``` 228 | 229 | We can now add a dynamic route. 230 | 231 | ```ts 232 | const routes: Routes = [ 233 | {path: "product/:id", component: ProductPageComponent}, 234 | {path: "**", component: HomepageComponent}, 235 | ]; 236 | ``` 237 | The "**" path is a wildcard route, generally used for 404 pages. It's important to add the wildcard route last, otherwise it will override the other routes. 238 | 239 | In `app.component.html`, `` will display the component related to the route. So in your browser, you can now go to `http://localhost:4200/` and it will point to our homepage! 240 | 241 | ## Create product page component: discovering services 242 | 243 | Components are responsible for data presentation, to improve our application modularity, they shouldn't access the application data directly. Instead, they should interact with **services** which handle data access. 244 | 245 | Let's refactor our `products` component so that it uses a service to handle data access. For now, this service will use mock data. 246 | 247 | In the terminal, enter the following command: `ng generate service product`. 248 | 249 | In te newly created `product.service.ts` fil, add the following content: 250 | 251 | ```ts 252 | import { Injectable } from '@angular/core'; 253 | import { PRODUCTS } from './mock-products'; 254 | 255 | @Injectable({ 256 | providedIn: 'root' 257 | }) 258 | export class ProductService { 259 | 260 | constructor() { } 261 | 262 | getProducts(): Product[] { 263 | return PRODUCTS; 264 | } 265 | ``` 266 | 267 | The `getProducts` method simply return our mock data. Later in the tutorial we will modify it to make it even more modular. Then, in `products.component.ts` replace the mock products assignation by a call from product service: 268 | 269 | ```ts 270 | export class ProductsComponent implements OnInit { 271 | products: Product[] = []; 272 | 273 | constructor(private productService: ProductService) {} 274 | 275 | getProducts(): void { 276 | this.products = this.productService.getProducts(); 277 | } 278 | 279 | ngOnInit() { 280 | this.getProducts(); 281 | } 282 | } 283 | ``` 284 | 285 | We have done 4 things here: first, we replaced the value of products with an empty array, secondly, we injected the productService in our constructor. Then we defined a getProducts method in our component that handles the products logic. Lastly, we called that method in the ngOnInit lifecycle method. 286 | 287 | Now for our product page, we will need data about a single product, let's add a `getProduct` method to our product service to fetch this data: 288 | 289 | ```ts 290 | // product.service.ts 291 | 292 | getProduct(id: number): Observable { 293 | const product = PRODUCTS.find(product => product.id === id); 294 | return of(product); 295 | } 296 | ``` 297 | 298 | This method returns an [observable](https://angular.io/guide/observables) which are used in angular for event handling and, as in our case, asynchronous programming. Later when designing our product page, we will see how to fetch observable values. 299 | 300 | Lt's use this method in our product component to display the product content: `ng generate component product`. 301 | 302 | 303 | ```ts 304 | // product.component.ts 305 | import { Component, Input, OnInit } from '@angular/core'; 306 | import { Product } from '../core/product'; 307 | 308 | @Component({ 309 | selector: 'app-product', 310 | templateUrl: './product.component.html', 311 | styleUrls: ['./product.component.scss'] 312 | }) 313 | export class ProductComponent implements OnInit { 314 | @Input() product: Product | undefined; 315 | imageUrl :string = ""; 316 | 317 | ngOnInit() { 318 | this.imageUrl = this.product?.imageUrls[0] ?? ''; 319 | } 320 | } 321 | ``` 322 | In the typescript file, we added a product input property for the inserted product, along with an `imageUrl` property, which we bind to the component's image src. 323 | 324 | ```html 325 | // product.component.html 326 |
327 |

{{ product?.name }}

328 | 329 |
330 | ``` 331 | 332 | In the html, we also added a router link to a product page, which we have not yet defined. Let's do it now. 333 | ### Add product page 334 | 335 | First, let's allow our users to select the flavor and size variant they want for their products. 336 | 337 | Along with an imageUrl property similar to the one we added to the product component, let's add a `getProduct` method that will get the dynamic parameter from our route and use it to call the corresponding method we defined in product service: 338 | 339 | ```ts 340 | // in product-page.component.ts 341 | imageUrl: string = ''; 342 | product: Product | undefined; 343 | 344 | getProduct(): void { 345 | const id = Number(this.route.snapshot.paramMap.get('id')); 346 | this.productService 347 | .getProduct(id) 348 | .subscribe((product) => (this.product = product)); 349 | } 350 | ``` 351 | 352 | We can see that the method calls our product service `getProduct` method and *subscribes* to the observable value. When the value from getProduct gets returned, it will be assigned to the `product` property. 353 | 354 | Now that we have all of the required data, let's display our product name, image url and price: 355 | 356 | ```html 357 |

{{ product?.name }}

358 | 359 |

Price: {{ product?.price }}

) 360 | ``` 361 | 362 | ```html 363 |

{{ product?.name }}

364 | 365 |

Price: {{ product?.price }}

366 | 375 | 376 | ``` 377 | 378 | And voilà! You should now be able to complete a test transaction! 379 | 380 | ## BONUS: Customizing customer consumptions with Snipcart's custom field 381 | If you look at the code repo, you will see I added some logic to create Snipcart's [custom fields](https://docs.snipcart.com/v3/setup/products). These will allow our users to select their ice-cream/popsicles flavor and sizes. 382 | 383 | First, I made 2 functions to give us the options separated by `|` caracter, that we will use to populate the buy button's custom option fields. 384 | 385 | ```ts 386 | get flavorOptions(): string { 387 | return ( 388 | this.product?.flavors?.map((flavor) => flavor.name).join('|') ?? '' 389 | ); 390 | } 391 | 392 | get sizeOptions(): string { 393 | return this.product?.sizes?.join('|') ?? ''; 394 | } 395 | ``` 396 | 397 | Then, I added a dropdown to select product size and chip components to select flavors... 398 | 399 | ```html 400 | // product-page.component.html 401 | 402 | Size 403 | 412 | 413 | {{ size }} 414 | 415 | 416 | 417 | 418 | 425 | {{ flavor.name }} 426 | 427 | 428 | ``` 429 | ..., along with some logic to keep track of the selected values: 430 | 431 | ```ts 432 | // core/selectedProductAttributes.ts 433 | 434 | import { Flavor } from "./flavor"; 435 | import { Size } from "./size"; 436 | 437 | export interface SelectedProductAttributes { 438 | flavor: Flavor | undefined; 439 | size: Size | undefined; 440 | } 441 | ``` 442 | ```ts 443 | // product-page.component.ts 444 | // (...) 445 | export class ProductPageComponent implements OnInit { 446 | imageUrl: string = ''; 447 | selectedAttributes: SelectedProductAttributes = { 448 | flavor: undefined, 449 | size: undefined, 450 | }; 451 | product: Product | undefined; 452 | // (...) 453 | ``` 454 | 455 | 456 | 457 | Now we simply have to add the custom field attributes to Snipcart's buy button: 458 | 459 | ```html 460 | 476 | ``` 477 | ## Conclusion 478 | I had a lot of fun writing this demo. I had worked with angular 8 a couple of years ago, and was a bit scared to go back to it, afraid their would be a lot of boilerplate code. However, I became pleasantly surprised in using angular opinionated architecture, as I found it helped me to create a more modular app. 479 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": "df1897f6-469f-4020-9225-8a88e766ed57" 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "snipcart-angular": { 10 | "projectType": "application", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | }, 15 | "@schematics/angular:application": { 16 | "strict": true 17 | } 18 | }, 19 | "root": "", 20 | "sourceRoot": "src", 21 | "prefix": "app", 22 | "architect": { 23 | "build": { 24 | "builder": "@angular-devkit/build-angular:browser", 25 | "options": { 26 | "outputPath": "dist/snipcart-angular", 27 | "index": "src/index.html", 28 | "main": "src/main.ts", 29 | "polyfills": "src/polyfills.ts", 30 | "tsConfig": "tsconfig.app.json", 31 | "aot": true, 32 | "assets": [ 33 | "src/favicon.ico", 34 | "src/assets" 35 | ], 36 | "styles": [ 37 | "./node_modules/@angular/material/prebuilt-themes/pink-bluegrey.css", 38 | "src/styles.scss" 39 | ], 40 | "scripts": [] 41 | }, 42 | "configurations": { 43 | "production": { 44 | "fileReplacements": [ 45 | { 46 | "replace": "src/environments/environment.ts", 47 | "with": "src/environments/environment.prod.ts" 48 | } 49 | ], 50 | "optimization": true, 51 | "outputHashing": "all", 52 | "sourceMap": false, 53 | "namedChunks": false, 54 | "extractLicenses": true, 55 | "vendorChunk": false, 56 | "buildOptimizer": true, 57 | "budgets": [ 58 | { 59 | "type": "initial", 60 | "maximumWarning": "500kb", 61 | "maximumError": "1mb" 62 | }, 63 | { 64 | "type": "anyComponentStyle", 65 | "maximumWarning": "2kb", 66 | "maximumError": "4kb" 67 | } 68 | ] 69 | } 70 | } 71 | }, 72 | "serve": { 73 | "builder": "@angular-devkit/build-angular:dev-server", 74 | "options": { 75 | "browserTarget": "snipcart-angular:build" 76 | }, 77 | "configurations": { 78 | "production": { 79 | "browserTarget": "snipcart-angular:build:production" 80 | } 81 | } 82 | }, 83 | "extract-i18n": { 84 | "builder": "@angular-devkit/build-angular:extract-i18n", 85 | "options": { 86 | "browserTarget": "snipcart-angular:build" 87 | } 88 | }, 89 | "test": { 90 | "builder": "@angular-devkit/build-angular:karma", 91 | "options": { 92 | "main": "src/test.ts", 93 | "polyfills": "src/polyfills.ts", 94 | "tsConfig": "tsconfig.spec.json", 95 | "karmaConfig": "karma.conf.js", 96 | "assets": [ 97 | "src/favicon.ico", 98 | "src/assets" 99 | ], 100 | "styles": [ 101 | "./node_modules/@angular/material/prebuilt-themes/pink-bluegrey.css", 102 | "src/styles.scss" 103 | ], 104 | "scripts": [] 105 | } 106 | }, 107 | "lint": { 108 | "builder": "@angular-devkit/build-angular:tslint", 109 | "options": { 110 | "tsConfig": [ 111 | "tsconfig.app.json", 112 | "tsconfig.spec.json", 113 | "e2e/tsconfig.json" 114 | ], 115 | "exclude": [ 116 | "**/node_modules/**" 117 | ] 118 | } 119 | }, 120 | "e2e": { 121 | "builder": "@angular-devkit/build-angular:protractor", 122 | "options": { 123 | "protractorConfig": "e2e/protractor.conf.js", 124 | "devServerTarget": "snipcart-angular:serve" 125 | }, 126 | "configurations": { 127 | "production": { 128 | "devServerTarget": "snipcart-angular:serve:production" 129 | } 130 | } 131 | } 132 | } 133 | } 134 | }, 135 | "defaultProject": "snipcart-angular" 136 | } 137 | -------------------------------------------------------------------------------- /api/products/[id].ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from '@vercel/node'; 2 | import { Product } from '../../src/app/core/product'; 3 | import { PRODUCTS } from '../../src/app/mock-products'; 4 | 5 | const findProduct = (id: number): Product | undefined => 6 | PRODUCTS.find((product) => product.id === id); 7 | 8 | export default function fetchProductInfo( 9 | req: VercelRequest, 10 | res: VercelResponse 11 | ) { 12 | const id = Number(req.query.id); 13 | const product = findProduct(id); 14 | res.statusCode = 200; 15 | res.send({ 16 | id: id, 17 | name: product?.name, 18 | price: product?.price, 19 | url: `/products/${id}`, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | SELENIUM_PROMISE_MANAGER: false, 20 | baseUrl: 'http://localhost:4200/', 21 | framework: 'jasmine', 22 | jasmineNodeOpts: { 23 | showColors: true, 24 | defaultTimeoutInterval: 30000, 25 | print: function() {} 26 | }, 27 | onPrepare() { 28 | require('ts-node').register({ 29 | project: require('path').join(__dirname, './tsconfig.json') 30 | }); 31 | jasmine.getEnv().addReporter(new SpecReporter({ 32 | spec: { 33 | displayStacktrace: StacktraceOption.PRETTY 34 | } 35 | })); 36 | } 37 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { browser, logging } from 'protractor'; 2 | import { AppPage } from './app.po'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', async () => { 12 | await page.navigateTo(); 13 | expect(await page.getTitleText()).toEqual('snipcart-angular app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | async navigateTo(): Promise { 5 | return browser.get(browser.baseUrl); 6 | } 7 | 8 | async getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/snipcart-angular'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snipcart-angular", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~11.2.13", 15 | "@angular/cdk": "^11.2.12", 16 | "@angular/common": "~11.2.13", 17 | "@angular/compiler": "~11.2.13", 18 | "@angular/core": "~11.2.13", 19 | "@angular/flex-layout": "^11.0.0-beta.33", 20 | "@angular/forms": "~11.2.13", 21 | "@angular/material": "^11.2.12", 22 | "@angular/platform-browser": "~11.2.13", 23 | "@angular/platform-browser-dynamic": "~11.2.13", 24 | "@angular/router": "~11.2.13", 25 | "rxjs": "~6.6.0", 26 | "tslib": "^2.0.0", 27 | "zone.js": "~0.11.3" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "~0.1102.12", 31 | "@angular/cli": "~11.2.12", 32 | "@angular/compiler-cli": "~11.2.13", 33 | "@types/jasmine": "~3.6.0", 34 | "@types/node": "^12.11.1", 35 | "@vercel/node": "^1.10.0", 36 | "codelyzer": "^6.0.0", 37 | "jasmine-core": "~3.6.0", 38 | "jasmine-spec-reporter": "~5.0.0", 39 | "karma": "~6.1.0", 40 | "karma-chrome-launcher": "~3.1.0", 41 | "karma-coverage": "~2.0.3", 42 | "karma-jasmine": "~4.0.0", 43 | "karma-jasmine-html-reporter": "^1.5.0", 44 | "protractor": "~7.0.0", 45 | "ts-node": "~8.3.0", 46 | "tslint": "~6.1.0", 47 | "typescript": "~4.1.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { HomepageComponent } from './homepage/homepage.component'; 4 | import { ProductPageComponent } from './product-page/product-page.component'; 5 | 6 | const routes: Routes = [ 7 | {path: "product/:id", component: ProductPageComponent}, 8 | {path: "**", component: HomepageComponent}, 9 | ]; 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forRoot(routes)], 13 | exports: [RouterModule] 14 | }) 15 | export class AppRoutingModule { } 16 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .main-div { 2 | height: 100%; 3 | } -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'snipcart-angular'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('snipcart-angular'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement; 33 | expect(compiled.querySelector('.content span').textContent).toContain('snipcart-angular app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'Infinite summer ice cream store'; 10 | subtitle = 'Which one do you want?'; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | import {MatToolbarModule} from '@angular/material/toolbar'; 7 | import { HomepageComponent } from './homepage/homepage.component'; 8 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 9 | import {MatCardModule} from '@angular/material/card'; 10 | import {MatChipsModule} from '@angular/material/chips'; 11 | import {MatIconModule} from '@angular/material/icon'; 12 | import {MatSelectModule} from '@angular/material/select'; 13 | import {MatButtonModule} from '@angular/material/button'; 14 | import { ProductsComponent } from './products/products.component'; 15 | import { ProductComponent } from './product/product.component'; 16 | import { ProductPageComponent } from './product-page/product-page.component'; 17 | import { FlexLayoutModule } from '@angular/flex-layout'; 18 | import { NavbarComponent } from './navbar/navbar.component'; 19 | 20 | @NgModule({ 21 | declarations: [ 22 | AppComponent, 23 | HomepageComponent, 24 | ProductsComponent, 25 | ProductComponent, 26 | ProductPageComponent, 27 | NavbarComponent, 28 | ], 29 | imports: [ 30 | BrowserModule, 31 | AppRoutingModule, 32 | BrowserAnimationsModule, 33 | FlexLayoutModule, 34 | MatButtonModule, 35 | MatCardModule, 36 | MatChipsModule, 37 | MatIconModule, 38 | MatSelectModule, 39 | MatToolbarModule 40 | ], 41 | providers: [], 42 | bootstrap: [AppComponent] 43 | }) 44 | export class AppModule { } 45 | -------------------------------------------------------------------------------- /src/app/core/flavor.ts: -------------------------------------------------------------------------------- 1 | export interface Flavor { 2 | name: string; 3 | color: string; 4 | } -------------------------------------------------------------------------------- /src/app/core/product.ts: -------------------------------------------------------------------------------- 1 | import { Flavor } from "./flavor"; 2 | import { Size } from "./size"; 3 | 4 | export interface Product { 5 | id: number; 6 | name: string; 7 | imageUrls: string[]; 8 | price: number; 9 | flavors: Flavor[]; 10 | sizes: Size[]; 11 | } -------------------------------------------------------------------------------- /src/app/core/selectedProductAttributes.ts: -------------------------------------------------------------------------------- 1 | import { Flavor } from "./flavor"; 2 | import { Size } from "./size"; 3 | 4 | export interface SelectedProductAttributes { 5 | flavor: Flavor | undefined; 6 | size: Size | undefined; 7 | } -------------------------------------------------------------------------------- /src/app/core/size.ts: -------------------------------------------------------------------------------- 1 | export enum Size { 2 | SMALL = "small", 3 | MEDIUM = "medium", 4 | LARGE = "large", 5 | // TODO refactor to object? 6 | } 7 | -------------------------------------------------------------------------------- /src/app/homepage/homepage.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ title }}

3 |

{{ subtitle }}

4 |
5 | 6 | -------------------------------------------------------------------------------- /src/app/homepage/homepage.component.scss: -------------------------------------------------------------------------------- 1 | h2 { 2 | padding: 0.5em 0; 3 | } 4 | 5 | .jumbo { 6 | font-family: 'Pacifico', cursive; 7 | } 8 | 9 | @media (min-width: 900px) { 10 | h2 { 11 | font-size: 26px; 12 | padding: 0.5em 0; 13 | } 14 | 15 | .header { 16 | padding: 4em 0; 17 | } 18 | 19 | .jumbo { 20 | font-size: 48px; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/homepage/homepage.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomepageComponent } from './homepage.component'; 4 | 5 | describe('HomepageComponent', () => { 6 | let component: HomepageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ HomepageComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HomepageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/homepage/homepage.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-homepage', 5 | templateUrl: './homepage.component.html', 6 | styleUrls: ['./homepage.component.scss'] 7 | }) 8 | export class HomepageComponent { 9 | title = 'Infinite summer ice cream store'; 10 | subtitle = 'Which one do you want?'; 11 | } -------------------------------------------------------------------------------- /src/app/mock-products.ts: -------------------------------------------------------------------------------- 1 | import { Product } from './core/product'; 2 | import { Size } from './core/size'; 3 | 4 | export const PRODUCTS: Product[] = [ 5 | { 6 | id: 1, 7 | name: 'Ice Cream', 8 | imageUrls: ['../assets/ice-cream-prune.svg', '../assets/ice-cream-cherry.svg', '../assets/ice-cream-squash.svg'], 9 | price: 10, 10 | flavors: [ 11 | { name: 'prune', color: '#5A188E' }, 12 | { name: 'squash', color: '#F88532' }, 13 | { name: 'cherry', color: '#E91E63' }, 14 | ], 15 | sizes: [Size.SMALL, Size.MEDIUM, Size.LARGE], 16 | }, 17 | { 18 | id: 2, 19 | name: 'Popsicle', 20 | imageUrls: ['../assets/popsicle-lime.svg', '../assets/popsicle-lettuce.svg', '../assets/popsicle-cherry.svg'], 21 | price: 8, 22 | flavors: [ 23 | { name: 'lime', color: '#00CACA' }, 24 | { name: 'lettuce', color: '#80DC0B' }, 25 | { name: 'cherry', color: '#E91E63' }, 26 | ], 27 | sizes: [Size.SMALL, Size.LARGE], 28 | }, 29 | ]; 30 | -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.scss: -------------------------------------------------------------------------------- 1 | .spacer { 2 | flex: 1 1 auto; 3 | } -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NavbarComponent } from './navbar.component'; 4 | 5 | describe('NavbarComponent', () => { 6 | let component: NavbarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ NavbarComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NavbarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-navbar', 5 | templateUrl: './navbar.component.html', 6 | styleUrls: ['./navbar.component.scss'] 7 | }) 8 | export class NavbarComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/product-page/product-page.component.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | {{ product?.name }} 9 | 10 | 11 | 12 | Size 13 | 22 | 23 | {{ size }} 24 | 25 | 26 | 27 |

Select your flavor:

28 | 29 | 40 | {{ flavor.name }} 41 | 42 | 43 |
44 | 45 | 63 | 64 |
65 | -------------------------------------------------------------------------------- /src/app/product-page/product-page.component.scss: -------------------------------------------------------------------------------- 1 | .product-page-card-title { 2 | display: flex; 3 | justify-content: center; 4 | font-family: "Pacifico", cursive; 5 | padding: 1em 0; 6 | } 7 | 8 | .product-page-card { 9 | margin-top: 6em; 10 | } 11 | 12 | .product-page-card img { 13 | object-fit: contain; 14 | max-height: 260px; 15 | } 16 | 17 | .chip { 18 | font-size: 18px; 19 | color: #FDF6EB; 20 | } 21 | 22 | 23 | @media (min-width: 900px) { 24 | .product-page-card img { 25 | object-fit: contain; 26 | } 27 | 28 | .chip:hover { 29 | cursor: pointer; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/app/product-page/product-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductPageComponent } from './product-page.component'; 4 | 5 | describe('ProductPageComponent', () => { 6 | let component: ProductPageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ProductPageComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductPageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/product-page/product-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { Flavor } from '../core/flavor'; 4 | import { Product } from '../core/product'; 5 | import { SelectedProductAttributes } from '../core/selectedProductAttributes'; 6 | import { Size } from '../core/size'; 7 | import { ProductService } from '../product.service'; 8 | 9 | @Component({ 10 | selector: 'app-product-page', 11 | templateUrl: './product-page.component.html', 12 | styleUrls: ['./product-page.component.scss'], 13 | }) 14 | export class ProductPageComponent implements OnInit { 15 | imageUrl: string = ''; 16 | selectedAttributes: SelectedProductAttributes = { 17 | flavor: undefined, 18 | size: undefined, 19 | }; 20 | product: Product | undefined; 21 | 22 | constructor( 23 | private route: ActivatedRoute, 24 | private productService: ProductService 25 | ) {} 26 | 27 | getProduct(): void { 28 | const id = Number(this.route.snapshot.paramMap.get('id')); 29 | this.productService 30 | .getProduct(id) 31 | .subscribe((product) => (this.product = product)); // TODO refactor to getter 32 | } 33 | 34 | get flavorOptions(): string { 35 | return ( 36 | this.product?.flavors?.map((flavor) => flavor.name).join('|') ?? 'yolii' 37 | ); 38 | } 39 | 40 | get sizeOptions(): string { 41 | return this.product?.sizes?.join('|') ?? 'yolii'; //TODO change default to "" 42 | } 43 | 44 | setImageUrl(flavor: Flavor): void { 45 | const flavorImageUrl = this.product?.imageUrls.find((url) => 46 | url.includes(flavor.name) 47 | ); 48 | if (!flavorImageUrl) { 49 | throw Error(`No flavor for ${flavor.name} value`); // TODO refactor for setter 50 | } 51 | this.imageUrl = flavorImageUrl; 52 | } 53 | 54 | ngOnInit() { 55 | this.getProduct(); 56 | this.setSelectedAttributes( 57 | this.product?.flavors[0], 58 | this.product?.sizes[0] 59 | ); 60 | if (this.selectedAttributes?.flavor) { 61 | this.setImageUrl(this.selectedAttributes.flavor); 62 | } 63 | } 64 | 65 | public updateSelectedProductAttributes(flavor: Flavor | undefined, size: Size | undefined) { 66 | this.setSelectedAttributes(flavor ?? {name: "none", color: "#DDD"}, size ?? Size.SMALL); 67 | if (this.selectedAttributes.flavor) { 68 | this.setImageUrl(this.selectedAttributes.flavor); 69 | } 70 | } 71 | 72 | private setSelectedAttributes( 73 | flavor: Flavor | undefined, 74 | size: Size | undefined 75 | ) { 76 | this.selectedAttributes = { 77 | flavor: flavor, 78 | size: size, 79 | }; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/app/product.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductService } from './product.service'; 4 | 5 | describe('ProductService', () => { 6 | let service: ProductService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ProductService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/product.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, of } from 'rxjs'; 3 | import { Product } from './core/product'; 4 | import { PRODUCTS } from './mock-products'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class ProductService { 10 | 11 | constructor() { } 12 | 13 | getProducts(): Product[] { 14 | return PRODUCTS; 15 | } 16 | 17 | getProduct(id: number): Observable { 18 | const product = PRODUCTS.find(product => product.id === id); 19 | return of(product); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/product/product.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ product?.name }} 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/product/product.component.scss: -------------------------------------------------------------------------------- 1 | .product-card { 2 | background-color: #FDF6EB; 3 | color: black; 4 | font-family: 'Pacifico', cursive; 5 | height: 100%; 6 | width: 80%; 7 | } 8 | 9 | .product-card:hover { 10 | cursor: pointer; 11 | } 12 | 13 | .mat-card-image { 14 | object-fit: contain; 15 | max-height: 290px; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/product/product.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductComponent } from './product.component'; 4 | 5 | describe('ProductComponent', () => { 6 | let component: ProductComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ProductComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/product/product.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { Product } from '../core/product'; 3 | 4 | @Component({ 5 | selector: 'app-product', 6 | templateUrl: './product.component.html', 7 | styleUrls: ['./product.component.scss'] 8 | }) 9 | export class ProductComponent implements OnInit { 10 | @Input() product: Product | undefined; 11 | imageUrl :string = ""; 12 | 13 | ngOnInit() { 14 | this.imageUrl = this.product?.imageUrls[0] ?? ''; 15 | } 16 | } -------------------------------------------------------------------------------- /src/app/products/products.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
4 | -------------------------------------------------------------------------------- /src/app/products/products.component.scss: -------------------------------------------------------------------------------- 1 | ul { 2 | margin: 0; 3 | padding: 0; 4 | } -------------------------------------------------------------------------------- /src/app/products/products.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductsComponent } from './products.component'; 4 | 5 | describe('ProductsComponent', () => { 6 | let component: ProductsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ProductsComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/products/products.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Product } from '../core/product'; 3 | import { ProductService } from '../product.service'; 4 | 5 | @Component({ 6 | selector: 'app-products', 7 | templateUrl: './products.component.html', 8 | styleUrls: ['./products.component.scss'] 9 | }) 10 | export class ProductsComponent implements OnInit { 11 | products: Product[] = []; 12 | 13 | constructor(private productService: ProductService) {} 14 | 15 | getProducts(): void { 16 | this.products = this.productService.getProducts(); 17 | } 18 | 19 | ngOnInit() { 20 | this.getProducts(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipcart/snipcart-angular-tutorial/a49440b61ac32aa4d1f04993c6587733b23043dc/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/popsicle-cherry.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/assets/popsicle-lettuce.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/assets/popsicle-lime.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipcart/snipcart-angular-tutorial/a49440b61ac32aa4d1f04993c6587733b23043dc/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SnipcartAngular 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 21 | 22 | 26 | 27 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 44 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * IE11 requires the following for NgClass support on SVG elements 23 | */ 24 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 25 | 26 | /** 27 | * Web Animations `@angular/platform-browser/animations` 28 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 29 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 30 | */ 31 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 32 | 33 | /** 34 | * By default, zone.js will patch all possible macroTask and DomEvents 35 | * user can disable parts of macroTask/DomEvents patch by setting following flags 36 | * because those flags need to be set before `zone.js` being loaded, and webpack 37 | * will put import in the top of bundle, so user need to create a separate file 38 | * in this directory (for example: zone-flags.ts), and put the following flags 39 | * into that file, and then add the following code before importing zone.js. 40 | * import './zone-flags'; 41 | * 42 | * The flags allowed in zone-flags.ts are listed here. 43 | * 44 | * The following flags will work for all browsers. 45 | * 46 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 47 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 48 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 49 | * 50 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 51 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 52 | * 53 | * (window as any).__Zone_enable_cross_context_check = true; 54 | * 55 | */ 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | /*************************************************************************************************** 64 | * APPLICATION IMPORTS 65 | */ 66 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '~@angular/material/theming'; 3 | 4 | $custom-typography: mat-typography-config($font-family: '"Raleway", sans-serif;'); 5 | @include mat-core($custom-typography); 6 | 7 | html, body { height: 100%; background-color: #FDF6EB} 8 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 9 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "sourceMap": true, 12 | "declaration": false, 13 | "downlevelIteration": true, 14 | "experimentalDecorators": true, 15 | "moduleResolution": "node", 16 | "importHelpers": true, 17 | "target": "es2015", 18 | "module": "es2020", 19 | "lib": [ 20 | "es2018", 21 | "dom" 22 | ] 23 | }, 24 | "angularCompilerOptions": { 25 | "enableI18nLegacyMessageIdFormat": false, 26 | "strictInjectionParameters": true, 27 | "strictInputAccessModifiers": true, 28 | "strictTemplates": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-return-shorthand": true, 15 | "curly": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "eofline": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": { 26 | "options": [ 27 | "spaces" 28 | ] 29 | }, 30 | "max-classes-per-file": false, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": { 72 | "options": [ 73 | "always" 74 | ] 75 | }, 76 | "space-before-function-paren": { 77 | "options": { 78 | "anonymous": "never", 79 | "asyncArrow": "always", 80 | "constructor": "never", 81 | "method": "never", 82 | "named": "never" 83 | } 84 | }, 85 | "typedef": [ 86 | true, 87 | "call-signature" 88 | ], 89 | "typedef-whitespace": { 90 | "options": [ 91 | { 92 | "call-signature": "nospace", 93 | "index-signature": "nospace", 94 | "parameter": "nospace", 95 | "property-declaration": "nospace", 96 | "variable-declaration": "nospace" 97 | }, 98 | { 99 | "call-signature": "onespace", 100 | "index-signature": "onespace", 101 | "parameter": "onespace", 102 | "property-declaration": "onespace", 103 | "variable-declaration": "onespace" 104 | } 105 | ] 106 | }, 107 | "variable-name": { 108 | "options": [ 109 | "ban-keywords", 110 | "check-format", 111 | "allow-pascal-case" 112 | ] 113 | }, 114 | "whitespace": { 115 | "options": [ 116 | "check-branch", 117 | "check-decl", 118 | "check-operator", 119 | "check-separator", 120 | "check-type", 121 | "check-typecast" 122 | ] 123 | }, 124 | "component-class-suffix": true, 125 | "contextual-lifecycle": true, 126 | "directive-class-suffix": true, 127 | "no-conflicting-lifecycle": true, 128 | "no-host-metadata-property": true, 129 | "no-input-rename": true, 130 | "no-inputs-metadata-property": true, 131 | "no-output-native": true, 132 | "no-output-on-prefix": true, 133 | "no-output-rename": true, 134 | "no-outputs-metadata-property": true, 135 | "template-banana-in-box": true, 136 | "template-no-negated-async": true, 137 | "use-lifecycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "directive-selector": [ 140 | true, 141 | "attribute", 142 | "app", 143 | "camelCase" 144 | ], 145 | "component-selector": [ 146 | true, 147 | "element", 148 | "app", 149 | "kebab-case" 150 | ] 151 | } 152 | } 153 | --------------------------------------------------------------------------------