diff --git a/assets/pasted-20260220-193237-23a07b2b.png b/assets/pasted-20260220-193237-23a07b2b.png new file mode 100644 index 0000000..869309f Binary files /dev/null and b/assets/pasted-20260220-193237-23a07b2b.png differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 881731c..30014fd 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 42d995d..d36b34c 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..fef83d9 100644 --- a/config/settings.py +++ b/config/settings.py @@ -155,6 +155,10 @@ STATICFILES_DIRS = [ BASE_DIR / 'node_modules', ] +# Media files (User-uploaded files) +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + # Email EMAIL_BACKEND = os.getenv( "EMAIL_BACKEND", @@ -179,4 +183,4 @@ if EMAIL_USE_SSL: # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' \ No newline at end of file diff --git a/config/urls.py b/config/urls.py index bcfc074..cccd18f 100644 --- a/config/urls.py +++ b/config/urls.py @@ -27,3 +27,4 @@ urlpatterns = [ if settings.DEBUG: urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 2964e11..94d4d0e 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 18a063c..50f7e00 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/pexels.cpython-311.pyc b/core/__pycache__/pexels.cpython-311.pyc new file mode 100644 index 0000000..6244dee Binary files /dev/null and b/core/__pycache__/pexels.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index ebb8c6e..8b31aba 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 8d204fa..0214b21 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..28679e5 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,67 @@ from django.contrib import admin +from .models import Category, Vendor, Product -# Register your models here. +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ('name', 'slug') + prepopulated_fields = {'slug': ('name',)} + +@admin.register(Vendor) +class VendorAdmin(admin.ModelAdmin): + list_display = ('name', 'whatsapp_number', 'created_at') + prepopulated_fields = {'slug': ('name',)} + search_fields = ('name', 'whatsapp_number') + + def get_queryset(self, request): + qs = super().get_queryset(request) + if request.user.is_superuser: + return qs + return qs.filter(user=request.user) + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ('name', 'vendor', 'category', 'price', 'stock', 'is_active', 'created_at') + list_filter = ('vendor', 'category', 'is_active') + prepopulated_fields = {'slug': ('name',)} + search_fields = ('name', 'vendor__name', 'category__name') + + def get_queryset(self, request): + qs = super().get_queryset(request) + if request.user.is_superuser: + return qs + # Filter products by the vendor linked to the user + try: + vendor = request.user.vendor_profile + return qs.filter(vendor=vendor) + except: + return qs.none() + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + if not request.user.is_superuser: + # If a vendor is logged in, hide the 'vendor' field from the form + if 'vendor' in form.base_fields: + form.base_fields['vendor'].required = False + # Remove it from the displayed fields if it's there + # However, for simplicity, we'll just handle it in save_model + return form + + def save_model(self, request, obj, form, change): + if not request.user.is_superuser: + # Automatically set the vendor if it's not set and the user is a vendor + try: + obj.vendor = request.user.vendor_profile + except: + pass + super().save_model(request, obj, form, change) + + def get_fieldsets(self, request, obj=None): + fieldsets = super().get_fieldsets(request, obj) + if not request.user.is_superuser: + # Exclude vendor field from the form for vendors + for fieldset in fieldsets: + if 'vendor' in fieldset[1]['fields']: + # Create a new tuple without 'vendor' + new_fields = tuple(f for f in fieldset[1]['fields'] if f != 'vendor') + fieldset[1]['fields'] = new_fields + return fieldsets diff --git a/core/management/__init__.py b/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/__pycache__/__init__.cpython-311.pyc b/core/management/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..5941dd6 Binary files /dev/null and b/core/management/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__init__.py b/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/commands/__pycache__/__init__.cpython-311.pyc b/core/management/commands/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..4c04dcc Binary files /dev/null and b/core/management/commands/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__pycache__/create_vendor_accounts.cpython-311.pyc b/core/management/commands/__pycache__/create_vendor_accounts.cpython-311.pyc new file mode 100644 index 0000000..c2cc51c Binary files /dev/null and b/core/management/commands/__pycache__/create_vendor_accounts.cpython-311.pyc differ diff --git a/core/management/commands/__pycache__/seed_data.cpython-311.pyc b/core/management/commands/__pycache__/seed_data.cpython-311.pyc new file mode 100644 index 0000000..7a895f9 Binary files /dev/null and b/core/management/commands/__pycache__/seed_data.cpython-311.pyc differ diff --git a/core/management/commands/__pycache__/update_images.cpython-311.pyc b/core/management/commands/__pycache__/update_images.cpython-311.pyc new file mode 100644 index 0000000..ff9f868 Binary files /dev/null and b/core/management/commands/__pycache__/update_images.cpython-311.pyc differ diff --git a/core/management/commands/create_vendor_accounts.py b/core/management/commands/create_vendor_accounts.py new file mode 100644 index 0000000..203a5bc --- /dev/null +++ b/core/management/commands/create_vendor_accounts.py @@ -0,0 +1,50 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import User, Permission +from django.contrib.contenttypes.models import ContentType +from core.models import Vendor, Product, Category + +class Command(BaseCommand): + help = 'Create user accounts for existing vendors and grant permissions' + + def handle(self, *args, **kwargs): + vendors = Vendor.objects.all() + password = 'vendorpass' + + # Get permissions + models = [Vendor, Product, Category] + permissions = [] + for model in models: + content_type = ContentType.objects.get_for_model(model) + permissions.extend(Permission.objects.filter(content_type=content_type)) + + for vendor in vendors: + if not vendor.user: + username = vendor.slug.replace('-', '_') + + # Ensure unique username + base_username = username + counter = 1 + while User.objects.filter(username=username).exists(): + username = f"{base_username}{counter}" + counter += 1 + + user = User.objects.create_user( + username=username, + email='', + password=password, + is_staff=True + ) + + vendor.user = user + vendor.save() + self.stdout.write(self.style.SUCCESS(f'Created user "{username}" for vendor "{vendor.name}"')) + else: + user = vendor.user + user.is_staff = True + user.save() + + # Grant permissions + user.user_permissions.add(*permissions) + self.stdout.write(self.style.SUCCESS(f'Granted permissions to user "{user.username}"')) + + self.stdout.write(self.style.SUCCESS('Successfully processed vendor accounts. Default password: vendorpass')) \ No newline at end of file diff --git a/core/management/commands/seed_data.py b/core/management/commands/seed_data.py new file mode 100644 index 0000000..2047306 --- /dev/null +++ b/core/management/commands/seed_data.py @@ -0,0 +1,124 @@ +from django.core.management.base import BaseCommand +from core.models import Category, Vendor, Product +from django.utils.text import slugify + +class Command(BaseCommand): + help = 'Seeds initial data for Pasar UMKM Pemagarsari' + + def handle(self, *args, **options): + # Create Categories + categories_data = [ + {'name': 'Makanan', 'icon': 'fa-utensils'}, + {'name': 'Minuman', 'icon': 'fa-coffee'}, + {'name': 'Kerajinan', 'icon': 'fa-palette'}, + {'name': 'Pakaian', 'icon': 'fa-tshirt'}, + {'name': 'Jasa', 'icon': 'fa-tools'}, + {'name': 'Pertanian', 'icon': 'fa-leaf'}, + ] + + cats = {} + for cat in categories_data: + obj, created = Category.objects.get_or_create( + name=cat['name'], + defaults={'icon': cat['icon']} + ) + cats[cat['name']] = obj + if created: + self.stdout.write(self.style.SUCCESS(f'Created category: {cat["name"]}')) + + # Create Vendors + vendors_data = [ + { + 'name': 'Dapur Bu Siti', + 'whatsapp_number': '628123456789', + 'description': 'Spesialis kue basah dan masakan tradisional khas Pemagarsari.', + 'address': 'RT 01 RW 02, Desa Pemagarsari' + }, + { + 'name': 'Kerajinan Bambu Jaya', + 'whatsapp_number': '628987654321', + 'description': 'Produk anyaman bambu berkualitas tinggi untuk dekorasi rumah.', + 'address': 'RT 05 RW 01, Desa Pemagarsari' + }, + { + 'name': 'Kopi Desa Kita', + 'whatsapp_number': '628555444333', + 'description': 'Kopi asli hasil perkebunan Desa Pemagarsari yang diproses secara tradisional.', + 'address': 'RT 03 RW 03, Desa Pemagarsari' + } + ] + + vens = {} + for ven in vendors_data: + obj, created = Vendor.objects.get_or_create( + name=ven['name'], + defaults={ + 'whatsapp_number': ven['whatsapp_number'], + 'description': ven['description'], + 'address': ven['address'] + } + ) + vens[ven['name']] = obj + if created: + self.stdout.write(self.style.SUCCESS(f'Created vendor: {ven["name"]}')) + + # Create Products + products_data = [ + { + 'name': 'Kue Klepon Lumer', + 'vendor': vens['Dapur Bu Siti'], + 'category': cats['Makanan'], + 'price': 15000, + 'description': 'Klepon dengan gula merah cair yang melimpah dan taburan kelapa gurih. Isi 10 pcs.' + }, + { + 'name': 'Nasi Liwet Komplit', + 'vendor': vens['Dapur Bu Siti'], + 'category': cats['Makanan'], + 'price': 25000, + 'description': 'Nasi liwet dengan lauk ayam goreng, tahu tempe, sambal, dan lalapan.' + }, + { + 'name': 'Tas Anyaman Bambu', + 'vendor': vens['Kerajinan Bambu Jaya'], + 'category': cats['Kerajinan'], + 'price': 85000, + 'description': 'Tas jinjing elegan hasil anyaman tangan pengrajin lokal. Kuat dan ramah lingkungan.' + }, + { + 'name': 'Wadah Serbaguna', + 'vendor': vens['Kerajinan Bambu Jaya'], + 'category': cats['Kerajinan'], + 'price': 45000, + 'description': 'Satu set wadah serbaguna untuk keperluan dapur atau dekorasi.' + }, + { + 'name': 'Kopi Robusta Pemagarsari (250g)', + 'vendor': vens['Kopi Desa Kita'], + 'category': cats['Minuman'], + 'price': 35000, + 'description': 'Bubuk kopi robusta pilihan dengan aroma yang kuat dan rasa yang mantap.' + }, + { + 'name': 'Gula Semut Aren', + 'vendor': vens['Kopi Desa Kita'], + 'category': cats['Minuman'], + 'price': 20000, + 'description': 'Gula aren kualitas premium, sangat cocok untuk pemanis kopi atau minuman lainnya.' + } + ] + + for prod in products_data: + obj, created = Product.objects.get_or_create( + name=prod['name'], + vendor=prod['vendor'], + defaults={ + 'category': prod['category'], + 'price': prod['price'], + 'description': prod['description'] + } + ) + if created: + self.stdout.write(self.style.SUCCESS(f'Created product: {prod["name"]}')) + + self.stdout.write(self.style.SUCCESS('Successfully seeded initial data.')) diff --git a/core/management/commands/update_images.py b/core/management/commands/update_images.py new file mode 100644 index 0000000..6c54de3 --- /dev/null +++ b/core/management/commands/update_images.py @@ -0,0 +1,36 @@ +from django.core.management.base import BaseCommand +from core.models import Vendor, Product +from core.pexels import fetch_first +import os + +class Command(BaseCommand): + help = 'Fetches sample images from Pexels for existing products and vendors' + + def handle(self, *args, **options): + self.stdout.write("Fetching images for vendors...") + for vendor in Vendor.objects.all(): + if not vendor.logo: + self.stdout.write(f"Fetching logo for vendor: {vendor.name}") + result = fetch_first(f"shop logo {vendor.name}", orientation="square") + if result: + vendor.logo = result['local_path'] + vendor.save() + self.stdout.write(self.style.SUCCESS(f"Updated logo for {vendor.name}")) + else: + self.stdout.write(self.style.WARNING(f"Could not fetch logo for {vendor.name}")) + + self.stdout.write("Fetching images for products...") + for product in Product.objects.all(): + if not product.image: + self.stdout.write(f"Fetching image for product: {product.name}") + # Use a specific query based on the product name and category + query = f"{product.category.name} {product.name}" + result = fetch_first(query, orientation="landscape") + if result: + product.image = result['local_path'] + product.save() + self.stdout.write(self.style.SUCCESS(f"Updated image for {product.name}")) + else: + self.stdout.write(self.style.WARNING(f"Could not fetch image for {product.name}")) + + self.stdout.write(self.style.SUCCESS('Successfully updated all missing images.')) diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..ed3d47a --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,56 @@ +# Generated by Django 5.2.7 on 2026-02-20 18:37 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('slug', models.SlugField(blank=True, unique=True)), + ('icon', models.CharField(blank=True, help_text='FontAwesome icon class', max_length=50)), + ], + options={ + 'verbose_name_plural': 'Categories', + }, + ), + migrations.CreateModel( + name='Vendor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('slug', models.SlugField(blank=True, unique=True)), + ('description', models.TextField(blank=True)), + ('whatsapp_number', models.CharField(help_text='Format: 628123456789', max_length=20)), + ('address', models.TextField(blank=True)), + ('logo', models.ImageField(blank=True, null=True, upload_to='vendors/')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('slug', models.SlugField(blank=True, unique=True)), + ('description', models.TextField()), + ('price', models.DecimalField(decimal_places=2, max_digits=12)), + ('image', models.ImageField(blank=True, null=True, upload_to='products/')), + ('stock', models.PositiveIntegerField(default=1)), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='core.category')), + ('vendor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='core.vendor')), + ], + ), + ] diff --git a/core/migrations/0002_vendor_user.py b/core/migrations/0002_vendor_user.py new file mode 100644 index 0000000..a6635ca --- /dev/null +++ b/core/migrations/0002_vendor_user.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.7 on 2026-02-20 19:03 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='vendor', + name='user', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='vendor_profile', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..517b6c5 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_vendor_user.cpython-311.pyc b/core/migrations/__pycache__/0002_vendor_user.cpython-311.pyc new file mode 100644 index 0000000..0f2d807 Binary files /dev/null and b/core/migrations/__pycache__/0002_vendor_user.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..9c7984d 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,64 @@ from django.db import models +from django.urls import reverse +from django.utils.text import slugify +from django.contrib.auth.models import User -# Create your models here. +class Category(models.Model): + name = models.CharField(max_length=100) + slug = models.SlugField(unique=True, blank=True) + icon = models.CharField(max_length=50, blank=True, help_text="FontAwesome icon class") + + class Meta: + verbose_name_plural = "Categories" + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + + def __str__(self): + return self.name + +class Vendor(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='vendor_profile', null=True, blank=True) + name = models.CharField(max_length=200) + slug = models.SlugField(unique=True, blank=True) + description = models.TextField(blank=True) + whatsapp_number = models.CharField(max_length=20, help_text="Format: 628123456789") + address = models.TextField(blank=True) + logo = models.ImageField(upload_to='vendors/', blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + + def __str__(self): + return self.name + +class Product(models.Model): + vendor = models.ForeignKey(Vendor, on_delete=models.CASCADE, related_name='products') + category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='products') + name = models.CharField(max_length=255) + slug = models.SlugField(unique=True, blank=True) + description = models.TextField() + price = models.DecimalField(max_digits=12, decimal_places=2) + image = models.ImageField(upload_to='products/', blank=True, null=True) + stock = models.PositiveIntegerField(default=1) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + + def __str__(self): + return self.name + + def get_whatsapp_url(self): + message = f"Halo {self.vendor.name}, saya tertarik membeli {self.name} seharga Rp {self.price:,.0f}. Apakah stok masih ada?" + import urllib.parse + encoded_message = urllib.parse.quote(message) + return f"https://wa.me/{self.vendor.whatsapp_number}?text={encoded_message}" \ No newline at end of file diff --git a/core/pexels.py b/core/pexels.py new file mode 100644 index 0000000..588b95c --- /dev/null +++ b/core/pexels.py @@ -0,0 +1,44 @@ +import os +from pathlib import Path +import httpx + +API_KEY = os.getenv("PEXELS_KEY", "Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18") +CACHE_DIR = Path("media/pexels") + +def _client(): + return httpx.Client( + base_url="https://api.pexels.com/v1/", + headers={"Authorization": API_KEY}, + timeout=15, + ) + +def fetch_first(query: str, orientation: str = "portrait") -> dict | None: + if not API_KEY: + return None + try: + with _client() as client: + resp = client.get( + "search", + params={"query": query, "orientation": orientation, "per_page": 1, "page": 1}, + ) + resp.raise_for_status() + data = resp.json() + photo = (data.get("photos") or [None])[0] + if not photo: + return None + src = photo["src"].get("large2x") or photo["src"].get("large") or photo["src"].get("original") + CACHE_DIR.mkdir(parents=True, exist_ok=True) + target = CACHE_DIR / f"{photo['id']}.jpg" + if src: + img = httpx.get(src, timeout=15) + img.raise_for_status() + target.write_bytes(img.content) + return { + "id": photo["id"], + "local_path": f"pexels/{photo['id']}.jpg", + "photographer": photo.get("photographer"), + "photographer_url": photo.get("photographer_url"), + } + except Exception as e: + print(f"Error fetching from Pexels: {e}") + return None diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..196857c 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,103 @@ +{% load static %} - - + - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - {% load static %} - - {% block head %}{% endblock %} + + + {% block title %}Pasar UMKM Desa Pemagarsari{% endblock %} + + + + + + + + + + + + + + + {% block extra_head %}{% endblock %} - - {% block content %}{% endblock %} - - + + + +
+ {% block content %}{% endblock %} +
+ + + + + + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..ffad65e 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,170 @@ {% extends "base.html" %} - -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% load static %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… + +
+
+
+
+

Selamat Datang di Pasar UMKM Desa Pemagarsari

+

Pasar digital untuk produk unggulan lokal dari desa kita sendiri. Berdayakan ekonomi lokal melalui kemudahan transaksi via WhatsApp.

+ +
+
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- + + + + + + +
+
+
+

Kategori Pilihan

+
+
+ {% if categories %} + {% for category in categories %} + + {% endfor %} + {% else %} + + + + + + + {% endif %} +
+
+
+ + +
+
+
+
+

Produk Terbaru

+

Temukan yang baru dari UMKM Desa Pemagarsari

+
+ + Lihat Semua + +
+
+ {% if featured_products %} + {% for product in featured_products %} +
+
+
+ {% if product.image %} + {{ product.name }} + {% else %} +
+ +
+ {% endif %} + {{ product.category.name }} +
+
+
+ {{ product.name }} +
+

{{ product.vendor.name }}

+
+
+ Rp {{ product.price|floatformat:0 }} + +
+
+
+
+
+ {% endfor %} + {% else %} + + {% for i in "1234" %} +
+
+
+ +
+
+
Produk Contoh {{ i }}
+

Rp XX.XXX

+
+
+
+ {% endfor %} + {% endif %} +
+
+
+ + +
+
+

Punya Usaha di Pemagarsari?

+

Daftarkan usaha Anda sekarang dan jangkau lebih banyak pembeli secara online.

+ + Daftar Sebagai Vendor + +
+
{% endblock %} \ No newline at end of file diff --git a/core/templates/core/product_detail.html b/core/templates/core/product_detail.html new file mode 100644 index 0000000..4b7dd73 --- /dev/null +++ b/core/templates/core/product_detail.html @@ -0,0 +1,117 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}{{ product.name }} - Pasar UMKM Pemagarsari{% endblock %} + +{% block content %} +
+
+ + + +
+ +
+
+ {% if product.image %} + {{ product.name }} + {% else %} +
+ +
+ {% endif %} +
+
+ + +
+
+ {{ product.category.name }} +

{{ product.name }}

+ +
+ Rp {{ product.price|floatformat:0 }} + {% if product.stock > 0 %} + Stok Tersedia + {% else %} + Stok Habis + {% endif %} +
+ +
+
+
Informasi Produk
+

{{ product.description }}

+
+
+ +
+
+
+ {% if product.vendor.logo %} + + {% else %} +
+ +
+ {% endif %} +
+
+
Dijual oleh:
+
{{ product.vendor.name }}
+
+ +
+
+ + +
+ +

Transaksi aman langsung ke penjual.

+
+
+
+
+ + +
+

Produk Serupa

+
+ {% for rel_prod in related_products %} +
+
+
+ {% if rel_prod.image %} + {{ rel_prod.name }} + {% else %} +
+ +
+ {% endif %} +
+
+
+ {{ rel_prod.name }} +
+

Rp {{ rel_prod.price|floatformat:0 }}

+
+
+
+ {% empty %} +
Belum ada produk serupa yang tersedia.
+ {% endfor %} +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/product_list.html b/core/templates/core/product_list.html new file mode 100644 index 0000000..24e9866 --- /dev/null +++ b/core/templates/core/product_list.html @@ -0,0 +1,98 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %} + {% if category %}{{ category.name }}{% else %}Semua Produk{% endif %} - Pasar UMKM Pemagarsari +{% endblock %} + +{% block content %} +
+
+

{% if category %}{{ category.name }}{% else %}Semua Produk{% endif %}

+

Jelajahi berbagai pilihan produk terbaik dari UMKM Desa Pemagarsari.

+
+
+ +
+
+
+ +
+ +
+ + Semua + + {% for cat in categories %} + + {{ cat.name }} + + {% endfor %} +
+ + +
+
+
Kategori
+
+ + Semua Kategori + + {% for cat in categories %} + + {{ cat.name }} + + {% endfor %} +
+
+
+
+ + +
+
+ {% if products %} + {% for product in products %} +
+
+
+ {% if product.image %} + {{ product.name }} + {% else %} +
+ +
+ {% endif %} + {{ product.category.name }} +
+
+
+ {{ product.name }} +
+

{{ product.vendor.name }}

+
+
+ Rp {{ product.price|floatformat:0 }} + +
+
+
+
+
+ {% endfor %} + {% else %} +
+ +

Maaf, belum ada produk yang tersedia.

+

Coba cari di kategori lain atau kembali lagi nanti.

+ Lihat Semua Produk +
+ {% endif %} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/vendor_detail.html b/core/templates/core/vendor_detail.html new file mode 100644 index 0000000..425cf4c --- /dev/null +++ b/core/templates/core/vendor_detail.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}{{ vendor.name }} - UMKM Desa Pemagarsari{% endblock %} + +{% block content %} + +
+
+
+ {% if vendor.logo %} + + {% else %} +
+ +
+ {% endif %} +
+

{{ vendor.name }}

+

{{ vendor.description|default:"Toko terpercaya di Desa Pemagarsari." }}

+ +
+ + Chat Penjual + +
+ {{ vendor.address|default:"Desa Pemagarsari" }} +
+
+
+
+ + +
+
+
+
+

Produk Dari Toko Ini

+

Katalog lengkap {{ vendor.name }}

+
+ {{ products|length }} Produk +
+ +
+ {% if products %} + {% for product in products %} +
+
+
+ {% if product.image %} + {{ product.name }} + {% else %} +
+ +
+ {% endif %} +
+
+
+ {{ product.name }} +
+

Rp {{ product.price|floatformat:0 }}

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

Maaf, toko ini belum memiliki produk.

+

Silakan kembali lagi nanti atau lihat produk dari toko lain.

+ Jelajahi Produk Lain +
+ {% endif %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 6299e3d..f735da5 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,10 @@ from django.urls import path - -from .views import home +from . import views urlpatterns = [ - path("", home, name="home"), -] + path("", views.home, name="home"), + path("produk/", views.product_list, name="product_list"), + path("kategori//", views.product_list, name="product_list_by_category"), + path("produk//", views.product_detail, name="product_detail"), + path("toko//", views.vendor_detail, name="vendor_detail"), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c9aed12..af76ac2 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,56 @@ import os import platform - -from django import get_version as django_version -from django.shortcuts import render +from django.shortcuts import render, get_object_or_404 from django.utils import timezone - +from .models import Category, Vendor, Product def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() + """Render the landing screen with marketplace products and categories.""" + categories = Category.objects.all() + featured_products = Product.objects.filter(is_active=True).order_by('-created_at')[:8] + vendors = Vendor.objects.all()[:6] context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + "project_name": "Pasar UMKM Desa Pemagarsari", + "categories": categories, + "featured_products": featured_products, + "vendors": vendors, } return render(request, "core/index.html", context) + +def product_list(request, category_slug=None): + """List all products, optionally filtered by category.""" + category = None + products = Product.objects.filter(is_active=True) + if category_slug: + category = get_object_or_404(Category, slug=category_slug) + products = products.filter(category=category) + + context = { + 'category': category, + 'products': products, + 'categories': Category.objects.all(), + } + return render(request, "core/product_list.html", context) + +def product_detail(request, slug): + """Show details of a single product.""" + product = get_object_or_404(Product, slug=slug, is_active=True) + related_products = Product.objects.filter(category=product.category).exclude(id=product.id)[:4] + + context = { + 'product': product, + 'related_products': related_products, + } + return render(request, "core/product_detail.html", context) + +def vendor_detail(request, slug): + """Show details of a single vendor and their products.""" + vendor = get_object_or_404(Vendor, slug=slug) + products = vendor.products.filter(is_active=True) + + context = { + 'vendor': vendor, + 'products': products, + } + return render(request, "core/vendor_detail.html", context) \ No newline at end of file diff --git a/media/pexels/15936702.jpg b/media/pexels/15936702.jpg new file mode 100644 index 0000000..6a8a332 Binary files /dev/null and b/media/pexels/15936702.jpg differ diff --git a/media/pexels/2074123.jpg b/media/pexels/2074123.jpg new file mode 100644 index 0000000..49a1cfa Binary files /dev/null and b/media/pexels/2074123.jpg differ diff --git a/media/pexels/221004.jpg b/media/pexels/221004.jpg new file mode 100644 index 0000000..7a3246e Binary files /dev/null and b/media/pexels/221004.jpg differ diff --git a/media/pexels/291539.jpg b/media/pexels/291539.jpg new file mode 100644 index 0000000..1cc4e0d Binary files /dev/null and b/media/pexels/291539.jpg differ diff --git a/media/pexels/35862670.jpg b/media/pexels/35862670.jpg new file mode 100644 index 0000000..7150312 Binary files /dev/null and b/media/pexels/35862670.jpg differ diff --git a/media/pexels/7429685.jpg b/media/pexels/7429685.jpg new file mode 100644 index 0000000..9eab893 Binary files /dev/null and b/media/pexels/7429685.jpg differ diff --git a/media/pexels/7438803.jpg b/media/pexels/7438803.jpg new file mode 100644 index 0000000..030591d Binary files /dev/null and b/media/pexels/7438803.jpg differ diff --git a/media/pexels/8995275.jpg b/media/pexels/8995275.jpg new file mode 100644 index 0000000..e7c9f78 Binary files /dev/null and b/media/pexels/8995275.jpg differ diff --git a/media/pexels/942801.jpg b/media/pexels/942801.jpg new file mode 100644 index 0000000..cbdeb1d Binary files /dev/null and b/media/pexels/942801.jpg differ diff --git a/requirements.txt b/requirements.txt index e22994c..80c2164 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django==5.2.7 mysqlclient==2.2.7 python-dotenv==1.1.1 +httpx diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..3ccc131 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,306 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +/* Custom styles for Pasar UMKM Desa Pemagarsari */ + +:root { + --primary: #2D6A4F; + --primary-dark: #1B4332; + --secondary: #95D5B2; + --accent: #FFB703; + --bg-light: #F8F9FA; + --text-dark: #212529; + --shadow-sm: 0 2px 8px rgba(0,0,0,.05); + --shadow-md: 0 4px 12px rgba(0,0,0,.08); + --border-radius: 15px; } + +body { + font-family: 'Open Sans', sans-serif; + background-color: var(--bg-light); + color: var(--text-dark); + font-size: 0.95rem; +} + +@media (min-width: 768px) { + body { + font-size: 1rem; + } +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Montserrat', sans-serif; + font-weight: 700; + color: var(--primary-dark); +} + +/* Navbar adjustments */ +.navbar { + background-color: var(--primary); + box-shadow: 0 2px 10px rgba(0,0,0,.15); + padding: 0.5rem 0; +} + +.navbar-brand { + font-family: 'Montserrat', sans-serif; + font-weight: 800; + letter-spacing: -0.5px; + font-size: 1.15rem; +} + +/* Hero Section - Mobile First */ +.hero-section { + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); + color: white; + padding: 40px 0; + border-radius: 0 0 20px 20px; +} + +.hero-section h1 { + font-size: 1.75rem; + line-height: 1.2; +} + +.hero-section .lead { + font-size: 1rem !important; + margin-bottom: 1.5rem !important; +} + +@media (min-width: 768px) { + .hero-section { + padding: 60px 0; + border-radius: 0 0 40px 40px; + } + .hero-section h1 { + font-size: 2.75rem; + } + .hero-section .lead { + font-size: 1.15rem !important; + margin-bottom: 2rem !important; + } +} + +/* Buttons */ +.btn { + border-radius: 10px; + padding: 10px 20px; + font-weight: 600; + transition: all 0.3s ease; +} + +.btn-lg { + padding: 12px 25px; + border-radius: 12px; +} + +.btn-primary { + background-color: var(--primary); + border-color: var(--primary); +} + +.btn-primary:hover { + background-color: var(--primary-dark); + border-color: var(--primary-dark); + transform: translateY(-2px); +} + +.btn-accent { + background-color: var(--accent); + color: var(--text-dark); + border: none; + box-shadow: 0 4px 15px rgba(255, 183, 3, 0.3); +} + +.btn-accent:hover { + background-color: #E9A602; + color: var(--text-dark); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(255, 183, 3, 0.4); +} + +/* Cards */ +.card { + border: none; + border-radius: var(--border-radius); + overflow: hidden; + box-shadow: var(--shadow-sm); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +@media (min-width: 992px) { + .card:hover { + transform: translateY(-8px); + box-shadow: var(--shadow-md); + } +} + +/* Category Pills - Mobile Responsive */ +.category-pill { + background: white; + padding: 12px 15px; + border-radius: 50px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + text-decoration: none; + color: var(--text-dark); + font-weight: 600; + font-size: 0.85rem; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; + height: 100%; + text-align: center; +} + +.category-pill i { + font-size: 1.25rem; +} + +@media (min-width: 768px) { + .category-pill { + flex-direction: row; + padding: 15px 25px; + font-size: 1rem; + } +} + +.category-pill:hover { + background: var(--secondary); + color: var(--primary-dark); + transform: scale(1.05); +} + +/* Product Pricing */ +.product-price { + color: var(--primary); + font-weight: 800; + font-size: 1.1rem; +} + +@media (min-width: 768px) { + .product-price { + font-size: 1.25rem; + } +} + +/* WA Button */ +.wa-btn { + background-color: #25D366; + color: white; + font-weight: 700; + border: none; +} + +.wa-btn:hover { + background-color: #128C7E; + color: white; +} + +/* Footer */ +.footer { + background-color: var(--primary-dark); + color: rgba(255,255,255,.8); + padding: 60px 0 20px; +} + +.footer h5 { + color: white; + margin-bottom: 25px; + font-size: 1.1rem; +} + +.footer-link { + color: rgba(255,255,255,.6); + text-decoration: none; + display: block; + margin-bottom: 12px; + transition: color 0.2s ease; +} + +.footer-link:hover { + color: white; + padding-left: 5px; +} + +/* Utility Classes */ +.max-width-700 { + max-width: 700px; +} + +.section-title { + position: relative; + display: inline-block; + margin-bottom: 1.5rem; +} + +.section-title::after { + content: ''; + position: absolute; + bottom: -10px; + left: 50%; + transform: translateX(-50%); + width: 50px; + height: 4px; + background-color: var(--accent); + border-radius: 2px; +} + +@media (max-width: 576px) { + .container { + padding-left: 20px; + padding-right: 20px; + } + + .py-5 { + padding-top: 2.5rem !important; + padding-bottom: 2.5rem !important; + } +} + +/* Product Card Improvements for Mobile Grid */ +@media (max-width: 767px) { + .card-body { + padding: 1rem !important; + } + + .card-title { + font-size: 0.95rem; + height: 2.4em; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + + .product-price { + display: block; + margin-bottom: 0.5rem; + } + + .wa-btn { + width: 100%; + padding: 8px; + font-size: 0.85rem; + } +} + +/* Banner Section */ +.banner-promo { + background-color: #fff; + padding-top: 2rem !important; + padding-bottom: 2rem !important; +} + +.banner-wrapper { + transition: transform 0.3s ease, box-shadow 0.3s ease; + border-radius: 15px; +} + +.banner-wrapper:hover { + transform: scale(1.01); + box-shadow: 0 10px 30px rgba(0,0,0,.12) !important; +} + +.banner-wrapper img { + border-radius: 15px; + display: block; +} \ No newline at end of file diff --git a/static/images/banner-umkm.png b/static/images/banner-umkm.png new file mode 100644 index 0000000..869309f Binary files /dev/null and b/static/images/banner-umkm.png differ diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..3ccc131 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,306 @@ +/* Custom styles for Pasar UMKM Desa Pemagarsari */ :root { - --bg-color-start: #6a11cb; - --bg-color-end: #2575fc; - --text-color: #ffffff; - --card-bg-color: rgba(255, 255, 255, 0.01); - --card-border-color: rgba(255, 255, 255, 0.1); + --primary: #2D6A4F; + --primary-dark: #1B4332; + --secondary: #95D5B2; + --accent: #FFB703; + --bg-light: #F8F9FA; + --text-dark: #212529; + --shadow-sm: 0 2px 8px rgba(0,0,0,.05); + --shadow-md: 0 4px 12px rgba(0,0,0,.08); + --border-radius: 15px; } + body { - margin: 0; - font-family: 'Inter', sans-serif; - background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); - color: var(--text-color); - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; - text-align: center; - overflow: hidden; - position: relative; + font-family: 'Open Sans', sans-serif; + background-color: var(--bg-light); + color: var(--text-dark); + font-size: 0.95rem; } + +@media (min-width: 768px) { + body { + font-size: 1rem; + } +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Montserrat', sans-serif; + font-weight: 700; + color: var(--primary-dark); +} + +/* Navbar adjustments */ +.navbar { + background-color: var(--primary); + box-shadow: 0 2px 10px rgba(0,0,0,.15); + padding: 0.5rem 0; +} + +.navbar-brand { + font-family: 'Montserrat', sans-serif; + font-weight: 800; + letter-spacing: -0.5px; + font-size: 1.15rem; +} + +/* Hero Section - Mobile First */ +.hero-section { + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); + color: white; + padding: 40px 0; + border-radius: 0 0 20px 20px; +} + +.hero-section h1 { + font-size: 1.75rem; + line-height: 1.2; +} + +.hero-section .lead { + font-size: 1rem !important; + margin-bottom: 1.5rem !important; +} + +@media (min-width: 768px) { + .hero-section { + padding: 60px 0; + border-radius: 0 0 40px 40px; + } + .hero-section h1 { + font-size: 2.75rem; + } + .hero-section .lead { + font-size: 1.15rem !important; + margin-bottom: 2rem !important; + } +} + +/* Buttons */ +.btn { + border-radius: 10px; + padding: 10px 20px; + font-weight: 600; + transition: all 0.3s ease; +} + +.btn-lg { + padding: 12px 25px; + border-radius: 12px; +} + +.btn-primary { + background-color: var(--primary); + border-color: var(--primary); +} + +.btn-primary:hover { + background-color: var(--primary-dark); + border-color: var(--primary-dark); + transform: translateY(-2px); +} + +.btn-accent { + background-color: var(--accent); + color: var(--text-dark); + border: none; + box-shadow: 0 4px 15px rgba(255, 183, 3, 0.3); +} + +.btn-accent:hover { + background-color: #E9A602; + color: var(--text-dark); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(255, 183, 3, 0.4); +} + +/* Cards */ +.card { + border: none; + border-radius: var(--border-radius); + overflow: hidden; + box-shadow: var(--shadow-sm); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +@media (min-width: 992px) { + .card:hover { + transform: translateY(-8px); + box-shadow: var(--shadow-md); + } +} + +/* Category Pills - Mobile Responsive */ +.category-pill { + background: white; + padding: 12px 15px; + border-radius: 50px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + text-decoration: none; + color: var(--text-dark); + font-weight: 600; + font-size: 0.85rem; + box-shadow: var(--shadow-sm); + transition: all 0.3s ease; + height: 100%; + text-align: center; +} + +.category-pill i { + font-size: 1.25rem; +} + +@media (min-width: 768px) { + .category-pill { + flex-direction: row; + padding: 15px 25px; + font-size: 1rem; + } +} + +.category-pill:hover { + background: var(--secondary); + color: var(--primary-dark); + transform: scale(1.05); +} + +/* Product Pricing */ +.product-price { + color: var(--primary); + font-weight: 800; + font-size: 1.1rem; +} + +@media (min-width: 768px) { + .product-price { + font-size: 1.25rem; + } +} + +/* WA Button */ +.wa-btn { + background-color: #25D366; + color: white; + font-weight: 700; + border: none; +} + +.wa-btn:hover { + background-color: #128C7E; + color: white; +} + +/* Footer */ +.footer { + background-color: var(--primary-dark); + color: rgba(255,255,255,.8); + padding: 60px 0 20px; +} + +.footer h5 { + color: white; + margin-bottom: 25px; + font-size: 1.1rem; +} + +.footer-link { + color: rgba(255,255,255,.6); + text-decoration: none; + display: block; + margin-bottom: 12px; + transition: color 0.2s ease; +} + +.footer-link:hover { + color: white; + padding-left: 5px; +} + +/* Utility Classes */ +.max-width-700 { + max-width: 700px; +} + +.section-title { + position: relative; + display: inline-block; + margin-bottom: 1.5rem; +} + +.section-title::after { + content: ''; + position: absolute; + bottom: -10px; + left: 50%; + transform: translateX(-50%); + width: 50px; + height: 4px; + background-color: var(--accent); + border-radius: 2px; +} + +@media (max-width: 576px) { + .container { + padding-left: 20px; + padding-right: 20px; + } + + .py-5 { + padding-top: 2.5rem !important; + padding-bottom: 2.5rem !important; + } +} + +/* Product Card Improvements for Mobile Grid */ +@media (max-width: 767px) { + .card-body { + padding: 1rem !important; + } + + .card-title { + font-size: 0.95rem; + height: 2.4em; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + + .product-price { + display: block; + margin-bottom: 0.5rem; + } + + .wa-btn { + width: 100%; + padding: 8px; + font-size: 0.85rem; + } +} + +/* Banner Section */ +.banner-promo { + background-color: #fff; + padding-top: 2rem !important; + padding-bottom: 2rem !important; +} + +.banner-wrapper { + transition: transform 0.3s ease, box-shadow 0.3s ease; + border-radius: 15px; +} + +.banner-wrapper:hover { + transform: scale(1.01); + box-shadow: 0 10px 30px rgba(0,0,0,.12) !important; +} + +.banner-wrapper img { + border-radius: 15px; + display: block; +} \ No newline at end of file diff --git a/staticfiles/images/banner-umkm.png b/staticfiles/images/banner-umkm.png new file mode 100644 index 0000000..869309f Binary files /dev/null and b/staticfiles/images/banner-umkm.png differ diff --git a/staticfiles/pasted-20260220-193237-23a07b2b.png b/staticfiles/pasted-20260220-193237-23a07b2b.png new file mode 100644 index 0000000..869309f Binary files /dev/null and b/staticfiles/pasted-20260220-193237-23a07b2b.png differ