From bb35486722527ca7857c24f95a30f0863f8ac03e Mon Sep 17 00:00:00 2001 From: Caleb Ukle Date: Thu, 15 Jun 2023 15:38:50 -0500 Subject: [PATCH] kind of working --- apps/cart-e2e/project.json | 3 +- apps/cart/.env.serve | 1 + apps/cart/functions/api/api.ts | 47 +++++++ apps/cart/project.json | 18 ++- apps/cart/public/index.html | 6 + apps/cart/src/_redirects | 5 +- apps/cart/src/app/app.spec.tsx | 2 + apps/cart/src/app/app.tsx | 6 +- .../cart/src/environments/environment.prod.ts | 1 + apps/cart/src/environments/environment.ts | 1 + apps/products/.env.serve | 1 + apps/products/functions/api/api.ts | 4 +- apps/products/netlify.toml | 15 --- apps/products/project.json | 36 ++---- apps/products/src/_redirects | 5 +- apps/products/src/app/app.module.ts | 3 +- .../src/environments/environment.prod.ts | 1 + apps/products/src/environments/environment.ts | 1 + .../cart-cart-page/cart-cart-page.spec.tsx | 30 ++++- .../src/lib/cart-cart-page/cart-cart-page.tsx | 117 ++++++++++-------- .../src/lib/cart-cart-page/cart-page-hooks.ts | 36 ++++++ .../lib/home-page/home-page.component.spec.ts | 17 ++- .../src/lib/home-page/home-page.component.ts | 1 - .../product-detail-page.component.html | 3 +- .../product-detail-page.component.scss | 7 ++ .../product-detail-page.component.spec.ts | 15 ++- .../product-detail-page.component.ts | 9 +- .../products-product-detail-page.module.ts | 1 - .../cart/state/src/lib/+state/cart.actions.ts | 17 ++- .../cart/state/src/lib/+state/cart.reducer.ts | 10 ++ libs/shared/product/state/src/index.ts | 1 + .../state/src/lib/+state/products.effects.ts | 43 +++++-- .../src/lib/+state/products.reducer.spec.ts | 4 +- .../state/src/lib/+state/products.reducer.ts | 7 +- libs/shared/product/state/src/react.ts | 1 + netlify.toml | 2 - tools/scripts/deploy.ts | 33 ----- 37 files changed, 338 insertions(+), 172 deletions(-) create mode 100644 apps/cart/.env.serve create mode 100644 apps/cart/functions/api/api.ts create mode 100644 apps/cart/public/index.html create mode 100644 apps/products/.env.serve delete mode 100644 apps/products/netlify.toml create mode 100644 libs/cart/cart-page/src/lib/cart-cart-page/cart-page-hooks.ts delete mode 100644 netlify.toml delete mode 100644 tools/scripts/deploy.ts diff --git a/apps/cart-e2e/project.json b/apps/cart-e2e/project.json index bf57fddf..b32731cc 100644 --- a/apps/cart-e2e/project.json +++ b/apps/cart-e2e/project.json @@ -9,7 +9,8 @@ "options": { "cypressConfig": "apps/cart-e2e/cypress.config.ts", "devServerTarget": "cart:serve", - "testingType": "e2e" + "testingType": "e2e", + "baseUrl": "http://localhost:4200" }, "configurations": { "production": { diff --git a/apps/cart/.env.serve b/apps/cart/.env.serve new file mode 100644 index 00000000..7ded5764 --- /dev/null +++ b/apps/cart/.env.serve @@ -0,0 +1 @@ +BASE_API_PATH=/.netlify/functions diff --git a/apps/cart/functions/api/api.ts b/apps/cart/functions/api/api.ts new file mode 100644 index 00000000..6cd40cef --- /dev/null +++ b/apps/cart/functions/api/api.ts @@ -0,0 +1,47 @@ +import { Handler } from '@netlify/functions'; +import fastify, { FastifyInstance, FastifyRequest } from 'fastify'; +import awsLambdaFastify from '@fastify/aws-lambda'; +import sensible from '@fastify/sensible'; +import { products } from '@nx-example/shared/product/data'; +import cors from '@fastify/cors'; +import { randomUUID } from 'crypto'; + +async function routes(fastify: FastifyInstance) { + fastify.get('/products', async () => { + return products; + }); + fastify.post( + '/checkout', + async ( + request: FastifyRequest<{ + Body: { productId: string; quanntity: number }[]; + }> + ) => { + const items = request.body; + console.log(request.body); + const price = items.reduce((acc, item) => { + const product = products.find((p) => p.id === item.productId); + return acc + product.price * item.quanntity; + }, 0); + + // gotta think real hard + await new Promise((resolve) => setTimeout(resolve, Math.random() * 1000)); + + return { success: true, orderId: randomUUID(), total: price }; + } + ); +} + +function init() { + const app = fastify(); + app.register(sensible); + app.register(cors); + // set the prefix for the netlify functions url + app.register(routes, { + prefix: `${process.env.BASE_API_PATH || ''}/api`, + }); + return app; +} + +// Note: Netlify deploys this function at the endpoint /.netlify/functions/api +export const handler: Handler = awsLambdaFastify(init()); diff --git a/apps/cart/project.json b/apps/cart/project.json index bac4ef89..e6edf0d3 100644 --- a/apps/cart/project.json +++ b/apps/cart/project.json @@ -70,7 +70,7 @@ "outputs": ["{options.outputPath}"], "defaultConfiguration": "production" }, - "serve": { + "serve-app": { "executor": "@nx/webpack:dev-server", "options": { "buildTarget": "cart:build" @@ -88,7 +88,10 @@ "lint": { "executor": "@nx/linter:eslint", "options": { - "lintFilePatterns": ["apps/cart/**/*.{ts,tsx,js,jsx}"] + "lintFilePatterns": [ + "apps/cart/**/*.{ts,tsx,js,jsx}", + "./functions/**/*.ts" + ] }, "outputs": ["{options.outputFile}"] }, @@ -100,15 +103,18 @@ }, "outputs": ["{workspaceRoot}/coverage/apps/cart"] }, - "deploy": { + "serve": { "executor": "nx:run-commands", "options": { "commands": [ - { - "command": "npx ts-node --project tools/tsconfig.tools.json tools/scripts/deploy --siteName nrwl-nx-examples-cart --outputPath dist/apps/cart" - } + "BROWSER=false netlify dev --functions=apps/cart/functions", + "nx serve-app cart" ] } + }, + "deploy": { + "dependsOn": ["build"], + "command": "netlify deploy --site=nx-examples-cart-test --dir dist/apps/cart --functions apps/cart/functions --prod" } }, "tags": ["type:app", "scope:cart"], diff --git a/apps/cart/public/index.html b/apps/cart/public/index.html new file mode 100644 index 00000000..29e67ca1 --- /dev/null +++ b/apps/cart/public/index.html @@ -0,0 +1,6 @@ +

Netlify Functions

+

+ The sample function is available at + /.netlify/functions/hello. +

diff --git a/apps/cart/src/_redirects b/apps/cart/src/_redirects index 50d93f23..6b14ac3a 100644 --- a/apps/cart/src/_redirects +++ b/apps/cart/src/_redirects @@ -1,3 +1,4 @@ /cart/* /index.html 200 -/ https://nrwl-nx-examples-products.netlify.com/ 301 -* https://nrwl-nx-examples-products.netlify.com/:splat 301 +/api/* https://nx-examples-cart-test.netlify.app/.netlify/functions/api/:splat 200 +/ https://nx-examples-products-test.netlify.com/cart 301 +* https://nx-examples-products-test.netlify.com/:splat 301 diff --git a/apps/cart/src/app/app.spec.tsx b/apps/cart/src/app/app.spec.tsx index be06392e..cf8f4988 100644 --- a/apps/cart/src/app/app.spec.tsx +++ b/apps/cart/src/app/app.spec.tsx @@ -2,6 +2,8 @@ import { MemoryRouter } from 'react-router-dom'; import { cleanup, render } from '@testing-library/react'; +jest.doMock('@nx-example/cart/cart-page'); +// eslint-disable-next-line import App from './app'; describe('App', () => { diff --git a/apps/cart/src/app/app.tsx b/apps/cart/src/app/app.tsx index 4206822d..3700fc69 100644 --- a/apps/cart/src/app/app.tsx +++ b/apps/cart/src/app/app.tsx @@ -3,13 +3,17 @@ import { Route, Routes } from 'react-router-dom'; import '@nx-example/shared/header'; import { CartCartPage } from '@nx-example/cart/cart-page'; +import { environment } from '../environments/environment'; export const App = () => { return ( <> - } /> + } + /> ); diff --git a/apps/cart/src/environments/environment.prod.ts b/apps/cart/src/environments/environment.prod.ts index c9669790..f1c78864 100644 --- a/apps/cart/src/environments/environment.prod.ts +++ b/apps/cart/src/environments/environment.prod.ts @@ -1,3 +1,4 @@ export const environment = { production: true, + baseApiPath: '', }; diff --git a/apps/cart/src/environments/environment.ts b/apps/cart/src/environments/environment.ts index 7ed83767..704bc470 100644 --- a/apps/cart/src/environments/environment.ts +++ b/apps/cart/src/environments/environment.ts @@ -3,4 +3,5 @@ export const environment = { production: false, + baseApiPath: 'http://localhost:8888/.netlify/functions', }; diff --git a/apps/products/.env.serve b/apps/products/.env.serve new file mode 100644 index 00000000..7ded5764 --- /dev/null +++ b/apps/products/.env.serve @@ -0,0 +1 @@ +BASE_API_PATH=/.netlify/functions diff --git a/apps/products/functions/api/api.ts b/apps/products/functions/api/api.ts index c4559c6a..e2460d55 100644 --- a/apps/products/functions/api/api.ts +++ b/apps/products/functions/api/api.ts @@ -1,5 +1,5 @@ import { Handler } from '@netlify/functions'; -import fastify, { FastifyInstance, FastifyRequest } from 'fastify'; +import fastify, { FastifyInstance } from 'fastify'; import awsLambdaFastify from '@fastify/aws-lambda'; import sensible from '@fastify/sensible'; import { products } from '@nx-example/shared/product/data'; @@ -16,7 +16,7 @@ function init() { app.register(sensible); app.register(cors); // set the prefix for the netlify functions url - app.register(routes, { prefix: '/.netlify/functions/api' }); + app.register(routes, { prefix: `${process.env.BASE_API_PATH || ''}/api` }); return app; } diff --git a/apps/products/netlify.toml b/apps/products/netlify.toml deleted file mode 100644 index 01a8bbe4..00000000 --- a/apps/products/netlify.toml +++ /dev/null @@ -1,15 +0,0 @@ -[build] -# Static files from this folder will be served at the root of the site. -publish = "public" -[functions] -# Directory with serverless functions, including background -# functions, to deploy. This is relative to the base directory -# if one has been set, or the root directory if -# a base hasn’t been set. -directory = "functions/" - -# Specifies \`esbuild\` for functions bundling, esbuild is the default for TS -# node_bundler = "esbuild" - -[functions."hello*"] -# Apply settings to any functions with a name beginning with "hello" \ No newline at end of file diff --git a/apps/products/project.json b/apps/products/project.json index 302f2d54..10f752b4 100644 --- a/apps/products/project.json +++ b/apps/products/project.json @@ -50,14 +50,14 @@ "with": "apps/products/src/environments/environment.prod.ts" } ], - "optimization": true, + "optimization": false, "outputHashing": "all", - "sourceMap": false, + "sourceMap": true, "namedChunks": false, - "aot": true, + "aot": false, "extractLicenses": true, "vendorChunk": false, - "buildOptimizer": true, + "buildOptimizer": false, "budgets": [ { "type": "initial", @@ -73,7 +73,7 @@ }, "outputs": ["{options.outputPath}"] }, - "serve": { + "serve-app": { "executor": "@angular-devkit/build-angular:dev-server", "options": { "browserTarget": "products:build" @@ -109,31 +109,19 @@ }, "outputs": ["{workspaceRoot}/coverage/apps/products"] }, - "deploy": { + + "serve": { "executor": "nx:run-commands", "options": { "commands": [ - { - "command": "npx ts-node --project tools/tsconfig.tools.json tools/scripts/deploy --siteName nrwl-nx-examples-products --outputPath dist/apps/products" - } + "BROWSER=false netlify dev --functions=apps/products/functions", + "nx serve-app products" ] } }, - "serve-functions": { - "command": "npx netlify dev" - }, - "deploy-functions": { - "dependsOn": ["lint"], - "command": "npx netlify deploy", - "options": { - "cwd": "apps/products" - }, - "configurations": { - "production": { - "command": "npx netlify deploy --prod", - "cwd": "apps/products" - } - } + "deploy": { + "dependsOn": ["build"], + "command": "netlify deploy --site=nx-examples-products-test --dir dist/apps/products --functions apps/products/functions --prod" } }, "tags": ["type:app", "scope:products"], diff --git a/apps/products/src/_redirects b/apps/products/src/_redirects index 7cbf76be..3cfa59e9 100644 --- a/apps/products/src/_redirects +++ b/apps/products/src/_redirects @@ -1,2 +1,3 @@ -/cart https://nrwl-nx-examples-cart.netlify.com/cart 301 -/* /index.html 200 +/api/* https://nx-examples-products-test.netlify.app/.netlify/functions/api/:splat 200 +/cart https://nx-examples-cart-test.netlify.com/cart 301 +/* /index.html 200 diff --git a/apps/products/src/app/app.module.ts b/apps/products/src/app/app.module.ts index 854b3446..db81db7f 100644 --- a/apps/products/src/app/app.module.ts +++ b/apps/products/src/app/app.module.ts @@ -5,6 +5,7 @@ import { RouterModule } from '@angular/router'; import { StoreModule } from '@ngrx/store'; import { AppComponent } from './app.component'; import { EffectsModule } from '@ngrx/effects'; +import { environment } from '../environments/environment'; @NgModule({ declarations: [AppComponent], @@ -33,7 +34,7 @@ import { EffectsModule } from '@ngrx/effects'; StoreModule.forRoot({}), EffectsModule.forRoot(), ], - providers: [], + providers: [{ provide: 'BASE_API_PATH', useValue: environment.baseApiPath }], bootstrap: [AppComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) diff --git a/apps/products/src/environments/environment.prod.ts b/apps/products/src/environments/environment.prod.ts index c9669790..f1c78864 100644 --- a/apps/products/src/environments/environment.prod.ts +++ b/apps/products/src/environments/environment.prod.ts @@ -1,3 +1,4 @@ export const environment = { production: true, + baseApiPath: '', }; diff --git a/apps/products/src/environments/environment.ts b/apps/products/src/environments/environment.ts index 99c3763c..16f9a302 100644 --- a/apps/products/src/environments/environment.ts +++ b/apps/products/src/environments/environment.ts @@ -4,6 +4,7 @@ export const environment = { production: false, + baseApiPath: 'http://localhost:8888/.netlify/functions', }; /* diff --git a/libs/cart/cart-page/src/lib/cart-cart-page/cart-cart-page.spec.tsx b/libs/cart/cart-page/src/lib/cart-cart-page/cart-cart-page.spec.tsx index 496df785..019d76ad 100644 --- a/libs/cart/cart-page/src/lib/cart-cart-page/cart-cart-page.spec.tsx +++ b/libs/cart/cart-page/src/lib/cart-cart-page/cart-cart-page.spec.tsx @@ -1,7 +1,31 @@ +import { useReducer } from 'react'; +jest.doMock('./cart-page-hooks', () => { + return { + useProducts: (): ReturnType => { + const [cartState, dispatchCart] = useReducer(cartReducer, { + items: products.map((p) => ({ productId: p.id, quantity: 1 })), + }); + const [productsState, dispatchProducts] = useReducer(productsReducer, { + products, + }); + + return [ + { cart: cartState, products: productsState }, + { cart: dispatchCart, products: dispatchProducts }, + ]; + }, + }; +}); + import { cleanup, fireEvent, render } from '@testing-library/react'; +import { products } from '@nx-example/shared/product/data'; + +import { cartReducer } from '@nx-example/shared/cart/state/react'; +import { productsReducer } from '@nx-example/shared/product/state/react'; import CartCartPage from './cart-cart-page'; +import { useProducts } from './cart-page-hooks'; describe(' CartCartPage', () => { afterEach(cleanup); @@ -11,9 +35,9 @@ describe(' CartCartPage', () => { }); it('should render products', () => { - expect( - render().baseElement.querySelectorAll('li figure').length - ).toEqual(5); + const { baseElement } = render(); + + expect(baseElement.querySelectorAll('li figure').length).toEqual(5); }); it('should render a total', () => { diff --git a/libs/cart/cart-page/src/lib/cart-cart-page/cart-cart-page.tsx b/libs/cart/cart-page/src/lib/cart-cart-page/cart-cart-page.tsx index 384ca9a1..1e5b2891 100644 --- a/libs/cart/cart-page/src/lib/cart-cart-page/cart-cart-page.tsx +++ b/libs/cart/cart-page/src/lib/cart-cart-page/cart-cart-page.tsx @@ -1,21 +1,16 @@ -import { useReducer } from 'react'; - import styled from '@emotion/styled'; import '@nx-example/shared/product/ui'; import { CartItem, - cartReducer, getItemCost, getTotalCost, SetQuantity, + CheckoutSuccess, } from '@nx-example/shared/cart/state/react'; -import { - getProduct, - initialState, - productsReducer, -} from '@nx-example/shared/product/state/react'; +import { getProduct } from '@nx-example/shared/product/state/react'; +import { useProducts } from './cart-page-hooks'; const StyledUl = styled.ul` display: flex; @@ -80,60 +75,78 @@ const StyledTotalLi = styled.li` const optionsArray = new Array(5).fill(null); -export const CartCartPage = () => { - const [productsState] = useReducer(productsReducer, initialState); - const { products } = productsState; - const [cartState, dispatch] = useReducer(cartReducer, { - items: products.map((product) => ({ - productId: product.id, - quantity: 1, - })), - }); +export const CartCartPage = (props) => { + const [state, dispatch] = useProducts(props.baseUrl); + + const handleCheckout = () => { + fetch('/api/checkout', { + method: 'POST', + body: JSON.stringify(state.cart.items), + headers: { + 'content-type': 'application/json', + }, + }) + .then((r) => r.json()) + .then((r) => { + dispatch.cart(new CheckoutSuccess(r.orderId)); + }); + }; return ( - {cartState.items.map((item: CartItem) => ( - - -
- -
-
- -

{getProduct(productsState, item.productId).name}

-
-

- -

- -

- -

-
- ))} + {state.products.products.length > 0 && + state.cart.items.length > 0 && + state.cart.items.map((item: CartItem) => ( + + +
+ +
+
+ +

{getProduct(state.products, item.productId)?.name}

+
+

+ +

+ +

+ +

+
+ ))}

Total

+

+ {state.cart.orderId ? ( + Checkout Success! Order ID: {state.cart.orderId} + ) : ( + + )} +

); }; diff --git a/libs/cart/cart-page/src/lib/cart-cart-page/cart-page-hooks.ts b/libs/cart/cart-page/src/lib/cart-cart-page/cart-page-hooks.ts new file mode 100644 index 00000000..bbdf4775 --- /dev/null +++ b/libs/cart/cart-page/src/lib/cart-cart-page/cart-page-hooks.ts @@ -0,0 +1,36 @@ +import { Product } from '@nx-example/shared/product/types'; +import { useEffect, useReducer } from 'react'; +import { cartReducer, SetItems } from '@nx-example/shared/cart/state/react'; +import { + initialState, + productsReducer, + LoadProductsSuccess, +} from '@nx-example/shared/product/state/react'; + +export const useProducts = (baseUrl: string) => { + const [cartState, dispatchCart] = useReducer(cartReducer, { + items: [], + }); + const [productsState, dispatchProducts] = useReducer( + productsReducer, + initialState + ); + + const cb = (products: Product[]): void => { + dispatchProducts(new LoadProductsSuccess(products)); + dispatchCart( + new SetItems(products.map((p) => ({ productId: p.id, quantity: 1 }))) + ); + }; + + useEffect(() => { + fetch(baseUrl + '/api/products') + .then((r) => r.json()) + .then(cb); + }, []); + + return [ + { cart: cartState, products: productsState }, + { cart: dispatchCart, products: dispatchProducts }, + ] as const; +}; diff --git a/libs/products/home-page/src/lib/home-page/home-page.component.spec.ts b/libs/products/home-page/src/lib/home-page/home-page.component.spec.ts index 87d1216a..1435d7b0 100644 --- a/libs/products/home-page/src/lib/home-page/home-page.component.spec.ts +++ b/libs/products/home-page/src/lib/home-page/home-page.component.spec.ts @@ -2,11 +2,15 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; -import { StoreModule } from '@ngrx/store'; - -import { SharedProductStateModule } from '@nx-example/shared/product/state'; +import { + createMockProductService, + SharedProductStateModule, +} from '@nx-example/shared/product/state'; +import { products } from '@nx-example/shared/product/data'; import { HomePageComponent } from './home-page.component'; +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; describe('HomePageComponent', () => { let component: HomePageComponent; @@ -15,12 +19,17 @@ describe('HomePageComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - StoreModule.forRoot({}), RouterTestingModule, + StoreModule.forRoot({}), + EffectsModule.forRoot(), SharedProductStateModule, ], declarations: [HomePageComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], + providers: [ + createMockProductService(products), + { provide: 'BASE_API_PATH', useValue: '' }, + ], }).compileComponents(); })); diff --git a/libs/products/home-page/src/lib/home-page/home-page.component.ts b/libs/products/home-page/src/lib/home-page/home-page.component.ts index d57240eb..34ab565d 100644 --- a/libs/products/home-page/src/lib/home-page/home-page.component.ts +++ b/libs/products/home-page/src/lib/home-page/home-page.component.ts @@ -26,7 +26,6 @@ export class HomePageComponent implements OnInit { constructor(private store: Store) {} ngOnInit() { - console.log('cmp init'); this.store.dispatch(new LoadProducts()); } } diff --git a/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.html b/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.html index ecf457f9..5199e886 100644 --- a/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.html +++ b/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.html @@ -12,8 +12,7 @@

{{ product.name }}

>

- - + Buy

diff --git a/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.scss b/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.scss index 98fbca89..d287aa12 100644 --- a/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.scss +++ b/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.scss @@ -25,3 +25,10 @@ img { max-width: 100%; max-height: 100%; } + +a.buy-link { + padding: 0.5rem 1rem; + border-radius: 4px; + background-color: #143055; + color: #eee; +} diff --git a/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.spec.ts b/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.spec.ts index 0de2f97b..cca8a372 100644 --- a/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.spec.ts +++ b/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.spec.ts @@ -5,9 +5,14 @@ import { ActivatedRoute } from '@angular/router'; import { StoreModule } from '@ngrx/store'; import { of } from 'rxjs'; -import { SharedProductStateModule } from '@nx-example/shared/product/state'; +import { + createMockProductService, + SharedProductStateModule, +} from '@nx-example/shared/product/state'; +import { products } from '@nx-example/shared/product/data'; import { ProductDetailPageComponent } from './product-detail-page.component'; +import { EffectsModule } from '@ngrx/effects'; class MockActivatedRoute { paramMap = of(new Map([['productId', '1']])); @@ -19,12 +24,18 @@ describe('ProductDetailPageComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [StoreModule.forRoot({}), SharedProductStateModule], + imports: [ + StoreModule.forRoot(), + EffectsModule.forRoot(), + SharedProductStateModule, + ], providers: [ { provide: ActivatedRoute, useClass: MockActivatedRoute, }, + createMockProductService(products), + { provide: 'BASE_API_PATH', useValue: '' }, ], declarations: [ProductDetailPageComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], diff --git a/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.ts b/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.ts index c20bd27b..77fd92e5 100644 --- a/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.ts +++ b/libs/products/product-detail-page/src/lib/product-detail-page/product-detail-page.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { select, Store } from '@ngrx/store'; @@ -7,6 +7,7 @@ import { concatMap, map } from 'rxjs/operators'; import { getProduct, getProductsState, + LoadProducts, ProductsPartialState, } from '@nx-example/shared/product/state'; import '@nx-example/shared/product/ui'; @@ -16,7 +17,7 @@ import '@nx-example/shared/product/ui'; templateUrl: './product-detail-page.component.html', styleUrls: ['./product-detail-page.component.scss'], }) -export class ProductDetailPageComponent { +export class ProductDetailPageComponent implements OnInit { product = this.route.paramMap.pipe( map((paramMap) => paramMap.get('productId')), concatMap((productId) => @@ -27,4 +28,8 @@ export class ProductDetailPageComponent { private store: Store, private route: ActivatedRoute ) {} + + ngOnInit(): void { + this.store.dispatch(new LoadProducts()); + } } diff --git a/libs/products/product-detail-page/src/lib/products-product-detail-page.module.ts b/libs/products/product-detail-page/src/lib/products-product-detail-page.module.ts index 3d77c3b1..833a1b9f 100644 --- a/libs/products/product-detail-page/src/lib/products-product-detail-page.module.ts +++ b/libs/products/product-detail-page/src/lib/products-product-detail-page.module.ts @@ -10,7 +10,6 @@ import { ProductDetailPageComponent } from './product-detail-page/product-detail imports: [ CommonModule, SharedProductStateModule, - RouterModule.forChild([ { path: ':productId', component: ProductDetailPageComponent }, ]), diff --git a/libs/shared/cart/state/src/lib/+state/cart.actions.ts b/libs/shared/cart/state/src/lib/+state/cart.actions.ts index 3cff41aa..bc824c50 100644 --- a/libs/shared/cart/state/src/lib/+state/cart.actions.ts +++ b/libs/shared/cart/state/src/lib/+state/cart.actions.ts @@ -1,8 +1,15 @@ import type { Action } from '@ngrx/store'; export enum CartActionTypes { - /* eslint-disable @typescript-eslint/no-shadow */ SetQuantity = '[Cart] Set Quantity', + SetItems = '[Cart] Set Items', + Checkout = '[Cart] Checkout Success', +} + +export class SetItems implements Action { + readonly type = CartActionTypes.SetItems; + + constructor(public items: { productId: string; quantity: number }[]) {} } export class SetQuantity implements Action { @@ -11,4 +18,10 @@ export class SetQuantity implements Action { constructor(public productId: string, public quantity: number) {} } -export type CartAction = SetQuantity; +export class CheckoutSuccess implements Action { + readonly type = CartActionTypes.Checkout; + + constructor(public orderId: string) {} +} + +export type CartAction = SetQuantity | SetItems | CheckoutSuccess; diff --git a/libs/shared/cart/state/src/lib/+state/cart.reducer.ts b/libs/shared/cart/state/src/lib/+state/cart.reducer.ts index 0583a02d..0e82554b 100644 --- a/libs/shared/cart/state/src/lib/+state/cart.reducer.ts +++ b/libs/shared/cart/state/src/lib/+state/cart.reducer.ts @@ -9,6 +9,7 @@ export interface CartItem { export interface CartState { items: CartItem[]; + orderId?: string; } export interface CartPartialState { @@ -21,6 +22,15 @@ export const initialState: CartState = { export const cartReducer = (state: CartState, action: CartAction) => { switch (action.type) { + case CartActionTypes.Checkout: { + return { + ...state, + orderId: action.orderId, + }; + } + case CartActionTypes.SetItems: { + return { items: action.items }; + } case CartActionTypes.SetQuantity: { return { ...state, diff --git a/libs/shared/product/state/src/index.ts b/libs/shared/product/state/src/index.ts index bd3bb6ec..3cbbb86d 100644 --- a/libs/shared/product/state/src/index.ts +++ b/libs/shared/product/state/src/index.ts @@ -6,3 +6,4 @@ export * from './lib/+state/products.selectors'; export * from './lib/+state/products.actions'; export const getProductsState = createFeatureSelector(PRODUCTS_FEATURE_KEY); export * from './lib/shared-product-state.module'; +export * from './lib/+state/products.effects'; diff --git a/libs/shared/product/state/src/lib/+state/products.effects.ts b/libs/shared/product/state/src/lib/+state/products.effects.ts index e9462894..b2eb206e 100644 --- a/libs/shared/product/state/src/lib/+state/products.effects.ts +++ b/libs/shared/product/state/src/lib/+state/products.effects.ts @@ -1,7 +1,21 @@ -import { Injectable } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { LoadProductsSuccess, ProductsActionTypes } from './products.actions'; -import { exhaustMap } from 'rxjs'; +import { exhaustMap, map, of } from 'rxjs'; +import { Product } from '@nx-example/shared/product/types'; + +@Injectable({ providedIn: 'root' }) +export class ProductsService { + constructor( + private http: HttpClient, + @Inject('BASE_API_PATH') private baseUrl: string + ) {} + + getProducts() { + return this.http.get(`${this.baseUrl}/products`); + } +} @Injectable({ providedIn: 'root' }) export class ProductsEffects { @@ -9,14 +23,27 @@ export class ProductsEffects { this.actions$.pipe( ofType(ProductsActionTypes.LoadProducts), exhaustMap(() => - fetch('http://localhost:8888/.netlify/functions/api/products') - .then((r) => r.json()) - .then((p) => new LoadProductsSuccess(p)) + this.productsService + .getProducts() + .pipe(map((p) => new LoadProductsSuccess(p))) ) ) ); - constructor(private actions$: Actions) { - console.log('ProductsEffects created'); - } + constructor( + private actions$: Actions, + private productsService: ProductsService + ) {} } + +export const createMockProductService = (products: Product[]) => { + class MockProductsService { + getProducts() { + return of(products); + } + } + return { + provide: ProductsService, + useClass: MockProductsService, + }; +}; diff --git a/libs/shared/product/state/src/lib/+state/products.reducer.spec.ts b/libs/shared/product/state/src/lib/+state/products.reducer.spec.ts index b2821f72..6e08d278 100644 --- a/libs/shared/product/state/src/lib/+state/products.reducer.spec.ts +++ b/libs/shared/product/state/src/lib/+state/products.reducer.spec.ts @@ -1,7 +1,7 @@ -import { Action } from '@ngrx/store'; import { mockProducts } from '@nx-example/shared/product/data/testing'; import { productsReducer, ProductsState } from './products.reducer'; +import { ProductsAction } from './products.actions'; describe('Products Reducer', () => { let productsState: ProductsState; @@ -14,7 +14,7 @@ describe('Products Reducer', () => { describe('unknown action', () => { it('should return the initial state', () => { - const action = {} as Action; + const action = {} as ProductsAction; const result = productsReducer(productsState, action); expect(result).toBe(productsState); diff --git a/libs/shared/product/state/src/lib/+state/products.reducer.ts b/libs/shared/product/state/src/lib/+state/products.reducer.ts index 520c69c0..4dd3606b 100644 --- a/libs/shared/product/state/src/lib/+state/products.reducer.ts +++ b/libs/shared/product/state/src/lib/+state/products.reducer.ts @@ -1,6 +1,6 @@ import { Product } from '@nx-example/shared/product/types'; -import { ProductsAction } from './products.actions'; +import { ProductsAction, ProductsActionTypes } from './products.actions'; export const PRODUCTS_FEATURE_KEY = 'products'; @@ -20,11 +20,10 @@ export function productsReducer( state: ProductsState = initialState, action: ProductsAction ): ProductsState { - console.log('productsReducer', action.type); switch (action.type) { - case '[Products] Load Products Success': + case ProductsActionTypes.LoadProductsSuccess: return { products: action.products }; - case '[Products] Load Products': + case ProductsActionTypes.LoadProducts: return { products: [] }; default: { return state; diff --git a/libs/shared/product/state/src/react.ts b/libs/shared/product/state/src/react.ts index fa1cb475..90a1f7f2 100644 --- a/libs/shared/product/state/src/react.ts +++ b/libs/shared/product/state/src/react.ts @@ -1,2 +1,3 @@ export * from './lib/+state/products.reducer'; export * from './lib/+state/products.selectors'; +export * from './lib/+state/products.actions'; diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index acfd3e7d..00000000 --- a/netlify.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -base = "apps/products" diff --git a/tools/scripts/deploy.ts b/tools/scripts/deploy.ts deleted file mode 100644 index 4e8d2ff3..00000000 --- a/tools/scripts/deploy.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { existsSync } from 'fs'; -import * as minimist from 'minimist'; -import * as NetlifyClient from 'netlify'; -import { join } from 'path'; - -const token = process.env.NETLIFY_AUTH_TOKEN; - -const argv = minimist(process.argv.slice(2)); - -const netlifyClient = new NetlifyClient(token); -const root = join(__dirname, '../..'); -const outDir = join(root, argv.outputPath); - -if (!existsSync(outDir)) { - throw new Error(`${outDir} does not exist`); -} - -(async () => { - try { - const sites = await netlifyClient.listSites(); - const site = sites.find((s) => argv.siteName === s.name); - if (!site) { - throw Error(`Could not find site ${argv.siteName}`); - } - console.log(`Deploying ${argv.siteName} to Netlify...`); - const deployResult = await netlifyClient.deploy(site.id, outDir); - console.log( - `\n🚀 New version of ${argv.siteName} is running at ${deployResult.deploy.ssl_url}!\n` - ); - } catch (e) { - console.error('Authentication Failure: Invalid Token'); - } -})();