├── .gitignore ├── README.md ├── ecomm-original ├── .babelrc ├── package.json ├── postcss.config.js ├── src │ ├── App.tsx │ ├── components │ │ ├── CartContent.tsx │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── HomeContent.tsx │ │ ├── Login.tsx │ │ ├── MainLayout.tsx │ │ ├── MiniCart.tsx │ │ └── PDPContent.tsx │ ├── index.html │ ├── index.scss │ ├── index.ts │ └── lib │ │ ├── cart.ts │ │ └── products.ts ├── tailwind.config.js ├── webpack.config.js └── yarn.lock ├── ecomm ├── .babelrc ├── package.json ├── postcss.config.js ├── src │ ├── App.tsx │ ├── components │ │ ├── CartContent.tsx │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── HomeContent.tsx │ │ ├── Login.tsx │ │ ├── MainLayout.tsx │ │ ├── MiniCart.tsx │ │ ├── PDPContent.tsx │ │ ├── homeModule.tsx │ │ └── pdpModule.tsx │ ├── index.html │ ├── index.scss │ ├── index.ts │ ├── lib │ │ ├── cart.ts │ │ └── products.ts │ └── router.tsx ├── tailwind.config.js ├── webpack.config.js └── yarn.lock ├── server ├── .eslintrc.js ├── .prettierrc ├── README.md ├── nest-cli.json ├── package.json ├── public │ ├── fidget-1.jpg │ ├── fidget-10.jpg │ ├── fidget-11.jpg │ ├── fidget-2.jpg │ ├── fidget-3.jpg │ ├── fidget-5.jpg │ ├── fidget-6.jpg │ ├── fidget-7.jpg │ ├── fidget-8.jpg │ └── fidget-9.jpg ├── src │ ├── app.controller.ts │ ├── app.module.ts │ ├── auth │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── constants.ts │ │ ├── jwt-auth.guard.ts │ │ ├── jwt.strategy.ts │ │ ├── local-auth.guard.ts │ │ └── local.strategy.ts │ ├── config.ts │ ├── main.ts │ ├── modules │ │ ├── cart │ │ │ ├── cart.controller.ts │ │ │ └── cart.module.ts │ │ └── products │ │ │ ├── products.controller.ts │ │ │ └── products.module.ts │ ├── products.ts │ └── users │ │ ├── users.module.ts │ │ └── users.service.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock └── testapp ├── .babelrc ├── package.json ├── postcss.config.js ├── src ├── App.tsx ├── index.html ├── index.scss └── index.ts ├── tailwind.config.js ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple eCommerce Example 2 | =============================================== 3 | 4 | A simepl eCommerce app using [react-router-dom](https://www.npmjs.com/package/react-router-dom) to manage the routing. 5 | 6 | # Installation 7 | 8 | ```sh 9 | cd ecomm 10 | yarn && yarn start 11 | ``` 12 | 13 | And 14 | 15 | ```sh 16 | cd server 17 | yarn && yarn start 18 | ``` 19 | -------------------------------------------------------------------------------- /ecomm-original/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-typescript", "@babel/preset-react", "@babel/preset-env"], 3 | "plugins": [ 4 | ["@babel/transform-runtime"] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /ecomm-original/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecomm", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "webpack --mode production", 6 | "build:dev": "webpack --mode development", 7 | "build:start": "cd dist && PORT=3000 npx serve", 8 | "start": "webpack serve --open --mode development", 9 | "start:live": "webpack serve --open --mode development --live-reload --hot" 10 | }, 11 | "license": "MIT", 12 | "author": { 13 | "name": "Jack Herrington", 14 | "email": "jherr@pobox.com" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.15.8", 18 | "@babel/plugin-transform-runtime": "^7.15.8", 19 | "@babel/preset-env": "^7.15.8", 20 | "@babel/preset-react": "^7.14.5", 21 | "@babel/preset-typescript": "^7.10.4", 22 | "@types/react": "^17.0.2", 23 | "@types/react-dom": "^17.0.2", 24 | "autoprefixer": "^10.1.0", 25 | "babel-loader": "^8.2.2", 26 | "css-loader": "^6.3.0", 27 | "html-webpack-plugin": "^5.3.2", 28 | "postcss": "^8.2.1", 29 | "postcss-loader": "^4.1.0", 30 | "style-loader": "^3.3.0", 31 | "tailwindcss": "^2.0.2", 32 | "webpack": "^5.57.1", 33 | "webpack-cli": "^4.9.0", 34 | "webpack-dev-server": "^4.3.1" 35 | }, 36 | "dependencies": { 37 | "react": "^17.0.2", 38 | "react-dom": "^17.0.2", 39 | "react-router-dom": "^6.0.2", 40 | "remixicon": "^2.5.0", 41 | "rxjs": "^7.4.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ecomm-original/postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require("autoprefixer"); 2 | const tailwindcss = require("tailwindcss"); 3 | 4 | module.exports = { 5 | plugins: [tailwindcss, autoprefixer], 6 | }; 7 | -------------------------------------------------------------------------------- /ecomm-original/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import "remixicon/fonts/remixicon.css"; 5 | import "./index.scss"; 6 | 7 | import MainLayout from "./components/MainLayout"; 8 | 9 | ReactDOM.render(, document.getElementById("app")); 10 | -------------------------------------------------------------------------------- /ecomm-original/src/components/CartContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | import { CartItem, cart, clearCart } from "../lib/cart"; 4 | import { currency } from "../lib/products"; 5 | 6 | export default function CartContent() { 7 | const [items, setItems] = useState([]); 8 | 9 | useEffect(() => { 10 | const sub = cart.subscribe((value) => setItems(value?.cartItems ?? [])); 11 | return () => sub.unsubscribe(); 12 | }, []); 13 | 14 | return ( 15 | <> 16 |
17 | {items.map((item) => ( 18 | 19 |
{item.quantity}
20 | {item.name} 21 |
{item.name}
22 |
23 | {currency.format(item.quantity * item.price)} 24 |
25 |
26 | ))} 27 |
28 |
29 |
30 |
31 | {currency.format(items.reduce((a, v) => a + v.quantity * v.price, 0))} 32 |
33 |
34 | {items.length > 0 && ( 35 |
36 |
37 | 44 |
45 |
46 | 52 |
53 |
54 | )} 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /ecomm-original/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Footer() { 4 | return ( 5 |
6 | Only The Best Spinners 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /ecomm-original/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import MiniCart from "./MiniCart"; 5 | import Login from "./Login"; 6 | 7 | export default function Header() { 8 | return ( 9 |
10 |
11 |
12 | Fidget Spinner World 13 |
|
14 | 15 | Cart 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /ecomm-original/src/components/HomeContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import { Product, getProducts, currency } from "../lib/products"; 5 | import { addToCart, useLoggedIn } from "../lib/cart"; 6 | 7 | export default function HomeContent() { 8 | const loggedIn = useLoggedIn(); 9 | const [products, setProducts] = useState([]); 10 | 11 | useEffect(() => { 12 | getProducts().then(setProducts); 13 | }, []); 14 | 15 | return ( 16 |
17 | {products.map((product) => ( 18 |
19 | 20 | {product.name} 21 | 22 |
23 |
24 | {product.name} 25 |
26 |
{currency.format(product.price)}
27 |
28 |
{product.description}
29 | {loggedIn && ( 30 |
31 | 38 |
39 | )} 40 |
41 | ))} 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /ecomm-original/src/components/Login.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import { login, useLoggedIn } from "../lib/cart"; 4 | 5 | export default function Login() { 6 | const loggedIn = useLoggedIn(); 7 | const [showLogin, setShowLogin] = useState(false); 8 | 9 | const [username, setUsername] = useState("sally"); 10 | const [password, setPassword] = useState("123"); 11 | 12 | if (loggedIn) return null; 13 | 14 | return ( 15 | <> 16 | setShowLogin(!showLogin)}> 17 | 18 | 19 | {showLogin && ( 20 |
28 | setUsername(evt.target.value)} 33 | className="border text-sm border-gray-400 p-2 rounded-md w-full" 34 | /> 35 | setPassword(evt.target.value)} 39 | className="border text-sm border-gray-400 p-2 rounded-md w-full mt-3" 40 | /> 41 | 48 |
49 | )} 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /ecomm-original/src/components/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; 3 | 4 | import Header from "./Header"; 5 | import Footer from "./Footer"; 6 | 7 | import PDPContent from "./PDPContent"; 8 | import HomeContent from "./HomeContent"; 9 | import CartContent from "./CartContent"; 10 | 11 | export default function MainLayout() { 12 | return ( 13 | 14 |
15 |
16 |
17 | 18 | } /> 19 | } /> 20 | } /> 21 | 22 |
23 |
24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /ecomm-original/src/components/MiniCart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | import { cart, clearCart } from "../lib/cart"; 4 | import { currency } from "../lib/products"; 5 | 6 | export default function MiniCart() { 7 | const [items, setItems] = useState(undefined); 8 | const [showCart, setShowCart] = useState(false); 9 | 10 | useEffect(() => { 11 | setItems(cart.value?.cartItems); 12 | const sub = cart.subscribe((c) => { 13 | setItems(c?.cartItems); 14 | }); 15 | return () => sub.unsubscribe(); 16 | }, []); 17 | 18 | if (!items) return null; 19 | 20 | return ( 21 | <> 22 | setShowCart(!showCart)} id="showcart_span"> 23 | 24 | {items.length} 25 | 26 | {showCart && ( 27 | <> 28 |
36 |
42 | {items.map((item) => ( 43 | 44 |
{item.quantity}
45 | {item.name} 46 |
{item.name}
47 |
48 | {currency.format(item.quantity * item.price)} 49 |
50 |
51 | ))} 52 |
53 |
54 |
55 |
56 | {currency.format( 57 | items.reduce((a, v) => a + v.quantity * v.price, 0) 58 | )} 59 |
60 |
61 |
62 |
63 | 70 |
71 |
72 | 78 |
79 |
80 |
81 | 82 | )} 83 | 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /ecomm-original/src/components/PDPContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | 4 | import { Product, getProductById, currency } from "../lib/products"; 5 | import { addToCart, useLoggedIn } from "../lib/cart"; 6 | 7 | export default function PDPContent() { 8 | const loggedIn = useLoggedIn(); 9 | const { id } = useParams(); 10 | const [product, setProduct] = useState(); 11 | 12 | useEffect(() => { 13 | if (id) { 14 | getProductById(id).then(setProduct); 15 | } else { 16 | setProduct(null); 17 | } 18 | }, [id]); 19 | 20 | if (!product) return null; 21 | 22 | return ( 23 |
24 |
25 | {product.name} 26 |
27 |
28 |
29 |

{product.name}

30 |
31 | {currency.format(product.price)} 32 |
33 |
34 | {loggedIn && ( 35 |
36 | 43 |
44 | )} 45 |
{product.description}
46 |
{product.longDescription}
47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /ecomm-original/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ecomm 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ecomm-original/src/index.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } -------------------------------------------------------------------------------- /ecomm-original/src/index.ts: -------------------------------------------------------------------------------- 1 | import("./App"); 2 | -------------------------------------------------------------------------------- /ecomm-original/src/lib/cart.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { BehaviorSubject } from "rxjs"; 3 | 4 | import type { Product } from "./products"; 5 | 6 | export interface CartItem extends Product { 7 | quantity: number; 8 | } 9 | 10 | export interface Cart { 11 | cartItems: CartItem[]; 12 | } 13 | 14 | const API_SERVER = "http://localhost:8080"; 15 | 16 | export const jwt = new BehaviorSubject(null); 17 | export const cart = new BehaviorSubject(null); 18 | 19 | export const getCart = (): Promise => 20 | fetch(`${API_SERVER}/cart`, { 21 | headers: { 22 | "Content-Type": "application/json", 23 | Authorization: `Bearer ${jwt.value}`, 24 | }, 25 | }) 26 | .then((res) => res.json()) 27 | .then((res) => { 28 | cart.next(res); 29 | return res; 30 | }); 31 | 32 | export const addToCart = (id): Promise => 33 | fetch(`${API_SERVER}/cart`, { 34 | method: "POST", 35 | headers: { 36 | "Content-Type": "application/json", 37 | Authorization: `Bearer ${jwt.value}`, 38 | }, 39 | body: JSON.stringify({ id }), 40 | }) 41 | .then((res) => res.json()) 42 | .then(() => { 43 | getCart(); 44 | }); 45 | 46 | export const clearCart = (): Promise => 47 | fetch(`${API_SERVER}/cart`, { 48 | method: "DELETE", 49 | headers: { 50 | "Content-Type": "application/json", 51 | Authorization: `Bearer ${jwt.value}`, 52 | }, 53 | }) 54 | .then((res) => res.json()) 55 | .then(() => { 56 | getCart(); 57 | }); 58 | 59 | export const login = (username: string, password: string): Promise => 60 | fetch(`${API_SERVER}/auth/login`, { 61 | method: "POST", 62 | headers: { 63 | "Content-Type": "application/json", 64 | }, 65 | body: JSON.stringify({ 66 | username, 67 | password, 68 | }), 69 | }) 70 | .then((res) => res.json()) 71 | .then((data) => { 72 | jwt.next(data.access_token); 73 | getCart(); 74 | return data.access_token; 75 | }); 76 | 77 | export function useLoggedIn(): boolean { 78 | const [loggedIn, setLoggedIn] = useState(!!jwt.value); 79 | useEffect(() => { 80 | setLoggedIn(!!jwt.value); 81 | const sub = jwt.subscribe((c) => { 82 | setLoggedIn(!!jwt.value); 83 | }); 84 | return () => sub.unsubscribe(); 85 | }, []); 86 | return loggedIn; 87 | } 88 | -------------------------------------------------------------------------------- /ecomm-original/src/lib/products.ts: -------------------------------------------------------------------------------- 1 | const API_SERVER = "http://localhost:8080"; 2 | 3 | export interface Product { 4 | id: number; 5 | name: string; 6 | price: number; 7 | description: string; 8 | image: string; 9 | longDescription: string; 10 | } 11 | 12 | export const getProducts = (): Promise => 13 | fetch(`${API_SERVER}/products`).then((res) => res.json()); 14 | 15 | export const getProductById = (id: string): Promise => 16 | fetch(`${API_SERVER}/products/${id}`).then((res) => res.json()); 17 | 18 | export const currency = new Intl.NumberFormat("en-US", { 19 | style: "currency", 20 | currency: "USD", 21 | }); 22 | -------------------------------------------------------------------------------- /ecomm-original/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /ecomm-original/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebPackPlugin = require("html-webpack-plugin"); 2 | const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); 3 | 4 | const deps = require("./package.json").dependencies; 5 | module.exports = { 6 | output: { 7 | publicPath: "http://localhost:3000/", 8 | }, 9 | 10 | resolve: { 11 | extensions: [".tsx", ".ts", ".jsx", ".js", ".json"], 12 | }, 13 | 14 | devServer: { 15 | port: 3000, 16 | historyApiFallback: true, 17 | }, 18 | 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.m?js/, 23 | type: "javascript/auto", 24 | resolve: { 25 | fullySpecified: false, 26 | }, 27 | }, 28 | { 29 | test: /\.(css|s[ac]ss)$/i, 30 | use: ["style-loader", "css-loader", "postcss-loader"], 31 | }, 32 | { 33 | test: /\.(ts|tsx|js|jsx)$/, 34 | exclude: /node_modules/, 35 | use: { 36 | loader: "babel-loader", 37 | }, 38 | }, 39 | ], 40 | }, 41 | 42 | plugins: [ 43 | new ModuleFederationPlugin({ 44 | name: "ecomm", 45 | filename: "remoteEntry.js", 46 | remotes: {}, 47 | exposes: {}, 48 | shared: { 49 | ...deps, 50 | react: { 51 | singleton: true, 52 | requiredVersion: deps.react, 53 | }, 54 | "react-dom": { 55 | singleton: true, 56 | requiredVersion: deps["react-dom"], 57 | }, 58 | }, 59 | }), 60 | new HtmlWebPackPlugin({ 61 | template: "./src/index.html", 62 | }), 63 | ], 64 | }; 65 | -------------------------------------------------------------------------------- /ecomm/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-typescript", "@babel/preset-react", "@babel/preset-env"], 3 | "plugins": [ 4 | ["@babel/transform-runtime"] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /ecomm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecomm", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "webpack --mode production", 6 | "build:dev": "webpack --mode development", 7 | "build:start": "cd dist && PORT=3000 npx serve", 8 | "start": "webpack serve --open --mode development", 9 | "start:live": "webpack serve --open --mode development --live-reload --hot" 10 | }, 11 | "license": "MIT", 12 | "author": { 13 | "name": "Jack Herrington", 14 | "email": "jherr@pobox.com" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.15.8", 18 | "@babel/plugin-transform-runtime": "^7.15.8", 19 | "@babel/preset-env": "^7.15.8", 20 | "@babel/preset-react": "^7.14.5", 21 | "@babel/preset-typescript": "^7.10.4", 22 | "@types/react": "^17.0.2", 23 | "@types/react-dom": "^17.0.2", 24 | "autoprefixer": "^10.1.0", 25 | "babel-loader": "^8.2.2", 26 | "css-loader": "^6.3.0", 27 | "html-webpack-plugin": "^5.3.2", 28 | "postcss": "^8.2.1", 29 | "postcss-loader": "^4.1.0", 30 | "style-loader": "^3.3.0", 31 | "tailwindcss": "^2.0.2", 32 | "webpack": "^5.57.1", 33 | "webpack-cli": "^4.9.0", 34 | "webpack-dev-server": "^4.3.1" 35 | }, 36 | "dependencies": { 37 | "react": "^17.0.2", 38 | "react-dom": "^17.0.2", 39 | "react-location": "^3.1.10", 40 | "remixicon": "^2.5.0", 41 | "rxjs": "^7.4.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ecomm/postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require("autoprefixer"); 2 | const tailwindcss = require("tailwindcss"); 3 | 4 | module.exports = { 5 | plugins: [tailwindcss, autoprefixer], 6 | }; 7 | -------------------------------------------------------------------------------- /ecomm/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import "remixicon/fonts/remixicon.css"; 5 | import "./index.scss"; 6 | 7 | import MainLayout from "./components/MainLayout"; 8 | 9 | ReactDOM.render(, document.getElementById("app")); 10 | -------------------------------------------------------------------------------- /ecomm/src/components/CartContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | import { CartItem, cart, clearCart } from "../lib/cart"; 4 | import { currency } from "../lib/products"; 5 | 6 | export default function CartContent() { 7 | const [items, setItems] = useState([]); 8 | 9 | useEffect( 10 | () => 11 | cart.subscribe((value) => setItems(value?.cartItems ?? [])).unsubscribe, 12 | [] 13 | ); 14 | 15 | return ( 16 | <> 17 |
18 | {items.map((item) => ( 19 | 20 |
{item.quantity}
21 | {item.name} 22 |
{item.name}
23 |
24 | {currency.format(item.quantity * item.price)} 25 |
26 |
27 | ))} 28 |
29 |
30 |
31 |
32 | {currency.format(items.reduce((a, v) => a + v.quantity * v.price, 0))} 33 |
34 |
35 | {items.length > 0 && ( 36 |
37 |
38 | 45 |
46 |
47 | 53 |
54 |
55 | )} 56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /ecomm/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Footer() { 4 | return ( 5 |
6 | Only The Best Spinners 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /ecomm/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-location"; 3 | 4 | import MiniCart from "./MiniCart"; 5 | import Login from "./Login"; 6 | 7 | export default function Header() { 8 | return ( 9 |
10 |
11 |
12 | Fidget Spinner World 13 |
|
14 | 15 | Cart 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /ecomm/src/components/HomeContent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-location"; 3 | import { useMatch } from "react-location"; 4 | 5 | import { currency } from "../lib/products"; 6 | import { addToCart, useLoggedIn } from "../lib/cart"; 7 | 8 | import type { LocationGenerics } from "../router"; 9 | 10 | export default function HomeContent() { 11 | const loggedIn = useLoggedIn(); 12 | const { products } = useMatch().data; 13 | 14 | return ( 15 |
16 | {products.map((product) => ( 17 |
18 | 19 | {product.name} 20 | 21 |
22 |
23 | {product.name} 24 |
25 |
{currency.format(product.price)}
26 |
27 |
{product.description}
28 | {loggedIn && ( 29 |
30 | 37 |
38 | )} 39 |
40 | ))} 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /ecomm/src/components/Login.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import { login, useLoggedIn } from "../lib/cart"; 4 | 5 | export default function Login() { 6 | const loggedIn = useLoggedIn(); 7 | const [showLogin, setShowLogin] = useState(false); 8 | 9 | const [username, setUsername] = useState("sally"); 10 | const [password, setPassword] = useState("123"); 11 | 12 | if (loggedIn) return null; 13 | 14 | return ( 15 | <> 16 | setShowLogin(!showLogin)}> 17 | 18 | 19 | {showLogin && ( 20 |
28 | setUsername(evt.target.value)} 33 | className="border text-sm border-gray-400 p-2 rounded-md w-full" 34 | /> 35 | setPassword(evt.target.value)} 39 | className="border text-sm border-gray-400 p-2 rounded-md w-full mt-3" 40 | /> 41 | 48 |
49 | )} 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /ecomm/src/components/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Router, Outlet } from "react-location"; 3 | 4 | import Header from "./Header"; 5 | import Footer from "./Footer"; 6 | 7 | import { routes, location } from "../router"; 8 | 9 | export default function MainLayout() { 10 | return ( 11 | 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /ecomm/src/components/MiniCart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | import { cart, clearCart } from "../lib/cart"; 4 | import { currency } from "../lib/products"; 5 | 6 | export default function MiniCart() { 7 | const [items, setItems] = useState(undefined); 8 | const [showCart, setShowCart] = useState(false); 9 | 10 | useEffect(() => { 11 | setItems(cart.value?.cartItems); 12 | return cart.subscribe((c) => setItems(c?.cartItems)).unsubscribe; 13 | }, []); 14 | 15 | if (!items) return null; 16 | 17 | return ( 18 | <> 19 | setShowCart(!showCart)} id="showcart_span"> 20 | 21 | {items.length} 22 | 23 | {showCart && ( 24 | <> 25 |
33 |
39 | {items.map((item) => ( 40 | 41 |
{item.quantity}
42 | {item.name} 43 |
{item.name}
44 |
45 | {currency.format(item.quantity * item.price)} 46 |
47 |
48 | ))} 49 |
50 |
51 |
52 |
53 | {currency.format( 54 | items.reduce((a, v) => a + v.quantity * v.price, 0) 55 | )} 56 |
57 |
58 |
59 |
60 | 67 |
68 |
69 | 75 |
76 |
77 |
78 | 79 | )} 80 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /ecomm/src/components/PDPContent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMatch } from "react-location"; 3 | 4 | import { currency } from "../lib/products"; 5 | import { addToCart, useLoggedIn } from "../lib/cart"; 6 | 7 | import type { LocationGenerics } from "../router"; 8 | 9 | export default function PDPContent() { 10 | const loggedIn = useLoggedIn(); 11 | const { product } = useMatch().data; 12 | 13 | return ( 14 |
15 |
16 | {product.name} 17 |
18 |
19 |
20 |

{product.name}

21 |
22 | {currency.format(product.price)} 23 |
24 |
25 | {loggedIn && ( 26 |
27 | 34 |
35 | )} 36 |
{product.description}
37 |
{product.longDescription}
38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /ecomm/src/components/homeModule.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getProducts } from "../lib/products"; 3 | 4 | export default { 5 | loader: async () => { 6 | return { 7 | products: await getProducts(), 8 | }; 9 | }, 10 | element: () => import("./HomeContent").then((module) => ), 11 | }; 12 | -------------------------------------------------------------------------------- /ecomm/src/components/pdpModule.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { getProductById } from "../lib/products"; 4 | 5 | export default { 6 | loader: async ({ params }) => { 7 | return { 8 | product: await getProductById(params.id), 9 | }; 10 | }, 11 | element: () => import("./PDPContent").then((module) => ), 12 | }; 13 | -------------------------------------------------------------------------------- /ecomm/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ecomm 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ecomm/src/index.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } -------------------------------------------------------------------------------- /ecomm/src/index.ts: -------------------------------------------------------------------------------- 1 | import("./App"); 2 | -------------------------------------------------------------------------------- /ecomm/src/lib/cart.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { BehaviorSubject } from "rxjs"; 3 | 4 | import type { Product } from "./products"; 5 | 6 | export interface CartItem extends Product { 7 | quantity: number; 8 | } 9 | 10 | export interface Cart { 11 | cartItems: CartItem[]; 12 | } 13 | 14 | const API_SERVER = "http://localhost:8080"; 15 | 16 | export const jwt = new BehaviorSubject(null); 17 | export const cart = new BehaviorSubject(null); 18 | 19 | export const getCart = (): Promise => 20 | fetch(`${API_SERVER}/cart`, { 21 | headers: { 22 | "Content-Type": "application/json", 23 | Authorization: `Bearer ${jwt.value}`, 24 | }, 25 | }) 26 | .then((res) => res.json()) 27 | .then((res) => { 28 | cart.next(res); 29 | return res; 30 | }); 31 | 32 | export const addToCart = (id): Promise => 33 | fetch(`${API_SERVER}/cart`, { 34 | method: "POST", 35 | headers: { 36 | "Content-Type": "application/json", 37 | Authorization: `Bearer ${jwt.value}`, 38 | }, 39 | body: JSON.stringify({ id }), 40 | }) 41 | .then((res) => res.json()) 42 | .then(() => { 43 | getCart(); 44 | }); 45 | 46 | export const clearCart = (): Promise => 47 | fetch(`${API_SERVER}/cart`, { 48 | method: "DELETE", 49 | headers: { 50 | "Content-Type": "application/json", 51 | Authorization: `Bearer ${jwt.value}`, 52 | }, 53 | }) 54 | .then((res) => res.json()) 55 | .then(() => { 56 | getCart(); 57 | }); 58 | 59 | export const login = (username: string, password: string): Promise => 60 | fetch(`${API_SERVER}/auth/login`, { 61 | method: "POST", 62 | headers: { 63 | "Content-Type": "application/json", 64 | }, 65 | body: JSON.stringify({ 66 | username, 67 | password, 68 | }), 69 | }) 70 | .then((res) => res.json()) 71 | .then((data) => { 72 | jwt.next(data.access_token); 73 | getCart(); 74 | return data.access_token; 75 | }); 76 | 77 | export function useLoggedIn(): boolean { 78 | const [loggedIn, setLoggedIn] = useState(!!jwt.value); 79 | useEffect(() => { 80 | setLoggedIn(!!jwt.value); 81 | const sub = jwt.subscribe((c) => { 82 | setLoggedIn(!!jwt.value); 83 | }); 84 | return () => sub.unsubscribe(); 85 | }, []); 86 | return loggedIn; 87 | } 88 | -------------------------------------------------------------------------------- /ecomm/src/lib/products.ts: -------------------------------------------------------------------------------- 1 | const API_SERVER = "http://localhost:8080"; 2 | 3 | export interface Product { 4 | id: number; 5 | name: string; 6 | price: number; 7 | description: string; 8 | image: string; 9 | longDescription: string; 10 | } 11 | 12 | export const getProducts = (): Promise => 13 | fetch(`${API_SERVER}/products`).then((res) => res.json()); 14 | 15 | export const getProductById = (id: string): Promise => 16 | fetch(`${API_SERVER}/products/${id}`).then((res) => res.json()); 17 | 18 | export const currency = new Intl.NumberFormat("en-US", { 19 | style: "currency", 20 | currency: "USD", 21 | }); 22 | -------------------------------------------------------------------------------- /ecomm/src/router.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, ReactLocation, MakeGenerics } from "react-location"; 3 | import { Product } from "./lib/products"; 4 | 5 | export const routes: Route[] = [ 6 | { 7 | path: "/", 8 | import: () => 9 | import("./components/homeModule").then((module) => module.default), 10 | }, 11 | { 12 | path: "product", 13 | children: [ 14 | { 15 | path: ":id", 16 | import: () => 17 | import("./components/pdpModule").then((module) => module.default), 18 | }, 19 | ], 20 | }, 21 | { 22 | path: "cart", 23 | element: () => 24 | import("./components/CartContent").then((module) => ), 25 | }, 26 | ]; 27 | 28 | export type LocationGenerics = MakeGenerics<{ 29 | LoaderData: { 30 | products: Product[]; 31 | product: Product; 32 | }; 33 | }>; 34 | 35 | export const location = new ReactLocation(); 36 | -------------------------------------------------------------------------------- /ecomm/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /ecomm/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebPackPlugin = require("html-webpack-plugin"); 2 | const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); 3 | 4 | const deps = require("./package.json").dependencies; 5 | module.exports = { 6 | output: { 7 | publicPath: "http://localhost:3000/", 8 | }, 9 | 10 | resolve: { 11 | extensions: [".tsx", ".ts", ".jsx", ".js", ".json"], 12 | }, 13 | 14 | devServer: { 15 | port: 3000, 16 | historyApiFallback: true, 17 | }, 18 | 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.m?js/, 23 | type: "javascript/auto", 24 | resolve: { 25 | fullySpecified: false, 26 | }, 27 | }, 28 | { 29 | test: /\.(css|s[ac]ss)$/i, 30 | use: ["style-loader", "css-loader", "postcss-loader"], 31 | }, 32 | { 33 | test: /\.(ts|tsx|js|jsx)$/, 34 | exclude: /node_modules/, 35 | use: { 36 | loader: "babel-loader", 37 | }, 38 | }, 39 | ], 40 | }, 41 | 42 | plugins: [ 43 | new ModuleFederationPlugin({ 44 | name: "ecomm", 45 | filename: "remoteEntry.js", 46 | remotes: {}, 47 | exposes: {}, 48 | shared: { 49 | ...deps, 50 | react: { 51 | singleton: true, 52 | requiredVersion: deps.react, 53 | }, 54 | "react-dom": { 55 | singleton: true, 56 | requiredVersion: deps["react-dom"], 57 | }, 58 | }, 59 | }), 60 | new HtmlWebPackPlugin({ 61 | template: "./src/index.html", 62 | }), 63 | ], 64 | }; 65 | -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | Static files are served out of the public directory. 2 | 3 | ``` 4 | $ curl http://localhost:8080/placeholder.txt 5 | $ # result -> Put your static files in this directory and then delete this file. 6 | ``` 7 | 8 | You can have un-authorized routes. 9 | 10 | ``` 11 | $ curl http://localhost:8080/unauthorized 12 | $ # result -> true 13 | ``` 14 | 15 | Trying authorized routes without a JWT will result in a 401. 16 | 17 | ``` 18 | $ curl http://localhost:8080/authorized 19 | $ # result -> {"statusCode":401,"message":"Unauthorized"} 20 | ``` 21 | 22 | Use the `/auth/login` route to login. 23 | 24 | ``` 25 | $ # POST /auth/login 26 | $ curl -X POST http://localhost:8080/auth/login -d '{"username": "maria", "password": "123"}' -H "Content-Type: application/json" 27 | $ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm... } 28 | ``` 29 | 30 | Send the JWT to authorized routes using the `Authorization` header and prefixing the JWT with `Bearer `. 31 | 32 | ``` 33 | $ # GET /profile using access_token returned from previous step as bearer code 34 | $ curl http://localhost:8080/authorized -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..." 35 | $ # result -> {"userId":2} 36 | ``` 37 | -------------------------------------------------------------------------------- /server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@": "nestjs/passport", 25 | "@nestjs/common": "^8.0.0", 26 | "@nestjs/core": "^8.0.0", 27 | "@nestjs/jwt": "^8.0.0", 28 | "@nestjs/passport": "^8.0.1", 29 | "@nestjs/platform-express": "^8.0.0", 30 | "@nestjs/serve-static": "^2.2.2", 31 | "class-validator": "^0.13.1", 32 | "jsonwebtoken": "^8.5.1", 33 | "passport": "^0.5.0", 34 | "passport-jwt": "^4.0.0", 35 | "passport-local": "^1.0.0", 36 | "reflect-metadata": "^0.1.13", 37 | "rimraf": "^3.0.2", 38 | "rxjs": "^7.2.0" 39 | }, 40 | "devDependencies": { 41 | "@nestjs/cli": "^8.0.0", 42 | "@nestjs/schematics": "^8.0.0", 43 | "@nestjs/testing": "^8.0.0", 44 | "@types/express": "^4.17.13", 45 | "@types/jest": "^27.0.1", 46 | "@types/node": "^16.0.0", 47 | "@types/passport-jwt": "^3.0.6", 48 | "@types/passport-local": "^1.0.34", 49 | "@types/supertest": "^2.0.11", 50 | "@typescript-eslint/eslint-plugin": "^4.28.2", 51 | "@typescript-eslint/parser": "^4.28.2", 52 | "eslint": "^7.30.0", 53 | "eslint-config-prettier": "^8.3.0", 54 | "eslint-plugin-prettier": "^3.4.0", 55 | "jest": "^27.0.6", 56 | "prettier": "^2.3.2", 57 | "supertest": "^6.1.3", 58 | "ts-jest": "^27.0.3", 59 | "ts-loader": "^9.2.3", 60 | "ts-node": "^10.0.0", 61 | "tsconfig-paths": "^3.10.1", 62 | "typescript": "^4.3.5" 63 | }, 64 | "jest": { 65 | "moduleFileExtensions": [ 66 | "js", 67 | "json", 68 | "ts" 69 | ], 70 | "rootDir": "src", 71 | "testRegex": ".*\\.spec\\.ts$", 72 | "transform": { 73 | "^.+\\.(t|j)s$": "ts-jest" 74 | }, 75 | "collectCoverageFrom": [ 76 | "**/*.(t|j)s" 77 | ], 78 | "coverageDirectory": "../coverage", 79 | "testEnvironment": "node" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /server/public/fidget-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/react-location-intro/fc8011b70ccd6197bbbe2ebe6f4563d65f198a18/server/public/fidget-1.jpg -------------------------------------------------------------------------------- /server/public/fidget-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/react-location-intro/fc8011b70ccd6197bbbe2ebe6f4563d65f198a18/server/public/fidget-10.jpg -------------------------------------------------------------------------------- /server/public/fidget-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/react-location-intro/fc8011b70ccd6197bbbe2ebe6f4563d65f198a18/server/public/fidget-11.jpg -------------------------------------------------------------------------------- /server/public/fidget-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/react-location-intro/fc8011b70ccd6197bbbe2ebe6f4563d65f198a18/server/public/fidget-2.jpg -------------------------------------------------------------------------------- /server/public/fidget-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/react-location-intro/fc8011b70ccd6197bbbe2ebe6f4563d65f198a18/server/public/fidget-3.jpg -------------------------------------------------------------------------------- /server/public/fidget-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/react-location-intro/fc8011b70ccd6197bbbe2ebe6f4563d65f198a18/server/public/fidget-5.jpg -------------------------------------------------------------------------------- /server/public/fidget-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/react-location-intro/fc8011b70ccd6197bbbe2ebe6f4563d65f198a18/server/public/fidget-6.jpg -------------------------------------------------------------------------------- /server/public/fidget-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/react-location-intro/fc8011b70ccd6197bbbe2ebe6f4563d65f198a18/server/public/fidget-7.jpg -------------------------------------------------------------------------------- /server/public/fidget-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/react-location-intro/fc8011b70ccd6197bbbe2ebe6f4563d65f198a18/server/public/fidget-8.jpg -------------------------------------------------------------------------------- /server/public/fidget-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jherr/react-location-intro/fc8011b70ccd6197bbbe2ebe6f4563d65f198a18/server/public/fidget-9.jpg -------------------------------------------------------------------------------- /server/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, UseGuards, Request } from '@nestjs/common'; 2 | import { LocalAuthGuard } from './auth/local-auth.guard'; 3 | import { AuthService } from './auth/auth.service'; 4 | 5 | @Controller() 6 | export class AppController { 7 | constructor(private authService: AuthService) {} 8 | 9 | @UseGuards(LocalAuthGuard) 10 | @Post('auth/login') 11 | async login(@Request() req) { 12 | return this.authService.login(req.user); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ServeStaticModule } from '@nestjs/serve-static'; 3 | import { join } from 'path'; 4 | 5 | import { CartModule } from './modules/cart/cart.module'; 6 | import { ProductsModule } from './modules/products/products.module'; 7 | import { AppController } from './app.controller'; 8 | import { AuthModule } from './auth/auth.module'; 9 | import { UsersService } from './users/users.service'; 10 | 11 | @Module({ 12 | controllers: [AppController], 13 | providers: [UsersService], 14 | imports: [ 15 | ServeStaticModule.forRoot({ 16 | rootPath: join(__dirname, '..', 'public'), 17 | }), 18 | CartModule, 19 | ProductsModule, 20 | AuthModule, 21 | ], 22 | }) 23 | export class AppModule {} 24 | -------------------------------------------------------------------------------- /server/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PassportModule } from '@nestjs/passport'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | 5 | import { AuthService } from './auth.service'; 6 | import { LocalStrategy } from './local.strategy'; 7 | import { JwtStrategy } from './jwt.strategy'; 8 | import { UsersModule } from '../users/users.module'; 9 | import { jwtConstants } from './constants'; 10 | 11 | @Module({ 12 | imports: [ 13 | UsersModule, 14 | PassportModule, 15 | JwtModule.register({ 16 | secret: jwtConstants.secret, 17 | signOptions: { expiresIn: '24h' }, 18 | }), 19 | ], 20 | providers: [AuthService, LocalStrategy, JwtStrategy], 21 | exports: [AuthService], 22 | }) 23 | export class AuthModule {} 24 | -------------------------------------------------------------------------------- /server/src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | 4 | import { UsersService } from '../users/users.service'; 5 | 6 | @Injectable() 7 | export class AuthService { 8 | constructor( 9 | private usersService: UsersService, 10 | private jwtService: JwtService, 11 | ) {} 12 | 13 | async validateUser(username: string, pass: string): Promise { 14 | const user = await this.usersService.findOne(username); 15 | if (user && user.password === pass) { 16 | const { password, ...result } = user; 17 | return result; 18 | } 19 | return null; 20 | } 21 | 22 | async login(user: any) { 23 | const payload = { username: user.username, sub: user.userId }; 24 | return { 25 | access_token: this.jwtService.sign(payload), 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/src/auth/constants.ts: -------------------------------------------------------------------------------- 1 | export const jwtConstants = { 2 | secret: 'secretKey', 3 | }; 4 | -------------------------------------------------------------------------------- /server/src/auth/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class JwtAuthGuard extends AuthGuard('jwt') {} 6 | -------------------------------------------------------------------------------- /server/src/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable } from '@nestjs/common'; 4 | 5 | import { jwtConstants } from './constants'; 6 | 7 | @Injectable() 8 | export class JwtStrategy extends PassportStrategy(Strategy) { 9 | constructor() { 10 | super({ 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | ignoreExpiration: false, 13 | secretOrKey: jwtConstants.secret, 14 | }); 15 | } 16 | 17 | async validate(payload: any) { 18 | return { userId: payload.sub, username: payload.username }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server/src/auth/local-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class LocalAuthGuard extends AuthGuard('local') {} 6 | -------------------------------------------------------------------------------- /server/src/auth/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy } from 'passport-local'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 4 | import { AuthService } from './auth.service'; 5 | 6 | @Injectable() 7 | export class LocalStrategy extends PassportStrategy(Strategy) { 8 | constructor(private authService: AuthService) { 9 | super(); 10 | } 11 | 12 | async validate(username: string, password: string): Promise { 13 | const user = await this.authService.validateUser(username, password); 14 | if (!user) { 15 | throw new UnauthorizedException(); 16 | } 17 | return user; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/src/config.ts: -------------------------------------------------------------------------------- 1 | export const JWT_SECRET = process.env.JWT_SECRET || 'secret'; 2 | -------------------------------------------------------------------------------- /server/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | 3 | import { AppModule } from './app.module'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | app.enableCors(); 8 | await app.listen(8080); 9 | } 10 | bootstrap(); 11 | -------------------------------------------------------------------------------- /server/src/modules/cart/cart.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Request, 5 | UseGuards, 6 | Post, 7 | Body, 8 | Delete, 9 | } from '@nestjs/common'; 10 | import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; 11 | 12 | import products, { Product } from '../../products'; 13 | 14 | interface CartItem extends Product { 15 | quantity: number; 16 | } 17 | 18 | interface Cart { 19 | cartItems: CartItem[]; 20 | } 21 | 22 | const initialCart = (indexes: number[]): Cart => ({ 23 | cartItems: indexes.map((index) => ({ 24 | ...products[index], 25 | quantity: 1, 26 | })), 27 | }); 28 | 29 | @Controller('cart') 30 | export class CartController { 31 | private carts: Record = { 32 | 1: initialCart([0, 2, 4]), 33 | 2: initialCart([1, 3]), 34 | }; 35 | 36 | constructor() {} 37 | 38 | @Get() 39 | @UseGuards(JwtAuthGuard) 40 | async index(@Request() req): Promise { 41 | return this.carts[req.user.userId] ?? { cartItems: [] }; 42 | } 43 | 44 | @Post() 45 | @UseGuards(JwtAuthGuard) 46 | async create(@Request() req, @Body() { id }: { id: string }): Promise { 47 | const cart = this.carts[req.user.userId]; 48 | const cartItem = cart.cartItems.find( 49 | (cartItem) => cartItem.id === parseInt(id), 50 | ); 51 | if (cartItem) { 52 | cartItem.quantity += 1; 53 | } else { 54 | cart.cartItems.push({ 55 | ...products.find((product) => product.id === parseInt(id)), 56 | quantity: 1, 57 | }); 58 | } 59 | return cart; 60 | } 61 | 62 | @Delete() 63 | @UseGuards(JwtAuthGuard) 64 | async destroy(@Request() req): Promise { 65 | this.carts[req.user.userId] = { cartItems: [] }; 66 | return this.carts[req.user.userId]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /server/src/modules/cart/cart.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { CartController } from './cart.controller'; 4 | 5 | @Module({ 6 | controllers: [CartController], 7 | }) 8 | export class CartModule {} 9 | -------------------------------------------------------------------------------- /server/src/modules/products/products.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param } from '@nestjs/common'; 2 | 3 | import products, { Product } from '../../products'; 4 | 5 | @Controller('products') 6 | export class ProductsController { 7 | constructor() {} 8 | 9 | @Get() 10 | async index(): Promise { 11 | return products; 12 | } 13 | 14 | @Get(':id') 15 | async show(@Param('id') id: string): Promise { 16 | return products.find((product) => product.id === parseInt(id)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/src/modules/products/products.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ProductsController } from './products.controller'; 4 | 5 | @Module({ 6 | controllers: [ProductsController], 7 | }) 8 | export class ProductsModule {} 9 | -------------------------------------------------------------------------------- /server/src/products.ts: -------------------------------------------------------------------------------- 1 | export interface Product { 2 | id: number; 3 | name: string; 4 | price: number; 5 | description: string; 6 | image: string; 7 | longDescription: string; 8 | } 9 | 10 | const products: Product[] = [ 11 | { 12 | id: 1, 13 | name: 'Wheel Spinner', 14 | price: 5.99, 15 | description: 'A wheel fidget spinner that spins wheels', 16 | image: 'http://localhost:8080/fidget-1.jpg', 17 | longDescription: 18 | 'Our Wheel Spinner is fun, functional, and maybe just a little bit strange. It allows you to spin your wheels with ease-- making it great for stress relief or just entertainment. This fidget spinner is handcrafted using brass and then plated in gold or rose gold to ensure the finest quality spinning action, making it the best way to get your wheels moving.', 19 | }, 20 | { 21 | id: 2, 22 | name: 'Solid Rainbow', 23 | price: 8.99, 24 | description: 'A solid steel of rainbow fidget spinning goodness.', 25 | image: 'http://localhost:8080/fidget-2.jpg', 26 | longDescription: 27 | 'The Solid Rainbow fidget spinner is a hit. Its full metal body is made from a single piece of steel that takes hours to cut and machine. The center body has been treated with an electroplated natural copper finish that will tarnish over time as it touches your skin, just as the copper toys from the 70s did. The rainbow finish on the outer ring can be customized using various methods, including leaving it raw to see the natural finish on the stainless steel or applying a colored powder coat finish.', 28 | }, 29 | { 30 | id: 3, 31 | name: 'Dragon Spinner', 32 | price: 7.99, 33 | description: 'A winged dragon of a spinner.', 34 | image: 'http://localhost:8080/fidget-3.jpg', 35 | longDescription: 36 | 'The Dragon Spinner is a new take on an ancient toy. Made of durable polycarbonate, it is strong enough to withstand the force generated by its razor-sharp spinning capabilities. It also features our signature edition design to make it one-of-a-kind — and one that you and your friends will love. What better way to relieve stress and anxiety than with a simple fidget spinner?', 37 | }, 38 | { 39 | id: 5, 40 | name: 'Rainbow Flames', 41 | price: 7.99, 42 | description: 'Flaming rainbow fun for all ages.', 43 | image: 'http://localhost:8080/fidget-5.jpg', 44 | longDescription: 45 | 'Rainbow Flames are small (1.5″ in diameter or approx. 46mm) fidget spinner toys. They are fun for people of all ages, including adults and kids. Kids love to play with Rainbow Flames because they are easy to spin and they come in assorted colors, like blue, green, red, white, purple and yellow possible combinations. Rainbow Flames fidget spinners are great for killing time; perfect for daydreaming, calming nerves, focusing attention & relaxing; better than nail biting & knuckle cracking. Rainbow Flames fidget spinners can be successfully incorporated into therapy sessions as fidget toys', 46 | }, 47 | { 48 | id: 6, 49 | name: 'Jammin Spinner', 50 | price: 6.99, 51 | description: 'Jammin this rainbow inspired spinner is so much fun!', 52 | image: 'http://localhost:8080/fidget-6.jpg', 53 | longDescription: 54 | 'Built with a 3v high-speed motor, this fidget spinner is sure to be a crowd pleaser. From the center axis to the buttons on each of the three caps, enough space was left between the pieces for a kinetic chain reaction -- creating a hypnotizing, spinning flowchart. The customizable caps give you 11 unique options to choose from. Our "Jammin" spinner comes in a stylish white box with a velvet lining and includes our standard 90 day manufacturer\'s warranty.', 55 | }, 56 | { 57 | id: 7, 58 | name: 'Goldy-spinner', 59 | price: 8.99, 60 | description: 'So golden, so spinny! Classic and beautiful.', 61 | image: 'http://localhost:8080/fidget-7.jpg', 62 | longDescription: 63 | 'This is the solid stainless steel fidget spinner that is perfect for any gold lover. It is build with 6 Fidget Spinner components giving it 6 super-sized bearings for maximum glide and endless spin time. A perfect combination of gold, steel & infinity.', 64 | }, 65 | { 66 | id: 8, 67 | name: 'Golden pointers', 68 | price: 7.99, 69 | description: 'Beautiful pointed golden spinner with jade highlights.', 70 | image: 'http://localhost:8080/fidget-8.jpg', 71 | longDescription: 72 | "This unique fidget spinner has a jade highlight and is great for all ages. Great for anxiety, focusing, adhd, autism, quitting bad habits, staying awake and also helps with reducing stress and panic attacks. This product is designed in the USA and made from high quality materials. The weight and feel are perfect in the hands. It's not too light or to heavy allowing for long term use without causing fatigue in the hands. High Performance Gyroscopes With Smooth Rotation", 73 | }, 74 | { 75 | id: 9, 76 | name: 'Rainbow Hearts', 77 | price: 8.99, 78 | description: "So much love in a spinner, it's all rainbows and hearts.", 79 | image: 'http://localhost:8080/fidget-9.jpg', 80 | longDescription: 81 | "Fidgeting is a human impulse. That's why we created the Rainbow Hearts: a fun and colorful fidget toy that brings together two of our most popular sleeves in one object. Rainbow Hearts is precision-machined in San Francisco, with a brushed stainless steel clicker, and clear acrylic rainbow colored hearts. And with all the customization that we offer, it will be truly unique!", 82 | }, 83 | { 84 | id: 10, 85 | name: 'Gold and Silver Gears', 86 | price: 9.99, 87 | description: 'Sleek and beautiful, this spinner is so fast.', 88 | image: 'http://localhost:8080/fidget-10.jpg', 89 | longDescription: 90 | "The Gold and Silver Gears Fidget Spinner, by Gear Beast, is a steal of a deal on a super flashy fidget spinner. The body spins on high-quality stainless steel bearings, while the center piece is made from brass. It comes in a variety of colors, so no matter your style you're sure to find a fidget spinner you'll fall in love with.", 91 | }, 92 | { 93 | id: 11, 94 | name: 'Gears Within Gears', 95 | price: 12.99, 96 | description: 97 | 'So many gears and so little time. You will be endlessly fascinated by this spinner.', 98 | image: 'http://localhost:8080/fidget-11.jpg', 99 | longDescription: 100 | "Did you know that a standard gear train has 3 gears? It is true! And, we've given you all three in this delightful fidget spinner. Spin away and watch your mind be blown away by the seemingly endless and rapid movement of these three little clockwork wheels.", 101 | }, 102 | ]; 103 | 104 | export default products; 105 | -------------------------------------------------------------------------------- /server/src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { UsersService } from './users.service'; 4 | 5 | @Module({ 6 | providers: [UsersService], 7 | exports: [UsersService], 8 | }) 9 | export class UsersModule {} 10 | -------------------------------------------------------------------------------- /server/src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | export type User = { 4 | userId: number; 5 | username: string; 6 | password: string; 7 | }; 8 | 9 | @Injectable() 10 | export class UsersService { 11 | private readonly users = [ 12 | { 13 | userId: 1, 14 | username: 'sally', 15 | password: '123', 16 | }, 17 | { 18 | userId: 2, 19 | username: 'maria', 20 | password: '123', 21 | }, 22 | ]; 23 | 24 | async findOne(username: string): Promise { 25 | return this.users.find((user) => user.username === username); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /server/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /testapp/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-typescript", "@babel/preset-react", "@babel/preset-env"], 3 | "plugins": [ 4 | ["@babel/transform-runtime"] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /testapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testapp", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "webpack --mode production", 6 | "build:dev": "webpack --mode development", 7 | "build:start": "cd dist && PORT=3001 npx serve", 8 | "start": "webpack serve --open --mode development", 9 | "start:live": "webpack serve --open --mode development --live-reload --hot" 10 | }, 11 | "license": "MIT", 12 | "author": { 13 | "name": "Jack Herrington", 14 | "email": "jherr@pobox.com" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.15.8", 18 | "@babel/plugin-transform-runtime": "^7.15.8", 19 | "@babel/preset-env": "^7.15.8", 20 | "@babel/preset-react": "^7.14.5", 21 | "@babel/preset-typescript": "^7.10.4", 22 | "@types/react": "^17.0.2", 23 | "@types/react-dom": "^17.0.2", 24 | "autoprefixer": "^10.1.0", 25 | "babel-loader": "^8.2.2", 26 | "css-loader": "^6.3.0", 27 | "html-webpack-plugin": "^5.3.2", 28 | "postcss": "^8.2.1", 29 | "postcss-loader": "^4.1.0", 30 | "style-loader": "^3.3.0", 31 | "tailwindcss": "^2.0.2", 32 | "webpack": "^5.57.1", 33 | "webpack-cli": "^4.9.0", 34 | "webpack-dev-server": "^4.3.1" 35 | }, 36 | "dependencies": { 37 | "react": "^17.0.2", 38 | "react-dom": "^17.0.2", 39 | "react-location": "^3.1.10" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /testapp/postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require("autoprefixer"); 2 | const tailwindcss = require("tailwindcss"); 3 | 4 | module.exports = { 5 | plugins: [tailwindcss, autoprefixer], 6 | }; 7 | -------------------------------------------------------------------------------- /testapp/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { 4 | Router, 5 | Route, 6 | Outlet, 7 | ReactLocation, 8 | Link, 9 | useMatch, 10 | } from "react-location"; 11 | 12 | import "./index.scss"; 13 | 14 | const Product = () => { 15 | const params = useMatch().params; 16 | 17 | return
Product page: {JSON.stringify(params)}
; 18 | }; 19 | 20 | const routes: Route[] = [ 21 | { 22 | path: "/", 23 | element:
Home Page
, 24 | }, 25 | { 26 | path: "product", 27 | children: [ 28 | { 29 | path: ":id", 30 | element: , 31 | }, 32 | ], 33 | }, 34 | { 35 | path: "cart", 36 | element:
Cart Page
, 37 | }, 38 | ]; 39 | 40 | const location = new ReactLocation(); 41 | 42 | const App = () => ( 43 | 44 |
45 | 46 | Go to cart 47 |
48 |
49 | ); 50 | ReactDOM.render(, document.getElementById("app")); 51 | -------------------------------------------------------------------------------- /testapp/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | testapp 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /testapp/src/index.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } -------------------------------------------------------------------------------- /testapp/src/index.ts: -------------------------------------------------------------------------------- 1 | import("./App"); 2 | -------------------------------------------------------------------------------- /testapp/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /testapp/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebPackPlugin = require("html-webpack-plugin"); 2 | const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); 3 | 4 | const deps = require("./package.json").dependencies; 5 | module.exports = { 6 | output: { 7 | publicPath: "http://localhost:3001/", 8 | }, 9 | 10 | resolve: { 11 | extensions: [".tsx", ".ts", ".jsx", ".js", ".json"], 12 | }, 13 | 14 | devServer: { 15 | port: 3001, 16 | historyApiFallback: true, 17 | }, 18 | 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.m?js/, 23 | type: "javascript/auto", 24 | resolve: { 25 | fullySpecified: false, 26 | }, 27 | }, 28 | { 29 | test: /\.(css|s[ac]ss)$/i, 30 | use: ["style-loader", "css-loader", "postcss-loader"], 31 | }, 32 | { 33 | test: /\.(ts|tsx|js|jsx)$/, 34 | exclude: /node_modules/, 35 | use: { 36 | loader: "babel-loader", 37 | }, 38 | }, 39 | ], 40 | }, 41 | 42 | plugins: [ 43 | new ModuleFederationPlugin({ 44 | name: "testapp", 45 | filename: "remoteEntry.js", 46 | remotes: {}, 47 | exposes: {}, 48 | shared: { 49 | ...deps, 50 | react: { 51 | singleton: true, 52 | requiredVersion: deps.react, 53 | }, 54 | "react-dom": { 55 | singleton: true, 56 | requiredVersion: deps["react-dom"], 57 | }, 58 | }, 59 | }), 60 | new HtmlWebPackPlugin({ 61 | template: "./src/index.html", 62 | }), 63 | ], 64 | }; 65 | --------------------------------------------------------------------------------