├── .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 |
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 |
51 |
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 |
14 | You need to enable JavaScript to run this app.
15 |
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 | ${orderItem.final_price}
52 |
53 |
54 |
55 | );
56 | })}
57 |
58 |
59 |
60 | -
61 |
62 |
63 | Order Total: ${data.total}
64 | {data.coupon && (
65 |
66 | Current coupon: {data.coupon.code} for $
67 | {data.coupon.amount}
68 |
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 |
103 | Coupon code
104 |
109 |
110 | Submit
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 |
290 | {billingAddresses.length > 0 ? (
291 |
299 | ) : (
300 |
301 | You need to add a billing address
302 |
303 | )}
304 | Select a shipping address
305 | {shippingAddresses.length > 0 ? (
306 |
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 |
342 | Submit
343 |
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 | Check Them Out
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 |
157 | Read More
158 |
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 |
176 | I'm Still Quite Interested
177 |
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 |
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 |
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 |
178 | ON DISCOUNT
179 |
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 |
207 | Checkout
208 |
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 |
133 | {item.label}
134 |
135 | )}
136 |
137 | }
138 | description={item.description}
139 | extra={
140 |
141 |
149 | Add to cart
150 |
151 |
152 |
153 | }
154 | />
155 | {formVisible && (
156 |
157 |
158 |
163 | {
170 | return {
171 | key: item.id,
172 | text: item.value,
173 | value: item.id
174 | };
175 | })}
176 | value={formData[name]}
177 | />
178 |
179 | );
180 | })}
181 | Add
182 |
183 |
184 | )}
185 |
186 |
187 |
188 | {data.variations &&
189 | data.variations.map(v => {
190 | return (
191 |
192 |
193 |
194 | {v.item_variations.map(iv => {
195 | return (
196 | -
197 | {iv.attachment && (
198 |
202 | )}
203 |
204 | {iv.value}
205 |
206 |
207 | );
208 | })}
209 |
210 |
211 | );
212 | })}
213 |
214 |
215 |
216 |
217 | );
218 | }
219 | }
220 |
221 | const mapDispatchToProps = dispatch => {
222 | return {
223 | refreshCart: () => dispatch(fetchCart())
224 | };
225 | };
226 |
227 | export default withRouter(
228 | connect(
229 | null,
230 | mapDispatchToProps
231 | )(ProductDetail)
232 | );
233 |
--------------------------------------------------------------------------------
/src/containers/ProductList.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import axios from "axios";
4 | import {
5 | Container,
6 | Dimmer,
7 | Image,
8 | Item,
9 | Label,
10 | Loader,
11 | Message,
12 | Segment
13 | } from "semantic-ui-react";
14 | import { productListURL, addToCartURL } from "../constants";
15 | import { fetchCart } from "../store/actions/cart";
16 | import { authAxios } from "../utils";
17 |
18 | class ProductList extends React.Component {
19 | state = {
20 | loading: false,
21 | error: null,
22 | data: []
23 | };
24 |
25 | componentDidMount() {
26 | this.setState({ loading: true });
27 | axios
28 | .get(productListURL)
29 | .then(res => {
30 | this.setState({ data: res.data, loading: false });
31 | })
32 | .catch(err => {
33 | this.setState({ error: err, loading: false });
34 | });
35 | }
36 |
37 | handleAddToCart = slug => {
38 | this.setState({ loading: true });
39 | authAxios
40 | .post(addToCartURL, { slug })
41 | .then(res => {
42 | this.props.refreshCart();
43 | this.setState({ loading: false });
44 | })
45 | .catch(err => {
46 | this.setState({ error: err, loading: false });
47 | });
48 | };
49 |
50 | render() {
51 | const { data, error, loading } = this.state;
52 | return (
53 |
54 | {error && (
55 |
60 | )}
61 | {loading && (
62 |
63 |
64 | Loading
65 |
66 |
67 |
68 |
69 | )}
70 |
71 | {data.map(item => {
72 | return (
73 | -
74 |
75 |
76 |
79 | this.props.history.push(`/products/${item.id}`)
80 | }
81 | >
82 | {item.title}
83 |
84 |
85 | {item.category}
86 |
87 | {item.description}
88 |
89 | {/* this.handleAddToCart(item.slug)}
95 | >
96 | Add to cart
97 |
98 | */}
99 | {item.discount_price && (
100 |
109 | {item.label}
110 |
111 | )}
112 |
113 |
114 |
115 | );
116 | })}
117 |
118 |
119 | );
120 | }
121 | }
122 |
123 | const mapDispatchToProps = dispatch => {
124 | return {
125 | refreshCart: () => dispatch(fetchCart())
126 | };
127 | };
128 |
129 | export default connect(
130 | null,
131 | mapDispatchToProps
132 | )(ProductList);
133 |
--------------------------------------------------------------------------------
/src/containers/Profile.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { Redirect } from "react-router-dom";
4 | import {
5 | Button,
6 | Card,
7 | Dimmer,
8 | Divider,
9 | Form,
10 | Grid,
11 | Header,
12 | Image,
13 | Label,
14 | Loader,
15 | Menu,
16 | Message,
17 | Segment,
18 | Select,
19 | Table
20 | } from "semantic-ui-react";
21 | import {
22 | countryListURL,
23 | addressListURL,
24 | addressCreateURL,
25 | addressUpdateURL,
26 | addressDeleteURL,
27 | userIDURL,
28 | paymentListURL
29 | } from "../constants";
30 | import { authAxios } from "../utils";
31 |
32 | const UPDATE_FORM = "UPDATE_FORM";
33 | const CREATE_FORM = "CREATE_FORM";
34 |
35 | class PaymentHistory extends React.Component {
36 | state = {
37 | payments: []
38 | };
39 |
40 | componentDidMount() {
41 | this.handleFetchPayments();
42 | }
43 |
44 | handleFetchPayments = () => {
45 | this.setState({ loading: true });
46 | authAxios
47 | .get(paymentListURL)
48 | .then(res => {
49 | this.setState({
50 | loading: false,
51 | payments: res.data
52 | });
53 | })
54 | .catch(err => {
55 | this.setState({ error: err, loading: false });
56 | });
57 | };
58 |
59 | render() {
60 | const { payments } = this.state;
61 | return (
62 |
63 |
64 |
65 | ID
66 | Amount
67 | Date
68 |
69 |
70 |
71 | {payments.map(p => {
72 | return (
73 |
74 | {p.id}
75 | ${p.amount}
76 | {new Date(p.timestamp).toUTCString()}
77 |
78 | );
79 | })}
80 |
81 |
82 | );
83 | }
84 | }
85 |
86 | class AddressForm extends React.Component {
87 | state = {
88 | error: null,
89 | loading: false,
90 | formData: {
91 | address_type: "",
92 | apartment_address: "",
93 | country: "",
94 | default: false,
95 | id: "",
96 | street_address: "",
97 | user: 1,
98 | zip: ""
99 | },
100 | saving: false,
101 | success: false
102 | };
103 |
104 | componentDidMount() {
105 | const { address, formType } = this.props;
106 | if (formType === UPDATE_FORM) {
107 | this.setState({ formData: address });
108 | }
109 | }
110 |
111 | handleToggleDefault = () => {
112 | const { formData } = this.state;
113 | const updatedFormdata = {
114 | ...formData,
115 | default: !formData.default
116 | };
117 | this.setState({
118 | formData: updatedFormdata
119 | });
120 | };
121 |
122 | handleChange = e => {
123 | const { formData } = this.state;
124 | const updatedFormdata = {
125 | ...formData,
126 | [e.target.name]: e.target.value
127 | };
128 | this.setState({
129 | formData: updatedFormdata
130 | });
131 | };
132 |
133 | handleSelectChange = (e, { name, value }) => {
134 | const { formData } = this.state;
135 | const updatedFormdata = {
136 | ...formData,
137 | [name]: value
138 | };
139 | this.setState({
140 | formData: updatedFormdata
141 | });
142 | };
143 |
144 | handleSubmit = e => {
145 | this.setState({ saving: true });
146 | e.preventDefault();
147 | const { formType } = this.props;
148 | if (formType === UPDATE_FORM) {
149 | this.handleUpdateAddress();
150 | } else {
151 | this.handleCreateAddress();
152 | }
153 | };
154 |
155 | handleCreateAddress = () => {
156 | const { userID, activeItem } = this.props;
157 | const { formData } = this.state;
158 | authAxios
159 | .post(addressCreateURL, {
160 | ...formData,
161 | user: userID,
162 | address_type: activeItem === "billingAddress" ? "B" : "S"
163 | })
164 | .then(res => {
165 | this.setState({
166 | saving: false,
167 | success: true,
168 | formData: { default: false }
169 | });
170 | this.props.callback();
171 | })
172 | .catch(err => {
173 | this.setState({ error: err });
174 | });
175 | };
176 |
177 | handleUpdateAddress = () => {
178 | const { userID, activeItem } = this.props;
179 | const { formData } = this.state;
180 | authAxios
181 | .put(addressUpdateURL(formData.id), {
182 | ...formData,
183 | user: userID,
184 | address_type: activeItem === "billingAddress" ? "B" : "S"
185 | })
186 | .then(res => {
187 | this.setState({
188 | saving: false,
189 | success: true,
190 | formData: { default: false }
191 | });
192 | this.props.callback();
193 | })
194 | .catch(err => {
195 | this.setState({ error: err });
196 | });
197 | };
198 |
199 | render() {
200 | const { countries } = this.props;
201 | const { error, formData, success, saving } = this.state;
202 | return (
203 |
211 |
218 |
219 |
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 |
376 | Default
377 |
378 | )}
379 |
380 | {a.street_address}, {a.apartment_address}
381 |
382 | {a.country}
383 | {a.zip}
384 |
385 |
386 | this.handleSelectAddress(a)}
389 | >
390 | Update
391 |
392 | this.handleDeleteAddress(a.id)}
395 | >
396 | Delete
397 |
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 |
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
--------------------------------------------------------------------------------