├── .env ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── core ├── __init__.py ├── admin.py ├── api │ ├── serializers.py │ ├── urls.py │ └── views.py ├── apps.py ├── forms.py ├── management │ └── commands │ │ └── rename.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_itemvariation_variation.py │ ├── 0003_auto_20190818_1553.py │ ├── 0004_auto_20190818_1553.py │ ├── 0005_orderitem_item_variations.py │ └── __init__.py ├── models.py ├── templatetags │ └── cart_template_tags.py ├── tests.py ├── urls.py └── views.py ├── db.sqlite3 ├── home ├── __init__.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── dev.py │ └── prod.py ├── urls.py └── wsgi │ ├── dev.py │ └── prod.py ├── manage.py ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── requirements.txt ├── src ├── App.js ├── App.test.js ├── constants.js ├── containers │ ├── Checkout.js │ ├── Home.js │ ├── Layout.js │ ├── Login.js │ ├── OrderSummary.js │ ├── ProductDetail.js │ ├── ProductList.js │ ├── Profile.js │ └── Signup.js ├── hoc │ └── hoc.js ├── index.js ├── registerServiceWorker.js ├── routes.js ├── store │ ├── actions │ │ ├── actionTypes.js │ │ ├── auth.js │ │ └── cart.js │ ├── reducers │ │ ├── auth.js │ │ └── cart.js │ └── utility.js └── utils.js └── thumbnail.png /.env: -------------------------------------------------------------------------------- 1 | 2 | STRIPE_TEST_PUBLIC_KEY='' 3 | STRIPE_TEST_SECRET_KEY='' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | **/*.pyc 3 | **/__pycache__ 4 | node_modules 5 | build 6 | static 7 | media 8 | package-lock.json -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "${workspaceFolder}/env/bin/python3", 3 | "editor.formatOnSave": true, 4 | "python.linting.pep8Enabled": true, 5 | "python.linting.pylintPath": "pylint", 6 | "python.linting.pylintArgs": ["--load-plugins", "pylint_django"], 7 | "python.linting.pylintEnabled": true, 8 | "python.venvPath": "${workspaceFolder}/env/bin/python3", 9 | "python.linting.pep8Args": ["--ignore=E501"], 10 | "files.exclude": { 11 | "**/*.pyc": true 12 | } 13 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | 4 | JustDjango 5 | 6 |

7 |

8 | The Definitive Django Learning Platform. 9 |

10 |

11 | 12 | # Django React Ecommerce 13 | 14 |

15 | 16 |

17 | 18 | This repository contains a Django and React ecommerce project. Among other functionality, users can create their account, add items to their cart and purchase those items using Stripe. 19 | 20 | [Watch the tutorials on how to build this project](https://youtu.be/RG_Y7lIDXPM) 21 | 22 | ## Backend development workflow 23 | 24 | ```json 25 | virtualenv env 26 | source env/bin/activate 27 | pip install -r requirements.txt 28 | python manage.py runserver 29 | ``` 30 | 31 | ## Frontend development workflow 32 | 33 | ```json 34 | npm i 35 | npm start 36 | ``` 37 | 38 | ## For deploying 39 | 40 | ```json 41 | npm run build 42 | ``` 43 | 44 | --- 45 | 46 |
47 | 48 | Other places you can find us:
49 | 50 | YouTube 51 | Twitter 52 | 53 |
54 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NinjaDevOps0831/django-react-ecommerce/80e2a609299ca22443e3685eca6510254b1969b1/core/__init__.py -------------------------------------------------------------------------------- /core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import ( 4 | Item, OrderItem, Order, Payment, Coupon, Refund, 5 | Address, UserProfile, Variation, ItemVariation 6 | ) 7 | 8 | 9 | def make_refund_accepted(modeladmin, request, queryset): 10 | queryset.update(refund_requested=False, refund_granted=True) 11 | 12 | 13 | make_refund_accepted.short_description = 'Update orders to refund granted' 14 | 15 | 16 | class OrderAdmin(admin.ModelAdmin): 17 | list_display = ['user', 18 | 'ordered', 19 | 'being_delivered', 20 | 'received', 21 | 'refund_requested', 22 | 'refund_granted', 23 | 'shipping_address', 24 | 'billing_address', 25 | 'payment', 26 | 'coupon' 27 | ] 28 | list_display_links = [ 29 | 'user', 30 | 'shipping_address', 31 | 'billing_address', 32 | 'payment', 33 | 'coupon' 34 | ] 35 | list_filter = ['ordered', 36 | 'being_delivered', 37 | 'received', 38 | 'refund_requested', 39 | 'refund_granted'] 40 | search_fields = [ 41 | 'user__username', 42 | 'ref_code' 43 | ] 44 | actions = [make_refund_accepted] 45 | 46 | 47 | class AddressAdmin(admin.ModelAdmin): 48 | list_display = [ 49 | 'user', 50 | 'street_address', 51 | 'apartment_address', 52 | 'country', 53 | 'zip', 54 | 'address_type', 55 | 'default' 56 | ] 57 | list_filter = ['default', 'address_type', 'country'] 58 | search_fields = ['user', 'street_address', 'apartment_address', 'zip'] 59 | 60 | 61 | class ItemVariationAdmin(admin.ModelAdmin): 62 | list_display = ['variation', 63 | 'value', 64 | 'attachment'] 65 | list_filter = ['variation', 'variation__item'] 66 | search_fields = ['value'] 67 | 68 | 69 | class ItemVariationInLineAdmin(admin.TabularInline): 70 | model = ItemVariation 71 | extra = 1 72 | 73 | 74 | class VariationAdmin(admin.ModelAdmin): 75 | list_display = ['item', 76 | 'name'] 77 | list_filter = ['item'] 78 | search_fields = ['name'] 79 | inlines = [ItemVariationInLineAdmin] 80 | 81 | 82 | admin.site.register(ItemVariation, ItemVariationAdmin) 83 | admin.site.register(Variation, VariationAdmin) 84 | admin.site.register(Item) 85 | admin.site.register(OrderItem) 86 | admin.site.register(Order, OrderAdmin) 87 | admin.site.register(Payment) 88 | admin.site.register(Coupon) 89 | admin.site.register(Refund) 90 | admin.site.register(Address, AddressAdmin) 91 | admin.site.register(UserProfile) 92 | -------------------------------------------------------------------------------- /core/api/serializers.py: -------------------------------------------------------------------------------- 1 | from django_countries.serializer_fields import CountryField 2 | from rest_framework import serializers 3 | from core.models import ( 4 | Address, Item, Order, OrderItem, Coupon, Variation, ItemVariation, 5 | Payment 6 | ) 7 | 8 | 9 | class StringSerializer(serializers.StringRelatedField): 10 | def to_internal_value(self, value): 11 | return value 12 | 13 | 14 | class CouponSerializer(serializers.ModelSerializer): 15 | class Meta: 16 | model = Coupon 17 | fields = ( 18 | 'id', 19 | 'code', 20 | 'amount' 21 | ) 22 | 23 | 24 | class ItemSerializer(serializers.ModelSerializer): 25 | category = serializers.SerializerMethodField() 26 | label = serializers.SerializerMethodField() 27 | 28 | class Meta: 29 | model = Item 30 | fields = ( 31 | 'id', 32 | 'title', 33 | 'price', 34 | 'discount_price', 35 | 'category', 36 | 'label', 37 | 'slug', 38 | 'description', 39 | 'image' 40 | ) 41 | 42 | def get_category(self, obj): 43 | return obj.get_category_display() 44 | 45 | def get_label(self, obj): 46 | return obj.get_label_display() 47 | 48 | 49 | class VariationDetailSerializer(serializers.ModelSerializer): 50 | item = serializers.SerializerMethodField() 51 | 52 | class Meta: 53 | model = Variation 54 | fields = ( 55 | 'id', 56 | 'name', 57 | 'item' 58 | ) 59 | 60 | def get_item(self, obj): 61 | return ItemSerializer(obj.item).data 62 | 63 | 64 | class ItemVariationDetailSerializer(serializers.ModelSerializer): 65 | variation = serializers.SerializerMethodField() 66 | 67 | class Meta: 68 | model = ItemVariation 69 | fields = ( 70 | 'id', 71 | 'value', 72 | 'attachment', 73 | 'variation' 74 | ) 75 | 76 | def get_variation(self, obj): 77 | return VariationDetailSerializer(obj.variation).data 78 | 79 | 80 | class OrderItemSerializer(serializers.ModelSerializer): 81 | item_variations = serializers.SerializerMethodField() 82 | item = serializers.SerializerMethodField() 83 | final_price = serializers.SerializerMethodField() 84 | 85 | class Meta: 86 | model = OrderItem 87 | fields = ( 88 | 'id', 89 | 'item', 90 | 'item_variations', 91 | 'quantity', 92 | 'final_price' 93 | ) 94 | 95 | def get_item(self, obj): 96 | return ItemSerializer(obj.item).data 97 | 98 | def get_item_variations(self, obj): 99 | return ItemVariationDetailSerializer(obj.item_variations.all(), many=True).data 100 | 101 | def get_final_price(self, obj): 102 | return obj.get_final_price() 103 | 104 | 105 | class OrderSerializer(serializers.ModelSerializer): 106 | order_items = serializers.SerializerMethodField() 107 | total = serializers.SerializerMethodField() 108 | coupon = serializers.SerializerMethodField() 109 | 110 | class Meta: 111 | model = Order 112 | fields = ( 113 | 'id', 114 | 'order_items', 115 | 'total', 116 | 'coupon' 117 | ) 118 | 119 | def get_order_items(self, obj): 120 | return OrderItemSerializer(obj.items.all(), many=True).data 121 | 122 | def get_total(self, obj): 123 | return obj.get_total() 124 | 125 | def get_coupon(self, obj): 126 | if obj.coupon is not None: 127 | return CouponSerializer(obj.coupon).data 128 | return None 129 | 130 | 131 | class ItemVariationSerializer(serializers.ModelSerializer): 132 | class Meta: 133 | model = ItemVariation 134 | fields = ( 135 | 'id', 136 | 'value', 137 | 'attachment' 138 | ) 139 | 140 | 141 | class VariationSerializer(serializers.ModelSerializer): 142 | item_variations = serializers.SerializerMethodField() 143 | 144 | class Meta: 145 | model = Variation 146 | fields = ( 147 | 'id', 148 | 'name', 149 | 'item_variations' 150 | ) 151 | 152 | def get_item_variations(self, obj): 153 | return ItemVariationSerializer(obj.itemvariation_set.all(), many=True).data 154 | 155 | 156 | class ItemDetailSerializer(serializers.ModelSerializer): 157 | category = serializers.SerializerMethodField() 158 | label = serializers.SerializerMethodField() 159 | variations = serializers.SerializerMethodField() 160 | 161 | class Meta: 162 | model = Item 163 | fields = ( 164 | 'id', 165 | 'title', 166 | 'price', 167 | 'discount_price', 168 | 'category', 169 | 'label', 170 | 'slug', 171 | 'description', 172 | 'image', 173 | 'variations' 174 | ) 175 | 176 | def get_category(self, obj): 177 | return obj.get_category_display() 178 | 179 | def get_label(self, obj): 180 | return obj.get_label_display() 181 | 182 | def get_variations(self, obj): 183 | return VariationSerializer(obj.variation_set.all(), many=True).data 184 | 185 | 186 | class AddressSerializer(serializers.ModelSerializer): 187 | country = CountryField() 188 | 189 | class Meta: 190 | model = Address 191 | fields = ( 192 | 'id', 193 | 'user', 194 | 'street_address', 195 | 'apartment_address', 196 | 'country', 197 | 'zip', 198 | 'address_type', 199 | 'default' 200 | ) 201 | 202 | 203 | class PaymentSerializer(serializers.ModelSerializer): 204 | class Meta: 205 | model = Payment 206 | fields = ( 207 | 'id', 208 | 'amount', 209 | 'timestamp' 210 | ) 211 | -------------------------------------------------------------------------------- /core/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import ( 3 | UserIDView, 4 | ItemListView, 5 | ItemDetailView, 6 | AddToCartView, 7 | OrderDetailView, 8 | OrderQuantityUpdateView, 9 | PaymentView, 10 | AddCouponView, 11 | CountryListView, 12 | AddressListView, 13 | AddressCreateView, 14 | AddressUpdateView, 15 | AddressDeleteView, 16 | OrderItemDeleteView, 17 | PaymentListView 18 | ) 19 | 20 | urlpatterns = [ 21 | path('user-id/', UserIDView.as_view(), name='user-id'), 22 | path('countries/', CountryListView.as_view(), name='country-list'), 23 | path('addresses/', AddressListView.as_view(), name='address-list'), 24 | path('addresses/create/', AddressCreateView.as_view(), name='address-create'), 25 | path('addresses//update/', 26 | AddressUpdateView.as_view(), name='address-update'), 27 | path('addresses//delete/', 28 | AddressDeleteView.as_view(), name='address-delete'), 29 | path('products/', ItemListView.as_view(), name='product-list'), 30 | path('products//', ItemDetailView.as_view(), name='product-detail'), 31 | path('add-to-cart/', AddToCartView.as_view(), name='add-to-cart'), 32 | path('order-summary/', OrderDetailView.as_view(), name='order-summary'), 33 | path('checkout/', PaymentView.as_view(), name='checkout'), 34 | path('add-coupon/', AddCouponView.as_view(), name='add-coupon'), 35 | path('order-items//delete/', 36 | OrderItemDeleteView.as_view(), name='order-item-delete'), 37 | path('order-item/update-quantity/', 38 | OrderQuantityUpdateView.as_view(), name='order-item-update-quantity'), 39 | path('payments/', PaymentListView.as_view(), name='payment-list'), 40 | 41 | ] 42 | -------------------------------------------------------------------------------- /core/api/views.py: -------------------------------------------------------------------------------- 1 | from django_countries import countries 2 | from django.db.models import Q 3 | from django.conf import settings 4 | from django.core.exceptions import ObjectDoesNotExist 5 | from django.http import Http404 6 | from django.shortcuts import render, get_object_or_404 7 | from django.utils import timezone 8 | from rest_framework.generics import ( 9 | ListAPIView, RetrieveAPIView, CreateAPIView, 10 | UpdateAPIView, DestroyAPIView 11 | ) 12 | from rest_framework.permissions import AllowAny, IsAuthenticated 13 | from rest_framework.views import APIView 14 | from rest_framework.response import Response 15 | from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST 16 | from core.models import Item, OrderItem, Order 17 | from .serializers import ( 18 | ItemSerializer, OrderSerializer, ItemDetailSerializer, AddressSerializer, 19 | PaymentSerializer 20 | ) 21 | from core.models import Item, OrderItem, Order, Address, Payment, Coupon, Refund, UserProfile, Variation, ItemVariation 22 | 23 | 24 | import stripe 25 | 26 | stripe.api_key = settings.STRIPE_SECRET_KEY 27 | 28 | 29 | class UserIDView(APIView): 30 | def get(self, request, *args, **kwargs): 31 | return Response({'userID': request.user.id}, status=HTTP_200_OK) 32 | 33 | 34 | class ItemListView(ListAPIView): 35 | permission_classes = (AllowAny,) 36 | serializer_class = ItemSerializer 37 | queryset = Item.objects.all() 38 | 39 | 40 | class ItemDetailView(RetrieveAPIView): 41 | permission_classes = (AllowAny,) 42 | serializer_class = ItemDetailSerializer 43 | queryset = Item.objects.all() 44 | 45 | 46 | class OrderQuantityUpdateView(APIView): 47 | def post(self, request, *args, **kwargs): 48 | slug = request.data.get('slug', None) 49 | if slug is None: 50 | return Response({"message": "Invalid data"}, status=HTTP_400_BAD_REQUEST) 51 | item = get_object_or_404(Item, slug=slug) 52 | order_qs = Order.objects.filter( 53 | user=request.user, 54 | ordered=False 55 | ) 56 | if order_qs.exists(): 57 | order = order_qs[0] 58 | # check if the order item is in the order 59 | if order.items.filter(item__slug=item.slug).exists(): 60 | order_item = OrderItem.objects.filter( 61 | item=item, 62 | user=request.user, 63 | ordered=False 64 | )[0] 65 | if order_item.quantity > 1: 66 | order_item.quantity -= 1 67 | order_item.save() 68 | else: 69 | order.items.remove(order_item) 70 | return Response(status=HTTP_200_OK) 71 | else: 72 | return Response({"message": "This item was not in your cart"}, status=HTTP_400_BAD_REQUEST) 73 | else: 74 | return Response({"message": "You do not have an active order"}, status=HTTP_400_BAD_REQUEST) 75 | 76 | 77 | class OrderItemDeleteView(DestroyAPIView): 78 | permission_classes = (IsAuthenticated, ) 79 | queryset = OrderItem.objects.all() 80 | 81 | 82 | class AddToCartView(APIView): 83 | def post(self, request, *args, **kwargs): 84 | slug = request.data.get('slug', None) 85 | variations = request.data.get('variations', []) 86 | if slug is None: 87 | return Response({"message": "Invalid request"}, status=HTTP_400_BAD_REQUEST) 88 | 89 | item = get_object_or_404(Item, slug=slug) 90 | 91 | minimum_variation_count = Variation.objects.filter(item=item).count() 92 | if len(variations) < minimum_variation_count: 93 | return Response({"message": "Please specify the required variation types"}, status=HTTP_400_BAD_REQUEST) 94 | 95 | order_item_qs = OrderItem.objects.filter( 96 | item=item, 97 | user=request.user, 98 | ordered=False 99 | ) 100 | for v in variations: 101 | order_item_qs = order_item_qs.filter( 102 | Q(item_variations__exact=v) 103 | ) 104 | 105 | if order_item_qs.exists(): 106 | order_item = order_item_qs.first() 107 | order_item.quantity += 1 108 | order_item.save() 109 | else: 110 | order_item = OrderItem.objects.create( 111 | item=item, 112 | user=request.user, 113 | ordered=False 114 | ) 115 | order_item.item_variations.add(*variations) 116 | order_item.save() 117 | 118 | order_qs = Order.objects.filter(user=request.user, ordered=False) 119 | if order_qs.exists(): 120 | order = order_qs[0] 121 | if not order.items.filter(item__id=order_item.id).exists(): 122 | order.items.add(order_item) 123 | return Response(status=HTTP_200_OK) 124 | 125 | else: 126 | ordered_date = timezone.now() 127 | order = Order.objects.create( 128 | user=request.user, ordered_date=ordered_date) 129 | order.items.add(order_item) 130 | return Response(status=HTTP_200_OK) 131 | 132 | 133 | class OrderDetailView(RetrieveAPIView): 134 | serializer_class = OrderSerializer 135 | permission_classes = (IsAuthenticated,) 136 | 137 | def get_object(self): 138 | try: 139 | order = Order.objects.get(user=self.request.user, ordered=False) 140 | return order 141 | except ObjectDoesNotExist: 142 | raise Http404("You do not have an active order") 143 | # return Response({"message": "You do not have an active order"}, status=HTTP_400_BAD_REQUEST) 144 | 145 | 146 | class PaymentView(APIView): 147 | 148 | def post(self, request, *args, **kwargs): 149 | order = Order.objects.get(user=self.request.user, ordered=False) 150 | userprofile = UserProfile.objects.get(user=self.request.user) 151 | token = request.data.get('stripeToken') 152 | billing_address_id = request.data.get('selectedBillingAddress') 153 | shipping_address_id = request.data.get('selectedShippingAddress') 154 | 155 | billing_address = Address.objects.get(id=billing_address_id) 156 | shipping_address = Address.objects.get(id=shipping_address_id) 157 | 158 | if userprofile.stripe_customer_id != '' and userprofile.stripe_customer_id is not None: 159 | customer = stripe.Customer.retrieve( 160 | userprofile.stripe_customer_id) 161 | customer.sources.create(source=token) 162 | 163 | else: 164 | customer = stripe.Customer.create( 165 | email=self.request.user.email, 166 | ) 167 | customer.sources.create(source=token) 168 | userprofile.stripe_customer_id = customer['id'] 169 | userprofile.one_click_purchasing = True 170 | userprofile.save() 171 | 172 | amount = int(order.get_total() * 100) 173 | 174 | try: 175 | 176 | # charge the customer because we cannot charge the token more than once 177 | charge = stripe.Charge.create( 178 | amount=amount, # cents 179 | currency="usd", 180 | customer=userprofile.stripe_customer_id 181 | ) 182 | # charge once off on the token 183 | # charge = stripe.Charge.create( 184 | # amount=amount, # cents 185 | # currency="usd", 186 | # source=token 187 | # ) 188 | 189 | # create the payment 190 | payment = Payment() 191 | payment.stripe_charge_id = charge['id'] 192 | payment.user = self.request.user 193 | payment.amount = order.get_total() 194 | payment.save() 195 | 196 | # assign the payment to the order 197 | 198 | order_items = order.items.all() 199 | order_items.update(ordered=True) 200 | for item in order_items: 201 | item.save() 202 | 203 | order.ordered = True 204 | order.payment = payment 205 | order.billing_address = billing_address 206 | order.shipping_address = shipping_address 207 | # order.ref_code = create_ref_code() 208 | order.save() 209 | 210 | return Response(status=HTTP_200_OK) 211 | 212 | except stripe.error.CardError as e: 213 | body = e.json_body 214 | err = body.get('error', {}) 215 | return Response({"message": f"{err.get('message')}"}, status=HTTP_400_BAD_REQUEST) 216 | 217 | except stripe.error.RateLimitError as e: 218 | # Too many requests made to the API too quickly 219 | messages.warning(self.request, "Rate limit error") 220 | return Response({"message": "Rate limit error"}, status=HTTP_400_BAD_REQUEST) 221 | 222 | except stripe.error.InvalidRequestError as e: 223 | print(e) 224 | # Invalid parameters were supplied to Stripe's API 225 | return Response({"message": "Invalid parameters"}, status=HTTP_400_BAD_REQUEST) 226 | 227 | except stripe.error.AuthenticationError as e: 228 | # Authentication with Stripe's API failed 229 | # (maybe you changed API keys recently) 230 | return Response({"message": "Not authenticated"}, status=HTTP_400_BAD_REQUEST) 231 | 232 | except stripe.error.APIConnectionError as e: 233 | # Network communication with Stripe failed 234 | return Response({"message": "Network error"}, status=HTTP_400_BAD_REQUEST) 235 | 236 | except stripe.error.StripeError as e: 237 | # Display a very generic error to the user, and maybe send 238 | # yourself an email 239 | return Response({"message": "Something went wrong. You were not charged. Please try again."}, status=HTTP_400_BAD_REQUEST) 240 | 241 | except Exception as e: 242 | # send an email to ourselves 243 | return Response({"message": "A serious error occurred. We have been notifed."}, status=HTTP_400_BAD_REQUEST) 244 | 245 | return Response({"message": "Invalid data received"}, status=HTTP_400_BAD_REQUEST) 246 | 247 | 248 | class AddCouponView(APIView): 249 | def post(self, request, *args, **kwargs): 250 | code = request.data.get('code', None) 251 | if code is None: 252 | return Response({"message": "Invalid data received"}, status=HTTP_400_BAD_REQUEST) 253 | order = Order.objects.get( 254 | user=self.request.user, ordered=False) 255 | coupon = get_object_or_404(Coupon, code=code) 256 | order.coupon = coupon 257 | order.save() 258 | return Response(status=HTTP_200_OK) 259 | 260 | 261 | class CountryListView(APIView): 262 | def get(self, request, *args, **kwargs): 263 | return Response(countries, status=HTTP_200_OK) 264 | 265 | 266 | class AddressListView(ListAPIView): 267 | permission_classes = (IsAuthenticated, ) 268 | serializer_class = AddressSerializer 269 | 270 | def get_queryset(self): 271 | address_type = self.request.query_params.get('address_type', None) 272 | qs = Address.objects.all() 273 | if address_type is None: 274 | return qs 275 | return qs.filter(user=self.request.user, address_type=address_type) 276 | 277 | 278 | class AddressCreateView(CreateAPIView): 279 | permission_classes = (IsAuthenticated, ) 280 | serializer_class = AddressSerializer 281 | queryset = Address.objects.all() 282 | 283 | 284 | class AddressUpdateView(UpdateAPIView): 285 | permission_classes = (IsAuthenticated, ) 286 | serializer_class = AddressSerializer 287 | queryset = Address.objects.all() 288 | 289 | 290 | class AddressDeleteView(DestroyAPIView): 291 | permission_classes = (IsAuthenticated, ) 292 | queryset = Address.objects.all() 293 | 294 | 295 | class PaymentListView(ListAPIView): 296 | permission_classes = (IsAuthenticated, ) 297 | serializer_class = PaymentSerializer 298 | 299 | def get_queryset(self): 300 | return Payment.objects.filter(user=self.request.user) 301 | -------------------------------------------------------------------------------- /core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = 'core' 6 | -------------------------------------------------------------------------------- /core/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django_countries.fields import CountryField 3 | from django_countries.widgets import CountrySelectWidget 4 | 5 | 6 | PAYMENT_CHOICES = ( 7 | ('S', 'Stripe'), 8 | ('P', 'PayPal') 9 | ) 10 | 11 | 12 | class CheckoutForm(forms.Form): 13 | shipping_address = forms.CharField(required=False) 14 | shipping_address2 = forms.CharField(required=False) 15 | shipping_country = CountryField(blank_label='(select country)').formfield( 16 | required=False, 17 | widget=CountrySelectWidget(attrs={ 18 | 'class': 'custom-select d-block w-100', 19 | })) 20 | shipping_zip = forms.CharField(required=False) 21 | 22 | billing_address = forms.CharField(required=False) 23 | billing_address2 = forms.CharField(required=False) 24 | billing_country = CountryField(blank_label='(select country)').formfield( 25 | required=False, 26 | widget=CountrySelectWidget(attrs={ 27 | 'class': 'custom-select d-block w-100', 28 | })) 29 | billing_zip = forms.CharField(required=False) 30 | 31 | same_billing_address = forms.BooleanField(required=False) 32 | set_default_shipping = forms.BooleanField(required=False) 33 | use_default_shipping = forms.BooleanField(required=False) 34 | set_default_billing = forms.BooleanField(required=False) 35 | use_default_billing = forms.BooleanField(required=False) 36 | 37 | payment_option = forms.ChoiceField( 38 | widget=forms.RadioSelect, choices=PAYMENT_CHOICES) 39 | 40 | 41 | class CouponForm(forms.Form): 42 | code = forms.CharField(widget=forms.TextInput(attrs={ 43 | 'class': 'form-control', 44 | 'placeholder': 'Promo code', 45 | 'aria-label': 'Recipient\'s username', 46 | 'aria-describedby': 'basic-addon2' 47 | })) 48 | 49 | 50 | class RefundForm(forms.Form): 51 | ref_code = forms.CharField() 52 | message = forms.CharField(widget=forms.Textarea(attrs={ 53 | 'rows': 4 54 | })) 55 | email = forms.EmailField() 56 | 57 | 58 | class PaymentForm(forms.Form): 59 | stripeToken = forms.CharField(required=False) 60 | save = forms.BooleanField(required=False) 61 | use_default = forms.BooleanField(required=False) 62 | -------------------------------------------------------------------------------- /core/management/commands/rename.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.core.management.base import BaseCommand 3 | 4 | 5 | class Command(BaseCommand): 6 | help = 'Renames a Django project' 7 | 8 | def add_arguments(self, parser): 9 | parser.add_argument('new_project_name', type=str, help='The new Django project name') 10 | 11 | def handle(self, *args, **kwargs): 12 | new_project_name = kwargs['new_project_name'] 13 | 14 | # logic for renaming the files 15 | 16 | files_to_rename = ['demo/settings/base.py', 'demo/wsgi.py', 'manage.py'] 17 | folder_to_rename = 'demo' 18 | 19 | for f in files_to_rename: 20 | with open(f, 'r') as file: 21 | filedata = file.read() 22 | 23 | filedata = filedata.replace('demo', new_project_name) 24 | 25 | with open(f, 'w') as file: 26 | file.write(filedata) 27 | 28 | os.rename(folder_to_rename, new_project_name) 29 | 30 | self.stdout.write(self.style.SUCCESS('Project has been renamed to %s' % new_project_name)) 31 | -------------------------------------------------------------------------------- /core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.3 on 2019-07-03 21:42 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django_countries.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Address', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('street_address', models.CharField(max_length=100)), 23 | ('apartment_address', models.CharField(max_length=100)), 24 | ('country', django_countries.fields.CountryField(max_length=2)), 25 | ('zip', models.CharField(max_length=100)), 26 | ('address_type', models.CharField(choices=[('B', 'Billing'), ('S', 'Shipping')], max_length=1)), 27 | ('default', models.BooleanField(default=False)), 28 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 29 | ], 30 | options={ 31 | 'verbose_name_plural': 'Addresses', 32 | }, 33 | ), 34 | migrations.CreateModel( 35 | name='Coupon', 36 | fields=[ 37 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 38 | ('code', models.CharField(max_length=15)), 39 | ('amount', models.FloatField()), 40 | ], 41 | ), 42 | migrations.CreateModel( 43 | name='Item', 44 | fields=[ 45 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 46 | ('title', models.CharField(max_length=100)), 47 | ('price', models.FloatField()), 48 | ('discount_price', models.FloatField(blank=True, null=True)), 49 | ('category', models.CharField(choices=[('S', 'Shirt'), ('SW', 'Sport wear'), ('OW', 'Outwear')], max_length=2)), 50 | ('label', models.CharField(choices=[('P', 'primary'), ('S', 'secondary'), ('D', 'danger')], max_length=1)), 51 | ('slug', models.SlugField()), 52 | ('description', models.TextField()), 53 | ('image', models.ImageField(upload_to='')), 54 | ], 55 | ), 56 | migrations.CreateModel( 57 | name='Order', 58 | fields=[ 59 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 60 | ('ref_code', models.CharField(blank=True, max_length=20, null=True)), 61 | ('start_date', models.DateTimeField(auto_now_add=True)), 62 | ('ordered_date', models.DateTimeField()), 63 | ('ordered', models.BooleanField(default=False)), 64 | ('being_delivered', models.BooleanField(default=False)), 65 | ('received', models.BooleanField(default=False)), 66 | ('refund_requested', models.BooleanField(default=False)), 67 | ('refund_granted', models.BooleanField(default=False)), 68 | ('billing_address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='billing_address', to='core.Address')), 69 | ('coupon', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.Coupon')), 70 | ], 71 | ), 72 | migrations.CreateModel( 73 | name='UserProfile', 74 | fields=[ 75 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 76 | ('stripe_customer_id', models.CharField(blank=True, max_length=50, null=True)), 77 | ('one_click_purchasing', models.BooleanField(default=False)), 78 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 79 | ], 80 | ), 81 | migrations.CreateModel( 82 | name='Refund', 83 | fields=[ 84 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 85 | ('reason', models.TextField()), 86 | ('accepted', models.BooleanField(default=False)), 87 | ('email', models.EmailField(max_length=254)), 88 | ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Order')), 89 | ], 90 | ), 91 | migrations.CreateModel( 92 | name='Payment', 93 | fields=[ 94 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 95 | ('stripe_charge_id', models.CharField(max_length=50)), 96 | ('amount', models.FloatField()), 97 | ('timestamp', models.DateTimeField(auto_now_add=True)), 98 | ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), 99 | ], 100 | ), 101 | migrations.CreateModel( 102 | name='OrderItem', 103 | fields=[ 104 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 105 | ('ordered', models.BooleanField(default=False)), 106 | ('quantity', models.IntegerField(default=1)), 107 | ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Item')), 108 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 109 | ], 110 | ), 111 | migrations.AddField( 112 | model_name='order', 113 | name='items', 114 | field=models.ManyToManyField(to='core.OrderItem'), 115 | ), 116 | migrations.AddField( 117 | model_name='order', 118 | name='payment', 119 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.Payment'), 120 | ), 121 | migrations.AddField( 122 | model_name='order', 123 | name='shipping_address', 124 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='shipping_address', to='core.Address'), 125 | ), 126 | migrations.AddField( 127 | model_name='order', 128 | name='user', 129 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 130 | ), 131 | ] 132 | -------------------------------------------------------------------------------- /core/migrations/0002_itemvariation_variation.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-18 15:37 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('core', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Variation', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=50)), 19 | ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Item')), 20 | ], 21 | options={ 22 | 'unique_together': {('item', 'name')}, 23 | }, 24 | ), 25 | migrations.CreateModel( 26 | name='ItemVariation', 27 | fields=[ 28 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 29 | ('value', models.CharField(max_length=50)), 30 | ('attachment', models.ImageField(upload_to='')), 31 | ('variation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Variation')), 32 | ], 33 | options={ 34 | 'unique_together': {('variation', 'value')}, 35 | }, 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /core/migrations/0003_auto_20190818_1553.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-18 15:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0002_itemvariation_variation'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='itemvariation', 15 | name='attachment', 16 | field=models.ImageField(null=True, upload_to=''), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /core/migrations/0004_auto_20190818_1553.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-18 15:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0003_auto_20190818_1553'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='itemvariation', 15 | name='attachment', 16 | field=models.ImageField(blank=True, upload_to=''), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /core/migrations/0005_orderitem_item_variations.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-21 19:33 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0004_auto_20190818_1553'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='orderitem', 15 | name='item_variations', 16 | field=models.ManyToManyField(to='core.ItemVariation'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NinjaDevOps0831/django-react-ecommerce/80e2a609299ca22443e3685eca6510254b1969b1/core/migrations/__init__.py -------------------------------------------------------------------------------- /core/models.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import post_save 2 | from django.conf import settings 3 | from django.db import models 4 | from django.db.models import Sum 5 | from django.shortcuts import reverse 6 | from django_countries.fields import CountryField 7 | 8 | 9 | CATEGORY_CHOICES = ( 10 | ('S', 'Shirt'), 11 | ('SW', 'Sport wear'), 12 | ('OW', 'Outwear') 13 | ) 14 | 15 | LABEL_CHOICES = ( 16 | ('P', 'primary'), 17 | ('S', 'secondary'), 18 | ('D', 'danger') 19 | ) 20 | 21 | ADDRESS_CHOICES = ( 22 | ('B', 'Billing'), 23 | ('S', 'Shipping'), 24 | ) 25 | 26 | 27 | class UserProfile(models.Model): 28 | user = models.OneToOneField( 29 | settings.AUTH_USER_MODEL, on_delete=models.CASCADE) 30 | stripe_customer_id = models.CharField(max_length=50, blank=True, null=True) 31 | one_click_purchasing = models.BooleanField(default=False) 32 | 33 | def __str__(self): 34 | return self.user.username 35 | 36 | 37 | class Item(models.Model): 38 | title = models.CharField(max_length=100) 39 | price = models.FloatField() 40 | discount_price = models.FloatField(blank=True, null=True) 41 | category = models.CharField(choices=CATEGORY_CHOICES, max_length=2) 42 | label = models.CharField(choices=LABEL_CHOICES, max_length=1) 43 | slug = models.SlugField() 44 | description = models.TextField() 45 | image = models.ImageField() 46 | 47 | def __str__(self): 48 | return self.title 49 | 50 | def get_absolute_url(self): 51 | return reverse("core:product", kwargs={ 52 | 'slug': self.slug 53 | }) 54 | 55 | def get_add_to_cart_url(self): 56 | return reverse("core:add-to-cart", kwargs={ 57 | 'slug': self.slug 58 | }) 59 | 60 | def get_remove_from_cart_url(self): 61 | return reverse("core:remove-from-cart", kwargs={ 62 | 'slug': self.slug 63 | }) 64 | 65 | 66 | class Variation(models.Model): 67 | item = models.ForeignKey(Item, on_delete=models.CASCADE) 68 | name = models.CharField(max_length=50) # size 69 | 70 | class Meta: 71 | unique_together = ( 72 | ('item', 'name') 73 | ) 74 | 75 | def __str__(self): 76 | return self.name 77 | 78 | 79 | class ItemVariation(models.Model): 80 | variation = models.ForeignKey(Variation, on_delete=models.CASCADE) 81 | value = models.CharField(max_length=50) # S, M, L 82 | attachment = models.ImageField(blank=True) 83 | 84 | class Meta: 85 | unique_together = ( 86 | ('variation', 'value') 87 | ) 88 | 89 | def __str__(self): 90 | return self.value 91 | 92 | 93 | class OrderItem(models.Model): 94 | user = models.ForeignKey(settings.AUTH_USER_MODEL, 95 | on_delete=models.CASCADE) 96 | ordered = models.BooleanField(default=False) 97 | item = models.ForeignKey(Item, on_delete=models.CASCADE) 98 | item_variations = models.ManyToManyField(ItemVariation) 99 | quantity = models.IntegerField(default=1) 100 | 101 | def __str__(self): 102 | return f"{self.quantity} of {self.item.title}" 103 | 104 | def get_total_item_price(self): 105 | return self.quantity * self.item.price 106 | 107 | def get_total_discount_item_price(self): 108 | return self.quantity * self.item.discount_price 109 | 110 | def get_amount_saved(self): 111 | return self.get_total_item_price() - self.get_total_discount_item_price() 112 | 113 | def get_final_price(self): 114 | if self.item.discount_price: 115 | return self.get_total_discount_item_price() 116 | return self.get_total_item_price() 117 | 118 | 119 | class Order(models.Model): 120 | user = models.ForeignKey(settings.AUTH_USER_MODEL, 121 | on_delete=models.CASCADE) 122 | ref_code = models.CharField(max_length=20, blank=True, null=True) 123 | items = models.ManyToManyField(OrderItem) 124 | start_date = models.DateTimeField(auto_now_add=True) 125 | ordered_date = models.DateTimeField() 126 | ordered = models.BooleanField(default=False) 127 | shipping_address = models.ForeignKey( 128 | 'Address', related_name='shipping_address', on_delete=models.SET_NULL, blank=True, null=True) 129 | billing_address = models.ForeignKey( 130 | 'Address', related_name='billing_address', on_delete=models.SET_NULL, blank=True, null=True) 131 | payment = models.ForeignKey( 132 | 'Payment', on_delete=models.SET_NULL, blank=True, null=True) 133 | coupon = models.ForeignKey( 134 | 'Coupon', on_delete=models.SET_NULL, blank=True, null=True) 135 | being_delivered = models.BooleanField(default=False) 136 | received = models.BooleanField(default=False) 137 | refund_requested = models.BooleanField(default=False) 138 | refund_granted = models.BooleanField(default=False) 139 | 140 | ''' 141 | 1. Item added to cart 142 | 2. Adding a billing address 143 | (Failed checkout) 144 | 3. Payment 145 | (Preprocessing, processing, packaging etc.) 146 | 4. Being delivered 147 | 5. Received 148 | 6. Refunds 149 | ''' 150 | 151 | def __str__(self): 152 | return self.user.username 153 | 154 | def get_total(self): 155 | total = 0 156 | for order_item in self.items.all(): 157 | total += order_item.get_final_price() 158 | if self.coupon: 159 | total -= self.coupon.amount 160 | return total 161 | 162 | 163 | class Address(models.Model): 164 | user = models.ForeignKey(settings.AUTH_USER_MODEL, 165 | on_delete=models.CASCADE) 166 | street_address = models.CharField(max_length=100) 167 | apartment_address = models.CharField(max_length=100) 168 | country = CountryField(multiple=False) 169 | zip = models.CharField(max_length=100) 170 | address_type = models.CharField(max_length=1, choices=ADDRESS_CHOICES) 171 | default = models.BooleanField(default=False) 172 | 173 | def __str__(self): 174 | return self.user.username 175 | 176 | class Meta: 177 | verbose_name_plural = 'Addresses' 178 | 179 | 180 | class Payment(models.Model): 181 | stripe_charge_id = models.CharField(max_length=50) 182 | user = models.ForeignKey(settings.AUTH_USER_MODEL, 183 | on_delete=models.SET_NULL, blank=True, null=True) 184 | amount = models.FloatField() 185 | timestamp = models.DateTimeField(auto_now_add=True) 186 | 187 | def __str__(self): 188 | return self.user.username 189 | 190 | 191 | class Coupon(models.Model): 192 | code = models.CharField(max_length=15) 193 | amount = models.FloatField() 194 | 195 | def __str__(self): 196 | return self.code 197 | 198 | 199 | class Refund(models.Model): 200 | order = models.ForeignKey(Order, on_delete=models.CASCADE) 201 | reason = models.TextField() 202 | accepted = models.BooleanField(default=False) 203 | email = models.EmailField() 204 | 205 | def __str__(self): 206 | return f"{self.pk}" 207 | 208 | 209 | def userprofile_receiver(sender, instance, created, *args, **kwargs): 210 | if created: 211 | userprofile = UserProfile.objects.create(user=instance) 212 | 213 | 214 | post_save.connect(userprofile_receiver, sender=settings.AUTH_USER_MODEL) 215 | -------------------------------------------------------------------------------- /core/templatetags/cart_template_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from core.models import Order 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.filter 8 | def cart_item_count(user): 9 | if user.is_authenticated: 10 | qs = Order.objects.filter(user=user, ordered=False) 11 | if qs.exists(): 12 | return qs[0].items.count() 13 | return 0 14 | -------------------------------------------------------------------------------- /core/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /core/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import ( 3 | ItemDetailView, 4 | CheckoutView, 5 | HomeView, 6 | OrderSummaryView, 7 | add_to_cart, 8 | remove_from_cart, 9 | remove_single_item_from_cart, 10 | PaymentView, 11 | AddCouponView, 12 | RequestRefundView 13 | ) 14 | 15 | app_name = 'core' 16 | 17 | urlpatterns = [ 18 | path('', HomeView.as_view(), name='home'), 19 | path('checkout/', CheckoutView.as_view(), name='checkout'), 20 | path('order-summary/', OrderSummaryView.as_view(), name='order-summary'), 21 | path('product//', ItemDetailView.as_view(), name='product'), 22 | path('add-to-cart//', add_to_cart, name='add-to-cart'), 23 | path('add-coupon/', AddCouponView.as_view(), name='add-coupon'), 24 | path('remove-from-cart//', remove_from_cart, name='remove-from-cart'), 25 | path('remove-item-from-cart//', remove_single_item_from_cart, 26 | name='remove-single-item-from-cart'), 27 | path('payment//', PaymentView.as_view(), name='payment'), 28 | path('request-refund/', RequestRefundView.as_view(), name='request-refund') 29 | ] 30 | -------------------------------------------------------------------------------- /core/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib import messages 3 | from django.core.exceptions import ObjectDoesNotExist 4 | from django.contrib.auth.decorators import login_required 5 | from django.contrib.auth.mixins import LoginRequiredMixin 6 | from django.shortcuts import render, get_object_or_404 7 | from django.views.generic import ListView, DetailView, View 8 | from django.shortcuts import redirect 9 | from django.utils import timezone 10 | from .forms import CheckoutForm, CouponForm, RefundForm, PaymentForm 11 | from .models import Item, OrderItem, Order, Address, Payment, Coupon, Refund, UserProfile 12 | 13 | import random 14 | import string 15 | import stripe 16 | stripe.api_key = settings.STRIPE_SECRET_KEY 17 | 18 | 19 | def create_ref_code(): 20 | return ''.join(random.choices(string.ascii_lowercase + string.digits, k=20)) 21 | 22 | 23 | def products(request): 24 | context = { 25 | 'items': Item.objects.all() 26 | } 27 | return render(request, "products.html", context) 28 | 29 | 30 | def is_valid_form(values): 31 | valid = True 32 | for field in values: 33 | if field == '': 34 | valid = False 35 | return valid 36 | 37 | 38 | class CheckoutView(View): 39 | def get(self, *args, **kwargs): 40 | try: 41 | order = Order.objects.get(user=self.request.user, ordered=False) 42 | form = CheckoutForm() 43 | context = { 44 | 'form': form, 45 | 'couponform': CouponForm(), 46 | 'order': order, 47 | 'DISPLAY_COUPON_FORM': True 48 | } 49 | 50 | shipping_address_qs = Address.objects.filter( 51 | user=self.request.user, 52 | address_type='S', 53 | default=True 54 | ) 55 | if shipping_address_qs.exists(): 56 | context.update( 57 | {'default_shipping_address': shipping_address_qs[0]}) 58 | 59 | billing_address_qs = Address.objects.filter( 60 | user=self.request.user, 61 | address_type='B', 62 | default=True 63 | ) 64 | if billing_address_qs.exists(): 65 | context.update( 66 | {'default_billing_address': billing_address_qs[0]}) 67 | 68 | return render(self.request, "checkout.html", context) 69 | except ObjectDoesNotExist: 70 | messages.info(self.request, "You do not have an active order") 71 | return redirect("core:checkout") 72 | 73 | def post(self, *args, **kwargs): 74 | form = CheckoutForm(self.request.POST or None) 75 | try: 76 | order = Order.objects.get(user=self.request.user, ordered=False) 77 | if form.is_valid(): 78 | 79 | use_default_shipping = form.cleaned_data.get( 80 | 'use_default_shipping') 81 | if use_default_shipping: 82 | print("Using the defualt shipping address") 83 | address_qs = Address.objects.filter( 84 | user=self.request.user, 85 | address_type='S', 86 | default=True 87 | ) 88 | if address_qs.exists(): 89 | shipping_address = address_qs[0] 90 | order.shipping_address = shipping_address 91 | order.save() 92 | else: 93 | messages.info( 94 | self.request, "No default shipping address available") 95 | return redirect('core:checkout') 96 | else: 97 | print("User is entering a new shipping address") 98 | shipping_address1 = form.cleaned_data.get( 99 | 'shipping_address') 100 | shipping_address2 = form.cleaned_data.get( 101 | 'shipping_address2') 102 | shipping_country = form.cleaned_data.get( 103 | 'shipping_country') 104 | shipping_zip = form.cleaned_data.get('shipping_zip') 105 | 106 | if is_valid_form([shipping_address1, shipping_country, shipping_zip]): 107 | shipping_address = Address( 108 | user=self.request.user, 109 | street_address=shipping_address1, 110 | apartment_address=shipping_address2, 111 | country=shipping_country, 112 | zip=shipping_zip, 113 | address_type='S' 114 | ) 115 | shipping_address.save() 116 | 117 | order.shipping_address = shipping_address 118 | order.save() 119 | 120 | set_default_shipping = form.cleaned_data.get( 121 | 'set_default_shipping') 122 | if set_default_shipping: 123 | shipping_address.default = True 124 | shipping_address.save() 125 | 126 | else: 127 | messages.info( 128 | self.request, "Please fill in the required shipping address fields") 129 | 130 | use_default_billing = form.cleaned_data.get( 131 | 'use_default_billing') 132 | same_billing_address = form.cleaned_data.get( 133 | 'same_billing_address') 134 | 135 | if same_billing_address: 136 | billing_address = shipping_address 137 | billing_address.pk = None 138 | billing_address.save() 139 | billing_address.address_type = 'B' 140 | billing_address.save() 141 | order.billing_address = billing_address 142 | order.save() 143 | 144 | elif use_default_billing: 145 | print("Using the defualt billing address") 146 | address_qs = Address.objects.filter( 147 | user=self.request.user, 148 | address_type='B', 149 | default=True 150 | ) 151 | if address_qs.exists(): 152 | billing_address = address_qs[0] 153 | order.billing_address = billing_address 154 | order.save() 155 | else: 156 | messages.info( 157 | self.request, "No default billing address available") 158 | return redirect('core:checkout') 159 | else: 160 | print("User is entering a new billing address") 161 | billing_address1 = form.cleaned_data.get( 162 | 'billing_address') 163 | billing_address2 = form.cleaned_data.get( 164 | 'billing_address2') 165 | billing_country = form.cleaned_data.get( 166 | 'billing_country') 167 | billing_zip = form.cleaned_data.get('billing_zip') 168 | 169 | if is_valid_form([billing_address1, billing_country, billing_zip]): 170 | billing_address = Address( 171 | user=self.request.user, 172 | street_address=billing_address1, 173 | apartment_address=billing_address2, 174 | country=billing_country, 175 | zip=billing_zip, 176 | address_type='B' 177 | ) 178 | billing_address.save() 179 | 180 | order.billing_address = billing_address 181 | order.save() 182 | 183 | set_default_billing = form.cleaned_data.get( 184 | 'set_default_billing') 185 | if set_default_billing: 186 | billing_address.default = True 187 | billing_address.save() 188 | 189 | else: 190 | messages.info( 191 | self.request, "Please fill in the required billing address fields") 192 | 193 | payment_option = form.cleaned_data.get('payment_option') 194 | 195 | if payment_option == 'S': 196 | return redirect('core:payment', payment_option='stripe') 197 | elif payment_option == 'P': 198 | return redirect('core:payment', payment_option='paypal') 199 | else: 200 | messages.warning( 201 | self.request, "Invalid payment option selected") 202 | return redirect('core:checkout') 203 | except ObjectDoesNotExist: 204 | messages.warning(self.request, "You do not have an active order") 205 | return redirect("core:order-summary") 206 | 207 | 208 | class PaymentView(View): 209 | def get(self, *args, **kwargs): 210 | order = Order.objects.get(user=self.request.user, ordered=False) 211 | if order.billing_address: 212 | context = { 213 | 'order': order, 214 | 'DISPLAY_COUPON_FORM': False 215 | } 216 | userprofile = self.request.user.userprofile 217 | if userprofile.one_click_purchasing: 218 | # fetch the users card list 219 | cards = stripe.Customer.list_sources( 220 | userprofile.stripe_customer_id, 221 | limit=3, 222 | object='card' 223 | ) 224 | card_list = cards['data'] 225 | if len(card_list) > 0: 226 | # update the context with the default card 227 | context.update({ 228 | 'card': card_list[0] 229 | }) 230 | return render(self.request, "payment.html", context) 231 | else: 232 | messages.warning( 233 | self.request, "You have not added a billing address") 234 | return redirect("core:checkout") 235 | 236 | def post(self, *args, **kwargs): 237 | order = Order.objects.get(user=self.request.user, ordered=False) 238 | form = PaymentForm(self.request.POST) 239 | userprofile = UserProfile.objects.get(user=self.request.user) 240 | if form.is_valid(): 241 | token = form.cleaned_data.get('stripeToken') 242 | save = form.cleaned_data.get('save') 243 | use_default = form.cleaned_data.get('use_default') 244 | 245 | if save: 246 | if userprofile.stripe_customer_id != '' and userprofile.stripe_customer_id is not None: 247 | customer = stripe.Customer.retrieve( 248 | userprofile.stripe_customer_id) 249 | customer.sources.create(source=token) 250 | 251 | else: 252 | customer = stripe.Customer.create( 253 | email=self.request.user.email, 254 | ) 255 | customer.sources.create(source=token) 256 | userprofile.stripe_customer_id = customer['id'] 257 | userprofile.one_click_purchasing = True 258 | userprofile.save() 259 | 260 | amount = int(order.get_total() * 100) 261 | 262 | try: 263 | 264 | if use_default or save: 265 | # charge the customer because we cannot charge the token more than once 266 | charge = stripe.Charge.create( 267 | amount=amount, # cents 268 | currency="usd", 269 | customer=userprofile.stripe_customer_id 270 | ) 271 | else: 272 | # charge once off on the token 273 | charge = stripe.Charge.create( 274 | amount=amount, # cents 275 | currency="usd", 276 | source=token 277 | ) 278 | 279 | # create the payment 280 | payment = Payment() 281 | payment.stripe_charge_id = charge['id'] 282 | payment.user = self.request.user 283 | payment.amount = order.get_total() 284 | payment.save() 285 | 286 | # assign the payment to the order 287 | 288 | order_items = order.items.all() 289 | order_items.update(ordered=True) 290 | for item in order_items: 291 | item.save() 292 | 293 | order.ordered = True 294 | order.payment = payment 295 | order.ref_code = create_ref_code() 296 | order.save() 297 | 298 | messages.success(self.request, "Your order was successful!") 299 | return redirect("/") 300 | 301 | except stripe.error.CardError as e: 302 | body = e.json_body 303 | err = body.get('error', {}) 304 | messages.warning(self.request, f"{err.get('message')}") 305 | return redirect("/") 306 | 307 | except stripe.error.RateLimitError as e: 308 | # Too many requests made to the API too quickly 309 | messages.warning(self.request, "Rate limit error") 310 | return redirect("/") 311 | 312 | except stripe.error.InvalidRequestError as e: 313 | # Invalid parameters were supplied to Stripe's API 314 | messages.warning(self.request, "Invalid parameters") 315 | return redirect("/") 316 | 317 | except stripe.error.AuthenticationError as e: 318 | # Authentication with Stripe's API failed 319 | # (maybe you changed API keys recently) 320 | messages.warning(self.request, "Not authenticated") 321 | return redirect("/") 322 | 323 | except stripe.error.APIConnectionError as e: 324 | # Network communication with Stripe failed 325 | messages.warning(self.request, "Network error") 326 | return redirect("/") 327 | 328 | except stripe.error.StripeError as e: 329 | # Display a very generic error to the user, and maybe send 330 | # yourself an email 331 | messages.warning( 332 | self.request, "Something went wrong. You were not charged. Please try again.") 333 | return redirect("/") 334 | 335 | except Exception as e: 336 | # send an email to ourselves 337 | messages.warning( 338 | self.request, "A serious error occurred. We have been notifed.") 339 | return redirect("/") 340 | 341 | messages.warning(self.request, "Invalid data received") 342 | return redirect("/payment/stripe/") 343 | 344 | 345 | class HomeView(ListView): 346 | model = Item 347 | paginate_by = 10 348 | template_name = "home.html" 349 | 350 | 351 | class OrderSummaryView(LoginRequiredMixin, View): 352 | def get(self, *args, **kwargs): 353 | try: 354 | order = Order.objects.get(user=self.request.user, ordered=False) 355 | context = { 356 | 'object': order 357 | } 358 | return render(self.request, 'order_summary.html', context) 359 | except ObjectDoesNotExist: 360 | messages.warning(self.request, "You do not have an active order") 361 | return redirect("/") 362 | 363 | 364 | class ItemDetailView(DetailView): 365 | model = Item 366 | template_name = "product.html" 367 | 368 | 369 | @login_required 370 | def add_to_cart(request, slug): 371 | item = get_object_or_404(Item, slug=slug) 372 | order_item, created = OrderItem.objects.get_or_create( 373 | item=item, 374 | user=request.user, 375 | ordered=False 376 | ) 377 | order_qs = Order.objects.filter(user=request.user, ordered=False) 378 | if order_qs.exists(): 379 | order = order_qs[0] 380 | # check if the order item is in the order 381 | if order.items.filter(item__slug=item.slug).exists(): 382 | order_item.quantity += 1 383 | order_item.save() 384 | messages.info(request, "This item quantity was updated.") 385 | return redirect("core:order-summary") 386 | else: 387 | order.items.add(order_item) 388 | messages.info(request, "This item was added to your cart.") 389 | return redirect("core:order-summary") 390 | else: 391 | ordered_date = timezone.now() 392 | order = Order.objects.create( 393 | user=request.user, ordered_date=ordered_date) 394 | order.items.add(order_item) 395 | messages.info(request, "This item was added to your cart.") 396 | return redirect("core:order-summary") 397 | 398 | 399 | @login_required 400 | def remove_from_cart(request, slug): 401 | item = get_object_or_404(Item, slug=slug) 402 | order_qs = Order.objects.filter( 403 | user=request.user, 404 | ordered=False 405 | ) 406 | if order_qs.exists(): 407 | order = order_qs[0] 408 | # check if the order item is in the order 409 | if order.items.filter(item__slug=item.slug).exists(): 410 | order_item = OrderItem.objects.filter( 411 | item=item, 412 | user=request.user, 413 | ordered=False 414 | )[0] 415 | order.items.remove(order_item) 416 | messages.info(request, "This item was removed from your cart.") 417 | return redirect("core:order-summary") 418 | else: 419 | messages.info(request, "This item was not in your cart") 420 | return redirect("core:product", slug=slug) 421 | else: 422 | messages.info(request, "You do not have an active order") 423 | return redirect("core:product", slug=slug) 424 | 425 | 426 | @login_required 427 | def remove_single_item_from_cart(request, slug): 428 | item = get_object_or_404(Item, slug=slug) 429 | order_qs = Order.objects.filter( 430 | user=request.user, 431 | ordered=False 432 | ) 433 | if order_qs.exists(): 434 | order = order_qs[0] 435 | # check if the order item is in the order 436 | if order.items.filter(item__slug=item.slug).exists(): 437 | order_item = OrderItem.objects.filter( 438 | item=item, 439 | user=request.user, 440 | ordered=False 441 | )[0] 442 | if order_item.quantity > 1: 443 | order_item.quantity -= 1 444 | order_item.save() 445 | else: 446 | order.items.remove(order_item) 447 | messages.info(request, "This item quantity was updated.") 448 | return redirect("core:order-summary") 449 | else: 450 | messages.info(request, "This item was not in your cart") 451 | return redirect("core:product", slug=slug) 452 | else: 453 | messages.info(request, "You do not have an active order") 454 | return redirect("core:product", slug=slug) 455 | 456 | 457 | def get_coupon(request, code): 458 | try: 459 | coupon = Coupon.objects.get(code=code) 460 | return coupon 461 | except ObjectDoesNotExist: 462 | messages.info(request, "This coupon does not exist") 463 | return redirect("core:checkout") 464 | 465 | 466 | class AddCouponView(View): 467 | def post(self, *args, **kwargs): 468 | form = CouponForm(self.request.POST or None) 469 | if form.is_valid(): 470 | try: 471 | code = form.cleaned_data.get('code') 472 | order = Order.objects.get( 473 | user=self.request.user, ordered=False) 474 | order.coupon = get_coupon(self.request, code) 475 | order.save() 476 | messages.success(self.request, "Successfully added coupon") 477 | return redirect("core:checkout") 478 | except ObjectDoesNotExist: 479 | messages.info(self.request, "You do not have an active order") 480 | return redirect("core:checkout") 481 | 482 | 483 | class RequestRefundView(View): 484 | def get(self, *args, **kwargs): 485 | form = RefundForm() 486 | context = { 487 | 'form': form 488 | } 489 | return render(self.request, "request_refund.html", context) 490 | 491 | def post(self, *args, **kwargs): 492 | form = RefundForm(self.request.POST) 493 | if form.is_valid(): 494 | ref_code = form.cleaned_data.get('ref_code') 495 | message = form.cleaned_data.get('message') 496 | email = form.cleaned_data.get('email') 497 | # edit the order 498 | try: 499 | order = Order.objects.get(ref_code=ref_code) 500 | order.refund_requested = True 501 | order.save() 502 | 503 | # store the refund 504 | refund = Refund() 505 | refund.order = order 506 | refund.reason = message 507 | refund.email = email 508 | refund.save() 509 | 510 | messages.info(self.request, "Your request was received.") 511 | return redirect("core:request-refund") 512 | 513 | except ObjectDoesNotExist: 514 | messages.info(self.request, "This order does not exist.") 515 | return redirect("core:request-refund") 516 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NinjaDevOps0831/django-react-ecommerce/80e2a609299ca22443e3685eca6510254b1969b1/db.sqlite3 -------------------------------------------------------------------------------- /home/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NinjaDevOps0831/django-react-ecommerce/80e2a609299ca22443e3685eca6510254b1969b1/home/__init__.py -------------------------------------------------------------------------------- /home/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NinjaDevOps0831/django-react-ecommerce/80e2a609299ca22443e3685eca6510254b1969b1/home/settings/__init__.py -------------------------------------------------------------------------------- /home/settings/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | from decouple import config 3 | 4 | BASE_DIR = os.path.dirname(os.path.dirname( 5 | os.path.dirname(os.path.abspath(__file__)))) 6 | SECRET_KEY = '-05sgp9!deq=q1nltm@^^2cc+v29i(tyybv3v2t77qi66czazj' 7 | DEBUG = True 8 | ALLOWED_HOSTS = [] 9 | 10 | INSTALLED_APPS = [ 11 | 'django.contrib.admin', 12 | 'django.contrib.auth', 13 | 'django.contrib.contenttypes', 14 | 'django.contrib.sessions', 15 | 'django.contrib.messages', 16 | 'django.contrib.staticfiles', 17 | 18 | 'django.contrib.sites', 19 | 'allauth', 20 | 'allauth.account', 21 | 'allauth.socialaccount', 22 | 'corsheaders', 23 | 'rest_auth', 24 | 'rest_auth.registration', 25 | 'rest_framework', 26 | 'rest_framework.authtoken', 27 | 28 | 'core' 29 | ] 30 | 31 | MIDDLEWARE = [ 32 | 'corsheaders.middleware.CorsMiddleware', 33 | 'django.middleware.security.SecurityMiddleware', 34 | 'django.contrib.sessions.middleware.SessionMiddleware', 35 | 'django.middleware.common.CommonMiddleware', 36 | 'django.middleware.csrf.CsrfViewMiddleware', 37 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 38 | 'django.contrib.messages.middleware.MessageMiddleware', 39 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 40 | ] 41 | 42 | ROOT_URLCONF = 'home.urls' 43 | 44 | TEMPLATES = [ 45 | { 46 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 47 | 'DIRS': [os.path.join(BASE_DIR, 'build')], 48 | 'APP_DIRS': True, 49 | 'OPTIONS': { 50 | 'context_processors': [ 51 | 'django.template.context_processors.debug', 52 | 'django.template.context_processors.request', 53 | 'django.contrib.auth.context_processors.auth', 54 | 'django.contrib.messages.context_processors.messages', 55 | ], 56 | }, 57 | }, 58 | ] 59 | 60 | LANGUAGE_CODE = 'en-us' 61 | TIME_ZONE = 'UTC' 62 | USE_I18N = True 63 | USE_L10N = True 64 | USE_TZ = True 65 | 66 | STATIC_URL = '/static/' 67 | MEDIA_URL = '/media/' 68 | STATICFILES_DIRS = [os.path.join(BASE_DIR, 'build/static')] 69 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 70 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 71 | SITE_ID = 1 72 | 73 | REST_FRAMEWORK = { 74 | 'DEFAULT_PERMISSION_CLASSES': ( 75 | 'rest_framework.permissions.AllowAny', 76 | ), 77 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 78 | 'rest_framework.authentication.TokenAuthentication', 79 | ), 80 | } 81 | 82 | ACCOUNT_EMAIL_REQUIRED = False 83 | ACCOUNT_AUTHENTICATION_METHOD = 'username' 84 | ACCOUNT_EMAIL_VERIFICATION = 'none' 85 | -------------------------------------------------------------------------------- /home/settings/dev.py: -------------------------------------------------------------------------------- 1 | '''Use this for development''' 2 | 3 | from .base import * 4 | 5 | ALLOWED_HOSTS += ['127.0.0.1'] 6 | DEBUG = True 7 | 8 | WSGI_APPLICATION = 'home.wsgi.dev.application' 9 | 10 | DATABASES = { 11 | 'default': { 12 | 'ENGINE': 'django.db.backends.sqlite3', 13 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 14 | } 15 | } 16 | 17 | CORS_ORIGIN_WHITELIST = ( 18 | 'http://localhost:3000', 19 | ) 20 | 21 | # Stripe 22 | 23 | STRIPE_PUBLIC_KEY = config('STRIPE_TEST_PUBLIC_KEY') 24 | STRIPE_SECRET_KEY = config('STRIPE_TEST_SECRET_KEY') 25 | -------------------------------------------------------------------------------- /home/settings/prod.py: -------------------------------------------------------------------------------- 1 | '''Use this for production''' 2 | 3 | from .base import * 4 | 5 | DEBUG = False 6 | ALLOWED_HOSTS += ['http://domain.com'] 7 | WSGI_APPLICATION = 'home.wsgi.prod.application' 8 | 9 | DATABASES = { 10 | 'default': { 11 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 12 | 'NAME': 'db_name', 13 | 'USER': 'db_user', 14 | 'PASSWORD': 'db_password', 15 | 'HOST': 'localhost', 16 | 'PORT': '', 17 | } 18 | } 19 | 20 | AUTH_PASSWORD_VALIDATORS = [ 21 | {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, 22 | {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, 23 | {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, 24 | {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, 25 | ] 26 | 27 | STATICFILES_STORAGE = 'whitenoise.django.GzipManifestStaticFilesStorage' 28 | -------------------------------------------------------------------------------- /home/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.urls import path, include, re_path 5 | from django.views.generic import TemplateView 6 | 7 | urlpatterns = [ 8 | path('api-auth/', include('rest_framework.urls')), 9 | path('rest-auth/', include('rest_auth.urls')), 10 | path('rest-auth/registration/', include('rest_auth.registration.urls')), 11 | path('admin/', admin.site.urls), 12 | path('api/', include('core.api.urls')), 13 | ] 14 | 15 | if settings.DEBUG: 16 | urlpatterns += static(settings.MEDIA_URL, 17 | document_root=settings.MEDIA_ROOT) 18 | 19 | 20 | if not settings.DEBUG: 21 | urlpatterns += [re_path(r'^.*', 22 | TemplateView.as_view(template_name='index.html'))] 23 | -------------------------------------------------------------------------------- /home/wsgi/dev.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "home.settings.dev") 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /home/wsgi/prod.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | from whitenoise.django import DjangoWhiteNoise 5 | 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "home.settings.prod") 7 | 8 | application = get_wsgi_application() 9 | application = DjangoWhiteNoise(application) 10 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | if __name__ == "__main__": 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "home.settings.dev") 6 | try: 7 | from django.core.management import execute_from_command_line 8 | except ImportError as exc: 9 | raise ImportError( 10 | "Couldn't import Django. Are you sure it's installed and " 11 | "available on your PYTHONPATH environment variable? Did you " 12 | "forget to activate a virtual environment?" 13 | ) from exc 14 | execute_from_command_line(sys.argv) 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-react-boilerplate", 3 | "version": "0.3.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.19.0", 7 | "prop-types": "^15.7.2", 8 | "react": "^16.8.6", 9 | "react-dom": "^16.8.6", 10 | "react-redux": "^7.1.0", 11 | "react-router-dom": "^5.0.1", 12 | "react-scripts": "^3.0.1", 13 | "react-stripe-elements": "^4.0.0", 14 | "redux": "^4.0.1", 15 | "redux-thunk": "^2.3.0", 16 | "semantic-ui-css": "^2.4.1", 17 | "semantic-ui-react": "^0.87.2" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test --env=jsdom", 23 | "eject": "react-scripts eject" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NinjaDevOps0831/django-react-ecommerce/80e2a609299ca22443e3685eca6510254b1969b1/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Django React Boilerplate 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | astroid==2.2.5 2 | autopep8==1.4.4 3 | certifi==2019.6.16 4 | chardet==3.0.4 5 | defusedxml==0.6.0 6 | Django==2.2.13 7 | django-allauth==0.39.1 8 | django-cors-headers==3.0.2 9 | django-countries==5.3.3 10 | django-rest-auth==0.9.5 11 | djangorestframework==3.9.4 12 | gunicorn==19.9.0 13 | idna==2.8 14 | isort==4.3.21 15 | lazy-object-proxy==1.4.1 16 | mccabe==0.6.1 17 | oauthlib==3.0.1 18 | pep8==1.7.1 19 | Pillow==6.2.0 20 | pycodestyle==2.5.0 21 | pylint==2.3.1 22 | python-decouple==3.1 23 | python3-openid==3.1.0 24 | pytz==2019.1 25 | requests==2.22.0 26 | requests-oauthlib==1.2.0 27 | six==1.12.0 28 | sqlparse==0.3.0 29 | stripe==2.33.0 30 | typed-ast==1.4.0 31 | urllib3==1.25.3 32 | wrapt==1.11.2 33 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { BrowserRouter as Router } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import BaseRouter from "./routes"; 5 | import * as actions from "./store/actions/auth"; 6 | import "semantic-ui-css/semantic.min.css"; 7 | import CustomLayout from "./containers/Layout"; 8 | 9 | class App extends Component { 10 | componentDidMount() { 11 | this.props.onTryAutoSignup(); 12 | } 13 | 14 | render() { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | } 24 | 25 | const mapStateToProps = state => { 26 | return { 27 | isAuthenticated: state.auth.token !== null 28 | }; 29 | }; 30 | 31 | const mapDispatchToProps = dispatch => { 32 | return { 33 | onTryAutoSignup: () => dispatch(actions.authCheckState()) 34 | }; 35 | }; 36 | 37 | export default connect( 38 | mapStateToProps, 39 | mapDispatchToProps 40 | )(App); 41 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | const localhost = "http://127.0.0.1:8000"; 2 | 3 | const apiURL = "/api"; 4 | 5 | export const endpoint = `${localhost}${apiURL}`; 6 | 7 | export const productListURL = `${endpoint}/products/`; 8 | export const productDetailURL = id => `${endpoint}/products/${id}/`; 9 | export const addToCartURL = `${endpoint}/add-to-cart/`; 10 | export const orderSummaryURL = `${endpoint}/order-summary/`; 11 | export const checkoutURL = `${endpoint}/checkout/`; 12 | export const addCouponURL = `${endpoint}/add-coupon/`; 13 | export const countryListURL = `${endpoint}/countries/`; 14 | export const userIDURL = `${endpoint}/user-id/`; 15 | export const addressListURL = addressType => 16 | `${endpoint}/addresses/?address_type=${addressType}`; 17 | export const addressCreateURL = `${endpoint}/addresses/create/`; 18 | export const addressUpdateURL = id => `${endpoint}/addresses/${id}/update/`; 19 | export const addressDeleteURL = id => `${endpoint}/addresses/${id}/delete/`; 20 | export const orderItemDeleteURL = id => `${endpoint}/order-items/${id}/delete/`; 21 | export const orderItemUpdateQuantityURL = `${endpoint}/order-item/update-quantity/`; 22 | export const paymentListURL = `${endpoint}/payments/`; 23 | -------------------------------------------------------------------------------- /src/containers/Checkout.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | CardElement, 4 | injectStripe, 5 | Elements, 6 | StripeProvider 7 | } from "react-stripe-elements"; 8 | import { 9 | Button, 10 | Container, 11 | Dimmer, 12 | Divider, 13 | Form, 14 | Header, 15 | Image, 16 | Item, 17 | Label, 18 | Loader, 19 | Message, 20 | Segment, 21 | Select 22 | } from "semantic-ui-react"; 23 | import { Link, withRouter } from "react-router-dom"; 24 | import { authAxios } from "../utils"; 25 | import { 26 | checkoutURL, 27 | orderSummaryURL, 28 | addCouponURL, 29 | addressListURL 30 | } from "../constants"; 31 | 32 | const OrderPreview = props => { 33 | const { data } = props; 34 | return ( 35 | 36 | {data && ( 37 | 38 | 39 | {data.order_items.map((orderItem, i) => { 40 | return ( 41 | 42 | 46 | 47 | 48 | {orderItem.quantity} x {orderItem.item.title} 49 | 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | })} 57 | 58 | 59 | 60 | 61 | 62 | 63 | Order Total: ${data.total} 64 | {data.coupon && ( 65 | 69 | )} 70 | 71 | 72 | 73 | 74 | 75 | )} 76 | 77 | ); 78 | }; 79 | 80 | class CouponForm extends Component { 81 | state = { 82 | code: "" 83 | }; 84 | 85 | handleChange = e => { 86 | this.setState({ 87 | code: e.target.value 88 | }); 89 | }; 90 | 91 | handleSubmit = e => { 92 | const { code } = this.state; 93 | this.props.handleAddCoupon(e, code); 94 | this.setState({ code: "" }); 95 | }; 96 | 97 | render() { 98 | const { code } = this.state; 99 | return ( 100 | 101 |
102 | 103 | 104 | 109 | 110 | 111 |
112 |
113 | ); 114 | } 115 | } 116 | 117 | class CheckoutForm extends Component { 118 | state = { 119 | data: null, 120 | loading: false, 121 | error: null, 122 | success: false, 123 | shippingAddresses: [], 124 | billingAddresses: [], 125 | selectedBillingAddress: "", 126 | selectedShippingAddress: "" 127 | }; 128 | 129 | componentDidMount() { 130 | this.handleFetchOrder(); 131 | this.handleFetchBillingAddresses(); 132 | this.handleFetchShippingAddresses(); 133 | } 134 | 135 | handleGetDefaultAddress = addresses => { 136 | const filteredAddresses = addresses.filter(el => el.default === true); 137 | if (filteredAddresses.length > 0) { 138 | return filteredAddresses[0].id; 139 | } 140 | return ""; 141 | }; 142 | 143 | handleFetchBillingAddresses = () => { 144 | this.setState({ loading: true }); 145 | authAxios 146 | .get(addressListURL("B")) 147 | .then(res => { 148 | this.setState({ 149 | billingAddresses: res.data.map(a => { 150 | return { 151 | key: a.id, 152 | text: `${a.street_address}, ${a.apartment_address}, ${a.country}`, 153 | value: a.id 154 | }; 155 | }), 156 | selectedBillingAddress: this.handleGetDefaultAddress(res.data), 157 | loading: false 158 | }); 159 | }) 160 | .catch(err => { 161 | this.setState({ error: err, loading: false }); 162 | }); 163 | }; 164 | 165 | handleFetchShippingAddresses = () => { 166 | this.setState({ loading: true }); 167 | authAxios 168 | .get(addressListURL("S")) 169 | .then(res => { 170 | this.setState({ 171 | shippingAddresses: res.data.map(a => { 172 | return { 173 | key: a.id, 174 | text: `${a.street_address}, ${a.apartment_address}, ${a.country}`, 175 | value: a.id 176 | }; 177 | }), 178 | selectedShippingAddress: this.handleGetDefaultAddress(res.data), 179 | loading: false 180 | }); 181 | }) 182 | .catch(err => { 183 | this.setState({ error: err, loading: false }); 184 | }); 185 | }; 186 | 187 | handleFetchOrder = () => { 188 | this.setState({ loading: true }); 189 | authAxios 190 | .get(orderSummaryURL) 191 | .then(res => { 192 | this.setState({ data: res.data, loading: false }); 193 | }) 194 | .catch(err => { 195 | if (err.response.status === 404) { 196 | this.props.history.push("/products"); 197 | } else { 198 | this.setState({ error: err, loading: false }); 199 | } 200 | }); 201 | }; 202 | 203 | handleAddCoupon = (e, code) => { 204 | e.preventDefault(); 205 | this.setState({ loading: true }); 206 | authAxios 207 | .post(addCouponURL, { code }) 208 | .then(res => { 209 | this.setState({ loading: false }); 210 | this.handleFetchOrder(); 211 | }) 212 | .catch(err => { 213 | this.setState({ error: err, loading: false }); 214 | }); 215 | }; 216 | 217 | handleSelectChange = (e, { name, value }) => { 218 | this.setState({ [name]: value }); 219 | }; 220 | 221 | submit = ev => { 222 | ev.preventDefault(); 223 | this.setState({ loading: true }); 224 | if (this.props.stripe) { 225 | this.props.stripe.createToken().then(result => { 226 | if (result.error) { 227 | this.setState({ error: result.error.message, loading: false }); 228 | } else { 229 | this.setState({ error: null }); 230 | const { 231 | selectedBillingAddress, 232 | selectedShippingAddress 233 | } = this.state; 234 | authAxios 235 | .post(checkoutURL, { 236 | stripeToken: result.token.id, 237 | selectedBillingAddress, 238 | selectedShippingAddress 239 | }) 240 | .then(res => { 241 | this.setState({ loading: false, success: true }); 242 | }) 243 | .catch(err => { 244 | this.setState({ loading: false, error: err }); 245 | }); 246 | } 247 | }); 248 | } else { 249 | console.log("Stripe is not loaded"); 250 | } 251 | }; 252 | 253 | render() { 254 | const { 255 | data, 256 | error, 257 | loading, 258 | success, 259 | billingAddresses, 260 | shippingAddresses, 261 | selectedBillingAddress, 262 | selectedShippingAddress 263 | } = this.state; 264 | 265 | return ( 266 |
267 | {error && ( 268 | 273 | )} 274 | {loading && ( 275 | 276 | 277 | Loading 278 | 279 | 280 | 281 | )} 282 | 283 | 284 | 285 | this.handleAddCoupon(e, code)} 287 | /> 288 | 289 |
Select a billing address
290 | {billingAddresses.length > 0 ? ( 291 | 314 | ) : ( 315 |

316 | You need to add a shipping address 317 |

318 | )} 319 | 320 | 321 | {billingAddresses.length < 1 || shippingAddresses.length < 1 ? ( 322 |

You need to add addresses before you can complete your purchase

323 | ) : ( 324 | 325 |
Would you like to complete the purchase?
326 | 327 | {success && ( 328 | 329 | Your payment was successful 330 |

331 | Go to your profile to see the order delivery status. 332 |

333 |
334 | )} 335 | 344 |
345 | )} 346 |
347 | ); 348 | } 349 | } 350 | 351 | const InjectedForm = withRouter(injectStripe(CheckoutForm)); 352 | 353 | const WrappedForm = () => ( 354 | 355 | 356 |
357 |

Complete your order

358 | 359 | 360 | 361 |
362 |
363 |
364 | ); 365 | 366 | export default WrappedForm; 367 | -------------------------------------------------------------------------------- /src/containers/Home.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React, { Component } from "react"; 3 | import { 4 | Button, 5 | Container, 6 | Divider, 7 | Grid, 8 | Header, 9 | Image, 10 | Responsive, 11 | Segment, 12 | Sidebar, 13 | Visibility 14 | } from "semantic-ui-react"; 15 | 16 | const getWidth = () => { 17 | const isSSR = typeof window === "undefined"; 18 | return isSSR ? Responsive.onlyTablet.minWidth : window.innerWidth; 19 | }; 20 | 21 | class DesktopContainer extends Component { 22 | state = {}; 23 | 24 | hideFixedMenu = () => this.setState({ fixed: false }); 25 | showFixedMenu = () => this.setState({ fixed: true }); 26 | 27 | render() { 28 | const { children } = this.props; 29 | 30 | return ( 31 | 32 | 37 | {children} 38 | 39 | ); 40 | } 41 | } 42 | 43 | DesktopContainer.propTypes = { 44 | children: PropTypes.node 45 | }; 46 | 47 | class MobileContainer extends Component { 48 | state = {}; 49 | 50 | handleSidebarHide = () => this.setState({ sidebarOpened: false }); 51 | 52 | handleToggle = () => this.setState({ sidebarOpened: true }); 53 | 54 | render() { 55 | const { children } = this.props; 56 | 57 | return ( 58 | 63 | {children} 64 | 65 | ); 66 | } 67 | } 68 | 69 | MobileContainer.propTypes = { 70 | children: PropTypes.node 71 | }; 72 | 73 | const ResponsiveContainer = ({ children }) => ( 74 |
75 | {children} 76 | {children} 77 |
78 | ); 79 | 80 | ResponsiveContainer.propTypes = { 81 | children: PropTypes.node 82 | }; 83 | 84 | const HomepageLayout = () => ( 85 | 86 | 87 | 88 | 89 | 90 |
91 | We Help Companies and Companions 92 |
93 |

94 | We can give your company superpowers to do things that they never 95 | thought possible. Let us delight your customers and empower your 96 | needs... through pure data analytics. 97 |

98 |
99 | We Make Bananas That Can Dance 100 |
101 |

102 | Yes that's right, you thought it was the stuff of dreams, but even 103 | bananas can be bioengineered. 104 |

105 |
106 | 107 | 113 | 114 |
115 | 116 | 117 | 118 | 119 | 120 |
121 |
122 | 123 | 124 | 125 | 126 |
127 | "What a Company" 128 |
129 |

130 | That is what they all say about us 131 |

132 |
133 | 134 |
135 | "I shouldn't have gone with their competitor." 136 |
137 |

138 | 139 | Nan Chief Fun Officer Acme Toys 140 |

141 |
142 |
143 |
144 |
145 | 146 | 147 |
148 | Breaking The Grid, Grabs Your Attention 149 |
150 |

151 | Instead of focusing on content creation and hard work, we have learned 152 | how to master the art of doing nothing by providing massive amounts of 153 | whitespace and generic content that can seem massive, monolithic and 154 | worth your attention. 155 |

156 | 159 | 165 | Case Studies 166 | 167 |
168 | Did We Tell You About Our Bananas? 169 |
170 |

171 | Yes I know you probably disregarded the earlier boasts as non-sequitur 172 | filler content, but it's really true. It took years of gene splicing 173 | and combinatory DNA research, but our bananas can really dance. 174 |

175 | 178 |
179 |
180 |
181 | ); 182 | export default HomepageLayout; 183 | -------------------------------------------------------------------------------- /src/containers/Layout.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Container, 4 | Divider, 5 | Dropdown, 6 | Grid, 7 | Header, 8 | Image, 9 | List, 10 | Menu, 11 | Segment 12 | } from "semantic-ui-react"; 13 | import { Link, withRouter } from "react-router-dom"; 14 | import { connect } from "react-redux"; 15 | import { logout } from "../store/actions/auth"; 16 | import { fetchCart } from "../store/actions/cart"; 17 | 18 | class CustomLayout extends React.Component { 19 | componentDidMount() { 20 | this.props.fetchCart(); 21 | } 22 | 23 | render() { 24 | const { authenticated, cart, loading } = this.props; 25 | return ( 26 |
27 | 28 | 29 | 30 | Home 31 | 32 | 33 | Products 34 | 35 | {authenticated ? ( 36 | 37 | 38 | 39 | Profile 40 | 41 | 48 | 49 | {cart !== null ? ( 50 | 51 | {cart.order_items.map(order_item => { 52 | return ( 53 | 54 | {order_item.quantity} x {order_item.item.title} 55 | 56 | ); 57 | })} 58 | {cart.order_items.length < 1 ? ( 59 | No items in your cart 60 | ) : null} 61 | 62 | 63 | 67 | this.props.history.push("/order-summary") 68 | } 69 | /> 70 | 71 | ) : ( 72 | No items in your cart 73 | )} 74 | 75 | 76 | this.props.logout()}> 77 | Logout 78 | 79 | 80 | 81 | ) : ( 82 | 83 | 84 | Login 85 | 86 | 87 | Signup 88 | 89 | 90 | )} 91 | 92 | 93 | 94 | {this.props.children} 95 | 96 | 101 | 102 | 103 | 104 |
105 | 106 | Link One 107 | Link Two 108 | Link Three 109 | Link Four 110 | 111 | 112 | 113 |
114 | 115 | Link One 116 | Link Two 117 | Link Three 118 | Link Four 119 | 120 | 121 | 122 |
123 | 124 | Link One 125 | Link Two 126 | Link Three 127 | Link Four 128 | 129 | 130 | 131 |
132 |

133 | Extra space for a call to action inside the footer that could 134 | help re-engage users. 135 |

136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | Site Map 144 | 145 | 146 | Contact Us 147 | 148 | 149 | Terms and Conditions 150 | 151 | 152 | Privacy Policy 153 | 154 | 155 | 156 | 157 |
158 | ); 159 | } 160 | } 161 | 162 | const mapStateToProps = state => { 163 | return { 164 | authenticated: state.auth.token !== null, 165 | cart: state.cart.shoppingCart, 166 | loading: state.cart.loading 167 | }; 168 | }; 169 | 170 | const mapDispatchToProps = dispatch => { 171 | return { 172 | logout: () => dispatch(logout()), 173 | fetchCart: () => dispatch(fetchCart()) 174 | }; 175 | }; 176 | 177 | export default withRouter( 178 | connect( 179 | mapStateToProps, 180 | mapDispatchToProps 181 | )(CustomLayout) 182 | ); 183 | -------------------------------------------------------------------------------- /src/containers/Login.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Button, 4 | Form, 5 | Grid, 6 | Header, 7 | Message, 8 | Segment 9 | } from "semantic-ui-react"; 10 | import { connect } from "react-redux"; 11 | import { NavLink, Redirect } from "react-router-dom"; 12 | import { authLogin } from "../store/actions/auth"; 13 | 14 | class LoginForm extends React.Component { 15 | state = { 16 | username: "", 17 | password: "" 18 | }; 19 | 20 | handleChange = e => { 21 | this.setState({ [e.target.name]: e.target.value }); 22 | }; 23 | 24 | handleSubmit = e => { 25 | e.preventDefault(); 26 | const { username, password } = this.state; 27 | this.props.login(username, password); 28 | }; 29 | 30 | render() { 31 | const { error, loading, token } = this.props; 32 | const { username, password } = this.state; 33 | if (token) { 34 | return ; 35 | } 36 | return ( 37 | 42 | 43 |
44 | Log-in to your account 45 |
46 | {error &&

{this.props.error.message}

} 47 | 48 | 49 |
50 | 51 | 60 | 70 | 71 | 80 | 81 |
82 | 83 | New to us? Sign Up 84 | 85 |
86 |
87 |
88 | ); 89 | } 90 | } 91 | 92 | const mapStateToProps = state => { 93 | return { 94 | loading: state.auth.loading, 95 | error: state.auth.error, 96 | token: state.auth.token 97 | }; 98 | }; 99 | 100 | const mapDispatchToProps = dispatch => { 101 | return { 102 | login: (username, password) => dispatch(authLogin(username, password)) 103 | }; 104 | }; 105 | 106 | export default connect( 107 | mapStateToProps, 108 | mapDispatchToProps 109 | )(LoginForm); 110 | -------------------------------------------------------------------------------- /src/containers/OrderSummary.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Container, 4 | Dimmer, 5 | Header, 6 | Icon, 7 | Image, 8 | Label, 9 | Loader, 10 | Table, 11 | Button, 12 | Message, 13 | Segment 14 | } from "semantic-ui-react"; 15 | import { connect } from "react-redux"; 16 | import { Link, Redirect } from "react-router-dom"; 17 | import { authAxios } from "../utils"; 18 | import { 19 | addToCartURL, 20 | orderSummaryURL, 21 | orderItemDeleteURL, 22 | orderItemUpdateQuantityURL 23 | } from "../constants"; 24 | 25 | class OrderSummary extends React.Component { 26 | state = { 27 | data: null, 28 | error: null, 29 | loading: false 30 | }; 31 | 32 | componentDidMount() { 33 | this.handleFetchOrder(); 34 | } 35 | 36 | handleFetchOrder = () => { 37 | this.setState({ loading: true }); 38 | authAxios 39 | .get(orderSummaryURL) 40 | .then(res => { 41 | this.setState({ data: res.data, loading: false }); 42 | }) 43 | .catch(err => { 44 | if (err.response.status === 404) { 45 | this.setState({ 46 | error: "You currently do not have an order", 47 | loading: false 48 | }); 49 | } else { 50 | this.setState({ error: err, loading: false }); 51 | } 52 | }); 53 | }; 54 | 55 | renderVariations = orderItem => { 56 | let text = ""; 57 | orderItem.item_variations.forEach(iv => { 58 | text += `${iv.variation.name}: ${iv.value}, `; 59 | }); 60 | return text; 61 | }; 62 | 63 | handleFormatData = itemVariations => { 64 | // convert [{id: 1},{id: 2}] to [1,2] - they're all variations 65 | return Object.keys(itemVariations).map(key => { 66 | return itemVariations[key].id; 67 | }); 68 | }; 69 | 70 | handleAddToCart = (slug, itemVariations) => { 71 | this.setState({ loading: true }); 72 | const variations = this.handleFormatData(itemVariations); 73 | authAxios 74 | .post(addToCartURL, { slug, variations }) 75 | .then(res => { 76 | this.handleFetchOrder(); 77 | this.setState({ loading: false }); 78 | }) 79 | .catch(err => { 80 | this.setState({ error: err, loading: false }); 81 | }); 82 | }; 83 | 84 | handleRemoveQuantityFromCart = slug => { 85 | authAxios 86 | .post(orderItemUpdateQuantityURL, { slug }) 87 | .then(res => { 88 | this.handleFetchOrder(); 89 | }) 90 | .catch(err => { 91 | this.setState({ error: err }); 92 | }); 93 | }; 94 | 95 | handleRemoveItem = itemID => { 96 | authAxios 97 | .delete(orderItemDeleteURL(itemID)) 98 | .then(res => { 99 | this.handleFetchOrder(); 100 | }) 101 | .catch(err => { 102 | this.setState({ error: err }); 103 | }); 104 | }; 105 | 106 | render() { 107 | const { data, error, loading } = this.state; 108 | const { isAuthenticated } = this.props; 109 | if (!isAuthenticated) { 110 | return ; 111 | } 112 | console.log(data); 113 | 114 | return ( 115 | 116 |
Order Summary
117 | {error && ( 118 | 123 | )} 124 | {loading && ( 125 | 126 | 127 | Loading 128 | 129 | 130 | 131 | 132 | )} 133 | {data && ( 134 | 135 | 136 | 137 | Item # 138 | Item name 139 | Item price 140 | Item quantity 141 | Total item price 142 | 143 | 144 | 145 | 146 | {data.order_items.map((orderItem, i) => { 147 | return ( 148 | 149 | {i + 1} 150 | 151 | {orderItem.item.title} -{" "} 152 | {this.renderVariations(orderItem)} 153 | 154 | ${orderItem.item.price} 155 | 156 | 160 | this.handleRemoveQuantityFromCart(orderItem.item.slug) 161 | } 162 | /> 163 | {orderItem.quantity} 164 | 168 | this.handleAddToCart( 169 | orderItem.item.slug, 170 | orderItem.item_variations 171 | ) 172 | } 173 | /> 174 | 175 | 176 | {orderItem.item.discount_price && ( 177 | 180 | )} 181 | ${orderItem.final_price} 182 | this.handleRemoveItem(orderItem.id)} 187 | /> 188 | 189 | 190 | ); 191 | })} 192 | 193 | 194 | 195 | 196 | 197 | Order Total: ${data.total} 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 209 | 210 | 211 | 212 | 213 |
214 | )} 215 |
216 | ); 217 | } 218 | } 219 | 220 | const mapStateToProps = state => { 221 | return { 222 | isAuthenticated: state.auth.token !== null 223 | }; 224 | }; 225 | 226 | export default connect(mapStateToProps)(OrderSummary); 227 | -------------------------------------------------------------------------------- /src/containers/ProductDetail.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import axios from "axios"; 5 | import { 6 | Button, 7 | Card, 8 | Container, 9 | Dimmer, 10 | Form, 11 | Grid, 12 | Header, 13 | Icon, 14 | Image, 15 | Item, 16 | Label, 17 | Loader, 18 | Message, 19 | Segment, 20 | Select, 21 | Divider 22 | } from "semantic-ui-react"; 23 | import { productDetailURL, addToCartURL } from "../constants"; 24 | import { fetchCart } from "../store/actions/cart"; 25 | import { authAxios } from "../utils"; 26 | 27 | class ProductDetail extends React.Component { 28 | state = { 29 | loading: false, 30 | error: null, 31 | formVisible: false, 32 | data: [], 33 | formData: {} 34 | }; 35 | 36 | componentDidMount() { 37 | this.handleFetchItem(); 38 | } 39 | 40 | handleToggleForm = () => { 41 | const { formVisible } = this.state; 42 | this.setState({ 43 | formVisible: !formVisible 44 | }); 45 | }; 46 | 47 | handleFetchItem = () => { 48 | const { 49 | match: { params } 50 | } = this.props; 51 | this.setState({ loading: true }); 52 | axios 53 | .get(productDetailURL(params.productID)) 54 | .then(res => { 55 | this.setState({ data: res.data, loading: false }); 56 | }) 57 | .catch(err => { 58 | this.setState({ error: err, loading: false }); 59 | }); 60 | }; 61 | 62 | handleFormatData = formData => { 63 | // convert {colour: 1, size: 2} to [1,2] - they're all variations 64 | return Object.keys(formData).map(key => { 65 | return formData[key]; 66 | }); 67 | }; 68 | 69 | handleAddToCart = slug => { 70 | this.setState({ loading: true }); 71 | const { formData } = this.state; 72 | const variations = this.handleFormatData(formData); 73 | authAxios 74 | .post(addToCartURL, { slug, variations }) 75 | .then(res => { 76 | this.props.refreshCart(); 77 | this.setState({ loading: false }); 78 | }) 79 | .catch(err => { 80 | this.setState({ error: err, loading: false }); 81 | }); 82 | }; 83 | 84 | handleChange = (e, { name, value }) => { 85 | const { formData } = this.state; 86 | const updatedFormData = { 87 | ...formData, 88 | [name]: value 89 | }; 90 | this.setState({ formData: updatedFormData }); 91 | }; 92 | 93 | render() { 94 | const { data, error, formData, formVisible, loading } = this.state; 95 | const item = data; 96 | return ( 97 | 98 | {error && ( 99 | 104 | )} 105 | {loading && ( 106 | 107 | 108 | Loading 109 | 110 | 111 | 112 | )} 113 | 114 | 115 | 116 | 122 | {item.category} 123 | {item.discount_price && ( 124 | 135 | )} 136 | 137 | } 138 | description={item.description} 139 | extra={ 140 | 141 | 152 | 153 | } 154 | /> 155 | {formVisible && ( 156 | 157 | 158 |
this.handleAddToCart(item.slug)}> 159 | {data.variations.map(v => { 160 | const name = v.name.toLowerCase(); 161 | return ( 162 | 163 | 230 | 231 | 238 | 244 | {success && ( 245 | 246 | )} 247 | {error && ( 248 | 253 | )} 254 | 255 | Save 256 | 257 | 258 | ); 259 | } 260 | } 261 | 262 | class Profile extends React.Component { 263 | state = { 264 | activeItem: "billingAddress", 265 | addresses: [], 266 | countries: [], 267 | userID: null, 268 | selectedAddress: null 269 | }; 270 | 271 | componentDidMount() { 272 | this.handleFetchAddresses(); 273 | this.handleFetchCountries(); 274 | this.handleFetchUserID(); 275 | } 276 | 277 | handleItemClick = name => { 278 | this.setState({ activeItem: name }, () => { 279 | this.handleFetchAddresses(); 280 | }); 281 | }; 282 | 283 | handleGetActiveItem = () => { 284 | const { activeItem } = this.state; 285 | if (activeItem === "billingAddress") { 286 | return "Billing Address"; 287 | } else if (activeItem === "shippingAddress") { 288 | return "Shipping Address"; 289 | } 290 | return "Payment History"; 291 | }; 292 | 293 | handleFormatCountries = countries => { 294 | const keys = Object.keys(countries); 295 | return keys.map(k => { 296 | return { 297 | key: k, 298 | text: countries[k], 299 | value: k 300 | }; 301 | }); 302 | }; 303 | 304 | handleDeleteAddress = addressID => { 305 | authAxios 306 | .delete(addressDeleteURL(addressID)) 307 | .then(res => { 308 | this.handleCallback(); 309 | }) 310 | .catch(err => { 311 | this.setState({ error: err }); 312 | }); 313 | }; 314 | 315 | handleSelectAddress = address => { 316 | this.setState({ selectedAddress: address }); 317 | }; 318 | 319 | handleFetchUserID = () => { 320 | authAxios 321 | .get(userIDURL) 322 | .then(res => { 323 | this.setState({ userID: res.data.userID }); 324 | }) 325 | .catch(err => { 326 | this.setState({ error: err }); 327 | }); 328 | }; 329 | 330 | handleFetchCountries = () => { 331 | authAxios 332 | .get(countryListURL) 333 | .then(res => { 334 | this.setState({ countries: this.handleFormatCountries(res.data) }); 335 | }) 336 | .catch(err => { 337 | this.setState({ error: err }); 338 | }); 339 | }; 340 | 341 | handleFetchAddresses = () => { 342 | this.setState({ loading: true }); 343 | const { activeItem } = this.state; 344 | authAxios 345 | .get(addressListURL(activeItem === "billingAddress" ? "B" : "S")) 346 | .then(res => { 347 | this.setState({ addresses: res.data, loading: false }); 348 | }) 349 | .catch(err => { 350 | this.setState({ error: err }); 351 | }); 352 | }; 353 | 354 | handleCallback = () => { 355 | this.handleFetchAddresses(); 356 | this.setState({ selectedAddress: null }); 357 | }; 358 | 359 | renderAddresses = () => { 360 | const { 361 | activeItem, 362 | addresses, 363 | countries, 364 | selectedAddress, 365 | userID 366 | } = this.state; 367 | return ( 368 | 369 | 370 | {addresses.map(a => { 371 | return ( 372 | 373 | 374 | {a.default && ( 375 | 378 | )} 379 | 380 | {a.street_address}, {a.apartment_address} 381 | 382 | {a.country} 383 | {a.zip} 384 | 385 | 386 | 392 | 398 | 399 | 400 | ); 401 | })} 402 | 403 | {addresses.length > 0 ? : null} 404 | {selectedAddress === null ? ( 405 | 412 | ) : null} 413 | {selectedAddress && ( 414 | 422 | )} 423 | 424 | ); 425 | }; 426 | 427 | render() { 428 | const { activeItem, error, loading } = this.state; 429 | const { isAuthenticated } = this.props; 430 | if (!isAuthenticated) { 431 | return ; 432 | } 433 | return ( 434 | 435 | 436 | 437 | {error && ( 438 | 443 | )} 444 | {loading && ( 445 | 446 | 447 | Loading 448 | 449 | 450 | 451 | )} 452 | 453 | 454 | 455 | 456 | 457 | this.handleItemClick("billingAddress")} 461 | /> 462 | this.handleItemClick("shippingAddress")} 466 | /> 467 | this.handleItemClick("paymentHistory")} 471 | /> 472 | 473 | 474 | 475 |
{this.handleGetActiveItem()}
476 | 477 | {activeItem === "paymentHistory" ? ( 478 | 479 | ) : ( 480 | this.renderAddresses() 481 | )} 482 |
483 |
484 |
485 | ); 486 | } 487 | } 488 | 489 | const mapStateToProps = state => { 490 | return { 491 | isAuthenticated: state.auth.token !== null 492 | }; 493 | }; 494 | 495 | export default connect(mapStateToProps)(Profile); 496 | -------------------------------------------------------------------------------- /src/containers/Signup.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Button, 4 | Form, 5 | Grid, 6 | Header, 7 | Message, 8 | Segment 9 | } from "semantic-ui-react"; 10 | import { connect } from "react-redux"; 11 | import { NavLink, Redirect } from "react-router-dom"; 12 | import { authSignup } from "../store/actions/auth"; 13 | 14 | class RegistrationForm extends React.Component { 15 | state = { 16 | username: "", 17 | email: "", 18 | password1: "", 19 | password2: "" 20 | }; 21 | 22 | handleSubmit = e => { 23 | e.preventDefault(); 24 | const { username, email, password1, password2 } = this.state; 25 | this.props.signup(username, email, password1, password2); 26 | }; 27 | 28 | handleChange = e => { 29 | this.setState({ [e.target.name]: e.target.value }); 30 | }; 31 | 32 | render() { 33 | const { username, email, password1, password2 } = this.state; 34 | const { error, loading, token } = this.props; 35 | if (token) { 36 | return ; 37 | } 38 | return ( 39 | 44 | 45 |
46 | Signup to your account 47 |
48 | {error &&

{this.props.error.message}

} 49 | 50 | 51 |
52 | 53 | 62 | 71 | 81 | 91 | 92 | 101 | 102 |
103 | 104 | Already have an account? Login 105 | 106 |
107 |
108 |
109 | ); 110 | } 111 | } 112 | 113 | const mapStateToProps = state => { 114 | return { 115 | loading: state.auth.loading, 116 | error: state.auth.error, 117 | token: state.auth.token 118 | }; 119 | }; 120 | 121 | const mapDispatchToProps = dispatch => { 122 | return { 123 | signup: (username, email, password1, password2) => 124 | dispatch(authSignup(username, email, password1, password2)) 125 | }; 126 | }; 127 | 128 | export default connect( 129 | mapStateToProps, 130 | mapDispatchToProps 131 | )(RegistrationForm); 132 | -------------------------------------------------------------------------------- /src/hoc/hoc.js: -------------------------------------------------------------------------------- 1 | const Hoc = props => props.children; 2 | 3 | export default Hoc; 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import registerServiceWorker from "./registerServiceWorker"; 5 | import { createStore, compose, applyMiddleware, combineReducers } from "redux"; 6 | import { Provider } from "react-redux"; 7 | import thunk from "redux-thunk"; 8 | 9 | import authReducer from "./store/reducers/auth"; 10 | import cartReducer from "./store/reducers/cart"; 11 | 12 | const composeEnhances = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 13 | 14 | const rootReducer = combineReducers({ 15 | auth: authReducer, 16 | cart: cartReducer 17 | }); 18 | 19 | const store = createStore(rootReducer, composeEnhances(applyMiddleware(thunk))); 20 | 21 | const app = ( 22 | 23 | 24 | 25 | ); 26 | 27 | ReactDOM.render(app, document.getElementById("root")); 28 | registerServiceWorker(); 29 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === "localhost" || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === "[::1]" || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener("load", () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | "This web app is being served cache-first by a service " + 44 | "worker. To learn more, visit https://goo.gl/SC7cgQ" 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === "installed") { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log("New content is available; please refresh."); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log("Content is cached for offline use."); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error("Error during service worker registration:", error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get("content-type").indexOf("javascript") === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | "No internet connection found. App is running in offline mode." 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ("serviceWorker" in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route } from "react-router-dom"; 3 | import Hoc from "./hoc/hoc"; 4 | 5 | import Login from "./containers/Login"; 6 | import Signup from "./containers/Signup"; 7 | import HomepageLayout from "./containers/Home"; 8 | import ProductList from "./containers/ProductList"; 9 | import ProductDetail from "./containers/ProductDetail"; 10 | import OrderSummary from "./containers/OrderSummary"; 11 | import Checkout from "./containers/Checkout"; 12 | import Profile from "./containers/Profile"; 13 | 14 | const BaseRouter = () => ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | 27 | export default BaseRouter; 28 | -------------------------------------------------------------------------------- /src/store/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const AUTH_START = "AUTH_START"; 2 | export const AUTH_SUCCESS = "AUTH_SUCCESS"; 3 | export const AUTH_FAIL = "AUTH_FAIL"; 4 | export const AUTH_LOGOUT = "AUTH_LOGOUT"; 5 | 6 | export const CART_START = "CART_START"; 7 | export const CART_SUCCESS = "CART_SUCCESS"; 8 | export const CART_FAIL = "CART_FAIL"; 9 | -------------------------------------------------------------------------------- /src/store/actions/auth.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import * as actionTypes from "./actionTypes"; 3 | 4 | export const authStart = () => { 5 | return { 6 | type: actionTypes.AUTH_START 7 | }; 8 | }; 9 | 10 | export const authSuccess = token => { 11 | return { 12 | type: actionTypes.AUTH_SUCCESS, 13 | token: token 14 | }; 15 | }; 16 | 17 | export const authFail = error => { 18 | return { 19 | type: actionTypes.AUTH_FAIL, 20 | error: error 21 | }; 22 | }; 23 | 24 | export const logout = () => { 25 | localStorage.removeItem("token"); 26 | localStorage.removeItem("expirationDate"); 27 | return { 28 | type: actionTypes.AUTH_LOGOUT 29 | }; 30 | }; 31 | 32 | export const checkAuthTimeout = expirationTime => { 33 | return dispatch => { 34 | setTimeout(() => { 35 | dispatch(logout()); 36 | }, expirationTime * 1000); 37 | }; 38 | }; 39 | 40 | export const authLogin = (username, password) => { 41 | return dispatch => { 42 | dispatch(authStart()); 43 | axios 44 | .post("http://127.0.0.1:8000/rest-auth/login/", { 45 | username: username, 46 | password: password 47 | }) 48 | .then(res => { 49 | const token = res.data.key; 50 | const expirationDate = new Date(new Date().getTime() + 3600 * 1000); 51 | localStorage.setItem("token", token); 52 | localStorage.setItem("expirationDate", expirationDate); 53 | dispatch(authSuccess(token)); 54 | dispatch(checkAuthTimeout(3600)); 55 | }) 56 | .catch(err => { 57 | dispatch(authFail(err)); 58 | }); 59 | }; 60 | }; 61 | 62 | export const authSignup = (username, email, password1, password2) => { 63 | return dispatch => { 64 | dispatch(authStart()); 65 | axios 66 | .post("http://127.0.0.1:8000/rest-auth/registration/", { 67 | username: username, 68 | email: email, 69 | password1: password1, 70 | password2: password2 71 | }) 72 | .then(res => { 73 | const token = res.data.key; 74 | const expirationDate = new Date(new Date().getTime() + 3600 * 1000); 75 | localStorage.setItem("token", token); 76 | localStorage.setItem("expirationDate", expirationDate); 77 | dispatch(authSuccess(token)); 78 | dispatch(checkAuthTimeout(3600)); 79 | }) 80 | .catch(err => { 81 | dispatch(authFail(err)); 82 | }); 83 | }; 84 | }; 85 | 86 | export const authCheckState = () => { 87 | return dispatch => { 88 | const token = localStorage.getItem("token"); 89 | if (token === undefined) { 90 | dispatch(logout()); 91 | } else { 92 | const expirationDate = new Date(localStorage.getItem("expirationDate")); 93 | if (expirationDate <= new Date()) { 94 | dispatch(logout()); 95 | } else { 96 | dispatch(authSuccess(token)); 97 | dispatch( 98 | checkAuthTimeout( 99 | (expirationDate.getTime() - new Date().getTime()) / 1000 100 | ) 101 | ); 102 | } 103 | } 104 | }; 105 | }; 106 | -------------------------------------------------------------------------------- /src/store/actions/cart.js: -------------------------------------------------------------------------------- 1 | import { CART_START, CART_SUCCESS, CART_FAIL } from "./actionTypes"; 2 | import { authAxios } from "../../utils"; 3 | import { orderSummaryURL } from "../../constants"; 4 | 5 | export const cartStart = () => { 6 | return { 7 | type: CART_START 8 | }; 9 | }; 10 | 11 | export const cartSuccess = data => { 12 | return { 13 | type: CART_SUCCESS, 14 | data 15 | }; 16 | }; 17 | 18 | export const cartFail = error => { 19 | return { 20 | type: CART_FAIL, 21 | error: error 22 | }; 23 | }; 24 | 25 | export const fetchCart = () => { 26 | return dispatch => { 27 | dispatch(cartStart()); 28 | authAxios 29 | .get(orderSummaryURL) 30 | .then(res => { 31 | dispatch(cartSuccess(res.data)); 32 | }) 33 | .catch(err => { 34 | dispatch(cartFail(err)); 35 | }); 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/store/reducers/auth.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from "../actions/actionTypes"; 2 | import { updateObject } from "../utility"; 3 | 4 | const initialState = { 5 | token: null, 6 | error: null, 7 | loading: false 8 | }; 9 | 10 | const authStart = (state, action) => { 11 | return updateObject(state, { 12 | error: null, 13 | loading: true 14 | }); 15 | }; 16 | 17 | const authSuccess = (state, action) => { 18 | return updateObject(state, { 19 | token: action.token, 20 | error: null, 21 | loading: false 22 | }); 23 | }; 24 | 25 | const authFail = (state, action) => { 26 | return updateObject(state, { 27 | error: action.error, 28 | loading: false 29 | }); 30 | }; 31 | 32 | const authLogout = (state, action) => { 33 | return updateObject(state, { 34 | token: null 35 | }); 36 | }; 37 | 38 | const reducer = (state = initialState, action) => { 39 | switch (action.type) { 40 | case actionTypes.AUTH_START: 41 | return authStart(state, action); 42 | case actionTypes.AUTH_SUCCESS: 43 | return authSuccess(state, action); 44 | case actionTypes.AUTH_FAIL: 45 | return authFail(state, action); 46 | case actionTypes.AUTH_LOGOUT: 47 | return authLogout(state, action); 48 | default: 49 | return state; 50 | } 51 | }; 52 | 53 | export default reducer; 54 | -------------------------------------------------------------------------------- /src/store/reducers/cart.js: -------------------------------------------------------------------------------- 1 | import { CART_START, CART_SUCCESS, CART_FAIL } from "../actions/actionTypes"; 2 | import { updateObject } from "../utility"; 3 | 4 | const initialState = { 5 | shoppingCart: null, 6 | error: null, 7 | loading: false 8 | }; 9 | 10 | const cartStart = (state, action) => { 11 | return updateObject(state, { 12 | error: null, 13 | loading: true 14 | }); 15 | }; 16 | 17 | const cartSuccess = (state, action) => { 18 | return updateObject(state, { 19 | shoppingCart: action.data, 20 | error: null, 21 | loading: false 22 | }); 23 | }; 24 | 25 | const cartFail = (state, action) => { 26 | return updateObject(state, { 27 | error: action.error, 28 | loading: false 29 | }); 30 | }; 31 | 32 | const reducer = (state = initialState, action) => { 33 | switch (action.type) { 34 | case CART_START: 35 | return cartStart(state, action); 36 | case CART_SUCCESS: 37 | return cartSuccess(state, action); 38 | case CART_FAIL: 39 | return cartFail(state, action); 40 | default: 41 | return state; 42 | } 43 | }; 44 | 45 | export default reducer; 46 | -------------------------------------------------------------------------------- /src/store/utility.js: -------------------------------------------------------------------------------- 1 | export const updateObject = (oldObject, updatedProperties) => { 2 | return { 3 | ...oldObject, 4 | ...updatedProperties 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { endpoint } from "./constants"; 3 | 4 | export const authAxios = axios.create({ 5 | baseURL: endpoint, 6 | headers: { 7 | Authorization: `Token ${localStorage.getItem("token")}` 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NinjaDevOps0831/django-react-ecommerce/80e2a609299ca22443e3685eca6510254b1969b1/thumbnail.png --------------------------------------------------------------------------------