Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bf719c945 | ||
|
|
24e3955692 | ||
|
|
920c22b36b | ||
|
|
fb5b5166c4 | ||
|
|
06ee114953 |
BIN
assets/pasted-20260630-003744-c5c1d69c.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
assets/pasted-20260630-004540-c35aefe5.jpg
Normal file
|
After Width: | Height: | Size: 105 KiB |
@ -149,6 +149,9 @@ STATIC_URL = 'static/'
|
|||||||
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
|
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
|
||||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
|
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
BASE_DIR / 'static',
|
BASE_DIR / 'static',
|
||||||
BASE_DIR / 'assets',
|
BASE_DIR / 'assets',
|
||||||
|
|||||||
@ -27,3 +27,4 @@ urlpatterns = [
|
|||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|||||||
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
@ -1,3 +1,40 @@
|
|||||||
from django.contrib import admin
|
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).")
|
||||||
|
|||||||
59
core/forms.py
Normal file
@ -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
|
||||||
84
core/migrations/0001_initial.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
65
core/migrations/0002_seed_tech_store.py
Normal file
@ -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),
|
||||||
|
]
|
||||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0002_seed_tech_store.cpython-311.pyc
Normal file
100
core/models.py
@ -1,3 +1,101 @@
|
|||||||
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models
|
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}"
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
|
{% load static %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="es">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{% block title %}{% if page_title %}{{ page_title }} · {% endif %}{{ project_name|default:'Flores Software Ideas' }}{% endblock %}</title>
|
||||||
|
<meta name="description" content="{{ meta_description|default:project_description|default:'Tienda virtual para servicios tecnológicos, pagos por QR, Transfermóvil y Enzona.' }}">
|
||||||
{% if project_description %}
|
{% if project_description %}
|
||||||
<meta name="description" content="{{ project_description }}">
|
|
||||||
<meta property="og:description" content="{{ project_description }}">
|
<meta property="og:description" content="{{ project_description }}">
|
||||||
<meta property="twitter:description" content="{{ project_description }}">
|
<meta property="twitter:description" content="{{ project_description }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -13,13 +15,95 @@
|
|||||||
<meta property="og:image" content="{{ project_image_url }}">
|
<meta property="og:image" content="{{ project_image_url }}">
|
||||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% load static %}
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<div id="techLoader" class="tech-loader" role="status" aria-live="polite" aria-label="Cargando {{ project_name }}">
|
||||||
|
<div class="loader-grid" aria-hidden="true"></div>
|
||||||
|
<div class="loader-particles" aria-hidden="true">
|
||||||
|
<span></span><span></span><span></span><span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
<div class="loader-card">
|
||||||
|
<div class="loader-brand">
|
||||||
|
<span class="loader-logo image-logo">
|
||||||
|
<img src="{% static 'images/app-logo-site.png' %}?v=20260630-logo" alt="Logo {{ project_name }}" width="64" height="64" decoding="async">
|
||||||
|
</span>
|
||||||
|
<span>{{ project_name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="loader-core" aria-hidden="true">
|
||||||
|
<span class="loader-ring loader-ring-outer"></span>
|
||||||
|
<span class="loader-ring loader-ring-inner"></span>
|
||||||
|
<span class="loader-scan"></span>
|
||||||
|
<span class="loader-chip image-chip"><img src="{% static 'images/app-logo-site.png' %}?v=20260630-logo" alt="" width="72" height="72" decoding="async"></span>
|
||||||
|
</div>
|
||||||
|
<p class="loader-title">Cargando experiencia tecnológica</p>
|
||||||
|
<p class="loader-copy">Conectando catálogo, pagos QR y soporte WhatsApp...</p>
|
||||||
|
<div class="loader-progress" aria-hidden="true"><span></span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark app-nav sticky-top">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand brand-mark" href="{% url 'home' %}" aria-label="Inicio {{ project_name }}">
|
||||||
|
<span class="brand-logo image-logo"><img src="{% static 'images/app-logo-site.png' %}?v=20260630-logo" alt="Logo {{ project_name }}" width="64" height="64" decoding="async"></span><span>{{ project_name }}</span>
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Abrir navegación">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="mainNav">
|
||||||
|
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'catalog' %}">Catálogo</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'confirm_payment' %}">Confirmar pago</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/admin/">Admin</a></li>
|
||||||
|
<li class="nav-item"><a class="btn btn-accent btn-sm" href="https://wa.me/{{ whatsapp_number }}" target="_blank" rel="noopener"><i class="bi bi-whatsapp me-1"></i> WhatsApp</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<div class="container mt-3">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags|default:'info' }} shadow-sm" role="alert">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
|
<a class="whatsapp-float" href="https://wa.me/{{ whatsapp_number }}" target="_blank" rel="noopener" aria-label="Contactar por WhatsApp">
|
||||||
|
<i class="bi bi-whatsapp"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container d-flex flex-column flex-md-row justify-content-between gap-3">
|
||||||
|
<div class="footer-brand">
|
||||||
|
<span class="footer-logo image-logo"><img src="{% static 'images/app-logo-site.png' %}?v=20260630-logo" alt="Logo {{ project_name }}" width="56" height="56" loading="lazy" decoding="async"></span>
|
||||||
|
<span><strong>{{ project_name }}</strong><br><span>Tiendas virtuales, páginas web y apps móviles para crecer.</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="footer-payments">QR · Transfermóvil · Enzona · Transferencia</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
var loader = document.getElementById('techLoader');
|
||||||
|
if (!loader) return;
|
||||||
|
window.setTimeout(function () { loader.classList.add('is-hidden'); }, 450);
|
||||||
|
});
|
||||||
|
window.setTimeout(function () {
|
||||||
|
var loader = document.getElementById('techLoader');
|
||||||
|
if (loader) loader.classList.add('is-hidden');
|
||||||
|
}, 3200);
|
||||||
|
</script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
32
core/templates/core/catalog.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ page_title }} · {{ project_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="page-shell">
|
||||||
|
<section class="container">
|
||||||
|
<div class="page-hero compact">
|
||||||
|
<span class="eyebrow">Catálogo</span>
|
||||||
|
<h1>{% if selected_category %}{{ selected_category.name }}{% else %}Productos y servicios tecnológicos{% endif %}</h1>
|
||||||
|
<p>Filtra por categoría, revisa detalles y avanza con contacto o confirmación de pago.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-bar mb-4">
|
||||||
|
<a class="filter-chip {% if not selected_category %}active{% endif %}" href="{% url 'catalog' %}">Todo</a>
|
||||||
|
{% for category in categories %}
|
||||||
|
<a class="filter-chip {% if selected_category == category %}active{% endif %}" href="{% url 'catalog' %}?categoria={{ category.slug }}">{{ category.name }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for product in products %}
|
||||||
|
{% include "core/partials/product_card.html" with product=product %}
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="empty-state">No hay productos en esta categoría todavía. Entra al admin para activar nuevos servicios.</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
44
core/templates/core/confirm_payment.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Confirmar pago · {{ project_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="page-shell">
|
||||||
|
<section class="container">
|
||||||
|
<div class="row g-5 align-items-start">
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<div class="page-hero compact text-start">
|
||||||
|
<span class="eyebrow">Confirmación manual</span>
|
||||||
|
<h1>Reporta tu pago</h1>
|
||||||
|
<p>Completa los datos de la operación. El administrador revisará la referencia y actualizará el estado desde el panel.</p>
|
||||||
|
</div>
|
||||||
|
<div class="payment-console mt-4">
|
||||||
|
<div class="console-top"><span></span><span></span><span></span></div>
|
||||||
|
<h2>Métodos disponibles</h2>
|
||||||
|
{% include "core/partials/payment_qr.html" %}
|
||||||
|
<div class="payment-methods"><span>Transfermóvil</span><span>Enzona</span><span>Transferencia</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<form class="form-panel" method="post" enctype="multipart/form-data" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if form.non_field_errors %}<div class="alert alert-danger">{{ form.non_field_errors }}</div>{% endif %}
|
||||||
|
<div class="row g-3">
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="col-md-{% if field.name == 'note' or field.name == 'receipt' %}12{% else %}6{% endif %}">
|
||||||
|
<label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}<div class="form-text">{{ field.help_text }}</div>{% endif %}
|
||||||
|
{% for error in field.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-accent btn-lg w-100 mt-4" type="submit"><i class="bi bi-send-check me-2"></i>Enviar para aprobación</button>
|
||||||
|
<p class="form-note mt-3 mb-0">Tu pago queda en estado <strong>pendiente</strong> hasta revisión del administrador.</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
@ -1,145 +1,103 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}{{ project_name }}{% endblock %}
|
{% block title %}{{ page_title }} · {{ project_name }}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
: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);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
|
||||||
animation: bg-pan 20s linear infinite;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bg-pan {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: 100% 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--card-bg-color);
|
|
||||||
border: 1px solid var(--card-border-color);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 2.5rem 2rem;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 1.2rem;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
margin: 1.5rem auto;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.runtime code {
|
|
||||||
background: rgba(0, 0, 0, 0.25);
|
|
||||||
padding: 0.15rem 0.45rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<header class="hero-section">
|
||||||
|
<div class="hero-grid"></div>
|
||||||
|
<div class="shape shape-one"></div>
|
||||||
|
<div class="shape shape-two"></div>
|
||||||
|
<div class="container position-relative">
|
||||||
|
<div class="row align-items-center g-5 py-5">
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<span class="eyebrow"><i class="bi bi-lightning-charge-fill"></i> Tienda virtual tecnológica</span>
|
||||||
|
<h1 class="display-3 fw-bold mt-3">Compra, cotiza y confirma pagos de soluciones digitales en minutos.</h1>
|
||||||
|
<p class="hero-copy mt-4">Creamos tiendas virtuales, páginas web, software empresarial, apps móviles y soporte técnico con pagos por QR, Transfermóvil, Enzona o transferencia.</p>
|
||||||
|
<div class="d-flex flex-column flex-sm-row gap-3 mt-4">
|
||||||
|
<a href="{% url 'catalog' %}" class="btn btn-accent btn-lg"><i class="bi bi-bag-check me-2"></i>Ver catálogo</a>
|
||||||
|
<a href="{% url 'confirm_payment' %}" class="btn btn-glass btn-lg"><i class="bi bi-qr-code me-2"></i>Confirmar pago</a>
|
||||||
|
</div>
|
||||||
|
<div class="hero-stats mt-5">
|
||||||
|
<div><strong>5</strong><span>Categorías activas</span></div>
|
||||||
|
<div><strong>24/7</strong><span>Solicitudes abiertas</span></div>
|
||||||
|
<div><strong>{{ pending_payments }}</strong><span>Pagos pendientes admin</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<div class="payment-console">
|
||||||
|
<div class="console-top"><span></span><span></span><span></span></div>
|
||||||
|
<h2>Zona de pago</h2>
|
||||||
|
<p>Escanea el QR o paga por las pasarelas disponibles. Luego reporta tu referencia para aprobación manual.</p>
|
||||||
|
{% include "core/partials/payment_qr.html" %}
|
||||||
|
<div class="payment-methods">
|
||||||
|
<span>Transfermóvil</span><span>Enzona</span><span>Transferencia</span>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'confirm_payment' %}" class="btn btn-outline-light w-100 mt-3">Ya pagué, confirmar</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="card">
|
<section class="section-pad">
|
||||||
<h1>Analyzing your requirements and generating your app…</h1>
|
<div class="container">
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<div class="section-heading text-center mx-auto">
|
||||||
<span class="sr-only">Loading…</span>
|
<span class="eyebrow dark">Servicios activos</span>
|
||||||
|
<h2>Catálogo listo para vender tecnología</h2>
|
||||||
|
<p>Elige una categoría, abre el detalle y continúa por WhatsApp o confirmando tu pago.</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
<div class="row g-4 mt-4">
|
||||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
{% for category in categories %}
|
||||||
<p class="runtime">
|
<div class="col-md-6 col-xl-4">
|
||||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
<a class="category-card" href="{% url 'catalog' %}?categoria={{ category.slug }}">
|
||||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
<span class="category-icon"><i class="bi {{ category.icon }}"></i></span>
|
||||||
</p>
|
<h3>{{ category.name }}</h3>
|
||||||
|
<p>{{ category.description }}</p>
|
||||||
|
<small>{{ category.active_products }} producto(s) activo(s)</small>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-12"><div class="empty-state">Aún no hay categorías. Puedes crearlas desde el panel admin.</div></div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section-pad product-strip">
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-end gap-3 mb-4">
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow dark">Primeros productos</span>
|
||||||
|
<h2 class="mb-0">Soluciones destacadas</h2>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'catalog' %}" class="btn btn-dark-soft">Explorar todo <i class="bi bi-arrow-right"></i></a>
|
||||||
|
</div>
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for product in featured_products %}
|
||||||
|
{% include "core/partials/product_card.html" with product=product %}
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-12"><div class="empty-state">No hay productos destacados todavía.</div></div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section-pad">
|
||||||
|
<div class="container">
|
||||||
|
<div class="cta-panel row g-4 align-items-center">
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<span class="eyebrow">Flujo de pago manual</span>
|
||||||
|
<h2>Reporta tu operación y el admin la aprueba en el panel.</h2>
|
||||||
|
<p>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.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<a href="{% url 'confirm_payment' %}" class="btn btn-accent btn-lg w-100 mb-3">Confirmar pago ahora</a>
|
||||||
|
<a href="/admin/" class="btn btn-glass btn-lg w-100">Entrar al panel admin</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
|
||||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
|
||||||
</footer>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
4
core/templates/core/partials/payment_qr.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{% load static %}
|
||||||
|
<div class="qr-card" aria-label="Código QR de pago de {{ project_name }}">
|
||||||
|
<img class="qr-image" src="{% static 'images/payment-qr.jpg' %}?v=20260630-qr" alt="Código QR de pago de {{ project_name }}" width="494" height="532" loading="lazy" decoding="async">
|
||||||
|
</div>
|
||||||
18
core/templates/core/partials/product_card.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<div class="col-md-6 col-xl-4">
|
||||||
|
<article class="product-card h-100">
|
||||||
|
<div class="product-card-top">
|
||||||
|
<span class="pill">{{ product.category.name }}</span>
|
||||||
|
<i class="bi {{ product.category.icon }}"></i>
|
||||||
|
</div>
|
||||||
|
<h3>{{ product.name }}</h3>
|
||||||
|
<p>{{ product.short_description }}</p>
|
||||||
|
<div class="product-meta">
|
||||||
|
<span>Desde <strong>${{ product.price_from }}</strong></span>
|
||||||
|
<span>{{ product.delivery_time }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2 mt-4">
|
||||||
|
<a class="btn btn-dark-soft flex-fill" href="{{ product.get_absolute_url }}">Detalle</a>
|
||||||
|
<a class="btn btn-accent flex-fill" href="https://wa.me/{{ whatsapp_number }}?text=Hola,%20quiero%20cotizar%20{{ product.name|urlencode }}" target="_blank" rel="noopener">WhatsApp</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
26
core/templates/core/payment_success.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Pago recibido · {{ project_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="page-shell">
|
||||||
|
<section class="container">
|
||||||
|
<div class="success-panel mx-auto">
|
||||||
|
<div class="success-icon"><i class="bi bi-check2-circle"></i></div>
|
||||||
|
<span class="eyebrow dark">Solicitud recibida</span>
|
||||||
|
<h1>Tu pago está pendiente de aprobación</h1>
|
||||||
|
<p>Registramos la confirmación de <strong>{{ confirmation.customer_name }}</strong>. El admin revisará la referencia <strong>{{ confirmation.reference }}</strong> y actualizará el estado.</p>
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div><span>Método</span><strong>{{ confirmation.get_method_display }}</strong></div>
|
||||||
|
<div><span>Monto</span><strong>${{ confirmation.amount }}</strong></div>
|
||||||
|
<div><span>Estado</span><strong>{{ confirmation.get_status_display }}</strong></div>
|
||||||
|
<div><span>Servicio</span><strong>{{ confirmation.product|default:'No especificado' }}</strong></div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column flex-sm-row gap-3 justify-content-center mt-4">
|
||||||
|
<a href="{% url 'catalog' %}" class="btn btn-dark-soft btn-lg">Volver al catálogo</a>
|
||||||
|
<a href="https://wa.me/{{ whatsapp_number }}" target="_blank" rel="noopener" class="btn btn-accent btn-lg">Hablar por WhatsApp</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
47
core/templates/core/product_detail.html
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{ product.name }} · {{ project_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="page-shell">
|
||||||
|
<section class="container">
|
||||||
|
<div class="row g-5 align-items-start">
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<a class="back-link" href="{% url 'catalog' %}"><i class="bi bi-arrow-left"></i> Volver al catálogo</a>
|
||||||
|
<div class="detail-panel mt-3">
|
||||||
|
<span class="pill">{{ product.category.name }}</span>
|
||||||
|
<h1>{{ product.name }}</h1>
|
||||||
|
<p class="lead">{{ product.short_description }}</p>
|
||||||
|
<div class="detail-price">Desde <strong>${{ product.price_from }}</strong></div>
|
||||||
|
<div class="detail-body">{{ product.description|linebreaks }}</div>
|
||||||
|
<div class="detail-actions">
|
||||||
|
<a class="btn btn-accent btn-lg" href="https://wa.me/{{ whatsapp_number }}?text=Hola,%20quiero%20cotizar%20{{ product.name|urlencode }}" target="_blank" rel="noopener"><i class="bi bi-whatsapp me-2"></i>Cotizar por WhatsApp</a>
|
||||||
|
<a class="btn btn-dark-soft btn-lg" href="{% url 'confirm_payment' %}?producto={{ product.slug }}"><i class="bi bi-credit-card me-2"></i>Confirmar pago</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<aside class="col-lg-5">
|
||||||
|
<div class="payment-console sticky-lg-top detail-payment">
|
||||||
|
<div class="console-top"><span></span><span></span><span></span></div>
|
||||||
|
<h2>Paga y reporta</h2>
|
||||||
|
{% include "core/partials/payment_qr.html" %}
|
||||||
|
<div class="payment-methods"><span>Transfermóvil</span><span>Enzona</span><span>Transferencia</span></div>
|
||||||
|
<a href="{% url 'confirm_payment' %}?producto={{ product.slug }}" class="btn btn-accent w-100 mt-3">Enviar confirmación</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if related_products %}
|
||||||
|
<div class="mt-5 pt-4">
|
||||||
|
<h2>También puede interesarte</h2>
|
||||||
|
<div class="row g-4 mt-2">
|
||||||
|
{% for product in related_products %}
|
||||||
|
{% include "core/partials/product_card.html" with product=product %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
@ -1,7 +1,11 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import home
|
from .views import catalog, confirm_payment, home, payment_success, product_detail
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", home, name="home"),
|
path("", home, name="home"),
|
||||||
|
path("catalogo/", catalog, name="catalog"),
|
||||||
|
path("servicio/<slug:slug>/", product_detail, name="product_detail"),
|
||||||
|
path("confirmar-pago/", confirm_payment, name="confirm_payment"),
|
||||||
|
path("confirmar-pago/<int:pk>/recibido/", payment_success, name="payment_success"),
|
||||||
]
|
]
|
||||||
|
|||||||
136
core/views.py
@ -1,25 +1,121 @@
|
|||||||
import os
|
from django.contrib import messages
|
||||||
import platform
|
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 .forms import PaymentConfirmationForm
|
||||||
from django.shortcuts import render
|
from .models import PaymentConfirmation, Product, ProductCategory
|
||||||
from django.utils import timezone
|
|
||||||
|
WHATSAPP_NUMBER = "5359177041"
|
||||||
|
BUSINESS_NAME = "Flores Software Ideas"
|
||||||
|
|
||||||
|
|
||||||
|
def _base_context(**extra):
|
||||||
|
context = {
|
||||||
|
"project_name": BUSINESS_NAME,
|
||||||
|
"project_description": "Flores Software Ideas ofrece soluciones tecnológicas, software empresarial y pagos por QR, Transfermóvil y Enzona.",
|
||||||
|
"whatsapp_number": WHATSAPP_NUMBER,
|
||||||
|
}
|
||||||
|
context.update(extra)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
"""Render the landing screen with loader and environment details."""
|
categories = ProductCategory.objects.filter(is_active=True).annotate(
|
||||||
host_name = request.get_host().lower()
|
active_products=Count("products", filter=Q(products__is_active=True))
|
||||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
)
|
||||||
now = timezone.now()
|
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 = {
|
return render(
|
||||||
"project_name": "New Style",
|
request,
|
||||||
"agent_brand": agent_brand,
|
"core/index.html",
|
||||||
"django_version": django_version(),
|
_base_context(
|
||||||
"python_version": platform.python_version(),
|
categories=categories,
|
||||||
"current_time": now,
|
featured_products=featured_products,
|
||||||
"host_name": host_name,
|
payment_form=payment_form,
|
||||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
pending_payments=recent_confirmations,
|
||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
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.",
|
||||||
return render(request, "core/index.html", context)
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
BIN
media/payment_receipts/0000.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
media/payment_receipts/0000_1JKj5J7.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
@ -1,4 +1,437 @@
|
|||||||
/* Custom styles for the application */
|
:root {
|
||||||
body {
|
--navy: #061724;
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
--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;
|
||||||
|
}
|
||||||
|
.brand-logo.image-logo {
|
||||||
|
position: relative;
|
||||||
|
width: 3.05rem;
|
||||||
|
height: 3.05rem;
|
||||||
|
padding: .2rem;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255, 255, 255, .96);
|
||||||
|
border: 1px solid rgba(0, 209, 255, .28);
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #e9fbff;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 18% 16%, rgba(0, 209, 255, .26), transparent 28rem),
|
||||||
|
radial-gradient(circle at 86% 78%, rgba(0, 242, 169, .22), transparent 26rem),
|
||||||
|
linear-gradient(135deg, #061724 0%, #082c43 58%, #031019 100%);
|
||||||
|
isolation: isolate;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: loaderAutoExit .7s ease 3s forwards;
|
||||||
|
}
|
||||||
|
.tech-loader::before,
|
||||||
|
.tech-loader::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -18%;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
.tech-loader::before {
|
||||||
|
opacity: .2;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(255,255,255,.12) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255,255,255,.12) 1px, transparent 1px);
|
||||||
|
background-size: 46px 46px;
|
||||||
|
transform: perspective(720px) rotateX(62deg) scale(1.15);
|
||||||
|
animation: loaderGridMove 5.5s linear infinite;
|
||||||
|
}
|
||||||
|
.tech-loader::after {
|
||||||
|
opacity: .55;
|
||||||
|
background: linear-gradient(180deg, transparent 0%, rgba(0, 209, 255, .16) 48%, rgba(0, 242, 169, .12) 52%, transparent 100%);
|
||||||
|
height: 32%;
|
||||||
|
top: -34%;
|
||||||
|
animation: loaderSweep 2.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.tech-loader.is-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transform: scale(1.015);
|
||||||
|
transition: opacity .55s ease, visibility .55s ease, transform .55s ease;
|
||||||
|
}
|
||||||
|
.loader-grid {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle, rgba(255,255,255,.22) 1px, transparent 1.5px) 0 0 / 34px 34px,
|
||||||
|
linear-gradient(120deg, transparent 40%, rgba(0,209,255,.10), transparent 60%);
|
||||||
|
mask-image: radial-gradient(circle at center, #000 0%, transparent 72%);
|
||||||
|
opacity: .32;
|
||||||
|
}
|
||||||
|
.loader-card {
|
||||||
|
position: relative;
|
||||||
|
width: min(90vw, 440px);
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid rgba(255,255,255,.16);
|
||||||
|
border-radius: 2rem;
|
||||||
|
background: linear-gradient(145deg, rgba(255,255,255,.14), rgba(255,255,255,.055));
|
||||||
|
box-shadow: 0 28px 90px rgba(0, 0, 0, .35), inset 0 1px 0 rgba(255,255,255,.18);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
}
|
||||||
|
.loader-brand {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .7rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
font-family: 'Space Grotesk', 'Manrope', sans-serif;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -.03em;
|
||||||
|
}
|
||||||
|
.loader-logo {
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 2.8rem;
|
||||||
|
height: 2.8rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
color: var(--navy);
|
||||||
|
background: linear-gradient(135deg, var(--secondary), var(--primary));
|
||||||
|
box-shadow: 0 0 34px rgba(0, 209, 255, .42);
|
||||||
|
font-size: .85rem;
|
||||||
|
}
|
||||||
|
.loader-logo.image-logo {
|
||||||
|
position: relative;
|
||||||
|
width: 3.35rem;
|
||||||
|
height: 3.35rem;
|
||||||
|
padding: .22rem;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255, 255, 255, .96);
|
||||||
|
border: 1px solid rgba(0, 242, 169, .3);
|
||||||
|
}
|
||||||
|
.image-logo img {
|
||||||
|
position: absolute;
|
||||||
|
inset: .24rem;
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - .48rem);
|
||||||
|
height: calc(100% - .48rem);
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: .75rem;
|
||||||
|
}
|
||||||
|
.loader-core {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 9.8rem;
|
||||||
|
height: 9.8rem;
|
||||||
|
margin: 0 auto 1.25rem;
|
||||||
|
}
|
||||||
|
.loader-core::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 16%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(0, 209, 255, .32), transparent 66%);
|
||||||
|
filter: blur(8px);
|
||||||
|
animation: loaderPulse 1.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.loader-ring {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: conic-gradient(from 0deg, var(--primary), var(--secondary), transparent 34%, var(--accent), var(--primary));
|
||||||
|
-webkit-mask: radial-gradient(circle, transparent 56%, #000 58%);
|
||||||
|
mask: radial-gradient(circle, transparent 56%, #000 58%);
|
||||||
|
}
|
||||||
|
.loader-ring-outer { inset: 0; animation: spin 1.25s linear infinite; }
|
||||||
|
.loader-ring-inner { inset: 1.45rem; opacity: .72; animation: spinReverse 1.8s linear infinite; }
|
||||||
|
.loader-scan {
|
||||||
|
position: absolute;
|
||||||
|
inset: 2.35rem;
|
||||||
|
border: 1px solid rgba(0, 242, 169, .55);
|
||||||
|
border-radius: 1.35rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, transparent, rgba(0, 242, 169, .22), transparent),
|
||||||
|
rgba(6, 23, 36, .58);
|
||||||
|
box-shadow: inset 0 0 22px rgba(0, 242, 169, .18);
|
||||||
|
animation: loaderScan 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.loader-chip {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 3.8rem;
|
||||||
|
height: 3.8rem;
|
||||||
|
border-radius: 1.15rem;
|
||||||
|
color: var(--navy);
|
||||||
|
background: linear-gradient(135deg, var(--accent), var(--secondary));
|
||||||
|
box-shadow: 0 0 34px rgba(0, 242, 169, .38);
|
||||||
|
font-size: 1.55rem;
|
||||||
|
animation: chipFloat 1.9s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.loader-chip.image-chip {
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255, 255, 255, .96);
|
||||||
|
border: 1px solid rgba(255, 183, 3, .34);
|
||||||
|
}
|
||||||
|
.loader-chip.image-chip img {
|
||||||
|
position: absolute;
|
||||||
|
inset: .34rem;
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - .68rem);
|
||||||
|
height: calc(100% - .68rem);
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: .8rem;
|
||||||
|
}
|
||||||
|
.loader-title {
|
||||||
|
margin: 0;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Space Grotesk', 'Manrope', sans-serif;
|
||||||
|
font-size: clamp(1.25rem, 4vw, 1.75rem);
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -.04em;
|
||||||
|
}
|
||||||
|
.loader-copy {
|
||||||
|
margin: .45rem auto 1.25rem;
|
||||||
|
max-width: 30ch;
|
||||||
|
color: rgba(233, 251, 255, .76);
|
||||||
|
font-size: .96rem;
|
||||||
|
}
|
||||||
|
.loader-progress {
|
||||||
|
width: 100%;
|
||||||
|
height: .52rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255,255,255,.11);
|
||||||
|
}
|
||||||
|
.loader-progress span {
|
||||||
|
display: block;
|
||||||
|
width: 46%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: linear-gradient(90deg, var(--secondary), var(--primary), var(--accent));
|
||||||
|
box-shadow: 0 0 22px rgba(0, 209, 255, .42);
|
||||||
|
animation: loaderProgress 1.55s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.loader-particles span {
|
||||||
|
position: absolute;
|
||||||
|
width: .55rem;
|
||||||
|
height: .55rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--primary);
|
||||||
|
box-shadow: 0 0 22px currentColor;
|
||||||
|
opacity: .7;
|
||||||
|
animation: particleFloat 4.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.loader-particles span:nth-child(1) { left: 14%; top: 22%; color: var(--primary); animation-delay: 0s; }
|
||||||
|
.loader-particles span:nth-child(2) { left: 78%; top: 18%; color: var(--secondary); animation-delay: .35s; }
|
||||||
|
.loader-particles span:nth-child(3) { left: 18%; top: 78%; color: var(--accent); animation-delay: .75s; }
|
||||||
|
.loader-particles span:nth-child(4) { left: 84%; top: 70%; color: var(--primary); animation-delay: 1s; }
|
||||||
|
.loader-particles span:nth-child(5) { left: 49%; top: 10%; color: var(--secondary); animation-delay: 1.25s; }
|
||||||
|
.loader-particles span:nth-child(6) { left: 52%; top: 88%; color: var(--accent); animation-delay: 1.55s; }
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
@keyframes spinReverse { to { transform: rotate(-360deg); } }
|
||||||
|
@keyframes loaderGridMove { to { background-position: 92px 92px; } }
|
||||||
|
@keyframes loaderSweep { 0% { transform: translateY(0); } 100% { transform: translateY(460%); } }
|
||||||
|
@keyframes loaderPulse { 50% { transform: scale(1.12); opacity: .65; } }
|
||||||
|
@keyframes loaderScan { 50% { transform: scale(.92); border-radius: 50%; } }
|
||||||
|
@keyframes chipFloat { 50% { transform: translateY(-6px) scale(1.04); } }
|
||||||
|
@keyframes particleFloat { 50% { transform: translate3d(18px, -24px, 0) scale(1.45); opacity: 1; } }
|
||||||
|
@keyframes loaderProgress { 0% { transform: translateX(-115%); } 100% { transform: translateX(235%); } }
|
||||||
|
@keyframes loaderAutoExit { 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; text-align: center; background: rgba(255,255,255,.10); border: 1px dashed rgba(255,255,255,.30); }
|
||||||
|
.qr-image { display: block; width: min(100%, 18rem); height: auto; margin: 0 auto; border-radius: 1rem; background: #fff; box-shadow: 0 16px 45px rgba(0,0,0,.22); }
|
||||||
|
.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-brand { display: inline-flex; align-items: center; gap: .85rem; }
|
||||||
|
.footer-logo {
|
||||||
|
position: relative;
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 3.2rem;
|
||||||
|
height: 3.2rem;
|
||||||
|
padding: .2rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255, 255, 255, .95);
|
||||||
|
box-shadow: 0 12px 32px rgba(0, 209, 255, .18);
|
||||||
|
}
|
||||||
|
.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; }
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
static/images/app-logo-site.png
Normal file
|
After Width: | Height: | Size: 463 KiB |
BIN
static/images/app-logo.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
static/images/payment-qr.jpg
Normal file
|
After Width: | Height: | Size: 105 KiB |
@ -1,21 +1,437 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-color-start: #6a11cb;
|
--navy: #061724;
|
||||||
--bg-color-end: #2575fc;
|
--navy-2: #0b2436;
|
||||||
--text-color: #ffffff;
|
--primary: #00d1ff;
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
--secondary: #00f2a9;
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
--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 {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Manrope', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
color: var(--ink);
|
||||||
color: var(--text-color);
|
background:
|
||||||
display: flex;
|
radial-gradient(circle at 10% 10%, rgba(0, 209, 255, 0.12), transparent 32rem),
|
||||||
justify-content: center;
|
radial-gradient(circle at 90% 0%, rgba(0, 242, 169, 0.10), transparent 30rem),
|
||||||
align-items: center;
|
var(--paper);
|
||||||
min-height: 100vh;
|
overflow-x: hidden;
|
||||||
text-align: center;
|
}
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
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;
|
||||||
|
}
|
||||||
|
.brand-logo.image-logo {
|
||||||
|
position: relative;
|
||||||
|
width: 3.05rem;
|
||||||
|
height: 3.05rem;
|
||||||
|
padding: .2rem;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255, 255, 255, .96);
|
||||||
|
border: 1px solid rgba(0, 209, 255, .28);
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #e9fbff;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 18% 16%, rgba(0, 209, 255, .26), transparent 28rem),
|
||||||
|
radial-gradient(circle at 86% 78%, rgba(0, 242, 169, .22), transparent 26rem),
|
||||||
|
linear-gradient(135deg, #061724 0%, #082c43 58%, #031019 100%);
|
||||||
|
isolation: isolate;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: loaderAutoExit .7s ease 3s forwards;
|
||||||
|
}
|
||||||
|
.tech-loader::before,
|
||||||
|
.tech-loader::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -18%;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
.tech-loader::before {
|
||||||
|
opacity: .2;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(255,255,255,.12) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255,255,255,.12) 1px, transparent 1px);
|
||||||
|
background-size: 46px 46px;
|
||||||
|
transform: perspective(720px) rotateX(62deg) scale(1.15);
|
||||||
|
animation: loaderGridMove 5.5s linear infinite;
|
||||||
|
}
|
||||||
|
.tech-loader::after {
|
||||||
|
opacity: .55;
|
||||||
|
background: linear-gradient(180deg, transparent 0%, rgba(0, 209, 255, .16) 48%, rgba(0, 242, 169, .12) 52%, transparent 100%);
|
||||||
|
height: 32%;
|
||||||
|
top: -34%;
|
||||||
|
animation: loaderSweep 2.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.tech-loader.is-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transform: scale(1.015);
|
||||||
|
transition: opacity .55s ease, visibility .55s ease, transform .55s ease;
|
||||||
|
}
|
||||||
|
.loader-grid {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle, rgba(255,255,255,.22) 1px, transparent 1.5px) 0 0 / 34px 34px,
|
||||||
|
linear-gradient(120deg, transparent 40%, rgba(0,209,255,.10), transparent 60%);
|
||||||
|
mask-image: radial-gradient(circle at center, #000 0%, transparent 72%);
|
||||||
|
opacity: .32;
|
||||||
|
}
|
||||||
|
.loader-card {
|
||||||
|
position: relative;
|
||||||
|
width: min(90vw, 440px);
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid rgba(255,255,255,.16);
|
||||||
|
border-radius: 2rem;
|
||||||
|
background: linear-gradient(145deg, rgba(255,255,255,.14), rgba(255,255,255,.055));
|
||||||
|
box-shadow: 0 28px 90px rgba(0, 0, 0, .35), inset 0 1px 0 rgba(255,255,255,.18);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
}
|
||||||
|
.loader-brand {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .7rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
font-family: 'Space Grotesk', 'Manrope', sans-serif;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -.03em;
|
||||||
|
}
|
||||||
|
.loader-logo {
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 2.8rem;
|
||||||
|
height: 2.8rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
color: var(--navy);
|
||||||
|
background: linear-gradient(135deg, var(--secondary), var(--primary));
|
||||||
|
box-shadow: 0 0 34px rgba(0, 209, 255, .42);
|
||||||
|
font-size: .85rem;
|
||||||
|
}
|
||||||
|
.loader-logo.image-logo {
|
||||||
|
position: relative;
|
||||||
|
width: 3.35rem;
|
||||||
|
height: 3.35rem;
|
||||||
|
padding: .22rem;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255, 255, 255, .96);
|
||||||
|
border: 1px solid rgba(0, 242, 169, .3);
|
||||||
|
}
|
||||||
|
.image-logo img {
|
||||||
|
position: absolute;
|
||||||
|
inset: .24rem;
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - .48rem);
|
||||||
|
height: calc(100% - .48rem);
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: .75rem;
|
||||||
|
}
|
||||||
|
.loader-core {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 9.8rem;
|
||||||
|
height: 9.8rem;
|
||||||
|
margin: 0 auto 1.25rem;
|
||||||
|
}
|
||||||
|
.loader-core::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 16%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(0, 209, 255, .32), transparent 66%);
|
||||||
|
filter: blur(8px);
|
||||||
|
animation: loaderPulse 1.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.loader-ring {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: conic-gradient(from 0deg, var(--primary), var(--secondary), transparent 34%, var(--accent), var(--primary));
|
||||||
|
-webkit-mask: radial-gradient(circle, transparent 56%, #000 58%);
|
||||||
|
mask: radial-gradient(circle, transparent 56%, #000 58%);
|
||||||
|
}
|
||||||
|
.loader-ring-outer { inset: 0; animation: spin 1.25s linear infinite; }
|
||||||
|
.loader-ring-inner { inset: 1.45rem; opacity: .72; animation: spinReverse 1.8s linear infinite; }
|
||||||
|
.loader-scan {
|
||||||
|
position: absolute;
|
||||||
|
inset: 2.35rem;
|
||||||
|
border: 1px solid rgba(0, 242, 169, .55);
|
||||||
|
border-radius: 1.35rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, transparent, rgba(0, 242, 169, .22), transparent),
|
||||||
|
rgba(6, 23, 36, .58);
|
||||||
|
box-shadow: inset 0 0 22px rgba(0, 242, 169, .18);
|
||||||
|
animation: loaderScan 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.loader-chip {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 3.8rem;
|
||||||
|
height: 3.8rem;
|
||||||
|
border-radius: 1.15rem;
|
||||||
|
color: var(--navy);
|
||||||
|
background: linear-gradient(135deg, var(--accent), var(--secondary));
|
||||||
|
box-shadow: 0 0 34px rgba(0, 242, 169, .38);
|
||||||
|
font-size: 1.55rem;
|
||||||
|
animation: chipFloat 1.9s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.loader-chip.image-chip {
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255, 255, 255, .96);
|
||||||
|
border: 1px solid rgba(255, 183, 3, .34);
|
||||||
|
}
|
||||||
|
.loader-chip.image-chip img {
|
||||||
|
position: absolute;
|
||||||
|
inset: .34rem;
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - .68rem);
|
||||||
|
height: calc(100% - .68rem);
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: .8rem;
|
||||||
|
}
|
||||||
|
.loader-title {
|
||||||
|
margin: 0;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Space Grotesk', 'Manrope', sans-serif;
|
||||||
|
font-size: clamp(1.25rem, 4vw, 1.75rem);
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -.04em;
|
||||||
|
}
|
||||||
|
.loader-copy {
|
||||||
|
margin: .45rem auto 1.25rem;
|
||||||
|
max-width: 30ch;
|
||||||
|
color: rgba(233, 251, 255, .76);
|
||||||
|
font-size: .96rem;
|
||||||
|
}
|
||||||
|
.loader-progress {
|
||||||
|
width: 100%;
|
||||||
|
height: .52rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255,255,255,.11);
|
||||||
|
}
|
||||||
|
.loader-progress span {
|
||||||
|
display: block;
|
||||||
|
width: 46%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: linear-gradient(90deg, var(--secondary), var(--primary), var(--accent));
|
||||||
|
box-shadow: 0 0 22px rgba(0, 209, 255, .42);
|
||||||
|
animation: loaderProgress 1.55s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.loader-particles span {
|
||||||
|
position: absolute;
|
||||||
|
width: .55rem;
|
||||||
|
height: .55rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--primary);
|
||||||
|
box-shadow: 0 0 22px currentColor;
|
||||||
|
opacity: .7;
|
||||||
|
animation: particleFloat 4.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.loader-particles span:nth-child(1) { left: 14%; top: 22%; color: var(--primary); animation-delay: 0s; }
|
||||||
|
.loader-particles span:nth-child(2) { left: 78%; top: 18%; color: var(--secondary); animation-delay: .35s; }
|
||||||
|
.loader-particles span:nth-child(3) { left: 18%; top: 78%; color: var(--accent); animation-delay: .75s; }
|
||||||
|
.loader-particles span:nth-child(4) { left: 84%; top: 70%; color: var(--primary); animation-delay: 1s; }
|
||||||
|
.loader-particles span:nth-child(5) { left: 49%; top: 10%; color: var(--secondary); animation-delay: 1.25s; }
|
||||||
|
.loader-particles span:nth-child(6) { left: 52%; top: 88%; color: var(--accent); animation-delay: 1.55s; }
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
@keyframes spinReverse { to { transform: rotate(-360deg); } }
|
||||||
|
@keyframes loaderGridMove { to { background-position: 92px 92px; } }
|
||||||
|
@keyframes loaderSweep { 0% { transform: translateY(0); } 100% { transform: translateY(460%); } }
|
||||||
|
@keyframes loaderPulse { 50% { transform: scale(1.12); opacity: .65; } }
|
||||||
|
@keyframes loaderScan { 50% { transform: scale(.92); border-radius: 50%; } }
|
||||||
|
@keyframes chipFloat { 50% { transform: translateY(-6px) scale(1.04); } }
|
||||||
|
@keyframes particleFloat { 50% { transform: translate3d(18px, -24px, 0) scale(1.45); opacity: 1; } }
|
||||||
|
@keyframes loaderProgress { 0% { transform: translateX(-115%); } 100% { transform: translateX(235%); } }
|
||||||
|
@keyframes loaderAutoExit { 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; text-align: center; background: rgba(255,255,255,.10); border: 1px dashed rgba(255,255,255,.30); }
|
||||||
|
.qr-image { display: block; width: min(100%, 18rem); height: auto; margin: 0 auto; border-radius: 1rem; background: #fff; box-shadow: 0 16px 45px rgba(0,0,0,.22); }
|
||||||
|
.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-brand { display: inline-flex; align-items: center; gap: .85rem; }
|
||||||
|
.footer-logo {
|
||||||
|
position: relative;
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 3.2rem;
|
||||||
|
height: 3.2rem;
|
||||||
|
padding: .2rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255, 255, 255, .95);
|
||||||
|
box-shadow: 0 12px 32px rgba(0, 209, 255, .18);
|
||||||
|
}
|
||||||
|
.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; }
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
staticfiles/images/app-logo-site.png
Normal file
|
After Width: | Height: | Size: 463 KiB |
BIN
staticfiles/images/app-logo.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
staticfiles/images/payment-qr.jpg
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
staticfiles/pasted-20260630-003744-c5c1d69c.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
staticfiles/pasted-20260630-004540-c35aefe5.jpg
Normal file
|
After Width: | Height: | Size: 105 KiB |