diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc
index 896bb4f..152c8c0 100644
Binary files a/config/__pycache__/__init__.cpython-311.pyc and b/config/__pycache__/__init__.cpython-311.pyc differ
diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc
index d79d6a7..8e88511 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 8cf22af..4d96c85 100644
Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ
diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc
index a1b4aa7..87f06e1 100644
Binary files a/config/__pycache__/wsgi.cpython-311.pyc and b/config/__pycache__/wsgi.cpython-311.pyc differ
diff --git a/config/settings.py b/config/settings.py
index 291d043..d92100d 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -149,6 +149,9 @@ STATIC_URL = 'static/'
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
STATIC_ROOT = BASE_DIR / 'staticfiles'
+MEDIA_URL = '/media/'
+MEDIA_ROOT = BASE_DIR / 'media'
+
STATICFILES_DIRS = [
BASE_DIR / 'static',
BASE_DIR / 'assets',
diff --git a/config/urls.py b/config/urls.py
index bcfc074..1a48858 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)
diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc
index 3f553f6..09d3063 100644
Binary files a/core/__pycache__/__init__.cpython-311.pyc and b/core/__pycache__/__init__.cpython-311.pyc differ
diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc
index 5e8987a..55bb8c0 100644
Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ
diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc
index 2fa4a49..463a1a5 100644
Binary files a/core/__pycache__/apps.cpython-311.pyc and b/core/__pycache__/apps.cpython-311.pyc differ
diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc
index 75bf223..78e13ff 100644
Binary files a/core/__pycache__/context_processors.cpython-311.pyc and b/core/__pycache__/context_processors.cpython-311.pyc differ
diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc
new file mode 100644
index 0000000..c57a000
Binary files /dev/null and b/core/__pycache__/forms.cpython-311.pyc differ
diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc
index a251b5f..0441c3e 100644
Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ
diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc
index f705988..ee47045 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 2f0989c..6705550 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..5a04705 100644
--- a/core/admin.py
+++ b/core/admin.py
@@ -1,3 +1,40 @@
from django.contrib import admin
+from django.utils import timezone
-# Register your models here.
+from .models import PaymentConfirmation, Product, ProductCategory
+
+
+@admin.register(ProductCategory)
+class ProductCategoryAdmin(admin.ModelAdmin):
+ list_display = ("name", "slug", "is_active", "sort_order")
+ list_editable = ("is_active", "sort_order")
+ prepopulated_fields = {"slug": ("name",)}
+ search_fields = ("name", "description")
+
+
+@admin.register(Product)
+class ProductAdmin(admin.ModelAdmin):
+ list_display = ("name", "category", "price_from", "delivery_time", "is_featured", "is_active")
+ list_filter = ("category", "is_featured", "is_active")
+ list_editable = ("price_from", "is_featured", "is_active")
+ prepopulated_fields = {"slug": ("name",)}
+ search_fields = ("name", "short_description", "description")
+
+
+@admin.register(PaymentConfirmation)
+class PaymentConfirmationAdmin(admin.ModelAdmin):
+ list_display = ("customer_name", "product", "method", "amount", "reference", "status", "created_at")
+ list_filter = ("status", "method", "created_at")
+ search_fields = ("customer_name", "customer_phone", "customer_email", "reference")
+ readonly_fields = ("created_at",)
+ actions = ("approve_payments", "reject_payments")
+
+ @admin.action(description="Aprobar pagos seleccionados")
+ def approve_payments(self, request, queryset):
+ updated = queryset.update(status=PaymentConfirmation.STATUS_APPROVED, reviewed_at=timezone.now())
+ self.message_user(request, f"{updated} confirmación(es) aprobada(s).")
+
+ @admin.action(description="Rechazar pagos seleccionados")
+ def reject_payments(self, request, queryset):
+ updated = queryset.update(status=PaymentConfirmation.STATUS_REJECTED, reviewed_at=timezone.now())
+ self.message_user(request, f"{updated} confirmación(es) rechazada(s).")
diff --git a/core/forms.py b/core/forms.py
new file mode 100644
index 0000000..1418d06
--- /dev/null
+++ b/core/forms.py
@@ -0,0 +1,59 @@
+from django import forms
+
+from .models import PaymentConfirmation
+
+
+class PaymentConfirmationForm(forms.ModelForm):
+ class Meta:
+ model = PaymentConfirmation
+ fields = [
+ "product",
+ "customer_name",
+ "customer_phone",
+ "customer_email",
+ "amount",
+ "method",
+ "reference",
+ "note",
+ "receipt",
+ ]
+ widgets = {
+ "product": forms.Select(attrs={"class": "form-select"}),
+ "customer_name": forms.TextInput(attrs={"class": "form-control", "placeholder": "Tu nombre o negocio"}),
+ "customer_phone": forms.TextInput(attrs={"class": "form-control", "placeholder": "+53..."}),
+ "customer_email": forms.EmailInput(attrs={"class": "form-control", "placeholder": "correo@ejemplo.com"}),
+ "amount": forms.NumberInput(attrs={"class": "form-control", "min": "1", "step": "0.01", "placeholder": "Monto pagado"}),
+ "method": forms.Select(attrs={"class": "form-select"}),
+ "reference": forms.TextInput(attrs={"class": "form-control", "placeholder": "No. operación / referencia"}),
+ "note": forms.Textarea(attrs={"class": "form-control", "rows": 4, "placeholder": "Detalles útiles: fecha, banco, servicio solicitado..."}),
+ "receipt": forms.ClearableFileInput(attrs={"class": "form-control"}),
+ }
+ labels = {
+ "product": "Producto o servicio",
+ "customer_name": "Nombre",
+ "customer_phone": "Teléfono / WhatsApp",
+ "customer_email": "Correo (opcional)",
+ "amount": "Monto pagado",
+ "method": "Método de pago",
+ "reference": "Referencia de pago",
+ "note": "Nota para el equipo",
+ "receipt": "Comprobante (opcional)",
+ }
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields["product"].queryset = self.fields["product"].queryset.filter(is_active=True).select_related("category")
+ self.fields["product"].empty_label = "Selecciona un servicio"
+
+ def clean_customer_phone(self):
+ phone = self.cleaned_data["customer_phone"].strip()
+ digits = [char for char in phone if char.isdigit()]
+ if len(digits) < 8:
+ raise forms.ValidationError("Escribe un teléfono válido para poder contactarte.")
+ return phone
+
+ def clean_reference(self):
+ reference = self.cleaned_data["reference"].strip()
+ if len(reference) < 4:
+ raise forms.ValidationError("La referencia debe tener al menos 4 caracteres.")
+ return reference
diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py
new file mode 100644
index 0000000..690f90f
--- /dev/null
+++ b/core/migrations/0001_initial.py
@@ -0,0 +1,84 @@
+# Generated by Django 5.2.7 on 2026-06-30 00:15
+
+import django.core.validators
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Product',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=160)),
+ ('slug', models.SlugField(blank=True, max_length=180, unique=True)),
+ ('short_description', models.CharField(max_length=240)),
+ ('description', models.TextField()),
+ ('price_from', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])),
+ ('delivery_time', models.CharField(default='A convenir', max_length=80)),
+ ('is_featured', models.BooleanField(default=False)),
+ ('is_active', models.BooleanField(default=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ 'verbose_name': 'producto/servicio',
+ 'verbose_name_plural': 'productos y servicios',
+ 'ordering': ['category__sort_order', 'name'],
+ },
+ ),
+ migrations.CreateModel(
+ name='ProductCategory',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=120, unique=True)),
+ ('slug', models.SlugField(blank=True, max_length=140, unique=True)),
+ ('description', models.TextField(blank=True)),
+ ('accent', models.CharField(default='#00D1FF', max_length=20)),
+ ('icon', models.CharField(default='bi-cpu', max_length=40)),
+ ('is_active', models.BooleanField(default=True)),
+ ('sort_order', models.PositiveIntegerField(default=0)),
+ ],
+ options={
+ 'verbose_name': 'categoría de producto',
+ 'verbose_name_plural': 'categorías de productos',
+ 'ordering': ['sort_order', 'name'],
+ },
+ ),
+ migrations.CreateModel(
+ name='PaymentConfirmation',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('customer_name', models.CharField(max_length=140)),
+ ('customer_phone', models.CharField(max_length=40)),
+ ('customer_email', models.EmailField(blank=True, max_length=254)),
+ ('amount', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(1)])),
+ ('method', models.CharField(choices=[('transfermovil', 'Transfermóvil'), ('enzona', 'Enzona'), ('qr', 'Código QR'), ('transferencia', 'Transferencia bancaria')], max_length=30)),
+ ('reference', models.CharField(max_length=120)),
+ ('note', models.TextField(blank=True)),
+ ('receipt', models.FileField(blank=True, upload_to='payment_receipts/')),
+ ('status', models.CharField(choices=[('pendiente', 'Pendiente'), ('aprobado', 'Aprobado'), ('rechazado', 'Rechazado')], default='pendiente', max_length=20)),
+ ('admin_note', models.TextField(blank=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('reviewed_at', models.DateTimeField(blank=True, null=True)),
+ ('product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='payment_confirmations', to='core.product')),
+ ],
+ options={
+ 'verbose_name': 'confirmación de pago',
+ 'verbose_name_plural': 'confirmaciones de pago',
+ 'ordering': ['-created_at'],
+ },
+ ),
+ migrations.AddField(
+ model_name='product',
+ name='category',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='core.productcategory'),
+ ),
+ ]
diff --git a/core/migrations/0002_seed_tech_store.py b/core/migrations/0002_seed_tech_store.py
new file mode 100644
index 0000000..5c17691
--- /dev/null
+++ b/core/migrations/0002_seed_tech_store.py
@@ -0,0 +1,65 @@
+# Generated seed data for the initial technology store MVP.
+from django.db import migrations
+
+
+def seed_catalog(apps, schema_editor):
+ ProductCategory = apps.get_model("core", "ProductCategory")
+ Product = apps.get_model("core", "Product")
+
+ categories = [
+ ("Software Empresarial", "software-empresarial", "Sistemas de gestión, inventario, ventas y operaciones para negocios.", "bi-window-stack", 1),
+ ("Hardware & Equipos", "hardware-equipos", "Equipos, componentes y kits tecnológicos para oficinas y proyectos.", "bi-pc-display", 2),
+ ("Desarrollo Web", "desarrollo-web", "Páginas web, tiendas virtuales y landings profesionales para vender online.", "bi-code-slash", 3),
+ ("Apps Móviles", "apps-moviles", "Aplicaciones móviles a medida para clientes, equipos y procesos internos.", "bi-phone", 4),
+ ("Soporte Técnico", "soporte-tecnico", "Mantenimiento, optimización, asistencia remota y soporte preventivo.", "bi-tools", 5),
+ ]
+
+ created_categories = {}
+ for name, slug, description, icon, order in categories:
+ category, _ = ProductCategory.objects.update_or_create(
+ slug=slug,
+ defaults={"name": name, "description": description, "icon": icon, "sort_order": order, "is_active": True},
+ )
+ created_categories[slug] = category
+
+ products = [
+ ("software-empresarial", "Software Empresarial a Medida", "Gestiona ventas, clientes, inventario y reportes desde un panel propio.", "Diseñamos un sistema empresarial ajustado a tu operación: roles, panel administrativo, reportes, base de datos segura y módulos escalables para crecer.", 250.00, "Desde 15 días", True),
+ ("hardware-equipos", "Hardware & Equipos", "Asesoría y venta de equipos para oficinas, tiendas y proyectos digitales.", "Te ayudamos a seleccionar equipos, periféricos y componentes adecuados para tu negocio, con orientación técnica y soporte de instalación.", 80.00, "Según disponibilidad", True),
+ ("desarrollo-web", "Tienda Virtual Profesional", "Catálogo, carrito, pagos, confirmaciones y panel admin para vender online.", "Creamos tu tienda virtual con diseño moderno, catálogo administrable, integración de contacto y flujo de confirmación de pagos por QR, Transfermóvil o Enzona.", 180.00, "Desde 7 días", True),
+ ("desarrollo-web", "Página Web Corporativa", "Sitio moderno para presentar servicios, captar leads y posicionar tu marca.", "Landing o web multi-sección con diseño responsive, SEO base, formularios de contacto y llamadas a la acción para convertir visitas en clientes.", 120.00, "Desde 5 días", True),
+ ("apps-moviles", "Aplicación Móvil para Clientes", "App Android/iOS para reservas, pedidos, notificaciones o gestión interna.", "Construimos apps móviles conectadas a backend, con experiencia de usuario clara, flujos de autenticación y panel para administrar contenido.", 320.00, "Desde 20 días", True),
+ ("soporte-tecnico", "Soporte Técnico y Mantenimiento", "Diagnóstico, instalación, optimización y asistencia remota para equipos y sistemas.", "Servicio técnico para mantener tu operación estable: revisiones, instalación de software, respaldo de información y acompañamiento continuo.", 25.00, "24-48 horas", True),
+ ]
+
+ for category_slug, name, short, description, price, delivery, featured in products:
+ Product.objects.update_or_create(
+ slug=name.lower().replace(" & ", "-").replace(" ", "-").replace("á", "a").replace("é", "e").replace("í", "i").replace("ó", "o").replace("ú", "u"),
+ defaults={
+ "category": created_categories[category_slug],
+ "name": name,
+ "short_description": short,
+ "description": description,
+ "price_from": price,
+ "delivery_time": delivery,
+ "is_featured": featured,
+ "is_active": True,
+ },
+ )
+
+
+def unseed_catalog(apps, schema_editor):
+ ProductCategory = apps.get_model("core", "ProductCategory")
+ ProductCategory.objects.filter(slug__in=[
+ "software-empresarial", "hardware-equipos", "desarrollo-web", "apps-moviles", "soporte-tecnico"
+ ]).delete()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("core", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.RunPython(seed_catalog, unseed_catalog),
+ ]
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..091456d
Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ
diff --git a/core/migrations/__pycache__/0002_seed_tech_store.cpython-311.pyc b/core/migrations/__pycache__/0002_seed_tech_store.cpython-311.pyc
new file mode 100644
index 0000000..2d5c223
Binary files /dev/null and b/core/migrations/__pycache__/0002_seed_tech_store.cpython-311.pyc differ
diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc
index 7995815..bd1b292 100644
Binary files a/core/migrations/__pycache__/__init__.cpython-311.pyc and b/core/migrations/__pycache__/__init__.cpython-311.pyc differ
diff --git a/core/models.py b/core/models.py
index 71a8362..36f2eb3 100644
--- a/core/models.py
+++ b/core/models.py
@@ -1,3 +1,101 @@
+from django.core.validators import MinValueValidator
from django.db import models
+from django.urls import reverse
+from django.utils.text import slugify
-# Create your models here.
+
+class ProductCategory(models.Model):
+ name = models.CharField(max_length=120, unique=True)
+ slug = models.SlugField(max_length=140, unique=True, blank=True)
+ description = models.TextField(blank=True)
+ accent = models.CharField(max_length=20, default="#00D1FF")
+ icon = models.CharField(max_length=40, default="bi-cpu")
+ is_active = models.BooleanField(default=True)
+ sort_order = models.PositiveIntegerField(default=0)
+
+ class Meta:
+ ordering = ["sort_order", "name"]
+ verbose_name = "categoría de producto"
+ verbose_name_plural = "categorías de productos"
+
+ 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):
+ category = models.ForeignKey(ProductCategory, on_delete=models.PROTECT, related_name="products")
+ name = models.CharField(max_length=160)
+ slug = models.SlugField(max_length=180, unique=True, blank=True)
+ short_description = models.CharField(max_length=240)
+ description = models.TextField()
+ price_from = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(0)])
+ delivery_time = models.CharField(max_length=80, default="A convenir")
+ is_featured = models.BooleanField(default=False)
+ is_active = models.BooleanField(default=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ ordering = ["category__sort_order", "name"]
+ verbose_name = "producto/servicio"
+ verbose_name_plural = "productos y servicios"
+
+ def save(self, *args, **kwargs):
+ if not self.slug:
+ self.slug = slugify(self.name)
+ super().save(*args, **kwargs)
+
+ def get_absolute_url(self):
+ return reverse("product_detail", kwargs={"slug": self.slug})
+
+ def __str__(self):
+ return self.name
+
+
+class PaymentConfirmation(models.Model):
+ METHOD_TRANSFERMOVIL = "transfermovil"
+ METHOD_ENZONA = "enzona"
+ METHOD_QR = "qr"
+ METHOD_BANK = "transferencia"
+ PAYMENT_METHODS = [
+ (METHOD_TRANSFERMOVIL, "Transfermóvil"),
+ (METHOD_ENZONA, "Enzona"),
+ (METHOD_QR, "Código QR"),
+ (METHOD_BANK, "Transferencia bancaria"),
+ ]
+
+ STATUS_PENDING = "pendiente"
+ STATUS_APPROVED = "aprobado"
+ STATUS_REJECTED = "rechazado"
+ STATUS_CHOICES = [
+ (STATUS_PENDING, "Pendiente"),
+ (STATUS_APPROVED, "Aprobado"),
+ (STATUS_REJECTED, "Rechazado"),
+ ]
+
+ product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True, blank=True, related_name="payment_confirmations")
+ customer_name = models.CharField(max_length=140)
+ customer_phone = models.CharField(max_length=40)
+ customer_email = models.EmailField(blank=True)
+ amount = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(1)])
+ method = models.CharField(max_length=30, choices=PAYMENT_METHODS)
+ reference = models.CharField(max_length=120)
+ note = models.TextField(blank=True)
+ receipt = models.FileField(upload_to="payment_receipts/", blank=True)
+ status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PENDING)
+ admin_note = models.TextField(blank=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+ reviewed_at = models.DateTimeField(null=True, blank=True)
+
+ class Meta:
+ ordering = ["-created_at"]
+ verbose_name = "confirmación de pago"
+ verbose_name_plural = "confirmaciones de pago"
+
+ def __str__(self):
+ return f"{self.customer_name} · {self.get_method_display()} · {self.amount}"
diff --git a/core/templates/base.html b/core/templates/base.html
index 1e7e5fb..fa396d0 100644
--- a/core/templates/base.html
+++ b/core/templates/base.html
@@ -1,11 +1,13 @@
+{% load static %}
-
+
- {% block title %}Knowledge Base{% endblock %}
+
+ {% block title %}{{ page_title|default:project_name }} · App Tecno Store{% endblock %}
+
{% if project_description %}
-
{% endif %}
@@ -13,13 +15,62 @@
{% endif %}
- {% load static %}
+
+
+
+
+
{% block head %}{% endblock %}
+
+
+
Conectando soluciones digitales...
+
+
+
+
+
+
+ {% if messages %}
+
+ {% for message in messages %}
+
{{ message }}
+ {% endfor %}
+
+ {% endif %}
+
{% block content %}{% endblock %}
+
+
+
+
+
+
+
+
diff --git a/core/templates/core/catalog.html b/core/templates/core/catalog.html
new file mode 100644
index 0000000..55e56ba
--- /dev/null
+++ b/core/templates/core/catalog.html
@@ -0,0 +1,32 @@
+{% extends "base.html" %}
+
+{% block title %}{{ page_title }} · App Tecno Store{% endblock %}
+
+{% block content %}
+
+
+
+
Catálogo
+
{% if selected_category %}{{ selected_category.name }}{% else %}Productos y servicios tecnológicos{% endif %}
+
Filtra por categoría, revisa detalles y avanza con contacto o confirmación de pago.
+
+
+
+
+
+ {% for product in products %}
+ {% include "core/partials/product_card.html" with product=product %}
+ {% empty %}
+
+
No hay productos en esta categoría todavía. Entra al admin para activar nuevos servicios.
+
+ {% endfor %}
+
+
+
+{% endblock %}
diff --git a/core/templates/core/confirm_payment.html b/core/templates/core/confirm_payment.html
new file mode 100644
index 0000000..7d86036
--- /dev/null
+++ b/core/templates/core/confirm_payment.html
@@ -0,0 +1,43 @@
+{% extends "base.html" %}
+
+{% block title %}Confirmar pago · App Tecno Store{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
Confirmación manual
+
Reporta tu pago
+
Completa los datos de la operación. El administrador revisará la referencia y actualizará el estado desde el panel.
+
+
+
+
Métodos disponibles
+
+
Transfermóvil Enzona Transferencia
+
+
+
+
+
+
+{% endblock %}
diff --git a/core/templates/core/index.html b/core/templates/core/index.html
index faec813..ef7ff54 100644
--- a/core/templates/core/index.html
+++ b/core/templates/core/index.html
@@ -1,145 +1,108 @@
{% extends "base.html" %}
+{% load static %}
-{% block title %}{{ project_name }}{% endblock %}
-
-{% block head %}
-
-
-
-
-{% endblock %}
+{% block title %}{{ page_title }} · App Tecno Store{% endblock %}
{% block content %}
-
-
-
Analyzing your requirements and generating your app…
-
-
Loading…
+
+
+
+
+
+
+
Servicios activos
+
Catálogo listo para vender tecnología
+
Elige una categoría, abre el detalle y continúa por WhatsApp o confirmando tu pago.
+
+
+ {% for category in categories %}
+
+ {% empty %}
+
Aún no hay categorías. Puedes crearlas desde el panel admin.
+ {% endfor %}
+
+
+
+
+
+
+
+
+ Primeros productos
+
Soluciones destacadas
+
+
Explorar todo
+
+
+ {% for product in featured_products %}
+ {% include "core/partials/product_card.html" with product=product %}
+ {% empty %}
+
No hay productos destacados todavía.
+ {% endfor %}
+
+
+
+
+
+
+
+
+
Flujo de pago manual
+
Reporta tu operación y el admin la aprueba en el panel.
+
Diseñado para operar hoy mismo: el cliente paga por QR, Transfermóvil o Enzona, sube su referencia y el administrador revisa el estado desde Django Admin.
+
+
+
+
+
-
- Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
-
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/core/templates/core/partials/product_card.html b/core/templates/core/partials/product_card.html
new file mode 100644
index 0000000..acd9848
--- /dev/null
+++ b/core/templates/core/partials/product_card.html
@@ -0,0 +1,18 @@
+
+
+
+ {{ product.category.name }}
+
+
+ {{ product.name }}
+ {{ product.short_description }}
+
+ Desde ${{ product.price_from }}
+ {{ product.delivery_time }}
+
+
+
+
diff --git a/core/templates/core/payment_success.html b/core/templates/core/payment_success.html
new file mode 100644
index 0000000..ba2c117
--- /dev/null
+++ b/core/templates/core/payment_success.html
@@ -0,0 +1,26 @@
+{% extends "base.html" %}
+
+{% block title %}Pago recibido · App Tecno Store{% endblock %}
+
+{% block content %}
+
+
+
+
+
Solicitud recibida
+
Tu pago está pendiente de aprobación
+
Registramos la confirmación de {{ confirmation.customer_name }} . El admin revisará la referencia {{ confirmation.reference }} y actualizará el estado.
+
+
Método {{ confirmation.get_method_display }}
+
Monto ${{ confirmation.amount }}
+
Estado {{ confirmation.get_status_display }}
+
Servicio {{ confirmation.product|default:'No especificado' }}
+
+
+
+
+
+{% endblock %}
diff --git a/core/templates/core/product_detail.html b/core/templates/core/product_detail.html
new file mode 100644
index 0000000..a943b2e
--- /dev/null
+++ b/core/templates/core/product_detail.html
@@ -0,0 +1,46 @@
+{% extends "base.html" %}
+
+{% block title %}{{ product.name }} · App Tecno Store{% endblock %}
+
+{% block content %}
+
+
+
+
+
Volver al catálogo
+
+
{{ product.category.name }}
+
{{ product.name }}
+
{{ product.short_description }}
+
Desde ${{ product.price_from }}
+
{{ product.description|linebreaks }}
+
+
+
+
+
+
+ {% if related_products %}
+
+
También puede interesarte
+
+ {% for product in related_products %}
+ {% include "core/partials/product_card.html" with product=product %}
+ {% endfor %}
+
+
+ {% endif %}
+
+
+{% endblock %}
diff --git a/core/urls.py b/core/urls.py
index 6299e3d..4343ad2 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -1,7 +1,11 @@
from django.urls import path
-from .views import home
+from .views import catalog, confirm_payment, home, payment_success, product_detail
urlpatterns = [
path("", home, name="home"),
+ path("catalogo/", catalog, name="catalog"),
+ path("servicio/
/", product_detail, name="product_detail"),
+ path("confirmar-pago/", confirm_payment, name="confirm_payment"),
+ path("confirmar-pago//recibido/", payment_success, name="payment_success"),
]
diff --git a/core/views.py b/core/views.py
index c9aed12..32399a6 100644
--- a/core/views.py
+++ b/core/views.py
@@ -1,25 +1,120 @@
-import os
-import platform
+from django.contrib import messages
+from django.db.models import Count, Q
+from django.shortcuts import get_object_or_404, redirect, render
+from django.urls import reverse
-from django import get_version as django_version
-from django.shortcuts import render
-from django.utils import timezone
+from .forms import PaymentConfirmationForm
+from .models import PaymentConfirmation, Product, ProductCategory
+
+WHATSAPP_NUMBER = "5359177041"
+
+
+def _base_context(**extra):
+ context = {
+ "project_name": "App Tecno Store",
+ "project_description": "Tienda virtual de servicios tecnológicos con pagos por QR, Transfermóvil y Enzona.",
+ "whatsapp_number": WHATSAPP_NUMBER,
+ }
+ context.update(extra)
+ return context
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()
+ categories = ProductCategory.objects.filter(is_active=True).annotate(
+ active_products=Count("products", filter=Q(products__is_active=True))
+ )
+ featured_products = Product.objects.filter(is_active=True, is_featured=True).select_related("category")[:6]
+ recent_confirmations = PaymentConfirmation.objects.filter(status=PaymentConfirmation.STATUS_PENDING).count()
+ payment_form = PaymentConfirmationForm()
- 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", ""),
- }
- return render(request, "core/index.html", context)
+ return render(
+ request,
+ "core/index.html",
+ _base_context(
+ categories=categories,
+ featured_products=featured_products,
+ payment_form=payment_form,
+ pending_payments=recent_confirmations,
+ page_title="Tienda virtual de soluciones tecnológicas",
+ meta_description="Compra o cotiza software empresarial, hardware, desarrollo web, apps móviles y soporte técnico con pagos por QR, Transfermóvil o Enzona.",
+ ),
+ )
+
+
+def catalog(request):
+ category_slug = request.GET.get("categoria")
+ categories = ProductCategory.objects.filter(is_active=True)
+ products = Product.objects.filter(is_active=True).select_related("category")
+ selected_category = None
+ if category_slug:
+ selected_category = get_object_or_404(ProductCategory, slug=category_slug, is_active=True)
+ products = products.filter(category=selected_category)
+
+ return render(
+ request,
+ "core/catalog.html",
+ _base_context(
+ categories=categories,
+ products=products,
+ selected_category=selected_category,
+ page_title="Catálogo tecnológico",
+ meta_description="Explora productos y servicios tecnológicos listos para cotizar y pagar en línea.",
+ ),
+ )
+
+
+def product_detail(request, slug):
+ product = get_object_or_404(Product.objects.select_related("category"), slug=slug, is_active=True)
+ related_products = Product.objects.filter(is_active=True, category=product.category).exclude(pk=product.pk)[:3]
+ payment_form = PaymentConfirmationForm(initial={"product": product, "amount": product.price_from})
+ return render(
+ request,
+ "core/product_detail.html",
+ _base_context(
+ product=product,
+ related_products=related_products,
+ payment_form=payment_form,
+ page_title=product.name,
+ meta_description=product.short_description,
+ ),
+ )
+
+
+def confirm_payment(request):
+ initial = {}
+ product_slug = request.GET.get("producto")
+ if product_slug:
+ product = get_object_or_404(Product, slug=product_slug, is_active=True)
+ initial = {"product": product, "amount": product.price_from}
+
+ if request.method == "POST":
+ form = PaymentConfirmationForm(request.POST, request.FILES)
+ if form.is_valid():
+ confirmation = form.save()
+ messages.success(request, "Tu confirmación fue enviada. Un administrador revisará el pago en el panel.")
+ return redirect("payment_success", pk=confirmation.pk)
+ else:
+ form = PaymentConfirmationForm(initial=initial)
+
+ return render(
+ request,
+ "core/confirm_payment.html",
+ _base_context(
+ form=form,
+ page_title="Confirmar pago",
+ meta_description="Reporta tu pago por QR, Transfermóvil, Enzona o transferencia para revisión manual del administrador.",
+ ),
+ )
+
+
+def payment_success(request, pk):
+ confirmation = get_object_or_404(PaymentConfirmation.objects.select_related("product"), pk=pk)
+ return render(
+ request,
+ "core/payment_success.html",
+ _base_context(
+ confirmation=confirmation,
+ page_title="Pago recibido para revisión",
+ meta_description="Confirmación de pago recibida y pendiente de aprobación manual.",
+ ),
+ )
diff --git a/static/css/custom.css b/static/css/custom.css
index 925f6ed..2b8e2e3 100644
--- a/static/css/custom.css
+++ b/static/css/custom.css
@@ -1,4 +1,214 @@
-/* Custom styles for the application */
-body {
- font-family: system-ui, -apple-system, sans-serif;
+:root {
+ --navy: #061724;
+ --navy-2: #0b2436;
+ --primary: #00d1ff;
+ --secondary: #00f2a9;
+ --accent: #ffb703;
+ --paper: #f7fbff;
+ --muted: #6b7a88;
+ --ink: #10212f;
+ --line: rgba(10, 36, 54, 0.12);
+ --glass: rgba(255, 255, 255, 0.12);
+ --shadow: 0 24px 70px rgba(4, 18, 30, 0.18);
+}
+
+* { box-sizing: border-box; }
+
+body {
+ margin: 0;
+ font-family: 'Manrope', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ color: var(--ink);
+ background:
+ radial-gradient(circle at 10% 10%, rgba(0, 209, 255, 0.12), transparent 32rem),
+ radial-gradient(circle at 90% 0%, rgba(0, 242, 169, 0.10), transparent 30rem),
+ var(--paper);
+ overflow-x: hidden;
+}
+
+h1, h2, h3, .brand-mark { font-family: 'Space Grotesk', 'Manrope', sans-serif; letter-spacing: -0.04em; }
+a { text-decoration: none; }
+
+.app-nav {
+ background: rgba(6, 23, 36, 0.88);
+ backdrop-filter: blur(18px);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+}
+.brand-mark { font-weight: 800; display: inline-flex; align-items: center; gap: .55rem; }
+.brand-logo {
+ display: inline-grid;
+ place-items: center;
+ width: 2.75rem;
+ height: 2.75rem;
+ border-radius: 1rem;
+ color: var(--navy);
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
+ box-shadow: 0 10px 30px rgba(0, 209, 255, .3);
+ font-size: .86rem;
+}
+.navbar .nav-link { color: rgba(255,255,255,.76); font-weight: 700; }
+.navbar .nav-link:hover, .navbar .nav-link:focus { color: #fff; }
+
+.btn { border-radius: 999px; font-weight: 800; padding: .78rem 1.2rem; }
+.btn-accent {
+ color: #03131d;
+ border: 0;
+ background: linear-gradient(135deg, var(--secondary), var(--primary));
+ box-shadow: 0 14px 36px rgba(0, 209, 255, .28);
+}
+.btn-accent:hover, .btn-accent:focus { color: #03131d; transform: translateY(-1px); box-shadow: 0 18px 44px rgba(0, 209, 255, .35); }
+.btn-glass { color: #fff; border: 1px solid rgba(255,255,255,.28); background: rgba(255,255,255,.08); backdrop-filter: blur(12px); }
+.btn-glass:hover, .btn-glass:focus { color: #fff; border-color: rgba(255,255,255,.55); background: rgba(255,255,255,.16); }
+.btn-dark-soft { color: var(--navy); background: #e8f4fb; border: 1px solid rgba(6,23,36,.08); }
+.btn-dark-soft:hover, .btn-dark-soft:focus { color: #fff; background: var(--navy); }
+
+.tech-loader {
+ position: fixed;
+ inset: 0;
+ z-index: 2000;
+ display: grid;
+ place-items: center;
+ gap: 1rem;
+ background: radial-gradient(circle, #0b3149, var(--navy));
+ color: #dff8ff;
+ animation: loaderExit .9s ease 1.35s forwards;
+ pointer-events: none;
+}
+.loader-orb {
+ width: 5rem;
+ height: 5rem;
+ border-radius: 50%;
+ border: 2px solid rgba(255,255,255,.15);
+ background: conic-gradient(from 0deg, var(--primary), var(--secondary), transparent, var(--primary));
+ mask: radial-gradient(circle, transparent 48%, #000 50%);
+ animation: spin 1s linear infinite;
+}
+.tech-loader span { margin-top: 7rem; position: absolute; font-weight: 800; letter-spacing: .02em; }
+@keyframes spin { to { transform: rotate(360deg); } }
+@keyframes loaderExit { to { opacity: 0; visibility: hidden; } }
+
+.hero-section {
+ position: relative;
+ min-height: 86vh;
+ color: #fff;
+ background:
+ linear-gradient(135deg, rgba(6,23,36,.98), rgba(8,44,67,.95)),
+ radial-gradient(circle at 78% 18%, rgba(0, 209, 255, .30), transparent 28rem);
+ overflow: hidden;
+}
+.hero-grid {
+ position: absolute;
+ inset: 0;
+ opacity: .23;
+ background-image: linear-gradient(rgba(255,255,255,.08) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.08) 1px, transparent 1px);
+ background-size: 46px 46px;
+ animation: panGrid 18s linear infinite;
+}
+@keyframes panGrid { to { background-position: 92px 92px; } }
+.shape { position: absolute; border-radius: 32px; filter: blur(.2px); opacity: .8; animation: floatShape 7s ease-in-out infinite; }
+.shape-one { width: 9rem; height: 9rem; right: 8%; top: 14%; background: linear-gradient(135deg, var(--secondary), transparent); transform: rotate(18deg); }
+.shape-two { width: 6rem; height: 6rem; left: 7%; bottom: 12%; border-radius: 50%; background: linear-gradient(135deg, var(--primary), var(--accent)); animation-delay: -2s; }
+@keyframes floatShape { 50% { transform: translateY(-18px) rotate(28deg); } }
+
+.eyebrow { display: inline-flex; align-items: center; gap: .45rem; text-transform: uppercase; font-size: .78rem; letter-spacing: .13em; font-weight: 900; color: var(--secondary); }
+.eyebrow.dark { color: #0089a9; }
+.hero-copy { max-width: 44rem; color: rgba(255,255,255,.78); font-size: clamp(1.08rem, 1vw + .8rem, 1.32rem); line-height: 1.8; }
+.hero-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .8rem; max-width: 40rem; }
+.hero-stats div { padding: 1rem; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.07); border-radius: 1.35rem; }
+.hero-stats strong { display: block; font-size: 1.6rem; color: #fff; }
+.hero-stats span { color: rgba(255,255,255,.68); font-size: .86rem; }
+
+.payment-console, .detail-panel, .form-panel, .success-panel, .cta-panel, .product-card, .category-card, .empty-state, .filter-bar {
+ border: 1px solid var(--line);
+ box-shadow: var(--shadow);
+}
+.payment-console {
+ padding: 1.35rem;
+ border-radius: 2rem;
+ color: #fff;
+ background: linear-gradient(145deg, rgba(255,255,255,.13), rgba(255,255,255,.05));
+ backdrop-filter: blur(18px);
+}
+.console-top { display: flex; gap: .45rem; margin-bottom: 1.2rem; }
+.console-top span { width: .72rem; height: .72rem; border-radius: 999px; background: var(--secondary); }
+.console-top span:nth-child(2) { background: var(--primary); }
+.console-top span:nth-child(3) { background: var(--accent); }
+.qr-card { border-radius: 1.35rem; padding: .8rem; background: rgba(255,255,255,.10); border: 1px dashed rgba(255,255,255,.30); }
+.qr-placeholder {
+ aspect-ratio: 1;
+ min-height: 210px;
+ display: grid;
+ place-items: center;
+ text-align: center;
+ border-radius: 1rem;
+ color: #061724;
+ background:
+ linear-gradient(45deg, #fff 25%, #d9f8ff 25% 50%, #fff 50% 75%, #d9f8ff 75%) 0 0/28px 28px;
+}
+.qr-placeholder i { font-size: 4rem; display: block; }
+.qr-placeholder span { display: block; font-weight: 900; }
+.payment-methods { display: flex; flex-wrap: wrap; gap: .55rem; margin-top: 1rem; }
+.payment-methods span { padding: .5rem .75rem; border-radius: 999px; background: rgba(255,255,255,.12); border: 1px solid rgba(255,255,255,.16); font-weight: 800; font-size: .82rem; }
+
+.section-pad { padding: 5.5rem 0; }
+.section-heading { max-width: 46rem; }
+.section-heading h2, .cta-panel h2 { font-size: clamp(2rem, 3vw, 3.2rem); }
+.section-heading p { color: var(--muted); font-size: 1.08rem; }
+.category-card, .product-card { display: block; height: 100%; border-radius: 1.5rem; padding: 1.35rem; background: rgba(255,255,255,.82); color: var(--ink); transition: .22s ease; }
+.category-card:hover, .product-card:hover { transform: translateY(-6px); border-color: rgba(0,209,255,.45); }
+.category-icon { width: 3.3rem; height: 3.3rem; border-radius: 1rem; display: grid; place-items: center; color: #061724; background: linear-gradient(135deg, var(--primary), var(--secondary)); margin-bottom: 1rem; font-size: 1.45rem; }
+.category-card h3, .product-card h3 { font-size: 1.28rem; margin-bottom: .55rem; }
+.category-card p, .product-card p { color: var(--muted); }
+.category-card small { font-weight: 900; color: #0089a9; }
+.product-strip { background: linear-gradient(180deg, rgba(232,244,251,.55), rgba(247,251,255,0)); }
+.product-card-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
+.product-card-top i { font-size: 1.75rem; color: #00a6c8; }
+.pill { display: inline-flex; padding: .45rem .75rem; border-radius: 999px; font-size: .78rem; font-weight: 900; color: #006e87; background: #dcf8ff; }
+.product-meta { display: flex; justify-content: space-between; gap: 1rem; color: var(--muted); border-top: 1px solid var(--line); padding-top: 1rem; font-size: .92rem; }
+.product-meta strong { color: var(--navy); }
+.cta-panel { border-radius: 2rem; padding: clamp(1.5rem, 4vw, 3rem); color: #fff; background: linear-gradient(135deg, var(--navy), #0a3a57); overflow: hidden; position: relative; }
+.cta-panel p { color: rgba(255,255,255,.76); }
+
+.page-shell { padding: 4.5rem 0 6rem; }
+.page-hero { text-align: center; max-width: 760px; margin: 0 auto 2rem; }
+.page-hero.compact { padding: 2rem; border-radius: 2rem; color: #fff; background: linear-gradient(135deg, var(--navy), #0d415f); box-shadow: var(--shadow); }
+.page-hero h1 { font-size: clamp(2.2rem, 4vw, 4rem); }
+.page-hero p { color: rgba(255,255,255,.75); font-size: 1.08rem; }
+.filter-bar { display: flex; flex-wrap: wrap; gap: .65rem; padding: .75rem; border-radius: 999px; background: #fff; }
+.filter-chip { color: var(--muted); border-radius: 999px; padding: .72rem 1rem; font-weight: 900; }
+.filter-chip.active, .filter-chip:hover { color: var(--navy); background: #dcf8ff; }
+.back-link { color: #007f9c; font-weight: 900; }
+.detail-panel, .form-panel, .success-panel { border-radius: 2rem; padding: clamp(1.4rem, 4vw, 2.4rem); background: rgba(255,255,255,.88); }
+.detail-panel h1 { font-size: clamp(2.2rem, 4vw, 4rem); margin-top: 1rem; }
+.detail-price { font-size: 1.25rem; margin: 1.4rem 0; color: var(--muted); }
+.detail-price strong { color: var(--navy); font-size: 2rem; }
+.detail-body { color: #415363; line-height: 1.85; }
+.detail-actions { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 2rem; }
+.detail-payment { top: 6rem; background: linear-gradient(145deg, #0a2d43, #061724); }
+.form-panel .form-control, .form-panel .form-select { border-radius: 1rem; padding: .9rem 1rem; border: 1px solid rgba(6,23,36,.12); }
+.form-panel .form-control:focus, .form-panel .form-select:focus { border-color: var(--primary); box-shadow: 0 0 0 .25rem rgba(0,209,255,.14); }
+.form-label { font-weight: 900; }
+.form-note { color: var(--muted); }
+.success-panel { max-width: 850px; text-align: center; }
+.success-icon { width: 5rem; height: 5rem; margin: 0 auto 1rem; display: grid; place-items: center; border-radius: 50%; color: var(--navy); background: linear-gradient(135deg, var(--secondary), var(--primary)); font-size: 2.4rem; }
+.summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .75rem; margin-top: 1.5rem; }
+.summary-grid div { padding: 1rem; border-radius: 1rem; background: #edf8fc; }
+.summary-grid span { display: block; color: var(--muted); font-size: .82rem; font-weight: 800; }
+.summary-grid strong { display: block; color: var(--navy); }
+.empty-state { padding: 2rem; border-radius: 1.5rem; background: #fff; color: var(--muted); text-align: center; }
+
+.whatsapp-float { position: fixed; right: 1.2rem; bottom: 1.2rem; z-index: 1000; display: grid; place-items: center; width: 3.8rem; height: 3.8rem; border-radius: 50%; color: #061724; background: linear-gradient(135deg, var(--secondary), var(--primary)); box-shadow: 0 18px 42px rgba(0,209,255,.38); font-size: 1.75rem; }
+.whatsapp-float:hover { color: #061724; transform: translateY(-2px); }
+.site-footer { padding: 2rem 0; color: rgba(255,255,255,.72); background: var(--navy); }
+.site-footer strong { color: #fff; }
+.footer-payments { color: var(--secondary); font-weight: 900; }
+
+@media (max-width: 991.98px) {
+ .hero-stats, .summary-grid { grid-template-columns: 1fr; }
+ .filter-bar { border-radius: 1.25rem; }
+ .page-shell { padding-top: 2.5rem; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
+ .tech-loader { display: none; }
}
diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css
index 108056f..2b8e2e3 100644
--- a/staticfiles/css/custom.css
+++ b/staticfiles/css/custom.css
@@ -1,21 +1,214 @@
-
: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);
+ --navy: #061724;
+ --navy-2: #0b2436;
+ --primary: #00d1ff;
+ --secondary: #00f2a9;
+ --accent: #ffb703;
+ --paper: #f7fbff;
+ --muted: #6b7a88;
+ --ink: #10212f;
+ --line: rgba(10, 36, 54, 0.12);
+ --glass: rgba(255, 255, 255, 0.12);
+ --shadow: 0 24px 70px rgba(4, 18, 30, 0.18);
}
+
+* { box-sizing: border-box; }
+
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: 'Manrope', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ color: var(--ink);
+ background:
+ radial-gradient(circle at 10% 10%, rgba(0, 209, 255, 0.12), transparent 32rem),
+ radial-gradient(circle at 90% 0%, rgba(0, 242, 169, 0.10), transparent 30rem),
+ var(--paper);
+ overflow-x: hidden;
+}
+
+h1, h2, h3, .brand-mark { font-family: 'Space Grotesk', 'Manrope', sans-serif; letter-spacing: -0.04em; }
+a { text-decoration: none; }
+
+.app-nav {
+ background: rgba(6, 23, 36, 0.88);
+ backdrop-filter: blur(18px);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+}
+.brand-mark { font-weight: 800; display: inline-flex; align-items: center; gap: .55rem; }
+.brand-logo {
+ display: inline-grid;
+ place-items: center;
+ width: 2.75rem;
+ height: 2.75rem;
+ border-radius: 1rem;
+ color: var(--navy);
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
+ box-shadow: 0 10px 30px rgba(0, 209, 255, .3);
+ font-size: .86rem;
+}
+.navbar .nav-link { color: rgba(255,255,255,.76); font-weight: 700; }
+.navbar .nav-link:hover, .navbar .nav-link:focus { color: #fff; }
+
+.btn { border-radius: 999px; font-weight: 800; padding: .78rem 1.2rem; }
+.btn-accent {
+ color: #03131d;
+ border: 0;
+ background: linear-gradient(135deg, var(--secondary), var(--primary));
+ box-shadow: 0 14px 36px rgba(0, 209, 255, .28);
+}
+.btn-accent:hover, .btn-accent:focus { color: #03131d; transform: translateY(-1px); box-shadow: 0 18px 44px rgba(0, 209, 255, .35); }
+.btn-glass { color: #fff; border: 1px solid rgba(255,255,255,.28); background: rgba(255,255,255,.08); backdrop-filter: blur(12px); }
+.btn-glass:hover, .btn-glass:focus { color: #fff; border-color: rgba(255,255,255,.55); background: rgba(255,255,255,.16); }
+.btn-dark-soft { color: var(--navy); background: #e8f4fb; border: 1px solid rgba(6,23,36,.08); }
+.btn-dark-soft:hover, .btn-dark-soft:focus { color: #fff; background: var(--navy); }
+
+.tech-loader {
+ position: fixed;
+ inset: 0;
+ z-index: 2000;
+ display: grid;
+ place-items: center;
+ gap: 1rem;
+ background: radial-gradient(circle, #0b3149, var(--navy));
+ color: #dff8ff;
+ animation: loaderExit .9s ease 1.35s forwards;
+ pointer-events: none;
+}
+.loader-orb {
+ width: 5rem;
+ height: 5rem;
+ border-radius: 50%;
+ border: 2px solid rgba(255,255,255,.15);
+ background: conic-gradient(from 0deg, var(--primary), var(--secondary), transparent, var(--primary));
+ mask: radial-gradient(circle, transparent 48%, #000 50%);
+ animation: spin 1s linear infinite;
+}
+.tech-loader span { margin-top: 7rem; position: absolute; font-weight: 800; letter-spacing: .02em; }
+@keyframes spin { to { transform: rotate(360deg); } }
+@keyframes loaderExit { to { opacity: 0; visibility: hidden; } }
+
+.hero-section {
+ position: relative;
+ min-height: 86vh;
+ color: #fff;
+ background:
+ linear-gradient(135deg, rgba(6,23,36,.98), rgba(8,44,67,.95)),
+ radial-gradient(circle at 78% 18%, rgba(0, 209, 255, .30), transparent 28rem);
+ overflow: hidden;
+}
+.hero-grid {
+ position: absolute;
+ inset: 0;
+ opacity: .23;
+ background-image: linear-gradient(rgba(255,255,255,.08) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.08) 1px, transparent 1px);
+ background-size: 46px 46px;
+ animation: panGrid 18s linear infinite;
+}
+@keyframes panGrid { to { background-position: 92px 92px; } }
+.shape { position: absolute; border-radius: 32px; filter: blur(.2px); opacity: .8; animation: floatShape 7s ease-in-out infinite; }
+.shape-one { width: 9rem; height: 9rem; right: 8%; top: 14%; background: linear-gradient(135deg, var(--secondary), transparent); transform: rotate(18deg); }
+.shape-two { width: 6rem; height: 6rem; left: 7%; bottom: 12%; border-radius: 50%; background: linear-gradient(135deg, var(--primary), var(--accent)); animation-delay: -2s; }
+@keyframes floatShape { 50% { transform: translateY(-18px) rotate(28deg); } }
+
+.eyebrow { display: inline-flex; align-items: center; gap: .45rem; text-transform: uppercase; font-size: .78rem; letter-spacing: .13em; font-weight: 900; color: var(--secondary); }
+.eyebrow.dark { color: #0089a9; }
+.hero-copy { max-width: 44rem; color: rgba(255,255,255,.78); font-size: clamp(1.08rem, 1vw + .8rem, 1.32rem); line-height: 1.8; }
+.hero-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .8rem; max-width: 40rem; }
+.hero-stats div { padding: 1rem; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.07); border-radius: 1.35rem; }
+.hero-stats strong { display: block; font-size: 1.6rem; color: #fff; }
+.hero-stats span { color: rgba(255,255,255,.68); font-size: .86rem; }
+
+.payment-console, .detail-panel, .form-panel, .success-panel, .cta-panel, .product-card, .category-card, .empty-state, .filter-bar {
+ border: 1px solid var(--line);
+ box-shadow: var(--shadow);
+}
+.payment-console {
+ padding: 1.35rem;
+ border-radius: 2rem;
+ color: #fff;
+ background: linear-gradient(145deg, rgba(255,255,255,.13), rgba(255,255,255,.05));
+ backdrop-filter: blur(18px);
+}
+.console-top { display: flex; gap: .45rem; margin-bottom: 1.2rem; }
+.console-top span { width: .72rem; height: .72rem; border-radius: 999px; background: var(--secondary); }
+.console-top span:nth-child(2) { background: var(--primary); }
+.console-top span:nth-child(3) { background: var(--accent); }
+.qr-card { border-radius: 1.35rem; padding: .8rem; background: rgba(255,255,255,.10); border: 1px dashed rgba(255,255,255,.30); }
+.qr-placeholder {
+ aspect-ratio: 1;
+ min-height: 210px;
+ display: grid;
+ place-items: center;
+ text-align: center;
+ border-radius: 1rem;
+ color: #061724;
+ background:
+ linear-gradient(45deg, #fff 25%, #d9f8ff 25% 50%, #fff 50% 75%, #d9f8ff 75%) 0 0/28px 28px;
+}
+.qr-placeholder i { font-size: 4rem; display: block; }
+.qr-placeholder span { display: block; font-weight: 900; }
+.payment-methods { display: flex; flex-wrap: wrap; gap: .55rem; margin-top: 1rem; }
+.payment-methods span { padding: .5rem .75rem; border-radius: 999px; background: rgba(255,255,255,.12); border: 1px solid rgba(255,255,255,.16); font-weight: 800; font-size: .82rem; }
+
+.section-pad { padding: 5.5rem 0; }
+.section-heading { max-width: 46rem; }
+.section-heading h2, .cta-panel h2 { font-size: clamp(2rem, 3vw, 3.2rem); }
+.section-heading p { color: var(--muted); font-size: 1.08rem; }
+.category-card, .product-card { display: block; height: 100%; border-radius: 1.5rem; padding: 1.35rem; background: rgba(255,255,255,.82); color: var(--ink); transition: .22s ease; }
+.category-card:hover, .product-card:hover { transform: translateY(-6px); border-color: rgba(0,209,255,.45); }
+.category-icon { width: 3.3rem; height: 3.3rem; border-radius: 1rem; display: grid; place-items: center; color: #061724; background: linear-gradient(135deg, var(--primary), var(--secondary)); margin-bottom: 1rem; font-size: 1.45rem; }
+.category-card h3, .product-card h3 { font-size: 1.28rem; margin-bottom: .55rem; }
+.category-card p, .product-card p { color: var(--muted); }
+.category-card small { font-weight: 900; color: #0089a9; }
+.product-strip { background: linear-gradient(180deg, rgba(232,244,251,.55), rgba(247,251,255,0)); }
+.product-card-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
+.product-card-top i { font-size: 1.75rem; color: #00a6c8; }
+.pill { display: inline-flex; padding: .45rem .75rem; border-radius: 999px; font-size: .78rem; font-weight: 900; color: #006e87; background: #dcf8ff; }
+.product-meta { display: flex; justify-content: space-between; gap: 1rem; color: var(--muted); border-top: 1px solid var(--line); padding-top: 1rem; font-size: .92rem; }
+.product-meta strong { color: var(--navy); }
+.cta-panel { border-radius: 2rem; padding: clamp(1.5rem, 4vw, 3rem); color: #fff; background: linear-gradient(135deg, var(--navy), #0a3a57); overflow: hidden; position: relative; }
+.cta-panel p { color: rgba(255,255,255,.76); }
+
+.page-shell { padding: 4.5rem 0 6rem; }
+.page-hero { text-align: center; max-width: 760px; margin: 0 auto 2rem; }
+.page-hero.compact { padding: 2rem; border-radius: 2rem; color: #fff; background: linear-gradient(135deg, var(--navy), #0d415f); box-shadow: var(--shadow); }
+.page-hero h1 { font-size: clamp(2.2rem, 4vw, 4rem); }
+.page-hero p { color: rgba(255,255,255,.75); font-size: 1.08rem; }
+.filter-bar { display: flex; flex-wrap: wrap; gap: .65rem; padding: .75rem; border-radius: 999px; background: #fff; }
+.filter-chip { color: var(--muted); border-radius: 999px; padding: .72rem 1rem; font-weight: 900; }
+.filter-chip.active, .filter-chip:hover { color: var(--navy); background: #dcf8ff; }
+.back-link { color: #007f9c; font-weight: 900; }
+.detail-panel, .form-panel, .success-panel { border-radius: 2rem; padding: clamp(1.4rem, 4vw, 2.4rem); background: rgba(255,255,255,.88); }
+.detail-panel h1 { font-size: clamp(2.2rem, 4vw, 4rem); margin-top: 1rem; }
+.detail-price { font-size: 1.25rem; margin: 1.4rem 0; color: var(--muted); }
+.detail-price strong { color: var(--navy); font-size: 2rem; }
+.detail-body { color: #415363; line-height: 1.85; }
+.detail-actions { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 2rem; }
+.detail-payment { top: 6rem; background: linear-gradient(145deg, #0a2d43, #061724); }
+.form-panel .form-control, .form-panel .form-select { border-radius: 1rem; padding: .9rem 1rem; border: 1px solid rgba(6,23,36,.12); }
+.form-panel .form-control:focus, .form-panel .form-select:focus { border-color: var(--primary); box-shadow: 0 0 0 .25rem rgba(0,209,255,.14); }
+.form-label { font-weight: 900; }
+.form-note { color: var(--muted); }
+.success-panel { max-width: 850px; text-align: center; }
+.success-icon { width: 5rem; height: 5rem; margin: 0 auto 1rem; display: grid; place-items: center; border-radius: 50%; color: var(--navy); background: linear-gradient(135deg, var(--secondary), var(--primary)); font-size: 2.4rem; }
+.summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .75rem; margin-top: 1.5rem; }
+.summary-grid div { padding: 1rem; border-radius: 1rem; background: #edf8fc; }
+.summary-grid span { display: block; color: var(--muted); font-size: .82rem; font-weight: 800; }
+.summary-grid strong { display: block; color: var(--navy); }
+.empty-state { padding: 2rem; border-radius: 1.5rem; background: #fff; color: var(--muted); text-align: center; }
+
+.whatsapp-float { position: fixed; right: 1.2rem; bottom: 1.2rem; z-index: 1000; display: grid; place-items: center; width: 3.8rem; height: 3.8rem; border-radius: 50%; color: #061724; background: linear-gradient(135deg, var(--secondary), var(--primary)); box-shadow: 0 18px 42px rgba(0,209,255,.38); font-size: 1.75rem; }
+.whatsapp-float:hover { color: #061724; transform: translateY(-2px); }
+.site-footer { padding: 2rem 0; color: rgba(255,255,255,.72); background: var(--navy); }
+.site-footer strong { color: #fff; }
+.footer-payments { color: var(--secondary); font-weight: 900; }
+
+@media (max-width: 991.98px) {
+ .hero-stats, .summary-grid { grid-template-columns: 1fr; }
+ .filter-bar { border-radius: 1.25rem; }
+ .page-shell { padding-top: 2.5rem; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
+ .tech-loader { display: none; }
}