diff --git a/media/products/Capture_décran_2025-03-17_à_17.11.10.png b/media/products/Capture_décran_2025-03-17_à_17.11.10.png new file mode 100644 index 0000000..b82c8c3 Binary files /dev/null and b/media/products/Capture_décran_2025-03-17_à_17.11.10.png differ diff --git a/media/products/IMG_4957.jpg b/media/products/IMG_4957.jpg new file mode 100644 index 0000000..3a67a20 Binary files /dev/null and b/media/products/IMG_4957.jpg differ diff --git a/media/products/IMG_4957_36DeRZt.jpg b/media/products/IMG_4957_36DeRZt.jpg new file mode 100644 index 0000000..3a67a20 Binary files /dev/null and b/media/products/IMG_4957_36DeRZt.jpg differ diff --git a/media/products/IMG_4957_gKJ3yDG.jpg b/media/products/IMG_4957_gKJ3yDG.jpg new file mode 100644 index 0000000..3a67a20 Binary files /dev/null and b/media/products/IMG_4957_gKJ3yDG.jpg differ diff --git a/media/products/IMG_4957_korPKtN.jpg b/media/products/IMG_4957_korPKtN.jpg new file mode 100644 index 0000000..3a67a20 Binary files /dev/null and b/media/products/IMG_4957_korPKtN.jpg differ diff --git a/media/products/WhatsApp_Image_2025-03-02_at_12.02.15.jpeg b/media/products/WhatsApp_Image_2025-03-02_at_12.02.15.jpeg new file mode 100644 index 0000000..8b3fdee Binary files /dev/null and b/media/products/WhatsApp_Image_2025-03-02_at_12.02.15.jpeg differ diff --git a/padelclub_backend/settings.py b/padelclub_backend/settings.py index 6184863..326ce52 100644 --- a/padelclub_backend/settings.py +++ b/padelclub_backend/settings.py @@ -15,6 +15,8 @@ import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # Quick-start development settings - unsuitable for production @@ -32,6 +34,7 @@ ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [ + 'shop', 'tournaments', # 'crm', 'django.contrib.admin', @@ -64,7 +67,7 @@ ROOT_URLCONF = 'padelclub_backend.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [os.path.join(BASE_DIR, 'templates')], # Project-level templates 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ diff --git a/shop/__init__.py b/shop/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shop/admin.py b/shop/admin.py new file mode 100644 index 0000000..ede1402 --- /dev/null +++ b/shop/admin.py @@ -0,0 +1,14 @@ +from django.contrib import admin +from .models import Product, Color, Size + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ("title", "price") + +@admin.register(Color) +class ColorAdmin(admin.ModelAdmin): + list_display = ("name",) + +@admin.register(Size) +class SizeAdmin(admin.ModelAdmin): + list_display = ("name",) diff --git a/shop/apps.py b/shop/apps.py new file mode 100644 index 0000000..1f05a2b --- /dev/null +++ b/shop/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ShopConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'shop' diff --git a/shop/cart.py b/shop/cart.py new file mode 100644 index 0000000..2df32fd --- /dev/null +++ b/shop/cart.py @@ -0,0 +1,52 @@ +from .models import CartItem, Product + +def get_or_create_cart_id(request): + """Get the cart ID from the session or create a new one""" + if 'cart_id' not in request.session: + request.session['cart_id'] = request.session.session_key or request.session.create() + return request.session['cart_id'] + +def get_cart_items(request): + """Get all cart items for the current session""" + cart_id = get_or_create_cart_id(request) + return CartItem.objects.filter(session_id=cart_id) + +def add_to_cart(request, product_id, quantity=1): + """Add a product to the cart or update its quantity""" + product = Product.objects.get(id=product_id) + cart_id = get_or_create_cart_id(request) + + try: + # Try to get existing cart item + cart_item = CartItem.objects.get(product=product, session_id=cart_id) + cart_item.quantity += quantity + cart_item.save() + except CartItem.DoesNotExist: + # Create new cart item + cart_item = CartItem.objects.create( + product=product, + quantity=quantity, + session_id=cart_id + ) + + return cart_item + +def remove_from_cart(request, product_id): + """Remove a product from the cart""" + cart_id = get_or_create_cart_id(request) + CartItem.objects.filter(product_id=product_id, session_id=cart_id).delete() + +def update_cart_item(request, product_id, quantity): + """Update the quantity of a cart item""" + cart_id = get_or_create_cart_id(request) + cart_item = CartItem.objects.get(product_id=product_id, session_id=cart_id) + + if quantity > 0: + cart_item.quantity = quantity + cart_item.save() + else: + cart_item.delete() + +def get_cart_total(request): + """Calculate the total price of all items in the cart""" + return sum(item.product.price * item.quantity for item in get_cart_items(request)) diff --git a/shop/migrations/0001_initial.py b/shop/migrations/0001_initial.py new file mode 100644 index 0000000..e1c35b8 --- /dev/null +++ b/shop/migrations/0001_initial.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.11 on 2025-03-17 17:27 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Color', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(choices=[('Red', 'Red'), ('Blue', 'Blue'), ('Green', 'Green'), ('Black', 'Black'), ('White', 'White')], max_length=10, unique=True)), + ], + ), + migrations.CreateModel( + name='Size', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(choices=[('S', 'Small'), ('M', 'Medium'), ('L', 'Large'), ('XL', 'X-Large')], max_length=5, unique=True)), + ], + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('image', models.ImageField(blank=True, null=True, upload_to='products/')), + ('price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)), + ('colors', models.ManyToManyField(blank=True, related_name='products', to='shop.color')), + ('sizes', models.ManyToManyField(blank=True, related_name='products', to='shop.size')), + ], + ), + migrations.CreateModel( + name='CartItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1)), + ('session_id', models.CharField(blank=True, max_length=255, null=True)), + ('date_added', models.DateTimeField(auto_now_add=True)), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shop.product')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/shop/migrations/0002_rename_color_productcolor_rename_size_productsize_and_more.py b/shop/migrations/0002_rename_color_productcolor_rename_size_productsize_and_more.py new file mode 100644 index 0000000..6033982 --- /dev/null +++ b/shop/migrations/0002_rename_color_productcolor_rename_size_productsize_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.11 on 2025-03-17 17:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0001_initial'), + ] + + operations = [ + migrations.RenameModel( + old_name='Color', + new_name='ProductColor', + ), + migrations.RenameModel( + old_name='Size', + new_name='ProductSize', + ), + migrations.RemoveField( + model_name='product', + name='colors', + ), + migrations.RemoveField( + model_name='product', + name='sizes', + ), + migrations.AddField( + model_name='product', + name='product_colors', + field=models.ManyToManyField(blank=True, to='shop.productcolor'), + ), + migrations.AddField( + model_name='product', + name='product_sizes', + field=models.ManyToManyField(blank=True, to='shop.productsize'), + ), + ] diff --git a/shop/migrations/0003_rename_productcolor_color_rename_productsize_size_and_more.py b/shop/migrations/0003_rename_productcolor_color_rename_productsize_size_and_more.py new file mode 100644 index 0000000..70c25ab --- /dev/null +++ b/shop/migrations/0003_rename_productcolor_color_rename_productsize_size_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.11 on 2025-03-17 17:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0002_rename_color_productcolor_rename_size_productsize_and_more'), + ] + + operations = [ + migrations.RenameModel( + old_name='ProductColor', + new_name='Color', + ), + migrations.RenameModel( + old_name='ProductSize', + new_name='Size', + ), + migrations.RemoveField( + model_name='product', + name='product_colors', + ), + migrations.RemoveField( + model_name='product', + name='product_sizes', + ), + migrations.AddField( + model_name='product', + name='colors', + field=models.ManyToManyField(blank=True, related_name='products', to='shop.color'), + ), + migrations.AddField( + model_name='product', + name='sizes', + field=models.ManyToManyField(blank=True, related_name='products', to='shop.size'), + ), + ] diff --git a/shop/migrations/__init__.py b/shop/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shop/models.py b/shop/models.py new file mode 100644 index 0000000..38ab895 --- /dev/null +++ b/shop/models.py @@ -0,0 +1,53 @@ +from django.contrib.auth.models import User +from django.db import models +from django.conf import settings + +class ColorChoices(models.TextChoices): + RED = "Red", "Red" + BLUE = "Blue", "Blue" + GREEN = "Green", "Green" + BLACK = "Black", "Black" + WHITE = "White", "White" + +class SizeChoices(models.TextChoices): + SMALL = "S", "Small" + MEDIUM = "M", "Medium" + LARGE = "L", "Large" + XLARGE = "XL", "X-Large" + +class Color(models.Model): + name = models.CharField(max_length=10, choices=ColorChoices.choices, unique=True) + + def __str__(self): + return self.name + +class Size(models.Model): + name = models.CharField(max_length=5, choices=SizeChoices.choices, unique=True) + + def __str__(self): + return self.name + +class Product(models.Model): + title = models.CharField(max_length=200) + image = models.ImageField(upload_to="products/", null=True, blank=True) + price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) + + # Use string references to prevent circular imports + colors = models.ManyToManyField("shop.Color", blank=True, related_name="products") + sizes = models.ManyToManyField("shop.Size", blank=True, related_name="products") + + def __str__(self): + return self.title + +class CartItem(models.Model): + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True) + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(default=1) + session_id = models.CharField(max_length=255, null=True, blank=True) + date_added = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.quantity} x {self.product.title}" + + def get_total_price(self): + return self.product.price * self.quantity diff --git a/shop/static/shop/css/shop.css b/shop/static/shop/css/shop.css new file mode 100644 index 0000000..ee4cde8 --- /dev/null +++ b/shop/static/shop/css/shop.css @@ -0,0 +1,37 @@ +.product-image { + width: 100%; + height: 180px; + object-fit: contain; + display: block; + margin: 0 auto; +} + +.bubble { + width: 100%; + height: 365px; + display: flex; + flex-direction: column; +} + +.bubble h3 { + margin-top: 10px; + font-size: 1.2em; + flex-grow: 1; +} + +.product-price { + font-weight: bold; + color: #f39200; + margin: 10px 0; +} + +.add-to-cart-form { + display: flex; + gap: 10px; + margin-top: auto; +} + +.quantity-input { + width: 50px; + padding: 5px; +} diff --git a/shop/templates/shop/cart.html b/shop/templates/shop/cart.html new file mode 100644 index 0000000..307285a --- /dev/null +++ b/shop/templates/shop/cart.html @@ -0,0 +1,48 @@ +{% extends 'tournaments/base.html' %} + +{% block head_title %}La boutique{% endblock %} +{% block first_title %}La boutique Padel Club{% endblock %} +{% block second_title %}Plein de goodies !{% endblock %} + +{% block content %} + + +
Votre panier est vide.
+ {% endif %} +No products available.
+{% endif %} +