diff --git a/Backend/app/admin.py b/Backend/app/admin.py deleted file mode 100644 index eeb02c0..0000000 --- a/Backend/app/admin.py +++ /dev/null @@ -1,106 +0,0 @@ -from django.contrib import admin -from .models import User, Category, Product, Rating, Order, OrderItem, Cart, CartItem -from django.contrib.auth.admin import UserAdmin as BaseUserAdmin - -@admin.register(User) -class UserAdmin(BaseUserAdmin): - list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'created_at') - search_fields = ('username', 'email', 'first_name', 'last_name') - list_filter = ('is_staff', 'is_active', 'created_at') - readonly_fields = ('created_at', 'updated_at') - fieldsets = ( - ('Personal Info', {'fields': ('username', 'first_name', 'last_name', 'email', 'password')}), - ('Customer Details', {'fields': ('phone', 'address')}), - ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}), - ('Dates', {'fields': ('created_at', 'updated_at', 'last_login')}), - ) - add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('username', 'email', 'first_name', 'last_name', 'password1', 'password2', 'phone', 'address'), - }), - ) - -@admin.register(Category) -class CategoryAdmin(admin.ModelAdmin): - list_display = ('name', 'created_at') - search_fields = ('name',) - list_filter = ('created_at',) - readonly_fields = ('created_at', 'updated_at') - - -class RatingInline(admin.TabularInline): - model = Rating - extra = 0 - readonly_fields = ('created_at',) - - -@admin.register(Product) -class ProductAdmin(admin.ModelAdmin): - list_display = ('name', 'category', 'price','stock','color','size', 'created_at', 'updated_at') - search_fields = ('name', 'description') - list_filter = ('category', 'created_at') - readonly_fields = ('created_at', 'updated_at') - inlines = [RatingInline] - - -@admin.register(Rating) -class RatingAdmin(admin.ModelAdmin): - list_display = ('user', 'product', 'rating', 'created_at') - search_fields = ('user__username', 'product__name', 'comment') - list_filter = ('rating', 'created_at') - readonly_fields = ('created_at', 'updated_at') - - -class OrderItemInline(admin.TabularInline): - model = OrderItem - extra = 0 - readonly_fields = ('get_subtotal',) - - -@admin.register(Order) -class OrderAdmin(admin.ModelAdmin): - list_display = ('id', 'user', 'total_price', 'status', 'created_at') - search_fields = ('user__username', 'shipping_address') - list_filter = ('status', 'created_at') - readonly_fields = ('created_at', 'updated_at', 'total_price') - inlines = [OrderItemInline] - list_editable = ('status',) - - -class OrderItemAdmin(admin.ModelAdmin): - list_display = ('order', 'product', 'quantity', 'price', 'get_subtotal') - list_filter = ('order__status',) - search_fields = ('order__id', 'product__name') - readonly_fields = ('get_subtotal',) - - def get_subtotal(self, obj): - return obj.get_subtotal() - get_subtotal.short_description = 'Subtotal' - - -admin.site.register(OrderItem, OrderItemAdmin) - - -class CartItemInline(admin.TabularInline): - model = CartItem - extra = 0 - readonly_fields = ['created_at', 'updated_at'] - -@admin.register(Cart) -class CartAdmin(admin.ModelAdmin): - list_display = ['user', 'get_total', 'created_at', 'updated_at'] - list_filter = ['created_at', 'updated_at'] - search_fields = ['user__username', 'user__email'] - readonly_fields = ['created_at', 'updated_at'] - inlines = [CartItemInline] - - def get_total(self, obj): - return obj.get_total() - get_total.short_description = 'Total' - - def get_subtotal(self, obj): - return obj.get_subtotal() - get_subtotal.short_description = 'Subtotal' - - diff --git a/Backend/app/carts.py b/Backend/app/carts.py deleted file mode 100644 index 5d8cd63..0000000 --- a/Backend/app/carts.py +++ /dev/null @@ -1,198 +0,0 @@ -from rest_framework import viewsets, status -from rest_framework.decorators import action -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from django.shortcuts import get_object_or_404 -from django.db import transaction -from .models import Cart, CartItem, Product, Order, OrderItem -from .serializers import CartSerializer, CartItemSerializer - -class CartViewSet(viewsets.ViewSet): - permission_classes = [IsAuthenticated] - - def get_cart(self, request): - """Get or create cart for the authenticated user""" - cart, created = Cart.objects.get_or_create(user=request.user) - return cart - - @action(detail=False, methods=['get']) - def items(self, request): - """Get all items in the cart""" - cart = self.get_cart(request) - serializer = CartSerializer(cart, context={'request': request}) - return Response(serializer.data) - - @action(detail=False, methods=['post']) - def add_item(self, request): - """Add an item to the cart""" - product_id = request.data.get('product_id') - quantity = int(request.data.get('quantity', 1)) - - if not product_id: - return Response({'error': 'Product ID is required'}, status=status.HTTP_400_BAD_REQUEST) - - try: - product = Product.objects.get(id=product_id) - except Product.DoesNotExist: - return Response({'error': 'Product not found'}, status=status.HTTP_404_NOT_FOUND) - - # Check if product is in stock - if product.stock < quantity: - return Response({ - 'error': f'Not enough stock available. Available: {product.stock}' - }, status=status.HTTP_400_BAD_REQUEST) - - cart = self.get_cart(request) - cart_item = cart.add_product(product, quantity) - - if cart_item: - serializer = CartItemSerializer(cart_item, context={'request': request}) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response({'error': 'Invalid quantity'}, status=status.HTTP_400_BAD_REQUEST) - - @action(detail=False, methods=['post']) - def remove_item(self, request): - """Remove an item from the cart""" - try: - product_id = request.data.get('product_id') - if not product_id: - return Response({'error': 'Product ID is required'}, status=status.HTTP_400_BAD_REQUEST) - - try: - product = Product.objects.get(id=product_id) - except Product.DoesNotExist: - return Response({'error': 'Product not found'}, status=status.HTTP_404_NOT_FOUND) - - cart = self.get_cart(request) - cart_item = CartItem.objects.filter(cart=cart, product=product).first() - - if cart_item: - cart_item.delete() - return Response({'message': 'Item removed from cart successfully'}) - return Response({'error': 'Item not found in cart'}, status=status.HTTP_404_NOT_FOUND) - except Exception as e: - return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) - - @action(detail=False, methods=['post']) - def update_quantity(self, request): - """Update the quantity of an item in the cart""" - try: - product_id = request.data.get('product_id') - quantity = int(request.data.get('quantity', 1)) - - if not product_id: - return Response({'error': 'Product ID is required'}, status=status.HTTP_400_BAD_REQUEST) - - if quantity <= 0: - return Response({'error': 'Quantity must be greater than 0'}, status=status.HTTP_400_BAD_REQUEST) - - try: - product = Product.objects.get(id=product_id) - except Product.DoesNotExist: - return Response({'error': 'Product not found'}, status=status.HTTP_404_NOT_FOUND) - - # Check if product is in stock - if product.stock < quantity: - return Response({ - 'error': f'Not enough stock available. Available: {product.stock}' - }, status=status.HTTP_400_BAD_REQUEST) - - cart = self.get_cart(request) - try: - cart_item = CartItem.objects.get(cart=cart, product=product) - cart_item.quantity = quantity - cart_item.save() - serializer = CartItemSerializer(cart_item, context={'request': request}) - return Response(serializer.data) - except CartItem.DoesNotExist: - return Response({'error': 'Item not found in cart'}, status=status.HTTP_404_NOT_FOUND) - except Exception as e: - return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) - - @action(detail=False, methods=['post']) - def clear(self, request): - """Clear all items from the cart""" - cart = self.get_cart(request) - cart.clear() - return Response({'message': 'Cart cleared'}) - - @action(detail=False, methods=['delete']) - def delete(self, request): - """Delete the entire cart""" - cart = self.get_cart(request) - cart.delete() - return Response({'message': 'Cart deleted'}, status=status.HTTP_204_NO_CONTENT) - - @action(detail=False, methods=['post']) - def checkout(self, request): - """Convert cart to order""" - cart = self.get_cart(request) - - if not cart.items.exists(): - return Response({'error': 'Cart is empty'}, status=status.HTTP_400_BAD_REQUEST) - - # Check stock availability for all items - for cart_item in cart.items.all(): - if cart_item.product.stock < cart_item.quantity: - return Response({ - 'error': f'Not enough stock for {cart_item.product.name}. Available: {cart_item.product.stock}' - }, status=status.HTTP_400_BAD_REQUEST) - - with transaction.atomic(): - # Create order from cart - order = Order.objects.create( - user=request.user, - status='Pending', - shipping_address=request.user.address or '', - total_price=cart.get_total() - ) - - # Add cart items to order - for cart_item in cart.items.all(): - OrderItem.objects.create( - order=order, - product=cart_item.product, - quantity=cart_item.quantity, - price=cart_item.product.price - ) - - # Update product stock - product = cart_item.product - product.stock -= cart_item.quantity - product.save() - - # Clear the cart - cart.clear() - - return Response({ - 'message': 'Order created successfully', - 'order_id': order.id - }, status=status.HTTP_201_CREATED) - - @action(detail=False, methods=['post']) - def merge(self, request): - """Merge another cart into the current cart""" - other_cart_id = request.data.get('cart_id') - - if not other_cart_id: - return Response({'error': 'Cart ID is required'}, status=status.HTTP_400_BAD_REQUEST) - - try: - other_cart = Cart.objects.get(id=other_cart_id) - except Cart.DoesNotExist: - return Response({'error': 'Cart not found'}, status=status.HTTP_404_NOT_FOUND) - - if other_cart.user != request.user: - return Response({'error': 'Cannot merge carts from different users'}, - status=status.HTTP_403_FORBIDDEN) - - current_cart = self.get_cart(request) - - with transaction.atomic(): - for cart_item in other_cart.items.all(): - current_cart.add_product(cart_item.product, cart_item.quantity) - - # Delete the other cart - other_cart.delete() - - return Response({'message': 'Carts merged successfully'}) \ No newline at end of file diff --git a/Backend/app/models.py b/Backend/app/models.py deleted file mode 100644 index 4804ac1..0000000 --- a/Backend/app/models.py +++ /dev/null @@ -1,334 +0,0 @@ -from django.db import models -from django.contrib.auth.hashers import make_password, check_password -from django.dispatch import receiver -from rest_framework.authtoken.models import Token -from django.db.models.signals import post_save -from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin -from django.core.validators import MinValueValidator, MaxValueValidator - -class UserManager(BaseUserManager): - def create_user(self, username, email, password=None, **extra_fields): - if not email: - raise ValueError('Users must have an email address') - if not username: - raise ValueError('Users must have a username') - - email = self.normalize_email(email) - user = self.model( - username=username, - email=email, - **extra_fields - ) - user.set_password(password) - user.save(using=self._db) - return user - - def create_superuser(self, username, email, password=None, **extra_fields): - extra_fields.setdefault('is_staff', True) - extra_fields.setdefault('is_superuser', True) - - return self.create_user(username, email, password, **extra_fields) - -class User(AbstractBaseUser, PermissionsMixin): - id = models.BigAutoField(primary_key=True) - username = models.CharField(max_length=150, unique=True) - first_name = models.CharField(max_length=30) - last_name = models.CharField(max_length=30) - email = models.EmailField(unique=True) - is_active = models.BooleanField(default=True) - is_staff = models.BooleanField(default=False) - phone = models.CharField(max_length=15, blank=True, null=True) - address = models.TextField(blank=True, null=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - phone_number = models.CharField(max_length=15, blank=True, null=True) - - objects = UserManager() - - USERNAME_FIELD = 'username' - EMAIL_FIELD = 'email' - REQUIRED_FIELDS = ['email', 'first_name', 'last_name'] - - def __str__(self): - return self.username - - def set_password(self, raw_password): - self.password = make_password(raw_password) - - def check_password(self, raw_password): - return check_password(raw_password, self.password) - - class Meta: - verbose_name = 'User' - verbose_name_plural = 'Users' - ordering = ['username'] - - def get_full_name(self): - return f"{self.first_name} {self.last_name}" - - def get_short_name(self): - return self.first_name - -@receiver(post_save, sender=User) -def create_auth_token(sender, instance=None, created=False, **kwargs): - if created: - Token.objects.create(user=instance) - -class Category(models.Model): - id = models.BigAutoField(primary_key=True) - name = models.CharField(max_length=100, unique=True) - image = models.ImageField(upload_to='categories/', blank=True, null=True) - description = models.TextField(blank=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return self.name - class Meta: - verbose_name = 'Category' - verbose_name_plural = 'Categories' - ordering = ['name'] - @property - def image_url(self): - if self.image: - return f'/media/{self.image.name}' - return None - -class Product(models.Model): - id = models.BigAutoField(primary_key= True) - name = models.CharField(max_length=100) - description = models.TextField() - price = models.DecimalField(max_digits=10, decimal_places=2) - image = models.ImageField(upload_to='products/', blank=True, null=True) - category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE) - size = models.CharField(max_length=10, blank=True, null=True) - color = models.CharField(max_length=20, blank=True, null=True) - stock = models.IntegerField(default=0) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - average_rating = models.DecimalField(max_digits=3, decimal_places=2, default=0) - - def __str__(self): - return self.name - class Meta: - verbose_name = 'Product' - verbose_name_plural = 'Products' - ordering = ['name'] - @property - def image_url(self): - if self.image: - return f'/media/{self.image.name}' - return None - - def update_average_rating(self): - avg_rating = self.ratings.aggregate(avg_rating=models.Avg('rating'))['avg_rating'] - self.average_rating = avg_rating or 0 - self.save() - -class Rating(models.Model): - id = models.BigAutoField(primary_key= True) - product = models.ForeignKey(Product, related_name='ratings', on_delete=models.CASCADE) - user = models.ForeignKey(User, related_name='ratings', on_delete=models.CASCADE) - rating = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)]) - comment = models.TextField(blank=True, null=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - class Meta: - unique_together = ('product', 'user') - verbose_name = 'Rating' - verbose_name_plural = 'Ratings' - ordering = ['-created_at'] - - def __str__(self): - return f'{self.user.username} - {self.product.name} - {self.rating}' - -class Order(models.Model): - id = models.AutoField(primary_key=True) - user = models.ForeignKey(User, related_name='orders', on_delete=models.CASCADE) - total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0) - status = models.CharField(max_length=20, default='Pending', choices=[ - ('Pending', 'Pending'), - ('Shipped', 'Shipped'), - ('Delivered', 'Delivered'), - ('Cancelled', 'Cancelled') - ]) - shipping_address = models.CharField(max_length=255, blank=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return f'Order {self.id} by {self.user.username}' - - def update_total(self): - """Automatically calculate total price from order items""" - order_items = self.order_items.all() - if not order_items.exists(): - self.total_price = 0 - else: - self.total_price = sum(item.get_subtotal() for item in order_items) - self.save() - - def add_product(self, product, quantity=1): - """Add a product to the order""" - if product is None: - return None - - if quantity is None or quantity <= 0: - quantity = 1 - - order_item, created = OrderItem.objects.get_or_create( - order=self, - product=product, - defaults={ - 'quantity': quantity, - 'price': product.price if product.price else 0 - } - ) - - if not created: - order_item.quantity += quantity - order_item.save() - - self.update_total() - return order_item - - class Meta: - verbose_name = 'Order' - verbose_name_plural = 'Orders' - ordering = ['-created_at'] - -class OrderItem(models.Model): - order = models.ForeignKey(Order, related_name='order_items', on_delete=models.CASCADE) - product = models.ForeignKey(Product, related_name='order_items', on_delete=models.CASCADE) - quantity = models.IntegerField() - price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) - - def get_subtotal(self): - """Calculate the subtotal for this item""" - if self.price is None or self.quantity is None: - return 0 - return self.price * self.quantity - - def __str__(self): - return f'{self.product.name} - {self.quantity}' - - def save(self, *args, **kwargs): - # Ensure price is set correctly if it's none or a new order item - if self.price is None or (not self.pk and not self.price): - self.price = self.product.price if self.product and self.product.price else 0 - - # Ensure quantity is never None - if self.quantity is None: - self.quantity = 1 - - super().save(*args, **kwargs) - # Update the order total when an item is saved - if self.order: - self.order.update_total() - - def delete(self, *args, **kwargs): - order = self.order - super().delete(*args, **kwargs) - # Update the order total when an item is deleted - if order: - order.update_total() - - class Meta: - verbose_name = 'Order Item' - verbose_name_plural = 'Order Items' - ordering = ['order'] - -@receiver(post_save, sender=Order) -def update_order_status(sender, instance, created, **kwargs): - """When an order status changes to Delivered, update product stock""" - if not created and instance.status == 'Delivered': - for item in instance.order_items.all(): - product = item.product - # Only update stock if we haven't already updated it - if product.stock >= item.quantity: - product.stock -= item.quantity - product.save() - -class Cart(models.Model): - user = models.OneToOneField(User, related_name='cart', on_delete=models.CASCADE) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return f"Cart for {self.user.username}" - - def get_total(self): - """Calculate total price of all items in cart""" - return sum(item.get_subtotal() for item in self.items.all()) - - def add_product(self, product, quantity=1): - """Add a product to the cart""" - if quantity <= 0: - return None - - cart_item, created = CartItem.objects.get_or_create( - cart=self, - product=product, - defaults={'quantity': quantity} - ) - - if not created: - cart_item.quantity += quantity - cart_item.save() - - return cart_item - - def remove_product(self, product): - """Remove a product from the cart""" - try: - cart_item = CartItem.objects.get(cart=self, product=product) - cart_item.delete() - return True - except CartItem.DoesNotExist: - return False - - def clear(self): - """Remove all items from the cart""" - self.items.all().delete() - - class Meta: - verbose_name = 'Cart' - verbose_name_plural = 'Carts' - ordering = ['-updated_at'] - -class CartItem(models.Model): - cart = models.ForeignKey(Cart, related_name='items', on_delete=models.CASCADE) - product = models.ForeignKey(Product, related_name='cart_items', on_delete=models.CASCADE) - quantity = models.PositiveIntegerField(default=1) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return f"{self.quantity} x {self.product.name} in {self.cart}" - - def get_subtotal(self): - """Calculate the subtotal for this item""" - return self.product.price * self.quantity - - class Meta: - verbose_name = 'Cart Item' - verbose_name_plural = 'Cart Items' - ordering = ['-updated_at'] - unique_together = ['cart', 'product'] - -@receiver(post_save, sender=User) -def create_user_cart(sender, instance, created, **kwargs): - """Create a cart for new users""" - if created: - Cart.objects.create(user=instance) - -class Favorite(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='favorites') - product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='favorites') - created_at = models.DateTimeField(auto_now_add=True) - - class Meta: - unique_together = ['user', 'product'] - - def __str__(self): - return f"{self.user.username}'s favorite: {self.product.name}" \ No newline at end of file diff --git a/Backend/app/serializers.py b/Backend/app/serializers.py deleted file mode 100644 index e242dd2..0000000 --- a/Backend/app/serializers.py +++ /dev/null @@ -1,178 +0,0 @@ -from rest_framework import serializers -from .models import User, Category, Product, Rating, Order, OrderItem, CartItem, Cart, Favorite - -class UserSerializer(serializers.ModelSerializer): - password = serializers.CharField(write_only=True) - - class Meta: - model = User - fields = ['id', 'username', 'first_name', 'last_name', 'email', 'password', - 'phone', 'address', 'created_at'] - read_only_fields = ['id', 'created_at'] - - def create(self, validated_data): - # Use the create_user method instead of direct creation - user = User.objects.create_user( - username=validated_data['username'], - email=validated_data['email'], - password=validated_data['password'], - first_name=validated_data.get('first_name', ''), - last_name=validated_data.get('last_name', ''), - phone=validated_data.get('phone', ''), - address=validated_data.get('address', '') - ) - return user - - -class CategorySerializer(serializers.ModelSerializer): - class Meta: - model = Category - fields = ['id', 'name', 'image', 'created_at'] - read_only_fields = ['id', 'created_at'] - - -class RatingSerializer(serializers.ModelSerializer): - username = serializers.ReadOnlyField(source='user.username') - - class Meta: - model = Rating - fields = ['id', 'product', 'user', 'username', 'rating', 'comment', 'created_at'] - read_only_fields = ['id', 'created_at', 'username'] - - -class ProductSerializer(serializers.ModelSerializer): - category_name = serializers.ReadOnlyField(source='category.name') - average_rating = serializers.SerializerMethodField() - - class Meta: - model = Product - fields = ['id', 'name', 'description', 'price', 'image', 'category', 'stock','color','size', - 'category_name', 'average_rating', 'created_at'] - read_only_fields = ['id', 'created_at', 'category_name', 'average_rating'] - - def get_average_rating(self, obj): - ratings = obj.ratings.all() - if ratings: - return sum(rating.rating for rating in ratings) / len(ratings) - return 0 - - -class ProductDetailSerializer(ProductSerializer): - ratings = RatingSerializer(many=True, read_only=True) - - class Meta(ProductSerializer.Meta): - fields = ProductSerializer.Meta.fields + ['ratings'] - - -class OrderItemSerializer(serializers.ModelSerializer): - product_name = serializers.ReadOnlyField(source='product.name') - product_image = serializers.ImageField(source='product.image', read_only=True) - subtotal = serializers.SerializerMethodField() - - class Meta: - model = OrderItem - fields = ['id', 'product', 'product_name', 'product_image', 'quantity', 'price', 'subtotal'] - read_only_fields = ['id', 'product_name', 'product_image', 'subtotal'] - - def get_subtotal(self, obj): - return obj.get_subtotal() - - -class OrderItemCreateSerializer(serializers.ModelSerializer): - class Meta: - model = OrderItem - fields = ['product', 'quantity'] - - -class OrderSerializer(serializers.ModelSerializer): - user_name = serializers.ReadOnlyField(source='user.get_full_name') - order_items = OrderItemSerializer(many=True, read_only=True) - items = OrderItemCreateSerializer(many=True, write_only=True, required=False) - - class Meta: - model = Order - fields = ['id', 'user', 'user_name', 'total_price', 'status', 'shipping_address', - 'created_at', 'order_items', 'items'] - read_only_fields = ['id', 'created_at', 'user_name', 'total_price', 'order_items'] - - def create(self, validated_data): - items_data = validated_data.pop('items', []) - order = Order.objects.create(**validated_data) - - # Create order items if provided - for item_data in items_data: - product = item_data.get('product') - quantity = item_data.get('quantity', 1) - order.add_product(product, quantity) - - return order - - def update(self, instance, validated_data): - items_data = validated_data.pop('items', []) - - # Update the order fields - for attr, value in validated_data.items(): - setattr(instance, attr, value) - instance.save() - - # Add new items if provided - for item_data in items_data: - product = item_data.get('product') - quantity = item_data.get('quantity', 1) - instance.add_product(product, quantity) - - return instance - - -class CartItemSerializer(serializers.ModelSerializer): - product_name = serializers.CharField(source='product.name', read_only=True) - product_price = serializers.SerializerMethodField() - product_image = serializers.SerializerMethodField() - subtotal = serializers.SerializerMethodField() - - class Meta: - model = CartItem - fields = ['id', 'product', 'product_name', 'product_price', 'product_image', 'quantity', 'subtotal', 'created_at', 'updated_at'] - read_only_fields = ['created_at', 'updated_at'] - - def get_subtotal(self, obj): - try: - return float(obj.product.price * obj.quantity) - except (AttributeError, TypeError): - return 0.0 - - def get_product_price(self, obj): - try: - return float(obj.product.price) - except (AttributeError, TypeError): - return 0.0 - - def get_product_image(self, obj): - if obj.product and obj.product.image: - request = self.context.get('request') - if request is not None: - return request.build_absolute_uri(obj.product.image.url) - return obj.product.image.url - return None - - -class CartSerializer(serializers.ModelSerializer): - items = CartItemSerializer(many=True, read_only=True) - total = serializers.SerializerMethodField() - - class Meta: - model = Cart - fields = ['id', 'user', 'items', 'total', 'created_at', 'updated_at'] - read_only_fields = ['created_at', 'updated_at'] - - def get_total(self, obj): - return obj.get_total() - - -class FavoriteSerializer(serializers.ModelSerializer): - product = ProductSerializer(read_only=True) - - class Meta: - model = Favorite - fields = ['id', 'product', 'created_at'] - read_only_fields = ['id', 'created_at'] \ No newline at end of file diff --git a/Backend/app/views.py b/Backend/app/views.py deleted file mode 100644 index ea21869..0000000 --- a/Backend/app/views.py +++ /dev/null @@ -1,282 +0,0 @@ -from django.conf import settings -from rest_framework import viewsets, status, filters -from rest_framework.decorators import action -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny -from django.db.models import Avg -from django.shortcuts import get_object_or_404 -from django.http import HttpResponseForbidden, JsonResponse -from django.views.decorators.csrf import ensure_csrf_cookie -from django.middleware.csrf import get_token -from rest_framework.exceptions import PermissionDenied -from django.conf import settings -from django.http import FileResponse, HttpResponseForbidden -from django.views.decorators.http import require_GET -from rest_framework.authentication import TokenAuthentication -from rest_framework.decorators import authentication_classes, permission_classes -from rest_framework.permissions import IsAuthenticated -import os -from .models import User, Category, Product, Rating, Order, OrderItem, Favorite -from .serializers import ( - UserSerializer, CategorySerializer, ProductSerializer, ProductDetailSerializer, - RatingSerializer, OrderSerializer, OrderItemSerializer, FavoriteSerializer -) - -class UserViewSet(viewsets.ModelViewSet): - queryset = User.objects.all() - serializer_class = UserSerializer - filter_backends = [filters.SearchFilter] - search_fields = ['username', 'email', 'first_name', 'last_name'] - permission_classes = [AllowAny] - - def get_queryset(self): - # If user_id query param is provided, filter by that - user_id = self.request.query_params.get('user_id', None) - if user_id: - return User.objects.filter(id=user_id) - - # If no specific user_id is requested, and user is staff, show all - if self.request.user.is_authenticated and self.request.user.is_staff: - return User.objects.all() - - # For anonymous or non-staff users requesting the list endpoint, return empty queryset - if self.action == 'list': - return User.objects.none() - - # For other actions like retrieve, let Django's permission system handle it - return User.objects.all() - - @action(detail=False, methods=['get']) - def me(self, request): - """Get current user's profile""" - if not request.user.is_authenticated: - return Response({'error': 'Authentication required'}, status=status.HTTP_401_UNAUTHORIZED) - - serializer = self.get_serializer(request.user) - return Response(serializer.data) - - -class CategoryViewSet(viewsets.ModelViewSet): - queryset = Category.objects.all() - serializer_class = CategorySerializer - filter_backends = [filters.SearchFilter] - search_fields = ['name'] - permission_classes = [AllowAny] - - def get_permissions(self): - if self.action in ['create', 'update', 'partial_update', 'destroy']: - permission_classes = [IsAdminUser] - else: - permission_classes = [AllowAny] - return [permission() for permission in permission_classes] - - @action(detail=True, methods=['get']) - def products(self, request, pk=None): - category = self.get_object() - products = Product.objects.filter(category=category) - serializer = ProductSerializer(products, many=True) - return Response(serializer.data) - - -class ProductViewSet(viewsets.ModelViewSet): - queryset = Product.objects.all() - serializer_class = ProductSerializer - filter_backends = [filters.SearchFilter, filters.OrderingFilter] - search_fields = ['name', 'description', 'category__name'] - ordering_fields = ['name', 'price', 'created_at'] - permission_classes = [AllowAny] - - def get_permissions(self): - if self.action in ['create', 'update', 'partial_update', 'destroy']: - permission_classes = [IsAdminUser] - else: - permission_classes = [AllowAny] - return [permission() for permission in permission_classes] - - def get_serializer_class(self): - if self.action == 'retrieve': - return ProductDetailSerializer - return ProductSerializer - - @action(detail=True, methods=['post']) - def rate(self, request, pk=None): - # Still require authentication for rating - if not request.user.is_authenticated: - return Response({'error': 'Authentication required'}, status=status.HTTP_401_UNAUTHORIZED) - - product = self.get_object() - rating_value = request.data.get('rating') - comment = request.data.get('comment', '') - - if not rating_value: - return Response({'error': 'Rating is required'}, status=status.HTTP_400_BAD_REQUEST) - - # Update or create rating - rating, created = Rating.objects.update_or_create( - product=product, - user=request.user, - defaults={'rating': rating_value, 'comment': comment} - ) - - serializer = RatingSerializer(rating) - return Response(serializer.data) - - -class RatingViewSet(viewsets.ModelViewSet): - queryset = Rating.objects.all() - serializer_class = RatingSerializer - permission_classes = [AllowAny] - - def get_permissions(self): - if self.action in ['update', 'partial_update', 'destroy']: - permission_classes = [IsAuthenticated] - else: - permission_classes = [AllowAny] - return [permission() for permission in permission_classes] - - def perform_create(self, serializer): - # Still require authentication for creating ratings - if not self.request.user.is_authenticated: - raise PermissionDenied('Authentication required to create ratings') - serializer.save(user=self.request.user) - - -class OrderViewSet(viewsets.ModelViewSet): - queryset = Order.objects.all() - serializer_class = OrderSerializer - filter_backends = [filters.OrderingFilter] - ordering_fields = ['created_at', 'status'] - permission_classes = [AllowAny] - - def get_queryset(self): - user_id = self.request.query_params.get('user_id', None) - if user_id: - return Order.objects.filter(user_id=user_id) - - if self.request.user.is_authenticated: - if self.request.user.is_staff: - return Order.objects.all() - return Order.objects.filter(user=self.request.user) - - # For anonymous users, return empty queryset - return Order.objects.none() - - def perform_create(self, serializer): - # For creating orders, require authentication - if not self.request.user.is_authenticated: - raise PermissionDenied('Authentication required to create orders') - serializer.save(user=self.request.user) - - @action(detail=True, methods=['post']) - def cancel(self, request, pk=None): - order = self.get_object() - - # Check if order belongs to current user or user is admin - if self.request.user.is_authenticated and (order.user.id == request.user.id or request.user.is_staff): - # Check if order can be canceled - if order.status not in ['Pending', 'Shipped']: - return Response({'error': 'Cannot cancel order in current status'}, - status=status.HTTP_400_BAD_REQUEST) - - order.status = 'Cancelled' - order.save() - serializer = OrderSerializer(order) - return Response(serializer.data) - else: - return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) - - -class OrderItemViewSet(viewsets.ModelViewSet): - queryset = OrderItem.objects.all() - serializer_class = OrderItemSerializer - permission_classes = [AllowAny] - - def get_queryset(self): - user_id = self.request.query_params.get('user_id', None) - if user_id: - return OrderItem.objects.filter(order__user_id=user_id) - - if self.request.user.is_authenticated: - if self.request.user.is_staff: - return OrderItem.objects.all() - return OrderItem.objects.filter(order__user=self.request.user) - - # For anonymous users, return empty queryset - return OrderItem.objects.none() - -def get_csrf_token(request): - """ - This view sets the CSRF cookie and returns the token in the response. - The client can then use this token for subsequent requests. - """ - token = get_token(request) - return JsonResponse({'csrfToken': token}) -@require_GET -@authentication_classes([TokenAuthentication]) -@permission_classes([IsAuthenticated]) -def serve_protected_media(request, path): - """ - Serve media files with authentication check. - The path parameter should be the relative path within MEDIA_ROOT. - """ - # Construct the full path - file_path = os.path.join(settings.MEDIA_ROOT, path) - - # Basic security check to prevent directory traversal - if not os.path.abspath(file_path).startswith(os.path.abspath(settings.MEDIA_ROOT)): - return HttpResponseForbidden('Invalid path') - - # Check if file exists - if not os.path.exists(file_path): - return HttpResponseForbidden('File not found') - - # Serve the file - return FileResponse(open(file_path, 'rb')) - -class FavoriteViewSet(viewsets.ViewSet): - permission_classes = [IsAuthenticated] - - def list(self, request): - """Get all favorites for the current user""" - favorites = Favorite.objects.filter(user=request.user) - serializer = FavoriteSerializer(favorites, many=True, context={'request': request}) - return Response(serializer.data) - - @action(detail=False, methods=['post']) - def toggle(self, request): - """Toggle favorite status for a product""" - product_id = request.data.get('product_id') - if not product_id: - return Response({'error': 'Product ID is required'}, status=status.HTTP_400_BAD_REQUEST) - - try: - product = Product.objects.get(id=product_id) - except Product.DoesNotExist: - return Response({'error': 'Product not found'}, status=status.HTTP_404_NOT_FOUND) - - # Check if favorite exists - favorite = Favorite.objects.filter(user=request.user, product=product).first() - - if favorite: - # Remove from favorites - favorite.delete() - return Response({'message': 'Product removed from favorites'}) - else: - # Add to favorites - Favorite.objects.create(user=request.user, product=product) - return Response({'message': 'Product added to favorites'}) - - @action(detail=False, methods=['get']) - def check(self, request): - """Check if a product is favorited""" - product_id = request.query_params.get('product_id') - if not product_id: - return Response({'error': 'Product ID is required'}, status=status.HTTP_400_BAD_REQUEST) - - try: - product = Product.objects.get(id=product_id) - except Product.DoesNotExist: - return Response({'error': 'Product not found'}, status=status.HTTP_404_NOT_FOUND) - - is_favorited = Favorite.objects.filter(user=request.user, product=product).exists() - return Response({'is_favorited': is_favorited}) \ No newline at end of file diff --git a/Backend/.gitignore b/backend/.gitignore similarity index 100% rename from Backend/.gitignore rename to backend/.gitignore diff --git a/Backend/.python-version b/backend/.python-version similarity index 100% rename from Backend/.python-version rename to backend/.python-version diff --git a/Backend/README.md b/backend/README.md similarity index 100% rename from Backend/README.md rename to backend/README.md diff --git a/Backend/app/__init__.py b/backend/app/__init__.py similarity index 100% rename from Backend/app/__init__.py rename to backend/app/__init__.py diff --git a/backend/app/admin.py b/backend/app/admin.py new file mode 100644 index 0000000..087174c --- /dev/null +++ b/backend/app/admin.py @@ -0,0 +1,239 @@ +from django.contrib import admin +from .models import User, Category, Product, Rating, Order, OrderItem, Cart, CartItem, PrivacyPolicy, FAQ, Contact, Slider, Coupon +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin + + +@admin.register(User) +class UserAdmin(BaseUserAdmin): + list_display = ( + "username", + "email", + "first_name", + "last_name", + "gender", + "is_staff", + "created_at", + ) + search_fields = ("username", "email", "first_name", "last_name") + list_filter = ("is_staff", "is_active", "created_at") + readonly_fields = ("created_at", "updated_at") + fieldsets = ( + ( + "Personal Info", + { + "fields": ( + "username", + "first_name", + "last_name", + "gender", + "email", + "password", + ) + }, + ), + ("Customer Details", {"fields": ("phone", "address")}), + ( + "Permissions", + { + "fields": ( + "is_active", + "is_staff", + "is_superuser", + "groups", + "user_permissions", + ) + }, + ), + ("Dates", {"fields": ("created_at", "updated_at", "last_login")}), + ) + add_fieldsets = ( + ( + None, + { + "classes": ("wide",), + "fields": ( + "username", + "email", + "first_name", + "last_name", + "password1", + "password2", + "phone", + "address", + "gender", + ), + }, + ), + ) + + +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ("name", "created_at") + search_fields = ("name",) + list_filter = ("created_at",) + readonly_fields = ("created_at", "updated_at") + + +class RatingInline(admin.TabularInline): + model = Rating + extra = 0 + readonly_fields = ("created_at",) + + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ( + "name", + "category", + "price", + "stock", + "color", + "size", + "created_at", + "updated_at", + ) + search_fields = ("name", "description") + list_filter = ("category", "created_at") + readonly_fields = ("created_at", "updated_at") + inlines = [RatingInline] + + +@admin.register(Rating) +class RatingAdmin(admin.ModelAdmin): + list_display = ("user", "product", "rating", "created_at") + search_fields = ("user__username", "product__name", "comment") + list_filter = ("rating", "created_at") + readonly_fields = ("created_at", "updated_at") + + +class OrderItemInline(admin.TabularInline): + model = OrderItem + extra = 0 + readonly_fields = ("get_subtotal",) + + +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + list_display = ("id", "user", "total_price", "status", "created_at") + search_fields = ("user__username", "shipping_address") + list_filter = ("status", "created_at") + readonly_fields = ("created_at", "updated_at", "total_price") + inlines = [OrderItemInline] + list_editable = ("status",) + + +class OrderItemAdmin(admin.ModelAdmin): + list_display = ("order", "product", "quantity", "price", "get_subtotal") + list_filter = ("order__status",) + search_fields = ("order__id", "product__name") + readonly_fields = ("get_subtotal",) + + def get_subtotal(self, obj): + return obj.get_subtotal() + + get_subtotal.short_description = "Subtotal" + + +admin.site.register(OrderItem, OrderItemAdmin) + + +class CartItemInline(admin.TabularInline): + model = CartItem + extra = 0 + readonly_fields = ["created_at", "updated_at"] + + +@admin.register(Cart) +class CartAdmin(admin.ModelAdmin): + list_display = ["user", "get_total", "created_at", "updated_at"] + list_filter = ["created_at", "updated_at"] + search_fields = ["user__username", "user__email"] + readonly_fields = ["created_at", "updated_at"] + inlines = [CartItemInline] + + def get_total(self, obj): + return obj.get_total() + + get_total.short_description = "Total" + + def get_subtotal(self, obj): + return obj.get_subtotal() + + get_subtotal.short_description = "Subtotal" + + +@admin.register(PrivacyPolicy) +class PrivacyPolicyAdmin(admin.ModelAdmin): + list_display = ('title', 'last_updated', 'is_active') + list_filter = ('is_active', 'last_updated') + search_fields = ('title', 'content') + readonly_fields = ('last_updated',) + fieldsets = ( + ('Policy Information', { + 'fields': ('title', 'content', 'is_active') + }), + ('Timestamps', { + 'fields': ('last_updated',), + 'classes': ('collapse',) + }), + ) + + +@admin.register(FAQ) +class FAQAdmin(admin.ModelAdmin): + list_display = ('question', 'category', 'is_active', 'created_at') + list_filter = ('category', 'is_active', 'created_at') + search_fields = ('question', 'answer') + readonly_fields = ('created_at', 'updated_at') + fieldsets = ( + ('FAQ Information', { + 'fields': ('question', 'answer', 'category', 'is_active') + }), + ('Timestamps', { + 'fields': ('created_at', 'updated_at'), + 'classes': ('collapse',) + }), + ) + + +@admin.register(Contact) +class ContactAdmin(admin.ModelAdmin): + list_display = ('contact_type', 'title', 'value', 'is_active', 'created_at') + list_filter = ('contact_type', 'is_active', 'created_at') + search_fields = ('title', 'value') + readonly_fields = ('created_at', 'updated_at') + fieldsets = ( + ('Contact Information', { + 'fields': ('contact_type', 'title', 'value', 'is_active') + }), + ('Timestamps', { + 'fields': ('created_at', 'updated_at'), + 'classes': ('collapse',) + }), + ) + + +@admin.register(Slider) +class SliderAdmin(admin.ModelAdmin): + list_display = ('title', 'category', 'is_active', 'created_at', 'updated_at') + list_filter = ('is_active', 'created_at', 'category') + search_fields = ('title', 'description') + readonly_fields = ('created_at', 'updated_at') + fieldsets = ( + ('Slider Information', { + 'fields': ('title', 'description', 'image', 'category', 'is_active') + }), + ('Timestamps', { + 'fields': ('created_at', 'updated_at'), + 'classes': ('collapse',) + }), + ) + + +@admin.register(Coupon) +class CouponAdmin(admin.ModelAdmin): + list_display = ('code', 'discount_type', 'discount_value', 'min_purchase', 'is_active', 'start_date', 'end_date') + list_filter = ('is_active', 'discount_type') + search_fields = ('code',) + ordering = ('-created_at',) diff --git a/Backend/app/apps.py b/backend/app/apps.py similarity index 100% rename from Backend/app/apps.py rename to backend/app/apps.py diff --git a/Backend/app/auth.py b/backend/app/auth.py similarity index 100% rename from Backend/app/auth.py rename to backend/app/auth.py diff --git a/backend/app/carts.py b/backend/app/carts.py new file mode 100644 index 0000000..8a5f4cc --- /dev/null +++ b/backend/app/carts.py @@ -0,0 +1,242 @@ +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from django.shortcuts import get_object_or_404 +from django.db import transaction +import logging +from .models import Cart, CartItem, Product, Order, OrderItem +from .serializers import CartSerializer, CartItemSerializer + +logger = logging.getLogger(__name__) + +class CartViewSet(viewsets.ViewSet): + permission_classes = [IsAuthenticated] + + def get_cart(self, request): + """Get or create cart for the authenticated user""" + try: + cart, created = Cart.objects.get_or_create(user=request.user) + return cart + except Exception as e: + logger.error(f"Error getting cart: {str(e)}") + return None + + @action(detail=False, methods=['get']) + def items(self, request): + """Get all items in the cart""" + try: + cart = self.get_cart(request) + if not cart: + return Response({'error': 'Failed to get cart'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + serializer = CartSerializer(cart, context={'request': request}) + return Response(serializer.data) + except Exception as e: + logger.error(f"Error getting cart items: {str(e)}") + return Response({'error': 'Failed to get cart items'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=False, methods=['post']) + def add_item(self, request): + """Add an item to the cart""" + try: + product_id = request.data.get('product_id') + quantity = int(request.data.get('quantity', 1)) + + if not product_id: + return Response({'error': 'Product ID is required'}, status=status.HTTP_400_BAD_REQUEST) + + try: + product = Product.objects.get(id=product_id) + except Product.DoesNotExist: + return Response({'error': 'Product not found'}, status=status.HTTP_404_NOT_FOUND) + + # Check if product is in stock + if product.stock < quantity: + return Response({ + 'error': f'Not enough stock available. Available: {product.stock}' + }, status=status.HTTP_400_BAD_REQUEST) + + cart = self.get_cart(request) + if not cart: + return Response({'error': 'Failed to get cart'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + cart_item = cart.add_product(product, quantity) + + if cart_item: + serializer = CartItemSerializer(cart_item, context={'request': request}) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response({'error': 'Invalid quantity'}, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + logger.error(f"Error adding item to cart: {str(e)}") + return Response({'error': 'Failed to add item to cart'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=False, methods=['post']) + def remove_item(self, request): + """Remove an item from the cart""" + try: + product_id = request.data.get('product_id') + if not product_id: + return Response({'error': 'Product ID is required'}, status=status.HTTP_400_BAD_REQUEST) + + try: + product = Product.objects.get(id=product_id) + except Product.DoesNotExist: + return Response({'error': 'Product not found'}, status=status.HTTP_404_NOT_FOUND) + + cart = self.get_cart(request) + if not cart: + return Response({'error': 'Failed to get cart'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + cart_item = CartItem.objects.filter(cart=cart, product=product).first() + + if cart_item: + cart_item.delete() + return Response({'message': 'Item removed from cart successfully'}) + return Response({'error': 'Item not found in cart'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"Error removing item from cart: {str(e)}") + return Response({'error': 'Failed to remove item from cart'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=False, methods=['post']) + def update_quantity(self, request): + """Update the quantity of an item in the cart""" + try: + product_id = request.data.get('product_id') + quantity = int(request.data.get('quantity', 1)) + + if not product_id: + return Response({'error': 'Product ID is required'}, status=status.HTTP_400_BAD_REQUEST) + + if quantity <= 0: + return Response({'error': 'Quantity must be greater than 0'}, status=status.HTTP_400_BAD_REQUEST) + + try: + product = Product.objects.get(id=product_id) + except Product.DoesNotExist: + return Response({'error': 'Product not found'}, status=status.HTTP_404_NOT_FOUND) + + # Check if product is in stock + if product.stock < quantity: + return Response({ + 'error': f'Not enough stock available. Available: {product.stock}' + }, status=status.HTTP_400_BAD_REQUEST) + + cart = self.get_cart(request) + if not cart: + return Response({'error': 'Failed to get cart'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + try: + cart_item = CartItem.objects.get(cart=cart, product=product) + cart_item.quantity = quantity + cart_item.save() + serializer = CartItemSerializer(cart_item, context={'request': request}) + return Response(serializer.data) + except CartItem.DoesNotExist: + return Response({'error': 'Item not found in cart'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"Error updating cart quantity: {str(e)}") + return Response({'error': 'Failed to update cart quantity'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=False, methods=['post']) + def clear(self, request): + """Clear all items from the cart""" + try: + cart = self.get_cart(request) + if not cart: + return Response({'error': 'Failed to get cart'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + cart.items.all().delete() + return Response({'message': 'Cart cleared'}) + except Exception as e: + logger.error(f"Error clearing cart: {str(e)}") + return Response({'error': 'Failed to clear cart'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=False, methods=['delete']) + def delete(self, request): + """Delete the entire cart""" + try: + cart = self.get_cart(request) + if not cart: + return Response({'error': 'Failed to get cart'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + cart.delete() + return Response({'message': 'Cart deleted'}, status=status.HTTP_204_NO_CONTENT) + except Exception as e: + logger.error(f"Error deleting cart: {str(e)}") + return Response({'error': 'Failed to delete cart'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=False, methods=['post']) + def checkout(self, request): + """Convert cart to order""" + try: + cart = self.get_cart(request) + if not cart: + return Response({'error': 'Failed to get cart'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + if not cart.items.exists(): + return Response({'error': 'Cart is empty'}, status=status.HTTP_400_BAD_REQUEST) + + # Check stock availability for all items + for cart_item in cart.items.all(): + if cart_item.product.stock < cart_item.quantity: + return Response({ + 'error': f'Not enough stock for {cart_item.product.name}. Available: {cart_item.product.stock}' + }, status=status.HTTP_400_BAD_REQUEST) + + with transaction.atomic(): + # Create order from cart + order = Order.objects.create( + user=request.user, + status='Pending', + shipping_address=request.user.address or '', + total_price=cart.get_total() + ) + + # Add cart items to order + for cart_item in cart.items.all(): + OrderItem.objects.create( + order=order, + product=cart_item.product, + quantity=cart_item.quantity, + price=cart_item.product.price + ) + + # Clear the cart + cart.items.all().delete() + + return Response({ + 'message': 'Order created successfully', + 'order_id': order.id + }, status=status.HTTP_201_CREATED) + except Exception as e: + logger.error(f"Error during checkout: {str(e)}") + return Response({'error': 'Failed to process checkout'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=False, methods=['post']) + def merge(self, request): + """Merge another cart into the current cart""" + other_cart_id = request.data.get('cart_id') + + if not other_cart_id: + return Response({'error': 'Cart ID is required'}, status=status.HTTP_400_BAD_REQUEST) + + try: + other_cart = Cart.objects.get(id=other_cart_id) + except Cart.DoesNotExist: + return Response({'error': 'Cart not found'}, status=status.HTTP_404_NOT_FOUND) + + if other_cart.user != request.user: + return Response({'error': 'Cannot merge carts from different users'}, + status=status.HTTP_403_FORBIDDEN) + + current_cart = self.get_cart(request) + + with transaction.atomic(): + for cart_item in other_cart.items.all(): + current_cart.add_product(cart_item.product, cart_item.quantity) + + # Delete the other cart + other_cart.delete() + + return Response({'message': 'Carts merged successfully'}) \ No newline at end of file diff --git a/Backend/app/middleware.py b/backend/app/middleware.py similarity index 100% rename from Backend/app/middleware.py rename to backend/app/middleware.py diff --git a/Backend/app/migrations/0001_initial.py b/backend/app/migrations/0001_initial.py similarity index 100% rename from Backend/app/migrations/0001_initial.py rename to backend/app/migrations/0001_initial.py diff --git a/Backend/app/migrations/0002_cart_cartitem.py b/backend/app/migrations/0002_cart_cartitem.py similarity index 100% rename from Backend/app/migrations/0002_cart_cartitem.py rename to backend/app/migrations/0002_cart_cartitem.py diff --git a/Backend/app/migrations/0003_category_description_product_average_rating_and_more.py b/backend/app/migrations/0003_category_description_product_average_rating_and_more.py similarity index 100% rename from Backend/app/migrations/0003_category_description_product_average_rating_and_more.py rename to backend/app/migrations/0003_category_description_product_average_rating_and_more.py diff --git a/backend/app/migrations/0004_user_gender.py b/backend/app/migrations/0004_user_gender.py new file mode 100644 index 0000000..3009d21 --- /dev/null +++ b/backend/app/migrations/0004_user_gender.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2 on 2025-04-23 17:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("app", "0003_category_description_product_average_rating_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="gender", + field=models.CharField( + choices=[("Male", "Male"), ("Female", "Female"), ("Other", "Other")], + default="Male", + max_length=10, + ), + ), + ] diff --git a/backend/app/migrations/0005_user_profile_picture.py b/backend/app/migrations/0005_user_profile_picture.py new file mode 100644 index 0000000..1ece3b1 --- /dev/null +++ b/backend/app/migrations/0005_user_profile_picture.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2025-04-23 18:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0004_user_gender'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='profile_picture', + field=models.ImageField(blank=True, null=True, upload_to='profile_pictures/'), + ), + ] diff --git a/backend/app/migrations/0006_privacypolicy.py b/backend/app/migrations/0006_privacypolicy.py new file mode 100644 index 0000000..66e14d9 --- /dev/null +++ b/backend/app/migrations/0006_privacypolicy.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2 on 2025-04-23 22:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0005_user_profile_picture'), + ] + + operations = [ + migrations.CreateModel( + name='PrivacyPolicy', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('content', models.TextField()), + ('last_updated', models.DateTimeField(auto_now=True)), + ('is_active', models.BooleanField(default=True)), + ], + options={ + 'verbose_name_plural': 'Privacy Policies', + }, + ), + ] diff --git a/backend/app/migrations/0007_faq.py b/backend/app/migrations/0007_faq.py new file mode 100644 index 0000000..8ff9a60 --- /dev/null +++ b/backend/app/migrations/0007_faq.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2 on 2025-04-23 22:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0006_privacypolicy'), + ] + + operations = [ + migrations.CreateModel( + name='FAQ', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('question', models.CharField(max_length=255)), + ('answer', models.TextField()), + ('category', models.CharField(choices=[('All', 'All'), ('Services', 'Services'), ('General', 'General'), ('Account', 'Account')], default='General', max_length=20)), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name_plural': 'FAQs', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/backend/app/migrations/0008_contact.py b/backend/app/migrations/0008_contact.py new file mode 100644 index 0000000..cdf6e27 --- /dev/null +++ b/backend/app/migrations/0008_contact.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2 on 2025-04-23 22:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0007_faq'), + ] + + operations = [ + migrations.CreateModel( + name='Contact', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('contact_type', models.CharField(choices=[('customer_service', 'Customer Service'), ('whatsapp', 'WhatsApp'), ('website', 'Website'), ('facebook', 'Facebook'), ('instagram', 'Instagram'), ('twitter', 'Twitter')], max_length=20)), + ('title', models.CharField(max_length=100)), + ('value', models.CharField(max_length=200)), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'ordering': ['contact_type'], + }, + ), + ] diff --git a/backend/app/migrations/0009_alter_contact_value.py b/backend/app/migrations/0009_alter_contact_value.py new file mode 100644 index 0000000..fec695c --- /dev/null +++ b/backend/app/migrations/0009_alter_contact_value.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2025-04-23 22:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0008_contact'), + ] + + operations = [ + migrations.AlterField( + model_name='contact', + name='value', + field=models.CharField(max_length=255), + ), + ] diff --git a/backend/app/migrations/0010_invitemessage.py b/backend/app/migrations/0010_invitemessage.py new file mode 100644 index 0000000..fc5bbfe --- /dev/null +++ b/backend/app/migrations/0010_invitemessage.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2 on 2025-04-23 23:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0009_alter_contact_value'), + ] + + operations = [ + migrations.CreateModel( + name='InviteMessage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('message', models.TextField()), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'Invite Message', + 'verbose_name_plural': 'Invite Messages', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/backend/app/migrations/0011_invitemessage_link.py b/backend/app/migrations/0011_invitemessage_link.py new file mode 100644 index 0000000..4058ec9 --- /dev/null +++ b/backend/app/migrations/0011_invitemessage_link.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2025-04-23 23:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0010_invitemessage'), + ] + + operations = [ + migrations.AddField( + model_name='invitemessage', + name='link', + field=models.URLField(default='https://play.google.com/store/apps/details?id=com.example.app', help_text='App store or download link', max_length=500), + ), + ] diff --git a/backend/app/migrations/0012_alter_invitemessage_link.py b/backend/app/migrations/0012_alter_invitemessage_link.py new file mode 100644 index 0000000..6275b71 --- /dev/null +++ b/backend/app/migrations/0012_alter_invitemessage_link.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2025-04-23 23:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0011_invitemessage_link'), + ] + + operations = [ + migrations.AlterField( + model_name='invitemessage', + name='link', + field=models.URLField(help_text='App store or download link', max_length=500), + ), + ] diff --git a/backend/app/migrations/0013_delete_invitemessage.py b/backend/app/migrations/0013_delete_invitemessage.py new file mode 100644 index 0000000..1457457 --- /dev/null +++ b/backend/app/migrations/0013_delete_invitemessage.py @@ -0,0 +1,16 @@ +# Generated by Django 5.2 on 2025-04-23 23:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0012_alter_invitemessage_link'), + ] + + operations = [ + migrations.DeleteModel( + name='InviteMessage', + ), + ] diff --git a/backend/app/migrations/0014_slider.py b/backend/app/migrations/0014_slider.py new file mode 100644 index 0000000..1cd35aa --- /dev/null +++ b/backend/app/migrations/0014_slider.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2 on 2025-04-24 00:30 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0013_delete_invitemessage'), + ] + + operations = [ + migrations.CreateModel( + name='Slider', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('description', models.TextField()), + ('image', models.ImageField(upload_to='sliders/')), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='app.category')), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/backend/app/migrations/0015_alter_slider_category.py b/backend/app/migrations/0015_alter_slider_category.py new file mode 100644 index 0000000..4f7c2cd --- /dev/null +++ b/backend/app/migrations/0015_alter_slider_category.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2 on 2025-04-24 00:32 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0014_slider'), + ] + + operations = [ + migrations.AlterField( + model_name='slider', + name='category', + field=models.ForeignKey(blank=True, help_text="Category to show when clicking the 'Show Now' button", null=True, on_delete=django.db.models.deletion.SET_NULL, to='app.category'), + ), + ] diff --git a/backend/app/migrations/0016_alter_slider_image.py b/backend/app/migrations/0016_alter_slider_image.py new file mode 100644 index 0000000..9eb0bb4 --- /dev/null +++ b/backend/app/migrations/0016_alter_slider_image.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2025-04-24 00:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0015_alter_slider_category'), + ] + + operations = [ + migrations.AlterField( + model_name='slider', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='sliders/'), + ), + ] diff --git a/backend/app/migrations/0017_coupon.py b/backend/app/migrations/0017_coupon.py new file mode 100644 index 0000000..8e363d3 --- /dev/null +++ b/backend/app/migrations/0017_coupon.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2 on 2025-04-24 10:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0016_alter_slider_image'), + ] + + 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=50, unique=True)), + ('discount_type', models.CharField(choices=[('percentage', 'Percentage'), ('fixed', 'Fixed Amount')], default='percentage', max_length=10)), + ('discount_value', models.DecimalField(decimal_places=2, max_digits=10)), + ('min_purchase', models.DecimalField(decimal_places=2, default=0, max_digits=10)), + ('max_discount', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('is_active', models.BooleanField(default=True)), + ('start_date', models.DateTimeField()), + ('end_date', models.DateTimeField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'Coupon', + 'verbose_name_plural': 'Coupons', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/backend/app/migrations/0018_alter_user_gender.py b/backend/app/migrations/0018_alter_user_gender.py new file mode 100644 index 0000000..fcb7d8d --- /dev/null +++ b/backend/app/migrations/0018_alter_user_gender.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2025-04-24 10:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0017_coupon'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='gender', + field=models.CharField(blank=True, choices=[('Male', 'Male'), ('Female', 'Female'), ('Other', 'Other')], default='Male', max_length=10), + ), + ] diff --git a/backend/app/migrations/0019_notification.py b/backend/app/migrations/0019_notification.py new file mode 100644 index 0000000..2d8ca9c --- /dev/null +++ b/backend/app/migrations/0019_notification.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2 on 2025-04-24 11:46 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0018_alter_user_gender'), + ] + + operations = [ + migrations.CreateModel( + name='Notification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(choices=[('order_placed', 'Order Placed'), ('order_shipped', 'Order Shipped'), ('order_delivered', 'Order Delivered'), ('order_cancelled', 'Order Cancelled'), ('coupon', 'Coupon')], max_length=20)), + ('message', models.TextField()), + ('read', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('coupon', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='app.coupon')), + ('order', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='app.order')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/Backend/app/migrations/__init__.py b/backend/app/migrations/__init__.py similarity index 100% rename from Backend/app/migrations/__init__.py rename to backend/app/migrations/__init__.py diff --git a/backend/app/models.py b/backend/app/models.py new file mode 100644 index 0000000..95cf3c0 --- /dev/null +++ b/backend/app/models.py @@ -0,0 +1,586 @@ +from django.db import models +from django.contrib.auth.hashers import make_password, check_password +from django.dispatch import receiver +from rest_framework.authtoken.models import Token +from django.db.models.signals import post_save +from django.contrib.auth.models import ( + AbstractBaseUser, + BaseUserManager, + PermissionsMixin, +) +from django.core.validators import MinValueValidator, MaxValueValidator +import logging + +logger = logging.getLogger(__name__) + + +GENDER_CHOICES = (("Male", "Male"), ("Female", "Female"), ("Other", "Other")) + + +class UserManager(BaseUserManager): + def create_user(self, username, email, password=None, **extra_fields): + if not email: + raise ValueError("Users must have an email address") + if not username: + raise ValueError("Users must have a username") + + email = self.normalize_email(email) + user = self.model(username=username, email=email, **extra_fields) + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, username, email, password=None, **extra_fields): + extra_fields.setdefault("is_staff", True) + extra_fields.setdefault("is_superuser", True) + + return self.create_user(username, email, password, **extra_fields) + + +class User(AbstractBaseUser, PermissionsMixin): + id = models.BigAutoField(primary_key=True) + username = models.CharField(max_length=150, unique=True) + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) + email = models.EmailField(unique=True) + gender = models.CharField(max_length=10, choices=GENDER_CHOICES, default="Male", blank=True) + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) + phone = models.CharField(max_length=15, blank=True, null=True) + address = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + phone_number = models.CharField(max_length=15, blank=True, null=True) + profile_picture = models.ImageField(upload_to='profile_pictures/', blank=True, null=True) + + objects = UserManager() + + USERNAME_FIELD = "username" + EMAIL_FIELD = "email" + REQUIRED_FIELDS = ["email", "first_name", "last_name"] + + def __str__(self): + return self.username + + def set_password(self, raw_password): + self.password = make_password(raw_password) + + def check_password(self, raw_password): + return check_password(raw_password, self.password) + + class Meta: + verbose_name = "User" + verbose_name_plural = "Users" + ordering = ["username"] + + def get_full_name(self): + return f"{self.first_name} {self.last_name}" + + def get_short_name(self): + return self.first_name + + +@receiver(post_save, sender=User) +def create_auth_token(sender, instance=None, created=False, **kwargs): + if created: + Token.objects.create(user=instance) + + +class Category(models.Model): + id = models.BigAutoField(primary_key=True) + name = models.CharField(max_length=100, unique=True) + image = models.ImageField(upload_to="categories/", blank=True, null=True) + description = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Category" + verbose_name_plural = "Categories" + ordering = ["name"] + + @property + def image_url(self): + if self.image: + return f"/media/{self.image.name}" + return None + + +class Product(models.Model): + id = models.BigAutoField(primary_key=True) + name = models.CharField(max_length=100) + description = models.TextField() + price = models.DecimalField(max_digits=10, decimal_places=2) + image = models.ImageField(upload_to="products/", blank=True, null=True) + category = models.ForeignKey( + Category, related_name="products", on_delete=models.CASCADE + ) + size = models.CharField(max_length=10, blank=True, null=True) + color = models.CharField(max_length=20, blank=True, null=True) + stock = models.IntegerField(default=0) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + average_rating = models.DecimalField(max_digits=3, decimal_places=2, default=0) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Product" + verbose_name_plural = "Products" + ordering = ["name"] + + @property + def image_url(self): + if self.image: + return f"/media/{self.image.name}" + return None + + def update_average_rating(self): + avg_rating = self.ratings.aggregate(avg_rating=models.Avg("rating"))[ + "avg_rating" + ] + self.average_rating = avg_rating or 0 + self.save() + + +class Rating(models.Model): + id = models.BigAutoField(primary_key=True) + product = models.ForeignKey( + Product, related_name="ratings", on_delete=models.CASCADE + ) + user = models.ForeignKey(User, related_name="ratings", on_delete=models.CASCADE) + rating = models.IntegerField( + validators=[MinValueValidator(1), MaxValueValidator(5)] + ) + comment = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + unique_together = ("product", "user") + verbose_name = "Rating" + verbose_name_plural = "Ratings" + ordering = ["-created_at"] + + def __str__(self): + return f"{self.user.username} - {self.product.name} - {self.rating}" + + +class Order(models.Model): + id = models.AutoField(primary_key=True) + user = models.ForeignKey(User, related_name="orders", on_delete=models.CASCADE) + total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0) + status = models.CharField( + max_length=20, + default="Pending", + choices=[ + ("Pending", "Pending"), + ("Shipped", "Shipped"), + ("Delivered", "Delivered"), + ("Cancelled", "Cancelled"), + ], + ) + shipping_address = models.CharField(max_length=255, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Order {self.id} by {self.user.username}" + + def update_total(self): + """Automatically calculate total price from order items""" + order_items = self.order_items.all() + if not order_items.exists(): + self.total_price = 0 + else: + self.total_price = sum(item.get_subtotal() for item in order_items) + self.save() + + def add_product(self, product, quantity=1): + """Add a product to the order""" + if product is None: + return None + + if quantity is None or quantity <= 0: + quantity = 1 + + order_item, created = OrderItem.objects.get_or_create( + order=self, + product=product, + defaults={ + "quantity": quantity, + "price": product.price if product.price else 0, + }, + ) + + if not created: + order_item.quantity += quantity + order_item.save() + + self.update_total() + return order_item + + class Meta: + verbose_name = "Order" + verbose_name_plural = "Orders" + ordering = ["-created_at"] + + +class OrderItem(models.Model): + order = models.ForeignKey( + Order, related_name="order_items", on_delete=models.CASCADE + ) + product = models.ForeignKey( + Product, related_name="order_items", on_delete=models.CASCADE + ) + quantity = models.IntegerField() + price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + + def get_subtotal(self): + """Calculate the subtotal for this item""" + if self.price is None or self.quantity is None: + return 0 + return self.price * self.quantity + + def __str__(self): + return f"{self.product.name} - {self.quantity}" + + def save(self, *args, **kwargs): + # Ensure price is set correctly if it's none or a new order item + if self.price is None or (not self.pk and not self.price): + self.price = ( + self.product.price if self.product and self.product.price else 0 + ) + + # Ensure quantity is never None + if self.quantity is None: + self.quantity = 1 + + super().save(*args, **kwargs) + # Update the order total when an item is saved + if self.order: + self.order.update_total() + + def delete(self, *args, **kwargs): + order = self.order + super().delete(*args, **kwargs) + # Update the order total when an item is deleted + if order: + order.update_total() + + class Meta: + verbose_name = "Order Item" + verbose_name_plural = "Order Items" + ordering = ["order"] + + +@receiver(post_save, sender=Order) +def update_order_status(sender, instance, created, **kwargs): + """When an order status changes to Delivered, update product stock""" + if not created and instance.status == "Delivered": + for item in instance.order_items.all(): + product = item.product + # Only update stock if we haven't already updated it + if product.stock >= item.quantity: + product.stock -= item.quantity + product.save() + else: + # Log error if stock is insufficient + logger.error(f"Insufficient stock for product {product.name} when delivering order {instance.id}") + + +class Cart(models.Model): + user = models.OneToOneField(User, related_name="cart", on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Cart for {self.user.username}" + + def get_total(self): + """Calculate total price of all items in cart""" + return sum(item.get_subtotal() for item in self.items.all()) + + def add_product(self, product, quantity=1): + """Add a product to the cart""" + if quantity <= 0: + return None + + cart_item, created = CartItem.objects.get_or_create( + cart=self, product=product, defaults={"quantity": quantity} + ) + + if not created: + cart_item.quantity += quantity + cart_item.save() + + return cart_item + + def remove_product(self, product): + """Remove a product from the cart""" + try: + cart_item = CartItem.objects.get(cart=self, product=product) + cart_item.delete() + return True + except CartItem.DoesNotExist: + return False + + def clear(self): + """Remove all items from the cart""" + self.items.all().delete() + + class Meta: + verbose_name = "Cart" + verbose_name_plural = "Carts" + ordering = ["-updated_at"] + + +class CartItem(models.Model): + cart = models.ForeignKey(Cart, related_name="items", on_delete=models.CASCADE) + product = models.ForeignKey( + Product, related_name="cart_items", on_delete=models.CASCADE + ) + quantity = models.PositiveIntegerField(default=1) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"{self.quantity} x {self.product.name} in {self.cart}" + + def get_subtotal(self): + """Calculate the subtotal for this item""" + return self.product.price * self.quantity + + class Meta: + verbose_name = "Cart Item" + verbose_name_plural = "Cart Items" + ordering = ["-updated_at"] + unique_together = ["cart", "product"] + + +@receiver(post_save, sender=User) +def create_user_cart(sender, instance, created, **kwargs): + """Create a cart for new users""" + if created: + Cart.objects.create(user=instance) + + +class Favorite(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="favorites") + product = models.ForeignKey( + Product, on_delete=models.CASCADE, related_name="favorites" + ) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ["user", "product"] + + def __str__(self): + return f"{self.user.username}'s favorite: {self.product.name}" + + +class PrivacyPolicy(models.Model): + title = models.CharField(max_length=200) + content = models.TextField() + last_updated = models.DateTimeField(auto_now=True) + is_active = models.BooleanField(default=True) + + def __str__(self): + return self.title + + class Meta: + verbose_name_plural = "Privacy Policies" + + +class FAQ(models.Model): + CATEGORY_CHOICES = ( + ('All', 'All'), + ('Services', 'Services'), + ('General', 'General'), + ('Account', 'Account') + ) + + question = models.CharField(max_length=255) + answer = models.TextField() + category = models.CharField(max_length=20, choices=CATEGORY_CHOICES, default='General') + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.question + + class Meta: + verbose_name_plural = "FAQs" + ordering = ['-created_at'] + + +class Contact(models.Model): + CONTACT_TYPES = [ + ('customer_service', 'Customer Service'), + ('whatsapp', 'WhatsApp'), + ('website', 'Website'), + ('facebook', 'Facebook'), + ('instagram', 'Instagram'), + ('twitter', 'Twitter'), + ] + + contact_type = models.CharField(max_length=20, choices=CONTACT_TYPES) + title = models.CharField(max_length=100) + value = models.CharField(max_length=255) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['contact_type'] + + def __str__(self): + return f"{self.get_contact_type_display()} - {self.title}" + + +class Slider(models.Model): + title = models.CharField(max_length=200) + description = models.TextField() + image = models.ImageField(upload_to='sliders/', blank=True, null=True) + category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True, blank=True, help_text="Category to show when clicking the 'Show Now' button") + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.title + + class Meta: + ordering = ['-created_at'] + + @property + def image_url(self): + if self.image: + return f"/media/{self.image.name}" + return None + + +class Coupon(models.Model): + code = models.CharField(max_length=50, unique=True) + discount_type = models.CharField( + max_length=10, + choices=[ + ('percentage', 'Percentage'), + ('fixed', 'Fixed Amount') + ], + default='percentage' + ) + discount_value = models.DecimalField(max_digits=10, decimal_places=2) + min_purchase = models.DecimalField(max_digits=10, decimal_places=2, default=0) + max_discount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + is_active = models.BooleanField(default=True) + start_date = models.DateTimeField() + end_date = models.DateTimeField() + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"{self.code} - {self.discount_value}{'%' if self.discount_type == 'percentage' else '$'}" + + class Meta: + verbose_name = "Coupon" + verbose_name_plural = "Coupons" + ordering = ['-created_at'] + + def is_valid(self): + from django.utils import timezone + now = timezone.now() + return self.is_active and self.start_date <= now <= self.end_date + + def calculate_discount(self, total_amount): + if not self.is_valid() or total_amount < self.min_purchase: + return 0 + + if self.discount_type == 'percentage': + discount = (total_amount * self.discount_value) / 100 + if self.max_discount: + discount = min(discount, self.max_discount) + return discount + else: + return self.discount_value + + +class Notification(models.Model): + NOTIFICATION_TYPES = ( + ('order_placed', 'Order Placed'), + ('order_shipped', 'Order Shipped'), + ('order_delivered', 'Order Delivered'), + ('order_cancelled', 'Order Cancelled'), + ('coupon', 'Coupon'), + ) + + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications') + type = models.CharField(max_length=20, choices=NOTIFICATION_TYPES) + message = models.TextField() + read = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + order = models.ForeignKey('Order', on_delete=models.SET_NULL, null=True, blank=True) + coupon = models.ForeignKey('Coupon', on_delete=models.SET_NULL, null=True, blank=True) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return f"{self.get_type_display()} - {self.user.username}" + + +@receiver(post_save, sender=Order) +def create_order_notification(sender, instance, created, **kwargs): + """Create notifications when orders are created or updated""" + if created: + # Create notification for new order + Notification.objects.create( + user=instance.user, + type='order_placed', + message=f'Your order #{instance.id} has been placed successfully', + order=instance + ) + else: + # Create notifications for order status changes + if instance.status == 'Shipped': + Notification.objects.create( + user=instance.user, + type='order_shipped', + message=f'Your order #{instance.id} has been shipped', + order=instance + ) + elif instance.status == 'Delivered': + Notification.objects.create( + user=instance.user, + type='order_delivered', + message=f'Your order #{instance.id} has been delivered', + order=instance + ) + elif instance.status == 'Cancelled': + Notification.objects.create( + user=instance.user, + type='order_cancelled', + message=f'Your order #{instance.id} has been cancelled', + order=instance + ) + + +@receiver(post_save, sender=Coupon) +def create_coupon_notification(sender, instance, created, **kwargs): + """Create notifications when new coupons are created""" + if created and instance.is_active: + # Get all active users + users = User.objects.filter(is_active=True) + # Create notifications for all users + for user in users: + Notification.objects.create( + user=user, + type='coupon', + message=f'New coupon code {instance.code} available! Get {instance.discount_value}{"%" if instance.discount_type == "percentage" else "$"} off', + coupon=instance + ) diff --git a/Backend/app/permissions.py b/backend/app/permissions.py similarity index 100% rename from Backend/app/permissions.py rename to backend/app/permissions.py diff --git a/backend/app/serializers.py b/backend/app/serializers.py new file mode 100644 index 0000000..04bcaba --- /dev/null +++ b/backend/app/serializers.py @@ -0,0 +1,326 @@ +from rest_framework import serializers +from .models import ( + User, + Category, + Product, + Rating, + Order, + OrderItem, + CartItem, + Cart, + Favorite, + PrivacyPolicy, + FAQ, + Contact, + Slider, + Coupon, + Notification +) + + +class UserSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True) + + class Meta: + model = User + fields = [ + "id", + "username", + "first_name", + "last_name", + "gender", + "email", + "password", + "phone", + "address", + "profile_picture", + "created_at", + ] + read_only_fields = ["id", "created_at"] + extra_kwargs = { + 'gender': {'required': False}, + 'phone': {'required': False}, + 'address': {'required': False}, + } + + def create(self, validated_data): + # Use the create_user method instead of direct creation + user = User.objects.create_user( + username=validated_data["username"], + email=validated_data["email"], + password=validated_data["password"], + first_name=validated_data.get("first_name", ""), + last_name=validated_data.get("last_name", ""), + phone=validated_data.get("phone", ""), + address=validated_data.get("address", ""), + gender=validated_data.get("gender", ""), + ) + return user + + +class CategorySerializer(serializers.ModelSerializer): + class Meta: + model = Category + fields = ["id", "name", "image", "created_at"] + read_only_fields = ["id", "created_at"] + + +class RatingSerializer(serializers.ModelSerializer): + username = serializers.ReadOnlyField(source="user.username") + + class Meta: + model = Rating + fields = [ + "id", + "product", + "user", + "username", + "rating", + "comment", + "created_at", + ] + read_only_fields = ["id", "created_at", "username"] + + +class ProductSerializer(serializers.ModelSerializer): + category_name = serializers.ReadOnlyField(source="category.name") + average_rating = serializers.SerializerMethodField() + image_url = serializers.SerializerMethodField() + + class Meta: + model = Product + fields = [ + "id", + "name", + "description", + "price", + "image", + "image_url", + "category", + "stock", + "color", + "size", + "category_name", + "average_rating", + "created_at", + ] + read_only_fields = ["id", "created_at", "category_name", "average_rating", "image_url"] + + def get_average_rating(self, obj): + try: + return float(obj.average_rating) if obj.average_rating is not None else 0.0 + except (TypeError, AttributeError): + return 0.0 + + def get_image_url(self, obj): + if obj.image: + request = self.context.get('request') + if request is not None: + return request.build_absolute_uri(obj.image.url) + return obj.image.url + return None + + +class ProductDetailSerializer(ProductSerializer): + ratings = RatingSerializer(many=True, read_only=True) + + class Meta(ProductSerializer.Meta): + fields = ProductSerializer.Meta.fields + ["ratings"] + + +class OrderItemSerializer(serializers.ModelSerializer): + product_name = serializers.ReadOnlyField(source="product.name") + product_image = serializers.ImageField(source="product.image", read_only=True) + subtotal = serializers.SerializerMethodField() + + class Meta: + model = OrderItem + fields = [ + "id", + "product", + "product_name", + "product_image", + "quantity", + "price", + "subtotal", + ] + read_only_fields = ["id", "product_name", "product_image", "subtotal"] + + def get_subtotal(self, obj): + return obj.get_subtotal() + + +class OrderItemCreateSerializer(serializers.ModelSerializer): + class Meta: + model = OrderItem + fields = ["product", "quantity"] + + +class OrderSerializer(serializers.ModelSerializer): + user_name = serializers.ReadOnlyField(source="user.get_full_name") + order_items = OrderItemSerializer(many=True, read_only=True) + items = OrderItemCreateSerializer(many=True, write_only=True, required=False) + + class Meta: + model = Order + fields = [ + "id", + "user", + "user_name", + "total_price", + "status", + "shipping_address", + "created_at", + "order_items", + "items", + ] + read_only_fields = [ + "id", + "created_at", + "user_name", + "total_price", + "order_items", + ] + + def create(self, validated_data): + items_data = validated_data.pop("items", []) + order = Order.objects.create(**validated_data) + + # Create order items if provided + for item_data in items_data: + product = item_data.get("product") + quantity = item_data.get("quantity", 1) + order.add_product(product, quantity) + + return order + + def update(self, instance, validated_data): + items_data = validated_data.pop("items", []) + + # Update the order fields + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + + # Add new items if provided + for item_data in items_data: + product = item_data.get("product") + quantity = item_data.get("quantity", 1) + instance.add_product(product, quantity) + + return instance + + +class CartItemSerializer(serializers.ModelSerializer): + product_name = serializers.CharField(source="product.name", read_only=True) + product_price = serializers.SerializerMethodField() + product_image = serializers.SerializerMethodField() + subtotal = serializers.SerializerMethodField() + + class Meta: + model = CartItem + fields = [ + "id", + "product", + "product_name", + "product_price", + "product_image", + "quantity", + "subtotal", + "created_at", + "updated_at", + ] + read_only_fields = ["created_at", "updated_at"] + + def get_subtotal(self, obj): + try: + return float(obj.product.price * obj.quantity) + except (AttributeError, TypeError): + return 0.0 + + def get_product_price(self, obj): + try: + return float(obj.product.price) + except (AttributeError, TypeError): + return 0.0 + + def get_product_image(self, obj): + if obj.product and obj.product.image: + request = self.context.get("request") + if request is not None: + return request.build_absolute_uri(obj.product.image.url) + return obj.product.image.url + return None + + +class CartSerializer(serializers.ModelSerializer): + items = CartItemSerializer(many=True, read_only=True) + total = serializers.SerializerMethodField() + + class Meta: + model = Cart + fields = ["id", "user", "items", "total", "created_at", "updated_at"] + read_only_fields = ["created_at", "updated_at"] + + def get_total(self, obj): + return obj.get_total() + + +class FavoriteSerializer(serializers.ModelSerializer): + product = ProductSerializer(read_only=True) + + class Meta: + model = Favorite + fields = ["id", "product", "created_at"] + read_only_fields = ["id", "created_at"] + + +class PrivacyPolicySerializer(serializers.ModelSerializer): + class Meta: + model = PrivacyPolicy + fields = ['id', 'title', 'content', 'last_updated'] + + +class FAQSerializer(serializers.ModelSerializer): + class Meta: + model = FAQ + fields = ['id', 'question', 'answer', 'category', 'is_active', 'created_at', 'updated_at'] + read_only_fields = ['id', 'created_at', 'updated_at'] + + +class ContactSerializer(serializers.ModelSerializer): + class Meta: + model = Contact + fields = ['id', 'contact_type', 'title', 'value', 'is_active', 'created_at', 'updated_at'] + read_only_fields = ['created_at', 'updated_at'] + + +class SliderSerializer(serializers.ModelSerializer): + image_url = serializers.SerializerMethodField() + + class Meta: + model = Slider + fields = ('id', 'title', 'description', 'image', 'image_url', 'category', 'is_active', 'created_at', 'updated_at') + read_only_fields = ('created_at', 'updated_at') + + def get_image_url(self, obj): + if obj.image: + return self.context['request'].build_absolute_uri(obj.image_url) + return None + + +class CouponSerializer(serializers.ModelSerializer): + class Meta: + model = Coupon + fields = ['id', 'code', 'discount_type', 'discount_value', 'min_purchase', + 'max_discount', 'is_active', 'start_date', 'end_date'] + read_only_fields = ['id', 'created_at', 'updated_at'] + + +class NotificationSerializer(serializers.ModelSerializer): + class Meta: + model = Notification + fields = ['id', 'type', 'message', 'read', 'created_at', 'order', 'coupon'] + read_only_fields = ['created_at'] + + diff --git a/Backend/app/tests.py b/backend/app/tests.py similarity index 100% rename from Backend/app/tests.py rename to backend/app/tests.py diff --git a/Backend/app/urls.py b/backend/app/urls.py similarity index 59% rename from Backend/app/urls.py rename to backend/app/urls.py index 96b2987..d186b90 100644 --- a/Backend/app/urls.py +++ b/backend/app/urls.py @@ -6,7 +6,9 @@ from .auth import CustomAuthToken, LogoutView from .views import ( UserViewSet, CategoryViewSet, ProductViewSet, RatingViewSet, OrderViewSet, OrderItemViewSet, get_csrf_token, serve_protected_media, - FavoriteViewSet + FavoriteViewSet, PrivacyPolicyViewSet, FAQViewSet, ContactViewSet, + SliderViewSet, CouponViewSet, ValidateCouponView, RegisterView, + NotificationViewSet ) router = DefaultRouter() @@ -17,19 +19,23 @@ router.register(r'ratings', RatingViewSet) router.register(r'orders', OrderViewSet) router.register(r'order-items', OrderItemViewSet) router.register(r'favorites', FavoriteViewSet, basename='favorite') - -# Cart endpoints -cart_router = DefaultRouter() -cart_router.register(r'cart', CartViewSet, basename='cart') +router.register(r'cart', CartViewSet, basename='cart') +router.register(r'privacy-policies', PrivacyPolicyViewSet, basename='privacy-policy') +router.register(r'faqs', FAQViewSet, basename='faq') +router.register(r'contacts', ContactViewSet, basename='contact') +router.register(r'sliders', SliderViewSet) +router.register(r'coupons', CouponViewSet) +router.register(r'notifications', NotificationViewSet, basename='notification') urlpatterns = [ path('', include(router.urls)), - path('', include(cart_router.urls)), path('auth/', include('rest_framework.urls', namespace='rest_framework')), # Token authentication endpoints path('get-csrf-token/', get_csrf_token, name='get-csrf-token'), path('token-auth/', CustomAuthToken.as_view(), name='token_auth'), path('logout/', LogoutView.as_view(), name='logout'), + path('auth/register/', RegisterView.as_view(), name='register'), path('media/', serve_protected_media, name='serve_protected_media'), + path('validate-coupon/', ValidateCouponView.as_view(), name='validate-coupon'), ] \ No newline at end of file diff --git a/backend/app/views.py b/backend/app/views.py new file mode 100644 index 0000000..095dfac --- /dev/null +++ b/backend/app/views.py @@ -0,0 +1,685 @@ +from django.conf import settings +from rest_framework import viewsets, status, filters, permissions +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny, IsAuthenticatedOrReadOnly +from django.db.models import Avg +from django.shortcuts import get_object_or_404 +from django.http import HttpResponseForbidden, JsonResponse +from django.views.decorators.csrf import ensure_csrf_cookie +from django.middleware.csrf import get_token +from rest_framework.exceptions import PermissionDenied +from django.conf import settings +from django.http import FileResponse, HttpResponseForbidden +from django.views.decorators.http import require_GET +from rest_framework.authentication import TokenAuthentication +from rest_framework.decorators import authentication_classes, permission_classes +from rest_framework.permissions import IsAuthenticated +import os +from .models import User, Category, Product, Rating, Order, OrderItem, Favorite, PrivacyPolicy, FAQ, Contact, Slider, Coupon, Notification +from .serializers import ( + UserSerializer, CategorySerializer, ProductSerializer, ProductDetailSerializer, + RatingSerializer, OrderSerializer, OrderItemSerializer, FavoriteSerializer, PrivacyPolicySerializer, + FAQSerializer, ContactSerializer, SliderSerializer, CouponSerializer, NotificationSerializer +) +from django.contrib.auth import authenticate +from rest_framework.authtoken.models import Token +from rest_framework.views import APIView + +# Add RegisterView class for user registration +class RegisterView(APIView): + permission_classes = [AllowAny] + + def post(self, request): + serializer = UserSerializer(data=request.data) + if serializer.is_valid(): + user = serializer.save() + # Create auth token for the new user + token, created = Token.objects.get_or_create(user=user) + return Response({ + 'token': token.key, + 'user_id': user.id, + 'username': user.username, + 'email': user.email, + 'first_name': user.first_name, + 'last_name': user.last_name, + 'message': 'User registered successfully' + }, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer + filter_backends = [filters.SearchFilter] + search_fields = ['username', 'email', 'first_name', 'last_name'] + permission_classes = [AllowAny] + + def get_queryset(self): + # If user_id query param is provided, filter by that + user_id = self.request.query_params.get('user_id', None) + if user_id: + return User.objects.filter(id=user_id) + + # If no specific user_id is requested, and user is staff, show all + if self.request.user.is_authenticated and self.request.user.is_staff: + return User.objects.all() + + # For anonymous or non-staff users requesting the list endpoint, return empty queryset + if self.action == 'list': + return User.objects.none() + + # For other actions like retrieve, let Django's permission system handle it + return User.objects.all() + + @action(detail=False, methods=['get']) + def me(self, request): + """Get current user's profile""" + if not request.user.is_authenticated: + return Response({'error': 'Authentication required'}, status=status.HTTP_401_UNAUTHORIZED) + + serializer = self.get_serializer(request.user) + return Response(serializer.data) + + @action(detail=False, methods=['post']) + def update_profile_picture(self, request): + """Update the current user's profile picture""" + if not request.user.is_authenticated: + return Response({'error': 'Authentication required'}, status=status.HTTP_401_UNAUTHORIZED) + + if 'profile_picture' not in request.FILES: + return Response({'error': 'No profile picture provided'}, status=status.HTTP_400_BAD_REQUEST) + + try: + user = request.user + # Save the new profile picture + user.profile_picture = request.FILES['profile_picture'] + user.save() + + # Return the updated user data + serializer = self.get_serializer(user) + response_data = serializer.data + + # Add the profile picture URL to the response + if user.profile_picture: + response_data['profile_picture'] = request.build_absolute_uri(user.profile_picture.url) + + return Response(response_data) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + + @action(detail=False, methods=['post']) + def update_profile(self, request): + """Update the current user's profile details""" + if not request.user.is_authenticated: + return Response({'error': 'Authentication required'}, status=status.HTTP_401_UNAUTHORIZED) + + try: + user = request.user + # Update user details + user.username = request.data.get('username', user.username) + user.first_name = request.data.get('first_name', user.first_name) + user.last_name = request.data.get('last_name', user.last_name) + user.phone = request.data.get('phone', user.phone) + user.email = request.data.get('email', user.email) + user.gender = request.data.get('gender', user.gender) + user.address = request.data.get('address', user.address) + user.save() + + # Return the updated user data + serializer = self.get_serializer(user) + return Response(serializer.data) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + + @action(detail=False, methods=['post']) + def login(self, request): + """Login user and return token""" + username = request.data.get('username') + password = request.data.get('password') + + if not username or not password: + return Response( + {'error': 'Please provide both username and password'}, + status=status.HTTP_400_BAD_REQUEST + ) + + user = authenticate(username=username, password=password) + if not user: + return Response( + {'error': 'Invalid credentials'}, + status=status.HTTP_401_UNAUTHORIZED + ) + + token, _ = Token.objects.get_or_create(user=user) + serializer = self.get_serializer(user) + + return Response({ + 'token': token.key, + 'user': serializer.data + }) + + @action(detail=False, methods=['get']) + def gender_options(self, request): + """Get available gender options""" + options = [ + {'value': 'Male', 'label': 'Male'}, + {'value': 'Female', 'label': 'Female'}, + {'value': 'Other', 'label': 'Other'} + ] + return Response(options) + + @action(detail=False, methods=['post']) + def update_password(self, request): + """Update the current user's password""" + if not request.user.is_authenticated: + return Response({'error': 'Authentication required'}, status=status.HTTP_401_UNAUTHORIZED) + + current_password = request.data.get('current_password') + new_password = request.data.get('new_password') + + if not current_password or not new_password: + return Response({'error': 'Both current and new passwords are required'}, status=status.HTTP_400_BAD_REQUEST) + + try: + user = request.user + # Verify current password + if not user.check_password(current_password): + return Response({'error': 'Current password is incorrect'}, status=status.HTTP_400_BAD_REQUEST) + + # Update password + user.set_password(new_password) + user.save() + + return Response({'message': 'Password updated successfully'}) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + + @action(detail=False, methods=['post']) + def delete_account(self, request): + """Delete the current user's account""" + if not request.user.is_authenticated: + return Response({'error': 'Authentication required'}, status=status.HTTP_401_UNAUTHORIZED) + + password = request.data.get('password') + + if not password: + return Response({'error': 'Password is required'}, status=status.HTTP_400_BAD_REQUEST) + + try: + user = request.user + # Verify password + if not user.check_password(password): + return Response({'error': 'Password is incorrect'}, status=status.HTTP_400_BAD_REQUEST) + + # Delete the user + user.delete() + + return Response({'message': 'Account deleted successfully'}) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + + +class CategoryViewSet(viewsets.ModelViewSet): + queryset = Category.objects.all() + serializer_class = CategorySerializer + filter_backends = [filters.SearchFilter] + search_fields = ['name'] + permission_classes = [AllowAny] + + def get_permissions(self): + if self.action in ['create', 'update', 'partial_update', 'destroy']: + permission_classes = [IsAdminUser] + else: + permission_classes = [AllowAny] + return [permission() for permission in permission_classes] + + @action(detail=True, methods=['get']) + def products(self, request, pk=None): + try: + # Validate that pk is a valid integer + try: + category_id = int(pk) + except (ValueError, TypeError): + return Response( + {"error": "Invalid category ID. Please provide a valid integer."}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Get the category + try: + category = Category.objects.get(id=category_id) + except Category.DoesNotExist: + return Response( + {"error": f"Category with ID {category_id} not found"}, + status=status.HTTP_404_NOT_FOUND + ) + + # Get products with related category + products = Product.objects.filter(category=category).select_related('category') + + # Serialize the products + serializer = ProductSerializer(products, many=True, context={'request': request}) + + return Response(serializer.data) + except Exception as e: + print(f"Error in products action: {str(e)}") + print(f"Error type: {type(e)}") + import traceback + print(traceback.format_exc()) + return Response( + {"error": f"An error occurred while fetching products: {str(e)}"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + +class ProductViewSet(viewsets.ModelViewSet): + queryset = Product.objects.all() + serializer_class = ProductSerializer + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['name', 'description', 'category__name'] + ordering_fields = ['name', 'price', 'created_at'] + permission_classes = [AllowAny] + + def get_permissions(self): + if self.action in ['create', 'update', 'partial_update', 'destroy']: + permission_classes = [IsAdminUser] + else: + permission_classes = [AllowAny] + return [permission() for permission in permission_classes] + + def get_serializer_class(self): + if self.action == 'retrieve': + return ProductDetailSerializer + return ProductSerializer + + @action(detail=True, methods=['post']) + def rate(self, request, pk=None): + # Still require authentication for rating + if not request.user.is_authenticated: + return Response({'error': 'Authentication required'}, status=status.HTTP_401_UNAUTHORIZED) + + product = self.get_object() + rating_value = request.data.get('rating') + comment = request.data.get('comment', '') + + if not rating_value: + return Response({'error': 'Rating is required'}, status=status.HTTP_400_BAD_REQUEST) + + # Update or create rating + rating, created = Rating.objects.update_or_create( + product=product, + user=request.user, + defaults={'rating': rating_value, 'comment': comment} + ) + + serializer = RatingSerializer(rating) + return Response(serializer.data) + + +class RatingViewSet(viewsets.ModelViewSet): + queryset = Rating.objects.all() + serializer_class = RatingSerializer + permission_classes = [AllowAny] + + def get_permissions(self): + if self.action in ['update', 'partial_update', 'destroy']: + permission_classes = [IsAuthenticated] + else: + permission_classes = [AllowAny] + return [permission() for permission in permission_classes] + + def perform_create(self, serializer): + # Still require authentication for creating ratings + if not self.request.user.is_authenticated: + raise PermissionDenied('Authentication required to create ratings') + serializer.save(user=self.request.user) + + +class OrderViewSet(viewsets.ModelViewSet): + queryset = Order.objects.all() + serializer_class = OrderSerializer + filter_backends = [filters.OrderingFilter] + ordering_fields = ['created_at', 'status'] + permission_classes = [AllowAny] + + def get_queryset(self): + user_id = self.request.query_params.get('user_id', None) + if user_id: + return Order.objects.filter(user_id=user_id) + + if self.request.user.is_authenticated: + if self.request.user.is_staff: + return Order.objects.all() + return Order.objects.filter(user=self.request.user) + + # For anonymous users, return empty queryset + return Order.objects.none() + + def perform_create(self, serializer): + # For creating orders, require authentication + if not self.request.user.is_authenticated: + raise PermissionDenied('Authentication required to create orders') + serializer.save(user=self.request.user) + + def update(self, request, *args, **kwargs): + """Override update to handle status changes and stock management""" + instance = self.get_object() + new_status = request.data.get('status') + + # Check if status is being updated + if new_status and new_status != instance.status: + # Validate status transition + valid_transitions = { + 'Pending': ['Shipped', 'Cancelled'], + 'Shipped': ['Delivered', 'Cancelled'], + 'Delivered': [], # No transitions allowed from Delivered + 'Cancelled': [] # No transitions allowed from Cancelled + } + + if new_status not in valid_transitions.get(instance.status, []): + return Response({ + 'error': f'Cannot transition from {instance.status} to {new_status}' + }, status=status.HTTP_400_BAD_REQUEST) + + # Check stock availability for Shipped status + if new_status == 'Shipped': + for item in instance.order_items.all(): + if item.product.stock < item.quantity: + return Response({ + 'error': f'Not enough stock for {item.product.name}. Available: {item.product.stock}' + }, status=status.HTTP_400_BAD_REQUEST) + + # Proceed with update + return super().update(request, *args, **kwargs) + + @action(detail=True, methods=['post']) + def cancel(self, request, pk=None): + order = self.get_object() + + # Check if order belongs to current user or user is admin + if self.request.user.is_authenticated and (order.user.id == request.user.id or request.user.is_staff): + # Check if order can be canceled + if order.status not in ['Pending', 'Shipped']: + return Response({'error': 'Cannot cancel order in current status'}, + status=status.HTTP_400_BAD_REQUEST) + + # Update order status + order.status = 'Cancelled' + order.save() + + # Notification will be created automatically via signal + + serializer = OrderSerializer(order) + return Response(serializer.data) + else: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + + @action(detail=True, methods=['post']) + def ship(self, request, pk=None): + """Mark an order as shipped""" + order = self.get_object() + + # Check if user is staff + if not request.user.is_staff: + return Response({'error': 'Only staff can ship orders'}, + status=status.HTTP_403_FORBIDDEN) + + # Check if order can be shipped + if order.status != 'Pending': + return Response({'error': 'Can only ship pending orders'}, + status=status.HTTP_400_BAD_REQUEST) + + # Check stock availability + for item in order.order_items.all(): + if item.product.stock < item.quantity: + return Response({ + 'error': f'Not enough stock for {item.product.name}. Available: {item.product.stock}' + }, status=status.HTTP_400_BAD_REQUEST) + + # Update order status + order.status = 'Shipped' + order.save() + + # Notification will be created automatically via signal + + serializer = OrderSerializer(order) + return Response(serializer.data) + + @action(detail=True, methods=['post']) + def deliver(self, request, pk=None): + """Mark an order as delivered""" + order = self.get_object() + + # Check if user is staff + if not request.user.is_staff: + return Response({'error': 'Only staff can mark orders as delivered'}, + status=status.HTTP_403_FORBIDDEN) + + # Check if order can be delivered + if order.status != 'Shipped': + return Response({'error': 'Can only deliver shipped orders'}, + status=status.HTTP_400_BAD_REQUEST) + + # Update order status + order.status = 'Delivered' + order.save() + + # Notification will be created automatically via signal + + serializer = OrderSerializer(order) + return Response(serializer.data) + + +class OrderItemViewSet(viewsets.ModelViewSet): + queryset = OrderItem.objects.all() + serializer_class = OrderItemSerializer + permission_classes = [AllowAny] + + def get_queryset(self): + user_id = self.request.query_params.get('user_id', None) + if user_id: + return OrderItem.objects.filter(order__user_id=user_id) + + if self.request.user.is_authenticated: + if self.request.user.is_staff: + return OrderItem.objects.all() + return OrderItem.objects.filter(order__user=self.request.user) + + # For anonymous users, return empty queryset + return OrderItem.objects.none() + +def get_csrf_token(request): + """ + Return CSRF token for the frontend to use in subsequent requests + """ + csrf_token = get_token(request) + return JsonResponse({'csrfToken': csrf_token}) + +@require_GET +@authentication_classes([TokenAuthentication]) +@permission_classes([IsAuthenticated]) +def serve_protected_media(request, path): + """ + Serve media files with authentication check. + The path parameter should be the relative path within MEDIA_ROOT. + """ + # Construct the full path + file_path = os.path.join(settings.MEDIA_ROOT, path) + + # Basic security check to prevent directory traversal + if not os.path.abspath(file_path).startswith(os.path.abspath(settings.MEDIA_ROOT)): + return HttpResponseForbidden('Invalid path') + + # Check if file exists + if not os.path.exists(file_path): + return HttpResponseForbidden('File not found') + + # Serve the file + return FileResponse(open(file_path, 'rb')) + +class FavoriteViewSet(viewsets.ViewSet): + permission_classes = [IsAuthenticated] + + def list(self, request): + """Get all favorites for the current user""" + favorites = Favorite.objects.filter(user=request.user) + serializer = FavoriteSerializer(favorites, many=True, context={'request': request}) + return Response(serializer.data) + + @action(detail=False, methods=['post']) + def toggle(self, request): + """Toggle favorite status for a product""" + product_id = request.data.get('product_id') + if not product_id: + return Response({'error': 'Product ID is required'}, status=status.HTTP_400_BAD_REQUEST) + + try: + product = Product.objects.get(id=product_id) + except Product.DoesNotExist: + return Response({'error': 'Product not found'}, status=status.HTTP_404_NOT_FOUND) + + # Check if favorite exists + favorite = Favorite.objects.filter(user=request.user, product=product).first() + + if favorite: + # Remove from favorites + favorite.delete() + return Response({'message': 'Product removed from favorites'}) + else: + # Add to favorites + Favorite.objects.create(user=request.user, product=product) + return Response({'message': 'Product added to favorites'}) + + # Return updated favorites list + favorites = Favorite.objects.filter(user=request.user) + serializer = FavoriteSerializer(favorites, many=True, context={'request': request}) + return Response(serializer.data) + + @action(detail=False, methods=['get']) + def check(self, request): + """Check if a product is favorited""" + product_id = request.query_params.get('product_id') + if not product_id: + return Response({'error': 'Product ID is required'}, status=status.HTTP_400_BAD_REQUEST) + + try: + product = Product.objects.get(id=product_id) + except Product.DoesNotExist: + return Response({'error': 'Product not found'}, status=status.HTTP_404_NOT_FOUND) + + is_favorited = Favorite.objects.filter(user=request.user, product=product).exists() + return Response({'is_favorited': is_favorited}) + +class PrivacyPolicyViewSet(viewsets.ModelViewSet): + queryset = PrivacyPolicy.objects.filter(is_active=True) + serializer_class = PrivacyPolicySerializer + permission_classes = [AllowAny] + + def get_queryset(self): + return PrivacyPolicy.objects.filter(is_active=True).order_by('-last_updated') + +class FAQViewSet(viewsets.ModelViewSet): + queryset = FAQ.objects.filter(is_active=True) + serializer_class = FAQSerializer + permission_classes = [AllowAny] + + def get_queryset(self): + category = self.request.query_params.get('category', None) + if category and category != 'All': + return FAQ.objects.filter(category=category, is_active=True) + return FAQ.objects.filter(is_active=True) + + @action(detail=False, methods=['get']) + def categories(self, request): + """Get all available FAQ categories""" + categories = FAQ.CATEGORY_CHOICES + return Response([{'value': value, 'label': label} for value, label in categories]) + +class ContactViewSet(viewsets.ModelViewSet): + queryset = Contact.objects.filter(is_active=True) + serializer_class = ContactSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def get_queryset(self): + return Contact.objects.filter(is_active=True).order_by('contact_type') + +class SliderViewSet(viewsets.ModelViewSet): + queryset = Slider.objects.filter(is_active=True) + serializer_class = SliderSerializer + permission_classes = [AllowAny] + + def get_queryset(self): + return Slider.objects.filter(is_active=True).order_by('-created_at') + + def get_serializer_context(self): + context = super().get_serializer_context() + context['request'] = self.request + return context + +class CouponViewSet(viewsets.ModelViewSet): + queryset = Coupon.objects.all() + serializer_class = CouponSerializer + permission_classes = [IsAuthenticatedOrReadOnly] + + def get_queryset(self): + queryset = Coupon.objects.filter(is_active=True) + code = self.request.query_params.get('code', None) + if code: + queryset = queryset.filter(code=code) + return queryset + +class ValidateCouponView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + code = request.data.get('code') + cart_total = request.data.get('cart_total', 0) + + try: + coupon = Coupon.objects.get(code=code, is_active=True) + if not coupon.is_valid(): + return Response({ + 'valid': False, + 'message': 'Coupon has expired or is not yet active' + }, status=400) + + if cart_total < coupon.min_purchase: + return Response({ + 'valid': False, + 'message': f'Minimum purchase amount of ${coupon.min_purchase} required' + }, status=400) + + discount = coupon.calculate_discount(cart_total) + return Response({ + 'valid': True, + 'discount': discount, + 'coupon': CouponSerializer(coupon).data + }) + + except Coupon.DoesNotExist: + return Response({ + 'valid': False, + 'message': 'Invalid coupon code' + }, status=400) + +class NotificationViewSet(viewsets.ModelViewSet): + serializer_class = NotificationSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return Notification.objects.filter(user=self.request.user) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + @action(detail=True, methods=['put']) + def read(self, request, pk=None): + notification = self.get_object() + notification.read = True + notification.save() + return Response(self.get_serializer(notification).data) + + @action(detail=False, methods=['delete']) + def clear_all(self, request): + self.get_queryset().delete() + return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file diff --git a/Backend/main.py b/backend/main.py similarity index 100% rename from Backend/main.py rename to backend/main.py diff --git a/Backend/manage.py b/backend/manage.py similarity index 100% rename from Backend/manage.py rename to backend/manage.py diff --git a/Backend/pyproject.toml b/backend/pyproject.toml similarity index 89% rename from Backend/pyproject.toml rename to backend/pyproject.toml index 16d3dcb..eaf7a10 100644 --- a/Backend/pyproject.toml +++ b/backend/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.13" dependencies = [ "django>=5.2", + "django-cors-headers>=4.7.0", "django-jazzmin>=3.0.1", "django-rest-framework>=0.1.0", "djangorestframework>=3.16.0", diff --git a/Backend/requirements.txt b/backend/requirements.txt similarity index 83% rename from Backend/requirements.txt rename to backend/requirements.txt index e12e731..4328ad0 100644 --- a/Backend/requirements.txt +++ b/backend/requirements.txt @@ -1,5 +1,6 @@ asgiref==3.8.1 django==5.2 +django-cors-headers==4.7.0 django-jazzmin==3.0.1 django-rest-framework==0.1.0 djangorestframework==3.16.0 diff --git a/Backend/shoppingApp/__init__.py b/backend/shoppingApp/__init__.py similarity index 100% rename from Backend/shoppingApp/__init__.py rename to backend/shoppingApp/__init__.py diff --git a/Backend/shoppingApp/asgi.py b/backend/shoppingApp/asgi.py similarity index 100% rename from Backend/shoppingApp/asgi.py rename to backend/shoppingApp/asgi.py diff --git a/Backend/shoppingApp/settings.py b/backend/shoppingApp/settings.py similarity index 80% rename from Backend/shoppingApp/settings.py rename to backend/shoppingApp/settings.py index 352951b..52b111c 100644 --- a/Backend/shoppingApp/settings.py +++ b/backend/shoppingApp/settings.py @@ -47,6 +47,7 @@ INSTALLED_APPS = [ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -79,14 +80,46 @@ TEMPLATES = [ ] # CORS settings -CORS_ORIGIN_ALLOW_ALL = True -CORS_ALLOW_ALL_ORIGINS = True -CORS_ALLOW_CREDENTIALS = True - CORS_ALLOWED_ORIGINS = [ - 'http://192.168.1.75:8081', - 'http://192.168.1.56:8081', + "http://192.168.18.4:8000", + "http://192.168.18.4:19006", + "http://localhost:19006", + "http://localhost:8000", + "http://127.0.0.1:19006", + "http://127.0.0.1:8000", ] + +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOW_ALL_ORIGINS = True # For development only +CORS_ORIGIN_ALLOW_ALL = True # For development only + +CORS_ALLOW_METHODS = [ + 'DELETE', + 'GET', + 'OPTIONS', + 'PATCH', + 'POST', + 'PUT', +] + +CORS_ALLOW_HEADERS = [ + 'accept', + 'accept-encoding', + 'authorization', + 'content-type', + 'dnt', + 'origin', + 'user-agent', + 'x-csrftoken', + 'x-requested-with', +] + +# Session settings +SESSION_COOKIE_SAMESITE = 'Lax' +SESSION_COOKIE_SECURE = False # Set to True in production with HTTPS +CSRF_COOKIE_SAMESITE = 'Lax' +CSRF_COOKIE_SECURE = False # Set to True in production with HTTPS + WSGI_APPLICATION = 'shoppingApp.wsgi.application' diff --git a/Backend/shoppingApp/urls.py b/backend/shoppingApp/urls.py similarity index 100% rename from Backend/shoppingApp/urls.py rename to backend/shoppingApp/urls.py diff --git a/Backend/shoppingApp/wsgi.py b/backend/shoppingApp/wsgi.py similarity index 100% rename from Backend/shoppingApp/wsgi.py rename to backend/shoppingApp/wsgi.py diff --git a/Backend/uv.lock b/backend/uv.lock similarity index 93% rename from Backend/uv.lock rename to backend/uv.lock index 743def3..cd50dd1 100644 --- a/Backend/uv.lock +++ b/backend/uv.lock @@ -17,6 +17,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "django" }, + { name = "django-cors-headers" }, { name = "django-jazzmin" }, { name = "django-rest-framework" }, { name = "djangorestframework" }, @@ -26,6 +27,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "django", specifier = ">=5.2" }, + { name = "django-cors-headers", specifier = ">=4.7.0" }, { name = "django-jazzmin", specifier = ">=3.0.1" }, { name = "django-rest-framework", specifier = ">=0.1.0" }, { name = "djangorestframework", specifier = ">=3.16.0" }, @@ -46,6 +48,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/e0/6a5b5ea350c5bd63fe94b05e4c146c18facb51229d9dee42aa39f9fc2214/Django-5.2-py3-none-any.whl", hash = "sha256:91ceed4e3a6db5aedced65e3c8f963118ea9ba753fc620831c77074e620e7d83", size = 8301361 }, ] +[[package]] +name = "django-cors-headers" +version = "4.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/6c/16f6cb6064c63074fd5b2bd494eb319afd846236d9c1a6c765946df2c289/django_cors_headers-4.7.0.tar.gz", hash = "sha256:6fdf31bf9c6d6448ba09ef57157db2268d515d94fc5c89a0a1028e1fc03ee52b", size = 21037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/a2/7bcfff86314bd9dd698180e31ba00604001606efb518a06cca6833a54285/django_cors_headers-4.7.0-py3-none-any.whl", hash = "sha256:f1c125dcd58479fe7a67fe2499c16ee38b81b397463cf025f0e2c42937421070", size = 12794 }, +] + [[package]] name = "django-jazzmin" version = "3.0.1" diff --git a/frontend/.expo/types/router.d.ts b/frontend/.expo/types/router.d.ts index 1967c85..ec588dd 100644 --- a/frontend/.expo/types/router.d.ts +++ b/frontend/.expo/types/router.d.ts @@ -6,9 +6,9 @@ export * from 'expo-router'; declare module 'expo-router' { export namespace ExpoRouter { export interface __routes { - hrefInputParams: { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/api`; params?: Router.UnknownInputParams; } | { pathname: `/`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; } | { pathname: `/components/ActiveOrder`; params?: Router.UnknownInputParams; } | { pathname: `/components/CancelledOrder`; params?: Router.UnknownInputParams; } | { pathname: `/components/Category`; params?: Router.UnknownInputParams; } | { pathname: `/components/CompletedOrder`; params?: Router.UnknownInputParams; } | { pathname: `/components/ContactUsCenter`; params?: Router.UnknownInputParams; } | { pathname: `/components/FaqCenter`; params?: Router.UnknownInputParams; } | { pathname: `/components/FavoriteButton`; params?: Router.UnknownInputParams; } | { pathname: `/components/FlashSales`; params?: Router.UnknownInputParams; } | { pathname: `/components/HomeCarousel`; params?: Router.UnknownInputParams; } | { pathname: `/components/ItemCard`; params?: Router.UnknownInputParams; } | { pathname: `/components/ProductQuantityInput`; params?: Router.UnknownInputParams; } | { pathname: `/constants/clothColor`; params?: Router.UnknownInputParams; } | { pathname: `/constants/couponData`; params?: Router.UnknownInputParams; } | { pathname: `/constants/flashSalesData`; params?: Router.UnknownInputParams; } | { pathname: `/constants/myCartData`; params?: Router.UnknownInputParams; } | { pathname: `/constants/paymentData`; params?: Router.UnknownInputParams; } | { pathname: `/constants/recentData`; params?: Router.UnknownInputParams; } | { pathname: `/constants/subShipping`; params?: Router.UnknownInputParams; } | { pathname: `/constants/usersData`; params?: Router.UnknownInputParams; } | { pathname: `/constants/wishlistData`; params?: Router.UnknownInputParams; } | { pathname: `/context/CartContext`; params?: Router.UnknownInputParams; } | { pathname: `/screens/AccessLocation`; params?: Router.UnknownInputParams; } | { pathname: `/screens/AddCardScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/CheckOutScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ChooseShippingScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/CompleteYourProfileScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ConfirmPayment`; params?: Router.UnknownInputParams; } | { pathname: `/screens/CouponScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/FashionScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/FilterScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/FlashSalesScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/HelpCenterScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/HomeScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/InviteFriendsScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/JacketScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/LeaveReviewScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/LocationScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/MyCartScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/NewPasswordScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/NotificationScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/OnboardingScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/OrderScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PasswordManagerScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PaymentMethodScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PaymentSuccess`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PrivacyPolicyScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ProductDetails`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ProductDetailsScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ProfileScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/RegisterScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SearchResultScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SearchScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SettingScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ShippingAddressScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SignInScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/TrackOrderScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/VerifyCodeScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/WellcomeScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/Wishlist`; params?: Router.UnknownInputParams; } | { pathname: `/screens/WishlistScreen`; params?: Router.UnknownInputParams; } | { pathname: `/themes/BottomTabBar`; params?: Router.UnknownInputParams; } | { pathname: `/themes/TabBar`; params?: Router.UnknownInputParams; }; - hrefOutputParams: { pathname: Router.RelativePathString, params?: Router.UnknownOutputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownOutputParams } | { pathname: `/api`; params?: Router.UnknownOutputParams; } | { pathname: `/`; params?: Router.UnknownOutputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownOutputParams; } | { pathname: `/components/ActiveOrder`; params?: Router.UnknownOutputParams; } | { pathname: `/components/CancelledOrder`; params?: Router.UnknownOutputParams; } | { pathname: `/components/Category`; params?: Router.UnknownOutputParams; } | { pathname: `/components/CompletedOrder`; params?: Router.UnknownOutputParams; } | { pathname: `/components/ContactUsCenter`; params?: Router.UnknownOutputParams; } | { pathname: `/components/FaqCenter`; params?: Router.UnknownOutputParams; } | { pathname: `/components/FavoriteButton`; params?: Router.UnknownOutputParams; } | { pathname: `/components/FlashSales`; params?: Router.UnknownOutputParams; } | { pathname: `/components/HomeCarousel`; params?: Router.UnknownOutputParams; } | { pathname: `/components/ItemCard`; params?: Router.UnknownOutputParams; } | { pathname: `/components/ProductQuantityInput`; params?: Router.UnknownOutputParams; } | { pathname: `/constants/clothColor`; params?: Router.UnknownOutputParams; } | { pathname: `/constants/couponData`; params?: Router.UnknownOutputParams; } | { pathname: `/constants/flashSalesData`; params?: Router.UnknownOutputParams; } | { pathname: `/constants/myCartData`; params?: Router.UnknownOutputParams; } | { pathname: `/constants/paymentData`; params?: Router.UnknownOutputParams; } | { pathname: `/constants/recentData`; params?: Router.UnknownOutputParams; } | { pathname: `/constants/subShipping`; params?: Router.UnknownOutputParams; } | { pathname: `/constants/usersData`; params?: Router.UnknownOutputParams; } | { pathname: `/constants/wishlistData`; params?: Router.UnknownOutputParams; } | { pathname: `/context/CartContext`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/AccessLocation`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/AddCardScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/CheckOutScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/ChooseShippingScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/CompleteYourProfileScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/ConfirmPayment`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/CouponScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/FashionScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/FilterScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/FlashSalesScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/HelpCenterScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/HomeScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/InviteFriendsScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/JacketScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/LeaveReviewScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/LocationScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/MyCartScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/NewPasswordScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/NotificationScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/OnboardingScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/OrderScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/PasswordManagerScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/PaymentMethodScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/PaymentSuccess`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/PrivacyPolicyScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/ProductDetails`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/ProductDetailsScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/ProfileScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/RegisterScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/SearchResultScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/SearchScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/SettingScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/ShippingAddressScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/SignInScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/TrackOrderScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/VerifyCodeScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/WellcomeScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/Wishlist`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/WishlistScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/themes/BottomTabBar`; params?: Router.UnknownOutputParams; } | { pathname: `/themes/TabBar`; params?: Router.UnknownOutputParams; }; - href: Router.RelativePathString | Router.ExternalPathString | `/api${`?${string}` | `#${string}` | ''}` | `/${`?${string}` | `#${string}` | ''}` | `/_sitemap${`?${string}` | `#${string}` | ''}` | `/components/ActiveOrder${`?${string}` | `#${string}` | ''}` | `/components/CancelledOrder${`?${string}` | `#${string}` | ''}` | `/components/Category${`?${string}` | `#${string}` | ''}` | `/components/CompletedOrder${`?${string}` | `#${string}` | ''}` | `/components/ContactUsCenter${`?${string}` | `#${string}` | ''}` | `/components/FaqCenter${`?${string}` | `#${string}` | ''}` | `/components/FavoriteButton${`?${string}` | `#${string}` | ''}` | `/components/FlashSales${`?${string}` | `#${string}` | ''}` | `/components/HomeCarousel${`?${string}` | `#${string}` | ''}` | `/components/ItemCard${`?${string}` | `#${string}` | ''}` | `/components/ProductQuantityInput${`?${string}` | `#${string}` | ''}` | `/constants/clothColor${`?${string}` | `#${string}` | ''}` | `/constants/couponData${`?${string}` | `#${string}` | ''}` | `/constants/flashSalesData${`?${string}` | `#${string}` | ''}` | `/constants/myCartData${`?${string}` | `#${string}` | ''}` | `/constants/paymentData${`?${string}` | `#${string}` | ''}` | `/constants/recentData${`?${string}` | `#${string}` | ''}` | `/constants/subShipping${`?${string}` | `#${string}` | ''}` | `/constants/usersData${`?${string}` | `#${string}` | ''}` | `/constants/wishlistData${`?${string}` | `#${string}` | ''}` | `/context/CartContext${`?${string}` | `#${string}` | ''}` | `/screens/AccessLocation${`?${string}` | `#${string}` | ''}` | `/screens/AddCardScreen${`?${string}` | `#${string}` | ''}` | `/screens/CheckOutScreen${`?${string}` | `#${string}` | ''}` | `/screens/ChooseShippingScreen${`?${string}` | `#${string}` | ''}` | `/screens/CompleteYourProfileScreen${`?${string}` | `#${string}` | ''}` | `/screens/ConfirmPayment${`?${string}` | `#${string}` | ''}` | `/screens/CouponScreen${`?${string}` | `#${string}` | ''}` | `/screens/FashionScreen${`?${string}` | `#${string}` | ''}` | `/screens/FilterScreen${`?${string}` | `#${string}` | ''}` | `/screens/FlashSalesScreen${`?${string}` | `#${string}` | ''}` | `/screens/HelpCenterScreen${`?${string}` | `#${string}` | ''}` | `/screens/HomeScreen${`?${string}` | `#${string}` | ''}` | `/screens/InviteFriendsScreen${`?${string}` | `#${string}` | ''}` | `/screens/JacketScreen${`?${string}` | `#${string}` | ''}` | `/screens/LeaveReviewScreen${`?${string}` | `#${string}` | ''}` | `/screens/LocationScreen${`?${string}` | `#${string}` | ''}` | `/screens/MyCartScreen${`?${string}` | `#${string}` | ''}` | `/screens/NewPasswordScreen${`?${string}` | `#${string}` | ''}` | `/screens/NotificationScreen${`?${string}` | `#${string}` | ''}` | `/screens/OnboardingScreen${`?${string}` | `#${string}` | ''}` | `/screens/OrderScreen${`?${string}` | `#${string}` | ''}` | `/screens/PasswordManagerScreen${`?${string}` | `#${string}` | ''}` | `/screens/PaymentMethodScreen${`?${string}` | `#${string}` | ''}` | `/screens/PaymentSuccess${`?${string}` | `#${string}` | ''}` | `/screens/PrivacyPolicyScreen${`?${string}` | `#${string}` | ''}` | `/screens/ProductDetails${`?${string}` | `#${string}` | ''}` | `/screens/ProductDetailsScreen${`?${string}` | `#${string}` | ''}` | `/screens/ProfileScreen${`?${string}` | `#${string}` | ''}` | `/screens/RegisterScreen${`?${string}` | `#${string}` | ''}` | `/screens/SearchResultScreen${`?${string}` | `#${string}` | ''}` | `/screens/SearchScreen${`?${string}` | `#${string}` | ''}` | `/screens/SettingScreen${`?${string}` | `#${string}` | ''}` | `/screens/ShippingAddressScreen${`?${string}` | `#${string}` | ''}` | `/screens/SignInScreen${`?${string}` | `#${string}` | ''}` | `/screens/TrackOrderScreen${`?${string}` | `#${string}` | ''}` | `/screens/VerifyCodeScreen${`?${string}` | `#${string}` | ''}` | `/screens/WellcomeScreen${`?${string}` | `#${string}` | ''}` | `/screens/Wishlist${`?${string}` | `#${string}` | ''}` | `/screens/WishlistScreen${`?${string}` | `#${string}` | ''}` | `/themes/BottomTabBar${`?${string}` | `#${string}` | ''}` | `/themes/TabBar${`?${string}` | `#${string}` | ''}` | { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/api`; params?: Router.UnknownInputParams; } | { pathname: `/`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; } | { pathname: `/components/ActiveOrder`; params?: Router.UnknownInputParams; } | { pathname: `/components/CancelledOrder`; params?: Router.UnknownInputParams; } | { pathname: `/components/Category`; params?: Router.UnknownInputParams; } | { pathname: `/components/CompletedOrder`; params?: Router.UnknownInputParams; } | { pathname: `/components/ContactUsCenter`; params?: Router.UnknownInputParams; } | { pathname: `/components/FaqCenter`; params?: Router.UnknownInputParams; } | { pathname: `/components/FavoriteButton`; params?: Router.UnknownInputParams; } | { pathname: `/components/FlashSales`; params?: Router.UnknownInputParams; } | { pathname: `/components/HomeCarousel`; params?: Router.UnknownInputParams; } | { pathname: `/components/ItemCard`; params?: Router.UnknownInputParams; } | { pathname: `/components/ProductQuantityInput`; params?: Router.UnknownInputParams; } | { pathname: `/constants/clothColor`; params?: Router.UnknownInputParams; } | { pathname: `/constants/couponData`; params?: Router.UnknownInputParams; } | { pathname: `/constants/flashSalesData`; params?: Router.UnknownInputParams; } | { pathname: `/constants/myCartData`; params?: Router.UnknownInputParams; } | { pathname: `/constants/paymentData`; params?: Router.UnknownInputParams; } | { pathname: `/constants/recentData`; params?: Router.UnknownInputParams; } | { pathname: `/constants/subShipping`; params?: Router.UnknownInputParams; } | { pathname: `/constants/usersData`; params?: Router.UnknownInputParams; } | { pathname: `/constants/wishlistData`; params?: Router.UnknownInputParams; } | { pathname: `/context/CartContext`; params?: Router.UnknownInputParams; } | { pathname: `/screens/AccessLocation`; params?: Router.UnknownInputParams; } | { pathname: `/screens/AddCardScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/CheckOutScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ChooseShippingScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/CompleteYourProfileScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ConfirmPayment`; params?: Router.UnknownInputParams; } | { pathname: `/screens/CouponScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/FashionScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/FilterScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/FlashSalesScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/HelpCenterScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/HomeScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/InviteFriendsScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/JacketScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/LeaveReviewScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/LocationScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/MyCartScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/NewPasswordScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/NotificationScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/OnboardingScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/OrderScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PasswordManagerScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PaymentMethodScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PaymentSuccess`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PrivacyPolicyScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ProductDetails`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ProductDetailsScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ProfileScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/RegisterScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SearchResultScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SearchScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SettingScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ShippingAddressScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SignInScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/TrackOrderScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/VerifyCodeScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/WellcomeScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/Wishlist`; params?: Router.UnknownInputParams; } | { pathname: `/screens/WishlistScreen`; params?: Router.UnknownInputParams; } | { pathname: `/themes/BottomTabBar`; params?: Router.UnknownInputParams; } | { pathname: `/themes/TabBar`; params?: Router.UnknownInputParams; }; + hrefInputParams: { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/api`; params?: Router.UnknownInputParams; } | { pathname: `/config`; params?: Router.UnknownInputParams; } | { pathname: `/`; params?: Router.UnknownInputParams; } | { pathname: `/theme`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; } | { pathname: `/components/ActiveOrder`; params?: Router.UnknownInputParams; } | { pathname: `/components/CancelledOrder`; params?: Router.UnknownInputParams; } | { pathname: `/components/Category`; params?: Router.UnknownInputParams; } | { pathname: `/components/CompletedOrder`; params?: Router.UnknownInputParams; } | { pathname: `/components/ContactUsCenter`; params?: Router.UnknownInputParams; } | { pathname: `/components/FaqCenter`; params?: Router.UnknownInputParams; } | { pathname: `/components/FavoriteButton`; params?: Router.UnknownInputParams; } | { pathname: `/components/FlashSales`; params?: Router.UnknownInputParams; } | { pathname: `/components/HomeCarousel`; params?: Router.UnknownInputParams; } | { pathname: `/components/ItemCard`; params?: Router.UnknownInputParams; } | { pathname: `/components/LocationModal`; params?: Router.UnknownInputParams; } | { pathname: `/components/ProductQuantityInput`; params?: Router.UnknownInputParams; } | { pathname: `/constants/clothColor`; params?: Router.UnknownInputParams; } | { pathname: `/constants/storageKeys`; params?: Router.UnknownInputParams; } | { pathname: `/constants/subShipping`; params?: Router.UnknownInputParams; } | { pathname: `/context/AuthContext`; params?: Router.UnknownInputParams; } | { pathname: `/context/CartContext`; params?: Router.UnknownInputParams; } | { pathname: `/context/NotificationContext`; params?: Router.UnknownInputParams; } | { pathname: `/screens/AccessLocation`; params?: Router.UnknownInputParams; } | { pathname: `/screens/AddBankScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/AddCardScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/CheckOutScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ChooseShippingScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/CompleteYourProfileScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ConfirmPayment`; params?: Router.UnknownInputParams; } | { pathname: `/screens/CouponScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/FilterScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/FlashSalesScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/HelpCenterScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/HomeScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/InviteFriendsScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/JacketScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/LeaveReviewScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/LocationScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/MyCartScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/NewPasswordScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/NotificationScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/OnboardingScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/OrderScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PasswordManagerScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PaymentMethodScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PaymentSuccess`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PrivacyPolicyScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ProductDetailsScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ProfileScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/RegisterScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SearchResultScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SearchScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SettingScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ShippingAddressScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SignInScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/TrackOrderScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/VerifyCodeScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/WellcomeScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/WishlistScreen`; params?: Router.UnknownInputParams; } | { pathname: `/themes/BottomTabBar`; params?: Router.UnknownInputParams; } | { pathname: `/themes/TabBar`; params?: Router.UnknownInputParams; }; + hrefOutputParams: { pathname: Router.RelativePathString, params?: Router.UnknownOutputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownOutputParams } | { pathname: `/api`; params?: Router.UnknownOutputParams; } | { pathname: `/config`; params?: Router.UnknownOutputParams; } | { pathname: `/`; params?: Router.UnknownOutputParams; } | { pathname: `/theme`; params?: Router.UnknownOutputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownOutputParams; } | { pathname: `/components/ActiveOrder`; params?: Router.UnknownOutputParams; } | { pathname: `/components/CancelledOrder`; params?: Router.UnknownOutputParams; } | { pathname: `/components/Category`; params?: Router.UnknownOutputParams; } | { pathname: `/components/CompletedOrder`; params?: Router.UnknownOutputParams; } | { pathname: `/components/ContactUsCenter`; params?: Router.UnknownOutputParams; } | { pathname: `/components/FaqCenter`; params?: Router.UnknownOutputParams; } | { pathname: `/components/FavoriteButton`; params?: Router.UnknownOutputParams; } | { pathname: `/components/FlashSales`; params?: Router.UnknownOutputParams; } | { pathname: `/components/HomeCarousel`; params?: Router.UnknownOutputParams; } | { pathname: `/components/ItemCard`; params?: Router.UnknownOutputParams; } | { pathname: `/components/LocationModal`; params?: Router.UnknownOutputParams; } | { pathname: `/components/ProductQuantityInput`; params?: Router.UnknownOutputParams; } | { pathname: `/constants/clothColor`; params?: Router.UnknownOutputParams; } | { pathname: `/constants/storageKeys`; params?: Router.UnknownOutputParams; } | { pathname: `/constants/subShipping`; params?: Router.UnknownOutputParams; } | { pathname: `/context/AuthContext`; params?: Router.UnknownOutputParams; } | { pathname: `/context/CartContext`; params?: Router.UnknownOutputParams; } | { pathname: `/context/NotificationContext`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/AccessLocation`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/AddBankScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/AddCardScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/CheckOutScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/ChooseShippingScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/CompleteYourProfileScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/ConfirmPayment`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/CouponScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/FilterScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/FlashSalesScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/HelpCenterScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/HomeScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/InviteFriendsScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/JacketScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/LeaveReviewScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/LocationScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/MyCartScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/NewPasswordScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/NotificationScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/OnboardingScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/OrderScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/PasswordManagerScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/PaymentMethodScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/PaymentSuccess`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/PrivacyPolicyScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/ProductDetailsScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/ProfileScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/RegisterScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/SearchResultScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/SearchScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/SettingScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/ShippingAddressScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/SignInScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/TrackOrderScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/VerifyCodeScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/WellcomeScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/screens/WishlistScreen`; params?: Router.UnknownOutputParams; } | { pathname: `/themes/BottomTabBar`; params?: Router.UnknownOutputParams; } | { pathname: `/themes/TabBar`; params?: Router.UnknownOutputParams; }; + href: Router.RelativePathString | Router.ExternalPathString | `/api${`?${string}` | `#${string}` | ''}` | `/config${`?${string}` | `#${string}` | ''}` | `/${`?${string}` | `#${string}` | ''}` | `/theme${`?${string}` | `#${string}` | ''}` | `/_sitemap${`?${string}` | `#${string}` | ''}` | `/components/ActiveOrder${`?${string}` | `#${string}` | ''}` | `/components/CancelledOrder${`?${string}` | `#${string}` | ''}` | `/components/Category${`?${string}` | `#${string}` | ''}` | `/components/CompletedOrder${`?${string}` | `#${string}` | ''}` | `/components/ContactUsCenter${`?${string}` | `#${string}` | ''}` | `/components/FaqCenter${`?${string}` | `#${string}` | ''}` | `/components/FavoriteButton${`?${string}` | `#${string}` | ''}` | `/components/FlashSales${`?${string}` | `#${string}` | ''}` | `/components/HomeCarousel${`?${string}` | `#${string}` | ''}` | `/components/ItemCard${`?${string}` | `#${string}` | ''}` | `/components/LocationModal${`?${string}` | `#${string}` | ''}` | `/components/ProductQuantityInput${`?${string}` | `#${string}` | ''}` | `/constants/clothColor${`?${string}` | `#${string}` | ''}` | `/constants/storageKeys${`?${string}` | `#${string}` | ''}` | `/constants/subShipping${`?${string}` | `#${string}` | ''}` | `/context/AuthContext${`?${string}` | `#${string}` | ''}` | `/context/CartContext${`?${string}` | `#${string}` | ''}` | `/context/NotificationContext${`?${string}` | `#${string}` | ''}` | `/screens/AccessLocation${`?${string}` | `#${string}` | ''}` | `/screens/AddBankScreen${`?${string}` | `#${string}` | ''}` | `/screens/AddCardScreen${`?${string}` | `#${string}` | ''}` | `/screens/CheckOutScreen${`?${string}` | `#${string}` | ''}` | `/screens/ChooseShippingScreen${`?${string}` | `#${string}` | ''}` | `/screens/CompleteYourProfileScreen${`?${string}` | `#${string}` | ''}` | `/screens/ConfirmPayment${`?${string}` | `#${string}` | ''}` | `/screens/CouponScreen${`?${string}` | `#${string}` | ''}` | `/screens/FilterScreen${`?${string}` | `#${string}` | ''}` | `/screens/FlashSalesScreen${`?${string}` | `#${string}` | ''}` | `/screens/HelpCenterScreen${`?${string}` | `#${string}` | ''}` | `/screens/HomeScreen${`?${string}` | `#${string}` | ''}` | `/screens/InviteFriendsScreen${`?${string}` | `#${string}` | ''}` | `/screens/JacketScreen${`?${string}` | `#${string}` | ''}` | `/screens/LeaveReviewScreen${`?${string}` | `#${string}` | ''}` | `/screens/LocationScreen${`?${string}` | `#${string}` | ''}` | `/screens/MyCartScreen${`?${string}` | `#${string}` | ''}` | `/screens/NewPasswordScreen${`?${string}` | `#${string}` | ''}` | `/screens/NotificationScreen${`?${string}` | `#${string}` | ''}` | `/screens/OnboardingScreen${`?${string}` | `#${string}` | ''}` | `/screens/OrderScreen${`?${string}` | `#${string}` | ''}` | `/screens/PasswordManagerScreen${`?${string}` | `#${string}` | ''}` | `/screens/PaymentMethodScreen${`?${string}` | `#${string}` | ''}` | `/screens/PaymentSuccess${`?${string}` | `#${string}` | ''}` | `/screens/PrivacyPolicyScreen${`?${string}` | `#${string}` | ''}` | `/screens/ProductDetailsScreen${`?${string}` | `#${string}` | ''}` | `/screens/ProfileScreen${`?${string}` | `#${string}` | ''}` | `/screens/RegisterScreen${`?${string}` | `#${string}` | ''}` | `/screens/SearchResultScreen${`?${string}` | `#${string}` | ''}` | `/screens/SearchScreen${`?${string}` | `#${string}` | ''}` | `/screens/SettingScreen${`?${string}` | `#${string}` | ''}` | `/screens/ShippingAddressScreen${`?${string}` | `#${string}` | ''}` | `/screens/SignInScreen${`?${string}` | `#${string}` | ''}` | `/screens/TrackOrderScreen${`?${string}` | `#${string}` | ''}` | `/screens/VerifyCodeScreen${`?${string}` | `#${string}` | ''}` | `/screens/WellcomeScreen${`?${string}` | `#${string}` | ''}` | `/screens/WishlistScreen${`?${string}` | `#${string}` | ''}` | `/themes/BottomTabBar${`?${string}` | `#${string}` | ''}` | `/themes/TabBar${`?${string}` | `#${string}` | ''}` | { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/api`; params?: Router.UnknownInputParams; } | { pathname: `/config`; params?: Router.UnknownInputParams; } | { pathname: `/`; params?: Router.UnknownInputParams; } | { pathname: `/theme`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; } | { pathname: `/components/ActiveOrder`; params?: Router.UnknownInputParams; } | { pathname: `/components/CancelledOrder`; params?: Router.UnknownInputParams; } | { pathname: `/components/Category`; params?: Router.UnknownInputParams; } | { pathname: `/components/CompletedOrder`; params?: Router.UnknownInputParams; } | { pathname: `/components/ContactUsCenter`; params?: Router.UnknownInputParams; } | { pathname: `/components/FaqCenter`; params?: Router.UnknownInputParams; } | { pathname: `/components/FavoriteButton`; params?: Router.UnknownInputParams; } | { pathname: `/components/FlashSales`; params?: Router.UnknownInputParams; } | { pathname: `/components/HomeCarousel`; params?: Router.UnknownInputParams; } | { pathname: `/components/ItemCard`; params?: Router.UnknownInputParams; } | { pathname: `/components/LocationModal`; params?: Router.UnknownInputParams; } | { pathname: `/components/ProductQuantityInput`; params?: Router.UnknownInputParams; } | { pathname: `/constants/clothColor`; params?: Router.UnknownInputParams; } | { pathname: `/constants/storageKeys`; params?: Router.UnknownInputParams; } | { pathname: `/constants/subShipping`; params?: Router.UnknownInputParams; } | { pathname: `/context/AuthContext`; params?: Router.UnknownInputParams; } | { pathname: `/context/CartContext`; params?: Router.UnknownInputParams; } | { pathname: `/context/NotificationContext`; params?: Router.UnknownInputParams; } | { pathname: `/screens/AccessLocation`; params?: Router.UnknownInputParams; } | { pathname: `/screens/AddBankScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/AddCardScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/CheckOutScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ChooseShippingScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/CompleteYourProfileScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ConfirmPayment`; params?: Router.UnknownInputParams; } | { pathname: `/screens/CouponScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/FilterScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/FlashSalesScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/HelpCenterScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/HomeScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/InviteFriendsScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/JacketScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/LeaveReviewScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/LocationScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/MyCartScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/NewPasswordScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/NotificationScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/OnboardingScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/OrderScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PasswordManagerScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PaymentMethodScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PaymentSuccess`; params?: Router.UnknownInputParams; } | { pathname: `/screens/PrivacyPolicyScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ProductDetailsScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ProfileScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/RegisterScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SearchResultScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SearchScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SettingScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/ShippingAddressScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/SignInScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/TrackOrderScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/VerifyCodeScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/WellcomeScreen`; params?: Router.UnknownInputParams; } | { pathname: `/screens/WishlistScreen`; params?: Router.UnknownInputParams; } | { pathname: `/themes/BottomTabBar`; params?: Router.UnknownInputParams; } | { pathname: `/themes/TabBar`; params?: Router.UnknownInputParams; }; } } } diff --git a/frontend/.gitignore b/frontend/.gitignore index 5873d9a..31feecf 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,4 +1,3 @@ - # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb # The following patterns were generated by expo-cli diff --git a/frontend/__MACOSX/._README.md b/frontend/__MACOSX/._README.md deleted file mode 100644 index 78483a9..0000000 Binary files a/frontend/__MACOSX/._README.md and /dev/null differ diff --git a/frontend/__MACOSX/._app b/frontend/__MACOSX/._app deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/._app and /dev/null differ diff --git a/frontend/__MACOSX/._app.json b/frontend/__MACOSX/._app.json deleted file mode 100644 index 6c31afe..0000000 Binary files a/frontend/__MACOSX/._app.json and /dev/null differ diff --git a/frontend/__MACOSX/._babel.config.js b/frontend/__MACOSX/._babel.config.js deleted file mode 100644 index 4a3dc80..0000000 Binary files a/frontend/__MACOSX/._babel.config.js and /dev/null differ diff --git a/frontend/__MACOSX/._expo-env.d.ts b/frontend/__MACOSX/._expo-env.d.ts deleted file mode 100644 index e9d8e12..0000000 Binary files a/frontend/__MACOSX/._expo-env.d.ts and /dev/null differ diff --git a/frontend/__MACOSX/._global.css b/frontend/__MACOSX/._global.css deleted file mode 100644 index 88356a2..0000000 Binary files a/frontend/__MACOSX/._global.css and /dev/null differ diff --git a/frontend/__MACOSX/._metro.config.js b/frontend/__MACOSX/._metro.config.js deleted file mode 100644 index 5e0eff6..0000000 Binary files a/frontend/__MACOSX/._metro.config.js and /dev/null differ diff --git a/frontend/__MACOSX/._nativewind-env.d.ts b/frontend/__MACOSX/._nativewind-env.d.ts deleted file mode 100644 index de7929d..0000000 Binary files a/frontend/__MACOSX/._nativewind-env.d.ts and /dev/null differ diff --git a/frontend/__MACOSX/._package-lock.json b/frontend/__MACOSX/._package-lock.json deleted file mode 100644 index 29aaa2f..0000000 Binary files a/frontend/__MACOSX/._package-lock.json and /dev/null differ diff --git a/frontend/__MACOSX/._package.json b/frontend/__MACOSX/._package.json deleted file mode 100644 index 70c05fe..0000000 Binary files a/frontend/__MACOSX/._package.json and /dev/null differ diff --git a/frontend/__MACOSX/._tailwind.config.js b/frontend/__MACOSX/._tailwind.config.js deleted file mode 100644 index bf5707f..0000000 Binary files a/frontend/__MACOSX/._tailwind.config.js and /dev/null differ diff --git a/frontend/__MACOSX/._tsconfig.json b/frontend/__MACOSX/._tsconfig.json deleted file mode 100644 index 01f5914..0000000 Binary files a/frontend/__MACOSX/._tsconfig.json and /dev/null differ diff --git a/frontend/__MACOSX/app/.__layout.jsx b/frontend/__MACOSX/app/.__layout.jsx deleted file mode 100644 index aa56ff9..0000000 Binary files a/frontend/__MACOSX/app/.__layout.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/._api b/frontend/__MACOSX/app/._api deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/._api and /dev/null differ diff --git a/frontend/__MACOSX/app/._components b/frontend/__MACOSX/app/._components deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/._components and /dev/null differ diff --git a/frontend/__MACOSX/app/._constants b/frontend/__MACOSX/app/._constants deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/._constants and /dev/null differ diff --git a/frontend/__MACOSX/app/._index.jsx b/frontend/__MACOSX/app/._index.jsx deleted file mode 100644 index aa56ff9..0000000 Binary files a/frontend/__MACOSX/app/._index.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/._screens b/frontend/__MACOSX/app/._screens deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/._screens and /dev/null differ diff --git a/frontend/__MACOSX/app/._themes b/frontend/__MACOSX/app/._themes deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/._themes and /dev/null differ diff --git a/frontend/__MACOSX/app/api/._index.jsx b/frontend/__MACOSX/app/api/._index.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/api/._index.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/components/._ActiveOrder.jsx b/frontend/__MACOSX/app/components/._ActiveOrder.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/components/._ActiveOrder.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/components/._CancelledOrder.jsx b/frontend/__MACOSX/app/components/._CancelledOrder.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/components/._CancelledOrder.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/components/._Category.jsx b/frontend/__MACOSX/app/components/._Category.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/components/._Category.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/components/._CompletedOrder.jsx b/frontend/__MACOSX/app/components/._CompletedOrder.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/components/._CompletedOrder.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/components/._ContactUsCenter.jsx b/frontend/__MACOSX/app/components/._ContactUsCenter.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/components/._ContactUsCenter.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/components/._FaqCenter.jsx b/frontend/__MACOSX/app/components/._FaqCenter.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/components/._FaqCenter.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/components/._FlashSales.jsx b/frontend/__MACOSX/app/components/._FlashSales.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/components/._FlashSales.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/components/._HomeCarousel.jsx b/frontend/__MACOSX/app/components/._HomeCarousel.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/components/._HomeCarousel.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/components/._ItemCard.jsx b/frontend/__MACOSX/app/components/._ItemCard.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/components/._ItemCard.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/constants/._clothColor.jsx b/frontend/__MACOSX/app/constants/._clothColor.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/constants/._clothColor.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/constants/._couponData.jsx b/frontend/__MACOSX/app/constants/._couponData.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/constants/._couponData.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/constants/._flashSalesData.jsx b/frontend/__MACOSX/app/constants/._flashSalesData.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/constants/._flashSalesData.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/constants/._myCartData.jsx b/frontend/__MACOSX/app/constants/._myCartData.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/constants/._myCartData.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/constants/._paymentData.jsx b/frontend/__MACOSX/app/constants/._paymentData.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/constants/._paymentData.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/constants/._recentData.jsx b/frontend/__MACOSX/app/constants/._recentData.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/constants/._recentData.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/constants/._subShipping.jsx b/frontend/__MACOSX/app/constants/._subShipping.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/constants/._subShipping.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/constants/._usersData.jsx b/frontend/__MACOSX/app/constants/._usersData.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/constants/._usersData.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/constants/._wishlistData.jsx b/frontend/__MACOSX/app/constants/._wishlistData.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/constants/._wishlistData.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._AccessLocation.jsx b/frontend/__MACOSX/app/screens/._AccessLocation.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._AccessLocation.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._AddCardScreen.jsx b/frontend/__MACOSX/app/screens/._AddCardScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._AddCardScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._CheckOutScreen.jsx b/frontend/__MACOSX/app/screens/._CheckOutScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._CheckOutScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._ChooseShippingScreen.jsx b/frontend/__MACOSX/app/screens/._ChooseShippingScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._ChooseShippingScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._CompleteYourProfileScreen.jsx b/frontend/__MACOSX/app/screens/._CompleteYourProfileScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._CompleteYourProfileScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._ConfirmPayment.jsx b/frontend/__MACOSX/app/screens/._ConfirmPayment.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._ConfirmPayment.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._CouponScreen.jsx b/frontend/__MACOSX/app/screens/._CouponScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._CouponScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._FashionScreen.jsx b/frontend/__MACOSX/app/screens/._FashionScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._FashionScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._FilterScreen.jsx b/frontend/__MACOSX/app/screens/._FilterScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._FilterScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._HelpCenterScreen.jsx b/frontend/__MACOSX/app/screens/._HelpCenterScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._HelpCenterScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._HomeScreen.jsx b/frontend/__MACOSX/app/screens/._HomeScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._HomeScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._InviteFriendsScreen.jsx b/frontend/__MACOSX/app/screens/._InviteFriendsScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._InviteFriendsScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._JacketScreen.jsx b/frontend/__MACOSX/app/screens/._JacketScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._JacketScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._LeaveReviewScreen.jsx b/frontend/__MACOSX/app/screens/._LeaveReviewScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._LeaveReviewScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._LocationScreen.jsx b/frontend/__MACOSX/app/screens/._LocationScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._LocationScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._MyCartScreen.jsx b/frontend/__MACOSX/app/screens/._MyCartScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._MyCartScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._NewPasswordScreen.jsx b/frontend/__MACOSX/app/screens/._NewPasswordScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._NewPasswordScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._NotificationScreen.jsx b/frontend/__MACOSX/app/screens/._NotificationScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._NotificationScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._OnboardingScreen.jsx b/frontend/__MACOSX/app/screens/._OnboardingScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._OnboardingScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._OrderScreen.jsx b/frontend/__MACOSX/app/screens/._OrderScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._OrderScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._PasswordManagerScreen.jsx b/frontend/__MACOSX/app/screens/._PasswordManagerScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._PasswordManagerScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._PaymentMethodScreen.jsx b/frontend/__MACOSX/app/screens/._PaymentMethodScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._PaymentMethodScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._PaymentSuccess.jsx b/frontend/__MACOSX/app/screens/._PaymentSuccess.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._PaymentSuccess.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._PrivacyPolicyScreen.jsx b/frontend/__MACOSX/app/screens/._PrivacyPolicyScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._PrivacyPolicyScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._ProductDetails.jsx b/frontend/__MACOSX/app/screens/._ProductDetails.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._ProductDetails.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._ProductDetailsScreen.jsx b/frontend/__MACOSX/app/screens/._ProductDetailsScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._ProductDetailsScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._ProfileScreen.jsx b/frontend/__MACOSX/app/screens/._ProfileScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._ProfileScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._SearchResultScreen.jsx b/frontend/__MACOSX/app/screens/._SearchResultScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._SearchResultScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._SearchScreen.jsx b/frontend/__MACOSX/app/screens/._SearchScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._SearchScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._SettingScreen.jsx b/frontend/__MACOSX/app/screens/._SettingScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._SettingScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._ShippingAddressScreen.jsx b/frontend/__MACOSX/app/screens/._ShippingAddressScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._ShippingAddressScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._SignInScreen.jsx b/frontend/__MACOSX/app/screens/._SignInScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._SignInScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._TrackOrderScreen.jsx b/frontend/__MACOSX/app/screens/._TrackOrderScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._TrackOrderScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._VerifyCodeScreen.jsx b/frontend/__MACOSX/app/screens/._VerifyCodeScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._VerifyCodeScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._WellcomeScreen.jsx b/frontend/__MACOSX/app/screens/._WellcomeScreen.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._WellcomeScreen.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/screens/._Wishlist.jsx b/frontend/__MACOSX/app/screens/._Wishlist.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/screens/._Wishlist.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/themes/._BottomTabBar.jsx b/frontend/__MACOSX/app/themes/._BottomTabBar.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/themes/._BottomTabBar.jsx and /dev/null differ diff --git a/frontend/__MACOSX/app/themes/._TabBar.jsx b/frontend/__MACOSX/app/themes/._TabBar.jsx deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/app/themes/._TabBar.jsx and /dev/null differ diff --git a/frontend/__MACOSX/assets/images/._pic.jpg b/frontend/__MACOSX/assets/images/._pic.jpg deleted file mode 100644 index 941d28f..0000000 Binary files a/frontend/__MACOSX/assets/images/._pic.jpg and /dev/null differ diff --git a/frontend/app/_layout.jsx b/frontend/app/_layout.jsx index ec70151..a8bfe92 100644 --- a/frontend/app/_layout.jsx +++ b/frontend/app/_layout.jsx @@ -1,17 +1,20 @@ import { Stack } from 'expo-router'; import { CartProvider } from './context/CartContext'; +import { NotificationProvider } from './context/NotificationContext'; // Import your global CSS file import "../global.css"; export default function Layout() { return ( - - - + + + + + ); } \ No newline at end of file diff --git a/frontend/app/api.jsx b/frontend/app/api.jsx index 5d49de8..5c539ac 100644 --- a/frontend/app/api.jsx +++ b/frontend/app/api.jsx @@ -1,6 +1,8 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; import axios from 'axios'; +import AsyncStorage from '@react-native-async-storage/async-storage'; import { View, Text } from 'react-native'; +import { getApiBaseUrl } from './config'; +import { STORAGE_KEYS } from './constants/storageKeys'; // Utility function to simulate network delay const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); @@ -62,127 +64,725 @@ export default function Api() { ); } - -// Storage Keys -const STORAGE_KEYS = { - CART: 'cart', - USER_TOKEN: 'userToken', - USER_DATA: 'userData', - FAVORITES: 'favorites', - CSRF_TOKEN: 'csrfToken' -}; - -// API Configuration8000 -export const MEDIA_BASE_URL = 'http://192.168.18.3:8000'; +// API Configuration +export const MEDIA_BASE_URL = getApiBaseUrl(); export const BASE_URL = `${MEDIA_BASE_URL}/api`; -// Create axios instance with consistent configuration -const apiClient = axios.create({ +// Create axios instance with default config +const api = axios.create({ baseURL: BASE_URL, - timeout: 10000, - withCredentials: true, // Important for CSRF cookie handling + withCredentials: true, headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' - } + 'Accept': 'application/json', + }, + // Ensure cookies are included in cross-origin requests + xsrfCookieName: 'csrftoken', + xsrfHeaderName: 'X-CSRFToken', }); -// Function to fetch CSRF token -export const fetchCSRFToken = async () => { +// Function to update base URL +export const updateBaseURL = (newURL) => { + api.defaults.baseURL = newURL; +}; + +// Function to get CSRF token +export const getCSRFToken = async () => { try { - const response = await axios.get(`${BASE_URL}/get-csrf-token/`, { - withCredentials: true, - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - }); - - if (response.data && response.data.csrfToken) { - await AsyncStorage.setItem(STORAGE_KEYS.CSRF_TOKEN, response.data.csrfToken); - // Also set it in the default headers - apiClient.defaults.headers.common['X-CSRFToken'] = response.data.csrfToken; - return response.data.csrfToken; + // First check if we already have a token stored + const storedToken = await getStoredCSRFToken(); + if (storedToken) { + return storedToken; } - console.error('No CSRF token in response:', response.data); + // If not, fetch a new token + const response = await api.get('/csrf-token/'); + + // Extract token from response + let csrfToken = null; + if (response.data && response.data.csrfToken) { + csrfToken = response.data.csrfToken; + } else if (response.headers && response.headers['x-csrf-token']) { + csrfToken = response.headers['x-csrf-token']; + } + + // Store the token if we got one + if (csrfToken) { + await AsyncStorage.setItem(STORAGE_KEYS.CSRF_TOKEN, csrfToken); + return csrfToken; + } + + console.warn('No CSRF token found in response'); return null; } catch (error) { - console.error('Failed to fetch CSRF token:', error.response || error); + console.error('Error fetching CSRF token:', error); return null; } }; -// Helper function to get stored CSRF token -const getCSRFToken = async () => { +// Function to get stored CSRF token +export const getStoredCSRFToken = async () => { try { return await AsyncStorage.getItem(STORAGE_KEYS.CSRF_TOKEN); } catch (error) { - console.error('Error getting CSRF token from storage:', error); + console.error('Error getting stored CSRF token:', error); return null; } }; -// Add token and CSRF token to requests when available -apiClient.interceptors.request.use( - async (config) => { - // Add auth token if available - const token = await AsyncStorage.getItem(STORAGE_KEYS.USER_TOKEN); +// Initialize API and fetch CSRF token +export const initializeAPI = async () => { + try { + // Get stored auth token + const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); if (token) { - config.headers.Authorization = `Token ${token}`; + api.defaults.headers.common['Authorization'] = `Token ${token}`; } - - // Add CSRF token if available - const csrfToken = await getCSRFToken(); - if (csrfToken) { - config.headers['X-CSRFToken'] = csrfToken; + + // Get CSRF token + await getCSRFToken(); + } catch (error) { + console.error('Error initializing API:', error); + } +}; + +// Request interceptor +api.interceptors.request.use( + async (config) => { + try { + // Add auth token to headers + const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); + if (token) { + config.headers['Authorization'] = `Token ${token}`; + } + } catch (error) { + console.error('Error in request interceptor:', error); } - return config; }, - (error) => Promise.reject(error) -); - -// Handle response errors, especially auth errors -apiClient.interceptors.response.use( - response => response, - async (error) => { - // Handle 403 CSRF errors - if (error.response && error.response.status === 403) { - const errorDetail = error.response.data?.detail || ''; - if (errorDetail.includes('CSRF') || - (typeof error.response.data === 'string' && error.response.data.includes('CSRF'))) { - console.log('CSRF token missing or invalid. Fetching new token...'); - - try { - // Get a new CSRF token - await fetchCSRFToken(); - - // Retry the original request with new CSRF token - const originalRequest = error.config; - const csrfToken = await getCSRFToken(); - if (csrfToken) { - originalRequest.headers['X-CSRFToken'] = csrfToken; - return axios(originalRequest); - } - } catch (csrfError) { - console.error('Failed to refresh CSRF token:', csrfError); - } - } - } - - // Handle 401 unauthorized errors - if (error.response && error.response.status === 401) { - // Clear auth data - await AsyncStorage.removeItem(STORAGE_KEYS.USER_TOKEN); - delete apiClient.defaults.headers.common['Authorization']; - } - + (error) => { return Promise.reject(error); } ); -// Process image URLs with authentication if needed +// Response interceptor +api.interceptors.response.use( + (response) => response, + async (error) => { + if (error.response?.status === 401) { + try { + await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN); + await AsyncStorage.removeItem(STORAGE_KEYS.USER_DATA); + delete api.defaults.headers.common['Authorization']; + } catch (storageError) { + console.error('Error clearing auth data:', storageError); + } + } + return Promise.reject(error); + } +); + +// Auth functions +export const login = async (credentials) => { + try { + const response = await api.post('/token-auth/', credentials); + if (response.data.token) { + // Set the auth token in the API instance + api.defaults.headers.common['Authorization'] = `Token ${response.data.token}`; + // Store the token and user data + await AsyncStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, response.data.token); + if (response.data.user_id) { + // Get user data + const userResponse = await api.get(`/users/${response.data.user_id}/`); + if (userResponse.data) { + await AsyncStorage.setItem(STORAGE_KEYS.USER_DATA, JSON.stringify(userResponse.data)); + } + } + } + return response.data; + } catch (error) { + console.error('Login error:', error); + throw error; + } +}; + +export const register = async (userData) => { + try { + // First, get a CSRF token + const csrfToken = await getCSRFToken(); + + // Make the registration request with the CSRF token in headers + const response = await api.post('/auth/register/', userData, { + headers: { + 'X-CSRFToken': csrfToken + } + }); + + // If registration is successful and we get a token, store it + if (response.data && response.data.token) { + // Set the auth token in the API instance + api.defaults.headers.common['Authorization'] = `Token ${response.data.token}`; + // Store the token + await AsyncStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, response.data.token); + + // If we have user data, store it + if (response.data.user_id) { + const userData = { + id: response.data.user_id, + username: response.data.username, + email: response.data.email, + first_name: response.data.first_name, + last_name: response.data.last_name + }; + await AsyncStorage.setItem(STORAGE_KEYS.USER_DATA, JSON.stringify(userData)); + } + } + + return response.data; + } catch (error) { + console.error('Registration error:', error.response?.data || error); + throw error; + } +}; + +export const logout = async () => { + try { + await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN); + await AsyncStorage.removeItem(STORAGE_KEYS.USER_DATA); + delete api.defaults.headers.common['Authorization']; + } catch (error) { + console.error('Logout error:', error); + throw error; + } +}; + +// User functions +export const getCurrentUser = async () => { + try { + // Check if we have a token + const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); + if (!token) return null; + + // Get user data from API + const response = await api.get('/users/me/'); + + // Store updated data + if (response.data) { + await AsyncStorage.setItem(STORAGE_KEYS.USER_DATA, JSON.stringify(response.data)); + } + + return response.data; + } catch (error) { + console.error('Error getting current user:', error); + + // Fallback to cached data + const userData = await AsyncStorage.getItem(STORAGE_KEYS.USER_DATA); + return userData ? JSON.parse(userData) : null; + } +}; + +export const getUserData = async () => { + try { + // First try to get from API + const currentUser = await getCurrentUser(); + if (currentUser) return currentUser; + + // If API fails, try to get from storage + const userData = await AsyncStorage.getItem(STORAGE_KEYS.USER_DATA); + return userData ? JSON.parse(userData) : null; + } catch (error) { + console.error('Error getting user data:', error); + return null; + } +}; + +export const updateUserProfile = async (profileData) => { + try { + const response = await api.post('/users/update_profile/', profileData); + if (response.data) { + await AsyncStorage.setItem(STORAGE_KEYS.USER_DATA, JSON.stringify(response.data)); + } + return response.data; + } catch (error) { + console.error('Profile update error:', error.response?.data || error); + throw error; + } +}; + +export const updateProfilePicture = async (imageUri) => { + try { + const formData = new FormData(); + formData.append('profile_picture', { + uri: imageUri, + type: 'image/jpeg', + name: 'profile_picture.jpg', + }); + + const response = await api.post('/users/update_profile_picture/', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + 'Accept': 'application/json', + }, + }); + + if (response.data && response.data.profile_picture) { + await AsyncStorage.setItem(STORAGE_KEYS.USER_DATA, JSON.stringify(response.data)); + } + return response.data; + } catch (error) { + console.error('Profile picture update error:', error.response?.data || error); + throw error; + } +}; + +export const getGenderOptions = async () => { + try { + const response = await api.get('/users/gender_options/'); + // Transform the response to match the backend values + return response.data.map(option => ({ + value: option.value.charAt(0).toUpperCase() + option.value.slice(1), + label: option.label + })); + } catch (error) { + // Return default options if API fails + return [ + { value: 'Male', label: 'Male' }, + { value: 'Female', label: 'Female' }, + { value: 'Other', label: 'Other' } + ]; + } +}; + +// Cart functions +export const getCart = async () => { + try { + const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); + if (!token) { + // If not logged in, get from local storage + const cartData = await AsyncStorage.getItem(STORAGE_KEYS.CART); + return cartData ? JSON.parse(cartData) : { items: [] }; + } + + // Set the auth token in headers + api.defaults.headers.common['Authorization'] = `Token ${token}`; + + const { data } = await api.get('/cart/items/'); + + return data; + } catch (error) { + console.error('Error getting cart:', error); + + if (error.response?.status === 401) { + // Clear auth data on 401 + await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN); + delete api.defaults.headers.common['Authorization']; + throw new Error('Session expired. Please login again.'); + } + + throw error; + } +}; + +// Add global refresh cart callback +let globalRefreshCartCallback = null; + +export const setGlobalRefreshCart = (callback) => { + globalRefreshCartCallback = callback; +}; + +// Update cart functions to use the callback +export const addToCart = async (productId, quantity = 1) => { + try { + const response = await api.post('/cart/add_item/', { + product_id: productId, + quantity: quantity + }); + if (globalRefreshCartCallback) { + globalRefreshCartCallback(); + } + return response.data; + } catch (error) { + console.error('Error adding to cart:', error); + if (error.response?.status === 401) { + await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN); + delete api.defaults.headers.common['Authorization']; + throw new Error('Session expired. Please login again.'); + } + throw error; + } +}; + +export const removeFromCart = async (cartItemId) => { + try { + // Ensure we have a valid token and set it in headers + const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); + if (!token) { + // If no token, fall back to local storage cart handling + console.log('No auth token found, using local storage for cart'); + const cartData = await AsyncStorage.getItem(STORAGE_KEYS.CART); + let cart = cartData ? JSON.parse(cartData) : { items: [] }; + + // Filter out the item + cart.items = cart.items.filter(item => item.id !== cartItemId); + await AsyncStorage.setItem(STORAGE_KEYS.CART, JSON.stringify(cart)); + return cart; + } + + // First, get the cart to find the correct product_id + const cartResponse = await api.get('/cart/items/'); + const cartItems = cartResponse.data.items || []; + + // Find the cart item that matches the ID we want to delete + const cartItem = cartItems.find(item => item.id === cartItemId); + + if (!cartItem) { + throw new Error('Cart item not found'); + } + + // Use the product_id from the cart item + const productId = cartItem.product_id || cartItem.product; + console.log(`Found product_id: ${productId} for cart item: ${cartItemId}`); + + // Use the correct endpoint and method with the product_id + const response = await api.post('/cart/remove_item/', { + product_id: productId + }); + + console.log('Item removed successfully'); + if (globalRefreshCartCallback) globalRefreshCartCallback(); + return response.data; + } catch (error) { + + if (error.response?.status === 401) { + await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN); + delete api.defaults.headers.common['Authorization']; + throw new Error('Session expired. Please login again.'); + } + + if (error.response?.status === 403) { + throw new Error('You do not have permission to remove this item from cart.'); + } + + if (error.response?.status === 404) { + throw new Error('Cart item not found'); + } + + throw error; + } +}; + +// Make deleteCartItem directly handle the delete functionality rather than just being an alias +export const deleteCartItem = async (itemId) => { + try { + return await removeFromCart(itemId); + } catch (error) { + console.error('Error in deleteCartItem:', error); + throw error; + } +}; + +export const updateCartItemQuantity = async (cartItemId, quantity) => { + try { + // Get the auth token + const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); + if (!token) { + throw new Error('Please login to update cart'); + } + + // Set the auth token in headers + api.defaults.headers.common['Authorization'] = `Token ${token}`; + + // Make the API call to update quantity + const response = await api.post('/cart/update_quantity/', { + product_id: cartItemId, + quantity: quantity + }); + + return response.data; + } catch (error) { + if (error.response?.status === 401) { + // Clear auth data on 401 + await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN); + delete api.defaults.headers.common['Authorization']; + throw new Error('Session expired. Please login again.'); + } + if (error.response?.status === 403) { + throw new Error('You do not have permission to update this cart item.'); + } + if (error.response?.status === 404) { + throw new Error('Cart item not found'); + } + throw error; + } +}; + +export const checkoutCart = async (shippingDetails) => { + try { + const { data } = await api.post('/cart/checkout/', shippingDetails); + return data; + } catch (error) { + console.error('Error during checkout:', error); + throw error; + } +}; + +// Product functions +export const fetchProducts = async (searchQuery = '') => { + try { + const { data } = await api.get('/products/', { + params: { + search: searchQuery + } + }); + return data; + } catch (error) { + console.error('Error fetching products:', error); + throw error; + } +}; + +export const fetchProductById = async (id) => { + try { + // First try the products endpoint + try { + const { data } = await api.get(`/products/${id}/`); + return { + ...data, + image: data.image ? await processImageUrl(data.image) : null + }; + } catch (productsError) { + // If products endpoint fails, try the posts endpoint + if (productsError.response?.status === 404) { + const { data } = await api.get(`/posts/${id}/`); + return { + ...data, + image: data.image ? await processImageUrl(data.image) : null + }; + } + throw productsError; + } + } catch (error) { + throw error; + } +}; + +// Category functions +export const fetchCategories = async () => { + try { + const { data } = await api.get('/categories/'); + return data; + } catch (error) { + throw error; + } +}; + +export const fetchCategoryById = async (id) => { + try { + const { data } = await api.get(`/categories/${id}/`); + return data; + } catch (error) { + throw error; + } +}; + +export const fetchProductsByCategory = async (categoryId) => { + try { + // Validate categoryId + if (!categoryId || categoryId === 'undefined' || isNaN(parseInt(categoryId))) { + throw new Error('Invalid category ID'); + } + + const { data } = await api.get(`/categories/${categoryId}/products/`); + + // Validate response data + if (!data) { + throw new Error('No data received from server'); + } + + if (!Array.isArray(data)) { + throw new Error('Invalid response format from server'); + } + + return data; + } catch (error) { + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + throw { + response: { + data: error.response.data, + status: error.response.status + } + }; + } else if (error.request) { + // The request was made but no response was received + throw new Error('No response from server'); + } else { + // Something happened in setting up the request that triggered an Error + throw error; + } + } +}; + +// Order functions +export const getOrders = async () => { + try { + const { data } = await api.get('/orders/'); + return data; + } catch (error) { + throw error; + } +}; + +export const getOrderById = async (orderId) => { + try { + const { data } = await api.get(`/orders/${orderId}/`); + return data; + } catch (error) { + throw error; + } +}; + +export const placeOrder = async (orderData) => { + try { + const { data } = await api.post('/orders/', orderData); + return data; + } catch (error) { + console.error('Error placing order:', error); + throw error; + } +}; + +// New order status management functions +export const updateOrderStatus = async (orderId, status) => { + try { + const { data } = await api.put(`/orders/${orderId}/`, { status }); + return data; + } catch (error) { + console.error('Error updating order status:', error); + throw error; + } +}; + +export const shipOrder = async (orderId) => { + try { + const { data } = await api.post(`/orders/${orderId}/ship/`); + return data; + } catch (error) { + console.error('Error shipping order:', error); + throw error; + } +}; + +export const deliverOrder = async (orderId) => { + try { + const { data } = await api.post(`/orders/${orderId}/deliver/`); + return data; + } catch (error) { + console.error('Error delivering order:', error); + throw error; + } +}; + +export const cancelOrder = async (orderId) => { + try { + const { data } = await api.post(`/orders/${orderId}/cancel/`); + return data; + } catch (error) { + console.error('Error cancelling order:', error); + throw error; + } +}; + +// Favorite functions +export const getFavorites = async () => { + try { + const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); + if (!token) { + // If not logged in, get from local storage + const favoritesData = await AsyncStorage.getItem(STORAGE_KEYS.FAVORITES); + return favoritesData ? JSON.parse(favoritesData) : []; + } + + const { data } = await api.get('/favorites/'); + return Array.isArray(data) ? data : []; + } catch (error) { + return []; + } +}; + +export const toggleFavorite = async (product) => { + try { + const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); + if (!token) { + // If not logged in, use local storage + const favoritesData = await AsyncStorage.getItem(STORAGE_KEYS.FAVORITES); + const favorites = favoritesData ? JSON.parse(favoritesData) : []; + + const index = favorites.findIndex(fav => fav.id === product.id); + if (index === -1) { + favorites.push(product); + } else { + favorites.splice(index, 1); + } + + await AsyncStorage.setItem(STORAGE_KEYS.FAVORITES, JSON.stringify(favorites)); + return favorites; + } + + // If logged in, use API + const response = await api.post('/favorites/toggle/', { + product_id: product.id + }); + console.log('API toggleFavorite response:', response.data); + + // The response should contain the updated favorites list + return Array.isArray(response.data) ? response.data : []; + } catch (error) { + console.error('Error toggling favorite:', error); + return []; + } +}; + +export const isProductFavorited = async (productId) => { + try { + const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); + if (!token) { + // If not logged in, check local storage + const favoritesData = await AsyncStorage.getItem(STORAGE_KEYS.FAVORITES); + const favorites = favoritesData ? JSON.parse(favoritesData) : []; + return favorites.some(fav => fav.id === productId); + } + + // If logged in, check API + const { data } = await api.get(`/favorites/check/${productId}/`); + return data.is_favorited; + } catch (error) { + console.error('Error checking favorite status:', error); + return false; + } +}; + +// Rating functions +export const rateProduct = async (productId, rating, comment = '') => { + try { + const { data } = await api.post(`/products/${productId}/rate/`, { + rating, + comment + }); + return data; + } catch (error) { + console.error('Error rating product:', error); + if (error.response?.status === 401) { + await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN); + delete api.defaults.headers.common['Authorization']; + throw new Error('Session expired. Please login again.'); + } + throw error; + } +}; + +// Utility functions export const processImageUrl = async (url) => { if (!url) { console.log('No image URL provided, using local fallback'); @@ -212,659 +812,37 @@ export const processImageUrl = async (url) => { } }; -// CATEGORIES -export const fetchCategories = async () => { +export const updatePassword = async (currentPassword, newPassword) => { try { - const { data } = await apiClient.get('/categories/'); - const processedData = await Promise.all(data.map(async category => ({ - ...category, - image: await processImageUrl(category.image) - }))); - return processedData; - } catch (error) { - console.error('Error fetching categories:', error); - throw error; - } -}; - -export const fetchCategoryById = async (id) => { - try { - const { data } = await apiClient.get(`/categories/${id}/`); - return { - ...data, - image: await processImageUrl(data.image) - }; - } catch (error) { - console.error('Error fetching category:', error); - throw error; - } -}; - -export const fetchProductsByCategory = async (categoryId) => { - try { - const { data } = await apiClient.get(`/categories/${categoryId}/products/`); - const processedData = await Promise.all( - data.map(async (product) => ({ - ...product, - image: await processImageUrl(product.image) - })) - ); - return processedData; - } catch (error) { - console.error('Error fetching products by category:', error); - throw error; - } -}; - -// PRODUCTS -export const fetchProducts = async (searchTerm = '') => { - try { - // Ensure searchTerm is a string - const searchQuery = String(searchTerm || '').trim(); - - // Simulate authentication check - await checkAuthentication(); - - // Make API call to fetch products - const { data } = await apiClient.get('/products/', { - params: { - search: searchQuery - } + const response = await api.post('/users/update_password/', { + current_password: currentPassword, + new_password: newPassword }); - - // Process product images - const processedData = await Promise.all( - data.map(async (product) => ({ - ...product, - image: await processImageUrl(product.image) - })) - ); - - // Save to recent searches if user is logged in and search term is not empty - if (searchQuery) { - try { - await saveUserRecentSearch(searchQuery); - } catch (error) { - // Silently fail if user is not logged in - console.log('User not logged in, not saving search'); - } - } - - return processedData; - } catch (error) { - console.error('Error fetching products:', error); - // Return empty array instead of throwing error - return []; - } -}; - -export const fetchProductById = async (id) => { - try { - // First try the products endpoint - try { - const { data } = await apiClient.get(`/products/${id}/`); - return { - ...data, - image: await processImageUrl(data.image) - }; - } catch (productsError) { - // If products endpoint fails, try the posts endpoint - if (productsError.response?.status === 404) { - const { data } = await apiClient.get(`/posts/${id}/`); - return { - ...data, - image: await processImageUrl(data.image), - // Map post fields to product fields if needed - name: data.title, - price: data.price || 0, - rating: data.rating || 0 - }; - } - throw productsError; - } - } catch (error) { - console.error('Error fetching product:', error); - if (error.response?.status === 404) { - throw new Error('Product not found'); - } - throw error; - } -}; - -// CART -export const getCart = async () => { - try { - const token = await AsyncStorage.getItem(STORAGE_KEYS.USER_TOKEN); - if (!token) { - // If not logged in, get from local storage - const localCart = await AsyncStorage.getItem(STORAGE_KEYS.CART); - return localCart ? JSON.parse(localCart) : { items: [] }; - } - - const { data } = await apiClient.get('/cart/items/'); - - // Process product images and prices - if (data && data.items) { - const processedItems = await Promise.all( - data.items.map(async (item) => { - const price = parseFloat(item.product_price); - return { - ...item, - product_image: await processImageUrl(item.product_image), - product_price: !isNaN(price) ? price : 0, - price: !isNaN(price) ? price : 0 - }; - }) - ); - return { - ...data, - items: processedItems - }; - } - return data; - } catch (error) { - console.error('Error getting cart:', error); - if (error.response?.status === 401) { - // If unauthorized, try to get from local storage - const localCart = await AsyncStorage.getItem(STORAGE_KEYS.CART); - return localCart ? JSON.parse(localCart) : { items: [] }; - } - throw error; - } -}; - -// Global refresh function -let globalRefreshCart = null; - -export const setGlobalRefreshCart = (refreshFunction) => { - globalRefreshCart = refreshFunction; -}; - -export const addToCart = async (productId, quantity = 1) => { - try { - const token = await AsyncStorage.getItem(STORAGE_KEYS.USER_TOKEN); - if (!token) { - // If not logged in, use local storage - const localCart = await AsyncStorage.getItem(STORAGE_KEYS.CART); - const currentCart = localCart ? JSON.parse(localCart) : { items: [] }; - - // Check if product already exists in cart - const existingItem = currentCart.items.find(item => item.product === productId); - if (existingItem) { - existingItem.quantity += quantity; - } else { - currentCart.items.push({ - product: productId, - quantity, - product_price: 0, // Will be updated when user logs in - product_image: null - }); - } - - await AsyncStorage.setItem(STORAGE_KEYS.CART, JSON.stringify(currentCart)); - - // Trigger cart refresh if available - if (globalRefreshCart) { - globalRefreshCart(); - } - - return currentCart; - } - - const { data } = await apiClient.post('/cart/add_item/', { - product_id: productId, - quantity - }); - - // Process product images and prices - if (data && data.items) { - const processedItems = await Promise.all( - data.items.map(async (item) => { - const price = parseFloat(item.product_price); - return { - ...item, - product_image: await processImageUrl(item.product_image), - product_price: !isNaN(price) ? price : 0, - price: !isNaN(price) ? price : 0 - }; - }) - ); - - // Trigger cart refresh if available - if (globalRefreshCart) { - globalRefreshCart(); - } - - return { - ...data, - items: processedItems - }; - } - return data; - } catch (error) { - console.error('Error adding to cart:', error); - if (error.response?.status === 401) { - // If unauthorized, try to add to local storage - return await addToCart(productId, quantity); - } - throw error; - } -}; - -export const updateCartItemQuantity = async (itemId, quantity) => { - try { - const { data } = await apiClient.post('/cart/update_quantity/', { - item_id: itemId, - quantity: quantity - }); - return data; - } catch (error) { - console.error('Error updating cart quantity:', error); - if (error.response?.status === 401) { - await AsyncStorage.removeItem(STORAGE_KEYS.USER_TOKEN); - delete apiClient.defaults.headers.common['Authorization']; - throw new Error('Session expired. Please login again.'); - } - throw error; - } -}; - -export const deleteCartItem = async (itemId) => { - try { - console.log('Attempting to delete cart item with ID:', itemId); - // First get the cart item to get the product ID - const cartResponse = await apiClient.get('/cart/items/'); - const cartItem = cartResponse.data.items.find(item => item.id === itemId); - - if (!cartItem) { - throw new Error('Cart item not found'); - } - - const response = await apiClient.post('/cart/remove_item/', { - product_id: cartItem.product - }); - console.log('Delete response:', response.data); return response.data; } catch (error) { - console.error('Error removing cart item:', error); - console.error('Error response:', error.response?.data); - console.error('Error status:', error.response?.status); - if (error.response?.status === 401) { - await AsyncStorage.removeItem(STORAGE_KEYS.USER_TOKEN); - delete apiClient.defaults.headers.common['Authorization']; - throw new Error('Session expired. Please login again.'); - } + console.error('Error updating password:', error); throw error; } }; -export const checkoutCart = async (shippingDetails) => { +export const deleteAccount = async (password) => { try { - const { data } = await apiClient.post('/cart/checkout/', shippingDetails); - return data; - } catch (error) { - console.error('Error checking out:', error); - throw error; - } -}; - -// AUTH - Fixed to match Django URLs -export const loginUser = async (credentials) => { - try { - // First ensure we have a CSRF token - const csrfToken = await fetchCSRFToken(); - if (!csrfToken) { - console.error('Failed to get CSRF token before login'); - return { success: false, error: 'Failed to establish secure connection' }; - } - - // Use the correct endpoint from your Django URLs - const response = await apiClient.post('/token-auth/', credentials); - const data = response.data; - - if (data && data.token) { - // Store token - await AsyncStorage.setItem(STORAGE_KEYS.USER_TOKEN, data.token); - - // Set the token in axios defaults - apiClient.defaults.headers.common['Authorization'] = `Token ${data.token}`; - - // Store user data - if (data.user) { - await AsyncStorage.setItem(STORAGE_KEYS.USER_DATA, JSON.stringify(data.user)); - } - - return { success: true, user: data.user }; - } - - return { success: false, error: data?.message || 'Invalid credentials' }; - } catch (error) { - console.error('Login Error:', error.response?.data || error); - - // Handle specific error cases - if (error.response) { - if (error.response.status === 401) { - return { success: false, error: 'Invalid username or password' }; - } else if (error.response.status === 403) { - // CSRF token might be invalid, try to refresh it - await fetchCSRFToken(); - return { success: false, error: 'Please try again' }; - } - return { - success: false, - error: error.response.data?.message || 'Login failed' - }; - } - - // Network or other errors - return { - success: false, - error: error.message || 'Network error. Please check your connection.' - }; - } -}; - -export const registerUser = async (userData) => { - try { - // Ensure we have a CSRF token - await fetchCSRFToken(); - - const { data } = await apiClient.post('/users/', userData); - return data; - } catch (error) { - console.error('Registration Error:', error); - throw error; - } -}; - -export const logoutUser = async () => { - try { - // Try to logout on server using the correct endpoint - try { - await apiClient.post('/logout/'); - } catch (apiError) { - console.log('API logout failed, continuing with local logout:', apiError); - } - - // Clear local storage - await AsyncStorage.removeItem(STORAGE_KEYS.USER_TOKEN); - await AsyncStorage.removeItem(STORAGE_KEYS.USER_DATA); - - // Clear API client headers - delete apiClient.defaults.headers.common['Authorization']; - - return { success: true }; - } catch (error) { - console.error('Error during logout:', error); - return { success: false, error: error.message }; - } -}; - -export const getCurrentUser = async () => { - try { - // Check if we have a token - const token = await AsyncStorage.getItem(STORAGE_KEYS.USER_TOKEN); - if (!token) return null; - - // Get user data from API - const { data } = await apiClient.get('/users/me/'); - - // Store updated data - await AsyncStorage.setItem(STORAGE_KEYS.USER_DATA, JSON.stringify(data)); - - return data; - } catch (error) { - console.error('Error getting current user:', error); - - // Fallback to cached data - const userData = await AsyncStorage.getItem(STORAGE_KEYS.USER_DATA); - return userData ? JSON.parse(userData) : null; - } -}; - -// ORDERS -export const getOrders = async () => { - try { - const { data } = await apiClient.get('/orders/'); - return data; - } catch (error) { - console.error('Error fetching orders:', error); - return []; - } -}; - -export const getOrderById = async (orderId) => { - try { - const { data } = await apiClient.get(`/orders/${orderId}/`); - return data; - } catch (error) { - console.error('Error fetching order details:', error); - throw error; - } -}; - -export const placeOrder = async (orderData) => { - try { - const { data } = await apiClient.post('/orders/', orderData); - return data; - } catch (error) { - console.error('Error placing order:', error); - throw error; - } -}; - -// FAVORITES -export const getFavorites = async () => { - try { - const token = await AsyncStorage.getItem(STORAGE_KEYS.USER_TOKEN); - if (!token) { - // If not logged in, get from local storage - const localFavorites = await AsyncStorage.getItem(STORAGE_KEYS.FAVORITES); - return localFavorites ? JSON.parse(localFavorites) : []; - } - - const { data } = await apiClient.get('/favorites/'); - - // Process images - if (Array.isArray(data)) { - const processedData = await Promise.all( - data.map(async (favorite) => { - const product = favorite.product || favorite; - return { - ...product, - image: await processImageUrl(product.image) - }; - }) - ); - return processedData; - } - - return data || []; - } catch (error) { - console.error('Error getting favorites:', error); - return []; - } -}; - -export const toggleFavorite = async (product) => { - try { - const token = await AsyncStorage.getItem(STORAGE_KEYS.USER_TOKEN); - if (!token) { - // If not logged in, use local storage - const localFavorites = await AsyncStorage.getItem(STORAGE_KEYS.FAVORITES); - const currentFavorites = localFavorites ? JSON.parse(localFavorites) : []; - - // Check if product exists in favorites - const exists = currentFavorites.some(item => item.id === product.id); - let updatedFavorites; - - if (exists) { - // Remove from favorites - updatedFavorites = currentFavorites.filter(item => item.id !== product.id); - } else { - // Add to favorites - const processedProduct = { - ...product, - image: await processImageUrl(product.image) - }; - updatedFavorites = [...currentFavorites, processedProduct]; - } - - // Save to local storage - await AsyncStorage.setItem(STORAGE_KEYS.FAVORITES, JSON.stringify(updatedFavorites)); - return updatedFavorites; - } - - // If logged in, use API - await apiClient.post('/favorites/toggle/', { - product_id: product.id + const response = await api.post('/users/delete_account/', { + password: password }); - - // Get updated favorites list - return await getFavorites(); + return response.data; } catch (error) { - console.error('Error toggling favorite:', error); - return []; - } -}; - -// RATINGS -export const rateProduct = async (productId, rating, comment = '') => { - try { - const { data } = await apiClient.post(`/products/${productId}/rate/`, { - rating, - comment - }); - return data; - } catch (error) { - console.error('Error rating product:', error); - if (error.response?.status === 401) { - await AsyncStorage.removeItem(STORAGE_KEYS.USER_TOKEN); - delete apiClient.defaults.headers.common['Authorization']; - throw new Error('Session expired. Please login again.'); - } + console.error('Error deleting account:', error); throw error; } }; -// Initialize API - Updated to handle CSRF token -export const initializeAPI = async () => { - try { - console.log('Initializing API...'); - - // First fetch CSRF token - const csrfToken = await fetchCSRFToken(); - if (!csrfToken) { - console.error('Failed to fetch CSRF token during initialization'); - return false; - } - console.log('CSRF token fetched successfully'); - - // Then check auth token - const token = await AsyncStorage.getItem(STORAGE_KEYS.USER_TOKEN); - if (token) { - apiClient.defaults.headers.common['Authorization'] = `Token ${token}`; - console.log('Auth token loaded'); - - // Verify token is valid - try { - const response = await apiClient.get('/users/me/'); - console.log('User authenticated:', response.data); - return true; - } catch (error) { - console.error('Token validation error:', error.response?.data || error); - if (error.response?.status === 401) { - // Token invalid, clear it - await AsyncStorage.removeItem(STORAGE_KEYS.USER_TOKEN); - delete apiClient.defaults.headers.common['Authorization']; - console.log('Invalid token cleared'); - } - } - } else { - console.log('No auth token found'); - } - return false; - } catch (error) { - console.error('Failed to initialize API:', error.response?.data || error); - return false; - } -}; - -// For backwards compatibility -export const fetchPosts = async (searchTerm) => { - try { - // Simulate authentication check - await checkAuthentication(); - - // Simulate network delay - await delay(500); - - // Filter products based on search term - const results = await Promise.all( - mockProducts - .filter(product => - product.title.toLowerCase().includes(searchTerm.toLowerCase()) || - product.description.toLowerCase().includes(searchTerm.toLowerCase()) || - product.category.toLowerCase().includes(searchTerm.toLowerCase()) - ) - .map(async product => ({ - ...product, - image: await processImageUrl(product.image) - })) - ); - - // Save to recent searches if user is logged in - try { - await saveUserRecentSearch(searchTerm); - } catch (error) { - // Silently fail if user is not logged in - console.log('User not logged in, not saving search'); - } - - return results; - } catch (error) { - console.error('Error fetching products:', error); - throw error; - } -}; - -export const fetchPostById = async (id) => { - try { - // Simulate authentication check - await checkAuthentication(); - - await delay(200); - const product = mockProducts.find(p => p.id === parseInt(id)); - if (!product) { - throw new Error('Product not found'); - } - return { - ...product, - image: await processImageUrl(product.image) - }; - } catch (error) { - throw error; - } -}; - -export const getUserData = async () => { - try { - const token = await AsyncStorage.getItem(STORAGE_KEYS.USER_TOKEN); - if (!token) return null; - - const { data } = await apiClient.get('/users/me/'); - return data; - } catch (error) { - console.error('Error fetching user data:', error); - return null; - } -}; - -// RECENT SEARCHES +// Recent searches functions export const getUserRecentSearches = async () => { try { - const searches = await AsyncStorage.getItem('recentSearches'); - return searches ? JSON.parse(searches) : []; + // Always use local storage for recent searches since the API endpoint doesn't exist + const recentSearchesData = await AsyncStorage.getItem(STORAGE_KEYS.RECENT_SEARCHES); + return recentSearchesData ? JSON.parse(recentSearchesData) : []; } catch (error) { console.error('Error getting recent searches:', error); return []; @@ -873,9 +851,22 @@ export const getUserRecentSearches = async () => { export const saveUserRecentSearch = async (searchTerm) => { try { - const searches = await getUserRecentSearches(); - const updatedSearches = [searchTerm, ...searches.filter(term => term !== searchTerm)].slice(0, 10); - await AsyncStorage.setItem('recentSearches', JSON.stringify(updatedSearches)); + if (!searchTerm || typeof searchTerm !== 'string') { + console.warn('Invalid search term provided to saveUserRecentSearch'); + return []; + } + + // Always use local storage for recent searches + const recentSearchesData = await AsyncStorage.getItem(STORAGE_KEYS.RECENT_SEARCHES); + const recentSearches = recentSearchesData ? JSON.parse(recentSearchesData) : []; + + // Remove if already exists (to move it to the front) + const filteredSearches = recentSearches.filter(term => term !== searchTerm); + + // Add to beginning and limit to 5 recent searches + const updatedSearches = [searchTerm, ...filteredSearches].slice(0, 5); + + await AsyncStorage.setItem(STORAGE_KEYS.RECENT_SEARCHES, JSON.stringify(updatedSearches)); return updatedSearches; } catch (error) { console.error('Error saving recent search:', error); @@ -885,9 +876,17 @@ export const saveUserRecentSearch = async (searchTerm) => { export const removeUserRecentSearch = async (searchTerm) => { try { - const searches = await getUserRecentSearches(); - const updatedSearches = searches.filter(term => term !== searchTerm); - await AsyncStorage.setItem('recentSearches', JSON.stringify(updatedSearches)); + if (!searchTerm || typeof searchTerm !== 'string') { + console.warn('Invalid search term provided to removeUserRecentSearch'); + return []; + } + + // Always use local storage for recent searches + const recentSearchesData = await AsyncStorage.getItem(STORAGE_KEYS.RECENT_SEARCHES); + const recentSearches = recentSearchesData ? JSON.parse(recentSearchesData) : []; + + const updatedSearches = recentSearches.filter(term => term !== searchTerm); + await AsyncStorage.setItem(STORAGE_KEYS.RECENT_SEARCHES, JSON.stringify(updatedSearches)); return updatedSearches; } catch (error) { console.error('Error removing recent search:', error); @@ -897,7 +896,8 @@ export const removeUserRecentSearch = async (searchTerm) => { export const clearAllUserRecentSearches = async () => { try { - await AsyncStorage.removeItem('recentSearches'); + // Always use local storage for recent searches + await AsyncStorage.removeItem(STORAGE_KEYS.RECENT_SEARCHES); return []; } catch (error) { console.error('Error clearing recent searches:', error); @@ -905,50 +905,232 @@ export const clearAllUserRecentSearches = async () => { } }; -// FAVORITES -export const isProductFavorited = async (productId) => { +export const getPrivacyPolicy = async () => { + try { + const response = await api.get('/privacy-policies/'); + return response.data; // Return all policies + } catch (error) { + console.error('Error fetching privacy policy:', error); + throw error; + } +}; + +export const getFAQs = async (category = 'All') => { + try { + const { data } = await api.get('/faqs/', { + params: { + category: category + } + }); + return data; + } catch (error) { + console.error('Error fetching FAQs:', error); + throw error; + } +}; + +export const getFAQCategories = async () => { + try { + const { data } = await api.get('/faqs/categories/'); + return data.map(category => category.value); + } catch (error) { + console.error('Error fetching FAQ categories:', error); + // Return default categories if API fails + return ['All', 'Services', 'General', 'Account']; + } +}; + +export const getContacts = async () => { try { - const token = await AsyncStorage.getItem(STORAGE_KEYS.USER_TOKEN); + const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); if (!token) { - // If not logged in, check local storage - const localFavorites = await AsyncStorage.getItem(STORAGE_KEYS.FAVORITES); - const favorites = localFavorites ? JSON.parse(localFavorites) : []; - return { - success: true, - isFavorited: favorites.some(item => item.id === productId) - }; + // Return default contacts if not logged in + return [ + { + id: 1, + contact_type: 'customer_service', + title: 'Customer Service', + value: '(480)555-0103', + is_active: true + }, + { + id: 2, + contact_type: 'whatsapp', + title: 'WhatsApp', + value: '(480)555-0103', + is_active: true + }, + { + id: 3, + contact_type: 'website', + title: 'Website', + value: 'www.example.com', + is_active: true + }, + { + id: 4, + contact_type: 'facebook', + title: 'Facebook', + value: '@example', + is_active: true + }, + { + id: 5, + contact_type: 'instagram', + title: 'Instagram', + value: '@example', + is_active: true + }, + { + id: 6, + contact_type: 'twitter', + title: 'Twitter', + value: '@example', + is_active: true + } + ]; } - // If logged in, check API - const { data } = await apiClient.get(`/favorites/check/${productId}/`); - return { - success: true, - isFavorited: data.is_favorited - }; + // Set the auth token in headers + api.defaults.headers.common['Authorization'] = `Token ${token}`; + + const { data } = await api.get('/contacts/'); + return data; } catch (error) { - console.error('Error checking favorite status:', error); - return { - success: false, - isFavorited: false - }; + console.error('Error fetching contacts:', error); + // Return default contacts on error + return [ + { + id: 1, + contact_type: 'customer_service', + title: 'Customer Service', + value: '(480)555-0103', + is_active: true + }, + { + id: 2, + contact_type: 'whatsapp', + title: 'WhatsApp', + value: '(480)555-0103', + is_active: true + }, + { + id: 3, + contact_type: 'website', + title: 'Website', + value: 'www.example.com', + is_active: true + }, + { + id: 4, + contact_type: 'facebook', + title: 'Facebook', + value: '@example', + is_active: true + }, + { + id: 5, + contact_type: 'instagram', + title: 'Instagram', + value: '@example', + is_active: true + }, + { + id: 6, + contact_type: 'twitter', + title: 'Twitter', + value: '@example', + is_active: true + } + ]; } }; -// Authentication check function -export const checkAuthentication = async () => { +export const getInviteMessage = async () => { + return { + message: "Hey! I've been shopping on Vendora App and it's been amazing! They have awesome deals, fast delivery, and quality products. Use my link to sign up and get a special discount on your first order!", + link: "https://play.google.com/store/apps/details?id=com.vendora.app" + }; +}; + +export const fetchSliders = async () => { try { - const token = await AsyncStorage.getItem(STORAGE_KEYS.USER_TOKEN); - if (!token) { - throw { - response: { - status: 401, - data: { detail: 'Authentication credentials were not provided.' } - } - }; - } - return token; + const { data } = await api.get('/sliders/'); + return data; } catch (error) { - console.error('Authentication check failed:', error); + console.error('Error fetching sliders:', error); throw error; } -}; \ No newline at end of file +}; + +// Coupon functions +export const getCoupons = async () => { + try { + const { data } = await api.get('/coupons/'); + return data; + } catch (error) { + console.error('Error fetching coupons:', error); + throw error; + } +}; + +export const validateCoupon = async (code, cartTotal) => { + try { + const { data } = await api.post('/validate-coupon/', { + code, + cart_total: cartTotal + }); + return data; + } catch (error) { + console.error('Error validating coupon:', error); + if (error.response?.data) { + // Return the error response data with a valid property set to false + return { + valid: false, + message: error.response.data.message || 'Invalid coupon code', + ...error.response.data + }; + } + throw error; + } +}; + +export const getNotifications = async () => { + try { + const { data } = await api.get('/notifications/'); + return data; + } catch (error) { + console.error('Error fetching notifications:', error); + throw error; + } +}; + +export const markNotificationAsRead = async (notificationId) => { + try { + const { data } = await api.put(`/notifications/${notificationId}/read/`); + return data; + } catch (error) { + console.error('Error marking notification as read:', error); + throw error; + } +}; + +export const deleteNotification = async (notificationId) => { + try { + const { data } = await api.delete(`/notifications/${notificationId}/`); + return data; + } catch (error) { + console.error('Error deleting notification:', error); + throw error; + } +}; + +export const clearAllNotifications = async () => { + try { + const { data } = await api.delete('/notifications/clear_all/'); + return data; + } catch (error) { + console.error('Error clearing notifications:', error); + throw error; + } +}; diff --git a/frontend/app/api/index.jsx b/frontend/app/api/index.jsx index 8d775fe..5c18d3e 100644 --- a/frontend/app/api/index.jsx +++ b/frontend/app/api/index.jsx @@ -1,15 +1,5 @@ // mock-api.js import AsyncStorage from '@react-native-async-storage/async-storage'; -import { View, Text } from 'react-native'; - -// Dummy component to fix Expo Router error -export default function ApiIndex() { - return ( - - API Service - This component is only to prevent Expo Router errors - - ); -} // Mock product data const mockProducts = [ @@ -98,7 +88,6 @@ const mockUsers = [ } ]; -// Add delay to simulate network request const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // Helper to simulate authentication in mock API @@ -224,13 +213,27 @@ export const deleteCartItem = async (productId) => { await checkAuthentication(); const cartData = await AsyncStorage.getItem('cart'); - let cart = cartData ? JSON.parse(cartData) : { data: [], totalPrice: 0 }; + if (!cartData) { + return { data: [], totalPrice: 0 }; + } + + let cart = JSON.parse(cartData); + + // Make sure cart has the expected structure + if (!cart.data) { + cart = { data: [], totalPrice: 0 }; + } + // Filter out the item to be deleted cart.data = cart.data.filter(item => item.id !== productId); - cart.totalPrice = cart.data.reduce((sum, item) => sum + item.totalPrice, 0); + + // Recalculate total price + cart.totalPrice = cart.data.reduce((sum, item) => sum + (item.totalPrice || 0), 0); + // Save updated cart to AsyncStorage await AsyncStorage.setItem('cart', JSON.stringify(cart)); - await delay(200); + + await delay(200); // Simulate network delay return cart; } catch (error) { console.error('Error deleting item from cart:', error); @@ -524,4 +527,66 @@ export const fetchProducts = async (searchTerm) => { console.error('Error fetching products:', error); throw error; } -}; \ No newline at end of file +}; + +export const getInviteMessage = async () => { + return { + message: "Hey! I've been shopping on this Shopping App and it's been amazing! They have awesome deals, fast delivery, and quality products. Use my link to sign up and get a special discount on your first order!", + link: "https://play.google.com/store/apps/details?id=com.vendora.app" + }; +}; + +// Mock slider data +const mockSliders = [ + { + id: 1, + title: "new collection", + description: "Discount 50% for the first transaction.", + image: "http://example.com/slider1.jpg", + is_active: true + }, + { + id: 2, + title: "kids collection", + description: "Discount 50% for the first transaction.", + image: "http://example.com/slider2.jpg", + is_active: true + }, + { + id: 3, + title: "traditional collection", + description: "Discount 50% for the first transaction.", + image: "http://example.com/slider3.jpg", + is_active: true + } +]; + +const api = { + fetchPosts, + fetchPostsById, + fetchCarts, + createCarts, + getCart, + deleteCartItem, + loginUser, + logoutUser, + getCurrentUser, + checkIsFavoriteForCurrentUser, + toggleFavoriteForCurrentUser, + getUserFavorites, + getUserRecentSearches, + saveUserRecentSearch, + removeUserRecentSearch, + clearAllUserRecentSearches, + getFavorites, + toggleFavorite, + isProductFavorited, + getUserData, + fetchProducts, + getInviteMessage +}; + +export default api; + + + diff --git a/frontend/app/components/ActiveOrder.jsx b/frontend/app/components/ActiveOrder.jsx index 6c70613..fc363c9 100644 --- a/frontend/app/components/ActiveOrder.jsx +++ b/frontend/app/components/ActiveOrder.jsx @@ -1,10 +1,12 @@ -import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native' -import React from 'react' +import { View, Text, Image, TouchableOpacity, ActivityIndicator, Alert } from 'react-native' +import React, { useState } from 'react' import { useNavigation } from '@react-navigation/native' +import { fetchProductById } from '../api' export default function ActiveOrder({ orders = [], navigation: propNavigation }) { // Use provided navigation prop if available, otherwise get from hook const navigation = propNavigation || useNavigation(); + const [loading, setLoading] = useState(false); // Safe number formatting helper const formatCurrency = (value) => { @@ -17,6 +19,48 @@ export default function ActiveOrder({ orders = [], navigation: propNavigation }) return '0.00'; }; + const handleReOrder = async (order) => { + console.log('Order data:', order); + + // Use order_items as primary source, fallback to items if order_items is empty + const orderItems = (order.order_items && order.order_items.length > 0) ? order.order_items : order.items; + console.log('Order items:', orderItems); + + if (!orderItems || orderItems.length === 0) { + Alert.alert('Error', 'No items found in this order'); + return; + } + + try { + setLoading(true); + const firstItem = orderItems[0]; + console.log('First item:', firstItem); + + const productId = firstItem.product || firstItem.product_id || firstItem.id; + console.log('Product ID:', productId); + + if (!productId) { + Alert.alert('Error', 'Could not find product ID'); + return; + } + + const product = await fetchProductById(productId); + console.log('Fetched product:', product); + + if (!product) { + Alert.alert('Error', 'Product not found'); + return; + } + + navigation.navigate('ProductDetails', { id: product.id }); + } catch (error) { + console.error('Error in handleReOrder:', error); + Alert.alert('Error', 'Failed to load product details. Please try again.'); + } finally { + setLoading(false); + } + }; + if (!orders || orders.length === 0) { return ( @@ -28,7 +72,7 @@ export default function ActiveOrder({ orders = [], navigation: propNavigation }) return ( {orders.map((order, index) => { - // Each order can have multiple items + const orderItems = (order.order_items && order.order_items.length > 0) ? order.order_items : order.items; return ( @@ -39,8 +83,8 @@ export default function ActiveOrder({ orders = [], navigation: propNavigation }) - {order.items && order.items.map((item, itemIndex) => ( - + {orderItems && orderItems.map((item, itemIndex) => ( + Total: ${formatCurrency(order.total_price || order.total_amount)} - navigation.navigate('Track Order', { orderId: order.id })} - className={`bg-primary rounded-full px-4 py-2`} - > - Track Order - + + navigation.navigate('Track Order', { orderId: order.id })} + className={`bg-primary rounded-full px-4 py-2`} + > + Track Order + + + ); diff --git a/frontend/app/components/CancelledOrder.jsx b/frontend/app/components/CancelledOrder.jsx index 511b79e..6f67277 100644 --- a/frontend/app/components/CancelledOrder.jsx +++ b/frontend/app/components/CancelledOrder.jsx @@ -1,10 +1,12 @@ -import { View, Text, Image, TouchableOpacity } from 'react-native' -import React from 'react' +import { View, Text, Image, TouchableOpacity, ActivityIndicator, Alert } from 'react-native' +import React, { useState } from 'react' import { useNavigation } from '@react-navigation/native' +import { fetchProductById } from '../api' export default function CancelledOrder({ orders = [], navigation: propNavigation }) { // Use provided navigation prop if available, otherwise get from hook const navigation = propNavigation || useNavigation(); + const [loading, setLoading] = useState(false); // Safe number formatting helper const formatCurrency = (value) => { @@ -17,6 +19,48 @@ export default function CancelledOrder({ orders = [], navigation: propNavigation return '0.00'; }; + const handleReOrder = async (order) => { + console.log('Order data:', order); + + // Use order_items as primary source, fallback to items if order_items is empty + const orderItems = (order.order_items && order.order_items.length > 0) ? order.order_items : order.items; + console.log('Order items:', orderItems); + + if (!orderItems || orderItems.length === 0) { + Alert.alert('Error', 'No items found in this order'); + return; + } + + try { + setLoading(true); + const firstItem = orderItems[0]; + console.log('First item:', firstItem); + + const productId = firstItem.product || firstItem.product_id || firstItem.id; + console.log('Product ID:', productId); + + if (!productId) { + Alert.alert('Error', 'Could not find product ID'); + return; + } + + const product = await fetchProductById(productId); + console.log('Fetched product:', product); + + if (!product) { + Alert.alert('Error', 'Product not found'); + return; + } + + navigation.navigate('ProductDetails', { id: product.id }); + } catch (error) { + console.error('Error in handleReOrder:', error); + Alert.alert('Error', 'Failed to load product details. Please try again.'); + } finally { + setLoading(false); + } + }; + if (!orders || orders.length === 0) { return ( @@ -28,7 +72,7 @@ export default function CancelledOrder({ orders = [], navigation: propNavigation return ( {orders.map((order, index) => { - // Each order can have multiple items + const orderItems = (order.order_items && order.order_items.length > 0) ? order.order_items : order.items; return ( @@ -39,8 +83,8 @@ export default function CancelledOrder({ orders = [], navigation: propNavigation - {order.items && order.items.map((item, itemIndex) => ( - + {orderItems && orderItems.map((item, itemIndex) => ( + Total: ${formatCurrency(order.total_price || order.total_amount)} navigation.navigate('Home')} + onPress={() => handleReOrder(order)} className={`bg-primary rounded-full px-4 py-2`} + disabled={loading} > - Re-Order + {loading ? ( + + ) : ( + Re-Order + )} diff --git a/frontend/app/components/Category.jsx b/frontend/app/components/Category.jsx index 2c38d0d..3c43142 100644 --- a/frontend/app/components/Category.jsx +++ b/frontend/app/components/Category.jsx @@ -34,44 +34,45 @@ export default function Category({ categories = [] }) { image: "https://cdn-icons-png.flaticon.com/512/2934/2934315.png" } ] - + + const handleCategoryPress = (category) => { + // Ensure we have a valid category ID + if (!category.id || isNaN(parseInt(category.id))) { + console.error('Invalid category ID:', category.id); + return; + } + + // Navigate to the category screen with the category ID as a number + navigation.navigate('Category', { + title: category.name, + categoryId: parseInt(category.id) + }); + }; + return ( - - - Category - See All - + - { - displayCategories.map((item, index) => { - return ( - { - navigation.navigate('Category', { - categoryId: item.id, - title: item.name - }) - }} - className={`${index + 1 == displayCategories.length ? "mr-4" : " mr-4"} flex-col items-center gap-2`} - > - - - - {item.name} - - ) - }) - } + {displayCategories.map((category, index) => ( + handleCategoryPress(category)} + className="items-center" + > + + + + {category.name} + + ))} - ) + ); } \ No newline at end of file diff --git a/frontend/app/components/CompletedOrder.jsx b/frontend/app/components/CompletedOrder.jsx index 6cd88d4..654ca8b 100644 --- a/frontend/app/components/CompletedOrder.jsx +++ b/frontend/app/components/CompletedOrder.jsx @@ -1,10 +1,12 @@ -import { View, Text, Image, TouchableOpacity } from 'react-native' -import React from 'react' +import { View, Text, Image, TouchableOpacity, ActivityIndicator, Alert } from 'react-native' +import React, { useState } from 'react' import { useNavigation } from '@react-navigation/native' +import { fetchProductById } from '../api' export default function CompletedOrder({ orders = [], navigation: propNavigation }) { // Use provided navigation prop if available, otherwise get from hook const navigation = propNavigation || useNavigation(); + const [loading, setLoading] = useState(false); // Safe number formatting helper const formatCurrency = (value) => { @@ -17,6 +19,95 @@ export default function CompletedOrder({ orders = [], navigation: propNavigation return '0.00'; }; + const handleReOrder = async (order) => { + console.log('Order data:', order); + + // Use order_items as primary source, fallback to items if order_items is empty + const orderItems = (order.order_items && order.order_items.length > 0) ? order.order_items : order.items; + console.log('Order items:', orderItems); + + if (!orderItems || orderItems.length === 0) { + Alert.alert('Error', 'No items found in this order'); + return; + } + + try { + setLoading(true); + const firstItem = orderItems[0]; + console.log('First item:', firstItem); + + const productId = firstItem.product || firstItem.product_id || firstItem.id; + console.log('Product ID:', productId); + + if (!productId) { + Alert.alert('Error', 'Could not find product ID'); + return; + } + + const product = await fetchProductById(productId); + console.log('Fetched product:', product); + + if (!product) { + Alert.alert('Error', 'Product not found'); + return; + } + + navigation.navigate('ProductDetails', { id: product.id }); + } catch (error) { + console.error('Error in handleReOrder:', error); + Alert.alert('Error', 'Failed to load product details. Please try again.'); + } finally { + setLoading(false); + } + }; + + const handleLeaveReview = async (order) => { + console.log('Order data for review:', order); + + // Use order_items as primary source, fallback to items if order_items is empty + const orderItems = (order.order_items && order.order_items.length > 0) ? order.order_items : order.items; + console.log('Order items for review:', orderItems); + + if (!orderItems || orderItems.length === 0) { + Alert.alert('Error', 'No items found in this order'); + return; + } + + try { + setLoading(true); + const firstItem = orderItems[0]; + console.log('First item for review:', firstItem); + + const productId = firstItem.product || firstItem.product_id || firstItem.id; + console.log('Product ID for review:', productId); + + if (!productId) { + Alert.alert('Error', 'Could not find product ID'); + return; + } + + const product = await fetchProductById(productId); + console.log('Fetched product for review:', product); + + if (!product) { + Alert.alert('Error', 'Product not found'); + return; + } + + navigation.navigate('Leave Review', { + productId: product.id, + productName: product.name, + productImage: product.image, + orderId: order.id + }); + } catch (error) { + console.error('Error in handleLeaveReview:', error); + Alert.alert('Error', 'Failed to load product details. Please try again.'); + } finally { + setLoading(false); + } + }; + if (!orders || orders.length === 0) { return ( @@ -28,7 +119,7 @@ export default function CompletedOrder({ orders = [], navigation: propNavigation return ( {orders.map((order, index) => { - // Each order can have multiple items + const orderItems = (order.order_items && order.order_items.length > 0) ? order.order_items : order.items; return ( @@ -39,8 +130,8 @@ export default function CompletedOrder({ orders = [], navigation: propNavigation - {order.items && order.items.map((item, itemIndex) => ( - + {orderItems && orderItems.map((item, itemIndex) => ( + Total: ${formatCurrency(order.total_price || order.total_amount)} - navigation.navigate('Leave Review')} - className={`bg-primary rounded-full px-4 py-2`} - > - Leave Review - + + handleLeaveReview(order)} + className={`bg-primary rounded-full px-4 py-2`} + disabled={loading} + > + {loading ? ( + + ) : ( + Leave Review + )} + + ); diff --git a/frontend/app/components/ContactUsCenter.jsx b/frontend/app/components/ContactUsCenter.jsx index f3e1cb5..cb7d985 100644 --- a/frontend/app/components/ContactUsCenter.jsx +++ b/frontend/app/components/ContactUsCenter.jsx @@ -1,68 +1,172 @@ -import { View, Text } from 'react-native' -import React from 'react' +import { View, Text, Animated, ActivityIndicator, Linking } from 'react-native' +import React, { useState, useRef, useEffect } from 'react' +import { TouchableOpacity } from 'react-native' import { ChevronDownIcon, ChevronUpIcon, GlobeAltIcon } from 'react-native-heroicons/outline' -import Icon from 'react-native-vector-icons/FontAwesome6'; +import Icon from 'react-native-vector-icons/FontAwesome6' +import { getContacts } from '../api' export default function ContactUsCenter() { + const [contacts, setContacts] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [expandedItems, setExpandedItems] = useState({}) + const animations = useRef({}) + + useEffect(() => { + fetchContacts() + }, []) + + const fetchContacts = async () => { + try { + setLoading(true) + const data = await getContacts() + setContacts(data) + + // Initialize expanded state and animations + const initialExpandedState = {} + data.forEach(contact => { + initialExpandedState[contact.id] = contact.contact_type === 'whatsapp' + animations.current[contact.id] = new Animated.Value(contact.contact_type === 'whatsapp' ? 1 : 0) + }) + setExpandedItems(initialExpandedState) + } catch (err) { + setError('Failed to load contacts') + console.error('Error fetching contacts:', err) + } finally { + setLoading(false) + } + } + + const toggleItem = (itemId) => { + const isExpanded = !expandedItems[itemId] + setExpandedItems(prev => ({ + ...prev, + [itemId]: isExpanded + })) + + Animated.timing(animations.current[itemId], { + toValue: isExpanded ? 1 : 0, + duration: 300, + useNativeDriver: false, + }).start() + } + + const handleContactPress = async (contact) => { + try { + let url = ''; + switch (contact.contact_type) { + case 'whatsapp': + // Remove any non-numeric characters from the phone number + const phoneNumber = contact.value.replace(/\D/g, ''); + url = `https://wa.me/${phoneNumber}`; + break; + case 'website': + url = contact.value.startsWith('http') ? contact.value : `https://${contact.value}`; + break; + case 'facebook': + url = `https://facebook.com/${contact.value.replace('@', '')}`; + break; + case 'instagram': + url = `https://instagram.com/${contact.value.replace('@', '')}`; + break; + case 'twitter': + url = `https://twitter.com/${contact.value.replace('@', '')}`; + break; + case 'customer_service': + // For phone numbers, use tel: protocol + const telNumber = contact.value.replace(/\D/g, ''); + url = `tel:${telNumber}`; + break; + default: + return; + } + + const supported = await Linking.canOpenURL(url); + if (supported) { + await Linking.openURL(url); + } else { + console.error(`Don't know how to open URL: ${url}`); + } + } catch (error) { + console.error('Error opening URL:', error); + } + } + + const getIcon = (contactType) => { + switch (contactType) { + case 'customer_service': + return + case 'whatsapp': + return + case 'website': + return + case 'facebook': + return + case 'instagram': + return + case 'twitter': + return + default: + return null + } + } + + if (loading) { + return ( + + + + ) + } + + if (error) { + return ( + + {error} + + ) + } + return ( - - - - - Customer Service - - - - - - - - - - WhatsApp - - - + + {contacts.map((contact) => ( + + + toggleItem(contact.id)} + className="flex-row items-center justify-between p-4"> + + {getIcon(contact.contact_type)} + {contact.title} + + {expandedItems[contact.id] ? ( + + ) : ( + + )} + + + + + handleContactPress(contact)}> + + {contact.value} + + + + - (480)555-0103 - - - - - Website - - - - - - - - - Facebook - - - - - - - - - Instagram - - - - - - - - - Twitter - - - - - + ))} ) } \ No newline at end of file diff --git a/frontend/app/components/FaqCenter.jsx b/frontend/app/components/FaqCenter.jsx index 55d69e0..9a0c662 100644 --- a/frontend/app/components/FaqCenter.jsx +++ b/frontend/app/components/FaqCenter.jsx @@ -1,68 +1,134 @@ -import { View, Text, ScrollView } from 'react-native' -import React, { useState } from 'react' +import { View, Text, ScrollView, ActivityIndicator, Animated } from 'react-native' +import React, { useState, useEffect, useRef } from 'react' import { TouchableOpacity } from 'react-native'; import { ChevronDownIcon, ChevronUpIcon } from 'react-native-heroicons/outline'; +import { getFAQs, getFAQCategories } from '../api'; export default function FaqCenter() { - const faq = ["All", "Services", "General", "Account"] - const [faqActive, setfaqActive] = useState(0); - return ( - - - { - faq.map((item, index) => { - let isActive = index == faqActive; - return ( - setfaqActive(index)} - className={` - border border-gray-200 bg-gray-200 px-5 mr-4 mb-4 rounded-full py-3 - ${isActive ? "bg-primary" : ""} - `}> - - {item} - - - ) - }) - } - - - - Can i track my order's delevery status ? - - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Ab distinctio nihil assumenda nostrum laboriosam quo, - officia eligendi obcaecati accusamus aliquam debitis, voluptatibus excepturi consequatur expedita eius quisquam velit. Rerum, officia? - - - - Is there a return policy ? - - - - Can i save my favorite item for later ? - - - - Can i share products with my friends ? - - - - How do i contact customer support ? - - - - What payment methods are accepted ? - - - - How to add review ? - - - + const [categories, setCategories] = useState(['All', 'Services', 'General', 'Account']); + const [activeCategory, setActiveCategory] = useState('All'); + const [faqs, setFaqs] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [expandedFaqs, setExpandedFaqs] = useState({}); + const animations = useRef({}); + useEffect(() => { + fetchCategories(); + }, []); + + useEffect(() => { + fetchFAQs(); + }, [activeCategory]); + + const fetchCategories = async () => { + try { + const data = await getFAQCategories(); + setCategories(data); + } catch (err) { + console.error('Error fetching categories:', err); + } + }; + + const fetchFAQs = async () => { + try { + setLoading(true); + const data = await getFAQs(activeCategory); + setFaqs(data); + const initialExpandedState = {}; + data.forEach(faq => { + initialExpandedState[faq.id] = false; + animations.current[faq.id] = new Animated.Value(0); + }); + setExpandedFaqs(initialExpandedState); + } catch (err) { + setError('Failed to load FAQs'); + console.error('Error fetching FAQs:', err); + } finally { + setLoading(false); + } + }; + + const toggleFaq = (faqId) => { + const isExpanded = !expandedFaqs[faqId]; + setExpandedFaqs(prev => ({ + ...prev, + [faqId]: isExpanded + })); + + Animated.timing(animations.current[faqId], { + toValue: isExpanded ? 1 : 0, + duration: 300, + useNativeDriver: false, + }).start(); + }; + + return ( + + + {categories.map((category, index) => { + const isActive = category === activeCategory; + return ( + setActiveCategory(category)} + className={` + border border-gray-200 bg-gray-200 px-5 mr-4 rounded-full py-3 + ${isActive ? "bg-primary" : ""} + `}> + + {category} + + + ); + })} + + + {loading ? ( + + ) : error ? ( + {error} + ) : faqs.length > 0 ? ( + faqs.map((faq) => ( + + + toggleFaq(faq.id)} + className="flex-row justify-between items-center p-4"> + {faq.question} + {expandedFaqs[faq.id] ? ( + + ) : ( + + )} + + + + + + {faq.answer} + + + + + + )) + ) : ( + + + No FAQs available for this category + + + )} - ) + ); } \ No newline at end of file diff --git a/frontend/app/components/FavoriteButton.jsx b/frontend/app/components/FavoriteButton.jsx index 6adb625..696c09f 100644 --- a/frontend/app/components/FavoriteButton.jsx +++ b/frontend/app/components/FavoriteButton.jsx @@ -1,7 +1,8 @@ -import { TouchableOpacity, ActivityIndicator } from 'react-native' -import React, { useState, useEffect } from 'react' +import { TouchableOpacity } from 'react-native' +import React from 'react' +import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import { HeartIcon } from 'react-native-heroicons/outline' -import { HeartIcon as HeartSolid } from 'react-native-heroicons/solid' +import { HeartIcon as SolidHeartIcon } from 'react-native-heroicons/solid' import { getFavorites } from '../api' /** @@ -17,15 +18,7 @@ import { getFavorites } from '../api' * @param {number} props.productId The ID of the product * @returns {React.Component} Favorite button component */ -export default function FavoriteButton({ - isFavorite = false, - onToggleFavorite, - size = 24, - activeColor = '#ff3b30', - inactiveColor = '#000000', - style = {}, - productId -}) { +const FavoriteButton = ({ isFavorite, onToggleFavorite, size = 25, style, productId }) => { const [loading, setLoading] = useState(false); const [favoriteState, setFavoriteState] = useState(isFavorite); @@ -70,16 +63,25 @@ export default function FavoriteButton({ {loading ? ( - + ) : favoriteState ? ( - + ) : ( - + )} - ) -} \ No newline at end of file + ); +} + +export default FavoriteButton; \ No newline at end of file diff --git a/frontend/app/components/FlashSales.jsx b/frontend/app/components/FlashSales.jsx index 1458687..e316cc3 100644 --- a/frontend/app/components/FlashSales.jsx +++ b/frontend/app/components/FlashSales.jsx @@ -2,23 +2,27 @@ // demoData //camel case ex:iAmAGoodBBoy // demo_data // snake case ex:i_am_a_good_boy -import { View, Text, ScrollView, TouchableOpacity, Image } from 'react-native' -import React, { useEffect, useState } from 'react' +import { View, Text, ScrollView, TouchableOpacity, Image, ActivityIndicator } from 'react-native' +import React, { useEffect, useState, forwardRef } from 'react' import MasonryList from '@react-native-seoul/masonry-list'; -import flashSalesData from '../constants/flashSalesData'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import { StarIcon } from 'react-native-heroicons/solid'; import { HeartIcon } from 'react-native-heroicons/outline'; +import { HeartIcon as SolidHeartIcon } from 'react-native-heroicons/solid'; import Icon from "react-native-vector-icons/FontAwesome" import { useNavigation } from '@react-navigation/native'; -import { getFavorites, toggleFavorite } from '../api'; +import { getFavorites, toggleFavorite, fetchProducts } from '../api'; +import TabBar from '../themes/TabBar' +import { ArrowLeftIcon, ChevronLeftIcon } from 'react-native-heroicons/outline' -const FlashSales = ({data}) => { +const FlashSales = forwardRef((props, ref) => { const navigation = useNavigation() const saleEnd = "2024-12-20T23:45:30"; const [endDate, setEndDate] = useState(calenderLastDate(saleEnd)) const [favorites, setFavorites] = useState([]); const [loading, setLoading] = useState(true); + const [products, setProducts] = useState([]); + const [error, setError] = useState(null); useEffect(() => { const timer = setInterval(() => { @@ -31,21 +35,28 @@ const FlashSales = ({data}) => { return () => clearInterval(timer) }, [saleEnd]) - // Load favorites when component mounts + // Load products and favorites when component mounts useEffect(() => { - const loadFavorites = async () => { + const loadData = async () => { try { setLoading(true); - const favoriteProducts = await getFavorites(); + const [productsData, favoriteProducts] = await Promise.all([ + fetchProducts(), + getFavorites() + ]); + setProducts(productsData || []); setFavorites(favoriteProducts || []); + setError(null); } catch (error) { - console.error('Error loading favorites:', error); + console.error('Error loading data:', error); + setError('Failed to load products'); + setProducts([]); setFavorites([]); } finally { setLoading(false); } }; - loadFavorites(); + loadData(); }, []); //Category toggle @@ -56,8 +67,13 @@ const FlashSales = ({data}) => { setActiveSet(item) } - const handleFavouriteToggle = async (product) => { + const handleFavouriteToggle = async (productId) => { try { + const product = products.find(p => p.id === productId); + if (!product) { + console.error('Product not found:', productId); + return; + } const updatedFavorites = await toggleFavorite(product); setFavorites(updatedFavorites || []); } catch (error) { @@ -66,20 +82,60 @@ const FlashSales = ({data}) => { }; const isProductFavorited = (productId) => { - if (!Array.isArray(favorites)) return false; return favorites.some(fav => fav.id === productId); }; + const navigateToProductDetails = (productId) => { + navigation.navigate('ProductDetails', { id: productId }); + }; + if (loading) { return ( - - Loading... + + + + ); + } + + if (error) { + return ( + + {error} + { + setLoading(true); + fetchProducts() + .then(data => { + setProducts(data); + setError(null); + }) + .catch(err => setError(err.message || 'Failed to fetch products')) + .finally(() => setLoading(false)); + }} + className="mt-4 bg-primary py-2 px-4 rounded-lg" + > + Retry + ); } return ( - + + + FlashSales { @@ -122,25 +178,31 @@ const FlashSales = ({data}) => { }) } - item.id} - numColumns={2} - showsVerticalScrollIndicator={false} - renderItem={({ item, index }) => ( - handleFavouriteToggle(item)} - isFavourite={isProductFavorited(item.id)} - navigation={navigation} - /> - )} - className="mx-4" - /> - + {products.length === 0 ? ( + + No products available + + ) : ( + item.id.toString()} + numColumns={2} + showsVerticalScrollIndicator={false} + renderItem={({ item, index }) => ( + handleFavouriteToggle(item.id)} + isFavourite={isProductFavorited(item.id)} + onProductPress={() => navigateToProductDetails(item.id)} + /> + )} + className="mx-4" + /> + )} + ) -} +}) const calenderLastDate = (saleEnd) => { const nowDate = new Date(); @@ -161,7 +223,8 @@ const calenderLastDate = (saleEnd) => { } } -const CardItem = ({ item, index, handleFavouriteToggle, isFavourite, navigation }) => { +const CardItem = ({ item, index, handleFavouriteToggle, isFavourite, onProductPress }) => { + const navigation = useNavigation(); let isEven = item.id % 2 == 0 const getRating = () => { @@ -183,7 +246,7 @@ const CardItem = ({ item, index, handleFavouriteToggle, isFavourite, navigation return ( - { navigation.navigate('ProductDetails',{id:item.id,}) }} className="relative"> + { @@ -192,12 +255,11 @@ const CardItem = ({ item, index, handleFavouriteToggle, isFavourite, navigation }} className="absolute right-4 top-4 bg-white/50 p-2 rounded-full" > - { - isFavourite ? - - : - - } + {isFavourite ? ( + + ) : ( + + )} diff --git a/frontend/app/components/HomeCarousel.jsx b/frontend/app/components/HomeCarousel.jsx index bcbf88b..05bfe44 100644 --- a/frontend/app/components/HomeCarousel.jsx +++ b/frontend/app/components/HomeCarousel.jsx @@ -1,9 +1,43 @@ -import { View, Text, Image } from 'react-native' +import { View, Text, Image, TouchableOpacity } from 'react-native' import React, { useEffect, useState } from 'react' import PagerView from 'react-native-pager-view'; import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; +import { useNavigation } from '@react-navigation/native'; const HomeCarousel = ({ setPage, slider }) => { + const navigation = useNavigation(); + + const handleShopNow = (category) => { + // Handle both category object and direct category ID + let categoryId; + let categoryName; + + if (typeof category === 'object' && category !== null) { + // If category is an object + if (!category.id) { + console.error('Invalid category data:', category); + return; + } + categoryId = parseInt(category.id); + categoryName = category.name || 'Category'; + } else { + // If category is a direct ID + categoryId = parseInt(category); + categoryName = 'Category'; + } + + if (isNaN(categoryId)) { + console.error('Invalid category ID:', category); + return; + } + + // Navigate to the category screen with the category ID as a number + navigation.navigate('Category', { + title: categoryName, + categoryId: categoryId + }); + }; + return ( { return ( - - {item.title} - {item.description} - - - Shop Now + + {item.title} + {item.description} + handleShopNow(item.category || item.category_id)} + className={`justify-center items-start`} + > + + Shop Now - + diff --git a/frontend/app/components/ItemCard.jsx b/frontend/app/components/ItemCard.jsx index 6c469d3..8241655 100644 --- a/frontend/app/components/ItemCard.jsx +++ b/frontend/app/components/ItemCard.jsx @@ -1,30 +1,50 @@ import React from 'react'; import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native'; -import Icon from 'react-native-vector-icons/FontAwesome'; +import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; +import { HeartIcon } from 'react-native-heroicons/outline'; +import { HeartIcon as SolidHeartIcon, StarIcon } from 'react-native-heroicons/solid'; +import { useNavigation } from '@react-navigation/native'; -const ItemCard = ({ item, onPress, onFavoritePress, isFavourite }) => ( - - - +const ItemCard = ({ item, isFavorite, onToggleFavorite }) => { + const navigation = useNavigation(); + + return ( + navigation.navigate('ProductDetails', { id: item.id })} + className="relative" + style={{ width: wp('45%') }} + > + { + e.stopPropagation(); + onToggleFavorite(item); + }} + className="absolute right-4 top-4 bg-white/50 p-2 rounded-full" > - + {isFavorite ? ( + + ) : ( + + )} + + {item.name || item.title} + + + {item.rating || 0} + + + + Rs {item.price || 0} + - {item.title} - ${item.price} - - {item.rating} - - - -); + ); +} const styles = StyleSheet.create({ card: { diff --git a/frontend/app/components/LocationModal.jsx b/frontend/app/components/LocationModal.jsx new file mode 100644 index 0000000..f3d8fb3 --- /dev/null +++ b/frontend/app/components/LocationModal.jsx @@ -0,0 +1,280 @@ +import { View, Text, TouchableOpacity, Alert, Modal, TextInput, FlatList, ActivityIndicator } from 'react-native' +import React, { useState, useEffect, useRef } from 'react' +import { ChevronLeftIcon, MagnifyingGlassIcon, XMarkIcon } from 'react-native-heroicons/outline' +import Icon from 'react-native-vector-icons/Ionicons' +import * as Location from 'expo-location'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +export default function LocationModal({ visible, onClose, onLocationSelected }) { + const [isLoading, setIsLoading] = useState(false); + const [currentLocation, setCurrentLocation] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const searchTimeoutRef = useRef(null); + const isMounted = useRef(true); + + // Cleanup function to prevent memory leaks and state updates after unmount + useEffect(() => { + return () => { + isMounted.current = false; + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + }; + }, []); + + // Reset state when modal becomes visible + useEffect(() => { + if (visible) { + isMounted.current = true; + setSearchQuery(''); + setSearchResults([]); + setIsSearching(false); + } + }, [visible]); + + useEffect(() => { + if (searchQuery.length > 2) { + // Clear any existing timeout + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + + // Debounce the search to prevent too many API calls + searchTimeoutRef.current = setTimeout(() => { + if (isMounted.current) { + searchLocations(); + } + }, 300); + } else { + setSearchResults([]); + } + + return () => { + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + }; + }, [searchQuery]); + + const searchLocations = async () => { + if (!isMounted.current) return; + + setIsSearching(true); + try { + const response = await fetch( + `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent( + searchQuery + )}&limit=10`, + { + headers: { + 'Accept': 'application/json', + 'User-Agent': 'ShopingApp/1.0' // Replace with your app name + } + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + if (!isMounted.current) return; + + if (data && Array.isArray(data)) { + setSearchResults(data); + } else { + setSearchResults([]); + } + } catch (error) { + if (isMounted.current) { + console.error('Search error:', error); + if (error.message.includes('429')) { + Alert.alert('Error', 'Too many requests. Please wait a moment and try again.'); + } else { + Alert.alert('Error', 'Failed to search locations. Please try again.'); + } + } + } finally { + if (isMounted.current) { + setIsSearching(false); + } + } + }; + + const handleUseCurrentLocation = async () => { + if (!isMounted.current) return; + + setIsLoading(true); + try { + const { status } = await Location.requestForegroundPermissionsAsync(); + if (status !== 'granted') { + if (isMounted.current) { + Alert.alert('Permission Denied', 'Please enable location services to use this feature.'); + } + return; + } + + const location = await Location.getCurrentPositionAsync({}); + const { latitude, longitude } = location.coords; + + // Get address from coordinates using reverse geocoding + const [address] = await Location.reverseGeocodeAsync({ + latitude, + longitude + }); + + if (!isMounted.current) return; + + if (address) { + const locationData = { + name: 'Current Location', + address: `${address.street || ''} ${address.name || ''}, ${address.city || ''}, ${address.region || ''} ${address.postalCode || ''}`.trim(), + coordinates: { latitude, longitude } + }; + + await saveLocation(locationData); + onLocationSelected(locationData); + handleClose(); + } else { + Alert.alert('Error', 'Could not determine your address. Please try again.'); + } + } catch (error) { + if (isMounted.current) { + Alert.alert('Error', 'Failed to get your location. Please try again.'); + } + } finally { + if (isMounted.current) { + setIsLoading(false); + } + } + }; + + const saveLocation = async (location) => { + try { + await AsyncStorage.setItem('userLocation', JSON.stringify(location)); + if (isMounted.current) { + setCurrentLocation(location); + } + } catch (error) { + if (isMounted.current) { + Alert.alert('Error', 'Failed to save location. Please try again.'); + } + } + }; + + const handleSelectLocation = (place) => { + const locationData = { + name: place.display_name.split(',')[0], + address: place.display_name, + coordinates: { + latitude: parseFloat(place.lat), + longitude: parseFloat(place.lon) + } + }; + + saveLocation(locationData); + onLocationSelected(locationData); + handleClose(); + }; + + const handleClose = () => { + // Reset state before closing to improve performance + setSearchQuery(''); + setSearchResults([]); + setIsSearching(false); + onClose(); + }; + + const renderSearchResult = ({ item }) => ( + handleSelectLocation(item)} + > + {item.display_name.split(',')[0]} + {item.display_name} + + ); + + return ( + + + + + + + + Select Location + + + + + {/* Search Bar */} + + + + {searchQuery.length > 0 && ( + setSearchQuery('')}> + + + )} + + + {/* Current Location Button */} + + + + {isLoading ? "Getting Location..." : "Use My Current Location"} + + + + {/* Search Results */} + {isSearching ? ( + + + Searching locations... + + ) : searchResults.length > 0 ? ( + item.place_id} + className="flex-1" + initialNumToRender={10} + maxToRenderPerBatch={10} + windowSize={5} + /> + ) : searchQuery.length > 2 ? ( + + No locations found + + ) : null} + + {currentLocation && !searchQuery && ( + + Current Location: + {currentLocation.address} + + )} + + + + + ); +} \ No newline at end of file diff --git a/frontend/app/components/ProductQuantityInput.jsx b/frontend/app/components/ProductQuantityInput.jsx index 85f96c9..0b44691 100644 --- a/frontend/app/components/ProductQuantityInput.jsx +++ b/frontend/app/components/ProductQuantityInput.jsx @@ -1,5 +1,6 @@ import { View, Text, TouchableOpacity } from 'react-native' -import React from 'react' +import React, { useState } from 'react' +import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import { MinusIcon, PlusIcon } from 'react-native-heroicons/outline' /** @@ -13,34 +14,34 @@ import { MinusIcon, PlusIcon } from 'react-native-heroicons/outline' * @param {Object} props.containerStyle Additional styles for the container * @returns {React.Component} Quantity selector component */ -export default function ProductQuantityInput({ - quantity = 1, - onIncrease, - onDecrease, - label = "Quantity", - containerStyle = {} -}) { - return ( - - {label && {label}} - - - - - - {quantity} - - - - - - - ) -} \ No newline at end of file +const ProductQuantityInput = ({ initialQuantity = 1, onQuantityChange }) => { + const [quantity, setQuantity] = useState(initialQuantity); + + const handleDecrease = () => { + if (quantity > 1) { + const newQuantity = quantity - 1; + setQuantity(newQuantity); + onQuantityChange(newQuantity); + } + }; + + const handleIncrease = () => { + const newQuantity = quantity + 1; + setQuantity(newQuantity); + onQuantityChange(newQuantity); + }; + + return ( + + + + + {quantity} + + + + + ); +} + +export default ProductQuantityInput; \ No newline at end of file diff --git a/frontend/app/config.js b/frontend/app/config.js new file mode 100644 index 0000000..337216e --- /dev/null +++ b/frontend/app/config.js @@ -0,0 +1,31 @@ +// Environment configuration +const config = { + development: { + apiBaseUrl: 'http://192.168.18.4:8000', + // Add other development-specific settings here + }, + production: { + apiBaseUrl: 'https://your-production-api.com', + // Add other production-specific settings here + }, + // Add more environments as needed +}; + +// Get current environment +const getEnv = () => { + if (__DEV__) return 'development'; + return 'production'; +}; + +// Get current configuration +export const getConfig = () => { + return config[getEnv()]; +}; + +// Get API base URL +export const getApiBaseUrl = () => { + return getConfig().apiBaseUrl; +}; + +// Export all config +export default config; \ No newline at end of file diff --git a/frontend/app/constants/couponData.jsx b/frontend/app/constants/couponData.jsx deleted file mode 100644 index bbb1f2d..0000000 --- a/frontend/app/constants/couponData.jsx +++ /dev/null @@ -1,46 +0,0 @@ -const couponData=[ - { - id : 1, - couponCode: "WELLCOME200", - price: 2, - discount:"get 50% off" - - }, - { - id : 2, - couponCode: "CASHBAC12", - price: 2, - discount:"up to $12.00 cashback" - - }, - { - id : 3, - couponCode: "FEST2COST", - price: 28, - discount:"get 50% off for combo" - - }, - { - id : 4, - couponCode: "WELLCOME200", - price: 2, - discount:"get 50% off" - - }, - { - id : 5, - couponCode: "WELLCOME200", - price: 2, - discount:"get 50% off" - - }, - { - id : 6, - couponCode: "WELLCOME200", - price: 2, - discount:"get 50% off" - - }, - -] -export default couponData \ No newline at end of file diff --git a/frontend/app/constants/flashSalesData.jsx b/frontend/app/constants/flashSalesData.jsx deleted file mode 100644 index 094446d..0000000 --- a/frontend/app/constants/flashSalesData.jsx +++ /dev/null @@ -1,32 +0,0 @@ -const flashSalesData = [ - { - id: 1, - name: "Brown Jacket", - price: 300, - rating: 4.0, - image: "https://devotedstore.com/cdn/shop/files/1000074562.jpg?v=1725134987" - }, - { - id: 2, - name: "Brown Suite", - price: 400, - rating: 4.5, - image: "https://www.belmeademenswear.com/wp-content/uploads/2023/02/N292C-Ike-archer-brown_web.jpeg" - }, - { - id: 3, - name: "Brown Jacket", - price: 300, - rating: 4.5, - image: "https://devotedstore.com/cdn/shop/files/1000074562.jpg?v=1725134987" - }, - { - id: 4, - name: "Brown Suite", - price: 400, - rating: 4.5, - image: "https://www.belmeademenswear.com/wp-content/uploads/2023/02/N292C-Ike-archer-brown_web.jpeg" - }, -] - -export default flashSalesData \ No newline at end of file diff --git a/frontend/app/constants/myCartData.jsx b/frontend/app/constants/myCartData.jsx deleted file mode 100644 index 51eb39c..0000000 --- a/frontend/app/constants/myCartData.jsx +++ /dev/null @@ -1,68 +0,0 @@ -const myCartData = [ - { - id: 1, - name: "Brown Jacket", - price: 300, - rating: 4.5, - size: "S", - image: "https://devotedstore.com/cdn/shop/files/1000074562.jpg?v=1725134987" - }, - { - id: 2, - name: "Brown Suite", - price: 400, - rating: 4.5, - size: "S", - image: "https://www.belmeademenswear.com/wp-content/uploads/2023/02/N292C-Ike-archer-brown_web.jpeg" - }, - { - id: 3, - name: "Brown Jacket", - price: 300, - rating: 4.5, - size: "S", - image: "https://devotedstore.com/cdn/shop/files/1000074562.jpg?v=1725134987" - }, - { - id: 4, - name: "Brown Suite", - price: 400, - rating: 4.5, - size: "S", - image: "https://www.belmeademenswear.com/wp-content/uploads/2023/02/N292C-Ike-archer-brown_web.jpeg" - }, - { - id: 4, - name: "Brown Suite", - price: 400, - rating: 4.5, - size: "S", - image: "https://www.belmeademenswear.com/wp-content/uploads/2023/02/N292C-Ike-archer-brown_web.jpeg" - }, - { - id: 4, - name: "Brown Suite", - price: 400, - rating: 4.5, - size: "S", - image: "https://www.belmeademenswear.com/wp-content/uploads/2023/02/N292C-Ike-archer-brown_web.jpeg" - }, - { - id: 4, - name: "Brown Suite", - price: 400, - rating: 4.5, - size: "S", - image: "https://www.belmeademenswear.com/wp-content/uploads/2023/02/N292C-Ike-archer-brown_web.jpeg" - }, - { - id: 4, - name: "Brown Suite", - price: 400, - rating: 4.5, - size: "S", - image: "https://www.belmeademenswear.com/wp-content/uploads/2023/02/N292C-Ike-archer-brown_web.jpeg" - }, -] - -export default myCartData \ No newline at end of file diff --git a/frontend/app/constants/paymentData.jsx b/frontend/app/constants/paymentData.jsx deleted file mode 100644 index 8953050..0000000 --- a/frontend/app/constants/paymentData.jsx +++ /dev/null @@ -1,18 +0,0 @@ -const paymentData=[ - { - id: 1, - image: "https://cdn-icons-png.flaticon.com/512/174/174861.png", - name: "Paypal" - }, - { - id: 2, - image: "https://cdn0.iconfinder.com/data/icons/social-glyph/30/apple-480.png", - name: "Apple Pay" - }, - { - id: 3, - image: "https://user-images.githubusercontent.com/194400/70987158-4069c900-20b7-11ea-892e-8a2e1166b6b7.png", - name: "Google Pay" - } -] -export default paymentData \ No newline at end of file diff --git a/frontend/app/constants/recentData.jsx b/frontend/app/constants/recentData.jsx deleted file mode 100644 index e1ff51f..0000000 --- a/frontend/app/constants/recentData.jsx +++ /dev/null @@ -1,79 +0,0 @@ -const recentData = [ - { - id: 1, - name: "Blue jacket", - }, - { - id: 2, - name: "CosmicChic jacket", - }, - { - id: 3, - name: "Black jacket", - }, - { - id: 4, - name: "Shirt", - }, - { - id: 5, - name: "T-shirt", - }, - { - id: 6, - name: "underwear", - }, - { - id: 8, - name: "Kattu", - }, - { - id: 9, - name: "Dress", - }, - { - id: 10, - name: "Dress", - }, - { - id: 11, - name: "Dress", - }, - { - id: 12, - name: "Dress", - }, - { - id: 13, - name: "Dress", - }, - { - id: 14, - name: "Dress", - }, - { - id: 14, - name: "Dress", - }, - { - id: 14, - name: "Dress", - }, - { - id: 14, - name: "Dress", - }, - { - id: 14, - name: "Dress", - }, - { - id: 14, - name: "Dress", - }, - { - id: 14, - name: "Dress", - }, -] -export default recentData \ No newline at end of file diff --git a/frontend/app/constants/storageKeys.js b/frontend/app/constants/storageKeys.js new file mode 100644 index 0000000..2d5d77c --- /dev/null +++ b/frontend/app/constants/storageKeys.js @@ -0,0 +1,12 @@ +export const STORAGE_KEYS = { + AUTH_TOKEN: '@auth_token', + USER_DATA: '@user_data', + LOCATION: '@location', + CART: '@cart', + FAVORITES: '@favorites', + SETTINGS: '@settings', + CSRF_TOKEN: 'csrfToken', + RECENT_SEARCHES: 'recentSearches' +}; + +export default STORAGE_KEYS; \ No newline at end of file diff --git a/frontend/app/constants/usersData.jsx b/frontend/app/constants/usersData.jsx deleted file mode 100644 index d7e89eb..0000000 --- a/frontend/app/constants/usersData.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import Pic from "../../assets/images/pic.jpg" - -const usersData = [ - - { - id: 1, - image: Pic, - name: "A_1 Tharu", - num: 9762457265, - }, - { - id: 2, - image: Pic, - name: "A_1 Tharu", - num: 9762457265, - }, - { - id: 3, - image: Pic, - name: "A_1 Tharu", - num: 9762457265, - }, - { - id: 4, - image: Pic, - name: "A_1 Tharu", - num: 9762457265, - }, - { - id: 5, - image: Pic, - name: "A_1 Tharu", - num: 9762457265, - }, - { - id: 6, - image: Pic, - name: "A_1 Tharu", - num: 9762457265, - }, - { - id: 7, - image: Pic, - name: "A_1 Tharu", - num: 9762457265, - }, - { - id: 8, - image: Pic, - name: "A_1 Tharu", - num: "9762457265", - }, - { - id: 9, - image: Pic, - name: "A_1 Tharu", - num: "9762457265", - } -] -export default usersData \ No newline at end of file diff --git a/frontend/app/constants/wishlistData.jsx b/frontend/app/constants/wishlistData.jsx deleted file mode 100644 index 535d314..0000000 --- a/frontend/app/constants/wishlistData.jsx +++ /dev/null @@ -1,5 +0,0 @@ -const WishlistData = [ - {} -] - -export default WishlistData \ No newline at end of file diff --git a/frontend/app/context/AuthContext.jsx b/frontend/app/context/AuthContext.jsx new file mode 100644 index 0000000..531ccca --- /dev/null +++ b/frontend/app/context/AuthContext.jsx @@ -0,0 +1,116 @@ +import React, { createContext, useState, useContext, useEffect } from 'react'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { STORAGE_KEYS } from '../constants/storageKeys'; +import api from '../api'; + +const AuthContext = createContext(); + +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + checkUser(); + }, []); + + const checkUser = async () => { + try { + const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); + const userData = await AsyncStorage.getItem(STORAGE_KEYS.USER_DATA); + + if (token && userData) { + setUser(JSON.parse(userData)); + } + } catch (err) { + console.error('Error checking user:', err); + setError(err.message); + } finally { + setLoading(false); + } + }; + + const login = async (email, password) => { + try { + setLoading(true); + setError(null); + + const response = await api.login(email, password); + + if (response.token) { + await AsyncStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, response.token); + await AsyncStorage.setItem(STORAGE_KEYS.USER_DATA, JSON.stringify(response.user)); + setUser(response.user); + return true; + } + return false; + } catch (err) { + console.error('Login error:', err); + setError(err.message); + return false; + } finally { + setLoading(false); + } + }; + + const logout = async () => { + try { + setLoading(true); + await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN); + await AsyncStorage.removeItem(STORAGE_KEYS.USER_DATA); + setUser(null); + } catch (err) { + console.error('Logout error:', err); + setError(err.message); + } finally { + setLoading(false); + } + }; + + const register = async (userData) => { + try { + setLoading(true); + setError(null); + + const response = await api.register(userData); + + if (response.token) { + await AsyncStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, response.token); + await AsyncStorage.setItem(STORAGE_KEYS.USER_DATA, JSON.stringify(response.user)); + setUser(response.user); + return true; + } + return false; + } catch (err) { + console.error('Registration error:', err); + setError(err.message); + return false; + } finally { + setLoading(false); + } + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; + +export default AuthContext; \ No newline at end of file diff --git a/frontend/app/context/CartContext.jsx b/frontend/app/context/CartContext.jsx index 0399b81..782fca2 100644 --- a/frontend/app/context/CartContext.jsx +++ b/frontend/app/context/CartContext.jsx @@ -1,23 +1,79 @@ import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; import { useNavigation } from '@react-navigation/native'; -import { getCart } from '../api'; +import { getCart, validateCoupon } from '../api'; import { setGlobalRefreshCart } from '../api'; const CartContext = createContext(); export const CartProvider = ({ children }) => { const [cartCount, setCartCount] = useState(0); + const [cartTotal, setCartTotal] = useState(0); + const [appliedCoupon, setAppliedCoupon] = useState(null); + const [couponDiscount, setCouponDiscount] = useState(0); + const [discount, setDiscount] = useState(0); const navigation = useNavigation(); const updateCartCount = useCallback(async () => { try { const cart = await getCart(); setCartCount(cart?.items?.length || 0); + + // Calculate cart total + const total = cart?.items?.reduce((sum, item) => { + const price = parseFloat(item.product_price || item.price || 0); + const quantity = parseInt(item.quantity || 1); + return sum + (price * quantity); + }, 0) || 0; + + setCartTotal(total); + + // Calculate discount (6% of total) + const calculatedDiscount = (total * 6) / 100; + setDiscount(calculatedDiscount); + + // If there's an applied coupon, recalculate the discount + if (appliedCoupon) { + applyCoupon(appliedCoupon.code); + } } catch (error) { console.error('Error updating cart count:', error); - // Don't set cart count to 0 on error, keep previous value } - }, []); + }, [appliedCoupon]); + + const applyCoupon = async (code) => { + try { + if (!code) return; + + const result = await validateCoupon(code, cartTotal); + + if (result.valid) { + setAppliedCoupon(result.coupon); + setCouponDiscount(result.discount); + return { success: true, discount: result.discount }; + } else { + setAppliedCoupon(null); + setCouponDiscount(0); + return { success: false, message: result.message }; + } + } catch (error) { + console.error('Error applying coupon:', error); + setAppliedCoupon(null); + setCouponDiscount(0); + + // Extract the error message from the response if available + const errorMessage = error.response?.data?.message || 'Failed to apply coupon'; + return { success: false, message: errorMessage }; + } + }; + + const removeCoupon = useCallback(() => { + // Clear all coupon-related state + setAppliedCoupon(null); + setCouponDiscount(0); + + // Force a cart refresh to update totals + updateCartCount(); + }, [updateCartCount]); useEffect(() => { // Set the global refresh function @@ -38,7 +94,16 @@ export const CartProvider = ({ children }) => { }, [navigation, updateCartCount]); return ( - + {children} ); @@ -50,4 +115,6 @@ export const useCart = () => { throw new Error('useCart must be used within a CartProvider'); } return context; -}; \ No newline at end of file +}; + +export default CartContext; \ No newline at end of file diff --git a/frontend/app/context/NotificationContext.jsx b/frontend/app/context/NotificationContext.jsx new file mode 100644 index 0000000..0a6cc46 --- /dev/null +++ b/frontend/app/context/NotificationContext.jsx @@ -0,0 +1,106 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { Alert } from 'react-native'; +import { + getNotifications, + markNotificationAsRead as markNotificationAsReadApi, + deleteNotification as deleteNotificationApi, + clearAllNotifications as clearAllNotificationsApi, +} from '../api'; +import axios from 'axios'; +import { API_URL } from '../config'; + +const NotificationContext = createContext(); + +export const NotificationProvider = ({ children }) => { + const [notifications, setNotifications] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchNotifications = async () => { + try { + setLoading(true); + setError(null); + const data = await getNotifications(); + setNotifications(Array.isArray(data) ? data : []); + } catch (err) { + setError('Failed to fetch notifications'); + console.error('Error fetching notifications:', err); + setNotifications([]); + } finally { + setLoading(false); + } + }; + + const markAsRead = async (notificationId) => { + try { + await markNotificationAsReadApi(notificationId); + setNotifications((prev) => + prev.map((notification) => + notification.id === notificationId + ? { ...notification, read: true } + : notification + ) + ); + } catch (err) { + console.error('Error marking notification as read:', err); + Alert.alert('Error', 'Failed to mark notification as read'); + } + }; + + const clearNotification = async (notificationId) => { + try { + await deleteNotificationApi(notificationId); + setNotifications((prev) => + prev.filter((notification) => notification.id !== notificationId) + ); + } catch (err) { + console.error('Error deleting notification:', err); + Alert.alert('Error', 'Failed to delete notification'); + } + }; + + const clearAllNotifications = async () => { + try { + await clearAllNotificationsApi(); + setNotifications([]); + } catch (err) { + console.error('Error clearing notifications:', err); + Alert.alert('Error', 'Failed to clear notifications'); + } + }; + + const getUnreadCount = () => { + return notifications.filter((notification) => !notification.read).length; + }; + + useEffect(() => { + fetchNotifications(); + }, []); + + return ( + + {children} + + ); +}; + +export const useNotifications = () => { + const context = useContext(NotificationContext); + if (!context) { + throw new Error('useNotifications must be used within a NotificationProvider'); + } + return context; +}; + +export default NotificationContext; \ No newline at end of file diff --git a/frontend/app/index.jsx b/frontend/app/index.jsx index a437604..01b8892 100644 --- a/frontend/app/index.jsx +++ b/frontend/app/index.jsx @@ -24,6 +24,7 @@ import ShippingAddressScreen from './screens/ShippingAddressScreen'; import ChooseShippingScreen from './screens/ChooseShippingScreen'; import ConfirmPayment from './screens/ConfirmPayment'; import AddCardScreen from './screens/AddCardScreen'; +import AddBankScreen from './screens/AddBankScreen'; import LocationScreen from './screens/LocationScreen'; import AccessLocation from './screens/AccessLocation'; import SignInScreen from './screens/SignInScreen'; @@ -123,6 +124,7 @@ export default function Index() { + diff --git a/frontend/app/screens/AddBankScreen.jsx b/frontend/app/screens/AddBankScreen.jsx new file mode 100644 index 0000000..f6ad678 --- /dev/null +++ b/frontend/app/screens/AddBankScreen.jsx @@ -0,0 +1,142 @@ +import { View, Text, TextInput, TouchableOpacity, Alert } from 'react-native' +import React, { useState } from 'react' +import TabBar from '../themes/TabBar' +import { ChevronLeftIcon } from 'react-native-heroicons/outline' +import { useNavigation, useRoute } from '@react-navigation/native' + +export default function AddBankScreen() { + const navigation = useNavigation(); + const route = useRoute(); + const { method } = route.params; + + const [bankDetails, setBankDetails] = useState({ + bankName: '', + accountNumber: '', + accountHolderName: '', + phoneNumber: '', + email: '' + }); + const [isLoading, setIsLoading] = useState(false); + + const handleAddBank = () => { + if (!bankDetails.bankName || !bankDetails.accountNumber || !bankDetails.accountHolderName) { + Alert.alert('Error', 'Please fill in all required fields'); + return; + } + + Alert.alert( + 'Confirm Bank Details', + `You will transfer money to:\n\nBank: ${bankDetails.bankName}\nAccount Number: ${bankDetails.accountNumber}\nAccount Holder: ${bankDetails.accountHolderName}\n\nPlease make sure the details are correct.`, + [ + { + text: 'Cancel', + style: 'cancel' + }, + { + text: 'Proceed', + onPress: () => { + // Here you would typically save the bank details + Alert.alert( + 'Success', + 'Bank details added successfully. Please complete the transfer and upload the receipt.', + [{ text: 'OK' }] + ); + navigation.goBack(); + } + } + ] + ); + }; + + return ( + + } + titleStyle={{ + color: "black", + fontWeight: "bold", + fontSize: 20 + }} + prefixStyle={{ + backgroundColor: "transparent" + }} + suffixStyle={{ + backgroundColor: "white", + borderWidth: 1, + borderColor: "black" + }} + containerStyle="" + suffixAction={() => navigation.goBack()} + /> + + + Enter Bank Details + + + + Bank Name * + setBankDetails({...bankDetails, bankName: text})} + /> + + + + Account Number * + setBankDetails({...bankDetails, accountNumber: text})} + keyboardType="numeric" + /> + + + + Account Holder Name * + setBankDetails({...bankDetails, accountHolderName: text})} + /> + + + + Phone Number + setBankDetails({...bankDetails, phoneNumber: text})} + keyboardType="phone-pad" + /> + + + + Email + setBankDetails({...bankDetails, email: text})} + keyboardType="email-address" + /> + + + + Add Bank Details + + + + + ) +} \ No newline at end of file diff --git a/frontend/app/screens/AddCardScreen.jsx b/frontend/app/screens/AddCardScreen.jsx index 958103b..14417a0 100644 --- a/frontend/app/screens/AddCardScreen.jsx +++ b/frontend/app/screens/AddCardScreen.jsx @@ -1,17 +1,83 @@ -import { View, Text, TextInput, TouchableOpacity } from 'react-native' +import { View, Text, TextInput, TouchableOpacity, Alert } from 'react-native' import React, { useState } from 'react' import TabBar from '../themes/TabBar' -import { CheckIcon, ChevronLeftIcon } from 'react-native-heroicons/outline' -import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; -import Checkbox from 'expo-checkbox'; -import { useNavigation } from '@react-navigation/native'; +import { ChevronLeftIcon } from 'react-native-heroicons/outline' +import { useNavigation, useRoute } from '@react-navigation/native' +import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen' export default function AddCardScreen() { - const navigation = useNavigation() + const navigation = useNavigation(); + const route = useRoute(); + const { method } = route.params; + + const [cardNumber, setCardNumber] = useState(''); + const [expiryDate, setExpiryDate] = useState(''); + const [cvv, setCvv] = useState(''); + const [cardholderName, setCardholderName] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const formatCardNumber = (text) => { + // Remove all non-digit characters + const cleaned = text.replace(/\D/g, ''); + // Add space after every 4 digits + const formatted = cleaned.replace(/(\d{4})/g, '$1 ').trim(); + setCardNumber(formatted); + }; + + const formatExpiryDate = (text) => { + // Remove all non-digit characters + const cleaned = text.replace(/\D/g, ''); + // Add slash after 2 digits + if (cleaned.length >= 2) { + const formatted = cleaned.slice(0, 2) + '/' + cleaned.slice(2, 4); + setExpiryDate(formatted); + } else { + setExpiryDate(cleaned); + } + }; + + const handleAddCard = async () => { + try { + setIsLoading(true); + + // Basic validation + if (!cardNumber || !expiryDate || !cvv || !cardholderName) { + Alert.alert('Error', 'Please fill in all card details'); + return; + } + + if (cardNumber.replace(/\s/g, '').length !== 16) { + Alert.alert('Error', 'Please enter a valid 16-digit card number'); + return; + } + + if (cvv.length < 3) { + Alert.alert('Error', 'Please enter a valid CVV'); + return; + } + + // Here you would typically make an API call to save the card + // For now, we'll just show a success message + Alert.alert( + 'Success', + 'Card added successfully!', + [ + { + text: 'OK', + onPress: () => navigation.goBack() + } + ] + ); + } catch (error) { + console.error('Error adding card:', error); + Alert.alert('Error', 'Failed to add card. Please try again.'); + } finally { + setIsLoading(false); + } + }; - const [isChecked, setCheck] = useState(false) return ( - + navigation.goBack()} /> - - - - Card Holder Name - - - - - - Card Number - - - - - - - + + + Card Details + Enter your card information securely - - - Expiry Date - - - - CVV - - - - - + + + + Cardholder Name + - - - Save Card + + Card Number + + + + Expiry Date + + + + CVV + + + + + + + + {isLoading ? 'Adding Card...' : 'Add Card'} + + + + + + Your card details are encrypted and secure. We never store your full card number. + - - - - Add Card - - - ) } \ No newline at end of file diff --git a/frontend/app/screens/CheckOutScreen.jsx b/frontend/app/screens/CheckOutScreen.jsx index 4c7c9f5..ebb7e3b 100644 --- a/frontend/app/screens/CheckOutScreen.jsx +++ b/frontend/app/screens/CheckOutScreen.jsx @@ -7,21 +7,31 @@ import { MapPinIcon } from 'react-native-heroicons/micro'; import Icon from 'react-native-vector-icons/FontAwesome'; import { useNavigation } from '@react-navigation/native'; import { checkoutCart, getCart } from '../api'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import * as Location from 'expo-location'; +import LocationModal from '../components/LocationModal'; +import { useCart } from '../context/CartContext'; +import ConfirmPayment from './ConfirmPayment'; export default function CheckOutScreen() { const navigation = useNavigation(); const [cart, setCart] = useState({ items: [], total_price: 0 }); - const [loading, setLoading] = useState(true); + const [loading, setIsLoading] = useState(true); const [placingOrder, setPlacingOrder] = useState(false); const [error, setError] = useState(null); const [refreshing, setRefreshing] = useState(false); - - // Default shipping address - const shippingAddress = { + const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(null); + const [shippingAddress, setShippingAddress] = useState({ type: 'Home', - address: '1901 Thornridge Cir. Shiloh, Hawaii', - zipCode: '06803' - }; + address: 'Loading address...', + zipCode: '' + }); + const [isLocationModalVisible, setIsLocationModalVisible] = useState(false); + const { appliedCoupon, couponDiscount, discount } = useCart(); + const [showPaymentSection, setShowPaymentSection] = useState(false); + + // Calculate final total + const finalTotal = cart.total_price - discount - couponDiscount + (selectedPaymentMethod?.type === 'cod' ? 150 : 0); // Default shipping method const shippingMethod = { @@ -29,6 +39,79 @@ export default function CheckOutScreen() { estimatedArrival: '25 August 2023' }; + // Fetch saved location from AsyncStorage + const fetchSavedLocation = useCallback(async () => { + try { + const savedLocation = await AsyncStorage.getItem('userLocation'); + if (savedLocation) { + const locationData = JSON.parse(savedLocation); + setShippingAddress({ + type: 'Current Location', + address: locationData.address, + zipCode: locationData.coordinates ? `${locationData.coordinates.latitude}, ${locationData.coordinates.longitude}` : '' + }); + } else { + // If no saved location, try to get current location + await getCurrentLocation(); + } + } catch (error) { + console.error('Error fetching saved location:', error); + setShippingAddress({ + type: 'Home', + address: 'Address not available', + zipCode: '' + }); + } + }, []); + + // Get current location + const getCurrentLocation = useCallback(async () => { + try { + const { status } = await Location.requestForegroundPermissionsAsync(); + if (status !== 'granted') { + Alert.alert('Permission Denied', 'Please enable location services to use this feature.'); + return; + } + + const location = await Location.getCurrentPositionAsync({}); + const { latitude, longitude } = location.coords; + + // Get address from coordinates using reverse geocoding + const [address] = await Location.reverseGeocodeAsync({ + latitude, + longitude + }); + + if (address) { + const formattedAddress = `${address.street || ''} ${address.name || ''}, ${address.city || ''}, ${address.region || ''} ${address.postalCode || ''}`.trim(); + setShippingAddress({ + type: 'Current Location', + address: formattedAddress, + zipCode: address.postalCode || `${latitude}, ${longitude}` + }); + + // Save to AsyncStorage + await AsyncStorage.setItem('userLocation', JSON.stringify({ + name: 'Current Location', + address: formattedAddress, + coordinates: { latitude, longitude } + })); + } + } catch (error) { + console.error('Error getting current location:', error); + Alert.alert('Error', 'Failed to get your location. Please try again.'); + } + }, []); + + // Handle location selection from modal + const handleLocationSelected = (locationData) => { + setShippingAddress({ + type: 'Selected Location', + address: locationData.address, + zipCode: locationData.coordinates ? `${locationData.coordinates.latitude}, ${locationData.coordinates.longitude}` : '' + }); + }; + // Calculate item subtotal const calculateSubtotal = (item) => { // Ensure price and quantity exist and are numbers @@ -45,23 +128,26 @@ export default function CheckOutScreen() { // Process each item to ensure it has the required properties const processedItems = cartData.items.map(item => { - // Calculate subtotal if not provided by the backend - if (!item.subtotal || typeof item.subtotal !== 'number') { - item.subtotal = calculateSubtotal(item); - } + // Get price from the correct field (product_price from API) + const price = typeof item.product_price === 'number' ? item.product_price : + typeof item.price === 'number' ? item.price : + parseFloat(item.product_price || item.price || 0); + + const quantity = parseInt(item.quantity) || 1; + const subtotal = price * quantity; - // Ensure other required properties exist return { ...item, - id: item.id || Date.now(), // Fallback ID if not provided + id: item.id || Date.now(), product_name: item.product_name || item.name || item.title || 'Unknown Product', product_image: item.product_image || item.image || 'https://via.placeholder.com/150', - price: typeof item.price === 'number' ? item.price : 0, - quantity: typeof item.quantity === 'number' ? item.quantity : 1 + price: price, + quantity: quantity, + subtotal: subtotal }; }); - // Calculate total price if not provided or if we want to ensure it's correct + // Calculate total price from processed items const total_price = processedItems.reduce((total, item) => total + item.subtotal, 0); return { @@ -70,9 +156,9 @@ export default function CheckOutScreen() { }; }; - const fetchCartData = async () => { + const fetchCartData = useCallback(async () => { try { - setLoading(true); + setIsLoading(true); const cartData = await getCart(); const processedCart = processCartData(cartData); setCart(processedCart); @@ -81,19 +167,29 @@ export default function CheckOutScreen() { console.error('Error fetching cart:', error); setError('Failed to load cart data'); } finally { - setLoading(false); + setIsLoading(false); setRefreshing(false); } - }; + }, []); const onRefresh = useCallback(() => { setRefreshing(true); fetchCartData(); - }, []); + }, [fetchCartData]); + // Initialize data useEffect(() => { - fetchCartData(); - }, []); + const initializeData = async () => { + try { + await fetchCartData(); + await fetchSavedLocation(); + } catch (error) { + console.error('Error initializing data:', error); + } + }; + + initializeData(); + }, [fetchCartData, fetchSavedLocation]); const handlePlaceOrder = async () => { if (!cart.items || cart.items.length === 0) { @@ -101,28 +197,88 @@ export default function CheckOutScreen() { return; } + if (!shippingAddress.address || shippingAddress.address === 'Loading address...' || shippingAddress.address === 'Address not available') { + Alert.alert('Address Required', 'Please set a valid shipping address before proceeding.'); + return; + } + + if (!selectedPaymentMethod) { + Alert.alert('Payment Method Required', 'Please select a payment method before proceeding.'); + return; + } + try { setPlacingOrder(true); - // Prepare order data + // Calculate final total with coupon discount + const subtotal = cart.total_price; + const deliveryFee = 10; // Fixed delivery fee + const discountPercentage = 6; + const discount = (subtotal * discountPercentage) / 100; + const finalTotal = subtotal + deliveryFee - discount - (couponDiscount || 0); + const orderData = { - shipping_address: `${shippingAddress.address}, ${shippingAddress.zipCode}`, - shipping_method: shippingMethod.type, + items: cart.items.map(item => ({ + id: item.id, + quantity: item.quantity, + price: item.price + })), + shipping_address: { + address: shippingAddress.address, + latitude: shippingAddress.latitude, + longitude: shippingAddress.longitude + }, + payment_method: selectedPaymentMethod, + shipping_method: shippingMethod, + subtotal: subtotal, + delivery_fee: deliveryFee, + discount: discount, + coupon_discount: couponDiscount || 0, + coupon_code: appliedCoupon?.code || null, + total: finalTotal }; - - // Place the order using checkout endpoint + const response = await checkoutCart(orderData); - // Navigate to success screen - navigation.navigate('Payment Success', { orderId: response.id || response.order_id }); + if (response && response.order_id) { + // Clear the cart after successful order + await AsyncStorage.removeItem('cart'); + + // Navigate to Payment Success screen + navigation.navigate('Payment Success'); + } else { + throw new Error('Invalid response from server'); + } } catch (error) { console.error('Error placing order:', error); - Alert.alert('Checkout Failed', 'Failed to place your order. Please try again.'); + Alert.alert( + 'Error', + error.response?.data?.message || error.message || 'Failed to place order. Please try again.' + ); } finally { setPlacingOrder(false); } }; + // Handle payment method selection result + useEffect(() => { + const unsubscribe = navigation.addListener('focus', () => { + // Check if payment method was selected and stored + AsyncStorage.getItem('selectedPaymentMethod').then(method => { + if (method) { + setSelectedPaymentMethod(JSON.parse(method)); + } + }); + }); + + return unsubscribe; + }, [navigation]); + + const handlePaymentMethodSelect = (method) => { + setSelectedPaymentMethod(method); + setShowPaymentSection(false); + }; + if (loading) { return ( @@ -150,7 +306,7 @@ export default function CheckOutScreen() { Your cart is empty navigation.navigate('Home')} + onPress={() => navigation.navigate('Main')} className="bg-primary py-2 px-4 rounded-lg" > Continue Shopping @@ -159,22 +315,16 @@ export default function CheckOutScreen() { ); } - // Calculate costs - const deliveryFee = 10; - const discountPercentage = 6; - const discount = (cart.total_price * discountPercentage) / 100; - const totalCost = cart.total_price + deliveryFee - discount; - return ( - + } + suffix={} titleStyle={{ color: "black", fontWeight: "bold", - fontSize: 20 + fontSize: wp('5%') }} prefixStyle={{ backgroundColor: "transparent" @@ -199,36 +349,121 @@ export default function CheckOutScreen() { } > - Shipping Address + Shipping Address - - {shippingAddress.type} + + {shippingAddress.type} - - {shippingAddress.address} - {navigation.navigate('Shipping Address')}}> - CHANGE + + + + {shippingAddress.address} + + + {shippingAddress.zipCode} + + + setIsLocationModalVisible(true)} + style={{ + paddingHorizontal: wp('2%'), + paddingVertical: hp('0.5%') + }} + > + + CHANGE + - {shippingAddress.zipCode} - Choose Shipping Type + Choose Shipping Type - - {shippingMethod.type} + + {shippingMethod.type} - - Estimated arrival {shippingMethod.estimatedArrival} - {navigation.navigate('Choose Shipping')}}> - CHANGE + + + + Estimated arrival {shippingMethod.estimatedArrival} + + + {navigation.navigate('Choose Shipping')}} + style={{ + paddingHorizontal: wp('2%'), + paddingVertical: hp('0.5%') + }} + > + + CHANGE + + + Payment Method + + + {selectedPaymentMethod ? selectedPaymentMethod.name : 'Select payment method'} + + setShowPaymentSection(true)}> + + {selectedPaymentMethod ? 'CHANGE' : 'SELECT'} + + + + {selectedPaymentMethod?.type === 'cod' && ( + + Additional COD charge: Rs150 + + )} + + - Order List - {cart.items.length} item{cart.items.length !== 1 ? 's' : ''} + Order List + {cart.items.length} item{cart.items.length !== 1 ? 's' : ''} {cart.items.map((item, index) => { @@ -240,9 +475,9 @@ export default function CheckOutScreen() { - {item.product_name} - Quantity: {item.quantity} - $ {itemSubtotal.toFixed(2)} + {item.product_name} + Quantity: {item.quantity} + Rs{itemSubtotal.toFixed(2)} @@ -251,38 +486,106 @@ export default function CheckOutScreen() { - Sub-Total - ${cart.total_price.toFixed(2)} + Sub-Total + Rs{cart.total_price.toFixed(2)} - Delivery fee - ${deliveryFee.toFixed(2)} + Delivery fee + Rs{10.00} + {couponDiscount > 0 && ( + + Coupon Discount + - Rs {couponDiscount.toFixed(2)} + + )} - Discount - {discountPercentage}% (${discount.toFixed(2)}) + Discount + {6}% (Rs{discount.toFixed(2)}) + {selectedPaymentMethod?.type === 'cod' && ( + + COD Fee + Rs{150.00} + + )} - Total Cost - ${totalCost.toFixed(2)} + Total Cost + Rs{finalTotal.toFixed(2)} + {/* Add padding at the bottom to prevent content from being hidden behind the button */} + { + if (!selectedPaymentMethod) { + Alert.alert('Payment Method Required', 'Please select a payment method to continue.'); + return; + } + handlePlaceOrder(); + }} + disabled={placingOrder || !selectedPaymentMethod} + style={{ + backgroundColor: 'white', + paddingVertical: hp('1.5%'), + borderTopLeftRadius: wp('4%'), + borderTopRightRadius: wp('4%'), + borderWidth: 1, + borderColor: '#d1d5db', + position: 'absolute', + bottom: 0, + width: '100%', + shadowColor: "#000", + shadowOffset: { + width: 0, + height: -2, + }, + shadowOpacity: 0.1, + shadowRadius: 3, + elevation: 5, + }} > - + {placingOrder ? ( ) : ( - Continue to Payment + + {selectedPaymentMethod ? 'Place Order' : 'Select Payment Method'} + )} + + setIsLocationModalVisible(false)} + onLocationSelected={handleLocationSelected} + /> + + {/* Payment Modal */} + {showPaymentSection && ( + + + + )} ); } \ No newline at end of file diff --git a/frontend/app/screens/CompleteYourProfileScreen.jsx b/frontend/app/screens/CompleteYourProfileScreen.jsx index e8d2856..79ad805 100644 --- a/frontend/app/screens/CompleteYourProfileScreen.jsx +++ b/frontend/app/screens/CompleteYourProfileScreen.jsx @@ -1,15 +1,151 @@ -import { View, Text, Image } from 'react-native' -import React from 'react' -import { widthPercentageToDP as wp, heightPercentageToDP as hp, heightPercentageToDP } from 'react-native-responsive-screen'; +import { View, Text, Image, TouchableOpacity, ActivityIndicator, Alert, ScrollView } from 'react-native' +import React, { useState, useEffect } from 'react' +import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import TabBar from '../themes/TabBar'; -import { ChevronDownIcon, ChevronLeftIcon, PencilSquareIcon } from 'react-native-heroicons/outline'; -import pic from "../../assets/images/pic.jpg" +import { ChevronLeftIcon, PencilSquareIcon } from 'react-native-heroicons/outline'; import { TextInput } from 'react-native'; import { useNavigation } from '@react-navigation/native'; +import { getUserData, updateUserProfile, updateProfilePicture, getGenderOptions } from '../api'; +import * as ImagePicker from 'expo-image-picker'; +import { Dropdown } from 'react-native-element-dropdown'; + +const GENDER_OPTIONS = [ + { label: 'Male', value: 'Male' }, + { label: 'Female', value: 'Female' }, + { label: 'Other', value: 'Other' } +]; export default function CompleteYourProfileScreen() { const navigation = useNavigation(); - + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [user, setUser] = useState(null); + const [genderOptions, setGenderOptions] = useState(GENDER_OPTIONS); + const [formData, setFormData] = useState({ + username: '', + first_name: '', + last_name: '', + email: '', + phone: '', + gender: '', + address: '' + }); + const [showGenderPicker, setShowGenderPicker] = useState(false); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + const [userData, genderOptions] = await Promise.all([ + getUserData(), + getGenderOptions() + ]); + + console.log('User Data:', userData); + console.log('Gender Options:', genderOptions); + console.log('User Gender:', userData?.gender); + + if (userData) { + setUser(userData); + setFormData({ + username: userData.username || '', + first_name: userData.first_name || '', + last_name: userData.last_name || '', + email: userData.email || '', + phone: userData.phone || '', + gender: userData.gender || '', + address: userData.address || '' + }); + } + + setGenderOptions(genderOptions); + } catch (error) { + console.error('Error loading data:', error); + Alert.alert('Error', 'Failed to load profile data'); + setGenderOptions(GENDER_OPTIONS); + } finally { + setLoading(false); + } + }; + + const handleUpdateProfile = async () => { + setSaving(true); + try { + const updatedUser = await updateUserProfile({ + username: formData.username, + first_name: formData.first_name, + last_name: formData.last_name, + email: formData.email, + phone: formData.phone, + gender: formData.gender, + address: formData.address + }); + setUser(updatedUser); + Alert.alert('Success', 'Profile updated successfully'); + } catch (error) { + console.error('Error updating profile:', error); + Alert.alert('Error', 'Failed to update profile'); + } finally { + setSaving(false); + } + }; + + const handleImagePick = async () => { + try { + const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (status !== 'granted') { + Alert.alert('Permission required', 'Please grant permission to access your photos'); + return; + } + + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaType.Images, + allowsEditing: true, + aspect: [1, 1], + quality: 0.8, + }); + + if (!result.canceled) { + setLoading(true); + try { + const updatedUser = await updateProfilePicture(result.assets[0].uri); + if (updatedUser) { + setUser(updatedUser); + Alert.alert('Success', 'Profile picture updated successfully'); + } + } catch (error) { + console.error('Profile picture update error:', error.response?.data || error); + Alert.alert('Error', 'Failed to update profile picture'); + } finally { + setLoading(false); + } + } + } catch (error) { + console.error('Error picking image:', error); + Alert.alert('Error', 'Failed to pick image'); + } + }; + + const handleGenderSelect = (item) => { + console.log('Selected gender:', item); + setFormData(prev => ({ + ...prev, + gender: item.value + })); + setShowGenderPicker(false); + }; + + if (loading) { + return ( + + + + ); + } + return ( navigation.goBack()} /> - - Complete Your Profile - Don't worry, only you can see your personal data. No one else will be able to see it. - - - - - - - + + + Complete Your Profile + + Don't worry, only you can see your personal data. No one else will be able to see it. + + + + + + + + + - - - Name - - - - - - Email - - - - - - Gender - - - - - - + + Username + + setFormData({...formData, username: text})} + /> + + + + First Name + + setFormData({...formData, first_name: text})} + /> + + + + Last Name + + setFormData({...formData, last_name: text})} + /> + + + + Gender + + + + + + Email + + setFormData({...formData, email: text})} + keyboardType="email-address" + autoCapitalize="none" + /> + + + + Phone + + setFormData({...formData, phone: text})} + keyboardType="phone-pad" + /> + + + + Address + + setFormData({...formData, address: text})} + keyboardType='phone-pad' + /> - - - - Complete Profile - + + {saving ? ( + + ) : ( + Complete Profile + )} + + - ) -} \ No newline at end of file + ); +} + +const styles = { + dropdown: { + height: 50, + borderWidth: 1, + borderColor: '#d1d5db', + borderRadius: 25, + paddingHorizontal: 16, + marginTop: 8, + backgroundColor: 'white', + }, + placeholderStyle: { + fontSize: 16, + color: 'gray', + }, + selectedTextStyle: { + fontSize: 16, + color: 'black', + }, + iconStyle: { + width: 20, + height: 20, + }, + inputSearchStyle: { + height: 40, + fontSize: 16, + }, + containerStyle: { + borderRadius: 12, + borderWidth: 1, + borderColor: '#d1d5db', + marginTop: 8, + zIndex: 1000, + elevation: 3, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + backgroundColor: 'white', + }, + itemContainerStyle: { + paddingVertical: 8, + paddingHorizontal: 16, + }, + itemTextStyle: { + fontSize: 16, + color: 'black', + }, +}; \ No newline at end of file diff --git a/frontend/app/screens/ConfirmPayment.jsx b/frontend/app/screens/ConfirmPayment.jsx index 812c6ba..ade2ff3 100644 --- a/frontend/app/screens/ConfirmPayment.jsx +++ b/frontend/app/screens/ConfirmPayment.jsx @@ -1,81 +1,309 @@ -import { View, Text, TextInput, Image } from 'react-native' -import React, { useState } from 'react' -import TabBar from '../themes/TabBar' -import { ChevronLeftIcon, ChevronRightIcon, CreditCardIcon } from 'react-native-heroicons/outline' -import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; -import paymentData from '../constants/paymentData'; -import { TouchableOpacity } from 'react-native'; -import { useNavigation } from '@react-navigation/native'; +import { + View, + Text, + TextInput, + Image, + TouchableOpacity, + Alert, +} from "react-native"; +import React, { useState, useEffect } from "react"; +import TabBar from "../themes/TabBar"; +import { + ChevronLeftIcon, + ChevronRightIcon, + CreditCardIcon, + BanknotesIcon, + CheckCircleIcon, +} from "react-native-heroicons/outline"; +import { + widthPercentageToDP as wp, + heightPercentageToDP as hp, +} from "react-native-responsive-screen"; +import { useNavigation } from "@react-navigation/native"; +import AsyncStorage from "@react-native-async-storage/async-storage"; + +// Import local assets +const esewaLogo = require("../../assets/images/esewa.jpg"); +const khaltiLogo = require("../../assets/images/khalti.jpg"); +const imePayLogo = require("../../assets/images/imepay.png"); export default function ConfirmPayment() { - const navigation = useNavigation() + const navigation = useNavigation(); + const [paymentDataActive, setpaymentData] = useState(null); + const [linkedMethods, setLinkedMethods] = useState({ + card: false, + paypal: false, + applePay: false, + esewa: false, + khalti: false, + ime: false, + }); - const [paymentDataActive, setpaymentData] = useState(0) + // Load the selected payment method and linked methods from AsyncStorage when the component mounts + useEffect(() => { + const loadData = async () => { + try { + // Load selected payment method + const storedMethod = await AsyncStorage.getItem( + "selectedPaymentMethod", + ); + if (storedMethod) { + const method = JSON.parse(storedMethod); + setpaymentData(method.index); + } + + // Load linked payment methods + const savedMethods = await AsyncStorage.getItem("linkedPaymentMethods"); + if (savedMethods) { + setLinkedMethods(JSON.parse(savedMethods)); + } + } catch (error) { + console.error("Error loading payment data:", error); + } + }; + + loadData(); + }, []); + + const handlePaymentSelect = (index, type) => { + setpaymentData(index); + + // Create payment method object based on selection + const selectedMethod = { + index, + type, + name: getPaymentMethodName(type), + }; + + // Store the selected payment method + AsyncStorage.setItem( + "selectedPaymentMethod", + JSON.stringify(selectedMethod), + ); + }; + + const getPaymentMethodName = (type) => { + switch (type) { + case "cod": + return "Cash on Delivery"; + case "card": + return "Credit/Debit Card"; + case "paypal": + return "PayPal"; + case "applePay": + return "Apple Pay"; + case "esewa": + return "eSewa"; + case "khalti": + return "Khalti"; + case "ime": + return "IME Pay"; + default: + return "Unknown"; + } + }; + + const handleConfirmPayment = async () => { + try { + if (paymentDataActive === null) { + Alert.alert("Error", "Please select a payment method"); + return; + } + + // Get the selected payment method + const selectedMethod = { + index: paymentDataActive, + type: getPaymentMethodType(paymentDataActive), + name: getPaymentMethodName(getPaymentMethodType(paymentDataActive)), + }; + + // Store the selected payment method + await AsyncStorage.setItem( + "selectedPaymentMethod", + JSON.stringify(selectedMethod), + ); + + // Navigate to the Checkout screen + navigation.replace('Checkout'); + } catch (error) { + console.error("Error confirming payment:", error); + Alert.alert("Error", "Failed to confirm payment. Please try again."); + } + }; + + const getPaymentMethodType = (index) => { + switch (index) { + case -1: + return "cod"; + case 0: + return "card"; + case 1: + return "paypal"; + case 2: + return "applePay"; + case 3: + return "esewa"; + case 4: + return "khalti"; + case 5: + return "ime"; + default: + return "unknown"; + } + }; + + const renderPaymentOption = (index, type, icon, name, isLinked = false) => { + const isActive = index === paymentDataActive; + + return ( + handlePaymentSelect(index, type)} + className={`border rounded-2xl mb-4 ${isActive ? "border-primary bg-primary/10" : "border-gray-300"}`} + disabled={!isLinked && type !== "cod"} + > + + + {icon} + + {name} + {!isLinked && type !== "cod" && ( + Not linked + )} + {type === "cod" && ( + + Additional charge: Rs150 + + )} + + + + {isActive && ( + + )} + + + + ); + }; return ( } + suffix={} titleStyle={{ color: "black", fontWeight: "bold", - fontSize: 20 + fontSize: 20, }} prefixStyle={{ - backgroundColor: "transparent" + backgroundColor: "transparent", }} suffixStyle={{ backgroundColor: "white", borderWidth: 1, - borderColor: "black" + borderColor: "black", }} containerStyle="" - suffixAction={() => navigation.goBack()} + suffixAction={() => navigation.replace('Checkout')} /> - - Credit & Debit Card + + + + Select Payment Method + + + {/* Cash on Delivery Option */} + {renderPaymentOption( + -1, + "cod", + , + "Cash on Delivery", + )} + + {/* Credit/Debit Card */} + {renderPaymentOption( + 0, + "card", + , + "Credit/Debit Card", + linkedMethods.card, + )} + + {/* PayPal */} + {renderPaymentOption( + 1, + "paypal", + , + "PayPal", + linkedMethods.paypal, + )} + + {/* Apple Pay */} + {renderPaymentOption( + 2, + "applePay", + , + "Apple Pay", + linkedMethods.applePay, + )} + + {/* eSewa */} + {renderPaymentOption( + 3, + "esewa", + , + "eSewa", + linkedMethods.esewa, + )} + + {/* Khalti */} + {renderPaymentOption( + 4, + "khalti", + , + "Khalti", + linkedMethods.khalti, + )} + + {/* IME Pay */} + {renderPaymentOption( + 5, + "ime", + , + "IME Pay", + linkedMethods.ime, + )} - - - - - - {navigation.navigate('Add Card')}} className={`justify-center items-center`}> - + + + + + Confirm Payment + - - More Payment Options - - - - { - paymentData.map((item, index) => { - let isActive = index == paymentDataActive; - return ( - setpaymentData(index)} className={`border border-gray-300 rounded-2xl mx-4`}> - - - {item.name} - - - - - - - - ) - }) - } - - - {navigation.navigate('Payment Success')}} className={`border rounded-full bg-primary flex-row justify-center items-center `} style={{ height: hp('5%') }}> - Confirm Payment - - - - ) -} \ No newline at end of file + ); +} diff --git a/frontend/app/screens/CouponScreen.jsx b/frontend/app/screens/CouponScreen.jsx index bfba3a1..f8a7a1a 100644 --- a/frontend/app/screens/CouponScreen.jsx +++ b/frontend/app/screens/CouponScreen.jsx @@ -1,12 +1,60 @@ -import { View, Text, ScrollView } from 'react-native' -import React from 'react' +import { View, Text, ScrollView, TouchableOpacity, Alert } from 'react-native' +import React, { useState, useEffect } from 'react' import { widthPercentageToDP as wp, heightPercentageToDP as hp, heightPercentageToDP } from 'react-native-responsive-screen'; import TabBar from '../themes/TabBar'; import { ChevronLeftIcon } from 'react-native-heroicons/outline'; import Icon from "react-native-vector-icons/MaterialCommunityIcons" -import couponData from '../constants/couponData'; +import { getCoupons, validateCoupon } from '../api'; +import { useNavigation } from '@react-navigation/native'; +import { useCart } from '../context/CartContext'; +import { useAuth } from '../context/AuthContext'; export default function CouponScreen() { + const [coupons, setCoupons] = useState([]); + const [loading, setLoading] = useState(true); + const navigation = useNavigation(); + const { cartTotal } = useCart(); + const { isAuthenticated } = useAuth(); + + useEffect(() => { + fetchCoupons(); + }, []); + + const fetchCoupons = async () => { + try { + setLoading(true); + const data = await getCoupons(); + setCoupons(data); + } catch (error) { + console.error('Error fetching coupons:', error); + Alert.alert('Error', 'Failed to load coupons. Please try again later.'); + } finally { + setLoading(false); + } + }; + + const handleCopyCode = async (coupon) => { + try { + if (!isAuthenticated) { + Alert.alert('Login Required', 'Please login to use coupons'); + navigation.navigate('Login'); + return; + } + + const result = await validateCoupon(coupon.code, cartTotal); + + if (result.valid) { + // Copy the code to clipboard + // You might want to use Clipboard API here + Alert.alert('Success', `Coupon code ${coupon.code} copied to clipboard!`); + } else { + Alert.alert('Invalid Coupon', result.message || 'This coupon cannot be applied to your cart.'); + } + } catch (error) { + console.error('Error validating coupon:', error); + Alert.alert('Error', 'Failed to validate coupon. Please try again later.'); + } + }; return ( @@ -34,21 +82,30 @@ export default function CouponScreen() { Best offers for you - { - couponData.map((item, index) => { + {loading ? ( + Loading coupons... + ) : coupons.length > 0 ? ( + coupons.map((item, index) => { return ( - {item.couponCode} - Add items worth ${item.price} more to unlock + {item.code} + Add items worth ${item.min_purchase} more to unlock - {item.discount} + + {item.discount_type === 'percentage' + ? `${item.discount_value}% OFF` + : `$${item.discount_value} OFF`} + - + handleCopyCode(item)} + > COPY CODE - + {/* this line of code make the semicircle i both side of container */} @@ -60,10 +117,9 @@ export default function CouponScreen() { ) }) - } - - - + ) : ( + No coupons available at the moment. + )} diff --git a/frontend/app/screens/FashionScreen.jsx b/frontend/app/screens/FashionScreen.jsx deleted file mode 100644 index d8d6b07..0000000 --- a/frontend/app/screens/FashionScreen.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import { View, Text } from 'react-native' -import React from 'react' -import Icon from 'react-native-vector-icons/FontAwesome' -import { widthPercentageToDP as wp, heightPercentageToDP as hp, heightPercentageToDP } from 'react-native-responsive-screen'; - -export default function FashionScreen() { - return ( - - - - - - fashion. - - - - ) -} \ No newline at end of file diff --git a/frontend/app/screens/FilterScreen.jsx b/frontend/app/screens/FilterScreen.jsx index a70a0b5..e1f9dd2 100644 --- a/frontend/app/screens/FilterScreen.jsx +++ b/frontend/app/screens/FilterScreen.jsx @@ -1,15 +1,17 @@ import { View, Text, TouchableOpacity, ScrollView } from 'react-native' -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { ChevronLeftIcon } from 'react-native-heroicons/outline' import TabBar from '../themes/TabBar' import { StarIcon } from 'react-native-heroicons/solid' import { widthPercentageToDP as wp, heightPercentageToDP as hp, heightPercentageToDP } from 'react-native-responsive-screen'; import MultiSlider from '@ptomasroos/react-native-multi-slider' -import { useNavigation } from '@react-navigation/native' - +import { useNavigation, useRoute } from '@react-navigation/native' +import AsyncStorage from '@react-native-async-storage/async-storage'; export default function FilterScreen() { const navigation = useNavigation() + const route = useRoute() + const { activeFilters } = route.params || {}; const brand = ["all", "adidas", "nike", "puma", "gucci", "parada", "ogiy", "reebook"] const [brandActive, setbrandActive] = useState(0) @@ -28,6 +30,67 @@ export default function FilterScreen() { setRange(values) } + // Initialize filters from activeFilters if available + useEffect(() => { + if (activeFilters) { + // Set brand filter + if (activeFilters.brand) { + const brandIndex = brand.findIndex(b => b.toLowerCase() === activeFilters.brand.toLowerCase()); + if (brandIndex !== -1) setbrandActive(brandIndex); + } + + // Set gender filter + if (activeFilters.gender) { + const genderIndex = gender.findIndex(g => g.toLowerCase() === activeFilters.gender.toLowerCase()); + if (genderIndex !== -1) setgenderActive(genderIndex); + } + + // Set sort by filter + if (activeFilters.sortBy) { + const sortIndex = sortBy.findIndex(s => s === activeFilters.sortBy); + if (sortIndex !== -1) setsortByActive(sortIndex); + } + + // Set review filter + if (activeFilters.subReview) { + const reviewIndex = subReview.findIndex(r => r === activeFilters.subReview); + if (reviewIndex !== -1) setsubReview(reviewIndex); + } + + // Set price range + if (activeFilters.range) { + setRange(activeFilters.range); + } + } + }, [activeFilters]); + + const handleReset = () => { + setbrandActive(0); + setgenderActive(0); + setsortByActive(0); + setsubReview(0); + setRange([10, 2000]); + }; + + const handleApply = async () => { + // Save filter settings to AsyncStorage + const filterSettings = { + brand: brand[brandActive], + gender: gender[genderActive], + sortBy: sortBy[sortByActive], + subReview: subReview[subReviewActive] + }; + + try { + await AsyncStorage.setItem('filterSettings', JSON.stringify(filterSettings)); + // Navigate back to HomeScreen + navigation.goBack(); + } catch (error) { + console.error('Error saving filter settings:', error); + navigation.goBack(); + } + }; + return ( - - Pricing Range : Rs {range[0]} to Rs {range[1]} - - - {/* Indicator below tthe slider*/} - - { - Array.from({ length: 6 }, (_, index) => { - // Calculate the value dynamically within the range - const min = 10; - const max = range[2000]; - const step = (max - min) / 5; // 5 steps for 6 values - const value = Math.round(min + index * step); - - return ( - - Rs {value} - - ) - }) - } - - - Reveiws @@ -223,12 +237,18 @@ export default function FilterScreen() { - + Reset Filter - - + + Apply - + ) diff --git a/frontend/app/screens/FlashSalesScreen.jsx b/frontend/app/screens/FlashSalesScreen.jsx index bbbe91a..9234367 100644 --- a/frontend/app/screens/FlashSalesScreen.jsx +++ b/frontend/app/screens/FlashSalesScreen.jsx @@ -1,123 +1,183 @@ -import { View, Text, ScrollView, TouchableOpacity, Image, RefreshControl } from 'react-native' -import React, { useState, useEffect, useCallback } from 'react' -import { HeartIcon } from 'react-native-heroicons/outline' -import { HeartIcon as SolidHeartIcon } from 'react-native-heroicons/solid' -import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen' -import { useNavigation, useFocusEffect } from '@react-navigation/native' -import flashSalesData from '../constants/flashSalesData' -import { StarIcon } from 'react-native-heroicons/solid' -import { getFavorites, toggleFavorite } from '../api' - -export default function FlashSalesScreen() { - const navigation = useNavigation(); - const [favorites, setFavorites] = useState([]); - const [loading, setLoading] = useState(true); - const [refreshing, setRefreshing] = useState(false); - - const loadFavorites = useCallback(async () => { - try { - const favoriteProducts = await getFavorites(); - setFavorites(favoriteProducts); - } catch (error) { - console.error('Error loading favorites:', error); - } finally { - setLoading(false); - setRefreshing(false); - } - }, []); - - // Load favorites when component mounts and when screen is focused - useFocusEffect( - useCallback(() => { - loadFavorites(); - }, [loadFavorites]) - ); - - const onRefresh = useCallback(() => { - setRefreshing(true); - loadFavorites(); - }, [loadFavorites]); - - const handleFavouriteToggle = async (product) => { - try { - const updatedFavorites = await toggleFavorite(product); - setFavorites(updatedFavorites); - } catch (error) { - console.error('Error toggling favorite:', error); - } - }; - - const isProductFavorited = (productId) => { - return favorites.some(fav => fav.id === productId); - }; - - const renderItem = ({ item, i }) => { - const isFavorite = isProductFavorited(item.id); - - return ( - - navigation.navigate('ProductDetails', { productId: item.id })} - > - - - { - e.stopPropagation(); - handleFavouriteToggle(item); - }} - className={`absolute top-3 right-3 z-10 bg-white rounded-full p-1`} - > - {isFavorite ? ( - - ) : ( - - )} - - - - {item.name} - - - {item.rating || 0} - - Rs {item.price} - - - - ); - }; - - if (loading && !refreshing) { - return ( - - Loading... - - ); - } - - return ( - - - } - > - - {flashSalesData.map((item, index) => renderItem({ item, i: index }))} - - - - ); -} \ No newline at end of file +import { View, Text, ScrollView, TouchableOpacity, Image, RefreshControl, ActivityIndicator } from 'react-native' +import React, { useState, useEffect, useCallback } from 'react' +import { HeartIcon } from 'react-native-heroicons/outline' +import { HeartIcon as SolidHeartIcon } from 'react-native-heroicons/solid' +import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen' +import { useNavigation, useFocusEffect } from '@react-navigation/native' +import { StarIcon } from 'react-native-heroicons/solid' +import { getFavorites, toggleFavorite, fetchProducts } from '../api' +import TabBar from '../themes/TabBar' + +export default function FlashSalesScreen() { + const navigation = useNavigation(); + const [favorites, setFavorites] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [flashSales, setFlashSales] = useState([]); + const [error, setError] = useState(null); + + const loadFlashSales = useCallback(async () => { + try { + // Fetch products from API with a 'flash_sale' filter if your API supports it + // This is an example - modify this to match your actual API + const products = await fetchProducts('flash_sale'); + setFlashSales(products); + setError(null); + } catch (error) { + console.error('Error loading flash sales:', error); + setError('Failed to load flash sales. Please try again.'); + } + }, []); + + const loadFavorites = useCallback(async () => { + try { + const favoriteProducts = await getFavorites(); + setFavorites(favoriteProducts); + } catch (error) { + console.error('Error loading favorites:', error); + } + }, []); + + const loadData = useCallback(async () => { + setLoading(true); + try { + await Promise.all([loadFlashSales(), loadFavorites()]); + } catch (error) { + console.error('Error loading data:', error); + } finally { + setLoading(false); + setRefreshing(false); + } + }, [loadFlashSales, loadFavorites]); + + // Load data when component mounts and when screen is focused + useFocusEffect( + useCallback(() => { + loadData(); + }, [loadData]) + ); + + const onRefresh = useCallback(() => { + setRefreshing(true); + loadData(); + }, [loadData]); + + const handleFavouriteToggle = async (product) => { + try { + const updatedFavorites = await toggleFavorite(product); + setFavorites(updatedFavorites); + } catch (error) { + console.error('Error toggling favorite:', error); + } + }; + + const isProductFavorited = (productId) => { + return favorites.some(fav => fav.id === productId); + }; + + const renderItem = (item, index) => { + const isFavorite = isProductFavorited(item.id); + + return ( + + navigation.navigate('ProductDetails', { productId: item.id })} + > + + + { + e.stopPropagation(); + handleFavouriteToggle(item); + }} + className={`absolute top-3 right-3 z-10 bg-white rounded-full p-1`} + > + {isFavorite ? ( + + ) : ( + + )} + + + + {item.name || item.title} + + + {item.rating || 0} + + Rs {item.price} + + + + ); + }; + + if (loading && !refreshing) { + return ( + + + Loading... + + ); + } + + return ( + + + + } + > + {error ? ( + + {error} + + Try Again + + + ) : flashSales.length === 0 && !loading ? ( + + No flash sales available at the moment. + + ) : ( + + {flashSales.map((item, index) => renderItem(item, index))} + + )} + + + ); +} diff --git a/frontend/app/screens/HomeScreen.jsx b/frontend/app/screens/HomeScreen.jsx index c5e64c1..70df073 100644 --- a/frontend/app/screens/HomeScreen.jsx +++ b/frontend/app/screens/HomeScreen.jsx @@ -5,30 +5,65 @@ import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-nat import Category from "../components/Category"; import FlashSales from "../components/FlashSales"; import { TouchableOpacity } from "react-native"; -import { useNavigation } from "@react-navigation/native"; +import { useNavigation, useFocusEffect } from "@react-navigation/native"; import HomeCarousel from "../components/HomeCarousel"; -import { useEffect, useState, useCallback } from "react"; -import { fetchCategories, fetchProducts } from "../api"; +import { useEffect, useState, useCallback, useRef } from "react"; +import { fetchCategories, fetchProducts, fetchSliders } from "../api"; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import LocationModal from "../components/LocationModal"; +import { useNotifications } from '../context/NotificationContext'; export default function HomeScreen() { const navigation = useNavigation() const [page, setPage] = useState(0); const [products, setProducts] = useState([]); const [categories, setCategories] = useState([]); + const [sliders, setSliders] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(null); + const [location, setLocation] = useState(null); + const [isLocationModalVisible, setIsLocationModalVisible] = useState(false); + const flashSalesRef = useRef(null); + const [filteredProducts, setFilteredProducts] = useState([]); + const [activeFilters, setActiveFilters] = useState(null); + const { getUnreadCount } = useNotifications(); + const [unreadCount, setUnreadCount] = useState(0); + + // Update unread count when component mounts and when screen is focused + useEffect(() => { + updateUnreadCount(); + }, []); + + useFocusEffect( + useCallback(() => { + updateUnreadCount(); + }, []) + ); + + const updateUnreadCount = () => { + try { + const count = getUnreadCount(); + setUnreadCount(count); + } catch (error) { + console.error('Error getting unread count:', error); + setUnreadCount(0); + } + }; const loadData = async () => { try { - // Fetch both products and categories - const [productsData, categoriesData] = await Promise.all([ + // Fetch products, categories, and sliders + const [productsData, categoriesData, slidersData] = await Promise.all([ fetchProducts(), - fetchCategories() + fetchCategories(), + fetchSliders() ]); setProducts(productsData); + setFilteredProducts(productsData); setCategories(categoriesData); + setSliders(slidersData); setError(null); } catch (error) { console.error('Error fetching data:', error); @@ -39,19 +74,135 @@ export default function HomeScreen() { } }; + const loadLocation = async () => { + try { + const savedLocation = await AsyncStorage.getItem('userLocation'); + if (savedLocation) { + setLocation(JSON.parse(savedLocation)); + } + } catch (error) { + console.error('Error loading location:', error); + } + }; + useEffect(() => { loadData(); + loadLocation(); }, []); const onRefresh = useCallback(() => { setRefreshing(true); loadData(); + loadLocation(); }, []); + const handleLocationSelected = (newLocation) => { + setLocation(newLocation); + }; + + useFocusEffect( + useCallback(() => { + // Check if we should scroll to Flash Sales + const shouldScrollToFlashSales = navigation.getState().routes.find( + route => route.name === 'Main' && route.params?.scrollToFlashSales + ); + + if (shouldScrollToFlashSales && flashSalesRef.current) { + // Reset the navigation params to prevent scrolling on subsequent focuses + navigation.setParams({ scrollToFlashSales: false }); + + // Scroll to Flash Sales section + flashSalesRef.current.scrollTo({ y: 0, animated: true }); + } + }, [navigation]) + ); + + // Check for filter settings when screen is focused + useFocusEffect( + useCallback(() => { + const checkFilters = async () => { + try { + const filterSettingsStr = await AsyncStorage.getItem('filterSettings'); + if (filterSettingsStr) { + const filterSettings = JSON.parse(filterSettingsStr); + handleFilterResults(filterSettings); + // Clear the filter settings after applying them + await AsyncStorage.removeItem('filterSettings'); + } + } catch (error) { + console.error('Error checking filter settings:', error); + } + }; + + checkFilters(); + }, []) + ); + + // Handle filter results + const handleFilterResults = (filters) => { + setActiveFilters(filters); + + // Apply filters to products + let filtered = [...products]; + + // Filter by brand + if (filters.brand && filters.brand !== 'all') { + filtered = filtered.filter(product => + product.brand?.toLowerCase() === filters.brand.toLowerCase() + ); + } + + // Filter by gender + if (filters.gender && filters.gender !== 'All') { + filtered = filtered.filter(product => + product.gender?.toLowerCase() === filters.gender.toLowerCase() + ); + } + + // Filter by rating + if (filters.subReview) { + const [minRating] = filters.subReview.split(' ')[0].split('.'); + filtered = filtered.filter(product => { + const rating = parseFloat(product.rating || 0); + return rating >= parseFloat(minRating); + }); + } + + // Sort products + if (filters.sortBy) { + switch (filters.sortBy) { + case 'Most Recent': + filtered.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + break; + case 'Populor': + filtered.sort((a, b) => (b.popularity || 0) - (a.popularity || 0)); + break; + case 'Price High': + filtered.sort((a, b) => { + const priceA = parseFloat(a.price || a.product_price || 0); + const priceB = parseFloat(b.price || b.product_price || 0); + return priceB - priceA; + }); + break; + case 'Price Low': + filtered.sort((a, b) => { + const priceA = parseFloat(a.price || a.product_price || 0); + const priceB = parseFloat(b.price || b.product_price || 0); + return priceA - priceB; + }); + break; + default: + break; + } + } + + setFilteredProducts(filtered); + }; + if(loading && !refreshing){ return ( - + ) } @@ -59,7 +210,7 @@ export default function HomeScreen() { if(error){ return ( - {error} + {error} { setLoading(true); @@ -67,108 +218,112 @@ export default function HomeScreen() { }} className="mt-4 bg-primary py-2 px-4 rounded-lg" > - Retry + Retry ) } - const slider = [ - { - title: "new collection", - description: "Discount 50% for the first transaction." - }, - { - title: "kids collection", - description: "Discount 50% for the first transaction." - }, - { - title: "traditional collection", - description: "Discount 50% for the first transaction." - }, - { - title: "traditional collection", - description: "Discount 50% for the first transaction." - }, - { - title: "traditional collection", - description: "Discount 50% for the first transaction." - }, - ] - return ( - - } - > - {/* header start */} - - - Location - + <> + + } + > + {/* header start */} + + setIsLocationModalVisible(true)} + className="flex-col gap-1" + style={{ maxWidth: wp('60%') }} + > + Location + - - - Kohalpur-11,Banke - - + + + + {location ? location.address : "Select Location"} + + + + + { navigation.navigate('Notification') }} + className="bg-gray-300 rounded-full flex justify-center items-center" + style={{ width: wp('14%'), height: wp('14%') }} + > + + {unreadCount > 0 && ( + + )} + - { navigation.navigate('Notification') }} className="bg-gray-300 h-14 w-14 rounded-full flex justify-center items-center"> - - - - - {/* header end */} - {/* search bar start */} - - navigation.navigate('Search')} - className="flex-row items-center flex-1 border border-primary px-3 h-14 rounded-full" - > + {/* header end */} + {/* search bar start */} + + - Search products - + + - { navigation.navigate('Filter') }} className="bg-primary h-14 w-14 rounded-full flex justify-center items-center"> + { navigator.navigate('Filter') }} className="bg-primary h-14 w-14 rounded-full flex justify-center items-center"> - {/* search bar start */} - {/* ads component start */} - - - - {Array.from({ length: slider.length }).map((_, index) => { - const isActive = index == page - return ( - - ) - })} - - - {/* ads component end */} - - {/* Pass categories data to the Category component */} - - - {products.length > 0 ? ( - - ) : ( - - No products available - - )} + {/* search bar end */} - + {/* ads component start */} + + + + {Array.from({ length: sliders.length }).map((_, index) => { + const isActive = index == page + return ( + + ) + })} + + + {/* ads component end */} + + {/* Pass categories data to the Category component */} + + + {/* Pass filtered products to FlashSales */} + {filteredProducts.length > 0 ? ( + + ) : ( + + No products available + + )} + + + setIsLocationModalVisible(false)} + onLocationSelected={handleLocationSelected} + /> + ); } diff --git a/frontend/app/screens/InviteFriendsScreen.jsx b/frontend/app/screens/InviteFriendsScreen.jsx index 5ade13e..5cfbee7 100644 --- a/frontend/app/screens/InviteFriendsScreen.jsx +++ b/frontend/app/screens/InviteFriendsScreen.jsx @@ -1,14 +1,111 @@ -import { View, Text, Image, TouchableOpacity } from 'react-native' -import React from 'react' +import { View, Text, Image, TouchableOpacity, ActivityIndicator, Platform } from 'react-native' +import React, { useState, useEffect } from 'react' import TabBar from '../themes/TabBar' import { ChevronLeftIcon } from 'react-native-heroicons/outline' -import { widthPercentageToDP as wp, heightPercentageToDP as hp, heightPercentageToDP } from 'react-native-responsive-screen'; -import usersData from '../constants/usersData'; -import { ScrollView } from 'react-native'; -import { useNavigation } from '@react-navigation/native'; +import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen' +import { ScrollView } from 'react-native' +import { useNavigation } from '@react-navigation/native' +import * as Contacts from 'expo-contacts' +import { Linking } from 'react-native' +import { getInviteMessage } from '../api' export default function InviteFriendsScreen() { const navigation = useNavigation() + const [contacts, setContacts] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [inviteMessage, setInviteMessage] = useState('') + const [inviteLink, setInviteLink] = useState('') + + useEffect(() => { + requestContactsPermission() + fetchInviteMessage() + }, []) + + const fetchInviteMessage = async () => { + try { + const response = await getInviteMessage() + setInviteMessage(response.message) + setInviteLink(response.link) + } catch (err) { + console.error('Error fetching invite message:', err) + setInviteMessage("Hey! I've been shopping on this Shopping App and it's been amazing! They have awesome deals, fast delivery, and quality products. Use my link to sign up and get a special discount on your first order!") + setInviteLink("https://play.google.com/store/apps/details?id=com.vendora.app") + } + } + + const requestContactsPermission = async () => { + try { + const { status } = await Contacts.requestPermissionsAsync() + if (status === 'granted') { + fetchContacts() + } else { + setError('Permission to access contacts was denied') + setLoading(false) + } + } catch (err) { + setError('Failed to request contacts permission') + setLoading(false) + } + } + + const fetchContacts = async () => { + try { + const { data } = await Contacts.getContactsAsync({ + fields: [Contacts.Fields.PhoneNumbers, Contacts.Fields.Name], + }) + + // Filter out contacts without phone numbers and sort by name + const filteredContacts = data + .filter(contact => contact.phoneNumbers && contact.phoneNumbers.length > 0) + .sort((a, b) => a.name.localeCompare(b.name)) + + setContacts(filteredContacts) + setLoading(false) + } catch (err) { + console.error('Error fetching contacts:', err) + setError('Failed to fetch contacts') + setLoading(false) + } + } + + const handleInvite = async (phoneNumber) => { + try { + const messageWithLink = `${inviteMessage}\n\n${inviteLink}` + const url = `https://wa.me/${phoneNumber.replace(/\D/g, '')}?text=${encodeURIComponent(messageWithLink)}` + + const supported = await Linking.canOpenURL(url) + if (supported) { + await Linking.openURL(url) + } else { + console.error('WhatsApp is not installed') + } + } catch (error) { + console.error('Error sending invite:', error) + } + } + + if (loading) { + return ( + + + + ) + } + + if (error) { + return ( + + {error} + + Try Again + + + ) + } return ( @@ -32,23 +129,28 @@ export default function InviteFriendsScreen() { containerStyle="" suffixAction={() => navigation.goBack()} /> - - { - usersData.map((item, index) => { - return ( - - - - {item.name} - {item.num} - - - Invite - - - ) - }) - } + + {contacts.map((contact, index) => ( + + + + {contact.name.charAt(0).toUpperCase()} + + + + {contact.name} + + {contact.phoneNumbers[0].number} + + + handleInvite(contact.phoneNumbers[0].number)} + > + Invite + + + ))} ) diff --git a/frontend/app/screens/JacketScreen.jsx b/frontend/app/screens/JacketScreen.jsx index d057193..017616f 100644 --- a/frontend/app/screens/JacketScreen.jsx +++ b/frontend/app/screens/JacketScreen.jsx @@ -2,6 +2,7 @@ import { View, Text, Image, ActivityIndicator } from 'react-native' import React, { useState, useEffect } from 'react' import TabBar from '../themes/TabBar' import { ChevronLeftIcon, HeartIcon } from 'react-native-heroicons/outline' +import { HeartIcon as SolidHeartIcon } from 'react-native-heroicons/solid' import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import MasonryList from '@react-native-seoul/masonry-list'; import { TouchableOpacity } from 'react-native'; @@ -32,12 +33,36 @@ export default function JacketScreen() { const getProducts = async () => { try { setLoading(true); - const data = await fetchProductsByCategory(categoryId); - setProducts(data); setError(null); + + // Validate categoryId + if (!categoryId) { + throw new Error('Category ID is required'); + } + + // Ensure categoryId is a number + const parsedCategoryId = parseInt(categoryId); + if (isNaN(parsedCategoryId)) { + throw new Error('Category ID must be a valid number'); + } + + console.log('Fetching products for category:', parsedCategoryId); + const data = await fetchProductsByCategory(parsedCategoryId); + + // Validate response data + if (!data) { + throw new Error('No data received from server'); + } + + if (!Array.isArray(data)) { + throw new Error('Invalid response format from server'); + } + + setProducts(data); } catch (error) { - console.error('Error fetching products by category:', error); + console.error('Error fetching products:', error); setError(error.message || 'Failed to fetch products'); + setProducts([]); } finally { setLoading(false); } @@ -48,61 +73,40 @@ export default function JacketScreen() { if (loading) { return ( - - + + + Loading products... ); } if (error) { return ( - + {error} { - setLoading(true); - fetchProductsByCategory(categoryId) - .then(data => { - setProducts(data); - setError(null); - }) - .catch(err => setError(err.message || 'Failed to fetch products')) - .finally(() => setLoading(false)); - }} - className="mt-4 bg-primary py-2 px-4 rounded-lg" + onPress={() => navigation.goBack()} + className="mt-4 bg-blue-500 px-4 py-2 rounded-lg" > - Retry + Go Back ); } return ( - - } - titleStyle={{ - color: "black", - fontWeight: "bold", - fontSize: 20 - }} - prefixStyle={{ - backgroundColor: "transparent" - }} - suffixStyle={{ - backgroundColor: "white", - borderWidth: 1, - borderColor: "black" - }} - containerStyle="" - suffixAction={() => navigation.goBack()} - /> - + + + navigation.goBack()}> + + + {title} + + + {products.length === 0 ? ( - - No products found in this category + + No products found in this category ) : ( item.id.toString()} numColumns={2} showsVerticalScrollIndicator={false} - renderItem={({ item, index }) => ( - + renderItem={({ item }) => ( + navigation.navigate('Product', { product: item })} + > + + + + {item.name} + ${item.price} + + + {item.average_rating || 0} + + + toggleFavorite(item.id)} + > + {favorites[item.id] ? ( + + ) : ( + + )} + + + )} - className="mx-4" /> )} - - ) -} - -const CardItem = ({ item, index, toggleFavorite, isFavorite, navigation }) => { - let isEven = index % 2 === 0; - - return ( - - { - navigation.navigate('ProductDetails', { id: item.id }) - }} - className="relative" - > - - toggleFavorite(item.id)} - className="absolute right-4 top-4 bg-white/50 p-2 rounded-full" - > - {isFavorite ? ( - - ) : ( - - )} - - - - - {item.name} - - - {item.average_rating || 0} - - - - Rs {item.price} - + - ) + ); } diff --git a/frontend/app/screens/LocationScreen.jsx b/frontend/app/screens/LocationScreen.jsx index aa6f938..0c36af8 100644 --- a/frontend/app/screens/LocationScreen.jsx +++ b/frontend/app/screens/LocationScreen.jsx @@ -4,18 +4,21 @@ import TabBar from '../themes/TabBar' import { ChevronLeftIcon, MagnifyingGlassIcon, XCircleIcon } from 'react-native-heroicons/outline' import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import Icon from 'react-native-vector-icons/Ionicons' +import { useNavigation } from '@react-navigation/native'; export default function LocationScreen() { + const navigation = useNavigation(); + return ( } + suffix={} titleStyle={{ color: "black", fontWeight: "bold", - fontSize: 20 + fontSize: wp('5%') }} prefixStyle={{ backgroundColor: "transparent" @@ -29,26 +32,30 @@ export default function LocationScreen() { suffixAction={() => navigation.goBack()} /> - - - - + + + + - + - - Use my current location + + Use my current location - S E A R C H R E S U L T + S E A R C H R E S U L T - - - Golden Avenue + + + Golden Avenue - 8502 Preston Rd. Ingl... + 8502 Preston Rd. Ingl... ) } \ No newline at end of file diff --git a/frontend/app/screens/MyCartScreen.jsx b/frontend/app/screens/MyCartScreen.jsx index e7453a0..3af7ff5 100644 --- a/frontend/app/screens/MyCartScreen.jsx +++ b/frontend/app/screens/MyCartScreen.jsx @@ -1,12 +1,13 @@ -import { View, Text, ScrollView, Image, TextInput, TouchableOpacity, Pressable, ActivityIndicator, Alert, RefreshControl } from 'react-native' +import { View, Text, ScrollView, Image, TextInput, TouchableOpacity, Pressable, ActivityIndicator, Alert, RefreshControl, KeyboardAvoidingView, Platform } from 'react-native' import React, { useEffect, useState, useCallback } from 'react' import TabBar from '../themes/TabBar' -import { ChevronLeftIcon, HeartIcon, MinusIcon, PlusIcon } from 'react-native-heroicons/outline' +import { ChevronLeftIcon, HeartIcon, MinusIcon, PlusIcon, XMarkIcon } from 'react-native-heroicons/outline' import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import { useNavigation, useFocusEffect } from '@react-navigation/native' import { deleteCartItem, getCart, updateCartItemQuantity, processImageUrl } from '../api' import EvIcons from "react-native-vector-icons/EvilIcons" import AsyncStorage from '@react-native-async-storage/async-storage'; +import { useCart } from '../context/CartContext'; export default function MyCartScreen() { const navigation = useNavigation(); @@ -16,6 +17,7 @@ export default function MyCartScreen() { const [refreshing, setRefreshing] = useState(false); const [promoCode, setPromoCode] = useState(''); const [isAuthenticated, setIsAuthenticated] = useState(false); + const { cartTotal, appliedCoupon, couponDiscount, applyCoupon, removeCoupon, refreshCart } = useCart(); // Check authentication status const checkAuth = async () => { @@ -63,21 +65,33 @@ export default function MyCartScreen() { } } + // Get the price from the correct field + const price = typeof item.product_price === 'number' ? item.product_price : + typeof item.price === 'number' ? item.price : + parseFloat(item.product_price || item.price || 0); + + // Calculate subtotal based on price and quantity + const quantity = parseInt(item.quantity || 1); + const subtotal = price * quantity; + // Ensure all required properties exist return { id: item.id, product_id: item.product, product_name: item.product_name, product_image: processedImageUrl, - price: parseFloat(item.product_price || 0), - quantity: parseInt(item.quantity || 1), - subtotal: parseFloat(item.subtotal || 0) + price: price, + quantity: quantity, + subtotal: subtotal }; })); + // Calculate total from processed items + const total = processedItems.reduce((sum, item) => sum + item.subtotal, 0); + return { items: processedItems, - total: parseFloat(cartData.total || 0) + total: total }; }; @@ -112,6 +126,13 @@ export default function MyCartScreen() { const handleUpdateQuantity = async (itemId, newQuantity) => { if (newQuantity < 1) return; + // Find the cart item to get the product ID + const cartItem = cartItems.find(item => item.id === itemId); + if (!cartItem) { + Alert.alert('Error', 'Cart item not found'); + return; + } + // Optimistic update setCartItems(prevItems => prevItems.map(item => @@ -120,13 +141,43 @@ export default function MyCartScreen() { ); try { - await updateCartItemQuantity(itemId, newQuantity); + await updateCartItemQuantity(cartItem.product, newQuantity); // Refresh cart data in background fetchCartData(); } catch (error) { console.error('Error updating quantity:', error); // Revert optimistic update on error fetchCartData(); + + // Handle different types of errors + if (error.message === 'Session expired. Please login again.') { + Alert.alert( + 'Session Expired', + 'Your session has expired. Please login again.', + [ + { + text: 'OK', + onPress: () => navigation.navigate('SignIn') + } + ] + ); + } else if (error.message === 'You do not have permission to update this cart item.') { + Alert.alert( + 'Permission Error', + 'You do not have permission to update this cart item. Please try again later.' + ); + } else if (error.message === 'Cart item not found') { + Alert.alert( + 'Error', + 'This item is no longer in your cart. The cart will be refreshed.' + ); + fetchCartData(); // Refresh the cart to get the current state + } else { + Alert.alert( + 'Error', + 'Failed to update quantity. Please try again.' + ); + } } }; @@ -173,18 +224,50 @@ export default function MyCartScreen() { try { setLoading(true); - // This would normally call an API endpoint to apply the promo code - // For now, just show a message - Alert.alert('Success', 'Promo code applied successfully!'); - setPromoCode(''); + const result = await applyCoupon(promoCode); + + if (result.success) { + Alert.alert('Success', `Coupon applied! You saved ${result.discount.toFixed(2)}`); + setPromoCode(''); + } else { + // Display the specific error message from the server + Alert.alert('Invalid Coupon', result.message || 'This coupon cannot be applied to your cart.'); + } } catch (error) { console.error('Error applying promo code:', error); - Alert.alert('Error', error.message || 'Failed to apply promo code'); + // Check if the error has a response with a message + const errorMessage = error.response?.data?.message || error.message || 'Failed to apply promo code'; + Alert.alert('Error', errorMessage); } finally { setLoading(false); } }; + const handleRemoveCoupon = async () => { + try { + setLoading(true); + + // Remove the coupon from context + removeCoupon(); + + // Clear the promo code input + setPromoCode(''); + + // Fetch fresh cart data and wait for it to complete + await fetchCartData(); + + // Force a refresh of the cart context + refreshCart(); + + setLoading(false); + Alert.alert('Success', 'Coupon removed successfully'); + } catch (error) { + console.error('Error removing coupon:', error); + setLoading(false); + Alert.alert('Error', 'Failed to remove coupon. Please try again.'); + } + }; + // Error banner component for non-blocking errors const ErrorBanner = ({ message, onDismiss }) => ( @@ -195,86 +278,92 @@ export default function MyCartScreen() { ); - const CartItem = ({ item, onUpdateQuantity, onDelete }) => { - const [imageError, setImageError] = useState(false); - const [imageLoading, setImageLoading] = useState(true); - - // Use local fallback image if no image URL or error - const imageSource = !item.product_image || imageError - ? require('../../assets/images/favicon.png') - : { uri: item.product_image, cache: 'force-cache' }; + // Move CartItem outside of the main component and memoize it + const CartItem = React.memo(({ item, onUpdateQuantity, onDelete }) => { + // Calculate the price, ensuring it's a number + const price = typeof item.price === 'number' ? item.price : + typeof item.product_price === 'number' ? item.product_price : + parseFloat(item.price || item.product_price || 0); return ( - - + + { - console.error('Error loading image for product:', item.product_image); - console.error('Error details:', e.nativeEvent); - setImageError(true); - }} - onLoadStart={() => { - if (item.product_image) { - console.log('Loading image:', item.product_image); - setImageLoading(true); - } - }} - onLoadEnd={() => { - if (item.product_image) { - console.log('Image loaded successfully:', item.product_image); - setImageLoading(false); - } - }} - onLoad={() => { - if (item.product_image) { - console.log('Image loaded and rendered:', item.product_image); - setImageLoading(false); - } + width: wp('20%'), + height: wp('20%'), + resizeMode: 'cover', + borderRadius: wp('2%') }} /> - {imageLoading && item.product_image && ( - - - - )} - - {item.product_name} - - ${typeof item.price === 'number' ? item.price.toFixed(2) : '0.00'} + + {item.product_name} + + Rs {price.toFixed(2)} - + onUpdateQuantity(item.id, item.quantity - 1)} - className="bg-gray-200 p-2 rounded-full" + style={{ + backgroundColor: '#e5e7eb', + padding: wp('2%'), + borderRadius: 9999 + }} > - + - {item.quantity} + {item.quantity} onUpdateQuantity(item.id, item.quantity + 1)} - className="bg-gray-200 p-2 rounded-full" + style={{ + backgroundColor: '#e5e7eb', + padding: wp('2%'), + borderRadius: 9999 + }} > - + onDelete(item.id)} - className="ml-auto" + style={{ marginLeft: 'auto' }} > - + ); - }; + }, (prevProps, nextProps) => { + // Only re-render if these props change + return ( + prevProps.item.id === nextProps.item.id && + prevProps.item.quantity === nextProps.item.quantity && + prevProps.item.product_image === nextProps.item.product_image + ); + }); if (loading && !refreshing) { return ( @@ -313,14 +402,18 @@ export default function MyCartScreen() { return total + (isNaN(itemSubtotal) ? 0 : itemSubtotal); }, 0); const discount = (subtotal * discountPercentage) / 100; - const totalCost = subtotal + deliveryFee - discount; + const totalCost = subtotal + deliveryFee - discount - couponDiscount; return ( - + } + suffix={} titleStyle={{ color: "black", fontWeight: "bold", @@ -348,12 +441,21 @@ export default function MyCartScreen() { {!cartItems || cartItems.length === 0 ? ( - Your cart is empty - navigation.navigate('Home')} - className="mt-4 bg-primary py-2 px-4 rounded-lg" + Your cart is empty + { + // Navigate to Main (BottomTabBar) and then to Home tab + navigation.goBack(); + }} + style={{ + marginTop: hp('2%'), + backgroundColor: '#704f38', + paddingVertical: hp('1%'), + paddingHorizontal: wp('4%'), + borderRadius: wp('2%') + }} > - Continue Shopping + Continue Shopping ) : ( @@ -368,6 +470,7 @@ export default function MyCartScreen() { tintColor="#704f38" /> } + className="flex-1" > {cartItems.map((item, index) => ( ))} - + + + Promo Code + + + + Apply + + + {appliedCoupon && ( + + + + Coupon "{appliedCoupon.code}" applied! You saved {couponDiscount.toFixed(2)} + + + + Remove Coupon + + + )} + - - - + + Order Summary + + + Subtotal + Rs {subtotal.toFixed(2)} + + + + Delivery Fee + Rs {deliveryFee.toFixed(2)} + + + + Discount + - Rs {discount.toFixed(2)} + + + {couponDiscount > 0 && ( + + Coupon Discount + - Rs {couponDiscount.toFixed(2)} + + )} + + + + + Total + Rs {totalCost.toFixed(2)} + + - Apply + Proceed to Checkout - - Sub-Total - ${subtotal.toFixed(2)} - - - Delivery fee - ${deliveryFee.toFixed(2)} - - - Discount - {discountPercentage}% (${discount.toFixed(2)}) - - - - Total Cost - ${totalCost.toFixed(2)} - - - Proceed To Checkout - - + )} - + ); -} \ No newline at end of file +} diff --git a/frontend/app/screens/NotificationScreen.jsx b/frontend/app/screens/NotificationScreen.jsx index 4810a1f..783c37d 100644 --- a/frontend/app/screens/NotificationScreen.jsx +++ b/frontend/app/screens/NotificationScreen.jsx @@ -1,24 +1,159 @@ -import { View, Text } from 'react-native' -import React from 'react' +import { View, Text, ScrollView, RefreshControl, ActivityIndicator } from 'react-native' +import React, { useState, useEffect } from 'react' import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import TabBar from '../themes/TabBar'; -import { ChevronLeftIcon, FolderMinusIcon, StarIcon } from 'react-native-heroicons/outline'; -import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import { + ChevronLeftIcon, + ShoppingCartIcon, + TruckIcon, + CheckCircleIcon, + XCircleIcon, + TagIcon, + BellIcon +} from 'react-native-heroicons/outline'; import { useNavigation } from '@react-navigation/native'; - +import { useNotifications } from '../context/NotificationContext'; const NotificationScreen = () => { - const navigation = useNavigation() + const navigation = useNavigation(); + const { notifications, loading, error, fetchNotifications } = useNotifications(); + const [refreshing, setRefreshing] = useState(false); + const [groupedNotifications, setGroupedNotifications] = useState({ today: [], yesterday: [] }); + + useEffect(() => { + fetchNotifications(); + }, []); + + useEffect(() => { + groupNotifications(); + }, [notifications]); + + const groupNotifications = () => { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + + const todayNotifications = []; + const yesterdayNotifications = []; + + notifications.forEach(notification => { + const notificationDate = new Date(notification.created_at); + notificationDate.setHours(0, 0, 0, 0); + + if (notificationDate.getTime() === today.getTime()) { + todayNotifications.push(notification); + } else if (notificationDate.getTime() === yesterday.getTime()) { + yesterdayNotifications.push(notification); + } + }); + + setGroupedNotifications({ + today: todayNotifications, + yesterday: yesterdayNotifications + }); + }; + + const onRefresh = async () => { + setRefreshing(true); + try { + await fetchNotifications(); + } finally { + setRefreshing(false); + } + }; + + const getNotificationIcon = (type) => { + switch (type) { + case 'order_placed': + return ; + case 'order_shipped': + return ; + case 'order_delivered': + return ; + case 'order_cancelled': + return ; + case 'coupon': + return ; + default: + return ; + } + }; + + const formatTimeAgo = (dateString) => { + const date = new Date(dateString); + const now = new Date(); + const diffInHours = Math.floor((now - date) / (1000 * 60 * 60)); + + if (diffInHours < 1) { + return 'Just now'; + } else if (diffInHours < 24) { + return `${diffInHours}h`; + } else { + const diffInDays = Math.floor(diffInHours / 24); + return `${diffInDays}d`; + } + }; + + const renderNotificationGroup = (notifications, title) => { + if (notifications.length === 0) return null; + + return ( + <> + + {title} + + {notifications.map((notification, index) => ( + + + {getNotificationIcon(notification.type)} + + + + + {notification.type.split('_').map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(' ')} + + {formatTimeAgo(notification.created_at)} + + {notification.message} + + + ))} + + ); + }; + + if (loading && !refreshing) { + return ( + + + + ); + } + + if (error) { + return ( + + {error} + + ); + } + + const unreadCount = notifications.filter(n => !n.read).length; return ( - - 2 - NEW - + unreadCount > 0 ? ( + + {unreadCount} + NEW + + ) : null } title={"Notification "} suffix={} @@ -46,89 +181,25 @@ const NotificationScreen = () => { containerStyle="" suffixAction={() => navigation.goBack()} /> - - - T O D A Y - Mark all as read - - - - + + } + > + {notifications.length === 0 ? ( + + No notifications yet - - - Order Sipped - 1h - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Eius, minus veniam. Veritatis nisi sequi tempore facere. - - - - - - - - - Flash Sale Alert - 1h - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Eius, minus veniam. Veritatis nisi sequi tempore facere. - - - - - - - - - Product Review Request - 1h - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Eius, minus veniam. Veritatis nisi sequi tempore facere. - - - - Y E S T E R D A Y - Mark all as read - - - - - - - - Order Sipped - 1d - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Eius, minus veniam. Veritatis nisi sequi tempore facere. - - - - - - - - - New Paypal Added - 1d - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Eius, minus veniam. Veritatis nisi sequi tempore facere. - - - - - - - - - Flash Sale Alert - 1d - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Eius, minus veniam. Veritatis nisi sequi tempore facere. - - - + ) : ( + <> + {renderNotificationGroup(groupedNotifications.today, "T O D A Y")} + {renderNotificationGroup(groupedNotifications.yesterday, "Y E S T E R D A Y")} + + )} + ) } -export default NotificationScreen \ No newline at end of file + +export default NotificationScreen diff --git a/frontend/app/screens/OrderScreen.jsx b/frontend/app/screens/OrderScreen.jsx index 7f78286..67f7446 100644 --- a/frontend/app/screens/OrderScreen.jsx +++ b/frontend/app/screens/OrderScreen.jsx @@ -1,4 +1,4 @@ -import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator } from 'react-native' +import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, RefreshControl } from 'react-native' import React, { useState, useEffect } from 'react' import TabBar from '../themes/TabBar' import { ChevronLeftIcon } from 'react-native-heroicons/outline' @@ -6,7 +6,7 @@ import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-nat import ActiveOrder from '../components/ActiveOrder' import CompletedOrder from '../components/CompletedOrder' import CancelledOrder from '../components/CancelledOrder' -import { useNavigation } from '@react-navigation/native' +import { useNavigation, useFocusEffect } from '@react-navigation/native' import { getOrders } from '../api' export default function OrderScreen() { @@ -14,13 +14,10 @@ export default function OrderScreen() { const [orders, setOrders] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [refreshing, setRefreshing] = useState(false); const orderState = ["active", "completed", "cancelled"]; const [initialValue, setValue] = useState(0); - useEffect(() => { - fetchOrders(); - }, []); - // Process order data to ensure consistent structure const processOrderData = (orderData) => { if (!Array.isArray(orderData)) { @@ -30,8 +27,8 @@ export default function OrderScreen() { return orderData.map(order => ({ ...order, - // Normalize status to lowercase to avoid case-sensitivity issues - status: (order.status || '').toLowerCase(), + // Keep status as is (case-sensitive) + status: order.status || 'Pending', // Ensure items array exists items: Array.isArray(order.items) ? order.items : [], // Ensure order has an ID @@ -54,8 +51,32 @@ export default function OrderScreen() { } }; + // Initial fetch + useEffect(() => { + fetchOrders(); + }, []); + + // Refresh when screen is focused + useFocusEffect( + React.useCallback(() => { + fetchOrders(); + }, []) + ); + + // Handle pull-to-refresh + const onRefresh = async () => { + setRefreshing(true); + try { + await fetchOrders(); + } catch (error) { + console.error('Error refreshing orders:', error); + } finally { + setRefreshing(false); + } + }; + const renderOrderState = () => { - if (loading) { + if (loading && !refreshing) { return ( @@ -79,16 +100,16 @@ export default function OrderScreen() { // Filter orders based on state const filteredOrders = orders.filter(order => { - const status = order.status.toLowerCase(); + const status = order.status; if (initialValue === 0) { - // Active orders are pending, processing - return ['pending', 'processing'].includes(status); + // Active orders are pending, processing, and shipped + return ['Pending', 'Processing', 'Shipped'].includes(status); } else if (initialValue === 1) { // Completed orders - return ['shipped', 'delivered'].includes(status); + return ['Delivered'].includes(status); } else { // Cancelled orders - return status === 'cancelled'; + return status === 'Cancelled'; } }); @@ -154,7 +175,17 @@ export default function OrderScreen() { } - + + } + > {renderOrderState()} diff --git a/frontend/app/screens/PasswordManagerScreen.jsx b/frontend/app/screens/PasswordManagerScreen.jsx index 0279612..7536030 100644 --- a/frontend/app/screens/PasswordManagerScreen.jsx +++ b/frontend/app/screens/PasswordManagerScreen.jsx @@ -1,12 +1,52 @@ -import { View, Text, TextInput, TouchableOpacity } from 'react-native' -import React from 'react' +import { View, Text, TextInput, TouchableOpacity, Alert, ActivityIndicator } from 'react-native' +import React, { useState } from 'react' import TabBar from '../themes/TabBar' -import { ChevronLeftIcon, EyeSlashIcon } from 'react-native-heroicons/outline' +import { ChevronLeftIcon, EyeSlashIcon, EyeIcon } from 'react-native-heroicons/outline' import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import { useNavigation } from '@react-navigation/native'; +import { updatePassword } from '../api'; export default function PasswordManagerScreen() { - const navigation = useNavigation() + const navigation = useNavigation(); + const [loading, setLoading] = useState(false); + const [showCurrentPassword, setShowCurrentPassword] = useState(false); + const [showNewPassword, setShowNewPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [passwords, setPasswords] = useState({ + currentPassword: '', + newPassword: '', + confirmPassword: '' + }); + + const handlePasswordChange = async () => { + // Validate passwords + if (!passwords.currentPassword || !passwords.newPassword || !passwords.confirmPassword) { + Alert.alert('Error', 'Please fill in all password fields'); + return; + } + + if (passwords.newPassword !== passwords.confirmPassword) { + Alert.alert('Error', 'New passwords do not match'); + return; + } + + if (passwords.newPassword.length < 8) { + Alert.alert('Error', 'New password must be at least 8 characters long'); + return; + } + + try { + setLoading(true); + await updatePassword(passwords.currentPassword, passwords.newPassword); + Alert.alert('Success', 'Password updated successfully'); + navigation.goBack(); + } catch (error) { + console.error('Password update error:', error); + Alert.alert('Error', error.response?.data?.error || 'Failed to update password'); + } finally { + setLoading(false); + } + }; return ( @@ -32,47 +72,86 @@ export default function PasswordManagerScreen() { /> - Current Password + Current Password - - + + setPasswords(prev => ({...prev, currentPassword: text}))} + /> - - - - - - Forgot Passwords ? - - - New Password - - - - - - - - - - - Conform New Password - - - - - - - - - - - - - Change Password + setShowCurrentPassword(!showCurrentPassword)}> + {showCurrentPassword ? ( + + ) : ( + + )} + + + New Password + + + + setPasswords(prev => ({...prev, newPassword: text}))} + /> + + setShowNewPassword(!showNewPassword)}> + {showNewPassword ? ( + + ) : ( + + )} + + + + + Confirm New Password + + + + setPasswords(prev => ({...prev, confirmPassword: text}))} + /> + + setShowConfirmPassword(!showConfirmPassword)}> + {showConfirmPassword ? ( + + ) : ( + + )} + + + + + + + {loading ? ( + + ) : ( + Change Password + )} + + ) } \ No newline at end of file diff --git a/frontend/app/screens/PaymentMethodScreen.jsx b/frontend/app/screens/PaymentMethodScreen.jsx index 01f9368..a9629a8 100644 --- a/frontend/app/screens/PaymentMethodScreen.jsx +++ b/frontend/app/screens/PaymentMethodScreen.jsx @@ -1,17 +1,406 @@ -import { View, Text, Image } from 'react-native' -import React from 'react' +import { View, Text, Image, TouchableOpacity, Alert, ScrollView } from 'react-native' +import React, { useState, useEffect } from 'react' import TabBar from '../themes/TabBar' -import { ChevronLeftIcon, CreditCardIcon } from 'react-native-heroicons/outline' +import { ChevronLeftIcon, CreditCardIcon, CheckCircleIcon, TrashIcon } from 'react-native-heroicons/outline' import { TextInput } from 'react-native' import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import { useNavigation } from '@react-navigation/native' +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { Linking } from 'react-native'; +// Import local assets +const esewaLogo = require('../../assets/images/esewa.jpg'); +const khaltiLogo = require('../../assets/images/khalti.jpg'); +const imePayLogo = require('../../assets/images/imepay.png'); export default function PaymentMethodScreen() { - const navigation = useNavigation() + const navigator = useNavigation() + const [cardNumber, setCardNumber] = useState('') + const [cardName, setCardName] = useState('') + const [expiryDate, setExpiryDate] = useState('') + const [cvv, setCvv] = useState('') + const [paypalEmail, setPaypalEmail] = useState('') + const [paypalPassword, setPaypalPassword] = useState('') + const [esewaPhone, setEsewaPhone] = useState('') + const [esewaPin, setEsewaPin] = useState('') + const [khaltiPhone, setKhaltiPhone] = useState('') + const [khaltiPin, setKhaltiPin] = useState('') + const [imePhone, setImePhone] = useState('') + const [imePin, setImePin] = useState('') + const [showCardDetails, setShowCardDetails] = useState(false) + const [showPaypalDetails, setShowPaypalDetails] = useState(false) + const [showApplePayDetails, setShowApplePayDetails] = useState(false) + const [showEsewaDetails, setShowEsewaDetails] = useState(false) + const [showKhaltiDetails, setShowKhaltiDetails] = useState(false) + const [showImeDetails, setShowImeDetails] = useState(false) + const [linkedMethods, setLinkedMethods] = useState({ + card: false, + paypal: false, + applePay: false, + esewa: false, + khalti: false, + ime: false + }) + + // Load saved payment methods on component mount + useEffect(() => { + loadSavedPaymentMethods(); + }, []); + + const loadSavedPaymentMethods = async () => { + try { + const savedMethods = await AsyncStorage.getItem('linkedPaymentMethods'); + if (savedMethods) { + setLinkedMethods(JSON.parse(savedMethods)); + } + } catch (error) { + console.error('Error loading saved payment methods:', error); + } + }; + + const saveLinkedMethods = async (methods) => { + try { + await AsyncStorage.setItem('linkedPaymentMethods', JSON.stringify(methods)); + } catch (error) { + console.error('Error saving payment methods:', error); + } + }; + + const handleUnlink = (method) => { + Alert.alert( + 'Unlink Account', + 'Are you sure you want to unlink this account?', + [ + { + text: 'Cancel', + style: 'cancel' + }, + { + text: 'Unlink', + style: 'destructive', + onPress: () => { + const updatedMethods = { ...linkedMethods, [method]: false }; + setLinkedMethods(updatedMethods); + saveLinkedMethods(updatedMethods); + } + } + ] + ) + } + + const handleCardLink = () => { + if (!cardNumber.trim() || !cardName.trim() || !expiryDate.trim() || !cvv.trim()) { + Alert.alert('Error', 'Please fill in all card details') + return + } + // Here you would typically validate the card details + // For now, we'll just show a success message + Alert.alert('Success', 'Card linked successfully', [ + { + text: 'OK', + onPress: () => { + setShowCardDetails(false) + setCardNumber('') + setCardName('') + setExpiryDate('') + setCvv('') + const updatedMethods = { ...linkedMethods, card: true }; + setLinkedMethods(updatedMethods); + saveLinkedMethods(updatedMethods); + } + } + ]) + } + + const handlePaypalLink = () => { + if (!paypalEmail.trim() || !paypalPassword.trim()) { + Alert.alert('Error', 'Please fill in all PayPal details') + return + } + // Here you would typically validate the PayPal credentials + Alert.alert('Success', 'PayPal account linked successfully', [ + { + text: 'OK', + onPress: () => { + setShowPaypalDetails(false) + setPaypalEmail('') + setPaypalPassword('') + const updatedMethods = { ...linkedMethods, paypal: true }; + setLinkedMethods(updatedMethods); + saveLinkedMethods(updatedMethods); + } + } + ]) + } + + const handleApplePayLink = () => { + // Apple Pay typically uses the device's built-in authentication + Alert.alert('Success', 'Apple Pay account linked successfully', [ + { + text: 'OK', + onPress: () => { + setShowApplePayDetails(false) + const updatedMethods = { ...linkedMethods, applePay: true }; + setLinkedMethods(updatedMethods); + saveLinkedMethods(updatedMethods); + } + } + ]) + } + + const handleEsewaLink = () => { + if (!esewaPhone.trim() || !esewaPin.trim()) { + Alert.alert('Error', 'Please fill in all eSewa details') + return + } + // Here you would typically validate the eSewa credentials + Alert.alert('Success', 'eSewa account linked successfully', [ + { + text: 'OK', + onPress: () => { + setShowEsewaDetails(false) + setEsewaPhone('') + setEsewaPin('') + const updatedMethods = { ...linkedMethods, esewa: true }; + setLinkedMethods(updatedMethods); + saveLinkedMethods(updatedMethods); + } + } + ]) + } + + const handleKhaltiLink = () => { + if (!khaltiPhone.trim() || !khaltiPin.trim()) { + Alert.alert('Error', 'Please fill in all Khalti details') + return + } + // Here you would typically validate the Khalti credentials + Alert.alert('Success', 'Khalti account linked successfully', [ + { + text: 'OK', + onPress: () => { + setShowKhaltiDetails(false) + setKhaltiPhone('') + setKhaltiPin('') + const updatedMethods = { ...linkedMethods, khalti: true }; + setLinkedMethods(updatedMethods); + saveLinkedMethods(updatedMethods); + } + } + ]) + } + + const handleImeLink = () => { + if (!imePhone.trim() || !imePin.trim()) { + Alert.alert('Error', 'Please fill in all IME Pay details') + return + } + // Here you would typically validate the IME Pay credentials + Alert.alert('Success', 'IME Pay account linked successfully', [ + { + text: 'OK', + onPress: () => { + setShowImeDetails(false) + setImePhone('') + setImePin('') + const updatedMethods = { ...linkedMethods, ime: true }; + setLinkedMethods(updatedMethods); + saveLinkedMethods(updatedMethods); + } + } + ]) + } + + const openEsewaApp = async () => { + try { + const esewaUrl = 'esewa://'; + const supported = await Linking.canOpenURL(esewaUrl); + + if (supported) { + await Linking.openURL(esewaUrl); + } else { + Alert.alert( + 'eSewa App Not Found', + 'Please install eSewa app to continue', + [ + { + text: 'Install', + onPress: () => Linking.openURL('https://play.google.com/store/apps/details?id=com.f1soft.esewa&hl=en') + }, + { + text: 'Cancel', + style: 'cancel' + } + ] + ); + } + } catch (error) { + Alert.alert('Error', 'Could not open eSewa app'); + } + }; + + const openKhaltiApp = async () => { + try { + const khaltiUrl = 'khalti://'; + const supported = await Linking.canOpenURL(khaltiUrl); + + if (supported) { + await Linking.openURL(khaltiUrl); + } else { + Alert.alert( + 'Khalti App Not Found', + 'Please install Khalti app to continue', + [ + { + text: 'Install', + onPress: () => Linking.openURL('https://play.google.com/store/apps/details?id=com.khalti&hl=en') + }, + { + text: 'Cancel', + style: 'cancel' + } + ] + ); + } + } catch (error) { + Alert.alert('Error', 'Could not open Khalti app'); + } + }; + + const openImeApp = async () => { + try { + const imeUrl = 'imepay://'; + const supported = await Linking.canOpenURL(imeUrl); + + if (supported) { + await Linking.openURL(imeUrl); + } else { + Alert.alert( + 'IME Pay App Not Found', + 'Please install IME Pay app to continue', + [ + { + text: 'Install', + onPress: () => Linking.openURL('https://play.google.com/store/apps/details?id=com.swifttechnology.imepay&hl=en') + }, + { + text: 'Cancel', + style: 'cancel' + } + ] + ); + } + } catch (error) { + Alert.alert('Error', 'Could not open IME Pay app'); + } + }; + + const renderLinkStatus = (isLinked) => { + if (isLinked) { + return ( + + + Linked + + ) + } + return Link + } + + const renderLinkedCardDetails = () => ( + + + Card Details + handleUnlink('card')}> + + + + Card ending in 1234 + Expires: 12/25 + John Doe + + ) + + const renderLinkedPaypalDetails = () => ( + + + PayPal Account + handleUnlink('paypal')}> + + + + user@example.com + + ) + + const renderLinkedApplePayDetails = () => ( + + + Apple Pay + handleUnlink('applePay')}> + + + + Using device's Apple Pay + + ) + + const renderLinkedEsewaDetails = () => ( + + + eSewa Account + handleUnlink('esewa')}> + + + + Phone: 98XXXXXXXX + + Open eSewa App + + + ) + + const renderLinkedKhaltiDetails = () => ( + + + Khalti Account + handleUnlink('khalti')}> + + + + Phone: 98XXXXXXXX + + Open Khalti App + + + ) + + const renderLinkedImeDetails = () => ( + + + IME Pay Account + handleUnlink('ime')}> + + + + Phone: 98XXXXXXXX + + Open IME Pay App + + + ) return ( - + navigation.goBack()} + suffixAction={() => navigator.goBack()} /> Credit & Debit Card - - + setShowCardDetails(!showCardDetails)} + className={`mx-4 flex-row items-center mt-2 justify-between p-2 border border-gray-300 rounded-2xl`} + > + - + Add New Card - - Link + {renderLinkStatus(linkedMethods.card)} + + + {showCardDetails && !linkedMethods.card && ( + + + + + + + + + Link Card + - + )} + + {showCardDetails && linkedMethods.card && renderLinkedCardDetails()} + More Payment Options - + setShowPaypalDetails(!showPaypalDetails)} + className={`flex-row items-center mt-2 justify-between p-4 border-b border-gray-300 gap-4`} + > Paypal - Link + {renderLinkStatus(linkedMethods.paypal)} + + + {showPaypalDetails && !linkedMethods.paypal && ( + + + + + Link PayPal + - - + )} + + {showPaypalDetails && linkedMethods.paypal && renderLinkedPaypalDetails()} + + setShowApplePayDetails(!showApplePayDetails)} + className={`flex-row items-center justify-between p-4 border-b border-gray-300 gap-4`} + > + Apple Pay - Link + {renderLinkStatus(linkedMethods.applePay)} + + + {showApplePayDetails && !linkedMethods.applePay && ( + + + Apple Pay will use your device's built-in authentication to securely link your account. + + + Link Apple Pay + + + )} + + {showApplePayDetails && linkedMethods.applePay && renderLinkedApplePayDetails()} + + setShowEsewaDetails(!showEsewaDetails)} + className={`flex-row items-center justify-between p-4 gap-4`} + > + + eSewa + {renderLinkStatus(linkedMethods.esewa)} + + + {showEsewaDetails && !linkedMethods.esewa && ( + + + + + Link eSewa + + + Open eSewa App + + + )} + + {showEsewaDetails && linkedMethods.esewa && renderLinkedEsewaDetails()} + + setShowKhaltiDetails(!showKhaltiDetails)} + className={`flex-row items-center justify-between p-4 border-b border-gray-300 gap-4`} + > + + Khalti + {renderLinkStatus(linkedMethods.khalti)} + + + {showKhaltiDetails && !linkedMethods.khalti && ( + + + + + Link Khalti + + + Open Khalti App + + + )} + + {showKhaltiDetails && linkedMethods.khalti && renderLinkedKhaltiDetails()} + + setShowImeDetails(!showImeDetails)} + className={`flex-row items-center justify-between p-4 gap-4`} + > + + IME Pay + {renderLinkStatus(linkedMethods.ime)} + + + {showImeDetails && !linkedMethods.ime && ( + + + + + Link IME Pay + + + Open IME Pay App + + )} + + {showImeDetails && linkedMethods.ime && renderLinkedImeDetails()} - + ) -} \ No newline at end of file +} diff --git a/frontend/app/screens/PaymentSuccess.jsx b/frontend/app/screens/PaymentSuccess.jsx index 03d03be..38620ee 100644 --- a/frontend/app/screens/PaymentSuccess.jsx +++ b/frontend/app/screens/PaymentSuccess.jsx @@ -9,11 +9,20 @@ import { useNavigation } from '@react-navigation/native'; export default function PaymentSuccess() { const navigation = useNavigation() + const handleBack = () => { + // Navigate to Main screen instead of going back + navigation.goBack(); + }; + + const handleViewOrder = () => { + navigation.navigate('Order'); + }; + return ( } titleStyle={{ color: "black", @@ -29,7 +38,7 @@ export default function PaymentSuccess() { borderColor: "black" }} containerStyle="" - suffixAction={() => navigation.goBack()} + suffixAction={handleBack} /> @@ -37,21 +46,20 @@ export default function PaymentSuccess() { - Payment Successful ! - Thank you for your purchase. + Order Successful! + + Thank you for your order. Your order has been placed successfully and will be processed soon. + - - - - - {navigation.navigate('Order')}} className={` rounded-full bg-primary flex-row justify-center items-center `} style={{ height: hp('5%') }}> - View Order + + + View Order - - - View E-Reciept - + ) } \ No newline at end of file diff --git a/frontend/app/screens/PrivacyPolicyScreen.jsx b/frontend/app/screens/PrivacyPolicyScreen.jsx index e7b0f89..7ada0e8 100644 --- a/frontend/app/screens/PrivacyPolicyScreen.jsx +++ b/frontend/app/screens/PrivacyPolicyScreen.jsx @@ -1,12 +1,58 @@ -import { View, Text, ScrollView } from 'react-native' -import React from 'react' +import { View, Text, ScrollView, ActivityIndicator } from 'react-native' +import React, { useEffect, useState } from 'react' import TabBar from '../themes/TabBar' import { ChevronLeftIcon } from 'react-native-heroicons/outline' -import { widthPercentageToDP as wp, heightPercentageToDP as hp, heightPercentageToDP } from 'react-native-responsive-screen'; +import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import { useNavigation } from '@react-navigation/native'; +import { getPrivacyPolicy } from '../api'; export default function PrivacyPolicyScreen() { - const navigation = useNavigation() + const navigation = useNavigation(); + const [policies, setPolicies] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchPrivacyPolicies(); + }, []); + + const fetchPrivacyPolicies = async () => { + try { + setLoading(true); + const data = await getPrivacyPolicy(); + setPolicies(data); + } catch (err) { + setError('Failed to load privacy policies'); + console.error('Error fetching privacy policies:', err); + } finally { + setLoading(false); + } + }; + + const renderContent = (content) => { + // Split content into sections based on numbered points + const sections = content.split(/\d+\./).filter(section => section.trim()); + + return sections.map((section, index) => { + const lines = section.split('\n').filter(line => line.trim()); + return ( + + {lines.map((line, lineIndex) => { + // Check if line is a sub-point (starts with spaces) + const isSubPoint = line.trim().startsWith('-') || line.trim().startsWith('•'); + return ( + + {line.trim()} + + ); + })} + + ); + }); + }; return ( @@ -31,30 +77,25 @@ export default function PrivacyPolicyScreen() { suffixAction={() => navigation.goBack()} /> - - Cancelation Policy - Lorem ipsum dolor sit amet consectetur, adipisicing elit. Ratione, non qui facilis minima iusto maxime. Architecto eos, culpa, - soluta minus quam quis quod accusantium nam quo nisi voluptatibus? Sunt enim error, illum voluptate mollitia, ipsum porro voluptas ullam earum, doloribus obcaecati. - Illum sapiente repellat dolor ex, modi eum aliquam sed corporis delectus architecto tempore itaque dolore exercitationem odio nemo voluptate praesentium aliquid - unde soluta eaque natus illo! Qui, aperiam saepe! - Terms and Condition - Lorem ipsum, dolor sit amet consectetur adipisicing elit. Tempore quibusdam perspiciatis nemo quis? Officia eius temporibus cumque rem aut optio inventore, - fuga incidunt, deserunt, aperiam nihil quo nemo impedit reiciendis eaque facilis fugit. Consequuntur vero amet ratione illum expedita ipsum aperiam nam eveniet aut - at illo earum temporibus, voluptatem facere adipisci iure hic cum ex labore dolore deleniti doloremque quibusdam? Dicta, itaque quisquam blanditiis eum praesentium - ab deleniti cumque. Voluptate quam quis reiciendis dolor nesciunt culpa. Voluptatum temporibus aliquid quis, magni sapiente optio error quasi in quae et illo labore - cumque minima quibusdam? Blanditiis optio alias distinctio similique consequatur inventore?Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae - veniam quam architecto repellat quia ullam distinctio corporis amet, pariatur est? Atque libero nisi, magni esse corporis neque unde! Quo ipsum inventore accusantium - quas beatae animi nobis vel repellat, porro saepe debitis temporibus sunt corrupti reprehenderit doloribus molestiae odit perspiciatis eveniet dolore culpa unde distinctio - pariatur? Mollitia blanditiis minima ex repellat beatae illo non doloribus. Voluptatum qui magnam quibusdam et architecto debitis officiis adipisci provident veritatis iste. - Molestiae hic ea obcaecati nemo neque dolor sequi aliquid quos provident cumque facilis repellendus odit optio facere illo, eum blanditiis quo voluptatem necessitatibus nisi? - Fugit repellat fugiat minima totam animi natus inventore asperiores perferendis numquam cumque, tenetur magnam, sequi eveniet sed laudantium et earum quidem beatae aliquam provident - nobis! Rerum distinctio aperiam soluta nam aliquid sit, maiores vel deleniti repudiandae. Provident dicta possimus dolorem! Debitis nisi, numquam deserunt sunt nesciunt facere aperiam - optio earum assumenda commodi, repudiandae alias rerum quod magni quis, iure odio cupiditate impedit amet doloremque quas! Ipsum neque architecto vero, quidem mollitia ab omnis earum - expedita officiis atque molestias quisquam ipsam aut eligendi harum, nostrum ullam provident, eius corporis reiciendis consequuntur corrupti nemo dolores optio. Delectus omnis voluptatem - quasi expedita ea. + + {loading ? ( + + ) : error ? ( + {error} + ) : policies.length > 0 ? ( + policies.map((policy, index) => ( + + + {policy.title} + + {renderContent(policy.content)} + + )) + ) : ( + No privacy policies available + )} - - ) + ); } \ No newline at end of file diff --git a/frontend/app/screens/ProductDetails.jsx b/frontend/app/screens/ProductDetails.jsx deleted file mode 100644 index 4e8116b..0000000 --- a/frontend/app/screens/ProductDetails.jsx +++ /dev/null @@ -1,466 +0,0 @@ -import { View, Text, TouchableOpacity, Image, ScrollView, ActivityIndicator, TextInput, Alert, RefreshControl } from 'react-native' -import React, { useEffect, useState, useCallback } from 'react' -import { ArrowLeftIcon, HeartIcon, ShoppingBagIcon, StarIcon as StarOutlineIcon } from 'react-native-heroicons/outline' -// import { ImageBackground } from 'react-native' -import pic from "../../assets/images/pic.jpg" -import clothColor from "../constants/clothColor" -import { StarIcon } from 'react-native-heroicons/solid'; -import { useNavigation, useRoute } from '@react-navigation/native'; -import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; -import { addToCart, BASE_URL, fetchProductById, processImageUrl, rateProduct, getCurrentUser } from '../api' - - -export default function ProductDetails() { - const route = useRoute(); - const { id } = route.params; - const navigation = useNavigation(); - const [product, setProduct] = useState(null); - const [productImageUrl, setProductImageUrl] = useState(null); - const [ratings, setRatings] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [quantity, setQuantity] = useState(1); - const [addingToCart, setAddingToCart] = useState(false); - const [userRating, setUserRating] = useState(0); - const [ratingComment, setRatingComment] = useState(''); - const [submittingRating, setSubmittingRating] = useState(false); - const [showRatingForm, setShowRatingForm] = useState(false); - const [refreshing, setRefreshing] = useState(false); - const [editingRating, setEditingRating] = useState(null); - const [editRatingValue, setEditRatingValue] = useState(0); - const [editComment, setEditComment] = useState(''); - const [editing, setEditing] = useState(false); - const [currentUser, setCurrentUser] = useState(null); - - useEffect(() => { - fetchProductData(); - fetchCurrentUser(); - }, [id]); - - const fetchProductData = async () => { - try { - setLoading(true); - const productData = await fetchProductById(id); - setProduct(productData); - - // Process the image URL - if (productData?.image) { - const processedUrl = await processImageUrl(productData.image); - setProductImageUrl(processedUrl); - } - - // Set ratings if available - if (productData.ratings) { - setRatings(productData.ratings); - } - - setError(null); - } catch (error) { - console.error('Error fetching product details:', error); - setError(error.message || 'Failed to load product details'); - } finally { - setLoading(false); - setRefreshing(false); - } - }; - - const fetchCurrentUser = async () => { - try { - const user = await getCurrentUser(); - setCurrentUser(user); - } catch (error) { - console.error('Error fetching current user:', error); - } - }; - - const onRefresh = useCallback(() => { - setRefreshing(true); - fetchProductData(); - }, []); - - const handleAddToCart = async () => { - if (!product) return; - - try { - setAddingToCart(true); - await addToCart(product.id, quantity); - Alert.alert("Success", "Product added to cart successfully"); - navigation.navigate("MyCart"); - } catch (error) { - console.error('Error adding to cart:', error); - setError(error.message || 'Failed to add item to cart'); - Alert.alert("Error", "Failed to add product to cart"); - } finally { - setAddingToCart(false); - } - }; - - const handleSubmitRating = async () => { - if (userRating === 0) { - Alert.alert("Error", "Please select a rating"); - return; - } - - try { - setSubmittingRating(true); - await rateProduct(id, userRating, ratingComment); - Alert.alert("Success", "Your rating has been submitted"); - setUserRating(0); - setRatingComment(''); - setShowRatingForm(false); - - // Refresh product data to show updated rating - const updatedProduct = await fetchProductById(id); - setProduct(updatedProduct); - } catch (error) { - console.error('Error submitting rating:', error); - Alert.alert("Error", "Failed to submit rating. Please try again."); - } finally { - setSubmittingRating(false); - } - }; - - const [isFavourite, setIsFavourite] = useState(false) - const handleFavouriteToggle = () => { - setIsFavourite(!isFavourite) - } - - // size toggle - const subSize = ["S", "M", "L", "XL", "XXL", "XXXL"] - const [activeSet, setActiveSet] = useState("S") - - const handleSubSize = (item) => { - setActiveSet(item) - } - - const [initialColor, setColor] = useState(0) - const toggleColor = (index) => { - setColor(index) - } - - const incrementQuantity = () => setQuantity(prev => prev + 1); - const decrementQuantity = () => setQuantity(prev => Math.max(1, prev - 1)); - - const handleEditRating = async () => { - if (editRatingValue === 0) { - Alert.alert("Error", "Please select a rating"); - return; - } - - try { - setEditing(true); - await rateProduct(id, editRatingValue, editComment); - Alert.alert("Success", "Your rating has been updated"); - setEditingRating(null); - setEditRatingValue(0); - setEditComment(''); - - // Refresh product data to show updated rating - const updatedProduct = await fetchProductById(id); - setProduct(updatedProduct); - if (updatedProduct.ratings) { - setRatings(updatedProduct.ratings); - } - } catch (error) { - console.error('Error updating rating:', error); - Alert.alert("Error", "Failed to update rating. Please try again."); - } finally { - setEditing(false); - } - }; - - const startEditing = (rating) => { - setEditingRating(rating); - setEditRatingValue(rating.rating); - setEditComment(rating.comment || ''); - }; - - const cancelEditing = () => { - setEditingRating(null); - setEditRatingValue(0); - setEditComment(''); - }; - - if (loading) { - return ( - - - - ); - } - - if (error) { - return ( - - {error} - - Retry - - - ); - } - - if (!product) { - return ( - - Product not found - - ); - } - - return ( - - - } - > - - navigation.goBack()} className="bg-gray-400 rounded-full flex-row justify-center items-center size-16"> - - - - {product.name} - - - - - - - - - - - - - {product.category_name} - - - {product.average_rating || 0} - - - {product.name} - Product Details - {product.description} - - {/* Quantity Selector */} - - Quantity: - - - - - {quantity} - - + - - - - {/* Product details */} - - - Color - {product.color || 'N/A'} - - - Size - {product.size || 'N/A'} - - - Stock - {product.stock || 0} items - - - - {/* Ratings Section */} - - - Ratings & Reviews - setShowRatingForm(!showRatingForm)} - className="bg-primary px-3 py-1 rounded-full" - > - Rate this product - - - - {showRatingForm && ( - - Your Rating - - {[1, 2, 3, 4, 5].map(star => ( - setUserRating(star)} - className="mr-2" - > - {userRating >= star ? ( - - ) : ( - - )} - - ))} - - Comment (optional) - - - {submittingRating ? ( - - ) : ( - Submit Rating - )} - - - )} - - {/* Display existing ratings */} - {ratings && ratings.length > 0 ? ( - - {ratings.map((rating, index) => ( - - - - - {[1, 2, 3, 4, 5].map(star => ( - - ))} - - {rating.username} - - {new Date(rating.created_at).toLocaleDateString()} - - - {rating.username === currentUser?.username && ( - startEditing(rating)} - className="bg-primary px-3 py-1 rounded-full" - > - Edit - - )} - - {rating.comment && ( - {rating.comment} - )} - - ))} - - ) : ( - - No ratings yet. Be the first to rate this product! - - )} - - {/* Edit Rating Form */} - {editingRating && ( - - Edit Your Rating - - {[1, 2, 3, 4, 5].map(star => ( - setEditRatingValue(star)} - className="mr-2" - > - {editRatingValue >= star ? ( - - ) : ( - - )} - - ))} - - Comment (optional) - - - - Cancel - - - {editing ? ( - - ) : ( - Update Rating - )} - - - - )} - - - - - - Total Price - ${(product.price * quantity).toFixed(2)} - - - {addingToCart ? ( - - ) : ( - - )} - - {addingToCart ? 'Adding...' : 'Add to Cart'} - - - - - ); -} - diff --git a/frontend/app/screens/ProfileScreen.jsx b/frontend/app/screens/ProfileScreen.jsx index 234a096..8d42116 100644 --- a/frontend/app/screens/ProfileScreen.jsx +++ b/frontend/app/screens/ProfileScreen.jsx @@ -12,7 +12,8 @@ import { } from '@gorhom/bottom-sheet'; import { BackHandler, Alert } from 'react-native'; import { useNavigation } from '@react-navigation/native'; -import { getUserData, logoutUser } from '../api'; +import { getUserData, logout, updateProfilePicture } from '../api'; +import * as ImagePicker from 'expo-image-picker'; export default function ProfileScreen() { const navigation = useNavigation(); @@ -39,10 +40,10 @@ export default function ProfileScreen() { fetchUser(); }, []); - const logout = async () => { + const handleLogout = async () => { try { setLoading(true); - await logoutUser(); + await logout(); navigation.navigate("SignIn"); } catch (error) { Alert.alert('Logout Failed', 'Could not log out. Please try again.'); @@ -74,6 +75,60 @@ export default function ProfileScreen() { return () => backHandler.remove(); // Cleanup }, []); + const handleProfilePictureUpdate = async () => { + try { + // Request permission to access the camera roll + const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (status !== 'granted') { + Alert.alert( + 'Permission Required', + 'Please grant permission to access your photos to update your profile picture.', + [{ text: 'OK' }] + ); + return; + } + + // Launch image picker + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [1, 1], + quality: 0.8, + base64: false, + }); + + if (!result.canceled && result.assets && result.assets[0]) { + setLoading(true); + try { + // Update profile picture + const updatedUser = await updateProfilePicture(result.assets[0].uri); + setUserData(updatedUser); + Alert.alert( + 'Success', + 'Profile picture updated successfully', + [{ text: 'OK' }] + ); + } catch (error) { + console.error('Error updating profile picture:', error); + Alert.alert( + 'Error', + 'Failed to update profile picture. Please try again.', + [{ text: 'OK' }] + ); + } finally { + setLoading(false); + } + } + } catch (error) { + console.error('Error in image picker:', error); + Alert.alert( + 'Error', + 'Failed to access image picker. Please try again.', + [{ text: 'OK' }] + ); + } + }; + if (loading) { return ( @@ -111,7 +166,7 @@ export default function ProfileScreen() { 'User'; return ( - + - - - - + + + + + + @@ -262,7 +335,7 @@ export default function ProfileScreen() { Cancel diff --git a/frontend/app/screens/RegisterScreen.jsx b/frontend/app/screens/RegisterScreen.jsx index 5607c55..20d3dc3 100644 --- a/frontend/app/screens/RegisterScreen.jsx +++ b/frontend/app/screens/RegisterScreen.jsx @@ -1,8 +1,8 @@ -import { View, Text, TextInput, TouchableOpacity, Image, ActivityIndicator, ScrollView } from 'react-native' +import { View, Text, TextInput, TouchableOpacity, Image, ActivityIndicator, ScrollView, Modal } from 'react-native' import React, { useState } from 'react' import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; -import { EyeIcon, EyeSlashIcon } from 'react-native-heroicons/outline'; -import { registerUser } from '../api'; +import { EyeIcon, EyeSlashIcon, ChevronDownIcon } from 'react-native-heroicons/outline'; +import { register } from '../api'; import { useNavigation } from '@react-navigation/native'; export default function RegisterScreen() { @@ -16,14 +16,22 @@ export default function RegisterScreen() { first_name: "", last_name: "", phone: "", - address: "" + address: "", + gender: "" }); const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [showGenderPicker, setShowGenderPicker] = useState(false); + const genderOptions = [ + { label: 'Male', value: 'Male' }, + { label: 'Female', value: 'Female' }, + { label: 'Other', value: 'Other' } + ]; + const validateForm = () => { if (!formData.username.trim()) { setError('Username is required'); @@ -60,6 +68,11 @@ export default function RegisterScreen() { setError('First and last name are required'); return false; } + + if (!formData.gender) { + setError('Gender is required'); + return false; + } return true; }; @@ -73,6 +86,14 @@ export default function RegisterScreen() { if (error) setError(null); }; + const handleGenderSelect = (gender) => { + setFormData(prev => ({ + ...prev, + gender: gender.value + })); + setShowGenderPicker(false); + }; + const handleRegister = async () => { // Clear previous errors setError(null); @@ -93,11 +114,20 @@ export default function RegisterScreen() { first_name: formData.first_name, last_name: formData.last_name, phone: formData.phone || null, - address: formData.address || "" + address: formData.address || "", + gender: formData.gender }; - const response = await registerUser(userData); - navigation.navigate("SignIn"); + const response = await register(userData); + + // If registration is successful, navigate to the appropriate screen + if (response && response.token) { + // Registration successful, navigate to sign in + navigation.navigate("SignIn"); + } else { + // Registration successful but no token, navigate to sign in + navigation.navigate("SignIn"); + } } catch (error) { // Handle different error types if (error.response) { @@ -212,6 +242,47 @@ export default function RegisterScreen() { + + Gender + setShowGenderPicker(true)} + className={`border ${error && !formData.gender ? 'border-red-500' : 'border-gray-300'} rounded-full p-2 mt-2 flex-row justify-between items-center`} + > + + {formData.gender ? genderOptions.find(g => g.value === formData.gender)?.label : 'Select gender'} + + + + + + + + + + Select Gender + setShowGenderPicker(false)}> + Cancel + + + {genderOptions.map((option) => ( + handleGenderSelect(option)} + className="py-3 border-b border-gray-200" + > + + {option.label} + + + ))} + + + + Password diff --git a/frontend/app/screens/SearchResultScreen.jsx b/frontend/app/screens/SearchResultScreen.jsx index f36c55b..d70eea3 100644 --- a/frontend/app/screens/SearchResultScreen.jsx +++ b/frontend/app/screens/SearchResultScreen.jsx @@ -102,9 +102,9 @@ const SearchResultScreen = () => { className="absolute right-4 top-4 bg-white/50 p-2 rounded-full" > {isFavorite ? ( - + ) : ( - + )} diff --git a/frontend/app/screens/SearchScreen.jsx b/frontend/app/screens/SearchScreen.jsx index 0505b9b..ee70b01 100644 --- a/frontend/app/screens/SearchScreen.jsx +++ b/frontend/app/screens/SearchScreen.jsx @@ -1,4 +1,4 @@ -import { View, Text, TextInput, ScrollView, TouchableOpacity, ActivityIndicator, Image } from 'react-native' +import { View, Text, TextInput, ScrollView, TouchableOpacity, ActivityIndicator, Image, Alert } from 'react-native' import React, { useState, useEffect } from 'react' import TabBar from '../themes/TabBar' import { ChevronLeftIcon, MagnifyingGlassIcon, XMarkIcon } from 'react-native-heroicons/outline' @@ -30,23 +30,29 @@ const SearchScreen = () => { }, []); const loadCurrentUser = async () => { - const currentUser = await getCurrentUser(); - setUser(currentUser); + try { + const currentUser = await getCurrentUser(); + setUser(currentUser); + } catch (error) { + console.error('Error loading current user:', error); + } }; const loadRecentSearches = async () => { try { const searches = await getUserRecentSearches(); - setRecentSearches(searches); + setRecentSearches(Array.isArray(searches) ? searches : []); } catch (error) { console.error('Error loading recent searches:', error); + setRecentSearches([]); } }; const handleClearRecentSearch = async (term) => { try { + if (!term) return; const updatedSearches = await removeUserRecentSearch(term); - setRecentSearches(updatedSearches); + setRecentSearches(Array.isArray(updatedSearches) ? updatedSearches : []); } catch (error) { console.error('Error clearing recent search:', error); } @@ -62,18 +68,21 @@ const SearchScreen = () => { }; const handleSearch = async (searchTerm = searchText) => { - if (!searchTerm.trim()) return; + if (!searchTerm || !searchTerm.trim()) return; setLoading(true); setErrorMsg(''); try { - const result = await fetchProducts({ search: searchTerm }); + const result = await fetchProducts(searchTerm); setProducts(result); // Save to recent searches await saveUserRecentSearch(searchTerm); + // Refresh recent searches + loadRecentSearches(); + navigation.navigate('SearchResult', { products: result, searchTerm: searchTerm @@ -87,6 +96,7 @@ const SearchScreen = () => { }; const handleRecentSearchPress = (term) => { + if (!term) return; setSearchText(term); handleSearch(term); }; @@ -122,7 +132,7 @@ const SearchScreen = () => { className="font-medium flex-1 h-full" value={searchText} onChangeText={setSearchText} - onSubmitEditing={handleSearch} + onSubmitEditing={() => handleSearch()} returnKeyType="search" autoFocus /> diff --git a/frontend/app/screens/SettingScreen.jsx b/frontend/app/screens/SettingScreen.jsx index f870187..54d2cf0 100644 --- a/frontend/app/screens/SettingScreen.jsx +++ b/frontend/app/screens/SettingScreen.jsx @@ -1,15 +1,55 @@ -import { View, Text, TouchableOpacity } from 'react-native' -import React from 'react' +import { View, Text, TouchableOpacity, Alert, TextInput, ActivityIndicator, Modal } from 'react-native' +import React, { useState } from 'react' import TabBar from '../themes/TabBar' -import { ChevronLeftIcon, ChevronRightIcon, CreditCardIcon, KeyIcon, UserCircleIcon } from 'react-native-heroicons/outline' -import { widthPercentageToDP as wp, heightPercentageToDP as hp, heightPercentageToDP } from 'react-native-responsive-screen'; +import { ChevronLeftIcon, ChevronRightIcon, CreditCardIcon, KeyIcon, UserCircleIcon, EyeIcon, EyeSlashIcon } from 'react-native-heroicons/outline' +import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import { useNavigation } from '@react-navigation/native'; +import { deleteAccount, logout } from '../api'; export default function SettingScreen() { - const navigation = useNavigation() + const navigation = useNavigation(); + const [loading, setLoading] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + + const handleDeleteAccount = async () => { + if (!password) { + Alert.alert('Error', 'Please enter your password'); + return; + } + + Alert.alert( + 'Delete Account', + 'Are you sure you want to delete your account? This action cannot be undone.', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Delete', + style: 'destructive', + onPress: async () => { + try { + setLoading(true); + await deleteAccount(password); + await logout(); + Alert.alert('Success', 'Your account has been deleted'); + navigation.navigate('SignIn'); + } catch (error) { + console.error('Delete account error:', error); + Alert.alert('Error', error.response?.data?.error || 'Failed to delete account'); + } finally { + setLoading(false); + setShowDeleteModal(false); + setPassword(''); + } + } + } + ] + ); + }; return ( - + navigation.goBack()} /> - - - - Notification Settings - - + + + + + + Notification Settings + - - - + + - {navigation.navigate('Password Manager')}} className={`mx-4 flex-row items-center justify-between`}> - - - Password Manager - - + navigation.navigate('Password Manager')} + className="flex-row items-center justify-between py-3" + > + + + Password Manager + - - - + + - - - - Delete Account - - + setShowDeleteModal(true)} + className="flex-row items-center justify-between py-3" + > + + + Delete Account + - + + - + + {/* Delete Account Modal */} + setShowDeleteModal(false)} + > + + + Delete Account + + This action cannot be undone. Please enter your password to confirm. + + + + + setShowPassword(!showPassword)}> + {showPassword ? ( + + ) : ( + + )} + + + + + { + setShowDeleteModal(false); + setPassword(''); + }} + className="flex-1 bg-gray-200 py-3 rounded-xl" + > + Cancel + + + {loading ? ( + + ) : ( + Delete + )} + + + + + ) } \ No newline at end of file diff --git a/frontend/app/screens/SignInScreen.jsx b/frontend/app/screens/SignInScreen.jsx index df9aeba..7f7bd80 100644 --- a/frontend/app/screens/SignInScreen.jsx +++ b/frontend/app/screens/SignInScreen.jsx @@ -2,8 +2,10 @@ import { View, Text, TextInput, TouchableOpacity, Image, ActivityIndicator } fro import React, { useState } from 'react' import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import { EyeIcon, EyeSlashIcon } from 'react-native-heroicons/outline'; -import { loginUser } from '../api'; +import { login } from '../api'; import { useNavigation } from '@react-navigation/native'; +import { STORAGE_KEYS } from '../constants/storageKeys'; +import AsyncStorage from '@react-native-async-storage/async-storage'; export default function SignInScreen() { const navigation = useNavigation(); @@ -45,10 +47,10 @@ export default function SignInScreen() { setLoading(true); try { - // Call loginUser with credentials - const result = await loginUser({ username, password }); + // Call login with credentials + const result = await login({ username, password }); - if (result && result.success) { + if (result && result.token) { console.log('Login successful'); navigation.navigate("Main"); } else { diff --git a/frontend/app/screens/TrackOrderScreen.jsx b/frontend/app/screens/TrackOrderScreen.jsx index efb723e..86eb6da 100644 --- a/frontend/app/screens/TrackOrderScreen.jsx +++ b/frontend/app/screens/TrackOrderScreen.jsx @@ -1,13 +1,226 @@ -import { View, Text } from 'react-native' -import React from 'react' +import { View, Text, ActivityIndicator, RefreshControl, FlatList, TouchableOpacity } from 'react-native' +import React, { useEffect, useState } from 'react' import { widthPercentageToDP as wp, heightPercentageToDP as hp } from 'react-native-responsive-screen'; import TabBar from '../themes/TabBar'; import { ChevronLeftIcon } from 'react-native-heroicons/outline'; import { Image } from 'react-native'; import Timeline from 'react-native-timeline-flatlist' import { Box, FileText, Package, Truck } from 'lucide-react'; +import { useNavigation, useRoute } from '@react-navigation/native'; +import { getOrderById, updateOrderStatus, shipOrder, deliverOrder, cancelOrder } from '../api'; + +const getStatusColor = (timelineStatus, orderStatus) => { + const statusOrder = ['Pending', 'Shipped', 'Delivered']; + const timelineIndex = statusOrder.indexOf(timelineStatus); + const orderIndex = statusOrder.indexOf(orderStatus); + + // If order is pending, only show first dot as active + if (orderStatus === 'Pending') { + return timelineStatus === 'Pending' ? '#22c55e' : '#d1d5db'; + } + + if (timelineIndex < orderIndex) { + return '#22c55e'; // Completed status - green-500 + } else if (timelineIndex === orderIndex) { + return '#22c55e'; // Current status - green-500 + } else { + return '#d1d5db'; // Pending status - gray-300 + } +}; export default function TrackOrderScreen() { + const navigation = useNavigation(); + const route = useRoute(); + const { orderId } = route.params || {}; + const [order, setOrder] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [refreshing, setRefreshing] = useState(false); + const [updating, setUpdating] = useState(false); + const [lastUpdated, setLastUpdated] = useState(null); + + const fetchOrder = async () => { + try { + setLoading(true); + const orderData = await getOrderById(orderId); + + // Check if the order status has changed + if (order && orderData.status !== order.status) { + setLastUpdated(new Date()); + } + + setOrder(orderData); + } catch (err) { + setError('Failed to fetch order details'); + console.error('Error fetching order:', err); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + useEffect(() => { + if (orderId) { + fetchOrder(); + + // Set up polling interval to check for status updates every 30 seconds + const intervalId = setInterval(() => { + fetchOrder(); + }, 30000); + + // Clean up interval on component unmount + return () => clearInterval(intervalId); + } + }, [orderId]); + + const onRefresh = React.useCallback(() => { + setRefreshing(true); + fetchOrder(); + }, [orderId]); + + const handleBack = () => { + navigation.goBack(); + }; + + const handleStatusUpdate = async (newStatus) => { + try { + setUpdating(true); + let updatedOrder; + + switch (newStatus) { + case 'shipped': + updatedOrder = await shipOrder(orderId); + break; + case 'delivered': + updatedOrder = await deliverOrder(orderId); + break; + case 'cancelled': + updatedOrder = await cancelOrder(orderId); + break; + default: + updatedOrder = await updateOrderStatus(orderId, newStatus.charAt(0).toUpperCase() + newStatus.slice(1)); + } + + setOrder(updatedOrder); + } catch (err) { + setError(err.response?.data?.error || 'Failed to update order status'); + console.error('Error updating order status:', err); + } finally { + setUpdating(false); + } + }; + + if (loading && !refreshing) { + return ( + + + + ); + } + + if (error || !order) { + return ( + + {error || 'Order not found'} + + ); + } + + const orderItems = (order.order_items && order.order_items.length > 0) ? order.order_items : order.items; + const firstItem = orderItems?.[0]; + + const formatCurrency = (value) => { + if (value === undefined || value === null) return '0.00'; + if (typeof value === 'number') return value.toFixed(2); + if (typeof value === 'string') { + const num = parseFloat(value); + return isNaN(num) ? '0.00' : num.toFixed(2); + } + return '0.00'; + }; + + const renderOrderDetails = () => { + return ( + <> + + + + + {firstItem?.product_name || firstItem?.product?.title || 'Product'} + Qty: {firstItem?.quantity || 1} + + ${formatCurrency(firstItem?.price || firstItem?.subtotal)} + + + + + + Order Details + + Expected Delivery Date + {new Date(order.expected_delivery_date || new Date().setDate(new Date().getDate() + 7)).toLocaleDateString()} + + + Tracking ID + {order.tracking_id || orderId || 'N/A'} + + {lastUpdated && ( + + Status Updated + {lastUpdated.toLocaleTimeString()} + + )} + + + Order Status + , + status: 'Pending' + }, + { + time: new Date(order.shipped_at || new Date(order.created_at).setDate(new Date(order.created_at).getDate() + 2)).toLocaleString(), + title: 'Shipped', + description: 'Your order is on the way', + icon: , + status: 'Shipped' + }, + { + time: new Date(order.delivered_at || new Date(order.created_at).setDate(new Date(order.created_at).getDate() + 5)).toLocaleString(), + title: 'Delivered', + description: 'Your order has been delivered', + icon: , + status: 'Delivered' + } + ].map(item => ({ + ...item, + circleColor: getStatusColor(item.status, order.status), + lineColor: getStatusColor(item.status, order.status), + titleStyle: { color: getStatusColor(item.status, order.status) } + }))} + circleSize={20} + timeContainerStyle={{ minWidth: 52 }} + descriptionStyle={{ color: 'gray' }} + /> + + {updating && ( + + + + )} + + + ); + }; + return ( navigation.goBack()} + suffixAction={handleBack} /> - - - - - Brown Jacket - Size: XXL| Qty: 10pcs - - Rs:3000 - - - + + renderOrderDetails()} + refreshControl={ + + } + contentContainerStyle={{ flexGrow: 1 }} + /> - - Order Details - - Expected Delivery Date - 03 Sep 2023 - - - Tracking ID - TRK38438783 - - - {/* Order Status */} - - Order Status - - - {/* Timeline Items */} - {/* - } - title="Order Placed" - date="23 Aug 2023, 04:25 PM" - isCompleted={true} - isFirst={true} - /> - } - title="In Progress" - date="23 Aug 2023, 03:54 PM" - isCompleted={true} - /> - } - title="Shipped" - date="Expected 02 Sep 2023" - isCompleted={false} - /> - } - title="Delivered" - date="23 Aug 2023, 2023" - isCompleted={false} - isLast={true} - /> - */} - - ) } diff --git a/frontend/app/screens/Wishlist.jsx b/frontend/app/screens/Wishlist.jsx deleted file mode 100644 index d38c40b..0000000 --- a/frontend/app/screens/Wishlist.jsx +++ /dev/null @@ -1,124 +0,0 @@ -import { View, Text, ScrollView, Image } from 'react-native' -import React, { useState } from 'react' -import { ArrowLeftIcon, ChevronLeftIcon, HeartIcon } from 'react-native-heroicons/outline' -import { TouchableOpacity } from 'react-native' -import TabBar from '../themes/TabBar' -import MasonryList from '@react-native-seoul/masonry-list'; -import flashSalesData from '../constants/flashSalesData' -import { widthPercentageToDP as wp, heightPercentageToDP as hp, heightPercentageToDP } from 'react-native-responsive-screen'; -import { StarIcon } from 'react-native-heroicons/solid' -import Icon from 'react-native-vector-icons/FontAwesome' -import { useNavigation } from '@react-navigation/native' - -const Wishlist = () => { - const navigation = useNavigation() - const [favorites, setFavorites] = useState({}) - - const subWishlist = ["All", "Shirt", "Paint", "T-shirt", "Jacket", "Suite", "Shoes"] - const [activeSet, setActiveSet] = useState("All") - - const handleSubWishlist = (item) => { - setActiveSet(item) - } - const handleFavouriteToggle = (productId) => { - setFavorites(prev => ({ - ...prev, - [productId]: !prev[productId] - })) - } - - return ( - - } - titleStyle={{ - color: "black", - fontWeight: "bold", - fontSize: 20 - }} - prefixStyle={{ - backgroundColor: "transparent" - }} - suffixStyle={{ - backgroundColor: "white", - borderWidth: 1, - borderColor: "black" - }} - containerStyle="" - suffixAction={() => navigation.goBack()} - /> - - - { - subWishlist.map((item, index) => { - let isActive = item == activeSet; - - return ( - handleSubWishlist(item)} - key={index} - className={` - border border-gray-500 px-4 ml-4 mb-4 rounded-full py-2 - ${isActive ? "bg-primary" : ""} - `}> - - {item} - - - - ) - }) - } - - item.id} - numColumns={2} - showsVerticalScrollIndicator={false} - renderItem={({ item, index }) => } - className="mx-4" - /> - - - - - ) -} -const CardItem = ({ item, index, handleFavouriteToggle, isFavourite, navigation }) => { - let isEven = item.id % 2 == 0 - return ( - - { navigation.navigate('ProductDetails',{title:item.name}) }} className="relative"> - - handleFavouriteToggle(item.id)} className="absolute right-4 top-4 bg-white/50 p-2 rounded-full"> - { - isFavourite ? - - : - - } - - - - - - {item.name} - - - {item.rating} - - - - Rs {item.price} - - - - ) -} - -export default Wishlist \ No newline at end of file diff --git a/frontend/app/screens/WishlistScreen.jsx b/frontend/app/screens/WishlistScreen.jsx index ce9585c..9e57848 100644 --- a/frontend/app/screens/WishlistScreen.jsx +++ b/frontend/app/screens/WishlistScreen.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { View, Text, StyleSheet, ScrollView, RefreshControl, TouchableOpacity, Image } from 'react-native'; +import { View, Text, StyleSheet, ScrollView, RefreshControl, TouchableOpacity, Image, ActivityIndicator } from 'react-native'; import { getFavorites, toggleFavorite } from '../api'; import { useFocusEffect } from '@react-navigation/native'; import { ChevronLeftIcon, HeartIcon } from 'react-native-heroicons/outline'; @@ -18,9 +18,10 @@ const WishlistScreen = ({ navigation }) => { const loadFavorites = async () => { try { const data = await getFavorites(); - setFavorites(data); + setFavorites(data || []); } catch (error) { console.error('Error loading favorites:', error); + setFavorites([]); } finally { setLoading(false); setRefreshing(false); @@ -40,21 +41,25 @@ const WishlistScreen = ({ navigation }) => { const handleFavoriteToggle = async (product) => { try { - const updatedFavorites = await toggleFavorite(product); - setFavorites(updatedFavorites); + await toggleFavorite(product); + // Reload favorites after toggling + loadFavorites(); } catch (error) { console.error('Error toggling favorite:', error); } }; + // Ensure favorites is always an array + const safeFavorites = Array.isArray(favorites) ? favorites : []; + const filteredFavorites = activeCategory === "All" - ? favorites - : favorites.filter(item => item.category === activeCategory); + ? safeFavorites + : safeFavorites.filter(item => item.product && item.product.category_name === activeCategory); if (loading && !refreshing) { return ( - - Loading... + + ); } @@ -129,17 +134,17 @@ const WishlistScreen = ({ navigation }) => { ]} > navigation.navigate('ProductDetails', { product: item })} + onPress={() => navigation.navigate('ProductDetails', { id: item.product.id })} style={styles.card} > handleFavoriteToggle(item)} + onPress={() => handleFavoriteToggle(item.product)} > { - {item.title} + {item.product.name} - ${item.price} + Rs {item.product.price} - {item.rating} + {item.product.average_rating || 0} diff --git a/frontend/app/theme.js b/frontend/app/theme.js new file mode 100644 index 0000000..07314ef --- /dev/null +++ b/frontend/app/theme.js @@ -0,0 +1,98 @@ +export const colors = { + primary: '#007AFF', + secondary: '#5856D6', + success: '#34C759', + danger: '#FF3B30', + warning: '#FF9500', + info: '#5AC8FA', + background: '#FFFFFF', + text: '#000000', + textSecondary: '#8E8E93', + border: '#E5E5EA', + card: '#F2F2F7', + placeholder: '#C7C7CC', + disabled: '#D1D1D6', + overlay: 'rgba(0, 0, 0, 0.5)', +}; + +export const spacing = { + xs: 4, + sm: 8, + md: 16, + lg: 24, + xl: 32, + xxl: 40, +}; + +export const typography = { + h1: { + fontSize: 32, + fontWeight: 'bold', + }, + h2: { + fontSize: 24, + fontWeight: 'bold', + }, + h3: { + fontSize: 20, + fontWeight: '600', + }, + body: { + fontSize: 16, + }, + caption: { + fontSize: 14, + color: colors.textSecondary, + }, +}; + +export const shadows = { + small: { + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 2, + }, + medium: { + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 4, + }, + shadowOpacity: 0.30, + shadowRadius: 4.65, + elevation: 4, + }, + large: { + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 6, + }, + shadowOpacity: 0.37, + shadowRadius: 7.49, + elevation: 6, + }, +}; + +export const borderRadius = { + sm: 4, + md: 8, + lg: 12, + xl: 16, + round: 999, +}; + +const theme = { + colors, + spacing, + typography, + shadows, + borderRadius, +}; + +export default theme; \ No newline at end of file diff --git a/frontend/assets/images/esewa.jpg b/frontend/assets/images/esewa.jpg new file mode 100644 index 0000000..a476550 Binary files /dev/null and b/frontend/assets/images/esewa.jpg differ diff --git a/frontend/assets/images/imepay.png b/frontend/assets/images/imepay.png new file mode 100644 index 0000000..90725de Binary files /dev/null and b/frontend/assets/images/imepay.png differ diff --git a/frontend/assets/images/khalti.jpg b/frontend/assets/images/khalti.jpg new file mode 100644 index 0000000..37897e2 Binary files /dev/null and b/frontend/assets/images/khalti.jpg differ diff --git a/frontend/babel.config.js b/frontend/babel.config.js index 59308f4..577e38b 100644 --- a/frontend/babel.config.js +++ b/frontend/babel.config.js @@ -1,7 +1,6 @@ module.exports = function (api) { api.cache(true); return { - presets: [ ["babel-preset-expo", { jsxImportSource: "nativewind" }], "nativewind/babel", diff --git a/frontend/navigation_fix_script.sh b/frontend/navigation_fix_script.sh deleted file mode 100644 index 7794de3..0000000 --- a/frontend/navigation_fix_script.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash -# Script to fix navigator -> navigation in React Native files - -echo "Starting navigation fix script..." - -# List of all files to check and fix -find ./app/screens -name "*.jsx" -type f | while read file; do - echo "Processing $file..." - - # Check if the file uses expo-router navigation - if grep -q "import { useNavigation } from 'expo-router'" "$file"; then - echo " Fixing expo-router import in $file" - sed -i "s/import { useNavigation } from 'expo-router'/import { useNavigation } from '@react-navigation\/native'/" "$file" - fi - - # Check if file has navigator variable - if grep -q "const navigator = useNavigation()" "$file"; then - echo " Fixing navigator variable in $file" - sed -i "s/const navigator = useNavigation()/const navigation = useNavigation()/" "$file" - fi - - # Check and fix suffixAction with navigator.goBack - if grep -q "suffixAction={() => navigator.goBack()}" "$file"; then - echo " Fixing navigator.goBack in suffixAction in $file" - sed -i "s/suffixAction={() => navigator.goBack()}/suffixAction={() => navigation.goBack()}/" "$file" - fi - - # Check and fix other direct navigator.goBack calls - if grep -q "onPress={() => navigator.goBack()}" "$file"; then - echo " Fixing navigator.goBack in onPress in $file" - sed -i "s/onPress={() => navigator.goBack()}/onPress={() => navigation.goBack()}/" "$file" - fi - - # Fix navigator.navigate calls - if grep -q "navigator.navigate" "$file"; then - echo " Fixing navigator.navigate calls in $file" - sed -i "s/navigator.navigate/navigation.navigate/g" "$file" - fi - - # Fix props passing - from navigator to navigation - if grep -q "navigator={navigator}" "$file"; then - echo " Fixing navigator props in $file" - sed -i "s/navigator={navigator}/navigation={navigation}/g" "$file" - fi - - # Fix function params that use navigator - if grep -q "({ .*, navigator" "$file"; then - echo " Fixing function parameters using navigator in $file" - sed -i "s/({ \(.*\), navigator/({ \1, navigation/g" "$file" - fi -done - -echo "Done processing files. Please check the changes before committing." \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9474b8b..735a3ee 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,8 @@ "@ptomasroos/react-native-multi-slider": "^2.2.2", "@react-native-async-storage/async-storage": "^2.1.2", "@react-native-community/blur": "^4.4.1", + "@react-native-community/geolocation": "^3.4.0", + "@react-native-picker/picker": "^2.11.0", "@react-native-seoul/masonry-list": "^1.4.2", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", @@ -22,9 +24,12 @@ "expo-blur": "~14.0.1", "expo-checkbox": "~4.0.1", "expo-constants": "~17.0.3", + "expo-contacts": "^14.0.5", "expo-font": "~13.0.1", "expo-haptics": "~14.0.0", + "expo-image-picker": "^16.0.6", "expo-linking": "~7.0.3", + "expo-location": "^18.0.10", "expo-router": "4.0.11", "expo-splash-screen": "~0.29.18", "expo-status-bar": "~2.0.0", @@ -38,6 +43,7 @@ "react-icons": "^5.4.0", "react-native": "0.76.5", "react-native-check-box": "^2.1.7", + "react-native-element-dropdown": "^2.12.4", "react-native-gesture-handler": "~2.20.2", "react-native-heroicons": "^4.0.0", "react-native-pager-view": "6.5.1", @@ -2372,15 +2378,15 @@ } }, "node_modules/@expo/config": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-10.0.6.tgz", - "integrity": "sha512-xXkfPElrtxznkOZxFASJ7OPa6E9IHSjcZwj5BQ6XUF2dz5M7AFa2h5sXM8AalSaDU5tEBSgoUOjTh5957TlR8g==", + "version": "10.0.11", + "resolved": "https://registry.npmjs.org/@expo/config/-/config-10.0.11.tgz", + "integrity": "sha512-nociJ4zr/NmbVfMNe9j/+zRlt7wz/siISu7PjdWE4WE+elEGxWWxsGzltdJG0llzrM+khx8qUiFK5aiVcdMBww==", "license": "MIT", "dependencies": { "@babel/code-frame": "~7.10.4", - "@expo/config-plugins": "~9.0.10", - "@expo/config-types": "^52.0.0", - "@expo/json-file": "^9.0.0", + "@expo/config-plugins": "~9.0.17", + "@expo/config-types": "^52.0.5", + "@expo/json-file": "^9.0.2", "deepmerge": "^4.3.1", "getenv": "^1.0.0", "glob": "^10.4.2", @@ -2393,14 +2399,14 @@ } }, "node_modules/@expo/config-plugins": { - "version": "9.0.12", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-9.0.12.tgz", - "integrity": "sha512-/Ko/NM+GzvJyRkq8PITm8ms0KY5v0wmN1OQFYRMkcJqOi3PjlhndW+G6bHpJI9mkQXBaUnHwAiGLqIC3+MQ5Wg==", + "version": "9.0.17", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-9.0.17.tgz", + "integrity": "sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg==", "license": "MIT", "dependencies": { - "@expo/config-types": "^52.0.0", - "@expo/json-file": "~9.0.0", - "@expo/plist": "^0.2.0", + "@expo/config-types": "^52.0.5", + "@expo/json-file": "~9.0.2", + "@expo/plist": "^0.2.2", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", @@ -2427,9 +2433,9 @@ } }, "node_modules/@expo/config-types": { - "version": "52.0.1", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-52.0.1.tgz", - "integrity": "sha512-vD8ZetyKV7U29lR6+NJohYeoLYTH+eNYXJeNiSOrWCz0witJYY11meMmEnpEaVbN89EfC6uauSUOa6wihtbyPQ==", + "version": "52.0.5", + "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-52.0.5.tgz", + "integrity": "sha512-AMDeuDLHXXqd8W+0zSjIt7f37vUd/BP8p43k68NHpyAvQO+z8mbQZm3cNQVAMySeayK2XoPigAFB1JF2NFajaA==", "license": "MIT" }, "node_modules/@expo/config/node_modules/@babel/code-frame": { @@ -2483,9 +2489,9 @@ } }, "node_modules/@expo/env": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-0.4.0.tgz", - "integrity": "sha512-g2JYFqck3xKIwJyK+8LxZ2ENZPWtRgjFWpeht9abnKgzXVXBeSNECFBkg+WQjQocSIdxXhEWM6hz4ZAe7Tc4ng==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-0.4.2.tgz", + "integrity": "sha512-TgbCgvSk0Kq0e2fLoqHwEBL4M0ztFjnBEz0YCDm5boc1nvkV1VMuIMteVdeBwnTh8Z0oPJTwHCD49vhMEt1I6A==", "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -2604,9 +2610,9 @@ } }, "node_modules/@expo/json-file": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-9.0.0.tgz", - "integrity": "sha512-M+55xFVrFzDcgMDf+52lPDLjKB5xwRfStWlv/b/Vu2OLgxGZLWpxoPYjlRoHqxjPbCQIi2ZCbobK+0KuNhsELg==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-9.0.2.tgz", + "integrity": "sha512-yAznIUrybOIWp3Uax7yRflB0xsEpvIwIEqIjao9SGi2Gaa+N0OamWfe0fnXBSWF+2zzF4VvqwT4W5zwelchfgw==", "license": "MIT", "dependencies": { "@babel/code-frame": "~7.10.4", @@ -2734,9 +2740,9 @@ "license": "MIT" }, "node_modules/@expo/plist": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.2.0.tgz", - "integrity": "sha512-F/IZJQaf8OIVnVA6XWUeMPC3OH6MV00Wxf0WC0JhTQht2QgjyHUa3U5Gs3vRtDq8tXNsZneOQRDVwpaOnd4zTQ==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.2.2.tgz", + "integrity": "sha512-ZZGvTO6vEWq02UAPs3LIdja+HRO18+LRI5QuDl6Hs3Ps7KX7xU6Y6kjahWKY37Rx2YjNpX07dGpBFzzC+vKa2g==", "license": "MIT", "dependencies": { "@xmldom/xmldom": "~0.7.7", @@ -3998,6 +4004,32 @@ "optional": true, "peer": true }, + "node_modules/@react-native-community/geolocation": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@react-native-community/geolocation/-/geolocation-3.4.0.tgz", + "integrity": "sha512-bzZH89/cwmpkPMKKveoC72C4JH0yF4St5Ceg/ZM9pA1SqX9MlRIrIrrOGZ/+yi++xAvFDiYfihtn9TvXWU9/rA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/@react-native-picker/picker": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.0.tgz", + "integrity": "sha512-QuZU6gbxmOID5zZgd/H90NgBnbJ3VV6qVzp6c7/dDrmWdX8S0X5YFYgDcQFjE3dRen9wB9FWnj2VVdPU64adSg==", + "license": "MIT", + "workspaces": [ + "example" + ], + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@react-native-seoul/masonry-list": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@react-native-seoul/masonry-list/-/masonry-list-1.4.2.tgz", @@ -7714,19 +7746,29 @@ } }, "node_modules/expo-constants": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.3.tgz", - "integrity": "sha512-lnbcX2sAu8SucHXEXxSkhiEpqH+jGrf+TF+MO6sHWIESjwOUVVYlT8qYdjR9xbxWmqFtrI4KV44FkeJf2DaFjQ==", + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.8.tgz", + "integrity": "sha512-XfWRyQAf1yUNgWZ1TnE8pFBMqGmFP5Gb+SFSgszxDdOoheB/NI5D4p7q86kI2fvGyfTrxAe+D+74nZkfsGvUlg==", "license": "MIT", "dependencies": { - "@expo/config": "~10.0.4", - "@expo/env": "~0.4.0" + "@expo/config": "~10.0.11", + "@expo/env": "~0.4.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, + "node_modules/expo-contacts": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/expo-contacts/-/expo-contacts-14.0.5.tgz", + "integrity": "sha512-PjuwJmXgQLQ5CnuhFvgP+jRdYFFh23ARP3urln9GXwMm1uqR5Y0ZdQC44PgJsKxGlJYVzPQlDxer03mGsoXN8g==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, "node_modules/expo-file-system": { "version": "18.0.5", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.5.tgz", @@ -7762,6 +7804,27 @@ "expo": "*" } }, + "node_modules/expo-image-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-5.0.0.tgz", + "integrity": "sha512-Eg+5FHtyzv3Jjw9dHwu2pWy4xjf8fu3V0Asyy42kO+t/FbvW/vjUixpTjPtgKQLQh+2/9Nk4JjFDV6FwCnF2ZA==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-image-picker": { + "version": "16.0.6", + "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-16.0.6.tgz", + "integrity": "sha512-HN4xZirFjsFDIsWFb12AZh19fRzuvZjj2ll17cGr19VNRP06S/VPQU3Tdccn5vwUzQhOBlLu704CnNm278boiQ==", + "license": "MIT", + "dependencies": { + "expo-image-loader": "~5.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-keep-awake": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.1.tgz", @@ -7786,6 +7849,15 @@ "react-native": "*" } }, + "node_modules/expo-location": { + "version": "18.0.10", + "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-18.0.10.tgz", + "integrity": "sha512-R0Iioz0UZ9Ts8TACPngh8uDFbajJhVa5/igLqWB8Pq/gp8UHuwj7PC8XbZV7avsFoShYjaxrOhf4U7IONeKLgg==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.0.4.tgz", @@ -13211,6 +13283,22 @@ "node": ">=10" } }, + "node_modules/react-native-element-dropdown": { + "version": "2.12.4", + "resolved": "https://registry.npmjs.org/react-native-element-dropdown/-/react-native-element-dropdown-2.12.4.tgz", + "integrity": "sha512-abZc5SVji9FIt7fjojRYrbuvp03CoeZJrgvezQoDoSOrpiTqkX69ix5m+j06W2AVncA0VWvbT+vCMam8SoVadw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-gesture-handler": { "version": "2.20.2", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index a9ebb30..b94369b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,8 @@ "@ptomasroos/react-native-multi-slider": "^2.2.2", "@react-native-async-storage/async-storage": "^2.1.2", "@react-native-community/blur": "^4.4.1", + "@react-native-community/geolocation": "^3.4.0", + "@react-native-picker/picker": "^2.11.0", "@react-native-seoul/masonry-list": "^1.4.2", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", @@ -29,9 +31,12 @@ "expo-blur": "~14.0.1", "expo-checkbox": "~4.0.1", "expo-constants": "~17.0.3", + "expo-contacts": "^14.0.5", "expo-font": "~13.0.1", "expo-haptics": "~14.0.0", + "expo-image-picker": "^16.0.6", "expo-linking": "~7.0.3", + "expo-location": "^18.0.10", "expo-router": "4.0.11", "expo-splash-screen": "~0.29.18", "expo-status-bar": "~2.0.0", @@ -45,6 +50,7 @@ "react-icons": "^5.4.0", "react-native": "0.76.5", "react-native-check-box": "^2.1.7", + "react-native-element-dropdown": "^2.12.4", "react-native-gesture-handler": "~2.20.2", "react-native-heroicons": "^4.0.0", "react-native-pager-view": "6.5.1", diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 2f35dd4..d44d4fb 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -11,8 +11,8 @@ "include": [ "**/*.ts", "**/*.tsx", + "nativewind-env.d.ts", ".expo/types/**/*.ts", - "expo-env.d.ts", - "nativewind-env.d.ts" + "expo-env.d.ts" ] -} \ No newline at end of file +}