diff --git a/main/settings.py b/main/settings.py index 3c0706331812a5dfdad0bf1b5d503e9e2ed88d8f..d552b4c118a63d50063b9a331edc69ea2e906196 100644 --- a/main/settings.py +++ b/main/settings.py @@ -42,7 +42,8 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'user_profile' + 'user_profile', + 'order' ] MIDDLEWARE = [ diff --git a/main/urls.py b/main/urls.py index b28f40f9efed2d9575a514541d8f425224f137a0..ddb5542fa4a0a7520405ce1f4a0581974f584ce8 100644 --- a/main/urls.py +++ b/main/urls.py @@ -22,4 +22,5 @@ urlpatterns = [ path('admin/', admin.site.urls), path("", show_home, name="home"), path('user/', include('user_profile.urls')), + path("product/", include("order.urls")), ] diff --git a/order/__init__.py b/order/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/order/admin.py b/order/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/order/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/order/apps.py b/order/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..42888e45f1201a39a346393cebb9fbb27f0d1ebe --- /dev/null +++ b/order/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class OrderConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'order' diff --git a/order/migrations/0001_initial.py b/order/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..724ac66dca26f4646d66abd2090bb22df1ed38b0 --- /dev/null +++ b/order/migrations/0001_initial.py @@ -0,0 +1,46 @@ +# Generated by Django 5.1.7 on 2025-03-30 07:19 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('stock', models.PositiveIntegerField()), + ('description', models.TextField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('total_price', models.DecimalField(decimal_places=2, default=0, max_digits=10)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='OrderDetail', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField()), + ('subtotal', models.DecimalField(decimal_places=2, max_digits=10)), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='details', to='order.order')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='order.product')), + ], + ), + ] diff --git a/order/migrations/0002_cart_cartitem_cart_products.py b/order/migrations/0002_cart_cartitem_cart_products.py new file mode 100644 index 0000000000000000000000000000000000000000..9c70bec09e24f7b9a67fae9d25eb82b95a858991 --- /dev/null +++ b/order/migrations/0002_cart_cartitem_cart_products.py @@ -0,0 +1,37 @@ +# Generated by Django 5.1.7 on 2025-03-30 07:40 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Cart', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='CartItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField()), + ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='order.cart')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='order.product')), + ], + ), + migrations.AddField( + model_name='cart', + name='products', + field=models.ManyToManyField(through='order.CartItem', to='order.product'), + ), + ] diff --git a/order/migrations/__init__.py b/order/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/order/models.py b/order/models.py new file mode 100644 index 0000000000000000000000000000000000000000..b8b830d0fd963c89df491e14a37e93fb2209ab04 --- /dev/null +++ b/order/models.py @@ -0,0 +1,43 @@ +from django.db import models +from django.contrib.auth.models import User + +class Product(models.Model): + name = models.CharField(max_length=255) + price = models.DecimalField(max_digits=10, decimal_places=2) + stock = models.PositiveIntegerField() + description = models.TextField(blank=True, null=True) + + def __str__(self): + return self.name + +class Order(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0) + + def __str__(self): + return f"Order {self.id} by {self.user.username}" + +class OrderDetail(models.Model): + order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="details") + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField() + subtotal = models.DecimalField(max_digits=10, decimal_places=2) + + def __str__(self): + return f"{self.quantity} x {self.product.name} in Order {self.order.id}" + + +class Cart(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + products = models.ManyToManyField(Product, through='CartItem') + + def __str__(self): + return f"Cart of {self.user.username}" + +class CartItem(models.Model): + cart = models.ForeignKey(Cart, on_delete=models.CASCADE) + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField() + def __str__(self): + return f"{self.quantity} x {self.product.name} in Cart of {self.cart.user.username}" \ No newline at end of file diff --git a/order/templates/product/cart.html b/order/templates/product/cart.html new file mode 100644 index 0000000000000000000000000000000000000000..3a4a24d50224feec6e2a9bf3dbc9c6c89f0f08b4 --- /dev/null +++ b/order/templates/product/cart.html @@ -0,0 +1,93 @@ +{% extends 'base.html' %} +{% block content %} + <main class="min-h-screen p-6 md:p-10 bg-gray-50 dark:bg-gray-900"> + <div class="max-w-4xl mx-auto"> + <div class="flex justify-between items-center mb-8"> + <h1 class="text-3xl font-bold text-gray-800 dark:text-white">Your Cart</h1> + <a href="{% url 'show_products' %}" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"> + Continue Shopping + </a> + </div> + + {% if cart_items %} + <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden mb-6"> + <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> + <thead class="bg-gray-50 dark:bg-gray-700"> + <tr> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Product</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Price</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Quantity</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Subtotal</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Actions</th> + </tr> + </thead> + <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"> + {% for item in cart_items %} + <tr> + <td class="px-6 py-4 whitespace-nowrap"> + <div class="flex items-center"> + <div class="flex-shrink-0 h-10 w-10 bg-gray-200 dark:bg-gray-700 rounded-full flex items-center justify-center"> + {% if item.product.image %} + <img src="{{ item.product.image.url }}" alt="{{ item.product.name }}" class="h-10 w-10 rounded-full object-cover"> + {% else %} + <span class="text-gray-500 dark:text-gray-400">📷</span> + {% endif %} + </div> + <div class="ml-4"> + <div class="text-sm font-medium text-gray-900 dark:text-white"> + {{ item.product.name }} + </div> + </div> + </div> + </td> + <td class="px-6 py-4 whitespace-nowrap"> + <div class="text-sm text-gray-900 dark:text-white">${{ item.product.price }}</div> + </td> + <td class="px-6 py-4 whitespace-nowrap"> + <div class="text-sm text-gray-900 dark:text-white">{{ item.quantity }}</div> + </td> + <td class="px-6 py-4 whitespace-nowrap"> + <div class="text-sm text-gray-900 dark:text-white">${{ item.subtotal }}</div> + </td> + <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> + <form method="post" action="{% url 'delete_cart_item' item.id %}"> + {% csrf_token %} + <button type="submit" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"> + Remove + </button> + </form> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + + <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6"> + <div class="flex justify-between items-center mb-6"> + <span class="text-lg font-semibold text-gray-800 dark:text-white">Total:</span> + <span class="text-2xl font-bold text-gray-800 dark:text-white">${{ total_price }}</span> + </div> + + <form method="post" action="{% url 'checkout' %}"> + {% csrf_token %} + <button type="submit" class="w-full py-3 px-4 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors"> + Proceed to Checkout + </button> + </form> + </div> + {% else %} + <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-10 text-center"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-gray-400 dark:text-gray-500 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" /> + </svg> + <h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-2">Your cart is empty</h2> + <p class="text-gray-600 dark:text-gray-300 mb-6">Looks like you haven't added any products to your cart yet.</p> + <a href="{% url 'show_products' %}" class="inline-block px-6 py-3 bg-indigo-600 text-white font-medium rounded-lg hover:bg-indigo-700 transition-colors"> + Start Shopping + </a> + </div> + {% endif %} + </div> + </main> +{% endblock %} \ No newline at end of file diff --git a/order/templates/product/index.html b/order/templates/product/index.html new file mode 100644 index 0000000000000000000000000000000000000000..9ff079c4ddc78bee514cf69acff60d475a2746d4 --- /dev/null +++ b/order/templates/product/index.html @@ -0,0 +1,74 @@ +{% extends 'base.html' %} +{% block content %} + <main class="min-h-screen p-6 md:p-10 bg-gray-50 dark:bg-gray-900"> + <div class="max-w-7xl mx-auto"> + <div class="flex justify-between items-center mb-8"> + <h1 class="text-3xl font-bold text-gray-800 dark:text-white">Products</h1> + <a href="{% url 'view_cart' %}" class="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" /> + </svg> + View Cart + </a> + </div> + + <section class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> + {% for product in products %} + <div class="rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300 bg-white dark:bg-gray-800 flex flex-col h-full"> + <!-- Product Image (placeholder) --> + <div class="h-48 bg-gray-200 dark:bg-gray-700 overflow-hidden"> + {% if product.image %} + <img src="{{ product.image.url }}" alt="{{ product.name }}" class="w-full h-full object-cover"> + {% else %} + <div class="w-full h-full flex items-center justify-center text-gray-400 dark:text-gray-500"> + <span class="text-4xl">📷</span> + </div> + {% endif %} + </div> + + <!-- Product Info --> + <div class="px-6 py-4 flex-grow"> + <div class="font-bold text-xl mb-2 text-gray-800 dark:text-white">{{ product.name }}</div> + <p class="text-gray-600 dark:text-gray-300 text-base mb-4">{{ product.description }}</p> + + <!-- Product Details --> + <div class="flex flex-wrap gap-2 mb-4"> + <span class="inline-block bg-blue-100 dark:bg-blue-900 rounded-full px-3 py-1 text-sm font-semibold text-blue-800 dark:text-blue-200"> + ${{ product.price }} + </span> + <span class="inline-block {% if product.stock > 0 %}bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200{% else %}bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200{% endif %} rounded-full px-3 py-1 text-sm font-semibold"> + {% if product.stock > 0 %} + In Stock: {{ product.stock }} + {% else %} + Out of Stock + {% endif %} + </span> + </div> + </div> + + <!-- Buy Button --> + <div class="px-6 pb-4"> + <form method="post" action="{% url 'add_product_to_cart' product.id %}"> + {% csrf_token %} + <button type="submit" + {% if product.stock <= 0 %}disabled{% endif %} + class="w-full py-2 px-4 rounded-md font-medium text-white bg-indigo-600 hover:bg-indigo-700 transition-colors duration-200 + {% if product.stock <= 0 %}opacity-50 cursor-not-allowed{% endif %}"> + {% if product.stock > 0 %} + Add to Cart + {% else %} + Out of Stock + {% endif %} + </button> + </form> + </div> + </div> + {% empty %} + <div class="col-span-full text-center py-10"> + <p class="text-gray-600 dark:text-gray-300 text-lg">No products available.</p> + </div> + {% endfor %} + </section> + </div> + </main> +{% endblock %} \ No newline at end of file diff --git a/order/templates/product/order_details.html b/order/templates/product/order_details.html new file mode 100644 index 0000000000000000000000000000000000000000..ed1a1ae8a9eac6d36031403db0d6bdd65b5ec6af --- /dev/null +++ b/order/templates/product/order_details.html @@ -0,0 +1,82 @@ +{% extends 'base.html' %} +{% block content %} + <main class="min-h-screen p-6 md:p-10 bg-gray-50 dark:bg-gray-900"> + <div class="max-w-4xl mx-auto"> + <div class="mb-8"> + <div class="flex items-center mb-4"> + <a href="{% url 'view_orders' %}" class="text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300 mr-2"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> + <path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" /> + </svg> + </a> + <h1 class="text-3xl font-bold text-gray-800 dark:text-white">Order #{{ order.id }}</h1> + </div> + <div class="flex flex-col sm:flex-row sm:justify-between sm:items-center"> + <p class="text-gray-600 dark:text-gray-300">Placed on {{ order.created_at|date:"F j, Y, g:i a" }}</p> + <p class="text-xl font-bold text-gray-800 dark:text-white mt-2 sm:mt-0">Total: ${{ order.total_price }}</p> + </div> + </div> + + <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden mb-6"> + <div class="px-6 py-4 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600"> + <h2 class="text-lg font-semibold text-gray-800 dark:text-white">Order Items</h2> + </div> + + <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> + <thead class="bg-gray-50 dark:bg-gray-700"> + <tr> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Product</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Price</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Quantity</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Subtotal</th> + </tr> + </thead> + <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"> + {% for item in order_details %} + <tr> + <td class="px-6 py-4 whitespace-nowrap"> + <div class="flex items-center"> + <div class="flex-shrink-0 h-10 w-10 bg-gray-200 dark:bg-gray-700 rounded-full flex items-center justify-center"> + {% if item.product.image %} + <img src="{{ item.product.image.url }}" alt="{{ item.product.name }}" class="h-10 w-10 rounded-full object-cover"> + {% else %} + <span class="text-gray-500 dark:text-gray-400">📷</span> + {% endif %} + </div> + <div class="ml-4"> + <div class="text-sm font-medium text-gray-900 dark:text-white"> + {{ item.product.name }} + </div> + </div> + </div> + </td> + <td class="px-6 py-4 whitespace-nowrap"> + <div class="text-sm text-gray-900 dark:text-white">${{ item.product.price }}</div> + </td> + <td class="px-6 py-4 whitespace-nowrap"> + <div class="text-sm text-gray-900 dark:text-white">{{ item.quantity }}</div> + </td> + <td class="px-6 py-4 whitespace-nowrap"> + <div class="text-sm text-gray-900 dark:text-white">${{ item.subtotal }}</div> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + + <div class="flex justify-between"> + <a href="{% url 'view_orders' %}" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"> + Back to Orders + </a> + + <form method="post" action="{% url 'delete_order' order.id %}" onsubmit="return confirm('Are you sure you want to delete this order?');"> + {% csrf_token %} + <button type="submit" class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"> + Delete Order + </button> + </form> + </div> + </div> + </main> +{% endblock %} \ No newline at end of file diff --git a/order/templates/product/orders.html b/order/templates/product/orders.html new file mode 100644 index 0000000000000000000000000000000000000000..a76372985ddc7f702d260282c4fc3a19bfa38b95 --- /dev/null +++ b/order/templates/product/orders.html @@ -0,0 +1,55 @@ +{% extends 'base.html' %} +{% block content %} + <main class="min-h-screen p-6 md:p-10 bg-gray-50 dark:bg-gray-900"> + <div class="max-w-4xl mx-auto"> + <div class="flex justify-between items-center mb-8"> + <h1 class="text-3xl font-bold text-gray-800 dark:text-white">Your Orders</h1> + <a href="{% url 'show_products' %}" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"> + Continue Shopping + </a> + </div> + + {% if orders %} + <div class="space-y-6"> + {% for order in orders %} + <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden"> + <div class="px-6 py-4 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 flex justify-between items-center"> + <div> + <h2 class="text-lg font-semibold text-gray-800 dark:text-white">Order #{{ order.id }}</h2> + <p class="text-sm text-gray-600 dark:text-gray-300">Placed on {{ order.created_at|date:"F j, Y, g:i a" }}</p> + </div> + <div class="text-right"> + <p class="text-lg font-bold text-gray-800 dark:text-white">${{ order.total_price }}</p> + </div> + </div> + + <div class="p-6 flex justify-between items-center"> + <a href="{% url 'view_order_details' order.id %}" class="text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300 font-medium"> + View Order Details + </a> + + <form method="post" action="{% url 'delete_order' order.id %}" onsubmit="return confirm('Are you sure you want to delete this order?');"> + {% csrf_token %} + <button type="submit" class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"> + Delete Order + </button> + </form> + </div> + </div> + {% endfor %} + </div> + {% else %} + <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-10 text-center"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-gray-400 dark:text-gray-500 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /> + </svg> + <h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-2">No orders yet</h2> + <p class="text-gray-600 dark:text-gray-300 mb-6">You haven't placed any orders yet.</p> + <a href="{% url 'show_products' %}" class="inline-block px-6 py-3 bg-indigo-600 text-white font-medium rounded-lg hover:bg-indigo-700 transition-colors"> + Start Shopping + </a> + </div> + {% endif %} + </div> + </main> +{% endblock %} \ No newline at end of file diff --git a/order/tests.py b/order/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/order/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/order/urls.py b/order/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..9a171b7f95ce646c896ef32021f063e235d32b99 --- /dev/null +++ b/order/urls.py @@ -0,0 +1,14 @@ +from django.urls import path +from .views import * + + +urlpatterns = [ + path("", show_products, name="show_products"), + path("add-to-cart/<int:product_id>/", add_product_to_cart, name="add_product_to_cart"), + path("cart/", view_cart, name="view_cart"), + path("cart/delete-item/<int:item_id>/", delete_cart_item, name="delete_cart_item"), + path("checkout/", checkout, name="checkout"), + path("orders/", view_orders, name="view_orders"), + path("orders/<int:order_id>/", view_order_details, name="view_order_details"), + path("orders/delete/<int:order_id>/", delete_order, name="delete_order"), +] \ No newline at end of file diff --git a/order/views.py b/order/views.py new file mode 100644 index 0000000000000000000000000000000000000000..6cdb46441b99de6eea12d86244aff9005cd40e8b --- /dev/null +++ b/order/views.py @@ -0,0 +1,82 @@ +from django.contrib.auth.decorators import login_required +from django.db import transaction; +from django.shortcuts import render, redirect +from .models import Product, Order, OrderDetail, Cart, CartItem +# Create your views here. +@login_required(login_url='/user/login/') # Redirects to login if not authenticated +def show_products(request): + products = Product.objects.all() + context = { + "products": products + } + return render(request, "product/index.html", context) + +@login_required(login_url='/user/login/') # Redirects to login if not authenticated +def add_product_to_cart(request, product_id): + product = Product.objects.get(id=product_id) + cart, created = Cart.objects.get_or_create(user=request.user) + cart_item, created = CartItem.objects.get_or_create(cart=cart, product=product, defaults={'quantity': 1}) + if not created: + cart_item.quantity += 1 + cart_item.save() + return redirect('show_products') + +@login_required(login_url='/user/login/') # Redirects to login if not authenticated +def view_cart(request): + cart, created = Cart.objects.get_or_create(user=request.user) + + cart_items = CartItem.objects.filter(cart=cart) + for item in cart_items: + item.subtotal = item.product.price * item.quantity + total_price = sum(item.subtotal for item in cart_items) + context = { + "cart_items": cart_items, + "total_price": total_price + } + return render(request, "product/cart.html", context) + +@login_required(login_url='/user/login/') # Redirects to login if not authenticated +def delete_cart_item(request, item_id): + cart_item = CartItem.objects.get(id=item_id) + cart_item.delete() + return redirect('view_cart') + +@login_required(login_url='/user/login/') # Redirects to login if not authenticated +def checkout(request): + # Transaction Implementation + with transaction.atomic(): + cart = request.user.cart + cart_items = CartItem.objects.filter(cart=cart) + total_price = sum(( + item.product.price * item.quantity + ) for item in cart_items) + order = Order.objects.create(user=request.user, total_price=total_price) + for item in cart_items: + subtotal = item.product.price * item.quantity + OrderDetail.objects.create(order=order, product=item.product, quantity=item.quantity, subtotal=subtotal) + cart_items.delete() + return redirect('show_products') + +@login_required(login_url='/user/login/') # Redirects to login if not authenticated +def view_orders(request): + orders = Order.objects.filter(user=request.user) + context = { + "orders": orders + } + return render(request, "product/orders.html", context) + +@login_required(login_url='/user/login/') # Redirects to login if not authenticated +def view_order_details(request, order_id): + order = Order.objects.get(id=order_id) + order_details = OrderDetail.objects.filter(order=order) + context = { + "order": order, + "order_details": order_details + } + return render(request, "product/order_details.html", context) + +@login_required(login_url='/user/login/') # Redirects to login if not authenticated +def delete_order(request, order_id): + order = Order.objects.get(id=order_id) + order.delete() + return redirect('view_orders') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c8a11c66e5f79c39501cedcb894b14c0cd732622..7e3e7b0a04f7400ff05fcc54cdaf00e7708476d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ django dotenv dj_database_url gunicorn -config \ No newline at end of file +config +faker \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index d34e131f7bed438d0494b269a079ddcb39c25aed..6987dad31efba2ca5d5d892889fee345650ac142 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,35 +1,246 @@ {% load static %} <!DOCTYPE html> -<html lang="en"> +<html lang="en" class="scroll-smooth"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <meta name="description" content="{% block meta_description %}A modern Django application{% endblock %}" /> <title> - {% block title %} - My Django App - {% endblock %} + {% block title %}My Django App{% endblock %} </title> - <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet" /> - <link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;700&display=swap" rel="stylesheet" /> + <!-- Updated to Tailwind CSS v3 --> + <script src="https://cdn.tailwindcss.com"></script> + <script> + tailwind.config = { + darkMode: 'class', + theme: { + extend: { + colors: { + primary: { + 50: '#f0f9ff', + 100: '#e0f2fe', + 200: '#bae6fd', + 300: '#7dd3fc', + 400: '#38bdf8', + 500: '#0ea5e9', + 600: '#0284c7', + 700: '#0369a1', + 800: '#075985', + 900: '#0c4a6e', + 950: '#082f49', + }, + secondary: { + 50: '#f5f3ff', + 100: '#ede9fe', + 200: '#ddd6fe', + 300: '#c4b5fd', + 400: '#a78bfa', + 500: '#8b5cf6', + 600: '#7c3aed', + 700: '#6d28d9', + 800: '#5b21b6', + 900: '#4c1d95', + 950: '#2e1065', + }, + }, + fontFamily: { + sans: ['Ubuntu', 'sans-serif'], + }, + boxShadow: { + 'inner-lg': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)', + }, + }, + }, + }; + </script> + <!-- Google Fonts --> + <link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap" rel="stylesheet" /> + <!-- Favicon --> + <link rel="icon" type="image/x-icon" href="{% static 'images/favicon.ico' %}" /> + <!-- DOMPurify --> + <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.9/purify.min.js"></script> + <!-- Custom CSS --> <style> - body { - font-family: 'Ubuntu', sans-serif; + /* Smooth scrolling and transitions */ + * { + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; + } + + /* Custom scrollbar */ + ::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + ::-webkit-scrollbar-track { + background: #f1f1f1; + } + + .dark ::-webkit-scrollbar-track { + background: #1f2937; + } + + ::-webkit-scrollbar-thumb { + background: #c5c5c5; + border-radius: 4px; + } + + .dark ::-webkit-scrollbar-thumb { + background: #4b5563; + } + + ::-webkit-scrollbar-thumb:hover { + background: #a3a3a3; + } + + .dark ::-webkit-scrollbar-thumb:hover { + background: #6b7280; + } + + /* Page transition */ + .page-transition-enter { + opacity: 0; + transform: translateY(10px); + } + + .page-transition-enter-active { + opacity: 1; + transform: translateY(0); + transition: opacity 0.3s, transform 0.3s; + } + + /* Focus styles */ + *:focus-visible { + outline: 2px solid #0ea5e9; + outline-offset: 2px; } </style> - <link rel="icon" type="image/x-icon" href="{% static 'images/favicon.ico' %}"> - <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.9/purify.min.js"></script> + {% block extra_head %}{% endblock %} </head> - <body> + <body class="bg-gray-50 text-gray-800 dark:bg-gray-900 dark:text-gray-100 flex flex-col min-h-screen"> + <!-- Theme toggle script (before content loads) --> + <script> + // Check for saved theme preference or use the system preference + if (localStorage.getItem('color-theme') === 'dark' || + (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + </script> + + <!-- Navbar --> {% include 'components/navbar.html' %} - <div class="pt-32 min-h-screen flex flex-col justify-between w-full"> + <!-- Main content --> + <main class="flex-grow pt-24 md:pt-28 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto w-full page-transition-enter page-transition-enter-active"> + {% if messages %} + <div class="messages mb-8"> + {% for message in messages %} + <div class="p-4 mb-4 rounded-lg {% if message.tags == 'error' %}bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300{% elif message.tags == 'success' %}bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300{% elif message.tags == 'warning' %}bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300{% else %}bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300{% endif %} flex items-center"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor"> + {% if message.tags == 'error' %} + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /> + {% elif message.tags == 'success' %} + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /> + {% elif message.tags == 'warning' %} + <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /> + {% else %} + <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /> + {% endif %} + </svg> + <span>{{ message }}</span> + </div> + {% endfor %} + </div> + {% endif %} + {% block content %} {% endblock %} - <footer class="bg-gray-100 text-center py-3 mt-4 w-full"> - <p>© 2025 Andrew Devito Aryo - 2306152494</p> - </footer> - </div> + </main> + + <!-- Footer --> + <footer class="mt-auto bg-white dark:bg-gray-800 shadow-inner-lg border-t border-gray-200 dark:border-gray-700"> + <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> + <div class="flex flex-col md:flex-row justify-between items-center"> + <div class="mb-4 md:mb-0"> + <p class="text-sm text-gray-600 dark:text-gray-400">© 2025 Andrew Devito Aryo - 2306152494</p> + </div> + <div class="flex space-x-6"> + <a href="#" class="text-gray-500 hover:text-primary-600 dark:text-gray-400 dark:hover:text-primary-400"> + <span class="sr-only">GitHub</span> + <svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> + <path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd" /> + </svg> + </a> + <a href="#" class="text-gray-500 hover:text-primary-600 dark:text-gray-400 dark:hover:text-primary-400"> + <span class="sr-only">LinkedIn</span> + <svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> + <path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/> + </svg> + </a> + <a href="#" class="text-gray-500 hover:text-primary-600 dark:text-gray-400 dark:hover:text-primary-400"> + <span class="sr-only">Email</span> + <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> + <path stroke-linecap="round" stroke-linejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /> + </svg> + </a> + <button id="theme-toggle" class="text-gray-500 hover:text-primary-600 dark:text-gray-400 dark:hover:text-primary-400 rounded-lg p-1"> + <span class="sr-only">Toggle dark mode</span> + <svg id="theme-toggle-dark-icon" class="hidden h-5 w-5" fill="currentColor" viewBox="0 0 20 20"> + <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path> + </svg> + <svg id="theme-toggle-light-icon" class="hidden h-5 w-5" fill="currentColor" viewBox="0 0 20 20"> + <path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path> + </svg> + </button> + </div> + </div> + <div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 text-center text-xs text-gray-500 dark:text-gray-400"> + <p>Made with ❤️ using Django and Tailwind CSS</p> + </div> + </div> + </footer> + + <!-- Theme toggle script --> + <script> + // Theme toggle functionality + const themeToggleBtn = document.getElementById('theme-toggle'); + const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon'); + const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon'); + + // Set the initial icon based on the current theme + if (localStorage.getItem('color-theme') === 'dark' || + (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + themeToggleLightIcon.classList.remove('hidden'); + } else { + themeToggleDarkIcon.classList.remove('hidden'); + } + + // Toggle theme when button is clicked + themeToggleBtn.addEventListener('click', () => { + themeToggleDarkIcon.classList.toggle('hidden'); + themeToggleLightIcon.classList.toggle('hidden'); + + if (localStorage.getItem('color-theme') === 'dark') { + document.documentElement.classList.remove('dark'); + localStorage.setItem('color-theme', 'light'); + } else { + document.documentElement.classList.add('dark'); + localStorage.setItem('color-theme', 'dark'); + } + }); + // Page transition animation + document.addEventListener('DOMContentLoaded', () => { + const mainContent = document.querySelector('main'); + if (mainContent) { + mainContent.classList.add('page-transition-enter-active'); + } + }); + </script> + + {% block extra_js %}{% endblock %} </body> -</html> +</html> \ No newline at end of file diff --git a/templates/components/navbar.html b/templates/components/navbar.html index 759f19c4abf9827449a3fe16269595591efca076..e24110d231b64eaa0f4919d3d38821abd117edf3 100644 --- a/templates/components/navbar.html +++ b/templates/components/navbar.html @@ -1,5 +1,144 @@ -<nav class="bg-gray-100 px-5 md:px-10 fixed w-full top-0 py-5"> - <div class="container flex items-center justify-between"> - <a class="text-xl font-bold" href="#">El Pekape</a> +<nav class="bg-white dark:bg-gray-800 shadow-md fixed w-full top-0 z-50 transition-all duration-300"> + <div class="container mx-auto px-4 sm:px-6 lg:px-8"> + <div class="flex justify-between h-16"> + <!-- Logo and Brand --> + <div class="flex items-center"> + <a href="#" class="flex items-center"> + <span class="text-2xl font-extrabold bg-gradient-to-r from-indigo-500 to-purple-600 text-transparent bg-clip-text hover:from-purple-600 hover:to-indigo-500 transition-all duration-300">El Pekape</span> + </a> + </div> + + <!-- Desktop Navigation --> + <div class="hidden md:flex md:items-center md:space-x-8"> + <a href="/" class="text-gray-700 dark:text-gray-200 hover:text-indigo-600 dark:hover:text-indigo-400 px-3 py-2 text-sm font-medium transition-colors duration-200 relative group"> + Home + <span class="absolute bottom-0 left-0 w-full h-0.5 bg-indigo-600 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-200"></span> + </a> + <a href="/product" class="text-gray-700 dark:text-gray-200 hover:text-indigo-600 dark:hover:text-indigo-400 px-3 py-2 text-sm font-medium transition-colors duration-200 relative group"> + Products + <span class="absolute bottom-0 left-0 w-full h-0.5 bg-indigo-600 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-200"></span> + </a> + </div> + + <!-- Search, Cart, and User Icons --> + <div class="hidden md:flex md:items-center md:space-x-4"> + <!-- Cart Button --> + <a href="/product/cart"> + <button class="p-1 rounded-full text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 focus:outline-none transition-colors duration-200 relative"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" /> + </svg> + </button> + </a> + + <!-- Theme Toggle Button --> + <button id="theme-toggle" class="p-1 rounded-full text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 focus:outline-none transition-colors duration-200"> + <svg id="theme-toggle-dark-icon" class="hidden h-6 w-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> + <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path> + </svg> + <svg id="theme-toggle-light-icon" class="hidden h-6 w-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> + <path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path> + </svg> + </button> + </div> + + <!-- Mobile menu button --> + <div class="flex md:hidden"> + <button id="mobile-menu-button" type="button" class="inline-flex items-center justify-center p-2 rounded-md text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none transition-colors duration-200" aria-controls="mobile-menu" aria-expanded="false"> + <span class="sr-only">Open main menu</span> + <svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> + </svg> + <svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> + </svg> + </button> + </div> + </div> </div> -</nav> \ No newline at end of file + + <!-- Mobile menu, show/hide based on menu state --> + <div id="mobile-menu" class="hidden md:hidden border-t border-gray-200 dark:border-gray-700"> + <div class="px-2 pt-2 pb-3 space-y-1 sm:px-3"> + <a href="/" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200">Home</a> + <a href="/products" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200">Products</a> + </div> + <div class="pt-4 pb-3 border-t border-gray-200 dark:border-gray-700"> + <div class="flex items-center px-5"> + <div class="flex-shrink-0"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> + </svg> + </div> + <div class="ml-3"> + <div class="text-base font-medium text-gray-800 dark:text-gray-200">Guest User</div> + <div class="text-sm font-medium text-gray-500 dark:text-gray-400">Sign in</div> + </div> + <button class="ml-auto p-1 rounded-full text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 focus:outline-none transition-colors duration-200"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" /> + </svg> + </button> + </div> + </div> + </div> +</nav> + +<!-- Add padding to the page content to prevent it from being hidden under the fixed navbar --> +<div class="pt-16"> + <!-- Your page content goes here --> +</div> + +<script> + // Mobile menu toggle + const mobileMenuButton = document.getElementById('mobile-menu-button'); + const mobileMenu = document.getElementById('mobile-menu'); + const menuIcon = document.getElementById('menu-icon'); + const closeIcon = document.getElementById('close-icon'); + + mobileMenuButton.addEventListener('click', () => { + mobileMenu.classList.toggle('hidden'); + menuIcon.classList.toggle('hidden'); + closeIcon.classList.toggle('hidden'); + }); + + // Theme toggle functionality + const themeToggleBtn = document.getElementById('theme-toggle'); + const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon'); + const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon'); + + // Set the initial icon based on the current theme + if (localStorage.getItem('color-theme') === 'dark' || + (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + themeToggleLightIcon.classList.remove('hidden'); + document.documentElement.classList.add('dark'); + } else { + themeToggleDarkIcon.classList.remove('hidden'); + } + + // Toggle theme when button is clicked + themeToggleBtn.addEventListener('click', () => { + themeToggleDarkIcon.classList.toggle('hidden'); + themeToggleLightIcon.classList.toggle('hidden'); + + if (localStorage.getItem('color-theme') === 'dark') { + document.documentElement.classList.remove('dark'); + localStorage.setItem('color-theme', 'light'); + } else { + document.documentElement.classList.add('dark'); + localStorage.setItem('color-theme', 'dark'); + } + }); + + // Shrink navbar on scroll + window.addEventListener('scroll', () => { + const nav = document.querySelector('nav'); + if (window.scrollY > 50) { + nav.classList.add('h-14'); + nav.classList.remove('h-16'); + } else { + nav.classList.add('h-16'); + nav.classList.remove('h-14'); + } + }); +</script> \ No newline at end of file diff --git a/user_profile/apps.py b/user_profile/apps.py index 9817557d439196e9c65a11b30d530eb1e2789a5f..17c95f22c33fc25bdecd203979d633bd8a97797f 100644 --- a/user_profile/apps.py +++ b/user_profile/apps.py @@ -21,4 +21,28 @@ class UserProfileConfig(AppConfig): User.objects.create_superuser(username, email, password) print("[[Dummy admin created: Username: admin | Password: admin123]]") else: - print("[[Admin already exists]]") \ No newline at end of file + print("[[Admin already exists]]") + + + # Seed product model + from order.models import Product + from faker import Faker + from random import randint + + fake = Faker() + + + products = Product.objects.all() + if products.exists(): + print("[[Dummy products already exist]]") + return + + for _ in range(10): + Product.objects.create( + name=fake.name(), + description=fake.text(), + price=randint(1000, 10000), + stock=randint(1, 100) + ) + + print("[[Dummy products created]]") \ No newline at end of file diff --git a/user_profile/migrations/0002_remove_userprofile_password.py b/user_profile/migrations/0002_remove_userprofile_password.py new file mode 100644 index 0000000000000000000000000000000000000000..a243fabf5c3cf9ee4dc0fddbe960beb5ea970afc --- /dev/null +++ b/user_profile/migrations/0002_remove_userprofile_password.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.7 on 2025-03-30 07:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_profile', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='userprofile', + name='password', + ), + ]