Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -23,19 +23,19 @@ DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
|
||||
ALLOWED_HOSTS = [
|
||||
"127.0.0.1",
|
||||
"localhost",
|
||||
"shinastore.flatlogic.app", # Tambahin domain lo di sini
|
||||
"*", # Tanda bintang ini buat bolehin semua (biar cepet fix)
|
||||
]
|
||||
os.getenv("HOST_FQDN", ""),
|
||||
]
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
origin for origin in [
|
||||
os.getenv("HOST_FQDN", ""),
|
||||
os.getenv("CSRF_TRUSTED_ORIGIN", "")
|
||||
] if origin
|
||||
]
|
||||
CCSRF_TRUSTED_ORIGINS = [
|
||||
"https://shinastore.flatlogic.app",
|
||||
"http://shinastore.flatlogic.app",
|
||||
]
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
f"https://{host}" if not host.startswith(("http://", "https://")) else host
|
||||
for host in CSRF_TRUSTED_ORIGINS
|
||||
]
|
||||
|
||||
# Cookies must always be HTTPS-only; SameSite=Lax keeps CSRF working behind the proxy.
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,27 +1,3 @@
|
||||
from django.contrib import admin
|
||||
from .models import Category, Product, Order, OrderItem, Profile
|
||||
|
||||
@admin.register(Category)
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'description')
|
||||
|
||||
@admin.register(Product)
|
||||
class ProductAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'category', 'price', 'stock', 'created_at')
|
||||
list_filter = ('category', 'created_at')
|
||||
search_fields = ('title', 'description')
|
||||
|
||||
class OrderItemInline(admin.TabularInline):
|
||||
model = OrderItem
|
||||
extra = 1
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'user', 'order_date', 'total_amount', 'status')
|
||||
list_filter = ('status', 'order_date')
|
||||
inlines = [OrderItemInline]
|
||||
|
||||
@admin.register(Profile)
|
||||
class ProfileAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'role')
|
||||
list_filter = ('role',)
|
||||
# Register your models here.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,50 +0,0 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from core.models import Category, Product
|
||||
from core.pexels import fetch_first
|
||||
import random
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Seeds the database with initial categories and products'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
categories_data = [
|
||||
{'name': 'Tech', 'description': 'Latest gadgets and hardware.'},
|
||||
{'name': 'Lifestyle', 'description': 'Modern living essentials.'},
|
||||
{'name': 'Minimalist', 'description': 'Simple and clean aesthetics.'},
|
||||
]
|
||||
|
||||
for cat_data in categories_data:
|
||||
category, created = Category.objects.get_or_create(
|
||||
name=cat_data['name'],
|
||||
defaults={'description': cat_data['description']}
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(f"Created category: {category.name}")
|
||||
|
||||
# Sample products
|
||||
products_data = [
|
||||
('Minimal Desk Lamp', 'Tech', 89.99, 'minimalist desk lamp'),
|
||||
('Mechanical Keyboard', 'Tech', 159.00, 'mechanical keyboard'),
|
||||
('Premium Notebook', 'Minimalist', 25.50, 'aesthetic notebook'),
|
||||
('Leather Wallet', 'Lifestyle', 45.00, 'leather wallet'),
|
||||
('Wireless Earbuds', 'Tech', 129.00, 'modern wireless earbuds'),
|
||||
('Ceramic Coffee Mug', 'Minimalist', 18.00, 'ceramic mug minimalist'),
|
||||
]
|
||||
|
||||
for title, cat_name, price, query in products_data:
|
||||
category = Category.objects.get(name=cat_name)
|
||||
if not Product.objects.filter(title=title).exists():
|
||||
img_data = fetch_first(query)
|
||||
img_url = ""
|
||||
if img_data:
|
||||
img_url = img_data['local_path']
|
||||
|
||||
Product.objects.create(
|
||||
title=title,
|
||||
category=category,
|
||||
price=price,
|
||||
description=f"This is a premium {title.lower()} designed for quality and style.",
|
||||
stock=random.randint(10, 50),
|
||||
image_url=img_url
|
||||
)
|
||||
self.stdout.write(f"Created product: {title}")
|
||||
@ -1,70 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-07 10:01
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('description', models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Categories',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Order',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('order_date', models.DateTimeField(auto_now_add=True)),
|
||||
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('paid', 'Paid'), ('shipped', 'Shipped'), ('cancelled', 'Cancelled')], default='pending', max_length=10)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Product',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('description', models.TextField()),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('stock', models.PositiveIntegerField(default=0)),
|
||||
('image_url', models.URLField(blank=True, max_length=500)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='core.category')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OrderItem',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('quantity', models.PositiveIntegerField(default=1)),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.order')),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(choices=[('admin', 'Admin'), ('customer', 'Customer')], default='customer', max_length=10)),
|
||||
('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -1,71 +1,3 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
class Profile(models.Model):
|
||||
ROLE_CHOICES = (
|
||||
('admin', 'Admin'),
|
||||
('customer', 'Customer'),
|
||||
)
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
role = models.CharField(max_length=10, choices=ROLE_CHOICES, default='customer')
|
||||
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} - {self.role}"
|
||||
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Categories"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Product(models.Model):
|
||||
category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE)
|
||||
title = models.CharField(max_length=200)
|
||||
description = models.TextField()
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
stock = models.PositiveIntegerField(default=0)
|
||||
image_url = models.URLField(max_length=500, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Order(models.Model):
|
||||
STATUS_CHOICES = (
|
||||
('pending', 'Pending'),
|
||||
('paid', 'Paid'),
|
||||
('shipped', 'Shipped'),
|
||||
('cancelled', 'Cancelled'),
|
||||
)
|
||||
user = models.ForeignKey(User, related_name='orders', on_delete=models.CASCADE)
|
||||
order_date = models.DateTimeField(auto_now_add=True)
|
||||
total_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
|
||||
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='pending')
|
||||
|
||||
def __str__(self):
|
||||
return f"Order {self.id} - {self.user.username}"
|
||||
|
||||
class OrderItem(models.Model):
|
||||
order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE)
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||
quantity = models.PositiveIntegerField(default=1)
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.quantity} x {self.product.title}"
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
Profile.objects.create(user=instance)
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def save_user_profile(sender, instance, **kwargs):
|
||||
instance.profile.save()
|
||||
# Create your models here.
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import httpx
|
||||
|
||||
API_KEY = os.getenv("PEXELS_KEY", "Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18")
|
||||
CACHE_DIR = Path("static/images/pexels")
|
||||
|
||||
def _client():
|
||||
return httpx.Client(
|
||||
base_url="https://api.pexels.com/v1/",
|
||||
headers={"Authorization": API_KEY},
|
||||
timeout=15,
|
||||
)
|
||||
|
||||
def fetch_first(query: str, orientation: str = "portrait") -> dict | None:
|
||||
if not API_KEY:
|
||||
return None
|
||||
try:
|
||||
with _client() as client:
|
||||
resp = client.get(
|
||||
"search",
|
||||
params={"query": query, "orientation": orientation, "per_page": 1, "page": 1},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
photo = (data.get("photos") or [None])[0]
|
||||
if not photo:
|
||||
return None
|
||||
src = photo["src"].get("large2x") or photo["src"].get("large") or photo["src"].get("original")
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
target = CACHE_DIR / f"{photo['id']}.jpg"
|
||||
if src:
|
||||
img = httpx.get(src, timeout=15)
|
||||
img.raise_for_status()
|
||||
target.write_bytes(img.content)
|
||||
return {
|
||||
"id": photo["id"],
|
||||
"local_path": f"images/pexels/{photo['id']}.jpg",
|
||||
"photographer": photo.get("photographer"),
|
||||
"photographer_url": photo.get("photographer_url"),
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error fetching from Pexels: {e}")
|
||||
return None
|
||||
@ -1,209 +1,25 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}ShinaStore | Premium E-Commerce{% endblock %}</title>
|
||||
<meta name="description" content="{% block meta_description %}Experience premium shopping with ShinaStore. Minimalist design, curated products.{% endblock %}">
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<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@300;400;500;600;700&family=Montserrat:wght@600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap 5 CDN -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #0F172A;
|
||||
--accent-color: #F43F5E;
|
||||
--bg-light: #F8FAFC;
|
||||
--text-main: #1E293B;
|
||||
--glass-bg: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: var(--bg-light);
|
||||
color: var(--text-main);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, .navbar-brand {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.5rem;
|
||||
letter-spacing: -1px;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.navbar-brand span {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #1e293b;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-weight: 500;
|
||||
color: var(--text-main) !important;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--accent-color) !important;
|
||||
}
|
||||
|
||||
.cart-badge {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -10px;
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
footer {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
padding: 4rem 0 2rem;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 1.5rem;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
{% block extra_css %}{% endblock %}
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
<meta property="og:description" content="{{ project_description }}">
|
||||
<meta property="twitter:description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
{% if project_image_url %}
|
||||
<meta property="og:image" content="{{ project_image_url }}">
|
||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||
{% endif %}
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{% url 'index' %}">SHINA<span>STORE</span></a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto align-items-center">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'index' %}">Shop</a>
|
||||
</li>
|
||||
<li class="nav-item position-relative me-3">
|
||||
<a class="nav-link" href="{% url 'cart_detail' %}">
|
||||
<i class="bi bi-bag"></i>
|
||||
{% if request.session.cart %}
|
||||
<span class="cart-badge">{{ request.session.cart|length }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% if user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn btn-primary ms-lg-3" href="/admin/logout/">Logout</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/login/?next={{ request.path }}">Login</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn btn-primary ms-lg-3" href="/admin/">Register</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% if messages %}
|
||||
<div class="container mt-3">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show shadow-sm" role="alert" style="border-radius: 12px;">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-4 mb-4 mb-lg-0">
|
||||
<span class="footer-logo">SHINASTORE</span>
|
||||
<p class="text-secondary">Premium minimalist e-commerce experience. Curated products for the modern lifestyle.</p>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-4 mb-4 mb-md-0">
|
||||
<h5 class="mb-4">Shop</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="#" class="text-secondary text-decoration-none">New Arrivals</a></li>
|
||||
<li><a href="#" class="text-secondary text-decoration-none">Best Sellers</a></li>
|
||||
<li><a href="#" class="text-secondary text-decoration-none">Sale</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-4 mb-4 mb-md-0">
|
||||
<h5 class="mb-4">Company</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="#" class="text-secondary text-decoration-none">About Us</a></li>
|
||||
<li><a href="#" class="text-secondary text-decoration-none">Contact</a></li>
|
||||
<li><a href="#" class="text-secondary text-decoration-none">Careers</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-4">
|
||||
<h5 class="mb-4">Subscribe</h5>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control bg-transparent border-secondary text-white" placeholder="Email address">
|
||||
<button class="btn btn-outline-light" type="button">Join</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-5 border-secondary">
|
||||
<div class="text-center text-secondary">
|
||||
<p>© 2026 ShinaStore. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@ -1,108 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Your Cart - ShinaStore{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<h1 class="display-5 fw-bold text-slate mb-5" style="font-family: 'Montserrat', sans-serif;">Your Shopping Cart</h1>
|
||||
|
||||
{% if cart %}
|
||||
<div class="row g-5">
|
||||
<div class="col-lg-8">
|
||||
<div class="card border-0 shadow-sm" style="border-radius: 20px;">
|
||||
<div class="table-responsive p-4">
|
||||
<table class="table table-borderless align-middle">
|
||||
<thead>
|
||||
<tr class="text-muted small text-uppercase">
|
||||
<th scope="col">Product</th>
|
||||
<th scope="col">Price</th>
|
||||
<th scope="col">Quantity</th>
|
||||
<th scope="col">Subtotal</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for product_id, item in cart.items %}
|
||||
<tr class="border-bottom">
|
||||
<td class="py-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<img src="{{ item.image_url }}" alt="{{ item.title }}" class="rounded me-3" style="width: 80px; height: 80px; object-fit: cover;">
|
||||
<div>
|
||||
<h6 class="fw-bold mb-0">{{ item.title }}</h6>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>${{ item.price }}</td>
|
||||
<td>
|
||||
<span class="badge bg-light text-dark px-3 py-2 border">{{ item.quantity }}</span>
|
||||
</td>
|
||||
<td class="fw-bold">${{ item.price }}</td>
|
||||
<td class="text-end">
|
||||
<a href="{% url 'remove_from_cart' product_id %}" class="btn btn-outline-danger btn-sm border-0">
|
||||
<i class="bi bi-trash"></i> Remove
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 shadow-sm p-4" style="border-radius: 20px; background-color: #F8FAFC;">
|
||||
<h4 class="fw-bold mb-4">Order Summary</h4>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">Subtotal</span>
|
||||
<span>${{ total|stringformat:".2f" }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">Shipping</span>
|
||||
<span class="text-success">Free</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between mb-4">
|
||||
<span class="fw-bold">Total</span>
|
||||
<span class="h4 fw-bold text-rose">${{ total|stringformat:".2f" }}</span>
|
||||
</div>
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<a href="{% url 'checkout' %}" class="btn btn-dark btn-lg w-100 py-3 fw-bold" style="border-radius: 12px;">
|
||||
Proceed to Checkout
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="alert alert-info small" style="border-radius: 12px;">
|
||||
Please <a href="/admin/login/?next={{ request.path }}" class="fw-bold">login</a> to place an order.
|
||||
</div>
|
||||
<button class="btn btn-dark btn-lg w-100 py-3 fw-bold" disabled style="border-radius: 12px;">
|
||||
Proceed to Checkout
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<a href="{% url 'index' %}" class="text-decoration-none text-muted small">
|
||||
<i class="bi bi-arrow-left"></i> Continue Shopping
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<div class="mb-4">
|
||||
<i class="bi bi-cart-x display-1 text-muted"></i>
|
||||
</div>
|
||||
<h3>Your cart is empty</h3>
|
||||
<p class="text-muted">Looks like you haven't added anything to your cart yet.</p>
|
||||
<a href="{% url 'index' %}" class="btn btn-rose btn-lg px-5 py-3 mt-3 text-white fw-bold" style="background-color: #F43F5E; border-radius: 12px;">
|
||||
Start Shopping
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.text-rose { color: #F43F5E; }
|
||||
.btn-rose:hover { background-color: #E11D48 !important; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -1,52 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Order Confirmed - ShinaStore{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5 text-center">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-4">
|
||||
<i class="bi bi-check-circle-fill display-1 text-success"></i>
|
||||
</div>
|
||||
<h1 class="display-4 fw-bold text-slate mb-3" style="font-family: 'Montserrat', sans-serif;">Thank You!</h1>
|
||||
<p class="h4 text-muted mb-5">Your order #{{ order.id }} has been placed successfully.</p>
|
||||
|
||||
<div class="card border-0 shadow-sm p-4 mb-5" style="border-radius: 20px; text-align: left;">
|
||||
<h5 class="fw-bold mb-4">Order Summary</h5>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">Order Date:</span>
|
||||
<span>{{ order.order_date|date:"F d, Y H:i" }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">Order Total:</span>
|
||||
<span class="fw-bold">${{ order.total_amount }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-4">
|
||||
<span class="text-muted">Status:</span>
|
||||
<span class="badge bg-success">{{ order.get_status_display }}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h6 class="fw-bold mb-3">Items Ordered:</h6>
|
||||
{% for item in order.items.all %}
|
||||
<div class="d-flex justify-content-between mb-2 small">
|
||||
<span>{{ item.quantity }} x {{ item.product.title }}</span>
|
||||
<span>${{ item.price }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-3 d-sm-flex justify-content-sm-center">
|
||||
<a href="{% url 'index' %}" class="btn btn-dark btn-lg px-4 py-3 fw-bold" style="border-radius: 12px;">
|
||||
Back to Store
|
||||
</a>
|
||||
<button onclick="window.print()" class="btn btn-outline-dark btn-lg px-4 py-3 fw-bold" style="border-radius: 12px;">
|
||||
Print Receipt
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,120 +1,145 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block 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 %}
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section py-5 mb-5" style="background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); position: relative; overflow: hidden;">
|
||||
<div class="container py-5">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-6 mb-5 mb-lg-0">
|
||||
<h1 class="display-3 mb-4" style="line-height: 1.1;">Define Your <span style="color: var(--accent-color);">Minimalist</span> Style.</h1>
|
||||
<p class="lead mb-5 text-secondary">Discover our curated collection of premium gadgets and lifestyle essentials designed for the modern home.</p>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="#products" class="btn btn-primary btn-lg px-4">Shop Collection</a>
|
||||
<a href="#" class="btn btn-outline-dark btn-lg px-4">Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="position-relative">
|
||||
<div class="hero-shape" style="position: absolute; width: 400px; height: 400px; background: rgba(244, 63, 94, 0.1); border-radius: 50%; filter: blur(50px); top: -50px; right: -50px; z-index: 0;"></div>
|
||||
<img src="https://images.pexels.com/photos/129731/pexels-photo-129731.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2" alt="Hero Image" class="img-fluid rounded-4 shadow-lg position-relative" style="z-index: 1;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your app…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Filter & Search Section -->
|
||||
<section class="mb-5" id="products">
|
||||
<div class="container">
|
||||
<div class="row align-items-center mb-4">
|
||||
<div class="col-md-6 mb-3 mb-md-0">
|
||||
<h2 class="mb-0">Featured Products</h2>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<form action="{% url 'index' %}" method="GET" class="d-flex gap-2">
|
||||
<input type="text" name="q" class="form-control rounded-pill px-4" placeholder="Search products..." value="{{ query|default:'' }}">
|
||||
{% if selected_category %}
|
||||
<input type="hidden" name="category" value="{{ selected_category }}">
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-primary rounded-pill px-4">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Categories -->
|
||||
<div class="d-flex gap-2 overflow-auto pb-3 mb-5" style="scrollbar-width: none;">
|
||||
<a href="{% url 'index' %}" class="btn {% if not selected_category %}btn-dark{% else %}btn-outline-dark{% endif %} rounded-pill px-4">All</a>
|
||||
{% for category in categories %}
|
||||
<a href="{% url 'index' %}?category={{ category.id }}{% if query %}&q={{ query }}{% endif %}"
|
||||
class="btn {% if selected_category == category.id %}btn-dark{% else %}btn-outline-dark{% endif %} rounded-pill px-4">
|
||||
{{ category.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Product Grid -->
|
||||
<div class="row g-4">
|
||||
{% for product in products %}
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 border-0 shadow-sm rounded-4 overflow-hidden product-card">
|
||||
<a href="{% url 'product_detail' product.pk %}" class="text-decoration-none">
|
||||
<div class="position-relative overflow-hidden" style="height: 250px;">
|
||||
{% if product.image_url %}
|
||||
<img src="{% static product.image_url %}" class="card-img-top h-100 w-100 object-fit-cover transition-scale" alt="{{ product.title }}">
|
||||
{% else %}
|
||||
<div class="h-100 w-100 bg-light d-flex align-items-center justify-content-center">
|
||||
<span class="text-muted">No image</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="position-absolute top-0 end-0 p-3">
|
||||
<span class="badge bg-white text-dark shadow-sm rounded-pill">${{ product.price }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="card-body p-4 d-flex flex-column">
|
||||
<small class="text-uppercase text-secondary fw-bold" style="font-size: 0.7rem; letter-spacing: 1px;">{{ product.category.name }}</small>
|
||||
<h5 class="card-title mt-1 mb-2">
|
||||
<a href="{% url 'product_detail' product.pk %}" class="text-decoration-none text-dark">{{ product.title }}</a>
|
||||
</h5>
|
||||
<p class="card-text text-secondary small mb-4 text-truncate-2">{{ product.description }}</p>
|
||||
<div class="mt-auto">
|
||||
<a href="{% url 'add_to_cart' product.pk %}" class="btn btn-outline-dark w-100 rounded-pill">Add to Cart</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<p class="text-muted">No products found matching your criteria.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.product-card {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.product-card:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04) !important;
|
||||
}
|
||||
.transition-scale {
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
.product-card:hover .transition-scale {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.text-truncate-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.object-fit-cover {
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
||||
<p class="runtime">
|
||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
</footer>
|
||||
{% endblock %}
|
||||
@ -1,104 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ product.title }} - ShinaStore{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'index' %}" class="text-decoration-none text-muted">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'index' %}?category={{ product.category.id }}" class="text-decoration-none text-muted">{{ product.category.name }}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ product.title }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row g-5">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm overflow-hidden" style="border-radius: 24px;">
|
||||
<img src="{{ product.image_url }}" class="img-fluid w-100" alt="{{ product.title }}" style="min-height: 500px; object-fit: cover;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="ps-md-4">
|
||||
<span class="badge bg-rose mb-3 px-3 py-2" style="background-color: #F43F5E;">{{ product.category.name }}</span>
|
||||
<h1 class="display-5 fw-bold text-slate mb-3" style="font-family: 'Montserrat', sans-serif;">{{ product.title }}</h1>
|
||||
<p class="h3 text-rose fw-bold mb-4" style="color: #F43F5E;">${{ product.price }}</p>
|
||||
|
||||
<div class="mb-4">
|
||||
<h5 class="fw-bold mb-3">Description</h5>
|
||||
<p class="text-muted leading-relaxed">
|
||||
{{ product.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-muted">Stock availability:
|
||||
{% if product.stock > 0 %}
|
||||
<span class="text-success fw-bold">In Stock ({{ product.stock }})</span>
|
||||
{% else %}
|
||||
<span class="text-danger fw-bold">Out of Stock</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
{% if product.stock > 0 %}
|
||||
<a href="{% url 'add_to_cart' product.pk %}" class="btn btn-lg py-3 fw-bold shadow-sm" style="background-color: #0F172A; color: white; border-radius: 12px; transition: transform 0.2s;">
|
||||
Add to Cart
|
||||
</a>
|
||||
{% else %}
|
||||
<button class="btn btn-lg btn-secondary py-3 fw-bold" disabled style="border-radius: 12px;">
|
||||
Out of Stock
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mt-5 p-4 bg-light" style="border-radius: 16px; border-left: 4px solid #F43F5E;">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="me-3">
|
||||
<i class="bi bi-shield-check h3 text-rose"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-1 fw-bold">Secure Checkout</h6>
|
||||
<p class="mb-0 small text-muted">100% Secure payment with 256-bit SSL encryption.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Related Products -->
|
||||
{% if related_products %}
|
||||
<div class="mt-5 pt-5">
|
||||
<h3 class="fw-bold mb-4" style="font-family: 'Montserrat', sans-serif;">You might also like</h3>
|
||||
<div class="row g-4">
|
||||
{% for rel in related_products %}
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 border-0 shadow-sm product-card" style="border-radius: 20px; transition: all 0.3s ease;">
|
||||
<a href="{% url 'product_detail' rel.pk %}" class="text-decoration-none">
|
||||
<img src="{{ rel.image_url }}" class="card-img-top" alt="{{ rel.title }}" style="height: 250px; object-fit: cover; border-top-left-radius: 20px; border-top-right-radius: 20px;">
|
||||
<div class="card-body p-3">
|
||||
<h6 class="text-slate fw-bold mb-1">{{ rel.title }}</h6>
|
||||
<p class="text-rose fw-bold mb-0">${{ rel.price }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.product-card:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.1) !important;
|
||||
}
|
||||
.bg-rose { background-color: #F43F5E; }
|
||||
.text-rose { color: #F43F5E; }
|
||||
.text-slate { color: #0F172A; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
10
core/urls.py
10
core/urls.py
@ -1,11 +1,7 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
from .views import home
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.index, name="index"),
|
||||
path("product/<int:pk>/", views.product_detail, name="product_detail"),
|
||||
path("cart/", views.cart_detail, name="cart_detail"),
|
||||
path("cart/add/<int:pk>/", views.add_to_cart, name="add_to_cart"),
|
||||
path("cart/remove/<int:pk>/", views.remove_from_cart, name="remove_from_cart"),
|
||||
path("checkout/", views.checkout, name="checkout"),
|
||||
path("", home, name="home"),
|
||||
]
|
||||
|
||||
116
core/views.py
116
core/views.py
@ -1,97 +1,25 @@
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from .models import Product, Category, Order, OrderItem
|
||||
from django.contrib import messages
|
||||
import os
|
||||
import platform
|
||||
|
||||
from django import get_version as django_version
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
def home(request):
|
||||
"""Render the landing screen with loader and environment details."""
|
||||
host_name = request.get_host().lower()
|
||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||
now = timezone.now()
|
||||
|
||||
def index(request):
|
||||
query = request.GET.get('q')
|
||||
category_id = request.GET.get('category')
|
||||
|
||||
products = Product.objects.all().order_by('-created_at')
|
||||
categories = Category.objects.all()
|
||||
|
||||
if query:
|
||||
products = products.filter(title__icontains=query) | products.filter(description__icontains=query)
|
||||
|
||||
if category_id:
|
||||
products = products.filter(category_id=category_id)
|
||||
|
||||
context = {
|
||||
'products': products,
|
||||
'categories': categories,
|
||||
'selected_category': int(category_id) if category_id and category_id.isdigit() else None,
|
||||
'query': query
|
||||
"project_name": "New Style",
|
||||
"agent_brand": agent_brand,
|
||||
"django_version": django_version(),
|
||||
"python_version": platform.python_version(),
|
||||
"current_time": now,
|
||||
"host_name": host_name,
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
}
|
||||
return render(request, 'core/index.html', context)
|
||||
|
||||
def product_detail(request, pk):
|
||||
product = get_object_or_404(Product, pk=pk)
|
||||
related_products = Product.objects.filter(category=product.category).exclude(pk=pk)[:4]
|
||||
return render(request, 'core/product_detail.html', {
|
||||
'product': product,
|
||||
'related_products': related_products
|
||||
})
|
||||
|
||||
def add_to_cart(request, pk):
|
||||
product = get_object_or_404(Product, pk=pk)
|
||||
cart = request.session.get('cart', {})
|
||||
|
||||
product_id = str(pk)
|
||||
if product_id in cart:
|
||||
cart[product_id]['quantity'] += 1
|
||||
else:
|
||||
cart[product_id] = {
|
||||
'title': product.title,
|
||||
'price': str(product.price),
|
||||
'quantity': 1,
|
||||
'image_url': product.image_url
|
||||
}
|
||||
|
||||
request.session['cart'] = cart
|
||||
messages.success(request, f"{product.title} added to cart.")
|
||||
return redirect('cart_detail')
|
||||
|
||||
def cart_detail(request):
|
||||
cart = request.session.get('cart', {})
|
||||
total = sum(float(item['price']) * item['quantity'] for item in cart.values())
|
||||
return render(request, 'core/cart.html', {'cart': cart, 'total': total})
|
||||
|
||||
def remove_from_cart(request, pk):
|
||||
cart = request.session.get('cart', {})
|
||||
product_id = str(pk)
|
||||
if product_id in cart:
|
||||
del cart[product_id]
|
||||
request.session['cart'] = cart
|
||||
messages.info(request, "Item removed from cart.")
|
||||
return redirect('cart_detail')
|
||||
|
||||
@login_required
|
||||
def checkout(request):
|
||||
cart = request.session.get('cart', {})
|
||||
if not cart:
|
||||
messages.warning(request, "Your cart is empty.")
|
||||
return redirect('index')
|
||||
|
||||
total = sum(float(item['price']) * item['quantity'] for item in cart.values())
|
||||
|
||||
# Create Order
|
||||
order = Order.objects.create(user=request.user, total_amount=total)
|
||||
|
||||
# Create OrderItems
|
||||
for product_id, item in cart.items():
|
||||
product = get_object_or_404(Product, pk=product_id)
|
||||
OrderItem.objects.create(
|
||||
order=order,
|
||||
product=product,
|
||||
quantity=item['quantity'],
|
||||
price=item['price']
|
||||
)
|
||||
# Update stock
|
||||
if product.stock >= item['quantity']:
|
||||
product.stock -= item['quantity']
|
||||
product.save()
|
||||
|
||||
# Clear cart
|
||||
request.session['cart'] = {}
|
||||
|
||||
return render(request, 'core/checkout_success.html', {'order': order})
|
||||
return render(request, "core/index.html", context)
|
||||
|
||||
@ -1,36 +1,3 @@
|
||||
anyio==4.12.1
|
||||
asgiref==3.10.0
|
||||
certifi==2022.9.24
|
||||
chardet==5.1.0
|
||||
charset-normalizer==3.0.1
|
||||
dbus-python==1.3.2
|
||||
distro-info==1.5+deb12u1
|
||||
Django==5.2.7
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httplib2==0.20.4
|
||||
httpx==0.28.1
|
||||
idna==3.3
|
||||
markdown-it-py==2.1.0
|
||||
mdurl==0.1.2
|
||||
mysqlclient==2.2.7
|
||||
netifaces==0.11.0
|
||||
pillow==12.1.0
|
||||
pycurl==7.45.2
|
||||
Pygments==2.14.0
|
||||
PyGObject==3.42.2
|
||||
pyparsing==3.0.9
|
||||
PySimpleSOAP==1.16.2
|
||||
python-apt==2.6.0
|
||||
python-debian==0.1.49
|
||||
python-debianbts==4.0.1
|
||||
python-dotenv==1.1.1
|
||||
PyYAML==6.0
|
||||
reportbug==12.0.0
|
||||
requests==2.28.1
|
||||
rich==13.3.1
|
||||
six==1.16.0
|
||||
sqlparse==0.5.3
|
||||
typing_extensions==4.15.0
|
||||
unattended-upgrades==0.1
|
||||
urllib3==1.26.12
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 179 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 88 KiB |
Loading…
x
Reference in New Issue
Block a user