├── .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 │ └── __init__.py ├── models.py ├── templatetags │ └── cart_template_tags.py ├── tests.py ├── urls.py └── views.py ├── home ├── __init__.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── dev.py │ └── prod.py ├── urls.py └── wsgi │ ├── dev.py │ └── prod.py ├── manage.py ├── media ├── add2.jpg └── add4.webp ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── requirements.txt ├── src ├── App.js ├── App.test.js ├── constants.js ├── containers │ ├── Home.js │ ├── Layout.js │ ├── Login.js │ ├── ProductList.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 /.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | **/*.pyc 3 | **/__pycache__ 4 | db.sqlite3 5 | node_modules 6 | build 7 | static 8 | package-lock.json -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "C:\\Users\\Dell\\envs\\env\\Scripts\\python.exe", 3 | "editor.formatOnSave": true, 4 | "python.linting.pycodestyleEnabled": 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.pycodestyleArgs": ["--ignore=E501"], 10 | "files.exclude": { 11 | "**/*.pyc": true 12 | } 13 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django React Ecommerce 2 | 3 | ## Backend development workflow 4 | 5 | `virtualenv env` 6 | 7 | # For Mac/ Linux 8 | 9 | `source env/bin/activate` 10 | 11 | # For Window 12 | 13 | `env\scripts\activate` 14 | 15 | `pip install -r requirements.txt` 16 | 17 | `python manage.py makemigrations` 18 | 19 | `python manage.py migrate` 20 | 21 | `python manage.py createsuperuser` for test login 22 | 23 | `python manage.py runserver` 24 | 25 | ## Frontend development workflow 26 | 27 | ```json 28 | npm i 29 | npm run start 30 | ``` 31 | 32 | ## For deploying 33 | 34 | ```json 35 | npm run build 36 | ``` 37 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zinmyoswe/React-and-Django-Ecommerce/1cb1f04a421d17ce85f1ab77fa3b88d35f0d613c/core/__init__.py -------------------------------------------------------------------------------- /core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Item, OrderItem, Order, Payment, Coupon, Refund, Address, UserProfile 4 | 5 | 6 | def make_refund_accepted(modeladmin, request, queryset): 7 | queryset.update(refund_requested=False, refund_granted=True) 8 | 9 | 10 | make_refund_accepted.short_description = 'Update orders to refund granted' 11 | 12 | 13 | class OrderAdmin(admin.ModelAdmin): 14 | list_display = ['user', 15 | 'ordered', 16 | 'being_delivered', 17 | 'received', 18 | 'refund_requested', 19 | 'refund_granted', 20 | 'shipping_address', 21 | 'billing_address', 22 | 'payment', 23 | 'coupon' 24 | ] 25 | list_display_links = [ 26 | 'user', 27 | 'shipping_address', 28 | 'billing_address', 29 | 'payment', 30 | 'coupon' 31 | ] 32 | list_filter = ['ordered', 33 | 'being_delivered', 34 | 'received', 35 | 'refund_requested', 36 | 'refund_granted'] 37 | search_fields = [ 38 | 'user__username', 39 | 'ref_code' 40 | ] 41 | actions = [make_refund_accepted] 42 | 43 | 44 | class AddressAdmin(admin.ModelAdmin): 45 | list_display = [ 46 | 'user', 47 | 'street_address', 48 | 'apartment_address', 49 | 'country', 50 | 'zip', 51 | 'address_type', 52 | 'default' 53 | ] 54 | list_filter = ['default', 'address_type', 'country'] 55 | search_fields = ['user', 'street_address', 'apartment_address', 'zip'] 56 | 57 | 58 | admin.site.register(Item) 59 | admin.site.register(OrderItem) 60 | admin.site.register(Order, OrderAdmin) 61 | admin.site.register(Payment) 62 | admin.site.register(Coupon) 63 | admin.site.register(Refund) 64 | admin.site.register(Address, AddressAdmin) 65 | admin.site.register(UserProfile) 66 | -------------------------------------------------------------------------------- /core/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from core.models import Item, Order, OrderItem 3 | 4 | 5 | class StringSerializer(serializers.StringRelatedField): 6 | def to_internal_value(self, value): 7 | return value 8 | 9 | 10 | class ItemSerializer(serializers.ModelSerializer): 11 | category = serializers.SerializerMethodField() 12 | label = serializers.SerializerMethodField() 13 | 14 | class Meta: 15 | model = Item 16 | fields = ( 17 | 'id', 18 | 'title', 19 | 'price', 20 | 'discount_price', 21 | 'category', 22 | 'label', 23 | 'slug', 24 | 'description', 25 | 'image', 26 | ) 27 | 28 | def get_category(self, obj): 29 | return obj.get_category_display() 30 | 31 | def get_label(self, obj): 32 | return obj.get_label_display() 33 | 34 | 35 | class OrderItemSerializer(serializers.ModelSerializer): 36 | item = StringSerializer() 37 | 38 | class Meta: 39 | model = OrderItem 40 | fields = ( 41 | 'id', 42 | 'item', 43 | 'quantity' 44 | ) 45 | 46 | 47 | class OrderSerializer(serializers.ModelSerializer): 48 | order_items = serializers.SerializerMethodField() 49 | 50 | class Meta: 51 | model = Order 52 | fields = ( 53 | 'id', 54 | 'order_items' 55 | ) 56 | 57 | def get_order_items(self, obj): 58 | return OrderItemSerializer(obj.items.all(), many=True).data 59 | -------------------------------------------------------------------------------- /core/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import ( 3 | ItemListView, 4 | AddToCartView, 5 | OrderDetailView 6 | ) 7 | urlpatterns = [ 8 | path('product-list/', ItemListView.as_view(), name='product-list'), 9 | path('add-to-cart/', AddToCartView.as_view(), name='add-to-cart'), 10 | path('order-summary/', OrderDetailView.as_view(), name='order-summary') 11 | ] 12 | -------------------------------------------------------------------------------- /core/api/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ObjectDoesNotExist 3 | from django.shortcuts import render, get_object_or_404 4 | from django.utils import timezone 5 | from rest_framework.generics import ListAPIView, RetrieveAPIView 6 | from rest_framework.permissions import AllowAny, IsAuthenticated 7 | from rest_framework.views import APIView 8 | from rest_framework.response import Response 9 | from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST 10 | from core.models import Item, OrderItem, Order 11 | from .serializers import ItemSerializer, OrderSerializer 12 | 13 | 14 | class ItemListView(ListAPIView): 15 | permission_classes = (AllowAny,) 16 | serializer_class = ItemSerializer 17 | queryset = Item.objects.all() 18 | 19 | 20 | class AddToCartView(APIView): 21 | def post(self, request, *args, **kwargs): 22 | slug = request.data.get('slug', None) 23 | if slug is None: 24 | return Response({"message": "Invalid request"}, status=HTTP_400_BAD_REQUEST) 25 | item = get_object_or_404(Item, slug=slug) 26 | order_item, created = OrderItem.objects.get_or_create( 27 | item=item, 28 | user=request.user, 29 | ordered=False 30 | ) 31 | order_qs = Order.objects.filter(user=request.user, ordered=False) 32 | if order_qs.exists(): 33 | order = order_qs[0] 34 | # check if the order item is in the order 35 | if order.items.filter(item__slug=item.slug).exists(): 36 | order_item.quantity += 1 37 | order_item.save() 38 | return Response(status=HTTP_200_OK) 39 | else: 40 | order.items.add(order_item) 41 | return Response(status=HTTP_200_OK) 42 | else: 43 | ordered_date = timezone.now() 44 | order = Order.objects.create( 45 | user=request.user, ordered_date=ordered_date) 46 | order.items.add(order_item) 47 | return Response(status=HTTP_200_OK) 48 | 49 | 50 | class OrderDetailView(RetrieveAPIView): 51 | serializer_class = OrderSerializer 52 | permission_classes = (IsAuthenticated,) 53 | 54 | def get_object(self): 55 | try: 56 | order = Order.objects.get( 57 | user=self.request.user, ordered=False) 58 | except ObjectDoesNotExist: 59 | return Response({"message": "You do not have an active order"}, status=HTTP_400_BAD_REQUEST) 60 | -------------------------------------------------------------------------------- /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('current', type=str, nargs='+', 10 | help='The current Django project folder name') 11 | parser.add_argument('new', type=str, nargs='+', 12 | help='The new Django project name') 13 | 14 | def handle(self, *args, **kwargs): 15 | current_project_name = kwargs['current'][0] 16 | new_project_name = kwargs['new'][0] 17 | 18 | # logic for renaming the files 19 | 20 | files_to_rename = [f'{current_project_name}/settings.py', 21 | f'{current_project_name}/wsgi.py', 'manage.py'] 22 | 23 | for f in files_to_rename: 24 | with open(f, 'r') as file: 25 | filedata = file.read() 26 | 27 | filedata = filedata.replace(current_project_name, new_project_name) 28 | 29 | with open(f, 'w') as file: 30 | file.write(filedata) 31 | 32 | os.rename(current_project_name, new_project_name) 33 | 34 | self.stdout.write(self.style.SUCCESS( 35 | 'Project has been renamed to %s' % new_project_name)) 36 | -------------------------------------------------------------------------------- /core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.3 on 2019-10-16 18:12 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/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zinmyoswe/React-and-Django-Ecommerce/1cb1f04a421d17ce85f1ab77fa3b88d35f0d613c/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 OrderItem(models.Model): 67 | user = models.ForeignKey(settings.AUTH_USER_MODEL, 68 | on_delete=models.CASCADE) 69 | ordered = models.BooleanField(default=False) 70 | item = models.ForeignKey(Item, on_delete=models.CASCADE) 71 | quantity = models.IntegerField(default=1) 72 | 73 | def __str__(self): 74 | return f"{self.quantity} of {self.item.title}" 75 | 76 | def get_total_item_price(self): 77 | return self.quantity * self.item.price 78 | 79 | def get_total_discount_item_price(self): 80 | return self.quantity * self.item.discount_price 81 | 82 | def get_amount_saved(self): 83 | return self.get_total_item_price() - self.get_total_discount_item_price() 84 | 85 | def get_final_price(self): 86 | if self.item.discount_price: 87 | return self.get_total_discount_item_price() 88 | return self.get_total_item_price() 89 | 90 | 91 | class Order(models.Model): 92 | user = models.ForeignKey(settings.AUTH_USER_MODEL, 93 | on_delete=models.CASCADE) 94 | ref_code = models.CharField(max_length=20, blank=True, null=True) 95 | items = models.ManyToManyField(OrderItem) 96 | start_date = models.DateTimeField(auto_now_add=True) 97 | ordered_date = models.DateTimeField() 98 | ordered = models.BooleanField(default=False) 99 | shipping_address = models.ForeignKey( 100 | 'Address', related_name='shipping_address', on_delete=models.SET_NULL, blank=True, null=True) 101 | billing_address = models.ForeignKey( 102 | 'Address', related_name='billing_address', on_delete=models.SET_NULL, blank=True, null=True) 103 | payment = models.ForeignKey( 104 | 'Payment', on_delete=models.SET_NULL, blank=True, null=True) 105 | coupon = models.ForeignKey( 106 | 'Coupon', on_delete=models.SET_NULL, blank=True, null=True) 107 | being_delivered = models.BooleanField(default=False) 108 | received = models.BooleanField(default=False) 109 | refund_requested = models.BooleanField(default=False) 110 | refund_granted = models.BooleanField(default=False) 111 | 112 | ''' 113 | 1. Item added to cart 114 | 2. Adding a billing address 115 | (Failed checkout) 116 | 3. Payment 117 | (Preprocessing, processing, packaging etc.) 118 | 4. Being delivered 119 | 5. Received 120 | 6. Refunds 121 | ''' 122 | 123 | def __str__(self): 124 | return self.user.username 125 | 126 | def get_total(self): 127 | total = 0 128 | for order_item in self.items.all(): 129 | total += order_item.get_final_price() 130 | if self.coupon: 131 | total -= self.coupon.amount 132 | return total 133 | 134 | 135 | class Address(models.Model): 136 | user = models.ForeignKey(settings.AUTH_USER_MODEL, 137 | on_delete=models.CASCADE) 138 | street_address = models.CharField(max_length=100) 139 | apartment_address = models.CharField(max_length=100) 140 | country = CountryField(multiple=False) 141 | zip = models.CharField(max_length=100) 142 | address_type = models.CharField(max_length=1, choices=ADDRESS_CHOICES) 143 | default = models.BooleanField(default=False) 144 | 145 | def __str__(self): 146 | return self.user.username 147 | 148 | class Meta: 149 | verbose_name_plural = 'Addresses' 150 | 151 | 152 | class Payment(models.Model): 153 | stripe_charge_id = models.CharField(max_length=50) 154 | user = models.ForeignKey(settings.AUTH_USER_MODEL, 155 | on_delete=models.SET_NULL, blank=True, null=True) 156 | amount = models.FloatField() 157 | timestamp = models.DateTimeField(auto_now_add=True) 158 | 159 | def __str__(self): 160 | return self.user.username 161 | 162 | 163 | class Coupon(models.Model): 164 | code = models.CharField(max_length=15) 165 | amount = models.FloatField() 166 | 167 | def __str__(self): 168 | return self.code 169 | 170 | 171 | class Refund(models.Model): 172 | order = models.ForeignKey(Order, on_delete=models.CASCADE) 173 | reason = models.TextField() 174 | accepted = models.BooleanField(default=False) 175 | email = models.EmailField() 176 | 177 | def __str__(self): 178 | return f"{self.pk}" 179 | 180 | 181 | def userprofile_receiver(sender, instance, created, *args, **kwargs): 182 | if created: 183 | userprofile = UserProfile.objects.create(user=instance) 184 | 185 | 186 | post_save.connect(userprofile_receiver, sender=settings.AUTH_USER_MODEL) 187 | -------------------------------------------------------------------------------- /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 | print(e) 315 | messages.warning(self.request, "Invalid parameters") 316 | return redirect("/") 317 | 318 | except stripe.error.AuthenticationError as e: 319 | # Authentication with Stripe's API failed 320 | # (maybe you changed API keys recently) 321 | messages.warning(self.request, "Not authenticated") 322 | return redirect("/") 323 | 324 | except stripe.error.APIConnectionError as e: 325 | # Network communication with Stripe failed 326 | messages.warning(self.request, "Network error") 327 | return redirect("/") 328 | 329 | except stripe.error.StripeError as e: 330 | # Display a very generic error to the user, and maybe send 331 | # yourself an email 332 | messages.warning( 333 | self.request, "Something went wrong. You were not charged. Please try again.") 334 | return redirect("/") 335 | 336 | except Exception as e: 337 | # send an email to ourselves 338 | messages.warning( 339 | self.request, "A serious error occurred. We have been notifed.") 340 | return redirect("/") 341 | 342 | messages.warning(self.request, "Invalid data received") 343 | return redirect("/payment/stripe/") 344 | 345 | 346 | class HomeView(ListView): 347 | model = Item 348 | paginate_by = 10 349 | template_name = "home.html" 350 | 351 | 352 | class OrderSummaryView(LoginRequiredMixin, View): 353 | def get(self, *args, **kwargs): 354 | try: 355 | order = Order.objects.get(user=self.request.user, ordered=False) 356 | context = { 357 | 'object': order 358 | } 359 | return render(self.request, 'order_summary.html', context) 360 | except ObjectDoesNotExist: 361 | messages.warning(self.request, "You do not have an active order") 362 | return redirect("/") 363 | 364 | 365 | class ItemDetailView(DetailView): 366 | model = Item 367 | template_name = "product.html" 368 | 369 | 370 | @login_required 371 | def add_to_cart(request, slug): 372 | item = get_object_or_404(Item, slug=slug) 373 | order_item, created = OrderItem.objects.get_or_create( 374 | item=item, 375 | user=request.user, 376 | ordered=False 377 | ) 378 | order_qs = Order.objects.filter(user=request.user, ordered=False) 379 | if order_qs.exists(): 380 | order = order_qs[0] 381 | # check if the order item is in the order 382 | if order.items.filter(item__slug=item.slug).exists(): 383 | order_item.quantity += 1 384 | order_item.save() 385 | messages.info(request, "This item quantity was updated.") 386 | return redirect("core:order-summary") 387 | else: 388 | order.items.add(order_item) 389 | messages.info(request, "This item was added to your cart.") 390 | return redirect("core:order-summary") 391 | else: 392 | ordered_date = timezone.now() 393 | order = Order.objects.create( 394 | user=request.user, ordered_date=ordered_date) 395 | order.items.add(order_item) 396 | messages.info(request, "This item was added to your cart.") 397 | return redirect("core:order-summary") 398 | 399 | 400 | @login_required 401 | def remove_from_cart(request, slug): 402 | item = get_object_or_404(Item, slug=slug) 403 | order_qs = Order.objects.filter( 404 | user=request.user, 405 | ordered=False 406 | ) 407 | if order_qs.exists(): 408 | order = order_qs[0] 409 | # check if the order item is in the order 410 | if order.items.filter(item__slug=item.slug).exists(): 411 | order_item = OrderItem.objects.filter( 412 | item=item, 413 | user=request.user, 414 | ordered=False 415 | )[0] 416 | order.items.remove(order_item) 417 | messages.info(request, "This item was removed from your cart.") 418 | return redirect("core:order-summary") 419 | else: 420 | messages.info(request, "This item was not in your cart") 421 | return redirect("core:product", slug=slug) 422 | else: 423 | messages.info(request, "You do not have an active order") 424 | return redirect("core:product", slug=slug) 425 | 426 | 427 | @login_required 428 | def remove_single_item_from_cart(request, slug): 429 | item = get_object_or_404(Item, slug=slug) 430 | order_qs = Order.objects.filter( 431 | user=request.user, 432 | ordered=False 433 | ) 434 | if order_qs.exists(): 435 | order = order_qs[0] 436 | # check if the order item is in the order 437 | if order.items.filter(item__slug=item.slug).exists(): 438 | order_item = OrderItem.objects.filter( 439 | item=item, 440 | user=request.user, 441 | ordered=False 442 | )[0] 443 | if order_item.quantity > 1: 444 | order_item.quantity -= 1 445 | order_item.save() 446 | else: 447 | order.items.remove(order_item) 448 | messages.info(request, "This item quantity was updated.") 449 | return redirect("core:order-summary") 450 | else: 451 | messages.info(request, "This item was not in your cart") 452 | return redirect("core:product", slug=slug) 453 | else: 454 | messages.info(request, "You do not have an active order") 455 | return redirect("core:product", slug=slug) 456 | 457 | 458 | def get_coupon(request, code): 459 | try: 460 | coupon = Coupon.objects.get(code=code) 461 | return coupon 462 | except ObjectDoesNotExist: 463 | messages.info(request, "This coupon does not exist") 464 | return redirect("core:checkout") 465 | 466 | 467 | class AddCouponView(View): 468 | def post(self, *args, **kwargs): 469 | form = CouponForm(self.request.POST or None) 470 | if form.is_valid(): 471 | try: 472 | code = form.cleaned_data.get('code') 473 | order = Order.objects.get( 474 | user=self.request.user, ordered=False) 475 | order.coupon = get_coupon(self.request, code) 476 | order.save() 477 | messages.success(self.request, "Successfully added coupon") 478 | return redirect("core:checkout") 479 | except ObjectDoesNotExist: 480 | messages.info(self.request, "You do not have an active order") 481 | return redirect("core:checkout") 482 | 483 | 484 | class RequestRefundView(View): 485 | def get(self, *args, **kwargs): 486 | form = RefundForm() 487 | context = { 488 | 'form': form 489 | } 490 | return render(self.request, "request_refund.html", context) 491 | 492 | def post(self, *args, **kwargs): 493 | form = RefundForm(self.request.POST) 494 | if form.is_valid(): 495 | ref_code = form.cleaned_data.get('ref_code') 496 | message = form.cleaned_data.get('message') 497 | email = form.cleaned_data.get('email') 498 | # edit the order 499 | try: 500 | order = Order.objects.get(ref_code=ref_code) 501 | order.refund_requested = True 502 | order.save() 503 | 504 | # store the refund 505 | refund = Refund() 506 | refund.order = order 507 | refund.reason = message 508 | refund.email = email 509 | refund.save() 510 | 511 | messages.info(self.request, "Your request was received.") 512 | return redirect("core:request-refund") 513 | 514 | except ObjectDoesNotExist: 515 | messages.info(self.request, "This order does not exist.") 516 | return redirect("core:request-refund") 517 | -------------------------------------------------------------------------------- /home/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zinmyoswe/React-and-Django-Ecommerce/1cb1f04a421d17ce85f1ab77fa3b88d35f0d613c/home/__init__.py -------------------------------------------------------------------------------- /home/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zinmyoswe/React-and-Django-Ecommerce/1cb1f04a421d17ce85f1ab77fa3b88d35f0d613c/home/settings/__init__.py -------------------------------------------------------------------------------- /home/settings/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname( 4 | os.path.dirname(os.path.abspath(__file__)))) 5 | SECRET_KEY = '-05sgp9!deq=q1nltm@^^2cc+v29i(tyybv3v2t77qi66czazj' 6 | DEBUG = True 7 | ALLOWED_HOSTS = [] 8 | 9 | INSTALLED_APPS = [ 10 | 'django.contrib.admin', 11 | 'django.contrib.auth', 12 | 'django.contrib.contenttypes', 13 | 'django.contrib.sessions', 14 | 'django.contrib.messages', 15 | 'django.contrib.staticfiles', 16 | 17 | 'django.contrib.sites', 18 | 'allauth', 19 | 'allauth.account', 20 | 'allauth.socialaccount', 21 | 'corsheaders', 22 | 'rest_auth', 23 | 'rest_auth.registration', 24 | 'rest_framework', 25 | 'rest_framework.authtoken', 26 | 'core', 27 | ] 28 | 29 | MIDDLEWARE = [ 30 | 'corsheaders.middleware.CorsMiddleware', 31 | 'django.middleware.security.SecurityMiddleware', 32 | 'django.contrib.sessions.middleware.SessionMiddleware', 33 | 'django.middleware.common.CommonMiddleware', 34 | 'django.middleware.csrf.CsrfViewMiddleware', 35 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 36 | 'django.contrib.messages.middleware.MessageMiddleware', 37 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 38 | ] 39 | 40 | ROOT_URLCONF = 'home.urls' 41 | 42 | TEMPLATES = [ 43 | { 44 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 45 | 'DIRS': [os.path.join(BASE_DIR, 'build')], 46 | 'APP_DIRS': True, 47 | 'OPTIONS': { 48 | 'context_processors': [ 49 | 'django.template.context_processors.debug', 50 | 'django.template.context_processors.request', 51 | 'django.contrib.auth.context_processors.auth', 52 | 'django.contrib.messages.context_processors.messages', 53 | ], 54 | }, 55 | }, 56 | ] 57 | 58 | LANGUAGE_CODE = 'en-us' 59 | TIME_ZONE = 'UTC' 60 | USE_I18N = True 61 | USE_L10N = True 62 | USE_TZ = True 63 | 64 | STATIC_URL = '/static/' 65 | MEDIA_URL = '/media/' 66 | STATICFILES_DIRS = [os.path.join(BASE_DIR, 'build/static')] 67 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 68 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 69 | SITE_ID = 1 70 | 71 | REST_FRAMEWORK = { 72 | 'DEFAULT_PERMISSION_CLASSES': ( 73 | 'rest_framework.permissions.AllowAny', 74 | ), 75 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 76 | 'rest_framework.authentication.TokenAuthentication', 77 | ), 78 | } 79 | 80 | ACCOUNT_EMAIL_REQUIRED = False 81 | ACCOUNT_AUTHENTICATION_METHOD = 'username' 82 | ACCOUNT_EMAIL_VERIFICATION = 'none' 83 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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('api/', include('core.api.urls')), 12 | path('admin/', admin.site.urls), 13 | 14 | ] 15 | 16 | if settings.DEBUG: 17 | urlpatterns += static(settings.MEDIA_URL, 18 | document_root=settings.MEDIA_ROOT) 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 | -------------------------------------------------------------------------------- /media/add2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zinmyoswe/React-and-Django-Ecommerce/1cb1f04a421d17ce85f1ab77fa3b88d35f0d613c/media/add2.jpg -------------------------------------------------------------------------------- /media/add4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zinmyoswe/React-and-Django-Ecommerce/1cb1f04a421d17ce85f1ab77fa3b88d35f0d613c/media/add4.webp -------------------------------------------------------------------------------- /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.10.2", 9 | "react-dom": "^16.10.2", 10 | "react-redux": "^7.1.1", 11 | "react-router-dom": "^5.1.2", 12 | "react-scripts": "^3.2.0", 13 | "redux": "^4.0.4", 14 | "redux-thunk": "^2.3.0", 15 | "semantic-ui-css": "^2.4.1", 16 | "semantic-ui-react": "^0.87.3" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zinmyoswe/React-and-Django-Ecommerce/1cb1f04a421d17ce85f1ab77fa3b88d35f0d613c/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Django React Boilerplate 10 | 11 | 12 | 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: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zinmyoswe/React-and-Django-Ecommerce/1cb1f04a421d17ce85f1ab77fa3b88d35f0d613c/requirements.txt -------------------------------------------------------------------------------- /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}/product-list/`; 8 | export const addToCartURL = `${endpoint}/add-to-cart/`; 9 | export const orderSummaryURL = `${endpoint}/order-summary/`; 10 | -------------------------------------------------------------------------------- /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 | Icon, 10 | Image, 11 | List, 12 | Menu, 13 | Responsive, 14 | Segment, 15 | Sidebar, 16 | Visibility 17 | } from "semantic-ui-react"; 18 | 19 | const getWidth = () => { 20 | const isSSR = typeof window === "undefined"; 21 | return isSSR ? Responsive.onlyTablet.minWidth : window.innerWidth; 22 | }; 23 | 24 | class DesktopContainer extends Component { 25 | state = {}; 26 | 27 | hideFixedMenu = () => this.setState({ fixed: false }); 28 | showFixedMenu = () => this.setState({ fixed: true }); 29 | 30 | render() { 31 | const { children } = this.props; 32 | const { fixed } = this.state; 33 | 34 | return ( 35 | 36 | 41 | {children} 42 | 43 | ); 44 | } 45 | } 46 | 47 | DesktopContainer.propTypes = { 48 | children: PropTypes.node 49 | }; 50 | 51 | class MobileContainer extends Component { 52 | state = {}; 53 | 54 | handleSidebarHide = () => this.setState({ sidebarOpened: false }); 55 | 56 | handleToggle = () => this.setState({ sidebarOpened: true }); 57 | 58 | render() { 59 | const { children } = this.props; 60 | const { sidebarOpened } = this.state; 61 | 62 | return ( 63 | 68 | {children} 69 | 70 | ); 71 | } 72 | } 73 | 74 | MobileContainer.propTypes = { 75 | children: PropTypes.node 76 | }; 77 | 78 | const ResponsiveContainer = ({ children }) => ( 79 |
80 | {children} 81 | {children} 82 |
83 | ); 84 | 85 | ResponsiveContainer.propTypes = { 86 | children: PropTypes.node 87 | }; 88 | 89 | const HomepageLayout = () => ( 90 | 91 | 92 | 93 | 94 | 95 |
96 | We Help Companies and Companions 97 |
98 |

99 | We can give your company superpowers to do things that they never 100 | thought possible. Let us delight your customers and empower your 101 | needs... through pure data analytics. 102 |

103 |
104 | We Make Bananas That Can Dance 105 |
106 |

107 | Yes that's right, you thought it was the stuff of dreams, but even 108 | bananas can be bioengineered. 109 |

110 |
111 | 112 | 118 | 119 |
120 | 121 | 122 | 123 | 124 | 125 |
126 |
127 | 128 | 129 | 130 | 131 |
132 | "What a Company" 133 |
134 |

135 | That is what they all say about us 136 |

137 |
138 | 139 |
140 | "I shouldn't have gone with their competitor." 141 |
142 |

143 | 144 | Nan Chief Fun Officer Acme Toys 145 |

146 |
147 |
148 |
149 |
150 | 151 | 152 |
153 | Breaking The Grid, Grabs Your Attention 154 |
155 |

156 | Instead of focusing on content creation and hard work, we have learned 157 | how to master the art of doing nothing by providing massive amounts of 158 | whitespace and generic content that can seem massive, monolithic and 159 | worth your attention. 160 |

161 | 164 | 170 | Case Studies 171 | 172 |
173 | Did We Tell You About Our Bananas? 174 |
175 |

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

180 | 183 |
184 |
185 |
186 | ); 187 | export default HomepageLayout; 188 | -------------------------------------------------------------------------------- /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 | console.log(cart); 26 | return ( 27 |
28 | 29 | 30 | 31 | Home 32 | 33 | 34 | 35 | Product 36 | 37 | 38 | {authenticated ? ( 39 | 40 | 53 | 54 | {cart && 55 | cart.order_items && 56 | cart.order_items.map(order_item => { 57 | return ( 58 | 59 | {order_item.quantity} *{order_item.item} 60 | 61 | ); 62 | })} 63 | {cart && 64 | cart.order_items && 65 | cart.order_items.length < 1 ? ( 66 | No items in your cart 67 | ) : null} 68 | 69 | 70 | Header Item 71 | 72 | 73 | Submenu 74 | 75 | List Item 76 | List Item 77 | 78 | 79 | List Item 80 | 81 | 82 | this.props.logout()}> 83 | Logout 84 | 85 | 86 | ) : ( 87 | 88 | 89 | Login 90 | 91 | 92 | Signup 93 | 94 | 95 | )} 96 | 97 | 98 | 99 | 100 | {this.props.children} 101 | 102 | 107 | 108 | 109 | 110 |
111 | 112 | Link One 113 | Link Two 114 | Link Three 115 | Link Four 116 | 117 | 118 | 119 |
120 | 121 | Link One 122 | Link Two 123 | Link Three 124 | Link Four 125 | 126 | 127 | 128 |
129 | 130 | Link One 131 | Link Two 132 | Link Three 133 | Link Four 134 | 135 | 136 | 137 |
138 |

139 | Extra space for a call to action inside the footer that could 140 | help re-engage users. 141 |

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

{this.props.error.message}

} 47 | 48 | 49 |
50 | 51 | 60 | 70 | 71 | 80 | 81 |
82 | 83 | New to us? Sign Up 84 | 85 |
86 |
87 |
88 | ); 89 | } 90 | } 91 | 92 | const mapStateToProps = state => { 93 | return { 94 | loading: state.auth.loading, 95 | error: state.auth.error, 96 | token: state.auth.token 97 | }; 98 | }; 99 | 100 | const mapDispatchToProps = dispatch => { 101 | return { 102 | login: (username, password) => dispatch(authLogin(username, password)) 103 | }; 104 | }; 105 | 106 | export default connect( 107 | mapStateToProps, 108 | mapDispatchToProps 109 | )(LoginForm); 110 | -------------------------------------------------------------------------------- /src/containers/ProductList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import axios from "axios"; 4 | import { 5 | Button, 6 | Container, 7 | Icon, 8 | Image, 9 | Item, 10 | Label, 11 | Segment, 12 | Loader, 13 | Dimmer, 14 | Message 15 | } from "semantic-ui-react"; 16 | import { productListURL, addToCartURL } from "../constants"; 17 | import { authAxios } from "../utils"; 18 | import { fetchCart } from "../store/actions/cart"; 19 | 20 | class ProductList extends React.Component { 21 | state = { 22 | loading: false, 23 | error: null, 24 | data: [] 25 | }; 26 | 27 | componentDidMount() { 28 | this.setState({ loading: true }); 29 | axios 30 | .get(productListURL) 31 | .then(res => { 32 | console.log(res.data); 33 | this.setState({ data: res.data, loading: false }); 34 | }) 35 | .catch(err => { 36 | this.setState({ error: err, loading: false }); 37 | }); 38 | } 39 | 40 | handleAddToCart = slug => { 41 | this.setState({ loading: true }); 42 | authAxios 43 | .post(addToCartURL, { slug }) 44 | .then(res => { 45 | console.log(res.data); 46 | this.setState({ loading: false }); 47 | }) 48 | .catch(err => { 49 | this.setState({ error: err, loading: false }); 50 | }); 51 | }; 52 | 53 | render() { 54 | const { data, error, loading } = this.state; 55 | return ( 56 | 57 | {error && ( 58 | 63 | )} 64 | {loading && ( 65 | 66 | 67 | Loading 68 | 69 | 70 | 71 | 72 | )} 73 | 74 | {data.map(item => { 75 | return ( 76 | 77 | 78 | 79 | 80 | {item.title} 81 | 82 | {item.category} 83 | 84 | {item.description} 85 | 86 | 87 | 110 | 111 | 112 | ); 113 | })} 114 | 115 | 116 | ); 117 | } 118 | } 119 | 120 | const mapDispatchToProps = dispatch => { 121 | return { 122 | fetchCart: () => dispatch(fetchCart()) 123 | }; 124 | }; 125 | 126 | export default connect( 127 | null, 128 | mapDispatchToProps 129 | )(ProductList); 130 | -------------------------------------------------------------------------------- /src/containers/Signup.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Button, 4 | Form, 5 | Grid, 6 | Header, 7 | Message, 8 | Segment 9 | } from "semantic-ui-react"; 10 | import { connect } from "react-redux"; 11 | import { NavLink, Redirect } from "react-router-dom"; 12 | import { authSignup } from "../store/actions/auth"; 13 | 14 | class RegistrationForm extends React.Component { 15 | state = { 16 | username: "", 17 | email: "", 18 | password1: "", 19 | password2: "" 20 | }; 21 | 22 | handleSubmit = e => { 23 | e.preventDefault(); 24 | const { username, email, password1, password2 } = this.state; 25 | this.props.signup(username, email, password1, password2); 26 | }; 27 | 28 | handleChange = e => { 29 | this.setState({ [e.target.name]: e.target.value }); 30 | }; 31 | 32 | render() { 33 | const { username, email, password1, password2 } = this.state; 34 | const { error, loading, token } = this.props; 35 | if (token) { 36 | return ; 37 | } 38 | return ( 39 | 44 | 45 |
46 | Signup to your account 47 |
48 | {error &&

{this.props.error.message}

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