commit f0c99fd4aad13687e8abc107a03dac8c831ab813 Author: Flatlogic Bot Date: Wed May 20 02:35:48 2026 +0000 Initial import diff --git a/myproject/accounts/__init__.py b/myproject/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/accounts/__pycache__/__init__.cpython-313.pyc b/myproject/accounts/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..86749d8 Binary files /dev/null and b/myproject/accounts/__pycache__/__init__.cpython-313.pyc differ diff --git a/myproject/accounts/__pycache__/admin.cpython-313.pyc b/myproject/accounts/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000..8f65de4 Binary files /dev/null and b/myproject/accounts/__pycache__/admin.cpython-313.pyc differ diff --git a/myproject/accounts/__pycache__/apps.cpython-313.pyc b/myproject/accounts/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000..fe71eb6 Binary files /dev/null and b/myproject/accounts/__pycache__/apps.cpython-313.pyc differ diff --git a/myproject/accounts/__pycache__/forms.cpython-313.pyc b/myproject/accounts/__pycache__/forms.cpython-313.pyc new file mode 100644 index 0000000..f21ee4f Binary files /dev/null and b/myproject/accounts/__pycache__/forms.cpython-313.pyc differ diff --git a/myproject/accounts/__pycache__/models.cpython-313.pyc b/myproject/accounts/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000..73d8dba Binary files /dev/null and b/myproject/accounts/__pycache__/models.cpython-313.pyc differ diff --git a/myproject/accounts/__pycache__/tests.cpython-313.pyc b/myproject/accounts/__pycache__/tests.cpython-313.pyc new file mode 100644 index 0000000..3992d2a Binary files /dev/null and b/myproject/accounts/__pycache__/tests.cpython-313.pyc differ diff --git a/myproject/accounts/__pycache__/urls.cpython-313.pyc b/myproject/accounts/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000..f3a8aee Binary files /dev/null and b/myproject/accounts/__pycache__/urls.cpython-313.pyc differ diff --git a/myproject/accounts/__pycache__/views.cpython-313.pyc b/myproject/accounts/__pycache__/views.cpython-313.pyc new file mode 100644 index 0000000..e70d6fb Binary files /dev/null and b/myproject/accounts/__pycache__/views.cpython-313.pyc differ diff --git a/myproject/accounts/admin.py b/myproject/accounts/admin.py new file mode 100644 index 0000000..5b7afaf --- /dev/null +++ b/myproject/accounts/admin.py @@ -0,0 +1,20 @@ +from django.contrib import admin +from django.utils.html import format_html + +from accounts.models import Profile + + +@admin.register(Profile) +class ProfileAdmin(admin.ModelAdmin): + list_display = ('user', 'is_seller', 'image_preview') + + def image_preview(self, obj): + if obj.image: + return format_html( + '', + obj.image.url + ) + return "No Image" + + image_preview.short_description = 'Image' diff --git a/myproject/accounts/apps.py b/myproject/accounts/apps.py new file mode 100644 index 0000000..3cab1e0 --- /dev/null +++ b/myproject/accounts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + name = 'accounts' diff --git a/myproject/accounts/forms.py b/myproject/accounts/forms.py new file mode 100644 index 0000000..7db28e6 --- /dev/null +++ b/myproject/accounts/forms.py @@ -0,0 +1,27 @@ +from django import forms +from django.contrib.auth.models import User +from .models import Profile + + +class ProfileForm(forms.ModelForm): + first_name = forms.CharField(required=False, max_length=30) + last_name = forms.CharField(required=False, max_length=150) + email = forms.EmailField(required=False) + + class Meta: + model = Profile + fields = ['image', 'bio'] + + def save(self, commit=True): + profile = super().save(commit=False) + # update related user fields + user = profile.user + user.first_name = self.cleaned_data.get('first_name', user.first_name) + user.last_name = self.cleaned_data.get('last_name', user.last_name) + email = self.cleaned_data.get('email') + if email: + user.email = email + if commit: + user.save() + profile.save() + return profile diff --git a/myproject/accounts/migrations/0001_initial.py b/myproject/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..55da443 --- /dev/null +++ b/myproject/accounts/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 6.0.5 on 2026-05-18 11:13 + +import accounts.models +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='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('bio', models.TextField(blank=True, null=True)), + ('image', models.ImageField(blank=True, null=True, upload_to=accounts.models.user_profile_upload_path)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/myproject/accounts/migrations/0002_profile_is_seller.py b/myproject/accounts/migrations/0002_profile_is_seller.py new file mode 100644 index 0000000..298a2fe --- /dev/null +++ b/myproject/accounts/migrations/0002_profile_is_seller.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.5 on 2026-05-19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='is_seller', + field=models.BooleanField(default=False), + ), + ] diff --git a/myproject/accounts/migrations/__init__.py b/myproject/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/accounts/migrations/__pycache__/0001_initial.cpython-313.pyc b/myproject/accounts/migrations/__pycache__/0001_initial.cpython-313.pyc new file mode 100644 index 0000000..415ce95 Binary files /dev/null and b/myproject/accounts/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/myproject/accounts/migrations/__pycache__/0002_profile_is_seller.cpython-313.pyc b/myproject/accounts/migrations/__pycache__/0002_profile_is_seller.cpython-313.pyc new file mode 100644 index 0000000..5e72e8a Binary files /dev/null and b/myproject/accounts/migrations/__pycache__/0002_profile_is_seller.cpython-313.pyc differ diff --git a/myproject/accounts/migrations/__pycache__/__init__.cpython-313.pyc b/myproject/accounts/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..4469db2 Binary files /dev/null and b/myproject/accounts/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/myproject/accounts/models.py b/myproject/accounts/models.py new file mode 100644 index 0000000..d2f6e52 --- /dev/null +++ b/myproject/accounts/models.py @@ -0,0 +1,31 @@ +from django.db import models +from django.contrib.auth.models import User +from django.db.models.signals import post_save +from django.dispatch import receiver + + +def user_profile_upload_path(instance, filename): + # Files will be uploaded to MEDIA_ROOT/profile_pics/user_/ + return f'profile_pics/user_{instance.user.id}/{filename}' + + +class Profile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') + bio = models.TextField(blank=True, null=True) + image = models.ImageField(upload_to=user_profile_upload_path, blank=True, null=True) + is_seller = models.BooleanField(default=False) + + def __str__(self): + return f'Profile for {self.user.username}' + + +@receiver(post_save, sender=User) +def ensure_profile_exists(sender, instance, created, **kwargs): + if created: + Profile.objects.create(user=instance) + else: + # save existing profile to ensure any related signals run + try: + instance.profile.save() + except Exception: + pass diff --git a/myproject/accounts/tests.py b/myproject/accounts/tests.py new file mode 100644 index 0000000..de8bdc0 --- /dev/null +++ b/myproject/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/myproject/accounts/urls.py b/myproject/accounts/urls.py new file mode 100644 index 0000000..3bb6e5b --- /dev/null +++ b/myproject/accounts/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('login/', views.login_view, name='login'), + path('register/', views.register_view, name='register'), + path('logout/', views.logout_view, name='logout'), + path('profile/', views.profile_view, name='profile'), + path('profile/edit/', views.edit_profile, name='edit_profile'), +] \ No newline at end of file diff --git a/myproject/accounts/views.py b/myproject/accounts/views.py new file mode 100644 index 0000000..2bcd653 --- /dev/null +++ b/myproject/accounts/views.py @@ -0,0 +1,132 @@ +from django.contrib import messages +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.db.models import Sum +from django.shortcuts import redirect, render + +from orders.models import Order +from products.models import WishlistItem + + +def login_view(request): + if request.user.is_authenticated: + return redirect('profile') + + if request.method == 'POST': + username = request.POST.get('username', '').strip() + password = request.POST.get('password', '').strip() + + if not username or not password: + return render(request, 'accounts/login.html', {'error': 'Username and password are required'}) + + user = authenticate(request, username=username, password=password) + + if user: + login(request, user) + messages.success(request, f'Welcome back, {username}!') + return redirect('profile') + + return render(request, 'accounts/login.html', {'error': 'Invalid username or password. Please check and try again.'}) + + return render(request, 'accounts/login.html') + + +def register_view(request): + if request.user.is_authenticated: + return redirect('profile') + + if request.method == 'POST': + username = request.POST.get('username', '').strip() + password = request.POST.get('password', '').strip() + confirm_password = request.POST.get('confirm_password', '').strip() + email = request.POST.get('email', '').strip() + register_as_seller = request.POST.get('register_as_seller') == 'on' + + if not username or not password or not confirm_password: + return render(request, 'accounts/register.html', {'error': 'All fields are required', 'username': username, 'email': email, 'register_as_seller': register_as_seller}) + if len(username) < 3: + return render(request, 'accounts/register.html', {'error': 'Username must be at least 3 characters long', 'username': username, 'email': email, 'register_as_seller': register_as_seller}) + if len(password) < 6: + return render(request, 'accounts/register.html', {'error': 'Password must be at least 6 characters long', 'username': username, 'email': email, 'register_as_seller': register_as_seller}) + if password != confirm_password: + return render(request, 'accounts/register.html', {'error': 'Passwords do not match', 'username': username, 'email': email, 'register_as_seller': register_as_seller}) + if User.objects.filter(username=username).exists(): + return render(request, 'accounts/register.html', {'error': 'Username already exists', 'email': email, 'register_as_seller': register_as_seller}) + if email and User.objects.filter(email=email).exists(): + return render(request, 'accounts/register.html', {'error': 'Email already registered', 'username': username, 'register_as_seller': register_as_seller}) + + user = User.objects.create_user(username=username, password=password, email=email) + if register_as_seller: + user.profile.is_seller = True + user.profile.save(update_fields=['is_seller']) + messages.success(request, 'Account created successfully! Please log in.') + return redirect('login') + + return render( + request, + 'accounts/register.html', + {'register_as_seller': request.GET.get('seller') == '1'}, + ) + + +def logout_view(request): + logout(request) + return redirect('/') + + +@login_required +def profile_view(request): + user_orders = Order.objects.filter(user=request.user) + delivered_orders = user_orders.filter(status='Delivered') + recent_orders = user_orders.order_by('-created_at')[:5] + + total_spent = delivered_orders.aggregate(total=Sum('total_price')).get('total') or 0 + wishlist_count = WishlistItem.objects.filter(user=request.user).count() + + return render( + request, + 'accounts/profile.html', + { + 'user': request.user, + 'orders_count': user_orders.count(), + 'delivered_count': delivered_orders.count(), + 'pending_count': user_orders.exclude(status='Delivered').count(), + 'wishlist_count': wishlist_count, + 'total_spent': total_spent, + 'recent_orders': recent_orders, + }, + ) + + +@login_required +def edit_profile(request): + from .forms import ProfileForm + + profile = getattr(request.user, 'profile', None) + if profile is None: + # ensure profile exists + from .models import Profile + + profile = Profile.objects.create(user=request.user) + + if request.method == 'POST': + form = ProfileForm(request.POST, request.FILES, instance=profile) + # populate user fields into form for display/save + form.fields['first_name'].initial = request.user.first_name + form.fields['last_name'].initial = request.user.last_name + form.fields['email'].initial = request.user.email + + if form.is_valid(): + form.save() + messages.success(request, 'Profile updated successfully.') + return redirect('profile') + else: + messages.error(request, 'Please correct the errors below.') + else: + form = ProfileForm(instance=profile) + form.fields['first_name'].initial = request.user.first_name + form.fields['last_name'].initial = request.user.last_name + form.fields['email'].initial = request.user.email + + return render(request, 'accounts/edit_profile.html', {'form': form, 'profile': profile}) diff --git a/myproject/cart/__init__.py b/myproject/cart/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/cart/__pycache__/__init__.cpython-313.pyc b/myproject/cart/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..4c47eab Binary files /dev/null and b/myproject/cart/__pycache__/__init__.cpython-313.pyc differ diff --git a/myproject/cart/__pycache__/admin.cpython-313.pyc b/myproject/cart/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000..6319c73 Binary files /dev/null and b/myproject/cart/__pycache__/admin.cpython-313.pyc differ diff --git a/myproject/cart/__pycache__/apps.cpython-313.pyc b/myproject/cart/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000..81c6ba2 Binary files /dev/null and b/myproject/cart/__pycache__/apps.cpython-313.pyc differ diff --git a/myproject/cart/__pycache__/models.cpython-313.pyc b/myproject/cart/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000..593c017 Binary files /dev/null and b/myproject/cart/__pycache__/models.cpython-313.pyc differ diff --git a/myproject/cart/__pycache__/tests.cpython-313.pyc b/myproject/cart/__pycache__/tests.cpython-313.pyc new file mode 100644 index 0000000..152c00c Binary files /dev/null and b/myproject/cart/__pycache__/tests.cpython-313.pyc differ diff --git a/myproject/cart/__pycache__/urls.cpython-313.pyc b/myproject/cart/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000..2f83c67 Binary files /dev/null and b/myproject/cart/__pycache__/urls.cpython-313.pyc differ diff --git a/myproject/cart/__pycache__/views.cpython-313.pyc b/myproject/cart/__pycache__/views.cpython-313.pyc new file mode 100644 index 0000000..b1c8c2c Binary files /dev/null and b/myproject/cart/__pycache__/views.cpython-313.pyc differ diff --git a/myproject/cart/admin.py b/myproject/cart/admin.py new file mode 100644 index 0000000..e343501 --- /dev/null +++ b/myproject/cart/admin.py @@ -0,0 +1,38 @@ +from django.contrib import admin + +from .models import Cart, CartItem, Coupon + + +@admin.register(Coupon) +class CouponAdmin(admin.ModelAdmin): + list_display = ('code', 'discount_percent', 'min_purchase', 'active', 'valid_from', 'valid_to') + list_filter = ('active',) + search_fields = ('code',) + + +class CartItemInline(admin.TabularInline): + model = CartItem + extra = 0 + autocomplete_fields = ('product',) + + +@admin.register(Cart) +class CartAdmin(admin.ModelAdmin): + list_display = ('user', 'created_at', 'updated_at', 'item_count', 'total_quantity') + search_fields = ('user__username', 'user__email') + inlines = (CartItemInline,) + + def item_count(self, obj): + return obj.items.count() + item_count.short_description = 'Items' + + def total_quantity(self, obj): + return sum(i.quantity for i in obj.items.all()) + total_quantity.short_description = 'Total Qty' + + +@admin.register(CartItem) +class CartItemAdmin(admin.ModelAdmin): + list_display = ('cart', 'product', 'quantity') + search_fields = ('product__name', 'cart__user__username') + autocomplete_fields = ('product',) diff --git a/myproject/cart/apps.py b/myproject/cart/apps.py new file mode 100644 index 0000000..4b6b018 --- /dev/null +++ b/myproject/cart/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CartConfig(AppConfig): + name = 'cart' diff --git a/myproject/cart/migrations/0001_initial.py b/myproject/cart/migrations/0001_initial.py new file mode 100644 index 0000000..f3a7e61 --- /dev/null +++ b/myproject/cart/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 6.0.5 on 2026-05-18 10:08 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('products', '0004_product_updated_at'), + 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')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='cart', 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(default=1)), + ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='cart.cart')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='products.product')), + ], + options={ + 'unique_together': {('cart', 'product')}, + }, + ), + ] diff --git a/myproject/cart/migrations/0002_coupon.py b/myproject/cart/migrations/0002_coupon.py new file mode 100644 index 0000000..2227283 --- /dev/null +++ b/myproject/cart/migrations/0002_coupon.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.5 on 2026-05-18 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cart', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Coupon', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=32, unique=True)), + ('discount_percent', models.DecimalField(decimal_places=2, max_digits=5)), + ('min_purchase', models.DecimalField(decimal_places=2, default=0, max_digits=10)), + ('active', models.BooleanField(default=True)), + ('valid_from', models.DateTimeField(blank=True, null=True)), + ('valid_to', models.DateTimeField(blank=True, null=True)), + ], + options={ + 'ordering': ['code'], + }, + ), + ] diff --git a/myproject/cart/migrations/__init__.py b/myproject/cart/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/cart/migrations/__pycache__/0001_initial.cpython-313.pyc b/myproject/cart/migrations/__pycache__/0001_initial.cpython-313.pyc new file mode 100644 index 0000000..06d6b48 Binary files /dev/null and b/myproject/cart/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/myproject/cart/migrations/__pycache__/0002_coupon.cpython-313.pyc b/myproject/cart/migrations/__pycache__/0002_coupon.cpython-313.pyc new file mode 100644 index 0000000..d9f8b7a Binary files /dev/null and b/myproject/cart/migrations/__pycache__/0002_coupon.cpython-313.pyc differ diff --git a/myproject/cart/migrations/__pycache__/__init__.cpython-313.pyc b/myproject/cart/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..d702f9f Binary files /dev/null and b/myproject/cart/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/myproject/cart/models.py b/myproject/cart/models.py new file mode 100644 index 0000000..1e53195 --- /dev/null +++ b/myproject/cart/models.py @@ -0,0 +1,40 @@ +from django.contrib.auth.models import User +from django.db import models + +from products.models import Product + + +class Cart(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='cart') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"{self.user.username}'s cart" + + +class CartItem(models.Model): + cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items') + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(default=1) + + class Meta: + unique_together = ('cart', 'product') + + def __str__(self): + return f"{self.product.name} x {self.quantity}" + + +class Coupon(models.Model): + code = models.CharField(max_length=32, unique=True) + discount_percent = models.DecimalField(max_digits=5, decimal_places=2) + min_purchase = models.DecimalField(max_digits=10, decimal_places=2, default=0) + active = models.BooleanField(default=True) + valid_from = models.DateTimeField(null=True, blank=True) + valid_to = models.DateTimeField(null=True, blank=True) + + class Meta: + ordering = ['code'] + + def __str__(self): + return self.code diff --git a/myproject/cart/tests.py b/myproject/cart/tests.py new file mode 100644 index 0000000..de8bdc0 --- /dev/null +++ b/myproject/cart/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/myproject/cart/urls.py b/myproject/cart/urls.py new file mode 100644 index 0000000..39843c6 --- /dev/null +++ b/myproject/cart/urls.py @@ -0,0 +1,13 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path('', views.cart_view, name='cart'), + path('add//', views.add_to_cart, name='add_to_cart'), + path('buy-now//', views.buy_now, name='buy_now'), + path('update//', views.update_cart, name='update_cart'), + path('remove//', views.remove_from_cart, name='remove_from_cart'), + path('coupon/apply/', views.apply_coupon, name='apply_coupon'), + path('coupon/remove/', views.remove_coupon, name='remove_coupon'), +] diff --git a/myproject/cart/views.py b/myproject/cart/views.py new file mode 100644 index 0000000..1037266 --- /dev/null +++ b/myproject/cart/views.py @@ -0,0 +1,159 @@ +from decimal import Decimal + +from django.contrib import messages +from django.shortcuts import get_object_or_404, redirect, render +from django.utils import timezone + +from products.models import Product + +from .models import Coupon + + +def get_cart(request): + return request.session.get('cart', {}) + + +def save_cart(request, cart): + request.session['cart'] = cart + request.session.modified = True + + +def _redirect_after_add(request): + next_url = request.GET.get('next', '').strip() + if next_url.startswith('/'): + return redirect(next_url) + return redirect('cart') + + +def add_to_cart(request, id): + product = get_object_or_404(Product, id=id) + cart = get_cart(request) + current_qty = cart.get(str(id), 0) + + if current_qty + 1 > product.stock: + messages.warning(request, f'Only {product.stock} units of {product.name} are available.') + else: + cart[str(id)] = current_qty + 1 + messages.success(request, f'Added {product.name} to your cart.') + + save_cart(request, cart) + return _redirect_after_add(request) + + +def buy_now(request, id): + product = get_object_or_404(Product, id=id) + if product.stock <= 0: + messages.error(request, f'{product.name} is out of stock.') + return redirect('product_detail', product_id=product.id) + + request.session['buy_now'] = {str(product.id): 1} + request.session.modified = True + messages.info(request, f'Proceeding to checkout for {product.name}.') + return redirect('checkout') + + +def update_cart(request, id): + if request.method == 'POST': + qty = int(request.POST.get('quantity', 1)) + product = get_object_or_404(Product, id=id) + cart = get_cart(request) + + if qty <= 0: + cart.pop(str(id), None) + messages.info(request, f'{product.name} removed from cart.') + elif qty > product.stock: + cart[str(id)] = product.stock + messages.warning(request, f'Quantity adjusted to {product.stock} for {product.name}.') + else: + cart[str(id)] = qty + messages.success(request, f'Cart updated for {product.name}.') + + save_cart(request, cart) + + return redirect('cart') + + +def remove_from_cart(request, id): + cart = get_cart(request) + if str(id) in cart: + del cart[str(id)] + messages.info(request, 'Item removed from your cart.') + save_cart(request, cart) + return redirect('cart') + + +def _get_coupon_for_cart(code, subtotal): + if not code: + return None, Decimal('0') + + now = timezone.now() + coupon = Coupon.objects.filter(code=code, active=True).first() + if not coupon: + return None, Decimal('0') + + if coupon.valid_from and now < coupon.valid_from: + return None, Decimal('0') + if coupon.valid_to and now > coupon.valid_to: + return None, Decimal('0') + if subtotal < coupon.min_purchase: + return coupon, Decimal('0') + + discount = (subtotal * (coupon.discount_percent / Decimal('100'))).quantize(Decimal('0.01')) + return coupon, discount + + +def apply_coupon(request): + if request.method == 'POST': + code = request.POST.get('coupon_code', '').strip().upper() + request.session['coupon_code'] = code + messages.success(request, f'Coupon {code} applied.') + return redirect('cart') + + +def remove_coupon(request): + request.session.pop('coupon_code', None) + messages.info(request, 'Coupon removed.') + return redirect('cart') + + +def cart_view(request): + cart = get_cart(request) + products = [] + subtotal = Decimal('0') + + for id, qty in cart.items(): + product = get_object_or_404(Product, id=int(id)) + product.qty = qty + product.subtotal = product.display_price * qty + subtotal += product.subtotal + products.append(product) + + shipping = Decimal('60') if products else Decimal('0') + + coupon_code = request.session.get('coupon_code', '') + coupon, discount = _get_coupon_for_cart(coupon_code, subtotal) + if coupon_code and not coupon: + request.session.pop('coupon_code', None) + messages.warning(request, 'Coupon is invalid or expired.') + coupon_code = '' + + if coupon and subtotal < coupon.min_purchase: + messages.warning(request, f'Coupon requires minimum purchase of Rs. {coupon.min_purchase}.') + discount = Decimal('0') + + grand_total = subtotal - discount + shipping + + return render( + request, + 'cart/cart.html', + { + 'products': products, + 'subtotal': subtotal, + 'total': subtotal, + 'shipping': shipping, + 'discount': discount, + 'grand_total': grand_total, + 'coupon_code': coupon_code, + 'coupon': coupon, + }, + ) diff --git a/myproject/core/__init__.py b/myproject/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/core/__pycache__/__init__.cpython-313.pyc b/myproject/core/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..9f8ec5b Binary files /dev/null and b/myproject/core/__pycache__/__init__.cpython-313.pyc differ diff --git a/myproject/core/__pycache__/admin.cpython-313.pyc b/myproject/core/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000..6928691 Binary files /dev/null and b/myproject/core/__pycache__/admin.cpython-313.pyc differ diff --git a/myproject/core/__pycache__/apps.cpython-313.pyc b/myproject/core/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000..9986038 Binary files /dev/null and b/myproject/core/__pycache__/apps.cpython-313.pyc differ diff --git a/myproject/core/__pycache__/context_processors.cpython-313.pyc b/myproject/core/__pycache__/context_processors.cpython-313.pyc new file mode 100644 index 0000000..736827a Binary files /dev/null and b/myproject/core/__pycache__/context_processors.cpython-313.pyc differ diff --git a/myproject/core/__pycache__/models.cpython-313.pyc b/myproject/core/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000..ed3b3f1 Binary files /dev/null and b/myproject/core/__pycache__/models.cpython-313.pyc differ diff --git a/myproject/core/__pycache__/tests.cpython-313.pyc b/myproject/core/__pycache__/tests.cpython-313.pyc new file mode 100644 index 0000000..62e0d16 Binary files /dev/null and b/myproject/core/__pycache__/tests.cpython-313.pyc differ diff --git a/myproject/core/__pycache__/urls.cpython-313.pyc b/myproject/core/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000..5f95367 Binary files /dev/null and b/myproject/core/__pycache__/urls.cpython-313.pyc differ diff --git a/myproject/core/__pycache__/views.cpython-313.pyc b/myproject/core/__pycache__/views.cpython-313.pyc new file mode 100644 index 0000000..f89b5b3 Binary files /dev/null and b/myproject/core/__pycache__/views.cpython-313.pyc differ diff --git a/myproject/core/admin.py b/myproject/core/admin.py new file mode 100644 index 0000000..ea5d68b --- /dev/null +++ b/myproject/core/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/myproject/core/apps.py b/myproject/core/apps.py new file mode 100644 index 0000000..ae16c3e --- /dev/null +++ b/myproject/core/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + name = 'core' diff --git a/myproject/core/context_processors.py b/myproject/core/context_processors.py new file mode 100644 index 0000000..dd21107 --- /dev/null +++ b/myproject/core/context_processors.py @@ -0,0 +1,159 @@ +from orders.models import Order +from products.models import Product + +CATEGORY_ICONS = { + 'fashion': '๐Ÿ‘•', + 'mobile': '๐Ÿ“ฑ', + 'mobiles': '๐Ÿ“ฑ', + 'electronics': '๐Ÿ’ป', + 'home': '๐Ÿ›‹๏ธ', + 'appliances': '๐Ÿ“บ', + 'toys': '๐Ÿงธ', + 'beauty': '๐Ÿ’„', + 'books': '๐Ÿ“š', + 'sports': '๐Ÿ€', + 'groceries': '๐Ÿ›’', + 'health': '๐Ÿ’Š', + 'jewelry': '๐Ÿ’', + 'home decor': '๐Ÿ•ฏ๏ธ', + 'stationery': '๐Ÿ“', + 'pets': '๐Ÿพ', + 'accessories': '๐Ÿงข', + 'shoes': '๐Ÿ‘Ÿ', + 'kids': '๐Ÿง’', + 'tools': '๐Ÿ› ๏ธ', + 'office': '๐Ÿ“Ž', + 'kitchen': '๐Ÿณ', + 'travel': 'โœˆ๏ธ', + 'automotive': '๐Ÿš—', + 'garden': '๐ŸŒฟ', + 'party': '๐ŸŽ‰', +} + +DEFAULT_CATEGORIES = [ + {'value': 'fashion', 'name': 'Fashion', 'icon': '๐Ÿ‘•'}, + {'value': 'mobiles', 'name': 'Mobiles', 'icon': '๐Ÿ“ฑ'}, + {'value': 'electronics', 'name': 'Electronics', 'icon': '๐Ÿ’ป'}, + {'value': 'home', 'name': 'Home', 'icon': '๐Ÿ›‹๏ธ'}, + {'value': 'appliances', 'name': 'Appliances', 'icon': '๐Ÿ“บ'}, + {'value': 'toys', 'name': 'Toys', 'icon': '๐Ÿงธ'}, + {'value': 'beauty', 'name': 'Beauty', 'icon': '๐Ÿ’„'}, + {'value': 'books', 'name': 'Books', 'icon': '๐Ÿ“š'}, + {'value': 'sports', 'name': 'Sports', 'icon': '๐Ÿ€'}, + {'value': 'groceries', 'name': 'Groceries', 'icon': '๐Ÿ›’'}, + {'value': 'health', 'name': 'Health', 'icon': '๐Ÿ’Š'}, + {'value': 'jewelry', 'name': 'Jewelry', 'icon': '๐Ÿ’'}, + {'value': 'pets', 'name': 'Pets', 'icon': '๐Ÿพ'}, + {'value': 'shoes', 'name': 'Shoes', 'icon': '๐Ÿ‘Ÿ'}, +] + +EN_LABELS = { + 'home': 'Home', + 'products': 'Products', + 'all_products': 'All Products', + 'featured': 'Featured', + 'categories': 'Categories', + 'cart': 'Cart', + 'wishlist': 'Wishlist', + 'about': 'About', + 'support': 'Help & Support', + 'orders': 'Orders', + 'my_profile': 'My Profile', + 'my_orders': 'My Orders', + 'login': 'Login', + 'register': 'Register', + 'logout': 'Logout', + 'shop_now': 'Shop Now', + 'learn_more': 'Learn More', + 'choose_language': 'Language', + 'english': 'English', + 'nepali': 'Nepali', + 'theme_toggle': 'Light Mode', + 'for_you': 'For You', +} + +NE_LABELS = { + 'home': 'Griha', + 'products': 'Utpadan', + 'all_products': 'Sabai Utpadan', + 'featured': 'Bisesh', + 'categories': 'Shreni', + 'cart': 'Kart', + 'wishlist': 'Icchha Suchi', + 'about': 'Hamro Barema', + 'support': 'Sahayog', + 'orders': 'Orderharu', + 'my_profile': 'Mero Profile', + 'my_orders': 'Mero Order', + 'login': 'Login', + 'register': 'Darta', + 'logout': 'Logout', + 'shop_now': 'Ahile Kinmel', + 'learn_more': 'Thap Jankari', + 'choose_language': 'Bhasha', + 'for_you': 'Tapaiko Lagi', + 'english': 'English', + 'nepali': 'Nepali', + 'theme_toggle': 'Light Mode', +} + + +def cart_summary(request): + cart = request.session.get('cart', {}) + total_items = sum(cart.values()) if isinstance(cart, dict) else 0 + order_count = 0 + + if request.user.is_authenticated: + order_count = Order.objects.filter(user=request.user).count() + + return { + 'cart_count': total_items, + 'order_count': order_count, + } + + +def language_context(request): + site_language = request.session.get('site_language', 'en') + if site_language not in {'en', 'ne'}: + site_language = 'en' + + ui = NE_LABELS if site_language == 'ne' else EN_LABELS + + raw_categories = Product.objects.values_list('category', flat=True).exclude(category='').distinct() + category_items = [] + seen = set() + for raw_value in raw_categories: + cleaned = (raw_value or '').strip() + if not cleaned: + continue + key = cleaned.lower() + if key in seen: + continue + seen.add(key) + category_items.append({ + 'value': cleaned, + 'name': cleaned.title(), + 'icon': CATEGORY_ICONS.get(key, '๐Ÿ›๏ธ'), + }) + + for default_category in DEFAULT_CATEGORIES: + if default_category['value'].lower() not in seen: + category_items.append(default_category) + seen.add(default_category['value'].lower()) + + delivery_location = request.session.get('delivery_location', '').strip() + if not delivery_location and request.user.is_authenticated: + last_address = Order.objects.filter(user=request.user).exclude(address='').order_by('-created_at').values_list('address', flat=True).first() + if last_address: + delivery_location = last_address.split('\n', 1)[0].strip() + + return { + 'site_language': site_language, + 'ui': ui, + 'language_options': [ + {'code': 'en', 'label': ui['english']}, + {'code': 'ne', 'label': ui['nepali']}, + ], + 'categories': category_items, + 'delivery_location': delivery_location, + } diff --git a/myproject/core/migrations/__init__.py b/myproject/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/core/migrations/__pycache__/__init__.cpython-313.pyc b/myproject/core/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..a301b10 Binary files /dev/null and b/myproject/core/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/myproject/core/models.py b/myproject/core/models.py new file mode 100644 index 0000000..fd18c6e --- /dev/null +++ b/myproject/core/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/myproject/core/tests.py b/myproject/core/tests.py new file mode 100644 index 0000000..de8bdc0 --- /dev/null +++ b/myproject/core/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/myproject/core/urls.py b/myproject/core/urls.py new file mode 100644 index 0000000..1bc8704 --- /dev/null +++ b/myproject/core/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('', views.home, name='home'), + path('home/', views.home, name='home'), + path('about/', views.about, name='about'), + path('support/', views.support, name='support'), + path('set-language/', views.set_language_preference, name='set_language_preference'), + path('landing/', views.landing, name='landing'), + path('settings/', views.settings_view, name='settings'), +] diff --git a/myproject/core/views.py b/myproject/core/views.py new file mode 100644 index 0000000..2b9d599 --- /dev/null +++ b/myproject/core/views.py @@ -0,0 +1,139 @@ +from decimal import Decimal + +from django.db.models import Avg +from django.shortcuts import redirect, render + +from products.models import Product + + +def _format_discount_offer(product): + if not product or product.price == 0 or product.discount_price is None: + return None + savings = product.price - product.discount_price + percent = int((savings / product.price * Decimal('100')).quantize(Decimal('1'))) + return { + 'title': f'{percent}% off', + 'description': f'{product.name} now available for Rs. {product.discount_price:.0f}', + 'note': f'Save Rs. {savings:.0f} on {product.category}', + } + + +def home(request): + trending_products = Product.objects.filter(featured=True).order_by('-created_at')[:4] + if not trending_products.exists(): + trending_products = Product.objects.order_by('-created_at')[:4] + + aggregates = Product.objects.aggregate(avg_rating=Avg('rating')) + discounted_products = list(Product.objects.filter(discount_price__isnull=False).order_by('discount_price')[:6]) + best_discount = None + if discounted_products: + best_discount = max( + discounted_products, + key=lambda p: ((p.price - p.discount_price) / p.price) if p.price else Decimal('0'), + ) + + featured_count = Product.objects.filter(featured=True).count() + categories_count = Product.objects.values('category').exclude(category='').distinct().count() + + special_deals = [] + offer_deal = _format_discount_offer(best_discount) + if offer_deal: + special_deals.append(offer_deal) + + special_deals.append({ + 'title': f'{len(discounted_products)} offers live', + 'description': 'Discounts available across top Nepali categories.', + 'note': 'Browse products with extra savings today.', + }) + + if featured_count: + special_deals.append({ + 'title': 'Featured Seller Picks', + 'description': f'{featured_count} curated products from trusted sellers.', + 'note': 'Popular with Nepali shoppers.', + }) + else: + special_deals.append({ + 'title': 'Daily Deals', + 'description': 'Fresh discount offers updated every day.', + 'note': 'Check back for more savings.', + }) + + advertised_products = sorted( + discounted_products, + key=lambda p: ((p.price - p.discount_price) / p.price if p.price else Decimal('0')), + reverse=True, + )[:4] + + if not advertised_products: + advertised_products = list(trending_products[:4]) + + for index, product in enumerate(advertised_products): + savings = (product.price - product.discount_price) if product.discount_price else Decimal('0') + percent = int((savings / product.price * Decimal('100')).quantize(Decimal('1'))) if product.price else 0 + if percent >= 50: + label = 'Mega Deal' + elif percent >= 30: + label = 'Hot Deal' + elif percent >= 15: + label = 'Limited Time' + else: + label = 'Special Offer' + if product.featured and percent >= 10: + label = 'Featured Deal' + product.deal_label = label + + return render( + request, + 'core/home.html', + { + 'trending_products': trending_products, + 'total_products': Product.objects.count(), + 'featured_products': featured_count, + 'categories_count': categories_count, + 'avg_rating': aggregates.get('avg_rating') or 0, + 'special_deals': special_deals, + 'advertised_products': advertised_products, + }, + ) + + +def about(request): + return render(request, 'core/about.html') + + +def support(request): + return render(request, 'core/support.html') + + +def set_language_preference(request): + if request.method == 'POST': + language = request.POST.get('language', 'en').strip().lower() + next_url = request.POST.get('next', '/').strip() + else: + language = request.GET.get('language', 'en').strip().lower() + next_url = request.GET.get('next', '/').strip() + + if language not in {'en', 'ne'}: + language = 'en' + + request.session['site_language'] = language + if next_url.startswith('/'): + return redirect(next_url) + return redirect('home') + + +def landing(request): + return render(request, 'core/landing.html') + + +def settings_view(request): + if request.method == 'POST': + delivery_location = request.POST.get('delivery_location', '').strip() + if delivery_location: + request.session['delivery_location'] = delivery_location + else: + request.session.pop('delivery_location', None) + return redirect('settings') + + return render(request, 'core/settings.html') diff --git a/myproject/db.sqlite3 b/myproject/db.sqlite3 new file mode 100644 index 0000000..d8f4295 Binary files /dev/null and b/myproject/db.sqlite3 differ diff --git a/myproject/manage.py b/myproject/manage.py new file mode 100644 index 0000000..6a0e408 --- /dev/null +++ b/myproject/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/myproject/media/products/1000195715.jpg b/myproject/media/products/1000195715.jpg new file mode 100644 index 0000000..82aea9e Binary files /dev/null and b/myproject/media/products/1000195715.jpg differ diff --git a/myproject/media/products/16sep-apple-iphone-17-pro1.webp b/myproject/media/products/16sep-apple-iphone-17-pro1.webp new file mode 100644 index 0000000..747038c Binary files /dev/null and b/myproject/media/products/16sep-apple-iphone-17-pro1.webp differ diff --git a/myproject/media/products/69ff0b50ffa0332bf327cad91a4b1f53b2b715d2.webp b/myproject/media/products/69ff0b50ffa0332bf327cad91a4b1f53b2b715d2.webp new file mode 100644 index 0000000..859a13c Binary files /dev/null and b/myproject/media/products/69ff0b50ffa0332bf327cad91a4b1f53b2b715d2.webp differ diff --git a/myproject/media/products/69ff0b50ffa0332bf327cad91a4b1f53b2b715d2_WMZeni4.webp b/myproject/media/products/69ff0b50ffa0332bf327cad91a4b1f53b2b715d2_WMZeni4.webp new file mode 100644 index 0000000..859a13c Binary files /dev/null and b/myproject/media/products/69ff0b50ffa0332bf327cad91a4b1f53b2b715d2_WMZeni4.webp differ diff --git a/myproject/media/products/OIP.jpg b/myproject/media/products/OIP.jpg new file mode 100644 index 0000000..5cb9151 Binary files /dev/null and b/myproject/media/products/OIP.jpg differ diff --git a/myproject/media/products/balen_ches.jpg b/myproject/media/products/balen_ches.jpg new file mode 100644 index 0000000..bbb489d Binary files /dev/null and b/myproject/media/products/balen_ches.jpg differ diff --git a/myproject/media/products/c06424817_1750x1285.avif b/myproject/media/products/c06424817_1750x1285.avif new file mode 100644 index 0000000..d79ceb8 Binary files /dev/null and b/myproject/media/products/c06424817_1750x1285.avif differ diff --git a/myproject/media/products/hl_yak_you.webp b/myproject/media/products/hl_yak_you.webp new file mode 100644 index 0000000..6ff9675 Binary files /dev/null and b/myproject/media/products/hl_yak_you.webp differ diff --git a/myproject/media/products/msl53vqmf4xb1.jpg b/myproject/media/products/msl53vqmf4xb1.jpg new file mode 100644 index 0000000..79d1c90 Binary files /dev/null and b/myproject/media/products/msl53vqmf4xb1.jpg differ diff --git a/myproject/media/profile_pics/user_1/samanya2_you.webp b/myproject/media/profile_pics/user_1/samanya2_you.webp new file mode 100644 index 0000000..581390f Binary files /dev/null and b/myproject/media/profile_pics/user_1/samanya2_you.webp differ diff --git a/myproject/myproject/__init__.py b/myproject/myproject/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/myproject/__pycache__/__init__.cpython-313.pyc b/myproject/myproject/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..5c87816 Binary files /dev/null and b/myproject/myproject/__pycache__/__init__.cpython-313.pyc differ diff --git a/myproject/myproject/__pycache__/settings.cpython-313.pyc b/myproject/myproject/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000..c7267b3 Binary files /dev/null and b/myproject/myproject/__pycache__/settings.cpython-313.pyc differ diff --git a/myproject/myproject/__pycache__/urls.cpython-313.pyc b/myproject/myproject/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000..f8379e9 Binary files /dev/null and b/myproject/myproject/__pycache__/urls.cpython-313.pyc differ diff --git a/myproject/myproject/__pycache__/wsgi.cpython-313.pyc b/myproject/myproject/__pycache__/wsgi.cpython-313.pyc new file mode 100644 index 0000000..fdd81d3 Binary files /dev/null and b/myproject/myproject/__pycache__/wsgi.cpython-313.pyc differ diff --git a/myproject/myproject/asgi.py b/myproject/myproject/asgi.py new file mode 100644 index 0000000..5b53b8f --- /dev/null +++ b/myproject/myproject/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for myproject project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') + +application = get_asgi_application() diff --git a/myproject/myproject/settings.py b/myproject/myproject/settings.py new file mode 100644 index 0000000..6db70f1 --- /dev/null +++ b/myproject/myproject/settings.py @@ -0,0 +1,136 @@ +""" +Django settings for myproject project. + +Generated by 'django-admin startproject' using Django 6.0.5. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/6.0/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-%)ie75*7@1xl91^h6^$d!npm$=cf7@oqcc9b&jqxqc8t!@9uj1' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['localhost', '127.0.0.1', '[::1]', 'testserver'] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'core', # core app for home, about, contact + 'accounts', # accounts app for user profiles + 'cart', # cart app for shopping cart functionality + 'orders', # orders app for order management + 'products', # products app for product management + +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'myproject.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'tempelates'], # โœ… FIX HERE + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'core.context_processors.cart_summary', + 'core.context_processors.language_context', + ], + }, + }, +] + +WSGI_APPLICATION = 'myproject.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/6.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + +# Internationalization +# https://docs.djangoproject.com/en/6.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/6.0/howto/static-files/ + +STATIC_URL = 'static/' + +STATICFILES_DIRS = [ + BASE_DIR / "static", +] + +# Authentication Settings +LOGIN_URL = 'login' +LOGIN_REDIRECT_URL = 'profile' +LOGOUT_REDIRECT_URL = 'home' diff --git a/myproject/myproject/urls.py b/myproject/myproject/urls.py new file mode 100644 index 0000000..62ae9d2 --- /dev/null +++ b/myproject/myproject/urls.py @@ -0,0 +1,20 @@ +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + + path('admin/', admin.site.urls), + + path('', include('core.urls')), # home, about, contact + # profile system + path('accounts/', include('accounts.urls')), + path('products/', include('products.urls')), + path('cart/', include('cart.urls')), + path('orders/', include('orders.urls')), +] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS[0]) \ No newline at end of file diff --git a/myproject/myproject/wsgi.py b/myproject/myproject/wsgi.py new file mode 100644 index 0000000..ed535a3 --- /dev/null +++ b/myproject/myproject/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for myproject project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') + +application = get_wsgi_application() diff --git a/myproject/orders/__init__.py b/myproject/orders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/orders/__pycache__/__init__.cpython-313.pyc b/myproject/orders/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..3c922a9 Binary files /dev/null and b/myproject/orders/__pycache__/__init__.cpython-313.pyc differ diff --git a/myproject/orders/__pycache__/admin.cpython-313.pyc b/myproject/orders/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000..10fff3f Binary files /dev/null and b/myproject/orders/__pycache__/admin.cpython-313.pyc differ diff --git a/myproject/orders/__pycache__/apps.cpython-313.pyc b/myproject/orders/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000..db4f8c1 Binary files /dev/null and b/myproject/orders/__pycache__/apps.cpython-313.pyc differ diff --git a/myproject/orders/__pycache__/models.cpython-313.pyc b/myproject/orders/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000..72c812d Binary files /dev/null and b/myproject/orders/__pycache__/models.cpython-313.pyc differ diff --git a/myproject/orders/__pycache__/tests.cpython-313.pyc b/myproject/orders/__pycache__/tests.cpython-313.pyc new file mode 100644 index 0000000..65ae394 Binary files /dev/null and b/myproject/orders/__pycache__/tests.cpython-313.pyc differ diff --git a/myproject/orders/__pycache__/urls.cpython-313.pyc b/myproject/orders/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000..31ae816 Binary files /dev/null and b/myproject/orders/__pycache__/urls.cpython-313.pyc differ diff --git a/myproject/orders/__pycache__/views.cpython-313.pyc b/myproject/orders/__pycache__/views.cpython-313.pyc new file mode 100644 index 0000000..4aa0455 Binary files /dev/null and b/myproject/orders/__pycache__/views.cpython-313.pyc differ diff --git a/myproject/orders/admin.py b/myproject/orders/admin.py new file mode 100644 index 0000000..fc07dac --- /dev/null +++ b/myproject/orders/admin.py @@ -0,0 +1,19 @@ +from django.contrib import admin + +from .models import Order, OrderItem + + +class OrderItemInline(admin.TabularInline): + model = OrderItem + extra = 0 + + +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + list_display = ('id', 'user', 'status', 'payment_method', 'total_price', 'created_at') + list_filter = ('status', 'payment_method') + search_fields = ('user__username', 'id') + inlines = [OrderItemInline] + + +admin.site.register(OrderItem) diff --git a/myproject/orders/apps.py b/myproject/orders/apps.py new file mode 100644 index 0000000..a8a85a7 --- /dev/null +++ b/myproject/orders/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class OrdersConfig(AppConfig): + name = 'orders' diff --git a/myproject/orders/migrations/0001_initial.py b/myproject/orders/migrations/0001_initial.py new file mode 100644 index 0000000..bbeda61 --- /dev/null +++ b/myproject/orders/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# Generated by Django 6.0.5 on 2026-05-15 02:32 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('products', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + 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, max_digits=10)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='OrderItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.IntegerField()), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='orders.order')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='products.product')), + ], + ), + ] diff --git a/myproject/orders/migrations/0002_order_payment_method_order_status_and_more.py b/myproject/orders/migrations/0002_order_payment_method_order_status_and_more.py new file mode 100644 index 0000000..58ac1d3 --- /dev/null +++ b/myproject/orders/migrations/0002_order_payment_method_order_status_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.5 on 2026-05-15 10:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='payment_method', + field=models.CharField(default='COD', max_length=100), + ), + migrations.AddField( + model_name='order', + name='status', + field=models.CharField(choices=[('Pending', 'Pending'), ('Paid', 'Paid'), ('Shipped', 'Shipped'), ('Delivered', 'Delivered')], default='Pending', max_length=20), + ), + migrations.AlterField( + model_name='orderitem', + name='quantity', + field=models.PositiveIntegerField(default=1), + ), + ] diff --git a/myproject/orders/migrations/0003_alter_order_options.py b/myproject/orders/migrations/0003_alter_order_options.py new file mode 100644 index 0000000..b3c71d5 --- /dev/null +++ b/myproject/orders/migrations/0003_alter_order_options.py @@ -0,0 +1,17 @@ +# Generated by Django 6.0.5 on 2026-05-18 10:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0002_order_payment_method_order_status_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='order', + options={'ordering': ['-created_at']}, + ), + ] diff --git a/myproject/orders/migrations/0004_order_delivery_fields.py b/myproject/orders/migrations/0004_order_delivery_fields.py new file mode 100644 index 0000000..c99c73c --- /dev/null +++ b/myproject/orders/migrations/0004_order_delivery_fields.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.5 on 2026-05-19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0003_alter_order_options'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='address', + field=models.TextField(blank=True, default=''), + ), + migrations.AddField( + model_name='order', + name='full_name', + field=models.CharField(blank=True, default='', max_length=150), + ), + migrations.AddField( + model_name='order', + name='phone', + field=models.CharField(blank=True, default='', max_length=30), + ), + ] diff --git a/myproject/orders/migrations/__init__.py b/myproject/orders/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/orders/migrations/__pycache__/0001_initial.cpython-313.pyc b/myproject/orders/migrations/__pycache__/0001_initial.cpython-313.pyc new file mode 100644 index 0000000..a4757c6 Binary files /dev/null and b/myproject/orders/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/myproject/orders/migrations/__pycache__/0002_order_payment_method_order_status_and_more.cpython-313.pyc b/myproject/orders/migrations/__pycache__/0002_order_payment_method_order_status_and_more.cpython-313.pyc new file mode 100644 index 0000000..02cd04a Binary files /dev/null and b/myproject/orders/migrations/__pycache__/0002_order_payment_method_order_status_and_more.cpython-313.pyc differ diff --git a/myproject/orders/migrations/__pycache__/0003_alter_order_options.cpython-313.pyc b/myproject/orders/migrations/__pycache__/0003_alter_order_options.cpython-313.pyc new file mode 100644 index 0000000..b3ad83f Binary files /dev/null and b/myproject/orders/migrations/__pycache__/0003_alter_order_options.cpython-313.pyc differ diff --git a/myproject/orders/migrations/__pycache__/0004_order_delivery_fields.cpython-313.pyc b/myproject/orders/migrations/__pycache__/0004_order_delivery_fields.cpython-313.pyc new file mode 100644 index 0000000..793fdc7 Binary files /dev/null and b/myproject/orders/migrations/__pycache__/0004_order_delivery_fields.cpython-313.pyc differ diff --git a/myproject/orders/migrations/__pycache__/__init__.cpython-313.pyc b/myproject/orders/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..eda4e13 Binary files /dev/null and b/myproject/orders/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/myproject/orders/models.py b/myproject/orders/models.py new file mode 100644 index 0000000..fc6d7d4 --- /dev/null +++ b/myproject/orders/models.py @@ -0,0 +1,38 @@ +from django.db import models +from django.contrib.auth.models import User + +from products.models import Product + + +class Order(models.Model): + STATUS_CHOICES = ( + ('Pending', 'Pending'), + ('Paid', 'Paid'), + ('Shipped', 'Shipped'), + ('Delivered', 'Delivered'), + ) + + 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) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='Pending') + payment_method = models.CharField(max_length=100, default='COD') + full_name = models.CharField(max_length=150, blank=True, default='') + phone = models.CharField(max_length=30, blank=True, default='') + address = models.TextField(blank=True, default='') + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return f"Order #{self.id} - {self.user.username}" + + +class OrderItem(models.Model): + order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE) + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(default=1) + price = models.DecimalField(max_digits=10, decimal_places=2) + + def __str__(self): + return f"{self.quantity} x {self.product.name} @ {self.price}" diff --git a/myproject/orders/tests.py b/myproject/orders/tests.py new file mode 100644 index 0000000..de8bdc0 --- /dev/null +++ b/myproject/orders/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/myproject/orders/urls.py b/myproject/orders/urls.py new file mode 100644 index 0000000..a9ec0cc --- /dev/null +++ b/myproject/orders/urls.py @@ -0,0 +1,11 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('checkout/', views.checkout, name='checkout'), + path('payment//', views.payment_page, name='payment'), + path('payment///', views.payment_gateway, name='payment_gateway'), + path('success/', views.success, name='success'), + path('my-orders/', views.my_orders, name='my_orders'), + path('order//', views.order_detail, name='order_detail'), +] \ No newline at end of file diff --git a/myproject/orders/views.py b/myproject/orders/views.py new file mode 100644 index 0000000..031004c --- /dev/null +++ b/myproject/orders/views.py @@ -0,0 +1,271 @@ +from datetime import timedelta +from decimal import Decimal +import re + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.shortcuts import get_object_or_404, redirect, render +from django.utils import timezone + +from cart.views import get_cart, save_cart +from products.models import Product + +from .models import Order, OrderItem + + +STATUS_SEQUENCE = ['Pending', 'Paid', 'Shipped', 'Delivered'] + + +def _build_timeline(status): + current_index = STATUS_SEQUENCE.index(status) if status in STATUS_SEQUENCE else 0 + timeline = [] + for idx, step in enumerate(STATUS_SEQUENCE): + state = 'done' if idx < current_index else 'current' if idx == current_index else 'upcoming' + timeline.append({'label': step, 'state': state}) + return timeline + + +def _sync_demo_status(order): + elapsed = timezone.now() - order.created_at + if elapsed >= timedelta(minutes=3): + target = 'Delivered' + elif elapsed >= timedelta(minutes=2): + target = 'Shipped' + elif elapsed >= timedelta(minutes=1): + target = 'Paid' + else: + target = 'Pending' + + current_index = STATUS_SEQUENCE.index(order.status) if order.status in STATUS_SEQUENCE else 0 + target_index = STATUS_SEQUENCE.index(target) + if target_index > current_index: + order.status = target + order.save(update_fields=['status']) + + +def _get_checkout_source(request): + buy_now = request.session.get('buy_now', {}) + if isinstance(buy_now, dict) and buy_now: + return buy_now, 'buy_now' + return get_cart(request), 'cart' + + +def _clear_checkout_source(request, source_key): + if source_key == 'buy_now': + request.session.pop('buy_now', None) + request.session.modified = True + else: + save_cart(request, {}) + + +def _validate_phone(phone): + digits = re.sub(r'\D+', '', phone) + return 7 <= len(digits) <= 15 + + +@login_required +def checkout(request): + source_items, source_key = _get_checkout_source(request) + if not source_items: + messages.warning(request, 'Your cart is empty. Add products before checking out.') + return redirect('cart') + + cart_products = [] + total = Decimal('0') + + for id, qty in source_items.items(): + try: + product_id = int(id) + quantity = int(qty) + except (TypeError, ValueError): + continue + + if quantity <= 0: + continue + + product = Product.objects.filter(id=product_id).first() + if not product: + messages.error(request, 'One of the selected products is no longer available.') + _clear_checkout_source(request, source_key) + return redirect('cart') + + if quantity > product.stock: + if source_key == 'buy_now': + messages.error(request, f'Only {product.stock} units of {product.name} are available.') + _clear_checkout_source(request, source_key) + return redirect('product_detail', product_id=product.id) + + messages.error(request, f'Only {product.stock} units of {product.name} are available.') + return redirect('cart') + + product.qty = quantity + product.subtotal = product.display_price * quantity + total += product.subtotal + cart_products.append(product) + + if not cart_products: + messages.warning(request, 'No valid products found for checkout.') + _clear_checkout_source(request, source_key) + return redirect('cart') + + if request.method == 'POST': + full_name = request.POST.get('full_name', '').strip() + phone = request.POST.get('phone', '').strip() + address = request.POST.get('address', '').strip() + + if not full_name or not phone or not address: + messages.error(request, 'Please fill in full name, phone, and address.') + return render( + request, + 'order/checkout.html', + { + 'products': cart_products, + 'total': total, + 'delivery': { + 'full_name': full_name, + 'phone': phone, + 'address': address, + }, + }, + ) + + if not _validate_phone(phone): + messages.error(request, 'Please enter a valid phone number.') + return render( + request, + 'order/checkout.html', + { + 'products': cart_products, + 'total': total, + 'delivery': { + 'full_name': full_name, + 'phone': phone, + 'address': address, + }, + }, + ) + + order = Order.objects.create( + user=request.user, + total_price=total, + status='Pending', + full_name=full_name, + phone=phone, + address=address, + ) + + for product in cart_products: + OrderItem.objects.create(order=order, product=product, quantity=product.qty, price=product.display_price) + product.stock = max(product.stock - product.qty, 0) + product.save(update_fields=['stock']) + + _clear_checkout_source(request, source_key) + messages.success(request, 'Order created successfully. Complete payment to finish checkout.') + return redirect('payment', order_id=order.id) + + return render( + request, + 'order/checkout.html', + { + 'products': cart_products, + 'total': total, + 'delivery': { + 'full_name': request.user.get_full_name(), + 'phone': '', + 'address': '', + }, + }, + ) + + +GATEWAY_OPTIONS = { + 'esewa': { + 'name': 'eSewa', + 'description': 'Pay securely using eSewa mobile wallet or QR code.', + 'button_text': 'Pay with eSewa', + }, + 'khalti': { + 'name': 'Khalti', + 'description': 'Complete payment instantly with Khalti.', + 'button_text': 'Pay with Khalti', + }, + 'fonpay': { + 'name': 'Fonpay', + 'description': 'Use Fonpay to pay from your mobile wallet.', + 'button_text': 'Pay with Fonpay', + }, +} + + +@login_required +def payment_page(request, order_id): + order = get_object_or_404(Order, id=order_id, user=request.user) + + if order.status == 'Paid': + messages.info(request, 'This order has already been paid.') + return redirect('order_detail', order_id=order.id) + + if request.method == 'POST': + method = request.POST.get('payment') + if not method: + messages.error(request, 'Please select a payment method.') + return redirect('payment', order_id=order.id) + + method_key = method.lower().replace(' ', '') + if method_key in GATEWAY_OPTIONS: + return redirect('payment_gateway', order_id=order.id, gateway=method_key) + + if method == 'Cash on Delivery': + order.payment_method = method + order.status = 'Paid' + order.save(update_fields=['payment_method', 'status']) + messages.success(request, 'Cash on Delivery selected. Your order is confirmed.') + return redirect('success') + + messages.error(request, 'Selected payment method is not supported.') + return redirect('payment', order_id=order.id) + + return render(request, 'order/payment.html', {'order': order, 'gateways': GATEWAY_OPTIONS}) + + +@login_required +def payment_gateway(request, order_id, gateway): + order = get_object_or_404(Order, id=order_id, user=request.user) + + if order.status == 'Paid': + messages.info(request, 'This order has already been paid.') + return redirect('order_detail', order_id=order.id) + + gateway_data = GATEWAY_OPTIONS.get(gateway) + if gateway_data is None: + messages.error(request, 'Selected payment gateway is not available.') + return redirect('payment', order_id=order.id) + + if request.method == 'POST': + order.payment_method = gateway_data['name'] + order.status = 'Paid' + order.save(update_fields=['payment_method', 'status']) + messages.success(request, f'Payment completed through {gateway_data["name"]}.') + return redirect('success') + + return render(request, 'order/payment_gateway.html', {'order': order, 'gateway': gateway_data}) + + +def success(request): + return render(request, 'order/success.html') + + +@login_required +def my_orders(request): + orders = list(Order.objects.filter(user=request.user).order_by('-created_at')) + for order in orders: + _sync_demo_status(order) + return render(request, 'order/my_orders.html', {'orders': orders}) + + +@login_required +def order_detail(request, order_id): + order = get_object_or_404(Order, id=order_id, user=request.user) + _sync_demo_status(order) + timeline = _build_timeline(order.status) + return render(request, 'order/order_detail.html', {'order': order, 'timeline': timeline}) diff --git a/myproject/products/__init__.py b/myproject/products/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/products/__pycache__/__init__.cpython-313.pyc b/myproject/products/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..e0ca6f1 Binary files /dev/null and b/myproject/products/__pycache__/__init__.cpython-313.pyc differ diff --git a/myproject/products/__pycache__/admin.cpython-313.pyc b/myproject/products/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000..f842107 Binary files /dev/null and b/myproject/products/__pycache__/admin.cpython-313.pyc differ diff --git a/myproject/products/__pycache__/apps.cpython-313.pyc b/myproject/products/__pycache__/apps.cpython-313.pyc new file mode 100644 index 0000000..1f6e8f9 Binary files /dev/null and b/myproject/products/__pycache__/apps.cpython-313.pyc differ diff --git a/myproject/products/__pycache__/models.cpython-313.pyc b/myproject/products/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000..c2f9e7c Binary files /dev/null and b/myproject/products/__pycache__/models.cpython-313.pyc differ diff --git a/myproject/products/__pycache__/tests.cpython-313.pyc b/myproject/products/__pycache__/tests.cpython-313.pyc new file mode 100644 index 0000000..44f476b Binary files /dev/null and b/myproject/products/__pycache__/tests.cpython-313.pyc differ diff --git a/myproject/products/__pycache__/urls.cpython-313.pyc b/myproject/products/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000..646aad3 Binary files /dev/null and b/myproject/products/__pycache__/urls.cpython-313.pyc differ diff --git a/myproject/products/__pycache__/views.cpython-313.pyc b/myproject/products/__pycache__/views.cpython-313.pyc new file mode 100644 index 0000000..d56bbc2 Binary files /dev/null and b/myproject/products/__pycache__/views.cpython-313.pyc differ diff --git a/myproject/products/admin.py b/myproject/products/admin.py new file mode 100644 index 0000000..21cfcd8 --- /dev/null +++ b/myproject/products/admin.py @@ -0,0 +1,16 @@ +from django.contrib import admin +from .models import Product, WishlistItem + + +@admin.register(WishlistItem) +class WishlistItemAdmin(admin.ModelAdmin): + list_display = ('user', 'product', 'created_at') + search_fields = ('user__username', 'product__name') + + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ('name', 'category', 'price', 'created_at') if hasattr(Product, 'price') else ('name',) + list_filter = ('category', 'featured') + exclude = ('listing_type', 'rental_price', 'rent_duration') + search_fields = ('name', 'description') if hasattr(Product, 'description') else ('name',) diff --git a/myproject/products/apps.py b/myproject/products/apps.py new file mode 100644 index 0000000..e2e4a85 --- /dev/null +++ b/myproject/products/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ProductsConfig(AppConfig): + name = 'products' diff --git a/myproject/products/migrations/0001_initial.py b/myproject/products/migrations/0001_initial.py new file mode 100644 index 0000000..1ed67c7 --- /dev/null +++ b/myproject/products/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 6.0.5 on 2026-05-15 01:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + 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=200)), + ('description', models.TextField()), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('image', models.ImageField(upload_to='products/')), + ('stock', models.IntegerField(default=0)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/myproject/products/migrations/0002_product_fields.py b/myproject/products/migrations/0002_product_fields.py new file mode 100644 index 0000000..0c4114d --- /dev/null +++ b/myproject/products/migrations/0002_product_fields.py @@ -0,0 +1,40 @@ +from django.db import migrations, models + +CATEGORY_CHOICES = [ + ('Electronics', 'Electronics'), + ('Fashion', 'Fashion'), + ('Home', 'Home'), + ('Beauty', 'Beauty'), + ('Sports', 'Sports'), + ('General', 'General'), +] + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='category', + field=models.CharField(choices=CATEGORY_CHOICES, default='General', max_length=100), + ), + migrations.AddField( + model_name='product', + name='discount_price', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True), + ), + migrations.AddField( + model_name='product', + name='featured', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='product', + name='rating', + field=models.DecimalField(default=0, decimal_places=2, max_digits=3), + ), + ] diff --git a/myproject/products/migrations/0003_alter_product_options.py b/myproject/products/migrations/0003_alter_product_options.py new file mode 100644 index 0000000..17e649d --- /dev/null +++ b/myproject/products/migrations/0003_alter_product_options.py @@ -0,0 +1,15 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0002_product_fields'), + ] + + operations = [ + migrations.AlterModelOptions( + name='product', + options={'ordering': ['-featured', '-created_at']}, + ), + ] diff --git a/myproject/products/migrations/0004_product_updated_at.py b/myproject/products/migrations/0004_product_updated_at.py new file mode 100644 index 0000000..514c255 --- /dev/null +++ b/myproject/products/migrations/0004_product_updated_at.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.5 on 2026-05-18 10:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0003_alter_product_options'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/myproject/products/migrations/0005_wishlistitem.py b/myproject/products/migrations/0005_wishlistitem.py new file mode 100644 index 0000000..c9e52ea --- /dev/null +++ b/myproject/products/migrations/0005_wishlistitem.py @@ -0,0 +1,29 @@ +# Generated by Django 6.0.5 on 2026-05-18 10:21 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0004_product_updated_at'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='WishlistItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wishlisted_by', to='products.product')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wishlist_items', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + 'unique_together': {('user', 'product')}, + }, + ), + ] diff --git a/myproject/products/migrations/0006_add_rental_fields.py b/myproject/products/migrations/0006_add_rental_fields.py new file mode 100644 index 0000000..01e97c3 --- /dev/null +++ b/myproject/products/migrations/0006_add_rental_fields.py @@ -0,0 +1,28 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0005_wishlistitem'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='rental_price', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True), + ), + migrations.AddField( + model_name='product', + name='rent_duration', + field=models.CharField(default='per day', max_length=50, blank=True), + preserve_default=False, + ), + migrations.AddField( + model_name='product', + name='listing_type', + field=models.CharField(choices=[('rent', 'Rent'), ('sale', 'Sale')], default='rent', max_length=10), + preserve_default=False, + ), + ] diff --git a/myproject/products/migrations/0007_alter_product_category_alter_product_listing_type_and_more.py b/myproject/products/migrations/0007_alter_product_category_alter_product_listing_type_and_more.py new file mode 100644 index 0000000..d7028b2 --- /dev/null +++ b/myproject/products/migrations/0007_alter_product_category_alter_product_listing_type_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.5 on 2026-05-20 00:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0006_add_rental_fields'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='category', + field=models.CharField(choices=[('Electronics', 'Electronics'), ('Fashion', 'Fashion'), ('Home', 'Home'), ('Beauty', 'Beauty'), ('Sports', 'Sports'), ('Books', 'Books'), ('Groceries', 'Groceries'), ('Pets', 'Pets'), ('Tools', 'Tools'), ('Office', 'Office'), ('Kitchen', 'Kitchen'), ('Travel', 'Travel'), ('Automotive', 'Automotive'), ('Garden', 'Garden'), ('Party', 'Party'), ('General', 'General')], default='General', max_length=100), + ), + migrations.AlterField( + model_name='product', + name='listing_type', + field=models.CharField(choices=[('rent', 'Rent'), ('sale', 'Sale')], default='rent', max_length=10), + ), + migrations.AlterField( + model_name='product', + name='rent_duration', + field=models.CharField(blank=True, default='per day', max_length=50), + ), + ] diff --git a/myproject/products/migrations/__init__.py b/myproject/products/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/products/migrations/__pycache__/0001_initial.cpython-313.pyc b/myproject/products/migrations/__pycache__/0001_initial.cpython-313.pyc new file mode 100644 index 0000000..fd0e842 Binary files /dev/null and b/myproject/products/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/myproject/products/migrations/__pycache__/0002_product_fields.cpython-313.pyc b/myproject/products/migrations/__pycache__/0002_product_fields.cpython-313.pyc new file mode 100644 index 0000000..5dbb0f8 Binary files /dev/null and b/myproject/products/migrations/__pycache__/0002_product_fields.cpython-313.pyc differ diff --git a/myproject/products/migrations/__pycache__/0003_alter_product_options.cpython-313.pyc b/myproject/products/migrations/__pycache__/0003_alter_product_options.cpython-313.pyc new file mode 100644 index 0000000..e2f94e3 Binary files /dev/null and b/myproject/products/migrations/__pycache__/0003_alter_product_options.cpython-313.pyc differ diff --git a/myproject/products/migrations/__pycache__/0004_product_updated_at.cpython-313.pyc b/myproject/products/migrations/__pycache__/0004_product_updated_at.cpython-313.pyc new file mode 100644 index 0000000..0e4d1d8 Binary files /dev/null and b/myproject/products/migrations/__pycache__/0004_product_updated_at.cpython-313.pyc differ diff --git a/myproject/products/migrations/__pycache__/0005_wishlistitem.cpython-313.pyc b/myproject/products/migrations/__pycache__/0005_wishlistitem.cpython-313.pyc new file mode 100644 index 0000000..34cfa28 Binary files /dev/null and b/myproject/products/migrations/__pycache__/0005_wishlistitem.cpython-313.pyc differ diff --git a/myproject/products/migrations/__pycache__/0006_add_rental_fields.cpython-313.pyc b/myproject/products/migrations/__pycache__/0006_add_rental_fields.cpython-313.pyc new file mode 100644 index 0000000..2865fb7 Binary files /dev/null and b/myproject/products/migrations/__pycache__/0006_add_rental_fields.cpython-313.pyc differ diff --git a/myproject/products/migrations/__pycache__/0007_alter_product_category_alter_product_listing_type_and_more.cpython-313.pyc b/myproject/products/migrations/__pycache__/0007_alter_product_category_alter_product_listing_type_and_more.cpython-313.pyc new file mode 100644 index 0000000..ee97e49 Binary files /dev/null and b/myproject/products/migrations/__pycache__/0007_alter_product_category_alter_product_listing_type_and_more.cpython-313.pyc differ diff --git a/myproject/products/migrations/__pycache__/__init__.cpython-313.pyc b/myproject/products/migrations/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..d18396b Binary files /dev/null and b/myproject/products/migrations/__pycache__/__init__.cpython-313.pyc differ diff --git a/myproject/products/models.py b/myproject/products/models.py new file mode 100644 index 0000000..5e5eeb9 --- /dev/null +++ b/myproject/products/models.py @@ -0,0 +1,84 @@ +from django.conf import settings +from django.db import models + +CATEGORY_CHOICES = [ + ('Electronics', 'Electronics'), + ('Mobiles', 'Mobiles'), + ('Fashion', 'Fashion'), + ('Home', 'Home'), + ('Beauty', 'Beauty'), + ('Sports', 'Sports'), + ('Books', 'Books'), + ('Groceries', 'Groceries'), + ('Pets', 'Pets'), + ('Tools', 'Tools'), + ('Office', 'Office'), + ('Kitchen', 'Kitchen'), + ('Travel', 'Travel'), + ('Automotive', 'Automotive'), + ('Garden', 'Garden'), + ('Party', 'Party'), + ('General', 'General'), +] + +LISTING_TYPE_CHOICES = [ + ('rent', 'Rent'), + ('sale', 'Sale'), +] + + +class Product(models.Model): + CATEGORY_CHOICES = CATEGORY_CHOICES + LISTING_TYPE_CHOICES = LISTING_TYPE_CHOICES + + name = models.CharField(max_length=200) + description = models.TextField() + price = models.DecimalField(max_digits=10, decimal_places=2) + discount_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + rental_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + rent_duration = models.CharField(max_length=50, blank=True, default='per day') + listing_type = models.CharField(max_length=10, choices=LISTING_TYPE_CHOICES, default='rent') + image = models.ImageField(upload_to='products/') + category = models.CharField(max_length=100, choices=CATEGORY_CHOICES, default='General') + stock = models.IntegerField(default=0) + featured = models.BooleanField(default=False) + rating = models.DecimalField(max_digits=3, decimal_places=2, default=0) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['-featured', '-created_at'] + + def __str__(self): + return self.name + + @property + def display_price(self): + return self.discount_price if self.discount_price else self.price + + @property + def is_low_stock(self): + return 0 < self.stock < 5 + + @property + def savings(self): + if self.discount_price and self.discount_price < self.price: + return self.price - self.discount_price + return 0 + + @property + def available(self): + return self.stock > 0 + + +class WishlistItem(models.Model): + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='wishlist_items') + product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='wishlisted_by') + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ('user', 'product') + ordering = ['-created_at'] + + def __str__(self): + return f"{self.user} - {self.product.name}" diff --git a/myproject/products/tests.py b/myproject/products/tests.py new file mode 100644 index 0000000..de8bdc0 --- /dev/null +++ b/myproject/products/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/myproject/products/urls.py b/myproject/products/urls.py new file mode 100644 index 0000000..f8ba85e --- /dev/null +++ b/myproject/products/urls.py @@ -0,0 +1,11 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('', views.product_list, name='product_list'), + path('category//', views.product_list, name='product_category'), + path('wishlist/', views.wishlist_view, name='wishlist'), + path('wishlist/toggle//', views.toggle_wishlist, name='toggle_wishlist'), + path('wishlist/move-to-cart//', views.move_wishlist_to_cart, name='move_wishlist_to_cart'), + path('/', views.product_detail, name='product_detail'), +] diff --git a/myproject/products/views.py b/myproject/products/views.py new file mode 100644 index 0000000..64adfb6 --- /dev/null +++ b/myproject/products/views.py @@ -0,0 +1,118 @@ +from django.contrib.auth.decorators import login_required +from django.db.models import Q +from django.db.models.functions import Coalesce +from django.shortcuts import get_object_or_404, redirect, render + +from cart.views import add_to_cart +from .models import Product, WishlistItem + + +def _normalized_categories(): + categories = [choice[0] for choice in Product.CATEGORY_CHOICES] + raw_categories = Product.objects.values_list('category', flat=True).distinct() + for value in raw_categories: + cleaned = (value or '').strip() + if cleaned and cleaned not in categories: + categories.append(cleaned) + return categories + + +def product_list(request, category=None): + products = Product.objects.annotate(final_price=Coalesce('discount_price', 'price')) + query = request.GET.get('q', '').strip() + sort = request.GET.get('sort', '') + selected_category = (request.GET.get('category', category) or '').strip() + featured_raw = (request.GET.get('featured') or '').strip().lower() + selected_featured = featured_raw in {'1', 'true', 'yes'} + + if selected_category: + products = products.filter(category__iexact=selected_category) + + if selected_featured: + products = products.filter(featured=True) + + if query: + products = products.filter( + Q(name__icontains=query) + | Q(description__icontains=query) + | Q(category__icontains=query) + ) + + if sort == 'price_asc': + products = products.order_by('final_price') + elif sort == 'price_desc': + products = products.order_by('-final_price') + elif sort == 'newest': + products = products.order_by('-created_at') + elif sort == 'rating': + products = products.order_by('-rating') + else: + products = products.order_by('-featured', '-created_at') + + categories = _normalized_categories() + wishlist_ids = set() + if request.user.is_authenticated: + wishlist_ids = set(WishlistItem.objects.filter(user=request.user).values_list('product_id', flat=True)) + + return render( + request, + 'products/product_list.html', + { + 'products': products, + 'categories': categories, + 'selected_category': selected_category, + 'selected_featured': selected_featured, + 'search_query': query, + 'selected_sort': sort, + 'wishlist_ids': wishlist_ids, + 'products_count': products.count(), + 'featured_count': products.filter(featured=True).count(), + 'in_stock_count': products.filter(stock__gt=0).count(), + }, + ) + + +def product_detail(request, product_id): + product = get_object_or_404(Product, id=product_id) + related_products = Product.objects.filter(category=product.category).exclude(id=product.id)[:4] + in_wishlist = False + if request.user.is_authenticated: + in_wishlist = WishlistItem.objects.filter(user=request.user, product=product).exists() + + return render( + request, + 'products/product_details.html', + { + 'product': product, + 'related_products': related_products, + 'in_wishlist': in_wishlist, + }, + ) + + +@login_required +def toggle_wishlist(request, product_id): + product = get_object_or_404(Product, id=product_id) + item, created = WishlistItem.objects.get_or_create(user=request.user, product=product) + if not created: + item.delete() + + next_url = request.GET.get('next') or request.META.get('HTTP_REFERER') or 'product_list' + if next_url.startswith('/'): + return redirect(next_url) + return redirect('product_list') + + +@login_required +def move_wishlist_to_cart(request, product_id): + item = get_object_or_404(WishlistItem, user=request.user, product_id=product_id) + response = add_to_cart(request, product_id) + item.delete() + return response + + +@login_required +def wishlist_view(request): + wishlist_items = WishlistItem.objects.filter(user=request.user).select_related('product') + return render(request, 'products/wishlist.html', {'wishlist_items': wishlist_items}) + diff --git a/myproject/static/css/about.css b/myproject/static/css/about.css new file mode 100644 index 0000000..5d04b71 --- /dev/null +++ b/myproject/static/css/about.css @@ -0,0 +1,76 @@ +.about-hero, +.about-intro, +.mission-vision, +.values { + max-width: 1180px; + margin: 0 auto; + padding: 28px 0; +} + +.about-hero-content, +.intro-text, +.mission-vision-grid, +.values-grid { + background: #fff; + border: 1px solid #dbe4f8; + border-radius: 22px; + padding: 28px; + box-shadow: var(--shadow-soft); +} + +.about-hero { + background: linear-gradient(135deg, #eff6ff 0%, #ffffff 100%); + padding: 40px 0; +} + +.about-hero-content { + background: transparent; + border: none; + padding: 0; +} + +.about-hero h1 { font-size: clamp(2.2rem, 3vw, 3rem); margin-bottom: 10px; } +.about-subtitle { + color: #475569; + font-size: 1.05rem; + margin-bottom: 4px; +} + +.about-subtitle, +.intro-text p, +.value-card p, +.mission-box p, +.vision-box p { color: #475569; line-height: 1.75; } + +.mission-vision-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; +} + +.values-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; +} + +.value-card, +.mission-box, +.vision-box { + background: #f8fbff; + border: 1px solid #dbe4f8; + border-radius: 20px; + padding: 22px; +} + +.value-card h3, +.mission-box h2, +.vision-box h2 { + margin-bottom: 10px; + color: #0f172a; +} + +@media (max-width: 900px) { + .mission-vision-grid, + .values-grid { grid-template-columns: 1fr; } +} diff --git a/myproject/static/css/auth.css b/myproject/static/css/auth.css new file mode 100644 index 0000000..acd8f4d --- /dev/null +++ b/myproject/static/css/auth.css @@ -0,0 +1,131 @@ +.auth-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 76vh; + padding: 24px; +} + +.auth-box { + background: #fff; + border: 1px solid #dbe4f8; + border-radius: 24px; + padding: 36px; + width: 100%; + max-width: 420px; + box-shadow: 0 24px 60px rgba(15, 23, 42, 0.08); +} + +.auth-box h2 { + font-size: 2rem; + margin-bottom: 18px; + color: #0f172a; + text-align: center; +} + +.auth-form { + margin-top: 24px; + display: grid; + gap: 18px; +} + +.form-group { + display: grid; + gap: 8px; +} + +.form-group label { + font-weight: 700; + color: #334155; +} + +.form-group input { + width: 100%; + padding: 14px 16px; + border: 1px solid #cbd5e1; + border-radius: 14px; + background: #f8fbff; + color: #0f172a; + font-size: 1rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.form-group input:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.12); +} + +.form-group input::placeholder { + color: #94a3b8; +} + +.form-group small { + color: #64748b; + font-size: 0.85rem; +} + +.btn-primary { + width: 100%; + padding: 14px; + background: linear-gradient(135deg, #2874f0 0%, #3b82f6 100%); + color: #fff; + border: none; + border-radius: 14px; + font-weight: 700; + font-size: 1rem; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; + margin-top: 10px; +} + +.btn-primary:hover { + transform: translateY(-1px); + box-shadow: 0 18px 30px rgba(40, 116, 240, 0.24); +} + +.alert { + padding: 14px 16px; + border-radius: 14px; + margin-bottom: 12px; + font-weight: 500; +} + +.alert-success { + background: #ecfdf5; + border: 1px solid #34d399; + color: #065f46; +} + +.alert-error { + background: #fef2f2; + border: 1px solid #ef4444; + color: #b91c1c; +} + +.auth-link { + text-align: center; + margin-top: 18px; + color: #475569; +} + +.auth-link a { + color: #2563eb; + text-decoration: none; + font-weight: 700; +} + +.auth-link a:hover { + text-decoration: underline; +} + +/* Responsive */ +@media (max-width: 480px) { + .auth-box { + padding: 24px; + } + + .auth-box h2 { + font-size: 1.6rem; + } +} diff --git a/myproject/static/css/footer.css b/myproject/static/css/footer.css new file mode 100644 index 0000000..9baa6a8 --- /dev/null +++ b/myproject/static/css/footer.css @@ -0,0 +1,133 @@ +.footer { + background: #fff; + color: var(--text); + padding: 60px 24px 20px; + box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.03); + border-top: none; + margin-top: 40px; +} + +.footer-grid { + display: grid; + grid-template-columns: 2fr 1fr 1fr 1.5fr; + gap: 40px; + max-width: 1320px; + margin: 0 auto; + padding-bottom: 40px; + border-bottom: 1px solid var(--border); +} + +.footer-brand .brand-text { + margin-top: 16px; + color: var(--muted); + font-size: 0.95rem; + line-height: 1.6; + max-width: 300px; +} + +.footer-logo-wrapper { + display: flex; + align-items: center; + gap: 12px; +} + +.footer-logo-wrapper .logo-mark { + width: 40px; + height: 40px; + display: grid; + place-items: center; + border-radius: 12px; + background: linear-gradient(135deg, var(--accent), var(--accent-2)); + color: #fff; + font-weight: 800; +} + +.footer-logo-wrapper strong { + font-size: 1.2rem; + color: var(--text); +} + +.footer-links h4, .footer-newsletter h4 { + font-size: 1.1rem; + color: var(--text); + margin-bottom: 20px; + font-weight: 700; +} + +.footer-nav { + display: flex; + flex-direction: column; + gap: 12px; +} + +.footer-nav a { + color: var(--muted); + font-size: 0.95rem; + transition: all 0.3s ease; + display: inline-block; + width: fit-content; +} + +.footer-nav a:hover { + color: var(--accent); + transform: translateX(4px); +} + +.footer-newsletter p { + color: var(--muted); + font-size: 0.95rem; + margin-bottom: 16px; +} + +.newsletter-form { + display: flex; + gap: 8px; +} + +.newsletter-form input { + flex: 1; + padding: 12px 16px; + border: 1px solid var(--border); + border-radius: 8px; + outline: none; + transition: border-color 0.3s ease; +} + +.newsletter-form input:focus { + border-color: var(--accent); +} + +.newsletter-form button { + background: var(--accent); + color: #fff; + border: none; + border-radius: 8px; + padding: 0 16px; + cursor: pointer; + font-weight: bold; + transition: background 0.3s ease; +} + +.newsletter-form button:hover { + background: var(--accent-2); +} + +.footer-bottom { + text-align: center; + padding-top: 24px; + color: var(--muted); + font-size: 0.9rem; +} + +@media (max-width: 1024px) { + .footer-grid { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 640px) { + .footer-grid { + grid-template-columns: 1fr; + gap: 30px; + } +} diff --git a/myproject/static/css/home.css b/myproject/static/css/home.css new file mode 100644 index 0000000..4f934de --- /dev/null +++ b/myproject/static/css/home.css @@ -0,0 +1,276 @@ +.hero { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(280px, 0.8fr); + gap: 26px; + background: linear-gradient(135deg, #2874f0 0%, #3b69d4 55%, #5ea3ff 100%); + border-radius: 28px; + padding: 40px; + color: #fff; + overflow: hidden; +} + +.hero-copy { + display: grid; + gap: 18px; +} + +.hero-highlights { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; +} + +.highlight-card { + background: rgba(255,255,255,0.14); + border: 1px solid rgba(255,255,255,0.18); + border-radius: 22px; + padding: 22px; + min-height: 120px; + display: grid; + gap: 10px; +} + +.highlight-card p { + color: rgba(255,255,255,0.88); +} + +.highlight-title { + font-size: 1rem; + font-weight: 800; + letter-spacing: 0.02em; +} + +.eyebrow { + color: #dbe9ff; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + font-size: 0.86rem; +} + +.hero-title { + font-size: clamp(2.4rem, 4.2vw, 3.8rem); + line-height: 1.05; +} + +.hero-description { + max-width: 660px; + color: rgba(255,255,255,0.92); + font-size: 1.05rem; + line-height: 1.8; +} + +.hero-buttons { + display: flex; + gap: 14px; + flex-wrap: wrap; +} + +.hero-highlights { + display: grid; + gap: 14px; +} + +.highlight-card { + background: rgba(255,255,255,0.12); + border: 1px solid rgba(255,255,255,0.18); + border-radius: 24px; + padding: 24px; + min-height: 120px; + display: grid; + gap: 10px; +} + +.highlight-title { + font-size: 1.1rem; + font-weight: 700; +} + +.highlight-card p { + color: rgba(255,255,255,0.88); + line-height: 1.7; +} + +.market-features { + max-width: 1180px; + margin: 28px auto 22px; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.advertised-deals { + max-width: 1180px; + margin: 0 auto 30px; +} + +.deal-cards { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 18px; +} + +.deal-card { + border: 1px solid #dbe4f8; + border-radius: 22px; + overflow: hidden; + display: grid; + grid-template-rows: auto 1fr; + background: #fff; +} + +.deal-card img { + width: 100%; + height: 220px; + object-fit: cover; +} + +.deal-badge { + display: inline-flex; + padding: 6px 10px; + border-radius: 999px; + background: #eff6ff; + color: #1d4ed8; + font-weight: 700; + margin-bottom: 10px; +} + +.deal-label { + display: inline-flex; + margin-top: 4px; + margin-bottom: 10px; + padding: 6px 12px; + border-radius: 999px; + background: #fef3c7; + color: #92400e; + font-size: 0.85rem; + font-weight: 700; + letter-spacing: 0.01em; +} + +.deal-note { + color: #475569; + font-size: 0.95rem; + margin-bottom: 12px; +} + +@media (max-width: 1024px) { + .deal-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 760px) { + .deal-cards { grid-template-columns: 1fr; } +} + +.market-pill { + background: #fff; + border: 1px solid #dbe4f8; + border-radius: 16px; + padding: 18px 16px; + font-weight: 700; + color: #1f2937; + text-align: center; + box-shadow: var(--shadow-soft); +} + +.market-link { + text-decoration: none; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.market-link:hover { + transform: translateY(-2px); + box-shadow: 0 16px 30px rgba(15, 23, 42, 0.1); +} + +.trending { + max-width: 1180px; + margin: 0 auto; +} + +.section-heading { + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 18px; + margin-bottom: 18px; + flex-wrap: wrap; +} + +.section-heading .section-title { + font-size: clamp(1.8rem, 2.5vw, 2.4rem); + margin: 0; +} + +.section-heading p { + color: var(--muted); + max-width: 540px; + line-height: 1.6; +} + +.cards { + max-width: 1180px; + margin: 0 auto 30px; + display: grid; + grid-template-columns: repeat(4, minmax(0,1fr)); + gap: 18px; +} + +.card { + background: #fff; + border-radius: 22px; + overflow: hidden; + display: grid; + grid-template-rows: auto 1fr; + box-shadow: var(--shadow-soft); +} + +.card img { + width: 100%; + height: 220px; + object-fit: cover; +} + +.card-content { + padding: 18px; + display: grid; + gap: 10px; +} + +.card h3 { + font-size: 1rem; + line-height: 1.4; + color: var(--text); + min-height: 3.2em; +} + +.price { + color: #2874f0; + font-weight: 800; + font-size: 1.05rem; +} + +.stock { + color: var(--muted); + font-size: 0.95rem; +} + +.card-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +@media (max-width: 1024px) { + .hero { grid-template-columns: 1fr; } + .market-features { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .cards { grid-template-columns: repeat(2, minmax(0,1fr)); } +} + +@media (max-width: 760px) { + .hero { grid-template-columns: 1fr; padding: 28px; } + .hero-highlights { grid-template-columns: 1fr; } + .market-features, .cards { grid-template-columns: 1fr; } + .top-category-bar { padding: 10px 16px; } + .top-navbar { padding: 12px 16px; } +} diff --git a/myproject/static/css/navbar.css b/myproject/static/css/navbar.css new file mode 100644 index 0000000..df5f5a2 --- /dev/null +++ b/myproject/static/css/navbar.css @@ -0,0 +1,324 @@ +.sticky-header { + position: sticky; + top: 0; + z-index: 1000; +} + +.fk-header { + width: 100%; + background: rgba(40, 116, 240, 0.9); + background: linear-gradient(135deg, rgba(40, 116, 240, 0.85) 0%, rgba(15, 92, 212, 0.85) 100%); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border-bottom: 1px solid rgba(255, 255, 255, 0.15); + color: #fff; + transition: background 0.3s ease, box-shadow 0.3s ease, padding 0.3s ease; +} + +.nav-scrolled .fk-header { + background: rgba(40, 116, 240, 0.82); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); +} + +.fk-container { + max-width: 1240px; + margin: 0 auto; + padding: 0 16px; +} + +/* TOP ROW */ +.fk-top-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; +} + +.fk-top-left { + display: flex; + gap: 8px; +} + +.fk-brand-btn { + background-color: #ffe11b; /* Flipkart yellow */ + color: #000; + font-weight: 700; + font-style: italic; + padding: 4px 12px; + border-radius: 4px; + text-decoration: none; + display: flex; + align-items: center; + gap: 6px; + font-size: 0.95rem; +} + +.fk-brand-icon { + color: #2874f0; + font-size: 1.1rem; + font-style: normal; +} + +.fk-travel-btn { + background-color: #f1f2f4; + color: #000; + font-weight: 600; + font-size: 0.85rem; + padding: 4px 12px; + border-radius: 4px; + text-decoration: none; + display: flex; + align-items: center; + gap: 6px; +} + +.fk-travel-icon { + color: #ff5722; +} + +.fk-top-right { + font-size: 0.85rem; + color: rgba(255,255,255,0.9); +} + +.fk-top-right a { + color: #fff; + text-decoration: none; + font-weight: 600; + margin-left: 4px; +} + +/* MIDDLE ROW */ +.fk-middle-row { + display: flex; + align-items: center; + gap: 24px; + padding-bottom: 12px; +} + +.fk-search-bar { + flex: 1; + display: flex; + align-items: center; + background-color: #f0f5ff; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 8px 16px; + transition: background-color 0.2s, box-shadow 0.2s; +} + +.fk-search-bar:focus-within { + background-color: #fff; + border-color: #2874f0; + box-shadow: 0 0 0 1px #2874f0; +} + +.fk-search-icon { + color: #717478; + margin-right: 12px; +} + +.fk-search-bar input { + width: 100%; + border: none; + background: transparent; + outline: none; + font-size: 1rem; + color: #000; +} + +.fk-main-actions { + display: flex; + align-items: center; + gap: 20px; +} + +.fk-action-btn { + display: flex; + align-items: center; + gap: 8px; + text-decoration: none; + color: #fff; + font-size: 0.95rem; + padding: 8px 12px; + border-radius: 8px; + transition: background-color 0.2s; +} + +.fk-action-btn:hover { + background-color: rgba(255,255,255,0.15); +} + +.fk-action-btn span:first-child { + font-size: 1.1rem; +} + +.fk-user-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + object-fit: cover; + border: 2px solid rgba(255,255,255,0.85); +} + +.fk-avatar-fallback { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + background: rgba(255,255,255,0.18); + color: #fff; + font-size: 1rem; +} + +.fk-caret { + font-size: 0.9rem; + line-height: 1; +} + +.fk-dropdown-wrapper { + position: relative; +} + +.fk-dropdown-menu { + position: absolute; + top: 100%; + left: 0; + background: #fff; + box-shadow: 0 4px 16px rgba(0,0,0,0.1); + border-radius: 8px; + min-width: 200px; + padding: 8px 0; + opacity: 0; + visibility: hidden; + transform: translateY(10px); + transition: all 0.2s ease; + z-index: 10; +} + +.fk-dropdown-wrapper:hover .fk-dropdown-menu { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.fk-dropdown-menu a { + display: block; + padding: 10px 16px; + color: #212121; + text-decoration: none; + font-size: 0.9rem; +} + +.fk-dropdown-menu a:hover { + background-color: #f1f2f4; +} + +.fk-badge { + background-color: #ff6161; + color: #fff; + font-size: 0.75rem; + font-weight: bold; + padding: 2px 6px; + border-radius: 10px; + margin-left: -4px; + margin-top: -12px; +} + +/* BOTTOM ROW: CATEGORIES */ +.fk-category-bar { + background: #f8fbff; + border-bottom: 1px solid #dbe4f8; + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.03); + transition: background 0.3s ease, box-shadow 0.3s ease, height 0.3s ease; + height: 90px; + box-sizing: border-box; + overflow: hidden; +} + +.nav-scrolled .fk-category-bar { + background: rgba(248, 251, 255, 0.96); + box-shadow: 0 1px 8px rgba(15, 23, 42, 0.06); + height: 72px; +} + +.fk-category-scroll { + display: flex; + gap: 12px; + overflow-x: auto; + padding: 14px 0 10px 0; + min-height: 90px; + transition: padding 0.3s ease, min-height 0.3s ease; +} + +.nav-scrolled .fk-category-scroll { + padding: 8px 0 8px 0; + min-height: 72px; +} + +.fk-category-scroll::-webkit-scrollbar { + display: none; +} + +.fk-cat-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-decoration: none; + color: #212121; + font-size: 0.85rem; + font-weight: 500; + min-width: 72px; + width: 72px; + height: 72px; + padding: 4px 0 4px; + border-bottom: 3px solid transparent; + transition: color 0.2s ease, opacity 0.2s ease, height 0.3s ease; +} + +.fk-cat-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + font-size: 1.5rem; + margin-bottom: 4px; + transition: opacity 0.2s ease, transform 0.3s ease; +} + +.nav-scrolled .fk-cat-icon { + opacity: 0; + visibility: hidden; + transform: translateY(-4px); +} + +.nav-scrolled .fk-cat-item { + height: 64px; + padding: 2px 0 2px; +} + +.fk-cat-item:hover, .fk-cat-item.active { + color: #2874f0; +} + +.fk-cat-item.active { + border-bottom-color: #2874f0; +} + +@media (max-width: 768px) { + .fk-middle-row { + flex-wrap: wrap; + } + .fk-search-bar { + order: 3; + min-width: 100%; + } + .fk-main-actions { + flex: 1; + justify-content: flex-end; + } +} diff --git a/myproject/static/css/product.css b/myproject/static/css/product.css new file mode 100644 index 0000000..babbb83 --- /dev/null +++ b/myproject/static/css/product.css @@ -0,0 +1,458 @@ +.product-hero { + max-width: 1180px; + margin: 0 auto 18px; + background: linear-gradient(135deg, #eff6ff, #ffffff); + border: 1px solid #dbe4f8; + border-radius: 24px; + padding: 28px 30px; + display: grid; + grid-template-columns: 1fr; + gap: 16px; +} + +.product-hero h1 { + font-size: 2.2rem; + margin-bottom: 8px; + color: #111827; +} + +.product-hero p { + color: #475569; +} + +.product-search-bar { + margin-top: 14px; + display: grid; + grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr) auto; + gap: 12px; +} + +.product-search-bar input, +.product-search-bar select { + border: 1px solid #dbe4f8; + border-radius: 999px; + padding: 14px 16px; + background: #fff; + color: #111827; +} + +.product-search-bar button { + border-radius: 999px; + padding: 14px 22px; +} + +.category-bar { + max-width: 1180px; + margin: 0 auto 16px; + display: flex; + gap: 10px; + flex-wrap: wrap; + overflow-x: auto; + padding-bottom: 4px; +} + +.category-chip { + text-decoration: none; + padding: 10px 16px; + border: 1px solid var(--border); + border-radius: 999px; + background: #fff; + color: #1f2937; + box-shadow: 0 12px 28px rgba(15, 23, 42, 0.04); + white-space: nowrap; +} + +.category-chip.active { + border-color: #2874f0; + color: #2874f0; + background: #eff6ff; +} + +.cards { + max-width: 1180px; + margin: 0 auto 30px; + display: grid; + grid-template-columns: repeat(4, minmax(0,1fr)); + gap: 18px; +} + +.card { + background: #fff; + border: 1px solid #dbe4f8; + border-radius: 24px; + overflow: hidden; + display: grid; + grid-template-rows: auto 1fr; + box-shadow: var(--shadow-soft); +} + +.card img, +.product-detail-image img { + width: 100%; + height: 220px; + object-fit: cover; +} + +.card-content { + padding: 18px; + display: grid; + gap: 10px; +} + +.tag-row { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.tag { + border-radius: 999px; + padding: 5px 10px; + font-size: .78rem; + font-weight: 700; +} + +.badge-pill { + background: #dbeafe; + color: #1d4ed8; +} + +.category-tag { + background: #eff6ff; + color: #1d4ed8; +} + +.low-stock { + background: #fef2f2; + color: #991b1b; +} + +.product-pricing { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; +} + +.price { + color: #2874f0; + font-weight: 800; +} + +.original-price { + text-decoration: line-through; + color: #94a3b8; +} + +.stock, +.description { + color: #475569; +} + +.card-actions, +.product-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.breadcrumb { + max-width: 1180px; + margin: 0 auto 20px; + font-size: 0.95rem; + color: #475569; + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + +.breadcrumb a { + color: #1f2937; + text-decoration: none; +} + +.breadcrumb a:hover { + text-decoration: underline; +} + +.breadcrumb-separator { + color: #c7d2fe; +} + +.product-detail-page { + max-width: 1180px; + margin: 0 auto 28px; +} + +.product-detail-card { + display: grid; + grid-template-columns: 1.05fr 0.95fr; + background: #fff; + border: 1px solid #dbe4f8; + border-radius: 28px; + overflow: hidden; + box-shadow: var(--shadow-soft); +} + +.product-detail-image { + min-height: 520px; + background: #f8fbff; + display: grid; + place-items: center; + overflow: hidden; +} + +.product-detail-image img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.product-detail-info { + padding: 36px 34px; + display: grid; + gap: 16px; +} + +.detail-head { + display: flex; + gap: 12px; + align-items: center; + flex-wrap: wrap; +} + +.detail-head h1 { + font-size: clamp(2rem, 2.4vw, 2.8rem); +} + +.detail-meta { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.detail-meta span { + display: inline-flex; + align-items: center; + gap: 6px; + border-radius: 999px; + padding: 8px 14px; + background: #eff6ff; + color: #1f2937; + font-size: 0.95rem; +} + +.product-pricing { + display: flex; + gap: 14px; + align-items: center; + flex-wrap: wrap; +} + +.price { + color: #2874f0; + font-size: 2rem; + font-weight: 800; +} + +.original-price { + text-decoration: line-through; + color: #94a3b8; +} + +.savings { + color: #047857; + font-weight: 700; + background: #ecfdf5; + border-radius: 999px; + padding: 6px 12px; +} + +.product-description { + color: #334155; + line-height: 1.75; + margin-top: 8px; +} + +.product-extras { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.product-extras div { + background: #eff6ff; + border: 1px solid #dbe4f8; + border-radius: 16px; + padding: 18px 16px; + color: #1f2937; + font-weight: 700; + text-align: center; +} + +.product-actions { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-top: 10px; +} + +.product-actions .btn-secondary { + min-width: 152px; +} + +.badge-pill { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 14px; + border-radius: 999px; + background: #fef3c7; + color: #b45309; + font-weight: 700; +} + +.related-products { + margin-top: 32px; +} + +.related-products h2 { + margin-bottom: 18px; + font-size: 1.5rem; +} + +.related-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; +} + +.small-card { + background: #fff; + border: 1px solid #dbe4f8; + border-radius: 22px; + overflow: hidden; + box-shadow: var(--shadow-soft); +} + +.small-card img { + height: 190px; + object-fit: cover; +} + +.small-card .card-content { + padding: 16px; + display: grid; + gap: 8px; +} + +.small-card h3 { + font-size: 1.05rem; + margin-bottom: 2px; +} + +.small-card .price { + margin-top: 4px; +} + +.product-image-placeholder { + min-height: 190px; + display: grid; + place-items: center; + color: var(--muted); + background: #f8fbff; +} + +.empty-state { + max-width: 1180px; + margin: 0 auto; + background: #fff; + border: 1px solid #dbe4f8; + border-radius: 20px; + padding: 28px; + text-align: center; +} + +@media (max-width: 1024px) { + .related-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 820px) { + .product-detail-card { grid-template-columns: 1fr; } + .product-detail-image { min-height: 320px; } +} + +.cart-page { max-width: 1180px; margin: 0 auto 24px; display: grid; grid-template-columns: 2fr 1fr; gap: 16px; } +.cart-card { background: #fff; border: 1px solid #dbe4f8; border-radius: 16px; padding: 16px; display: flex; justify-content: space-between; gap: 12px; } +.cart-product { display: flex; align-items: center; gap: 14px; margin-bottom: 14px; } +.cart-product-avatar { width: 64px; height: 64px; border-radius: 999px; overflow: hidden; display: grid; place-items: center; background: #f8fbff; border: 1px solid #e2e8f0; } +.cart-product-avatar img { width: 100%; height: 100%; object-fit: cover; } +.avatar-placeholder { color: #475569; font-weight: 700; font-size: 1rem; } +.cart-meta { color: #475569; margin: 8px 0; } +.cart-qty-form { display: grid; gap: 8px; max-width: 220px; } +.cart-qty-form input { border: 1px solid #dbe4f8; border-radius: 10px; padding: 9px 11px; } +.cart-card-right { text-align: right; display: grid; align-content: start; gap: 10px; } +.summary-box { background: #fff; border: 1px solid #dbe4f8; border-radius: 16px; padding: 18px; display: grid; gap: 12px; } +.summary-row { display: flex; justify-content: space-between; } +.total-row { font-size: 1.1rem; font-weight: 800; } +.order-card-items { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; } +.order-thumb { width: 52px; height: 52px; border-radius: 999px; overflow: hidden; display: grid; place-items: center; background: #f8fbff; border: 1px solid #e2e8f0; } +.order-thumb img { width: 100%; height: 100%; object-fit: cover; } +.order-thumb.more { display: inline-flex; align-items: center; justify-content: center; font-size: .9rem; color: #475569; font-weight: 700; } +.order-item-row { display: flex; justify-content: space-between; gap: 10px; border: 1px solid #edf2f7; border-radius: 12px; padding: 12px; background: #fbfdff; } +.order-item-left { display: flex; align-items: center; gap: 12px; } +.order-item-avatar { width: 52px; height: 52px; border-radius: 999px; overflow: hidden; display: grid; place-items: center; background: #f8fbff; border: 1px solid #e2e8f0; } +.order-item-avatar img { width: 100%; height: 100%; object-fit: cover; } + +.payment-page, +.order-detail-page { max-width: 1180px; margin: 0 auto 28px; display: grid; grid-template-columns: 2fr 1fr; gap: 16px; } +.payment-panel, +.order-detail-box, +.order-card { background: #fff; border: 1px solid #dbe4f8; border-radius: 16px; padding: 18px; } +.payment-total { color: #475569; margin: 8px 0 12px; } +.payment-form { display: grid; gap: 10px; margin-top: 12px; } +.payment-option { border: 1px solid #dbe4f8; border-radius: 12px; padding: 12px; display: flex; align-items: center; gap: 10px; background: #f8fbff; } +.order-summary-list { list-style: none; display: grid; gap: 8px; margin-top: 8px; } +.order-summary-list li { display: flex; justify-content: space-between; gap: 10px; border-bottom: 1px solid #eef2f7; padding-bottom: 8px; } +.orders-list { max-width: 1180px; margin: 0 auto 28px; display: grid; gap: 14px; } +.order-card-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; } +.order-meta { display: flex; flex-wrap: wrap; gap: 10px; color: #64748b; } +.order-items { display: grid; gap: 10px; margin-top: 12px; } +.order-item-row { display: flex; justify-content: space-between; gap: 10px; border: 1px solid #edf2f7; border-radius: 12px; padding: 12px; background: #fbfdff; } +.status-tag { display: inline-flex; align-items: center; border-radius: 999px; padding: 4px 10px; font-size: .8rem; font-weight: 700; border: 1px solid transparent; } +.status-pending { color: #92400e; background: #fffbeb; border-color: #fde68a; } +.status-paid { color: #065f46; background: #ecfdf5; border-color: #a7f3d0; } +.status-shipped { color: #1e40af; background: #eff6ff; border-color: #bfdbfe; } +.status-delivered { color: #14532d; background: #f0fdf4; border-color: #bbf7d0; } + +@media (max-width: 1024px) { + .cards { grid-template-columns: repeat(2, minmax(0,1fr)); } + .product-detail-card, .cart-page, .payment-page, .order-detail-page { grid-template-columns: 1fr; } +} + +@media (max-width: 700px) { + .cards { grid-template-columns: 1fr; } + .product-search-bar { grid-template-columns: 1fr; } + .order-card-header { align-items: flex-start; flex-direction: column; } +} + +.shop-feature-strip { max-width: 1180px; margin: 0 auto 16px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; } +.shop-feature-strip div { background: #fff; border: 1px solid #dbe4f8; border-radius: 12px; padding: 10px 12px; font-weight: 700; color: #1f2937; text-align: center; } +.field-input { width: 100%; border: 1px solid #dbe4f8; border-radius: 10px; padding: 10px 12px; background: #fff; } +.field-textarea { min-height: 90px; resize: vertical; } +@media (max-width: 768px) { .shop-feature-strip { grid-template-columns: 1fr; } } + + +.coupon-form { display: grid; gap: 8px; margin-bottom: 10px; } +.timeline { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin: 10px 0 16px; } +.timeline-step { text-align: center; padding: 10px 8px; border-radius: 10px; border: 1px solid #ead9c8; background: #fff; } +.timeline-dot { width: 10px; height: 10px; border-radius: 999px; margin: 0 auto 6px; background: #d1d5db; } +.timeline-step.done { background: #ecfdf5; border-color: #bbf7d0; } +.timeline-step.done .timeline-dot { background: #16a34a; } +.timeline-step.current { background: #fff7ed; border-color: #fed7aa; } +.timeline-step.current .timeline-dot { background: #ea580c; } +.timeline-step.upcoming { background: #f8fafc; } +@media (max-width: 760px) { .timeline { grid-template-columns: 1fr 1fr; } } + diff --git a/myproject/static/css/profile.css b/myproject/static/css/profile.css new file mode 100644 index 0000000..3c63d3a --- /dev/null +++ b/myproject/static/css/profile.css @@ -0,0 +1,252 @@ +.profile-container { + padding: 24px; +} + +.profile-shell { + max-width: 920px; + margin: 0 auto; + display: grid; + gap: 24px; +} + +.profile-head { + background: linear-gradient(to bottom right, var(--surface), var(--surface-soft)); + border: 1px solid rgba(0, 0, 0, 0.05); + border-radius: 20px; + padding: 30px; + display: flex; + align-items: center; + gap: 24px; + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05); + position: relative; + overflow: hidden; +} + +.profile-head::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 5px; + background: linear-gradient(90deg, var(--accent), var(--accent-2)); +} + +.profile-avatar { + width: 100px; + height: 100px; + border-radius: 50%; + overflow: hidden; + flex-shrink: 0; + border: 4px solid var(--surface); + box-shadow: 0 0 0 2px var(--accent-soft), 0 8px 16px rgba(40, 116, 240, 0.15); + background: var(--surface-soft); +} + +.profile-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.avatar-placeholder { + width: 100%; + height: 100%; + display: grid; + place-items: center; + font-weight: 800; + font-size: 2rem; + background: linear-gradient(135deg, var(--accent-soft), var(--surface-strong)); + color: var(--accent); +} + +.profile-head h1 { + font-size: 1.8rem; + margin-bottom: 6px; + color: var(--text); +} + +.profile-head p { + color: var(--muted); + font-size: 1rem; + margin-bottom: 4px; +} + +.profile-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 16px; +} + +.stat-card { + background: var(--surface); + border: 1px solid rgba(0, 0, 0, 0.03); + border-radius: 16px; + padding: 20px; + text-align: center; + box-shadow: var(--shadow-soft); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.stat-card:hover { + transform: translateY(-4px); + box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08); +} + +.stat-card h3 { + font-size: 1.5rem; + color: var(--accent); + margin-bottom: 6px; +} + +.stat-card p { + color: var(--muted); + font-size: 0.9rem; + font-weight: 600; +} + +.profile-panel { + background: var(--surface); + border: 1px solid rgba(0, 0, 0, 0.03); + border-radius: 20px; + padding: 24px; + box-shadow: var(--shadow-soft); +} + +.profile-panel h2 { + margin-bottom: 16px; + font-size: 1.3rem; + color: var(--text); + border-bottom: 2px solid var(--surface-soft); + padding-bottom: 12px; +} + +.recent-orders { + display: grid; + gap: 12px; +} + +.recent-order-row { + display: grid; + grid-template-columns: 80px minmax(0, 1fr) 110px 130px; + gap: 14px; + align-items: center; + text-decoration: none; + color: var(--text); + border: 1px solid var(--border); + border-radius: 12px; + padding: 14px 18px; + background: var(--surface-soft); + font-size: 0.95rem; + transition: all 0.3s ease; +} + +.recent-order-row:hover { + background: var(--surface); + border-color: var(--accent-soft); + box-shadow: 0 4px 12px rgba(40, 116, 240, 0.08); + transform: translateX(4px); +} + +.recent-order-row strong { + justify-self: end; + color: var(--accent); +} + +.profile-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 8px; +} + +.profile-actions .btn { + padding: 12px 20px; + font-size: 0.95rem; + border-radius: 999px; +} + +.profile-edit-form { + max-width: 640px; + display: grid; + gap: 16px; +} + +.profile-edit-form .form-row { + display: grid; + gap: 8px; +} + +.profile-edit-form label { + font-size: 0.95rem; + color: var(--text); + font-weight: 600; +} + +.profile-edit-form input, +.profile-edit-form textarea, +.profile-edit-form select { + width: 100%; + border: 2px solid var(--surface-strong); + border-radius: 12px; + padding: 12px 16px; + background: var(--surface-soft); + color: var(--text); + transition: all 0.3s ease; +} + +.profile-edit-form input:focus, +.profile-edit-form textarea:focus, +.profile-edit-form select:focus { + border-color: var(--accent); + background: var(--surface); + outline: none; + box-shadow: 0 0 0 3px rgba(40, 116, 240, 0.1); +} + +.profile-edit-form textarea { + min-height: 120px; + resize: vertical; +} + +.profile-edit-form .current-image img { + width: 90px; + height: 90px; + border-radius: 50%; + object-fit: cover; + border: 3px solid var(--surface); + box-shadow: 0 0 0 2px var(--accent-soft); +} + +.profile-edit-form .form-row:last-child { + display: flex; + gap: 12px; + align-items: center; + margin-top: 8px; +} + +@media (max-width: 900px) { + .recent-order-row { + grid-template-columns: 1fr 1fr; + } + + .recent-order-row strong { + justify-self: start; + } +} + +@media (max-width: 560px) { + .profile-container { + padding: 16px; + } + + .profile-head { + flex-direction: column; + align-items: center; + text-align: center; + padding: 24px; + } + + .profile-edit-form .form-row:last-child { + flex-direction: column; + align-items: stretch; + } +} diff --git a/myproject/static/css/style.css b/myproject/static/css/style.css new file mode 100644 index 0000000..61257a6 --- /dev/null +++ b/myproject/static/css/style.css @@ -0,0 +1,292 @@ +:root { + --bg: #f5f8ff; + --surface: #ffffff; + --surface-soft: #eff4ff; + --surface-strong: #dbe9ff; + --text: #1f2937; + --muted: #475569; + --accent: #2874f0; + --accent-2: #0f5cd4; + --accent-alt: #fb641b; + --accent-soft: #e7f0ff; + --border: #dbe4f8; + --line: #c7d2fe; + --shadow: 0 22px 60px rgba(15, 23, 42, 0.12); + --shadow-soft: 0 14px 40px rgba(15, 23, 42, 0.06); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: 'Manrope', 'Segoe UI', Arial, sans-serif; + background: linear-gradient(180deg, #edf4ff 0%, #f8fbff 100%); + color: var(--text); + min-height: 100vh; +} + +body.theme-dark { + --bg: #0f172a; + --surface: #111827; + --surface-soft: #1f2937; + --surface-strong: #111827; + --text: #e5e7eb; + --muted: #94a3b8; + --accent: #60a5fa; + --accent-soft: #1e293b; + --border: #334155; + --line: #334155; + --shadow: 0 24px 70px rgba(0, 0, 0, 0.45); + background: #020617; +} + +img { + max-width: 100%; + display: block; +} + +a { + color: inherit; + text-decoration: none; +} + +button, +input, +select, +textarea { + font: inherit; +} + +.site-main { + margin: 0 auto; + max-width: 1320px; + padding: 40px 24px 40px; + transition: padding 0.25s ease; +} + +.message-panel { + max-width: 1180px; + margin: 0 auto 24px; + display: grid; + gap: 12px; +} + +.message { + padding: 16px 20px; + border-radius: 18px; + background: var(--surface); + border: 1px solid var(--border); + color: var(--text); + box-shadow: var(--shadow-soft); +} + +.message.success { + border-color: #34d399; + background: #ecfdf5; +} + +.message.error, +.message.danger, +.message.warning { + border-color: #fca5a5; + background: #fef2f2; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: 999px; + border: none; + padding: 12px 20px; + font-weight: 700; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; +} + +.btn:hover { + transform: translateY(-1px); +} + +.btn-primary { + background: linear-gradient(135deg, var(--accent), var(--accent-2)); + color: #fff; + box-shadow: 0 16px 34px rgba(40, 116, 240, 0.24); +} + +.btn-secondary { + background: #fff; + color: var(--text); + border: 1px solid var(--border); +} + +.btn.disabled, +.btn[disabled] { + opacity: 0.65; + cursor: not-allowed; + transform: none; +} + +.btn.small { + padding: 10px 16px; +} + +.page-header { + max-width: 1180px; + margin: 0 auto 28px; + padding: 24px 28px; + background: #fff; + border-radius: 24px; + border: 1px solid #dbe4f8; + box-shadow: var(--shadow-soft); +} + +.page-header h1 { + font-size: clamp(2.2rem, 3vw, 3rem); + margin-bottom: 8px; +} + +.page-header p { + color: var(--muted); + max-width: 760px; + line-height: 1.8; +} + +.empty-state { + background: #fff; + border: 1px solid var(--border); + border-radius: 24px; + padding: 40px 28px; + text-align: center; + color: var(--muted); + box-shadow: var(--shadow-soft); +} + +.empty-state h2 { + margin-bottom: 14px; + color: var(--text); +} + +@media (max-width: 1024px) { + .site-main { + padding: 32px 20px 32px; + } +} + +@media (max-width: 768px) { + .site-main { + padding: 24px 16px 24px; + } +} + +/* Settings Page Styles */ +.settings-container { + max-width: 800px; + margin: 0 auto; + padding: 24px; +} + +.settings-header { + text-align: center; + margin-bottom: 40px; +} + +.settings-header h1 { + font-size: 2.5rem; + color: var(--text); + margin-bottom: 10px; +} + +.settings-header p { + color: var(--muted); + font-size: 1.1rem; +} + +.settings-grid { + display: grid; + gap: 24px; +} + +.settings-card { + background: linear-gradient(to bottom right, var(--surface), var(--surface-soft)); + border: 1px solid rgba(0, 0, 0, 0.03); + border-radius: 20px; + padding: 32px; + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05), inset 0 2px 4px rgba(255, 255, 255, 0.5); + display: flex; + flex-direction: column; + gap: 12px; + transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); + position: relative; + overflow: hidden; +} + +.settings-card::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 4px; + background: linear-gradient(90deg, var(--accent), var(--accent-2)); + opacity: 0; + transition: opacity 0.4s ease; +} + +.settings-card:hover { + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(15, 23, 42, 0.12), inset 0 2px 4px rgba(255, 255, 255, 0.8); +} + +.settings-card:hover::before { + opacity: 1; +} + +.settings-card h3 { + font-size: 1.4rem; + color: var(--text); + margin: 0; +} + +.settings-card p { + color: var(--muted); + margin: 0; + line-height: 1.6; +} + +.settings-action { + margin-top: 10px; + display: flex; + align-items: center; +} + +.form-select { + width: 100%; + max-width: 300px; + padding: 14px 20px; + border: 2px solid transparent; + border-radius: 12px; + background-color: var(--surface-soft); + color: var(--text); + font-size: 1rem; + font-weight: 600; + cursor: pointer; + box-shadow: inset 0 2px 4px rgba(0,0,0,0.02); + transition: all 0.3s ease; + appearance: none; + background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%232874f0%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E"); + background-repeat: no-repeat, repeat; + background-position: right .7em top 50%, 0 0; + background-size: .65em auto, 100%; +} + +.form-select:hover, .form-select:focus { + border-color: var(--accent); + outline: none; +} diff --git a/myproject/static/css/support.css b/myproject/static/css/support.css new file mode 100644 index 0000000..a6a52bc --- /dev/null +++ b/myproject/static/css/support.css @@ -0,0 +1,44 @@ +.support-grid { + max-width: 1180px; + margin: 0 auto 24px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.support-card { + background: #fff; + border: 1px solid var(--border); + border-radius: 20px; + padding: 22px; + box-shadow: var(--shadow-soft); + display: grid; + gap: 10px; +} + +.support-card h2 { + font-size: 1.3rem; +} + +.support-card p, +.support-card li { + color: var(--muted); + line-height: 1.7; +} + +.support-card ul { + margin-left: 18px; + display: grid; + gap: 8px; +} + +.support-card a { + color: var(--accent); + text-decoration: underline; +} + +@media (max-width: 820px) { + .support-grid { + grid-template-columns: 1fr; + } +} diff --git a/myproject/static/js/animation.js b/myproject/static/js/animation.js new file mode 100644 index 0000000..3604c9b --- /dev/null +++ b/myproject/static/js/animation.js @@ -0,0 +1,5 @@ +function showWelcome(){ + + alert("Welcome to Hamro Karma ๐Ÿ‡ณ๐Ÿ‡ต"); + +} \ No newline at end of file diff --git a/myproject/static/js/app.js b/myproject/static/js/app.js new file mode 100644 index 0000000..569884f --- /dev/null +++ b/myproject/static/js/app.js @@ -0,0 +1,66 @@ +document.addEventListener('DOMContentLoaded', function () { + const sidebarToggle = document.querySelector('.sidebar-toggle'); + const sidebarOverlay = document.querySelector('.sidebar-overlay'); + const sidebarClose = document.querySelector('.sidebar-close'); + const modeToggle = document.querySelector('.mode-toggle'); + const body = document.body; + + const collapseKey = 'hk_nav_collapsed'; + const themeKey = 'hk_theme'; + + const setCollapsed = function (collapsed) { + body.classList.toggle('nav-collapsed', collapsed); + localStorage.setItem(collapseKey, collapsed ? '1' : '0'); + }; + + const setTheme = function (theme) { + const dark = theme === 'dark'; + body.classList.toggle('theme-dark', dark); + if (modeToggle) { + modeToggle.textContent = dark ? 'Light' : 'Dark'; + modeToggle.setAttribute('aria-label', dark ? 'Switch to light mode' : 'Switch to dark mode'); + } + localStorage.setItem(themeKey, dark ? 'dark' : 'light'); + }; + + const savedTheme = localStorage.getItem(themeKey) || 'light'; + setTheme(savedTheme); + + if (window.innerWidth > 1024) { + setCollapsed(localStorage.getItem(collapseKey) === '1'); + } + + if (modeToggle) { + modeToggle.addEventListener('click', function () { + setTheme(body.classList.contains('theme-dark') ? 'light' : 'dark'); + }); + } + + if (sidebarToggle) { + sidebarToggle.addEventListener('click', function () { + if (window.innerWidth <= 1024) { + body.classList.toggle('sidebar-open'); + return; + } + setCollapsed(!body.classList.contains('nav-collapsed')); + }); + } + + if (sidebarOverlay) { + sidebarOverlay.addEventListener('click', function () { + body.classList.remove('sidebar-open'); + }); + } + + if (sidebarClose) { + sidebarClose.addEventListener('click', function () { + body.classList.remove('sidebar-open'); + }); + } + + document.addEventListener('keydown', function (event) { + if (event.key === 'Escape') { + body.classList.remove('sidebar-open'); + } + }); +}); diff --git a/myproject/tempelates/accounts/edit_profile.html b/myproject/tempelates/accounts/edit_profile.html new file mode 100644 index 0000000..8d7cd47 --- /dev/null +++ b/myproject/tempelates/accounts/edit_profile.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} +
+
+

Edit Profile

+
+ {% csrf_token %} +
+ + {{ form.first_name }} +
+
+ + {{ form.last_name }} +
+
+ + {{ form.email }} +
+
+ + {% if profile.image %} +
avatar
+ {% endif %} + {{ form.image }} +
+
+ + {{ form.bio }} +
+
+ + Cancel +
+
+
+
+{% endblock %} diff --git a/myproject/tempelates/accounts/login.html b/myproject/tempelates/accounts/login.html new file mode 100644 index 0000000..5785fe6 --- /dev/null +++ b/myproject/tempelates/accounts/login.html @@ -0,0 +1,47 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + + +{% endblock %} + +{% block content %} + +
+
+

Login

+ + {% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + + {% if error %} +
{{ error }}
+ {% endif %} + +
+ {% csrf_token %} + +
+ + +
+ +
+ + +
+ + +
+ + +
+
+ +{% endblock %} \ No newline at end of file diff --git a/myproject/tempelates/accounts/profile.html b/myproject/tempelates/accounts/profile.html new file mode 100644 index 0000000..685fdc0 --- /dev/null +++ b/myproject/tempelates/accounts/profile.html @@ -0,0 +1,63 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} +
+
+
+
+ {% if user.profile.image %} + {{ user.username }} + {% else %} +
{{ user.username|slice:":1"|upper }}
+ {% endif %} +
+
+

{{ user.username }}

+

{{ user.email|default:'No email added yet' }}

+ {% if user.profile.is_seller %} +

Seller Account: Active

+ {% endif %} +
+
+ +
+

{{ orders_count }}

Total Orders

+

{{ delivered_count }}

Delivered

+

{{ pending_count }}

In Progress

+

{{ wishlist_count }}

Wishlist

+

Rs. {{ total_spent|floatformat:2 }}

Total Spent

+

{{ user.date_joined|date:'Y' }}

Member Since

+
+ +
+

Recent Orders

+ {% if recent_orders %} + + {% else %} +

No orders yet.

+ {% endif %} +
+ + +
+
+{% endblock %} diff --git a/myproject/tempelates/accounts/register.html b/myproject/tempelates/accounts/register.html new file mode 100644 index 0000000..9c0f8d9 --- /dev/null +++ b/myproject/tempelates/accounts/register.html @@ -0,0 +1,67 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + + +{% endblock %} + +{% block content %} + +
+
+

Create Account

+ + {% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + + {% if error %} +
{{ error }}
+ {% endif %} + +
+ {% csrf_token %} + +
+ + + At least 3 characters +
+ +
+ + +
+ +
+ + + At least 6 characters +
+ +
+ + +
+ +
+ + Enable this if you want to sell products on Hamro Karma. +
+ + +
+ + +
+
+ +{% endblock %} diff --git a/myproject/tempelates/base.html b/myproject/tempelates/base.html new file mode 100644 index 0000000..b787654 --- /dev/null +++ b/myproject/tempelates/base.html @@ -0,0 +1,38 @@ +{% load static %} + + + + + + Hamro Karma | Smart Shopping + + + + + + + {% block extra_head %}{% endblock %} + + + + +
+ {% if messages %} +
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + + {% block content %}{% endblock %} +
+ + {% include 'includes/footer.html' %} + + + + + diff --git a/myproject/tempelates/cart/cart.html b/myproject/tempelates/cart/cart.html new file mode 100644 index 0000000..3b2783a --- /dev/null +++ b/myproject/tempelates/cart/cart.html @@ -0,0 +1,84 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} + + +
+
+ {% if products %} + {% for p in products %} +
+
+
+
+ {% if p.image %} + {{ p.name }} + {% else %} + {{ p.name|slice:":1"|upper }} + {% endif %} +
+
+

{{ p.name }}

+

Unit Rs. {{ p.display_price|floatformat:2 }} | Stock {{ p.stock }}

+
+
+
+ {% csrf_token %} + + + +
+
+
+

Subtotal

+ Rs. {{ p.subtotal|floatformat:2 }} +
+ Remove +
+
+
+ {% endfor %} + {% else %} +
+

Your cart is empty.

+

Browse products and add items to your cart.

+
+ {% endif %} +
+ + {% if products %} + + {% endif %} +
+{% endblock %} diff --git a/myproject/tempelates/core/about.html b/myproject/tempelates/core/about.html new file mode 100644 index 0000000..f57cba9 --- /dev/null +++ b/myproject/tempelates/core/about.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} +
+
+

About Hamro Karma

+

Empowering Nepal Through Digital Commerce

+
+
+ +
+
+

Hamro Karma is a digital marketplace that connects buyers with trusted sellers across Nepal. Our goal is to make online shopping reliable, simple, and transparent.

+
+
+ +
+
+
+

Our Mission

+

Deliver dependable online shopping backed by verified products, fair pricing, and customer-first service.

+
+
+

Our Vision

+

Build Nepal's most trusted and modern e-commerce ecosystem for long-term digital growth.

+
+
+
+ +
+

Core Values

+
+

Trust

Reliable products and clear communication.

+

Quality

Better standards in every listing and delivery.

+

Community

Strong support for local sellers and buyers.

+

Innovation

Continuous product and service improvements.

+
+
+{% endblock %} diff --git a/myproject/tempelates/core/home.html b/myproject/tempelates/core/home.html new file mode 100644 index 0000000..cb7274f --- /dev/null +++ b/myproject/tempelates/core/home.html @@ -0,0 +1,91 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + + +{% endblock %} + +{% block content %} +
+
+

Nepal Special Deals

+

Shop the best offers across electronics, fashion, and home essentials.

+

Fast delivery, secure checkout, and everyday savings on products you love.

+ +
+
+ {% for deal in special_deals %} +
+ {{ deal.title }} +

{{ deal.description }}

+ {% if deal.note %}{{ deal.note }}{% endif %} +
+ {% endfor %} +
+
+ +
+ {{ ui.products }}: {{ total_products }} + {{ ui.featured }}: {{ featured_products }} + {{ ui.categories }}: {{ categories_count }} +
Avg Rating: {{ avg_rating|floatformat:1 }}/5
+
+ +
+
+

Advertised Deals

+

Shop the hottest offers, discounts, and promotions curated for Nepali customers.

+
+ {% if advertised_products %} +
+ {% for product in advertised_products %} +
+ {% if product.image %} + {{ product.name }} + {% else %} +
No image
+ {% endif %} +
+ Save Rs. {{ product.savings|floatformat:0 }} + {{ product.deal_label }} +

{{ product.name }}

+

Rs. {{ product.discount_price|default:product.price|floatformat:2 }}

+

{{ product.category }}

+ View Deal +
+
+ {% endfor %} +
+ {% else %} +

No deals available.

Check back soon for new offers.

+ {% endif %} +
+ + +{% endblock %} diff --git a/myproject/tempelates/core/landing.html b/myproject/tempelates/core/landing.html new file mode 100644 index 0000000..26753ae --- /dev/null +++ b/myproject/tempelates/core/landing.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} + + + +
+ +

+ Discover the Future of Nepali E-Commerce ๐Ÿš€ +

+ +

+ Join thousands of users exploring modern digital shopping. +

+ + + +
+ +{% endblock %} \ No newline at end of file diff --git a/myproject/tempelates/core/settings.html b/myproject/tempelates/core/settings.html new file mode 100644 index 0000000..4ae73c4 --- /dev/null +++ b/myproject/tempelates/core/settings.html @@ -0,0 +1,61 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+

โš™๏ธ Settings

+

Manage your account settings and preferences.

+
+ +
+
+

๐ŸŽจ Appearance

+

Customize how the app looks on your device.

+
+ +
+
+ +
+

๐ŸŒ Language

+

Select your preferred language.

+
+
+ {% csrf_token %} + + +
+
+
+ +
+

๐Ÿ“ Delivery Location

+

Save your default delivery location for faster checkout.

+
+
+ {% csrf_token %} + + +
+
+
+ +
+

๐Ÿ‘ค Account

+

Update your profile information and manage your account.

+
+ {% if user.is_authenticated %} + Go to Profile + {% else %} + Login to manage account + {% endif %} +
+
+
+
+{% endblock %} diff --git a/myproject/tempelates/core/support.html b/myproject/tempelates/core/support.html new file mode 100644 index 0000000..0555322 --- /dev/null +++ b/myproject/tempelates/core/support.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} + + +
+
+

Customer Care

+

Email: support@hamrokarma.com

+

Phone: +977-9800000000

+

Hours: 9:00 AM - 7:00 PM (NPT)

+
+ + +
+{% endblock %} diff --git a/myproject/tempelates/includes/footer.html b/myproject/tempelates/includes/footer.html new file mode 100644 index 0000000..4398901 --- /dev/null +++ b/myproject/tempelates/includes/footer.html @@ -0,0 +1,41 @@ + diff --git a/myproject/tempelates/includes/navbar.html b/myproject/tempelates/includes/navbar.html new file mode 100644 index 0000000..0e94bcf --- /dev/null +++ b/myproject/tempelates/includes/navbar.html @@ -0,0 +1,114 @@ +
+ +
+ + + + + diff --git a/myproject/tempelates/order/checkout.html b/myproject/tempelates/order/checkout.html new file mode 100644 index 0000000..500d32b --- /dev/null +++ b/myproject/tempelates/order/checkout.html @@ -0,0 +1,54 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} + + +
+
+

Order Summary

+
+ {% for p in products %} +
+
+

{{ p.name }}

+

Quantity: {{ p.qty }}

+
+ Rs. {{ p.subtotal|floatformat:2 }} +
+ {% endfor %} +
+ +
+ Total + Rs. {{ total|floatformat:2 }} +
+
+ + +
+{% endblock %} diff --git a/myproject/tempelates/order/my_order.html b/myproject/tempelates/order/my_order.html new file mode 100644 index 0000000..5dd7d7f --- /dev/null +++ b/myproject/tempelates/order/my_order.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} +{% block content %} + +

My Orders ๐Ÿ“ฆ

+ +
+ + {% for order in orders %} + +
+ +

Order #{{ order.id }}

+

Date: {{ order.created_at }}

+

Total: Rs. {{ order.total_price }}

+ +
+ + {% empty %} + +

No orders yet.

+ + {% endfor %} + +
+ +{% endblock %} \ No newline at end of file diff --git a/myproject/tempelates/order/my_orders.html b/myproject/tempelates/order/my_orders.html new file mode 100644 index 0000000..f21768f --- /dev/null +++ b/myproject/tempelates/order/my_orders.html @@ -0,0 +1,58 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} + + +
+ {% if orders %} + {% for order in orders %} +
+
+
+

Order #{{ order.id }}

+

{{ order.created_at|date:"M d, Y" }} | {{ order.status }}

+
+ View Details +
+ +
+ {% for item in order.items.all|slice:":3" %} +
+ {% if item.product.image %} + {{ item.product.name }} + {% else %} + {{ item.product.name|slice:":1"|upper }} + {% endif %} +
+ {% endfor %} + {% if order.items.count > 3 %} +
+{{ order.items.count|add:'-3' }}
+ {% endif %} +
+ +
+ Total: Rs. {{ order.total_price|floatformat:2 }} + {{ order.items.count }} item{{ order.items.count|pluralize }} + Payment: {{ order.payment_method }} +
+
+ {% endfor %} + {% else %} +
+

No orders yet.

+

Start shopping and your orders will appear here.

+ Browse Products +
+ {% endif %} +
+{% endblock %} diff --git a/myproject/tempelates/order/order_detail.html b/myproject/tempelates/order/order_detail.html new file mode 100644 index 0000000..f59240b --- /dev/null +++ b/myproject/tempelates/order/order_detail.html @@ -0,0 +1,73 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} + + +
+
+

Tracking Timeline

+
+ {% for step in timeline %} +
+
+
{{ step.label }}
+
+ {% endfor %} +
+ +

Order Summary

+
+ Payment method + {{ order.payment_method }} +
+
+ Total amount + Rs. {{ order.total_price|floatformat:2 }} +
+
+ Customer + {{ order.full_name|default:order.user.username }} +
+
+ Phone + {{ order.phone|default:'-' }} +
+
+ Address + {{ order.address|default:'-' }} +
+ +

Items

+
+ {% for item in order.items.all %} +
+
+
+ {% if item.product.image %} + {{ item.product.name }} + {% else %} + {{ item.product.name|slice:":1"|upper }} + {% endif %} +
+
+

{{ item.product.name }}

+

Quantity: {{ item.quantity }}

+
+
+ Rs. {{ item.price|floatformat:2 }} +
+ {% endfor %} +
+
+
+{% endblock %} diff --git a/myproject/tempelates/order/payment.html b/myproject/tempelates/order/payment.html new file mode 100644 index 0000000..85e55e5 --- /dev/null +++ b/myproject/tempelates/order/payment.html @@ -0,0 +1,61 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} + + +
+
+

Order #{{ order.id }}

+

Amount due: Rs. {{ order.total_price|floatformat:2 }}

+

Status: {{ order.status }}

+ +
+ {% csrf_token %} + + + + + +
+
+ + +
+{% endblock %} diff --git a/myproject/tempelates/order/payment_gateway.html b/myproject/tempelates/order/payment_gateway.html new file mode 100644 index 0000000..6dd8aab --- /dev/null +++ b/myproject/tempelates/order/payment_gateway.html @@ -0,0 +1,52 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} + + +
+
+

Order #{{ order.id }}

+

Amount due: Rs. {{ order.total_price|floatformat:2 }}

+

Status: {{ order.status }}

+ +
+ {% csrf_token %} +

Please click the button below to complete your payment through {{ gateway.name }}.

+ +
+ +
+

Payment steps

+
    +
  1. Select {{ gateway.name }} on the previous page.
  2. +
  3. Press {{ gateway.button_text }}.
  4. +
  5. Follow the instructions on the payment screen.
  6. +
  7. After payment completes, your order will be confirmed.
  8. +
+
+
+ + +
+{% endblock %} diff --git a/myproject/tempelates/order/success.html b/myproject/tempelates/order/success.html new file mode 100644 index 0000000..851920a --- /dev/null +++ b/myproject/tempelates/order/success.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} +
+
+

Order confirmed

+

Order placed successfully

+

Thank you for shopping with Hamro Karma. We will start processing your order right away.

+ +
+
+{% endblock %} diff --git a/myproject/tempelates/products/product_details.html b/myproject/tempelates/products/product_details.html new file mode 100644 index 0000000..267f87b --- /dev/null +++ b/myproject/tempelates/products/product_details.html @@ -0,0 +1,95 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} + + +
+
+
+ {% if product.image %} + {{ product.name }} + {% else %} +
No image available
+ {% endif %} +
+ +
+
+

{{ product.name }}

+ {% if product.featured %} + Featured + {% endif %} +
+ +
+ โญ {{ product.rating|default:0 }}/5 + Category: {{ product.category }} + Stock: {{ product.stock|default:0 }} +
+ +
+ Rs. {{ product.display_price|floatformat:2 }} + {% if product.discount_price %} + Rs. {{ product.price|floatformat:2 }} + Save Rs. {{ product.savings|floatformat:2 }} + {% endif %} +
+ +

{{ product.description }}

+ +
+
Fast delivery
+
Secure checkout
+
Easy return
+
+ +
+ Back to Products + {% if product.stock > 0 %} + Add to Cart + Buy Now + {% else %} + Sold Out + {% endif %} +
+
+
+ + {% if related_products %} + + {% endif %} +
+{% endblock %} diff --git a/myproject/tempelates/products/product_list.html b/myproject/tempelates/products/product_list.html new file mode 100644 index 0000000..36406d3 --- /dev/null +++ b/myproject/tempelates/products/product_list.html @@ -0,0 +1,79 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} +
+
+

Shop the best products

+

Browse items, compare prices, and find great deals near you.

+
+ + +
+ +
+
Items: {{ products_count }}
+
Featured: {{ featured_count }}
+
In Stock: {{ in_stock_count }}
+
+ +
+ All + Featured + {% for category in categories %} + {{ category }} + {% endfor %} +
+ +{% if products %} +
+{% for product in products %} +
+ {% if product.image %}{{ product.name }}{% else %}
No image available
{% endif %} +
+
+ {% if product.featured %}Featured{% endif %} + {{ product.category }} + {% if product.is_low_stock %}Low Stock{% endif %} +
+

{{ product.name }}

+

{{ product.description|truncatewords:16 }}

+
+ Rs. {{ product.display_price|floatformat:2 }} + {% if product.discount_price %} + Rs. {{ product.price|floatformat:2 }} + {% endif %} +
+

Stock: {{ product.stock|default:0 }}

+ +
+
+{% endfor %} +
+{% else %} +

No products found.

Try adjusting search or filters.

+{% endif %} +{% endblock %} diff --git a/myproject/tempelates/products/search.html b/myproject/tempelates/products/search.html new file mode 100644 index 0000000..e69de29 diff --git a/myproject/tempelates/products/wishlist.html b/myproject/tempelates/products/wishlist.html new file mode 100644 index 0000000..5b9dbda --- /dev/null +++ b/myproject/tempelates/products/wishlist.html @@ -0,0 +1,36 @@ +{% extends 'base.html' %} +{% load static %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} + + +{% if wishlist_items %} +
+ {% for item in wishlist_items %} +
+ {% if item.product.image %}{{ item.product.name }}{% else %}
No image
{% endif %} +
+

{{ item.product.name }}

+

Rs. {{ item.product.display_price|floatformat:2 }}

+ +
+
+ {% endfor %} +
+{% else %} +

No wishlist items yet.

Add products to wishlist while browsing.

+{% endif %} +{% endblock %} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ee4de89 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Django>=5.1,<6.1 +Pillow>=10.0 diff --git a/temp_unused_scan.py b/temp_unused_scan.py new file mode 100644 index 0000000..0c60271 --- /dev/null +++ b/temp_unused_scan.py @@ -0,0 +1,43 @@ +import os +import re +root = os.path.abspath(os.path.dirname(__file__)) +exclude_dirs = {'__pycache__', '.git', 'node_modules'} +all_files = [] +for dirpath, dirnames, filenames in os.walk(root): + dirnames[:] = [d for d in dirnames if d not in exclude_dirs] + for fn in filenames: + p = os.path.join(dirpath, fn) + if p.endswith(('.pyc', '.sqlite3', '.db')): + continue + all_files.append(p) +file_texts = { + p: open(p, encoding='utf-8', errors='ignore').read() + for p in all_files + if os.path.splitext(p)[1] in {'.py', '.html', '.txt', '.md', '.js', '.css'} +} +unused = [] +for p in all_files: + ext = os.path.splitext(p)[1] + if ext not in {'.py', '.html', '.css', '.js', '.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg', '.txt', '.md'}: + continue + norm = p.replace('\\', '/') + if 'migrations' in norm or '__pycache__' in norm: + continue + if os.path.basename(p) == '__init__.py': + continue + name = os.path.basename(p) + found = False + regex = re.escape(name) + for qpath, text in file_texts.items(): + if qpath == p: + continue + if re.search(regex, text): + found = True + break + if not found: + unused.append(p) +out = ['TOTAL %s' % len(all_files), 'CANDIDATE_UNUSED %s' % len(unused)] +out.extend(sorted(unused)) +with open(os.path.join(root, 'unused-files-report.txt'), 'w', encoding='utf-8') as f: + f.write('\n'.join(out)) +print('Report written:', os.path.join(root, 'unused-files-report.txt'))