├── .env-example ├── .github └── workflows │ └── default.yml ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── cmd └── app │ └── main.go ├── config └── config.yml ├── db ├── Dockerfile ├── generate.py └── requirements.txt ├── deploy ├── Dockerfile-backend ├── Dockerfile-frontend └── docker-compose.yml ├── docker-compose.yml ├── docs ├── E-commerce API diagram.png ├── Preview.jpg ├── docs.go └── swagger.yaml ├── frontend ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── nginx.conf ├── package-lock.json ├── package.json ├── public │ ├── img.png │ └── index.html └── src │ ├── App.css │ ├── App.js │ ├── Product.css │ ├── api │ ├── api.js │ ├── auth.js │ ├── cart.js │ └── products.js │ ├── components │ ├── Comment.css │ ├── Comment.jsx │ ├── Error.css │ ├── Error.jsx │ ├── Footer.css │ ├── Footer.jsx │ ├── Product.css │ ├── Product.jsx │ ├── Toolbar.css │ ├── Toolbar.jsx │ ├── auth │ │ ├── SignIn.jsx │ │ └── SignUp.jsx │ ├── cart │ │ ├── Cart.css │ │ ├── Cart.jsx │ │ ├── CartCard.css │ │ └── CartCard.jsx │ ├── catalog │ │ ├── Card.css │ │ ├── Card.jsx │ │ ├── Catalog.css │ │ ├── Catalog.jsx │ │ ├── Filter.css │ │ ├── Filter.jsx │ │ └── img.png │ └── navbar │ │ ├── Navbar.css │ │ └── Navbar.jsx │ ├── index.js │ └── pages │ ├── CartPage.jsx │ ├── CatalogPage.jsx │ ├── ProductPage.jsx │ ├── SignInPage.jsx │ └── SignUpPage.jsx ├── go.mod ├── go.sum ├── internal ├── app │ └── app.go ├── config │ └── config.go ├── delivery │ └── http │ │ ├── handler.go │ │ └── v1 │ │ ├── admin-users.go │ │ ├── admins.go │ │ ├── auth.go │ │ ├── cart.go │ │ ├── handler.go │ │ ├── orders.go │ │ ├── payment.go │ │ ├── products.go │ │ ├── response.go │ │ ├── reviews.go │ │ ├── user-auth.go │ │ └── users.go ├── domain │ ├── admin.go │ ├── cart.go │ ├── dto │ │ ├── admin.go │ │ ├── cart.go │ │ ├── order.go │ │ ├── product.go │ │ ├── review.go │ │ └── user.go │ ├── order.go │ ├── product.go │ ├── review.go │ └── user.go ├── repository │ ├── admins.go │ ├── carts.go │ ├── collections.go │ ├── mocks │ │ ├── admins.go │ │ ├── carts.go │ │ ├── orders.go │ │ ├── products.go │ │ ├── reviews.go │ │ └── users.go │ ├── orders.go │ ├── products.go │ ├── products_test.go │ ├── repository.go │ ├── repository_test.go │ ├── reviews.go │ └── users.go └── service │ ├── admins.go │ ├── cart.go │ ├── cart_test.go │ ├── mocks │ └── products.go │ ├── orders.go │ ├── payment.go │ ├── products.go │ ├── reviews.go │ ├── service.go │ └── users.go └── pkg ├── auth └── jwt.go ├── database ├── mongodb │ └── mongodb.go └── redis │ └── redis.go ├── logging └── logging.go └── payment └── stripe.go /.env-example: -------------------------------------------------------------------------------- 1 | HOST=localhost 2 | PORT=8080 3 | 4 | STRIPE_KEY=sk_test_51JAvElJwIMEm9c8xHuUPgoOlnFy1HRMV5CC4ThiM9DBNCtJCiLtHjcH3EeDMylOOdmx0AGDtlDmRNynxD2bBrOXd00j4BFdj7s 5 | 6 | JWT_SECRET=jwtSecret 7 | 8 | DB_NAME=ecommerce 9 | DB_URI=mongodb://mongo:27017 10 | DB_USERNAME= 11 | DB_PASSWORD= 12 | 13 | TEST_DB_NAME=ecommerce-test 14 | TEST_DB_URI=mongodb://localhost:27017 15 | TEST_DB_USERNAME= 16 | TEST_DB_PASSWORD= 17 | 18 | REDIS_URI=redis:6379 -------------------------------------------------------------------------------- /.github/workflows/default.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | env: 9 | BACKEND_IMAGE: ecommerce 10 | FRONTEND_IMAGE: ecommerce-frontend 11 | GENERATE_IMAGE: generate 12 | TAG: latest 13 | 14 | jobs: 15 | build-and-push: 16 | name: Build and Push 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Check out the repo 21 | uses: actions/checkout@v2 22 | 23 | - name: Login to Docker Hub 24 | uses: docker/login-action@v1 25 | with: 26 | username: ${{ secrets.DOCKER_USERNAME }} 27 | password: ${{ secrets.DOCKER_PASSWORD }} 28 | 29 | - name: Set up Docker Buildx 30 | uses: docker/setup-buildx-action@v1 31 | 32 | - name: Push to Docker Hub Ecommerce image 33 | uses: docker/build-push-action@v2 34 | with: 35 | context: . 36 | file: ./deploy/Dockerfile-backend 37 | push: true 38 | tags: ${{ secrets.DOCKER_HUB_USERNAME }}/${{ env.BACKEND_IMAGE }}:${{ env.TAG }} 39 | 40 | - name: Push to Docker Hub React Frontend image 41 | uses: docker/build-push-action@v2 42 | with: 43 | context: ./frontend 44 | file: ./deploy/Dockerfile-frontend 45 | push: true 46 | tags: ${{ secrets.DOCKER_HUB_USERNAME }}/${{ env.FRONTEND_IMAGE }}:${{ env.TAG }} 47 | 48 | - name: Push to Docker Hub generate mongodb image 49 | uses: docker/build-push-action@v2 50 | with: 51 | context: ./db 52 | push: true 53 | tags: ${{ secrets.DOCKER_HUB_USERNAME }}/${{ env.GENERATE_IMAGE }}:${{ env.TAG }} 54 | 55 | deploy: 56 | name: Deploy to AWS 57 | runs-on: ubuntu-latest 58 | needs: build-and-push 59 | 60 | steps: 61 | - uses: actions/checkout@master 62 | - name: Copy files 63 | uses: appleboy/scp-action@master 64 | with: 65 | host: ${{ secrets.HOST }} 66 | username: ${{ secrets.SERVER_USERNAME }} 67 | key: ${{ secrets.SSHKEY }} 68 | source: "deploy/,!deploy/Dockerfile-frontend,!deploy/Dockerfile-backend" 69 | target: "app" 70 | strip_components: 1 71 | 72 | - name: Deploy ssh 73 | uses: appleboy/ssh-action@v0.1.3 74 | with: 75 | host: ${{ secrets.HOST }} 76 | username: ${{ secrets.SERVER_USERNAME }} 77 | key: ${{ secrets.SSHKEY }} 78 | script: | 79 | export STRIPE_KEY="${{ secrets.STRIPE_KEY }}" 80 | export JWT_SECRET="${{ secrets.JWT_SECRET }}" 81 | 82 | export HOST="${{ secrets.HOST }}" 83 | export PORT="${{ secrets.PORT }}" 84 | 85 | export DB_NAME="${{ secrets.DB_NAME }}" 86 | export DB_URI="${{ secrets.DB_URI }}" 87 | 88 | export REDIS_URI="${{ secrets.REDIS_URI }}" 89 | 90 | export DOCKER_HUB_USERNAME="${{ secrets.DOCKER_HUB_USERNAME }}" 91 | export GENERATE_IMAGE="${{ env.GENERATE_IMAGE }}" 92 | export BACKEND_IMAGE="${{ env.BACKEND_IMAGE }}" 93 | export FRONTEND_IMAGE="${{ env.FRONTEND_IMAGE }}" 94 | export TAG="${{ env.TAG }}" 95 | 96 | cd app 97 | docker-compose -f docker-compose.yml stop 98 | docker-compose -f docker-compose.yml pull 99 | docker-compose -f docker-compose.yml up -d frontend app db redis 100 | docker image prune -f --filter="dangling=true" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | logs 4 | docs/swagger.json 5 | /db/data 6 | **/.DS_Store 7 | .env 8 | .data 9 | .bin 10 | 11 | node_modules 12 | /dist 13 | 14 | # local env files 15 | .env.local 16 | .env.*.local 17 | 18 | # Log files 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | pnpm-debug.log* 23 | 24 | # Editor directories and files 25 | .vscode 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | WORKDIR /app 4 | 5 | CMD ["./app"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | export 3 | 4 | build: 5 | go mod download && CGO_ENABLED=0 GOOS=linux go build -o ./.bin/app ./cmd/app/main.go && env 6 | 7 | run: build 8 | docker-compose up app redis db 9 | 10 | clean: 11 | rm -rf .bin .data 12 | 13 | swag: 14 | swag fmt -g cmd/app/main.go 15 | swag init -g cmd/app/main.go 16 | go run cmd/app/main.go 17 | 18 | init: 19 | @cd ./db \ 20 | && pip3 install -r requirements.txt \ 21 | && python3 generate.py \ 22 | && (bash init.sh "ecommerce"); 23 | 24 | test: 25 | go test -v ./internal/service/ 26 | 27 | .DEFAULT_GOAL := run 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # E-commerce Rest API 2 | 3 | API for web stores to sell different types of products. Integrated with payment system Stripe. Also has admin dashboard. 4 | 5 | ![Preview](docs/Preview.jpg) 6 | 7 | ### [Swagger API documentation](https://app.swaggerhub.com/apis-docs/paw1a/E-commerce) 8 | 9 | # Contents 10 | 11 | 1. [Run](#Run) 12 | 2. [API](#API) 13 | 3. [Implementation](#Implementation) 14 | 15 | # Run 16 | 17 | To run application you need: 18 | - docker-compose 19 | - .env file 20 | 21 | ## Env configuration 22 | 23 | To run API locally you should create your own .env file in the root directory or rename `.env-example` to `.env` 24 | Example `.env`: 25 | 26 | ```env 27 | HOST=localhost 28 | PORT=8080 29 | 30 | STRIPE_KEY= 31 | 32 | JWT_SECRET=jwtSecret 33 | 34 | DB_NAME=ecommerce 35 | DB_URI=mongodb://mongo:27017 36 | DB_USERNAME= 37 | DB_PASSWORD= 38 | 39 | TEST_DB_NAME=ecommerce-test 40 | TEST_DB_URI=mongodb://localhost:27017 41 | TEST_DB_USERNAME= 42 | TEST_DB_PASSWORD= 43 | 44 | REDIS_URI=redis:6379 45 | ``` 46 | 47 | ## Local run 48 | 49 | ``` 50 | make run 51 | ``` 52 | 53 | or 54 | 55 | ``` 56 | docker-compose up 57 | ``` 58 | 59 | Run test data generation for MongoDB 60 | 61 | ``` 62 | make init 63 | ``` 64 | 65 | Run frontend only 66 | 67 | ``` 68 | docker-compose up frontend 69 | ``` 70 | 71 | # API 72 | 73 | Base url: `:8080/api/v1/` 74 | 75 | API documentation can be found [here](https://app.swaggerhub.com/apis-docs/paw1a/E-commerce) 76 | 77 | # Implementation 78 | 79 | - REST API 80 | - Clean architecture design 81 | - Using MongoDB as a main data storage 82 | - Using Redis for cache and user session storage 83 | - Env based application configuration 84 | - Automatically generated Swagger API docs 85 | - Run with docker-compose 86 | - Full automated CI/CD process 87 | - Configured deploy to AWS 88 | - React.js frontend (*in progress*) 89 | - Stripe API payment integration 90 | 91 | ### Project structure 92 | 93 | ``` 94 | . 95 | ├── .bin // app binary files 96 | ├── .data // directory to store local db data 97 | ├── .github // github actions ci/cd directory 98 | ├── cmd // entry point 99 | ├── db // db generation scripts for mongo 100 | ├── deploy // deploy docker files 101 | ├── docs // swagger docs and readme images 102 | ├── frontend // react.js app directory 103 | ├── internal 104 | │ ├── app // main application package 105 | │ ├── config // config loading utils 106 | │ ├── delivery // http handlers layer 107 | │ ├── domain // all business entities and dto's 108 | │ ├── repository // database repository layer 109 | │ └── service // business logic services layer 110 | ├── pkg 111 | │ ├── auth // jwt auth utils 112 | │ ├── database // database connection utils 113 | │ ├── logging // logger configuration 114 | │ └── payment // payment service configuration 115 | └─ 116 | ``` 117 | 118 | # Diagram 119 | ![image not found](docs/E-commerce%20API%20diagram.png) 120 | -------------------------------------------------------------------------------- /cmd/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/paw1a/ecommerce-api/internal/app" 4 | 5 | // @title E-commerce API 6 | // @version 1.0 7 | // @description This is simple api of e-commerce shop 8 | 9 | // @contact.name API Support 10 | // @contact.url https://t.me/paw1a 11 | // @contact.email paw1a@yandex.ru 12 | 13 | // @host 52.29.184.51:8080 14 | // @BasePath /api/v1 15 | 16 | // @schemes http 17 | 18 | // @securityDefinitions.apikey AdminAuth 19 | // @in header 20 | // @name Authorization 21 | 22 | // @securityDefinitions.apikey UserAuth 23 | // @in header 24 | // @name Authorization 25 | func main() { 26 | app.Run("config/config.yml") 27 | } 28 | -------------------------------------------------------------------------------- /config/config.yml: -------------------------------------------------------------------------------- 1 | jwt: 2 | accessTokenTime: 1 # 15 minutes, 300000 for tests 3 | refreshTokenTime: 86400 # 60 days -------------------------------------------------------------------------------- /db/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine3.9 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN pip install -r requirements.txt 8 | 9 | CMD [ "python", "./generate.py"] -------------------------------------------------------------------------------- /db/generate.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import time 4 | 5 | import bson 6 | from faker import Faker 7 | import faker_commerce 8 | 9 | import json 10 | import stripe 11 | 12 | from pymongo import MongoClient 13 | 14 | USER_NUM = 10 15 | PRODUCT_NUM = 10 16 | REVIEW_NUM = 6 17 | 18 | stripe.api_key = os.getenv('STRIPE_KEY') 19 | CONNECTION_STRING = "mongodb://mongo:27017" 20 | 21 | client = MongoClient(CONNECTION_STRING) 22 | db = client['ecommerce'] 23 | 24 | fake = Faker(['en_US']) 25 | fake.add_provider(faker_commerce.Provider) 26 | 27 | # generate users 28 | user_list = [] 29 | 30 | for _ in range(USER_NUM): 31 | user = fake.json(data_columns={'name': 'name', 32 | 'email': 'free_email', 33 | 'password': 'password'}, num_rows=1) 34 | parsed_user = json.loads(user) 35 | user_list.append(parsed_user) 36 | 37 | user_collection = db['users'] 38 | user_collection.drop() 39 | user_collection.insert_many(user_list) 40 | 41 | # generate products 42 | fake.set_arguments('product_desc_arg', {'nb_words': 40}) 43 | fake.set_arguments('category_desc_arg', {'nb_words': 5}) 44 | fake.set_arguments('price', {'min_value': 100, 'max_value': 100000}) 45 | 46 | product_list = [] 47 | 48 | for _ in range(PRODUCT_NUM): 49 | categories_formatter = [{'name': 'ecommerce_category', 50 | 'description': 'sentence:category_desc_arg'}] * random.randint(0, 5) 51 | 52 | product: str = fake.json(data_columns={'name': 'ecommerce_name', 53 | 'description': 'sentence:product_desc_arg', 54 | 'price': 'pyint:price', 55 | 'categories': categories_formatter}, num_rows=1) 56 | 57 | parsed_product = json.loads(product) 58 | product_list.append(parsed_product) 59 | 60 | product_collection = db['products'] 61 | product_collection.drop() 62 | product_collection.insert_many(product_list) 63 | 64 | for product in product_list: 65 | product: dict 66 | stripe.Product.create(name=product["name"], 67 | description=product["description"], 68 | id=product['_id']) 69 | stripe.Price.create(unit_amount_decimal=product["price"] * 100, 70 | currency="RUB", 71 | product=product['_id']) 72 | 73 | # generate reviews 74 | fake.set_arguments('rating', {'min_value': 1, 'max_value': 5}) 75 | 76 | reviews_list = [] 77 | 78 | for product in product_list: 79 | product: dict 80 | for _ in range(random.randint(REVIEW_NUM // 2, REVIEW_NUM)): 81 | reviewText = fake.text().replace('\n', ' ') 82 | review = fake.json(data_columns={ 83 | 'text': f'@{reviewText}', 84 | 'rating': f'pyint:rating', 85 | }, num_rows=1) 86 | 87 | parsed_review: dict = json.loads(review) 88 | parsed_review['productID'] = product['_id'] 89 | user = random.choice(user_list) 90 | parsed_review['userID'] = user['_id'] 91 | parsed_review['username'] = user['name'] 92 | date = bson.timestamp.Timestamp(int(time.time()), 0) 93 | parsed_review['date'] = date 94 | 95 | reviews_list.append(parsed_review) 96 | 97 | review_collection = db['reviews'] 98 | review_collection.drop() 99 | review_collection.insert_many(reviews_list) 100 | 101 | # generate admins 102 | admin_collection = db['admins'] 103 | admin_collection.drop() 104 | 105 | admin_1 = {'name': 'Admin', 106 | 'email': 'paw1a@yandex.ru', 107 | 'password': '123'} 108 | admin_collection.insert_one(admin_1) 109 | 110 | admin_2 = {'name': 'Admin 2', 111 | 'email': 'admin@admin.com', 112 | 'password': 'admin'} 113 | admin_collection.insert_one(admin_2) 114 | 115 | # clean up carts and orders collections 116 | carts_collection = db['carts'] 117 | carts_collection.drop() 118 | 119 | orders_collection = db['orders'] 120 | orders_collection.drop() 121 | -------------------------------------------------------------------------------- /db/requirements.txt: -------------------------------------------------------------------------------- 1 | faker 2 | faker_commerce 3 | bson 4 | stripe 5 | pymongo -------------------------------------------------------------------------------- /deploy/Dockerfile-backend: -------------------------------------------------------------------------------- 1 | FROM golang:1.17-alpine AS builder 2 | 3 | RUN apk update && apk upgrade && apk add --no-cache bash git openssh 4 | 5 | WORKDIR /github.com/paw1a/ecommerce/ 6 | COPY . /github.com/paw1a/ecommerce/ 7 | 8 | RUN go mod download 9 | 10 | RUN GOOS=linux go build -o ./.bin/app ./cmd/app/main.go 11 | 12 | 13 | FROM alpine 14 | 15 | WORKDIR /app 16 | 17 | COPY --from=builder /github.com/paw1a/ecommerce/.bin/ . 18 | COPY --from=builder /github.com/paw1a/ecommerce/config/ ./config/ 19 | COPY --from=builder /github.com/paw1a/ecommerce/docs/ ./docs/ 20 | 21 | EXPOSE 8080 22 | 23 | CMD ["./app"] -------------------------------------------------------------------------------- /deploy/Dockerfile-frontend: -------------------------------------------------------------------------------- 1 | FROM node:17-alpine as builder 2 | WORKDIR /app 3 | COPY package.json /app/package.json 4 | RUN npm install --only=prod 5 | COPY . /app 6 | RUN npm run build 7 | 8 | FROM nginx:alpine 9 | COPY nginx.conf /etc/nginx/conf.d/default.conf 10 | COPY --from=builder /app/build /usr/share/nginx/html 11 | EXPOSE 80 12 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /deploy/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | db: 5 | image: 'mongo:4.4-bionic' 6 | container_name: 'mongo' 7 | environment: 8 | - MONGO_INITDB_DATABASE=${DB_NAME} 9 | volumes: 10 | - ./.data/mongo/:/data/db/ 11 | ports: 12 | - '27017:27017' 13 | networks: 14 | - backend 15 | 16 | generate: 17 | image: ${DOCKER_HUB_USERNAME}/${GENERATE_IMAGE}:${TAG} 18 | container_name: ${GENERATE_IMAGE} 19 | environment: 20 | - STRIPE_KEY 21 | depends_on: 22 | - db 23 | networks: 24 | - backend 25 | 26 | redis: 27 | image: 'redis:alpine' 28 | container_name: 'redis' 29 | volumes: 30 | - ./.data/redis/:/data/ 31 | ports: 32 | - '6379:6379' 33 | networks: 34 | - backend 35 | command: redis-server --save 60 1 --loglevel warning 36 | 37 | app: 38 | image: ${DOCKER_HUB_USERNAME}/${BACKEND_IMAGE}:${TAG} 39 | container_name: ${APP_IMAGE} 40 | environment: 41 | - STRIPE_KEY 42 | - JWT_SECRET 43 | - HOST 44 | - PORT 45 | - DB_NAME 46 | - DB_URI 47 | - REDIS_URI 48 | depends_on: 49 | - db 50 | - generate 51 | - redis 52 | ports: 53 | - '8080:8080' 54 | networks: 55 | - backend 56 | 57 | frontend: 58 | image: ${DOCKER_HUB_USERNAME}/${FRONTEND_IMAGE}:${TAG} 59 | container_name: ${FRONTEND_IMAGE} 60 | depends_on: 61 | - app 62 | ports: 63 | - '3000:80' 64 | networks: 65 | - backend 66 | 67 | networks: 68 | backend: 69 | driver: bridge -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | db: 5 | image: 'mongo:4.4-bionic' 6 | container_name: 'mongo' 7 | environment: 8 | - MONGO_INITDB_DATABASE=${DB_NAME} 9 | volumes: 10 | - ./.data/mongo/:/data/db/ 11 | ports: 12 | - '27017:27017' 13 | networks: 14 | - backend 15 | 16 | generate: 17 | image: 'generate-db' 18 | container_name: 'generate-db' 19 | build: 20 | context: db 21 | dockerfile: Dockerfile 22 | env_file: 23 | - .env 24 | depends_on: 25 | - db 26 | networks: 27 | - backend 28 | 29 | redis: 30 | image: 'redis:alpine' 31 | container_name: 'redis' 32 | volumes: 33 | - ./.data/redis/:/data/ 34 | ports: 35 | - '6379:6379' 36 | networks: 37 | - backend 38 | command: redis-server --save 60 1 --loglevel warning 39 | 40 | app: 41 | image: 'app' 42 | container_name: 'app' 43 | build: 44 | context: . 45 | dockerfile: Dockerfile 46 | volumes: 47 | - ./.bin/:/app/ 48 | - ./config/:/app/config/ 49 | env_file: 50 | - .env 51 | depends_on: 52 | - db 53 | - redis 54 | ports: 55 | - '8080:8080' 56 | networks: 57 | - backend 58 | 59 | frontend: 60 | image: 'frontend' 61 | container_name: 'frontend' 62 | build: 63 | context: frontend 64 | dockerfile: Dockerfile 65 | volumes: 66 | - /app/node_modules 67 | - ./frontend/:/app/ 68 | depends_on: 69 | - app 70 | ports: 71 | - '3000:3000' 72 | environment: 73 | - CHOKIDAR_USEPOLLING=true 74 | networks: 75 | - backend 76 | 77 | networks: 78 | backend: 79 | driver: bridge -------------------------------------------------------------------------------- /docs/E-commerce API diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paw1a/ecommerce-api/c108da254f6fc84e9c912749a733c9a062782ab0/docs/E-commerce API diagram.png -------------------------------------------------------------------------------- /docs/Preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paw1a/ecommerce-api/c108da254f6fc84e9c912749a733c9a062782ab0/docs/Preview.jpg -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | build 4 | .dockerignore 5 | Dockerfile -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13.12.0-alpine 2 | 3 | WORKDIR /app 4 | 5 | ENV PATH /app/node_modules/.bin:$PATH 6 | 7 | COPY package.json ./ 8 | COPY package-lock.json ./ 9 | RUN npm install --silent 10 | RUN npm install react-scripts@3.4.1 -g --silent 11 | 12 | COPY . ./ 13 | 14 | EXPOSE 3000 15 | 16 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html index.htm; 8 | try_files $uri /index.html; 9 | } 10 | 11 | error_page 500 502 503 504 /50x.html; 12 | location = /50x.html { 13 | root /usr/share/nginx/html; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:8080", 6 | "dependencies": { 7 | "@emotion/react": "^11.9.0", 8 | "@emotion/styled": "^11.8.1", 9 | "@mui/icons-material": "^5.6.1", 10 | "@mui/material": "^5.6.1", 11 | "@testing-library/jest-dom": "^5.16.4", 12 | "@testing-library/react": "^13.0.1", 13 | "@testing-library/user-event": "^13.5.0", 14 | "axios": "^0.26.1", 15 | "react": "^18.0.0", 16 | "react-dom": "^18.0.0", 17 | "react-router": "^6.3.0", 18 | "react-router-dom": "^6.3.0", 19 | "react-scripts": "5.0.1", 20 | "web-vitals": "^2.1.4" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frontend/public/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paw1a/ecommerce-api/c108da254f6fc84e9c912749a733c9a062782ab0/frontend/public/img.png -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React App 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | .App { 7 | background-color: #f5f5f5; 8 | } -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import './App.css' 3 | 4 | import {Link} from "react-router-dom"; 5 | import {CssBaseline} from "@mui/material"; 6 | 7 | function App() { 8 | return ( 9 |
10 | 11 | App |{" "} 12 | Catalog |{" "} 13 | Cart |{" "} 14 | Product |{" "} 15 | Sign-in |{" "} 16 | Sign-up |{" "} 17 |
18 | ) 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /frontend/src/Product.css: -------------------------------------------------------------------------------- 1 | .productList { 2 | display: block; 3 | } -------------------------------------------------------------------------------- /frontend/src/api/api.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import axios from "axios"; 3 | import {getToken} from "./auth"; 4 | 5 | const HOST = "localhost"; 6 | 7 | // axiosClient.defaults.baseURL = 'http://' + HOST + ':8080/api/v1'; 8 | // 9 | // axiosClient.defaults.headers = { 10 | // 'Content-Type': 'application/json', 11 | // 'Access-Control-Allow-Origin': '*', 12 | // 'Accept': 'application/json' 13 | // }; 14 | // 15 | // axiosClient.defaults.timeout = 6000; 16 | 17 | //axiosClient.defaults.withCredentials = true; 18 | 19 | // axios.interceptors.response.use(function (response) { 20 | // return response; 21 | // }, function (error) { 22 | // if(error.response.status === 401) { 23 | // console.log("Error 401"); 24 | // } 25 | // return Promise.reject(error); 26 | // }); 27 | 28 | export const useAxios = (url, method='get', payload='') => { 29 | const [data, setData] = useState(null); 30 | const [loaded, setLoaded] = useState(false); 31 | const [error, setError] = useState(false); 32 | 33 | useEffect(() => { 34 | const accessToken = getToken(); 35 | 36 | axios({ 37 | method: method, 38 | url: '/api/v1' + url, 39 | withCredentials: true, 40 | data: payload, 41 | headers: { 42 | Authorization: 'Bearer ' + accessToken 43 | } 44 | }) 45 | .then(resp => { 46 | const data = resp.data.data; 47 | console.log(data); 48 | setData(data); 49 | }) 50 | .catch(error => { 51 | console.log(error.response.data); 52 | setError(error.response.data); 53 | if (error.response.status === 401) { 54 | window.location = '/sign-in'; 55 | } 56 | }) 57 | .finally(() => setLoaded(true)); 58 | 59 | }, [setData, setError, setLoaded]); 60 | 61 | return [data, loaded, error]; 62 | } 63 | 64 | export const useAuth = (url, method='get', payload='') => { 65 | const [data, setData] = useState(null); 66 | const [loaded, setLoaded] = useState(false); 67 | const [error, setError] = useState(false); 68 | 69 | useEffect(() => { 70 | 71 | 72 | axios({ 73 | method: method, 74 | url: '/api/v1' + url, 75 | withCredentials: true, 76 | data: payload, 77 | }) 78 | .then(resp => { 79 | const data = resp.data.data; 80 | console.log(data); 81 | setData(data); 82 | }) 83 | .catch(error => { 84 | console.log(error.response.data); 85 | setError(error.response.data); 86 | }) 87 | .finally(() => setLoaded(true)); 88 | 89 | }, [setData, setError, setLoaded]); 90 | 91 | return [data, loaded, error]; 92 | } 93 | 94 | export const apiRequest = (url, method='get', payload='') => { 95 | let data, loaded, error 96 | 97 | axios({ 98 | method: method, 99 | url: '/api/v1' + url, 100 | withCredentials: true, 101 | data: payload, 102 | }) 103 | .then(resp => { 104 | data = resp.data.data; 105 | console.log(data); 106 | }) 107 | .catch(err => { 108 | error = err.response.data 109 | console.log(error); 110 | }) 111 | .finally(() => loaded = true); 112 | 113 | return [data, loaded, error] 114 | } 115 | -------------------------------------------------------------------------------- /frontend/src/api/auth.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import {useState} from "react"; 3 | 4 | let accessToken = JSON.parse(localStorage.getItem('ACCESS_TOKEN')) || null 5 | 6 | export const getExpirationDate = (jwtToken) => { 7 | if (!jwtToken) { 8 | return null; 9 | } 10 | 11 | const jwt = JSON.parse(atob(jwtToken.split('.')[1])); 12 | 13 | return jwt && jwt.exp && jwt.exp * 1000 || null; 14 | }; 15 | 16 | export const isExpired = (exp) => { 17 | if (!exp) { 18 | return false; 19 | } 20 | 21 | return Date.now() > exp; 22 | }; 23 | 24 | export const getToken = () => { 25 | if (!accessToken) { 26 | return null; 27 | } 28 | 29 | if (isExpired(getExpirationDate(accessToken))) { 30 | refreshToken(); 31 | } 32 | 33 | return accessToken; 34 | }; 35 | 36 | export const refreshToken = () => { 37 | let updatedToken, refreshError 38 | 39 | axios({ 40 | method: 'post', 41 | url: '/api/v1/users/auth/refresh', 42 | withCredentials: true, 43 | data: { 44 | fingerprint: "fingerprint" 45 | } 46 | }) 47 | .then(resp => { 48 | updatedToken = resp.data.data; 49 | setToken(updatedToken); 50 | }) 51 | .catch(error => { 52 | console.log(error.response.data); 53 | refreshError = error.response.data; 54 | }) 55 | } 56 | 57 | export const signIn = (event) => { 58 | event.preventDefault(); 59 | 60 | if (isLoggedIn()) 61 | return 62 | 63 | const data = new FormData(event.currentTarget); 64 | let token = null 65 | 66 | axios({ 67 | method: 'post', 68 | url: '/api/v1/users/auth/sign-in', 69 | withCredentials: true, 70 | data: { 71 | email: data.get('email'), 72 | password: data.get('password'), 73 | fingerprint: "fingerprint" 74 | } 75 | }) 76 | .then(resp => { 77 | token = resp.data.data; 78 | setToken(token); 79 | console.log(token); 80 | window.location = '/catalog'; 81 | }) 82 | .catch(error => { 83 | console.log(error.response.data); 84 | }) 85 | }; 86 | 87 | export const signUp = (event) => { 88 | event.preventDefault(); 89 | 90 | const data = new FormData(event.currentTarget); 91 | 92 | return axios({ 93 | method: 'post', 94 | url: '/api/v1/users/auth/sign-up', 95 | withCredentials: true, 96 | data: { 97 | email: data.get('email'), 98 | password: data.get('password'), 99 | name: data.get('name') 100 | } 101 | }) 102 | }; 103 | 104 | export const setToken = (token) => { 105 | if (token) { 106 | localStorage.setItem('ACCESS_TOKEN', JSON.stringify(token)); 107 | } else { 108 | localStorage.removeItem('ACCESS_TOKEN'); 109 | } 110 | 111 | accessToken = token; 112 | }; 113 | 114 | export const isLoggedIn = () => { 115 | return !!accessToken; 116 | }; 117 | -------------------------------------------------------------------------------- /frontend/src/api/cart.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paw1a/ecommerce-api/c108da254f6fc84e9c912749a733c9a062782ab0/frontend/src/api/cart.js -------------------------------------------------------------------------------- /frontend/src/api/products.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paw1a/ecommerce-api/c108da254f6fc84e9c912749a733c9a062782ab0/frontend/src/api/products.js -------------------------------------------------------------------------------- /frontend/src/components/Comment.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paw1a/ecommerce-api/c108da254f6fc84e9c912749a733c9a062782ab0/frontend/src/components/Comment.css -------------------------------------------------------------------------------- /frontend/src/components/Comment.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Card from '@mui/material/Card'; 3 | import CardHeader from '@mui/material/CardHeader'; 4 | import CardContent from '@mui/material/CardContent'; 5 | import Avatar from '@mui/material/Avatar'; 6 | import IconButton from '@mui/material/IconButton'; 7 | import Typography from '@mui/material/Typography'; 8 | import StarIcon from '@mui/icons-material/Star'; 9 | 10 | import './Comment.css' 11 | 12 | export default function Comment({comment}) { 13 | return ( 14 | 15 | 18 | } 19 | title={comment.username} 20 | subheader={comment.date} 21 | action={ 22 | 23 | 24 | {comment.rating} 25 | 26 | } 27 | sx={{paddingLeft: 0}} 28 | /> 29 | 30 | 31 | {comment.text} 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/components/Error.css: -------------------------------------------------------------------------------- 1 | .error-container { 2 | margin: 0 auto; 3 | max-width: 500px; 4 | border: 4px double red; 5 | margin-top: 30px; 6 | } -------------------------------------------------------------------------------- /frontend/src/components/Error.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Alert, AlertTitle} from "@mui/material"; 3 | 4 | import './Error.css' 5 | 6 | const Error = ({message}) => { 7 | return ( 8 |
9 | 10 | Error 11 | {message} 12 | 13 |
14 | ); 15 | }; 16 | 17 | export default Error; -------------------------------------------------------------------------------- /frontend/src/components/Footer.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | padding: 50px; 3 | background-color: white; 4 | } -------------------------------------------------------------------------------- /frontend/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Grid} from "@mui/material"; 3 | import Typography from "@mui/material/Typography"; 4 | 5 | import './Footer.css' 6 | 7 | const Footer = () => { 8 | const footers = [ 9 | { 10 | title: 'Company', 11 | description: ['Team', 'History', 'Contact us', 'Locations'], 12 | }, 13 | { 14 | title: 'Features', 15 | description: ['Cool stuff', 'Random feature', 'Team feature', 'Developer stuff', 'Another one'], 16 | }, 17 | { 18 | title: 'Resources', 19 | description: ['Resource', 'Resource name', 'Another resource', 'Final resource'], 20 | }, 21 | { 22 | title: 'Legal', 23 | description: ['Privacy policy', 'Terms of use'], 24 | }, 25 | ]; 26 | 27 | return ( 28 |
29 | 30 | {footers.map(footer => ( 31 | 32 | 33 | {footer.title} 34 | 35 | {footer.description.map(item => ( 36 | 37 | {item} 38 | 39 | ))} 40 | 41 | ))} 42 | 43 |
44 | ); 45 | }; 46 | 47 | export default Footer; -------------------------------------------------------------------------------- /frontend/src/components/Product.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 50px auto; 3 | padding: 50px; 4 | max-width: 1200px; 5 | } 6 | 7 | .image { 8 | 9 | } 10 | 11 | .content { 12 | padding: 0 50px; 13 | } 14 | 15 | .product-button { 16 | color: #bdbdbd; 17 | font-size: 18px; 18 | } 19 | 20 | .product-button:hover { 21 | color: #3b3b3b; 22 | } 23 | 24 | .price { 25 | font-size: 36px; 26 | margin: 30px 0; 27 | } 28 | 29 | .comments-container { 30 | margin: 50px 0; 31 | } -------------------------------------------------------------------------------- /frontend/src/components/Product.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button, Grid} from "@mui/material"; 3 | import image from "./catalog/img.png"; 4 | 5 | import './Product.css' 6 | import IconButton from "@mui/material/IconButton"; 7 | import StarIcon from "@mui/icons-material/Star"; 8 | import ModeCommentIcon from "@mui/icons-material/ModeComment"; 9 | import BookmarkIcon from "@mui/icons-material/Bookmark"; 10 | import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; 11 | import Comment from "./Comment"; 12 | import {useParams} from "react-router"; 13 | import {useAxios} from "../api/api"; 14 | import Error from "./Error"; 15 | 16 | const Product = () => { 17 | 18 | const { productID } = useParams() 19 | 20 | const [product, productLoaded, productError] = useAxios("/products/" + productID); 21 | const [comments, commentsLoaded, commentsError] = useAxios("/products/" + productID + "/reviews"); 22 | 23 | return ( 24 |
25 | {(productError || commentsError) && 26 | 27 | } 28 | {productLoaded && commentsLoaded && !commentsError && !productError && 29 |
30 | 31 | 32 | image 33 | 34 | 35 |
36 |

{product.name}

37 |

{product.description}

38 |
39 | 40 | 41 | {product.totalRating} 42 | 43 | 44 | 45 | 46 | {comments.length} 47 | 48 |
49 | 50 |
{product.price} $
51 | 56 | 57 | 58 | 59 |
60 |
61 |
62 | 63 |
64 |

Comments

65 | {comments.map(comment => ( 66 | 67 | ))} 68 |
69 |
70 | } 71 |
72 | ); 73 | }; 74 | 75 | export default Product; -------------------------------------------------------------------------------- /frontend/src/components/Toolbar.css: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | margin: 20px; 3 | } -------------------------------------------------------------------------------- /frontend/src/components/Toolbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {FormControl, InputLabel, Select} from "@mui/material"; 3 | import MenuItem from "@mui/material/MenuItem"; 4 | import './Toolbar.css' 5 | 6 | const Toolbar = () => { 7 | const [age, setAge] = React.useState(''); 8 | 9 | const handleChange = (event) => { 10 | setAge(event.target.value); 11 | }; 12 | 13 | return ( 14 |
15 | 16 | Age 17 | 29 | 30 |
31 | ); 32 | }; 33 | 34 | export default Toolbar; -------------------------------------------------------------------------------- /frontend/src/components/auth/SignIn.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Avatar from '@mui/material/Avatar'; 3 | import Button from '@mui/material/Button'; 4 | import CssBaseline from '@mui/material/CssBaseline'; 5 | import TextField from '@mui/material/TextField'; 6 | import FormControlLabel from '@mui/material/FormControlLabel'; 7 | import Checkbox from '@mui/material/Checkbox'; 8 | import Link from '@mui/material/Link'; 9 | import Grid from '@mui/material/Grid'; 10 | import Box from '@mui/material/Box'; 11 | import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; 12 | import Typography from '@mui/material/Typography'; 13 | import Container from '@mui/material/Container'; 14 | import { createTheme, ThemeProvider } from '@mui/material/styles'; 15 | import {signIn} from "../../api/auth"; 16 | 17 | function Copyright(props) { 18 | return ( 19 | 20 | {'Copyright © '} 21 | 22 | Ecommerce 23 | {' '} 24 | {new Date().getFullYear()} 25 | {'.'} 26 | 27 | ); 28 | } 29 | 30 | const theme = createTheme(); 31 | 32 | export default function SignIn() { 33 | return ( 34 | 35 | 36 | 37 | 45 | 46 | 47 | 48 | 49 | Sign in 50 | 51 | 52 | 62 | 72 | } 76 | label="Remember me" 77 | /> 78 | 87 | 88 | 89 | 90 | Forgot password? 91 | 92 | 93 | 94 | 95 | {"Don't have an account? Sign Up"} 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | ); 105 | } -------------------------------------------------------------------------------- /frontend/src/components/auth/SignUp.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Avatar from '@mui/material/Avatar'; 3 | import Button from '@mui/material/Button'; 4 | import CssBaseline from '@mui/material/CssBaseline'; 5 | import TextField from '@mui/material/TextField'; 6 | import Link from '@mui/material/Link'; 7 | import Grid from '@mui/material/Grid'; 8 | import Box from '@mui/material/Box'; 9 | import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; 10 | import Typography from '@mui/material/Typography'; 11 | import Container from '@mui/material/Container'; 12 | import { createTheme, ThemeProvider } from '@mui/material/styles'; 13 | import Error from "../Error"; 14 | import {useEffect, useState} from "react"; 15 | import {signUp} from "../../api/auth"; 16 | 17 | function Copyright(props) { 18 | return ( 19 | 20 | {'Copyright © '} 21 | 22 | Ecommerce 23 | {' '} 24 | {new Date().getFullYear()} 25 | {'.'} 26 | 27 | ); 28 | } 29 | 30 | const theme = createTheme(); 31 | 32 | export default function SignUp() { 33 | let [error, setError] = useState(null) 34 | 35 | const handleSubmit = (event) => { 36 | signUp(event) 37 | .then(() => { 38 | window.location = '/catalog'; 39 | }) 40 | .catch(err => { 41 | setError(err.response.data) 42 | console.log(error); 43 | }) 44 | }; 45 | 46 | return ( 47 | 48 | 49 | 50 | 51 | {error && } 52 | 53 | 61 | 62 | 63 | 64 | 65 | Sign up 66 | 67 | 68 | 69 | 70 | 79 | 80 | 81 | 89 | 90 | 91 | 99 | 100 | 101 | 110 | 111 | 112 | 121 | 122 | 123 | 124 | Already have an account? Sign in 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | ); 134 | } -------------------------------------------------------------------------------- /frontend/src/components/cart/Cart.css: -------------------------------------------------------------------------------- 1 | .cart-container { 2 | width: 80%; 3 | margin: 30px auto; 4 | } 5 | 6 | .card-header { 7 | padding-top: 20px; 8 | display: flex; 9 | justify-content: space-between; 10 | padding-right: 30px; 11 | } 12 | 13 | .cart-summary { 14 | padding: 20px; 15 | background-color: #e7e7e7; 16 | } 17 | 18 | .cart-content { 19 | padding-right: 30px; 20 | } 21 | 22 | .total-price { 23 | display: flex; 24 | justify-content: space-between; 25 | align-items: center; 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/cart/Cart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button, Grid} from "@mui/material"; 3 | 4 | import './Cart.css' 5 | import CartCard from "./CartCard"; 6 | import Link from "@mui/material/Link"; 7 | 8 | const Cart = () => { 9 | return ( 10 |
11 | 12 | {'Return to catalog'} 13 | 14 | 15 | 16 |
17 |

Shopping Cart

18 |

3 items

19 |
20 |
21 | 22 | 23 | 24 |
25 |
26 | 27 |
28 |

Order Summary

29 |
30 |

Total cost

31 | 3456$ 32 |
33 | 37 |
38 |
39 |
40 |
41 | ); 42 | }; 43 | 44 | export default Cart; -------------------------------------------------------------------------------- /frontend/src/components/cart/CartCard.css: -------------------------------------------------------------------------------- 1 | .cart-card { 2 | display: flex; 3 | justify-content: space-between; 4 | margin: 30px 0; 5 | align-items: center; 6 | flex-wrap: wrap; 7 | } 8 | 9 | .cart-image { 10 | height: 100px; 11 | width: auto; 12 | margin-right: 20px; 13 | } 14 | 15 | .product-image-name { 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | margin-bottom: 20px; 20 | } 21 | 22 | .counter { 23 | height: 35px; 24 | } 25 | 26 | .Mui-disabled { 27 | color: #3b3b3b; 28 | border-color: #3b3b3b; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/components/cart/CartCard.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | 3 | import image from '../catalog/img.png' 4 | import './CartCard.css' 5 | import {Button, ButtonGroup} from "@mui/material"; 6 | import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; 7 | import IconButton from "@mui/material/IconButton"; 8 | 9 | const CartCard = () => { 10 | const [counter, setCounter] = useState(0); 11 | 12 | return ( 13 |
14 |
15 | image 16 |
17 |

Name of the product

18 | 1200$ 19 |
20 |
21 | 22 | 23 | 27 | 28 | 29 | 30 | 34 | 35 | 36 |
37 | Total: 12000$ 38 |
39 | 40 | 41 | 42 | 43 |
44 | ); 45 | }; 46 | 47 | export default CartCard; -------------------------------------------------------------------------------- /frontend/src/components/catalog/Card.css: -------------------------------------------------------------------------------- 1 | .product-card { 2 | background-color: white; 3 | padding: 20px; 4 | height: 100%; 5 | } 6 | 7 | .image-row { 8 | flex-wrap: wrap; 9 | justify-content: space-between; 10 | display: flex; 11 | } 12 | 13 | .price-cart { 14 | display: flex; 15 | justify-content: space-between; 16 | } 17 | 18 | .card-button { 19 | color: #bdbdbd 20 | } 21 | 22 | .card-button:hover { 23 | color: #3b3b3b; 24 | } -------------------------------------------------------------------------------- /frontend/src/components/catalog/Card.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import image from './img.png' 3 | import BookmarkIcon from '@mui/icons-material/Bookmark'; 4 | import './Card.css' 5 | import IconButton from "@mui/material/IconButton"; 6 | import StarIcon from '@mui/icons-material/Star'; 7 | import ModeCommentIcon from '@mui/icons-material/ModeComment'; 8 | import AddShoppingCartIcon from '@mui/icons-material/AddShoppingCart'; 9 | import Link from "@mui/material/Link"; 10 | 11 | export default function ProductCard({product}) { 12 | return ( 13 |
14 |
15 | 16 | image 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | {product.totalRating} 26 | 27 | 28 | 29 | 30 | 17 31 | 32 |
33 |
34 | 35 | 36 |

{product.name}

37 | 38 |
39 |

{product.price} $

40 | 41 | 42 | 43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/components/catalog/Catalog.css: -------------------------------------------------------------------------------- 1 | .catalog { 2 | display: flex; 3 | flex-wrap: wrap; 4 | margin: 0 auto; 5 | padding: 30px; 6 | justify-content: center; 7 | } 8 | 9 | img { 10 | width: 100%; 11 | } 12 | 13 | .product-catalog { 14 | display: flex; 15 | flex-wrap: wrap; 16 | margin-left: 10px; 17 | order: 2; 18 | max-width: 900px; 19 | } 20 | 21 | .show-more { 22 | width: 100%; 23 | background-color: #e8e8e8; 24 | color: black; 25 | border-radius: 0; 26 | margin-bottom: 30px; 27 | height: 50px; 28 | } 29 | 30 | .show-more:hover { 31 | background-color: #dcdcdc; 32 | } 33 | 34 | .pagination-container { 35 | width: 100%; 36 | margin-bottom: 100px; 37 | justify-content: center; 38 | display: flex; 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/components/catalog/Catalog.jsx: -------------------------------------------------------------------------------- 1 | import './Catalog.css' 2 | import Filter from "./Filter"; 3 | import {Button, Grid, LinearProgress, Pagination} from "@mui/material"; 4 | import ProductCard from "./Card"; 5 | import {useAxios} from "../../api/api"; 6 | import Error from "../Error"; 7 | import React from "react"; 8 | 9 | const Catalog = () => { 10 | 11 | let [products, loaded, error] = useAxios("/products/"); 12 | 13 | console.log(products); 14 | 15 | return ( 16 | 17 | {!loaded && } 18 | {error && } 19 | {loaded && !error && 20 |
21 | 22 | 23 |
24 | 25 | {products.map(product => ( 26 | 27 | 28 | 29 | ))} 30 | 31 | 42 |
43 | 44 |
45 |
46 |
47 | } 48 |
49 | ); 50 | }; 51 | 52 | export default Catalog; -------------------------------------------------------------------------------- /frontend/src/components/catalog/Filter.css: -------------------------------------------------------------------------------- 1 | .filter { 2 | display: block; 3 | padding-left: 20px; 4 | width: 200px; 5 | } 6 | 7 | .price-box { 8 | display: flex; 9 | } 10 | 11 | @media (max-width: 700px) { 12 | .filter { 13 | width: 100%; 14 | flex-grow: 0; 15 | } 16 | } -------------------------------------------------------------------------------- /frontend/src/components/catalog/Filter.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import './Filter.css' 3 | import {Checkbox, FormControlLabel, List, Paper, Slider} from "@mui/material"; 4 | 5 | const Filter = () => { 6 | const [sliderValue, setSliderValue] = useState([0, 100]); 7 | const [price, setPrice] = useState([0, 120000]) 8 | 9 | const updateRange = (e, data) => { 10 | const from = Math.floor(120000 * (data[0] / 100)); 11 | const to = Math.floor(120000 * (data[1] / 100)); 12 | setPrice([from, to]); 13 | setSliderValue(data); 14 | }; 15 | 16 | return ( 17 |
18 |

Фильтр

19 | 20 |
Цена
21 |
22 |
23 |
От
24 |
{price[0]}
25 |
26 |
27 |
До
28 |
{price[1]}
29 |
30 |
31 | 'Minimum distance'} 34 | value={sliderValue} 35 | onChange={updateRange} 36 | disableSwap 37 | /> 38 | 39 |
Бренд
40 | 41 | 42 | } label="Adidas" /> 43 | } label="Adidas" /> 44 | } label="Adidas" /> 45 | } label="Adidas" /> 46 | } label="Adidas" /> 47 | } label="Adidas" /> 48 | } label="Adidas" /> 49 | } label="Adidas" /> 50 | } label="Adidas" /> 51 | } label="Adidas" /> 52 | } label="Adidas" /> 53 | 54 | 55 | 56 |
Пол
57 | } label="Мужской" /> 58 | } label="Женский" /> 59 | } label="Унисекс" /> 60 | 61 |
Категория
62 | 63 | 64 | } label="Унисекс" /> 65 | } label="Унисекс" /> 66 | } label="Унисекс" /> 67 | } label="Унисекс" /> 68 | } label="Унисекс" /> 69 | } label="Унисекс" /> 70 | } label="Унисекс" /> 71 | } label="Унисекс" /> 72 | 73 | 74 |
75 | ); 76 | }; 77 | 78 | export default Filter; -------------------------------------------------------------------------------- /frontend/src/components/catalog/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paw1a/ecommerce-api/c108da254f6fc84e9c912749a733c9a062782ab0/frontend/src/components/catalog/img.png -------------------------------------------------------------------------------- /frontend/src/components/navbar/Navbar.css: -------------------------------------------------------------------------------- 1 | header.MuiAppBar-root { 2 | box-shadow: none; 3 | } 4 | 5 | header.MuiPaper-root { 6 | box-shadow: none; 7 | } 8 | 9 | .profile-menu { 10 | padding: 30px; 11 | } 12 | 13 | .right-menu { 14 | 15 | } -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { 4 | BrowserRouter, 5 | Routes, 6 | Route, 7 | } from "react-router-dom"; 8 | 9 | import CatalogPage from "./pages/CatalogPage"; 10 | import ProductPage from "./pages/ProductPage"; 11 | import App from "./App"; 12 | import {CssBaseline} from "@mui/material"; 13 | 14 | import './App.css' 15 | import SignInPage from "./pages/SignInPage"; 16 | import SignUpPage from "./pages/SignUpPage"; 17 | import CartPage from "./pages/CartPage"; 18 | 19 | const container = document.getElementById('root'); 20 | const root = ReactDOM.createRoot(container); 21 | 22 | root.render( 23 | 24 | 25 |
26 | 27 | } /> 28 | } /> 29 | } /> 30 | } /> 31 | } /> 32 | } /> 33 | Page not found
} /> 34 | 35 | 36 |
, 37 | document.getElementById("root") 38 | ); 39 | -------------------------------------------------------------------------------- /frontend/src/pages/CartPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CssBaseline from "@mui/material/CssBaseline"; 3 | import Navbar from "../components/navbar/Navbar"; 4 | import Catalog from "../components/catalog/Catalog"; 5 | import Footer from "../components/Footer"; 6 | import Cart from "../components/cart/Cart"; 7 | 8 | const CartPage = () => { 9 | return ( 10 | 11 | 12 | 13 | 14 |