├── cart ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── admin.py ├── tests.py ├── apps.py ├── urls.py ├── models.py ├── templates │ └── cart │ │ └── detail.html └── views.py ├── orders ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── tests.py ├── apps.py ├── forms.py ├── urls.py ├── admin.py ├── templates │ └── orders │ │ ├── order_confirmation.html │ │ └── order_create.html ├── models.py └── views.py ├── products ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── tests.py ├── apps.py ├── urls.py ├── admin.py ├── views.py ├── models.py └── templates │ └── products │ └── product │ ├── list.html │ └── detail.html ├── ecommercesite ├── __init__.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── static ├── src │ └── styles.css └── dist │ └── styles.css ├── requirements.txt ├── mediafiles └── products │ ├── 2.jpg │ ├── green.jpg │ ├── T-Shirt.png │ ├── dress2.jpg │ ├── leather.jpg │ ├── leather2.webp │ ├── leather3.jpg │ ├── T-Shirt_RZC9O52.png │ ├── leather_2jkxrby.jpg │ ├── leather3_8g8xdro.jpg │ └── c50d068b-f425-44c8-9978-432494f1c0d9.webp ├── tailwind.config.js ├── package.json ├── .gitignore ├── manage.py ├── templates ├── base.html └── includes │ └── nav.html └── readme.md /cart/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /orders/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /products/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cart/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ecommercesite/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /orders/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /products/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cart/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /cart/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /orders/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /static/src/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /products/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikoCanFly/E-commerce-Site-with-Django-and-TailwindCSS/HEAD/requirements.txt -------------------------------------------------------------------------------- /mediafiles/products/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikoCanFly/E-commerce-Site-with-Django-and-TailwindCSS/HEAD/mediafiles/products/2.jpg -------------------------------------------------------------------------------- /mediafiles/products/green.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikoCanFly/E-commerce-Site-with-Django-and-TailwindCSS/HEAD/mediafiles/products/green.jpg -------------------------------------------------------------------------------- /mediafiles/products/T-Shirt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikoCanFly/E-commerce-Site-with-Django-and-TailwindCSS/HEAD/mediafiles/products/T-Shirt.png -------------------------------------------------------------------------------- /mediafiles/products/dress2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikoCanFly/E-commerce-Site-with-Django-and-TailwindCSS/HEAD/mediafiles/products/dress2.jpg -------------------------------------------------------------------------------- /mediafiles/products/leather.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikoCanFly/E-commerce-Site-with-Django-and-TailwindCSS/HEAD/mediafiles/products/leather.jpg -------------------------------------------------------------------------------- /mediafiles/products/leather2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikoCanFly/E-commerce-Site-with-Django-and-TailwindCSS/HEAD/mediafiles/products/leather2.webp -------------------------------------------------------------------------------- /mediafiles/products/leather3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikoCanFly/E-commerce-Site-with-Django-and-TailwindCSS/HEAD/mediafiles/products/leather3.jpg -------------------------------------------------------------------------------- /mediafiles/products/T-Shirt_RZC9O52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikoCanFly/E-commerce-Site-with-Django-and-TailwindCSS/HEAD/mediafiles/products/T-Shirt_RZC9O52.png -------------------------------------------------------------------------------- /mediafiles/products/leather_2jkxrby.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikoCanFly/E-commerce-Site-with-Django-and-TailwindCSS/HEAD/mediafiles/products/leather_2jkxrby.jpg -------------------------------------------------------------------------------- /mediafiles/products/leather3_8g8xdro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikoCanFly/E-commerce-Site-with-Django-and-TailwindCSS/HEAD/mediafiles/products/leather3_8g8xdro.jpg -------------------------------------------------------------------------------- /cart/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CartConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'cart' 7 | -------------------------------------------------------------------------------- /orders/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class OrdersConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'orders' 7 | -------------------------------------------------------------------------------- /products/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProductsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'products' 7 | -------------------------------------------------------------------------------- /mediafiles/products/c50d068b-f425-44c8-9978-432494f1c0d9.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikoCanFly/E-commerce-Site-with-Django-and-TailwindCSS/HEAD/mediafiles/products/c50d068b-f425-44c8-9978-432494f1c0d9.webp -------------------------------------------------------------------------------- /orders/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from .models import Order 3 | 4 | class OrderCreateForm(forms.ModelForm): 5 | class Meta: 6 | model = Order 7 | fields = ["full_name", "email", "address"] -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./templates/**/*.html', 4 | './products/templates/**/*.html', 5 | './cart/templates/**/*.html', 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | 13 | -------------------------------------------------------------------------------- /orders/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import order_create, order_confirmation 3 | 4 | app_name = "orders" 5 | 6 | urlpatterns = [ 7 | path('create', order_create, name="order_create"), 8 | path("confirmation/", order_confirmation, name="order_confirmation"), 9 | ] 10 | -------------------------------------------------------------------------------- /cart/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import cart_add, cart_detail, cart_remove 3 | 4 | 5 | app_name = "cart" 6 | urlpatterns = [ 7 | path('add//', cart_add, name="cart_add"), 8 | path("",cart_detail, name="cart_detail"), 9 | path("remove//", cart_remove, name="remove_item") 10 | ] 11 | -------------------------------------------------------------------------------- /products/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | 5 | app_name= 'products' 6 | 7 | urlpatterns = [ 8 | path('', views.product_list, name='product_list'), 9 | path('/', views.product_list, name='product_list_by_category'), 10 | path('product///', views.product_detail, name="product_detail"), 11 | ] -------------------------------------------------------------------------------- /orders/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Order, OrderItem 3 | 4 | class OrderItemInline(admin.TabularInline): 5 | model = OrderItem 6 | raw_id_fields = ["product"] 7 | 8 | @admin.register(Order) 9 | class OrderAdmin(admin.ModelAdmin): 10 | list_display = ["id", "full_name", "email"] 11 | inlines = [OrderItemInline] 12 | readonly_fields=("created_at",) 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecommercesite", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "watch:css":"npx tailwindcss -i ./static/src/styles.css -o ./static/dist/styles.css --watch" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "description": "", 13 | "devDependencies": { 14 | "tailwindcss": "^3.4.7" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /products/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Category, Product 3 | 4 | @admin.register(Category) 5 | class CategoryAdmin(admin.ModelAdmin): 6 | list_display = ["name", "slug"] 7 | prepopulated_fields = {'slug':('name',)} 8 | 9 | @admin.register(Product) 10 | class ProductAdmin(admin.ModelAdmin): 11 | list_display = ["name", "price", "available", "created", "updated", "category"] 12 | prepopulated_fields = {'slug':('name',)} 13 | -------------------------------------------------------------------------------- /ecommercesite/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for ecommercesite project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ecommercesite.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /ecommercesite/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for ecommercesite project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ecommercesite.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | *.pyc 3 | *.pyo 4 | *.pyd 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # Django 9 | *.log 10 | *.sqlite3 11 | /media 12 | /staticfiles 13 | .env 14 | *.pot 15 | 16 | # Virtual Environment 17 | venv/ 18 | .env/ 19 | 20 | # IDEs and Editors 21 | .vscode/ 22 | .idea/ 23 | 24 | # OS-specific files 25 | .DS_Store 26 | Thumbs.db 27 | 28 | # Package Distribution 29 | *.egg 30 | *.egg-info 31 | 32 | # Build and Dist 33 | dist/ 34 | !dist/styles.css 35 | build/ 36 | 37 | # Node.js 38 | node_modules/ 39 | -------------------------------------------------------------------------------- /orders/templates/orders/order_confirmation.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {%block title%}Order Confirmation{%endblock%} 3 | 4 | 5 | {%block content%} 6 | 7 |

Your order has been placed! Below are the details.

8 | 9 |

Order ID: #{{order.id}}

10 |
    11 | {%for item in order.items.all%} 12 |
  • 13 | {{item.quantity}} x {{item.product.name}} 14 |
    15 | {{item.price}} 16 | 17 |
  • 18 | {%endfor%} 19 |
      20 | 21 |

      Total" ${{order.get_total_cost}}

      22 | {%endblock%} 23 | -------------------------------------------------------------------------------- /orders/templates/orders/order_create.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {%block title%}Order Create{%endblock%} 3 | 4 | 5 | 6 | {%block content%} 7 |
      8 |

      Checkout

      9 |
      10 | {%csrf_token%} 11 | {{form.as_p}} 12 | 13 |
      14 |
      15 | 16 | 21 | {%endblock%} 22 | -------------------------------------------------------------------------------- /cart/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from products.models import Product 3 | 4 | 5 | class Cart(models.Model): 6 | created_at = models.DateTimeField(auto_now_add=True) 7 | 8 | def get_total_price(self): 9 | return sum(item.get_total_price() for item in self.items.all()) 10 | 11 | class CartItem(models.Model): 12 | cart = models.ForeignKey(Cart, related_name="items", on_delete=models.CASCADE) 13 | product = models.ForeignKey(Product, related_name="cart_items", on_delete=models.CASCADE) 14 | quantity = models.PositiveIntegerField(default=1) 15 | 16 | def get_total_price(self): 17 | return self.product.price * self.quantity 18 | 19 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ecommercesite.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /products/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, get_object_or_404 2 | from .models import Category, Product 3 | 4 | def product_list(request, category_slug=None): 5 | category = None 6 | products = Product.objects.filter(available=True) 7 | categories = Category.objects.all() 8 | if category_slug: 9 | category = get_object_or_404(Category, slug=category_slug) 10 | products = products.filter(category=category) 11 | 12 | return render(request, 'products/product/list.html', { 13 | 'category':category, 14 | 'products':products, 15 | 'categories':categories, 16 | }) 17 | 18 | def product_detail(request, id, slug): 19 | product = get_object_or_404(Product, id=id, slug=slug, available=True) 20 | return render(request, 'products/product/detail.html', {'product':product}) -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {%block title%}My Super Cool Shop Site{%endblock%} 6 | 7 | {% load static %} 8 | 9 | 10 | 11 | 12 | 13 |
      {% include "includes/nav.html" %}
      14 | 15 |
      16 | {%block content%}{%endblock%} 17 |
      18 | 19 |
      20 |
      21 |

      @2024 E-Com Inc. All rights reserved.

      22 |
      23 |
      24 | 25 | 26 | -------------------------------------------------------------------------------- /templates/includes/nav.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /orders/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from products.models import Product 3 | 4 | class Order(models.Model): 5 | full_name = models.CharField(max_length=250) 6 | email = models.EmailField() 7 | address = models.CharField(max_length=250) 8 | created_at = models.DateTimeField(auto_now_add=True) 9 | updated_at = models.DateTimeField(auto_now=True) 10 | paid = models.BooleanField(default=False) 11 | 12 | def get_total_cost(self): 13 | return sum(item.get_cost() for item in self.items.all()) 14 | 15 | class OrderItem(models.Model): 16 | order = models.ForeignKey(Order, related_name="items", on_delete=models.CASCADE) 17 | product = models.ForeignKey(Product, related_name="order_items", on_delete=models.CASCADE) 18 | price = models.DecimalField(max_digits=10, decimal_places=2) 19 | quantity = models.PositiveIntegerField(default=1) 20 | 21 | def get_cost(self): 22 | return self.price * self.quantity -------------------------------------------------------------------------------- /products/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | 4 | 5 | class Category(models.Model): 6 | name = models.CharField(max_length=250) 7 | slug = models.SlugField(unique=True) 8 | 9 | class Meta: 10 | verbose_name_plural = "categories" 11 | 12 | def __str__(self): 13 | return self.name 14 | 15 | 16 | class Product(models.Model): 17 | category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE) 18 | name = models.CharField(max_length=250) 19 | slug = models.SlugField(max_length=250) 20 | description = models.TextField(blank=True) 21 | price = models.DecimalField(max_digits=10, decimal_places=2) 22 | available = models.BooleanField(default=True) 23 | created = models.DateTimeField(auto_now_add=True) 24 | updated = models.DateTimeField(auto_now=True) 25 | image = models.ImageField(upload_to='products', blank=True, null=True) 26 | 27 | def __str__(self) -> str: 28 | return self.name 29 | 30 | def get_absolute_url(self): 31 | return reverse('products:product_detail', kwargs={'id':self.id, 'slug':self.slug}) 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /ecommercesite/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for ecommercesite project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/5.0/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import path, include 19 | from django.conf import settings 20 | from django.conf.urls.static import static 21 | 22 | urlpatterns = [ 23 | path('admin/', admin.site.urls), 24 | path("cart/", include('cart.urls')), 25 | path("orders/", include("orders.urls")), 26 | path('', include('products.urls', namespace='products')), 27 | 28 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 29 | 30 | -------------------------------------------------------------------------------- /cart/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.7 on 2024-08-13 15:01 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('products', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Cart', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('created_at', models.DateTimeField(auto_now_add=True)), 21 | ], 22 | ), 23 | migrations.CreateModel( 24 | name='CartItem', 25 | fields=[ 26 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('quantity', models.PositiveIntegerField(default=1)), 28 | ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='cart.cart')), 29 | ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cart_items', to='products.product')), 30 | ], 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # E-Commerce Site with Django & TailwindCSS 2 | 3 | 4 | This repository contains the code for a functional e-commerce website built with Django and styled using TailwindCSS as shown in my Tutorial [Video on Youtube](https://youtu.be/GUaUVhEBgyk): 5 | 6 | ## Features 7 | 8 | - **Django & TailwindCSS Integration**: Set up Django with TailwindCSS for easy styling. 9 | - **Product Catalog**: Manage products efficiently, with the option to sort by categories. 10 | - **Shopping Cart**: Implement a dynamic cart allowing users to add and remove items. 11 | - **Checkout Process**: Provide a checkout experience, including order confirmation. 12 | - **Order Management**: Administer customer orders from the backend. 13 | - **Admin Interface**: Use Django’s admin panel for management of products, categories, and orders. 14 | 15 | ## Tutorial: 16 | 17 | [![Watch the Tutorial](https://img.youtube.com/vi/GUaUVhEBgyk/0.jpg)](https://www.youtube.com/watch?v=GUaUVhEBgyk) 18 | 19 | ## Support 20 | 21 | If you found this project helpful, consider becoming a patron on my Patreon: 22 | 23 | [![Support me on Patreon](https://img.shields.io/badge/Support%20me%20on-Patreon-orange.svg)](https://www.patreon.com/PikoCanFly) 24 | 25 | Your contributions will help me continue creating helpful content. Thank you for your support! 26 | 27 | ## Contributing 28 | 29 | This project was made to help spread knowledge. If you'd like to contribute, feel free to fork the repository, make your changes, and submit a pull request. 30 | 31 | -------------------------------------------------------------------------------- /orders/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect, get_object_or_404 2 | from cart.models import Cart 3 | from .forms import OrderCreateForm 4 | from .models import OrderItem, Order 5 | 6 | 7 | 8 | def order_create(request): 9 | cart=None 10 | cart_id = request.session.get('cart_id') 11 | 12 | if cart_id: 13 | cart = Cart.objects.get(id=cart_id) 14 | 15 | if not cart or not cart.items.exists(): 16 | return redirect("cart:cart_detail") 17 | 18 | if request.method == "POST": 19 | form = OrderCreateForm(request.POST) 20 | if form.is_valid(): 21 | order = form.save(commit=False) 22 | order.save() 23 | 24 | for item in cart.items.all(): 25 | OrderItem.objects.create( 26 | order = order, 27 | product = item.product, 28 | price = item.product.price, 29 | quantity = item.quantity 30 | ) 31 | cart.delete() 32 | del request.session["cart_id"] 33 | return redirect("orders:order_confirmation", order.id) 34 | else: 35 | form = OrderCreateForm() 36 | 37 | return render(request, "orders/order_create.html", { 38 | "cart":cart, "form":form 39 | }) 40 | 41 | def order_confirmation(request, order_id): 42 | order = get_object_or_404(Order, id=order_id) 43 | return render(request, "orders/order_confirmation.html", {"order":order}) -------------------------------------------------------------------------------- /cart/templates/cart/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {%block title%}My Cart{%endblock%} 3 | 4 | 5 | {%block content%} 6 |

      My Cart

      7 | 8 | {%if cart %} 9 |
        10 | {%for item in cart.items.all %} 11 |
      • 12 | {%if item.product.image%} 13 | {{item.product.name}} 14 | {%endif%} 15 |
        16 | 17 |

        {{item.product.name}}

        18 |
        19 |

        {{item.product.description}}

        20 |

        ${{item.product.price}}

        21 |

        Quantity: {{item.quantity}}

        22 | 23 |
        24 |
        25 | {%csrf_token%} 26 | 27 |
        28 |
        29 |
        30 |
      • 31 | {%endfor%} 32 |
      33 | 34 |

      Total Price: ${{cart.get_total_price}}

      35 | {%else%} 36 |

      Your Cart is empty.

      37 | {% endif %} 38 |
      39 | {%csrf_token%} 40 | 41 |
      42 | {%endblock%} 43 | -------------------------------------------------------------------------------- /products/templates/products/product/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {%block title%}My site{%endblock%} 4 | 5 | {%block content%} 6 | 7 |
      8 | 19 |
      20 | {%if category%} 21 |

      {{category.name}}

      22 | {%else%} 23 |

      All Products:

      24 | 25 | {%endif%} 26 | 44 |
      45 | 46 |
      47 | 48 | {%endblock%} -------------------------------------------------------------------------------- /orders/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.7 on 2024-08-17 14:54 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('products', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Order', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('full_name', models.CharField(max_length=250)), 21 | ('email', models.EmailField(max_length=254)), 22 | ('address', models.CharField(max_length=250)), 23 | ('created_at', models.DateTimeField(auto_now_add=True)), 24 | ('updated_at', models.DateTimeField(auto_now=True)), 25 | ('paid', models.BooleanField(default=False)), 26 | ], 27 | ), 28 | migrations.CreateModel( 29 | name='OrderItem', 30 | fields=[ 31 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('price', models.DecimalField(decimal_places=2, max_digits=10)), 33 | ('quantity', models.PositiveIntegerField(default=1)), 34 | ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='orders.order')), 35 | ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order_items', to='products.product')), 36 | ], 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /cart/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, get_object_or_404, redirect 2 | from products.models import Product 3 | from .models import Cart, CartItem 4 | from django.views.decorators.http import require_POST 5 | from django.http import JsonResponse 6 | 7 | @require_POST 8 | def cart_add(request, product_id): 9 | cart_id = request.session.get('cart_id') 10 | 11 | if cart_id: 12 | try: 13 | cart = Cart.objects.get(id=cart_id) 14 | except Cart.DoesNotExist: 15 | cart = Cart.objects.create() 16 | else: 17 | cart = Cart.objects.create() 18 | request.session['cart_id'] = cart.id 19 | 20 | product = get_object_or_404(Product, id=product_id) 21 | 22 | cart_item, created = CartItem.objects.get_or_create(cart=cart, product=product) 23 | 24 | if not created: 25 | cart_item.quantity += 1 26 | 27 | cart_item.save() 28 | 29 | response_data = { 30 | "success":True, 31 | "message": f'Added {product.name} to cart' 32 | } 33 | 34 | return JsonResponse(response_data) 35 | 36 | 37 | def cart_detail(request): 38 | cart_id = request.session.get('cart_id') 39 | cart=None 40 | 41 | if cart_id: 42 | cart = get_object_or_404(Cart, id=cart_id) 43 | if not cart or not cart.items.exists(): 44 | cart=None 45 | 46 | return render(request, "cart/detail.html", {"cart":cart}) 47 | 48 | def cart_remove(request, product_id): 49 | cart_id = request.session.get('cart_id') 50 | cart = get_object_or_404(Cart, id=cart_id) 51 | item = get_object_or_404(CartItem, id=product_id, cart=cart) 52 | item.delete() 53 | 54 | return redirect("cart:cart_detail") 55 | 56 | -------------------------------------------------------------------------------- /products/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.7 on 2024-08-03 17:08 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Category', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=250)), 20 | ('slug', models.SlugField(unique=True)), 21 | ], 22 | options={ 23 | 'verbose_name_plural': 'categories', 24 | }, 25 | ), 26 | migrations.CreateModel( 27 | name='Product', 28 | fields=[ 29 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 30 | ('name', models.CharField(max_length=250)), 31 | ('slug', models.SlugField(max_length=250)), 32 | ('description', models.TextField(blank=True)), 33 | ('price', models.DecimalField(decimal_places=2, max_digits=10)), 34 | ('available', models.BooleanField(default=True)), 35 | ('created', models.DateTimeField(auto_now_add=True)), 36 | ('updated', models.DateTimeField(auto_now=True)), 37 | ('image', models.ImageField(blank=True, null=True, upload_to='products')), 38 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='products.category')), 39 | ], 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /products/templates/products/product/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {%block title%}{{product.name}}{%endblock%} 4 | 5 | {%block content%} 6 |
      7 | 8 |
      9 | {%if product.image%} 10 | {{product.name}} 11 | {%else%} 12 |

      The image is not available

      13 | {%endif%} 14 |

      {{product.name}}

      15 |

      {{product.description}}

      16 |

      ${{product.price}}

      17 | 18 |
      19 | {% csrf_token %} 20 | 21 |

      22 |
      23 |
      24 | 25 | 26 | 27 |
      28 | 29 | 58 | {%endblock%} 59 | 60 | -------------------------------------------------------------------------------- /ecommercesite/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for ecommercesite project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.0.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/5.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-vw$4kjb@u+@o#ic7k+=_$__x0c#%d*u@jzk2kz1b$ind+&6-b3' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'products', 41 | 'cart', 42 | 'orders', 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | ROOT_URLCONF = 'ecommercesite.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [BASE_DIR/'templates'], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'ecommercesite.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/5.0/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.sqlite3', 82 | 'NAME': BASE_DIR / 'db.sqlite3', 83 | } 84 | } 85 | 86 | 87 | # Password validation 88 | # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators 89 | 90 | AUTH_PASSWORD_VALIDATORS = [ 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 102 | }, 103 | ] 104 | 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/5.0/topics/i18n/ 108 | 109 | LANGUAGE_CODE = 'en-us' 110 | 111 | TIME_ZONE = 'UTC' 112 | 113 | USE_I18N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/5.0/howto/static-files/ 120 | 121 | STATIC_URL = 'static/' 122 | 123 | # Default primary key field type 124 | # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field 125 | 126 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 127 | STATICFILES_DIRS = [BASE_DIR / "static",] 128 | 129 | MEDIA_URL = 'media/' 130 | STATIC_ROOT = BASE_DIR/'staticfiles' 131 | MEDIA_ROOT = BASE_DIR/'mediafiles' -------------------------------------------------------------------------------- /static/dist/styles.css: -------------------------------------------------------------------------------- 1 | *, ::before, ::after { 2 | --tw-border-spacing-x: 0; 3 | --tw-border-spacing-y: 0; 4 | --tw-translate-x: 0; 5 | --tw-translate-y: 0; 6 | --tw-rotate: 0; 7 | --tw-skew-x: 0; 8 | --tw-skew-y: 0; 9 | --tw-scale-x: 1; 10 | --tw-scale-y: 1; 11 | --tw-pan-x: ; 12 | --tw-pan-y: ; 13 | --tw-pinch-zoom: ; 14 | --tw-scroll-snap-strictness: proximity; 15 | --tw-gradient-from-position: ; 16 | --tw-gradient-via-position: ; 17 | --tw-gradient-to-position: ; 18 | --tw-ordinal: ; 19 | --tw-slashed-zero: ; 20 | --tw-numeric-figure: ; 21 | --tw-numeric-spacing: ; 22 | --tw-numeric-fraction: ; 23 | --tw-ring-inset: ; 24 | --tw-ring-offset-width: 0px; 25 | --tw-ring-offset-color: #fff; 26 | --tw-ring-color: rgb(59 130 246 / 0.5); 27 | --tw-ring-offset-shadow: 0 0 #0000; 28 | --tw-ring-shadow: 0 0 #0000; 29 | --tw-shadow: 0 0 #0000; 30 | --tw-shadow-colored: 0 0 #0000; 31 | --tw-blur: ; 32 | --tw-brightness: ; 33 | --tw-contrast: ; 34 | --tw-grayscale: ; 35 | --tw-hue-rotate: ; 36 | --tw-invert: ; 37 | --tw-saturate: ; 38 | --tw-sepia: ; 39 | --tw-drop-shadow: ; 40 | --tw-backdrop-blur: ; 41 | --tw-backdrop-brightness: ; 42 | --tw-backdrop-contrast: ; 43 | --tw-backdrop-grayscale: ; 44 | --tw-backdrop-hue-rotate: ; 45 | --tw-backdrop-invert: ; 46 | --tw-backdrop-opacity: ; 47 | --tw-backdrop-saturate: ; 48 | --tw-backdrop-sepia: ; 49 | --tw-contain-size: ; 50 | --tw-contain-layout: ; 51 | --tw-contain-paint: ; 52 | --tw-contain-style: ; 53 | } 54 | 55 | ::backdrop { 56 | --tw-border-spacing-x: 0; 57 | --tw-border-spacing-y: 0; 58 | --tw-translate-x: 0; 59 | --tw-translate-y: 0; 60 | --tw-rotate: 0; 61 | --tw-skew-x: 0; 62 | --tw-skew-y: 0; 63 | --tw-scale-x: 1; 64 | --tw-scale-y: 1; 65 | --tw-pan-x: ; 66 | --tw-pan-y: ; 67 | --tw-pinch-zoom: ; 68 | --tw-scroll-snap-strictness: proximity; 69 | --tw-gradient-from-position: ; 70 | --tw-gradient-via-position: ; 71 | --tw-gradient-to-position: ; 72 | --tw-ordinal: ; 73 | --tw-slashed-zero: ; 74 | --tw-numeric-figure: ; 75 | --tw-numeric-spacing: ; 76 | --tw-numeric-fraction: ; 77 | --tw-ring-inset: ; 78 | --tw-ring-offset-width: 0px; 79 | --tw-ring-offset-color: #fff; 80 | --tw-ring-color: rgb(59 130 246 / 0.5); 81 | --tw-ring-offset-shadow: 0 0 #0000; 82 | --tw-ring-shadow: 0 0 #0000; 83 | --tw-shadow: 0 0 #0000; 84 | --tw-shadow-colored: 0 0 #0000; 85 | --tw-blur: ; 86 | --tw-brightness: ; 87 | --tw-contrast: ; 88 | --tw-grayscale: ; 89 | --tw-hue-rotate: ; 90 | --tw-invert: ; 91 | --tw-saturate: ; 92 | --tw-sepia: ; 93 | --tw-drop-shadow: ; 94 | --tw-backdrop-blur: ; 95 | --tw-backdrop-brightness: ; 96 | --tw-backdrop-contrast: ; 97 | --tw-backdrop-grayscale: ; 98 | --tw-backdrop-hue-rotate: ; 99 | --tw-backdrop-invert: ; 100 | --tw-backdrop-opacity: ; 101 | --tw-backdrop-saturate: ; 102 | --tw-backdrop-sepia: ; 103 | --tw-contain-size: ; 104 | --tw-contain-layout: ; 105 | --tw-contain-paint: ; 106 | --tw-contain-style: ; 107 | } 108 | 109 | /* 110 | ! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com 111 | */ 112 | 113 | /* 114 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 115 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 116 | */ 117 | 118 | *, 119 | ::before, 120 | ::after { 121 | box-sizing: border-box; 122 | /* 1 */ 123 | border-width: 0; 124 | /* 2 */ 125 | border-style: solid; 126 | /* 2 */ 127 | border-color: #e5e7eb; 128 | /* 2 */ 129 | } 130 | 131 | ::before, 132 | ::after { 133 | --tw-content: ''; 134 | } 135 | 136 | /* 137 | 1. Use a consistent sensible line-height in all browsers. 138 | 2. Prevent adjustments of font size after orientation changes in iOS. 139 | 3. Use a more readable tab size. 140 | 4. Use the user's configured `sans` font-family by default. 141 | 5. Use the user's configured `sans` font-feature-settings by default. 142 | 6. Use the user's configured `sans` font-variation-settings by default. 143 | 7. Disable tap highlights on iOS 144 | */ 145 | 146 | html, 147 | :host { 148 | line-height: 1.5; 149 | /* 1 */ 150 | -webkit-text-size-adjust: 100%; 151 | /* 2 */ 152 | -moz-tab-size: 4; 153 | /* 3 */ 154 | -o-tab-size: 4; 155 | tab-size: 4; 156 | /* 3 */ 157 | font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 158 | /* 4 */ 159 | font-feature-settings: normal; 160 | /* 5 */ 161 | font-variation-settings: normal; 162 | /* 6 */ 163 | -webkit-tap-highlight-color: transparent; 164 | /* 7 */ 165 | } 166 | 167 | /* 168 | 1. Remove the margin in all browsers. 169 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 170 | */ 171 | 172 | body { 173 | margin: 0; 174 | /* 1 */ 175 | line-height: inherit; 176 | /* 2 */ 177 | } 178 | 179 | /* 180 | 1. Add the correct height in Firefox. 181 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 182 | 3. Ensure horizontal rules are visible by default. 183 | */ 184 | 185 | hr { 186 | height: 0; 187 | /* 1 */ 188 | color: inherit; 189 | /* 2 */ 190 | border-top-width: 1px; 191 | /* 3 */ 192 | } 193 | 194 | /* 195 | Add the correct text decoration in Chrome, Edge, and Safari. 196 | */ 197 | 198 | abbr:where([title]) { 199 | -webkit-text-decoration: underline dotted; 200 | text-decoration: underline dotted; 201 | } 202 | 203 | /* 204 | Remove the default font size and weight for headings. 205 | */ 206 | 207 | h1, 208 | h2, 209 | h3, 210 | h4, 211 | h5, 212 | h6 { 213 | font-size: inherit; 214 | font-weight: inherit; 215 | } 216 | 217 | /* 218 | Reset links to optimize for opt-in styling instead of opt-out. 219 | */ 220 | 221 | a { 222 | color: inherit; 223 | text-decoration: inherit; 224 | } 225 | 226 | /* 227 | Add the correct font weight in Edge and Safari. 228 | */ 229 | 230 | b, 231 | strong { 232 | font-weight: bolder; 233 | } 234 | 235 | /* 236 | 1. Use the user's configured `mono` font-family by default. 237 | 2. Use the user's configured `mono` font-feature-settings by default. 238 | 3. Use the user's configured `mono` font-variation-settings by default. 239 | 4. Correct the odd `em` font sizing in all browsers. 240 | */ 241 | 242 | code, 243 | kbd, 244 | samp, 245 | pre { 246 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 247 | /* 1 */ 248 | font-feature-settings: normal; 249 | /* 2 */ 250 | font-variation-settings: normal; 251 | /* 3 */ 252 | font-size: 1em; 253 | /* 4 */ 254 | } 255 | 256 | /* 257 | Add the correct font size in all browsers. 258 | */ 259 | 260 | small { 261 | font-size: 80%; 262 | } 263 | 264 | /* 265 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 266 | */ 267 | 268 | sub, 269 | sup { 270 | font-size: 75%; 271 | line-height: 0; 272 | position: relative; 273 | vertical-align: baseline; 274 | } 275 | 276 | sub { 277 | bottom: -0.25em; 278 | } 279 | 280 | sup { 281 | top: -0.5em; 282 | } 283 | 284 | /* 285 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 286 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 287 | 3. Remove gaps between table borders by default. 288 | */ 289 | 290 | table { 291 | text-indent: 0; 292 | /* 1 */ 293 | border-color: inherit; 294 | /* 2 */ 295 | border-collapse: collapse; 296 | /* 3 */ 297 | } 298 | 299 | /* 300 | 1. Change the font styles in all browsers. 301 | 2. Remove the margin in Firefox and Safari. 302 | 3. Remove default padding in all browsers. 303 | */ 304 | 305 | button, 306 | input, 307 | optgroup, 308 | select, 309 | textarea { 310 | font-family: inherit; 311 | /* 1 */ 312 | font-feature-settings: inherit; 313 | /* 1 */ 314 | font-variation-settings: inherit; 315 | /* 1 */ 316 | font-size: 100%; 317 | /* 1 */ 318 | font-weight: inherit; 319 | /* 1 */ 320 | line-height: inherit; 321 | /* 1 */ 322 | letter-spacing: inherit; 323 | /* 1 */ 324 | color: inherit; 325 | /* 1 */ 326 | margin: 0; 327 | /* 2 */ 328 | padding: 0; 329 | /* 3 */ 330 | } 331 | 332 | /* 333 | Remove the inheritance of text transform in Edge and Firefox. 334 | */ 335 | 336 | button, 337 | select { 338 | text-transform: none; 339 | } 340 | 341 | /* 342 | 1. Correct the inability to style clickable types in iOS and Safari. 343 | 2. Remove default button styles. 344 | */ 345 | 346 | button, 347 | input:where([type='button']), 348 | input:where([type='reset']), 349 | input:where([type='submit']) { 350 | -webkit-appearance: button; 351 | /* 1 */ 352 | background-color: transparent; 353 | /* 2 */ 354 | background-image: none; 355 | /* 2 */ 356 | } 357 | 358 | /* 359 | Use the modern Firefox focus style for all focusable elements. 360 | */ 361 | 362 | :-moz-focusring { 363 | outline: auto; 364 | } 365 | 366 | /* 367 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 368 | */ 369 | 370 | :-moz-ui-invalid { 371 | box-shadow: none; 372 | } 373 | 374 | /* 375 | Add the correct vertical alignment in Chrome and Firefox. 376 | */ 377 | 378 | progress { 379 | vertical-align: baseline; 380 | } 381 | 382 | /* 383 | Correct the cursor style of increment and decrement buttons in Safari. 384 | */ 385 | 386 | ::-webkit-inner-spin-button, 387 | ::-webkit-outer-spin-button { 388 | height: auto; 389 | } 390 | 391 | /* 392 | 1. Correct the odd appearance in Chrome and Safari. 393 | 2. Correct the outline style in Safari. 394 | */ 395 | 396 | [type='search'] { 397 | -webkit-appearance: textfield; 398 | /* 1 */ 399 | outline-offset: -2px; 400 | /* 2 */ 401 | } 402 | 403 | /* 404 | Remove the inner padding in Chrome and Safari on macOS. 405 | */ 406 | 407 | ::-webkit-search-decoration { 408 | -webkit-appearance: none; 409 | } 410 | 411 | /* 412 | 1. Correct the inability to style clickable types in iOS and Safari. 413 | 2. Change font properties to `inherit` in Safari. 414 | */ 415 | 416 | ::-webkit-file-upload-button { 417 | -webkit-appearance: button; 418 | /* 1 */ 419 | font: inherit; 420 | /* 2 */ 421 | } 422 | 423 | /* 424 | Add the correct display in Chrome and Safari. 425 | */ 426 | 427 | summary { 428 | display: list-item; 429 | } 430 | 431 | /* 432 | Removes the default spacing and border for appropriate elements. 433 | */ 434 | 435 | blockquote, 436 | dl, 437 | dd, 438 | h1, 439 | h2, 440 | h3, 441 | h4, 442 | h5, 443 | h6, 444 | hr, 445 | figure, 446 | p, 447 | pre { 448 | margin: 0; 449 | } 450 | 451 | fieldset { 452 | margin: 0; 453 | padding: 0; 454 | } 455 | 456 | legend { 457 | padding: 0; 458 | } 459 | 460 | ol, 461 | ul, 462 | menu { 463 | list-style: none; 464 | margin: 0; 465 | padding: 0; 466 | } 467 | 468 | /* 469 | Reset default styling for dialogs. 470 | */ 471 | 472 | dialog { 473 | padding: 0; 474 | } 475 | 476 | /* 477 | Prevent resizing textareas horizontally by default. 478 | */ 479 | 480 | textarea { 481 | resize: vertical; 482 | } 483 | 484 | /* 485 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 486 | 2. Set the default placeholder color to the user's configured gray 400 color. 487 | */ 488 | 489 | input::-moz-placeholder, textarea::-moz-placeholder { 490 | opacity: 1; 491 | /* 1 */ 492 | color: #9ca3af; 493 | /* 2 */ 494 | } 495 | 496 | input::placeholder, 497 | textarea::placeholder { 498 | opacity: 1; 499 | /* 1 */ 500 | color: #9ca3af; 501 | /* 2 */ 502 | } 503 | 504 | /* 505 | Set the default cursor for buttons. 506 | */ 507 | 508 | button, 509 | [role="button"] { 510 | cursor: pointer; 511 | } 512 | 513 | /* 514 | Make sure disabled buttons don't get the pointer cursor. 515 | */ 516 | 517 | :disabled { 518 | cursor: default; 519 | } 520 | 521 | /* 522 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 523 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 524 | This can trigger a poorly considered lint error in some tools but is included by design. 525 | */ 526 | 527 | img, 528 | svg, 529 | video, 530 | canvas, 531 | audio, 532 | iframe, 533 | embed, 534 | object { 535 | display: block; 536 | /* 1 */ 537 | vertical-align: middle; 538 | /* 2 */ 539 | } 540 | 541 | /* 542 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 543 | */ 544 | 545 | img, 546 | video { 547 | max-width: 100%; 548 | height: auto; 549 | } 550 | 551 | /* Make elements with the HTML hidden attribute stay hidden by default */ 552 | 553 | [hidden]:where(:not([hidden="until-found"])) { 554 | display: none; 555 | } 556 | 557 | .container { 558 | width: 100%; 559 | } 560 | 561 | @media (min-width: 640px) { 562 | .container { 563 | max-width: 640px; 564 | } 565 | } 566 | 567 | @media (min-width: 768px) { 568 | .container { 569 | max-width: 768px; 570 | } 571 | } 572 | 573 | @media (min-width: 1024px) { 574 | .container { 575 | max-width: 1024px; 576 | } 577 | } 578 | 579 | @media (min-width: 1280px) { 580 | .container { 581 | max-width: 1280px; 582 | } 583 | } 584 | 585 | @media (min-width: 1536px) { 586 | .container { 587 | max-width: 1536px; 588 | } 589 | } 590 | 591 | .static { 592 | position: static; 593 | } 594 | 595 | .mx-auto { 596 | margin-left: auto; 597 | margin-right: auto; 598 | } 599 | 600 | .mb-4 { 601 | margin-bottom: 1rem; 602 | } 603 | 604 | .mr-4 { 605 | margin-right: 1rem; 606 | } 607 | 608 | .mt-0\.5 { 609 | margin-top: 0.125rem; 610 | } 611 | 612 | .block { 613 | display: block; 614 | } 615 | 616 | .flex { 617 | display: flex; 618 | } 619 | 620 | .grid { 621 | display: grid; 622 | } 623 | 624 | .h-64 { 625 | height: 16rem; 626 | } 627 | 628 | .max-h-\[80vh\] { 629 | max-height: 80vh; 630 | } 631 | 632 | .min-h-screen { 633 | min-height: 100vh; 634 | } 635 | 636 | .w-1\/4 { 637 | width: 25%; 638 | } 639 | 640 | .w-24 { 641 | width: 6rem; 642 | } 643 | 644 | .w-3\/4 { 645 | width: 75%; 646 | } 647 | 648 | .w-44 { 649 | width: 11rem; 650 | } 651 | 652 | .w-8 { 653 | width: 2rem; 654 | } 655 | 656 | .flex-shrink { 657 | flex-shrink: 1; 658 | } 659 | 660 | .flex-grow { 661 | flex-grow: 1; 662 | } 663 | 664 | .transform { 665 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 666 | } 667 | 668 | .grid-cols-3 { 669 | grid-template-columns: repeat(3, minmax(0, 1fr)); 670 | } 671 | 672 | .flex-col { 673 | flex-direction: column; 674 | } 675 | 676 | .items-center { 677 | align-items: center; 678 | } 679 | 680 | .justify-between { 681 | justify-content: space-between; 682 | } 683 | 684 | .gap-4 { 685 | gap: 1rem; 686 | } 687 | 688 | .space-y-4 > :not([hidden]) ~ :not([hidden]) { 689 | --tw-space-y-reverse: 0; 690 | margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); 691 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); 692 | } 693 | 694 | .rounded { 695 | border-radius: 0.25rem; 696 | } 697 | 698 | .bg-gray-100 { 699 | --tw-bg-opacity: 1; 700 | background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); 701 | } 702 | 703 | .bg-indigo-600 { 704 | --tw-bg-opacity: 1; 705 | background-color: rgb(79 70 229 / var(--tw-bg-opacity, 1)); 706 | } 707 | 708 | .bg-red-600 { 709 | --tw-bg-opacity: 1; 710 | background-color: rgb(220 38 38 / var(--tw-bg-opacity, 1)); 711 | } 712 | 713 | .object-cover { 714 | -o-object-fit: cover; 715 | object-fit: cover; 716 | } 717 | 718 | .p-4 { 719 | padding: 1rem; 720 | } 721 | 722 | .px-4 { 723 | padding-left: 1rem; 724 | padding-right: 1rem; 725 | } 726 | 727 | .py-1 { 728 | padding-top: 0.25rem; 729 | padding-bottom: 0.25rem; 730 | } 731 | 732 | .py-2 { 733 | padding-top: 0.5rem; 734 | padding-bottom: 0.5rem; 735 | } 736 | 737 | .py-8 { 738 | padding-top: 2rem; 739 | padding-bottom: 2rem; 740 | } 741 | 742 | .text-center { 743 | text-align: center; 744 | } 745 | 746 | .text-2xl { 747 | font-size: 1.5rem; 748 | line-height: 2rem; 749 | } 750 | 751 | .text-lg { 752 | font-size: 1.125rem; 753 | line-height: 1.75rem; 754 | } 755 | 756 | .text-xs { 757 | font-size: 0.75rem; 758 | line-height: 1rem; 759 | } 760 | 761 | .font-bold { 762 | font-weight: 700; 763 | } 764 | 765 | .font-semibold { 766 | font-weight: 600; 767 | } 768 | 769 | .text-gray-600 { 770 | --tw-text-opacity: 1; 771 | color: rgb(75 85 99 / var(--tw-text-opacity, 1)); 772 | } 773 | 774 | .text-gray-700 { 775 | --tw-text-opacity: 1; 776 | color: rgb(55 65 81 / var(--tw-text-opacity, 1)); 777 | } 778 | 779 | .text-gray-800 { 780 | --tw-text-opacity: 1; 781 | color: rgb(31 41 55 / var(--tw-text-opacity, 1)); 782 | } 783 | 784 | .text-indigo-600 { 785 | --tw-text-opacity: 1; 786 | color: rgb(79 70 229 / var(--tw-text-opacity, 1)); 787 | } 788 | 789 | .text-white { 790 | --tw-text-opacity: 1; 791 | color: rgb(255 255 255 / var(--tw-text-opacity, 1)); 792 | } 793 | 794 | .shadow-md { 795 | --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); 796 | --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); 797 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 798 | } --------------------------------------------------------------------------------