Autosave: 20260204-231352
This commit is contained in:
parent
88ba420f9e
commit
77d103ab40
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,5 +1,13 @@
|
||||
from django import forms
|
||||
from .models import WardrobeItem, Accessory, Outfit, Category, OutfitFolder
|
||||
from .models import WardrobeItem, Accessory, Outfit, Category, OutfitFolder, UserProfile
|
||||
|
||||
class UserProfileForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ['avatar']
|
||||
widgets = {
|
||||
'avatar': forms.FileInput(attrs={'class': 'form-control bg-dark text-white border-secondary'}),
|
||||
}
|
||||
|
||||
class WardrobeItemForm(forms.ModelForm):
|
||||
main_category = forms.ModelChoiceField(
|
||||
@ -163,4 +171,4 @@ class OutfitForm(forms.ModelForm):
|
||||
if commit:
|
||||
instance.save()
|
||||
self.save_m2m()
|
||||
return instance
|
||||
return instance
|
||||
|
||||
24
core/migrations/0004_userprofile.py
Normal file
24
core/migrations/0004_userprofile.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-04 23:00
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_outfitfolder_is_preset'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0004_userprofile.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0004_userprofile.cpython-311.pyc
Normal file
Binary file not shown.
@ -1,7 +1,28 @@
|
||||
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
|
||||
import os
|
||||
from .utils import process_clothing_image
|
||||
|
||||
class UserProfile(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
||||
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username}'s Profile"
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
UserProfile.objects.get_or_create(user=instance)
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def save_user_profile(sender, instance, **kwargs):
|
||||
if not hasattr(instance, 'profile'):
|
||||
UserProfile.objects.create(user=instance)
|
||||
instance.profile.save()
|
||||
|
||||
class Category(models.Model):
|
||||
TYPE_CHOICES = [
|
||||
('wardrobe', 'Wardrobe'),
|
||||
@ -102,4 +123,4 @@ class CalendarAssignment(models.Model):
|
||||
outfit = models.ForeignKey(Outfit, on_delete=models.CASCADE, related_name='assignments')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.date}: {self.outfit}"
|
||||
return f"{self.date}: {self.outfit}"
|
||||
@ -14,6 +14,7 @@
|
||||
--bg-dark: #121212;
|
||||
--surface-dark: #1E1E1E;
|
||||
--primary-accent: #BB86FC;
|
||||
--accent-lavender: #BB86FC;
|
||||
--secondary-accent: #03DAC6;
|
||||
--text-main: #E0E0E0;
|
||||
--text-dim: #9E9E9E;
|
||||
@ -112,14 +113,14 @@
|
||||
<i class="fas fa-tshirt"></i>
|
||||
<span>Outfits</span>
|
||||
</a>
|
||||
<a href="{% url 'accessory_list' %}" class="nav-item {% if request.resolver_match.url_name == 'accessory_list' %}active{% endif %}">
|
||||
<i class="fas fa-gem"></i>
|
||||
<span>Accs</span>
|
||||
</a>
|
||||
<a href="{% url 'wardrobe_list' %}" class="nav-item {% if request.resolver_match.url_name == 'wardrobe_list' %}active{% endif %}">
|
||||
<i class="fas fa-box"></i>
|
||||
<span>Wardrobe</span>
|
||||
</a>
|
||||
<a href="{% url 'accessory_list' %}" class="nav-item {% if request.resolver_match.url_name == 'accessory_list' %}active{% endif %}">
|
||||
<i class="fas fa-gem"></i>
|
||||
<span>Accs</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
@ -2,38 +2,59 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h2 mb-0">Accessories</h1>
|
||||
<a href="{% url 'add_accessory_item' %}" class="btn btn-primary shadow-sm">
|
||||
<i class="bi bi-plus-lg"></i> Add Accessory
|
||||
<div class="container-fluid py-2">
|
||||
<!-- Header with smaller button for mobile -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0 fw-bold">Accessories</h4>
|
||||
<a href="{% url 'add_accessory_item' %}" class="btn btn-primary btn-sm rounded-pill px-3 shadow-sm d-flex align-items-center">
|
||||
<i class="bi bi-plus-lg me-1"></i> <span class="d-none d-sm-inline">Add Accessory</span><span class="d-inline d-sm-none">Add</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Filter Toggle and Sort Bars -->
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm rounded-pill px-3 mb-2" type="button" data-bs-toggle="collapse" data-bs-target="#filterCollapse" aria-expanded="false">
|
||||
<i class="bi bi-filter me-1"></i> Filter
|
||||
</button>
|
||||
|
||||
<div class="collapse {% if sort != 'recent' %}show{% endif %}" id="filterCollapse">
|
||||
<div class="d-flex flex-column gap-2 mt-2">
|
||||
<a href="?sort=recent{% if current_main %}&main_category={{ current_main.id }}{% endif %}{% if current_sub %}&subcategory={{ current_sub.id }}{% endif %}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}"
|
||||
class="btn {% if sort == 'recent' %}btn-lavender{% else %}btn-outline-secondary{% endif %} btn-sm py-2 text-start px-3 rounded-3 border-0 shadow-sm">
|
||||
Most Recently Added
|
||||
</a>
|
||||
<a href="?sort=alpha{% if current_main %}&main_category={{ current_main.id }}{% endif %}{% if current_sub %}&subcategory={{ current_sub.id }}{% endif %}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}"
|
||||
class="btn {% if sort == 'alpha' %}btn-lavender{% else %}btn-outline-secondary{% endif %} btn-sm py-2 text-start px-3 rounded-3 border-0 shadow-sm">
|
||||
A-Z
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Categories Tabs -->
|
||||
<ul class="nav nav-pills mb-3 overflow-auto flex-nowrap pb-2" id="mainCatTabs" role="tablist">
|
||||
<ul class="nav nav-pills mb-3 overflow-auto flex-nowrap pb-2 gap-2" id="mainCatTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link {% if not current_main %}active{% endif %}" href="{% url 'accessory_list' %}">All</a>
|
||||
<a class="nav-link rounded-pill py-1 px-3 small {% if not current_main %}active bg-lavender{% else %}bg-secondary text-white{% endif %}" href="?sort={{ sort }}">All</a>
|
||||
</li>
|
||||
{% for main_cat in main_categories %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link {% if current_main.id == main_cat.id %}active{% endif %}"
|
||||
href="?main_category={{ main_cat.id }}">{{ main_cat.name }}</a>
|
||||
<a class="nav-link rounded-pill py-1 px-3 small {% if current_main.id == main_cat.id %}active bg-lavender{% else %}bg-secondary text-white{% endif %}"
|
||||
href="?main_category={{ main_cat.id }}&sort={{ sort }}">{{ main_cat.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<!-- Subcategories Tabs (if main category selected) -->
|
||||
<!-- Subcategories Tabs -->
|
||||
{% if current_main and subcategories %}
|
||||
<div class="mb-4">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a href="?main_category={{ current_main.id }}"
|
||||
class="btn btn-sm {% if not current_sub %}btn-secondary{% else %}btn-outline-secondary{% endif %} rounded-pill">
|
||||
<div class="mb-3 overflow-auto">
|
||||
<div class="d-flex flex-nowrap gap-2 pb-2">
|
||||
<a href="?main_category={{ current_main.id }}&sort={{ sort }}"
|
||||
class="btn btn-sm {% if not current_sub %}btn-secondary{% else %}btn-outline-secondary{% endif %} rounded-pill py-1 px-3 small">
|
||||
All {{ current_main.name }}
|
||||
</a>
|
||||
{% for sub in subcategories %}
|
||||
<a href="?main_category={{ current_main.id }}&subcategory={{ sub.id }}"
|
||||
class="btn btn-sm {% if current_sub.id == sub.id %}btn-secondary{% else %}btn-outline-secondary{% endif %} rounded-pill">
|
||||
<a href="?main_category={{ current_main.id }}&subcategory={{ sub.id }}&sort={{ sort }}"
|
||||
class="btn btn-sm {% if current_sub.id == sub.id %}btn-secondary{% else %}btn-outline-secondary{% endif %} rounded-pill py-1 px-3 small">
|
||||
{{ sub.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
@ -42,22 +63,21 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<form method="get" class="d-flex shadow-sm">
|
||||
{% if current_main %}<input type="hidden" name="main_category" value="{{ current_main.id }}">{% endif %}
|
||||
{% if current_sub %}<input type="hidden" name="subcategory" value="{{ current_sub.id }}">{% endif %}
|
||||
<input type="text" name="q" class="form-control bg-dark text-white border-secondary" placeholder="Search accessories..." value="{{ request.GET.q }}">
|
||||
<button class="btn btn-secondary border-secondary" type="submit"><i class="bi bi-search"></i></button>
|
||||
</form>
|
||||
<form method="get" class="mb-4">
|
||||
{% if current_main %}<input type="hidden" name="main_category" value="{{ current_main.id }}">{% endif %}
|
||||
{% if current_sub %}<input type="hidden" name="subcategory" value="{{ current_sub.id }}">{% endif %}
|
||||
<input type="hidden" name="sort" value="{{ sort }}">
|
||||
<div class="input-group input-group-sm glass-card p-1">
|
||||
<span class="input-group-text bg-transparent border-0 text-dim"><i class="fas fa-search"></i></span>
|
||||
<input type="text" name="q" class="form-control bg-transparent border-0 text-white" placeholder="Search accessories..." value="{{ request.GET.q }}">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Items Grid -->
|
||||
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 g-4">
|
||||
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 g-3">
|
||||
{% for item in items %}
|
||||
<div class="col">
|
||||
<div class="card h-100 bg-dark text-white border-secondary shadow-sm hover-card position-relative">
|
||||
<div class="card h-100 bg-dark text-white border-secondary shadow-sm hover-card position-relative overflow-hidden" style="border-radius: 12px;">
|
||||
<a href="{% url 'accessory_item_detail' item.pk %}" class="stretched-link"></a>
|
||||
{% if item.image %}
|
||||
<img src="{{ item.image.url }}" class="card-img-top" style="aspect-ratio: 1/1; object-fit: cover;" alt="{{ item.name }}">
|
||||
@ -67,28 +87,30 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-body p-2">
|
||||
<h6 class="card-title mb-0 text-truncate">{{ item.name|default:"Unnamed" }}</h6>
|
||||
<p class="card-text small text-muted mb-0">{{ item.category.name|default:"Uncategorized" }}</p>
|
||||
{% if item.season %}
|
||||
<span class="badge bg-primary position-absolute top-0 end-0 m-2">{{ item.get_season_display }}</span>
|
||||
{% endif %}
|
||||
<h6 class="card-title mb-0 text-truncate small fw-bold">{{ item.name|default:"Unnamed" }}</h6>
|
||||
<p class="card-text text-dim mb-0" style="font-size: 0.7rem;">{{ item.category.name|default:"Uncategorized" }}</p>
|
||||
</div>
|
||||
{% if item.season %}
|
||||
<span class="badge bg-primary position-absolute top-0 end-0 m-2" style="font-size: 0.6rem;">{{ item.get_season_display }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||||
<p class="mt-2">No accessories found in this category.</p>
|
||||
<i class="bi bi-inbox text-dim" style="font-size: 3rem;"></i>
|
||||
<p class="mt-2 text-dim">No accessories found.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bg-lavender { background-color: var(--accent-lavender) !important; color: #fff !important; }
|
||||
.btn-lavender { background-color: var(--accent-lavender); color: #fff; }
|
||||
.text-dim { color: var(--text-dim); }
|
||||
.hover-card { transition: transform 0.2s, box-shadow 0.2s; }
|
||||
.hover-card:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0,0,0,0.5) !important; z-index: 10; }
|
||||
.nav-pills .nav-link { color: #adb5bd; border: 1px solid transparent; margin-right: 5px; }
|
||||
.nav-pills .nav-link.active { background-color: #0d6efd; color: white; }
|
||||
.nav-pills .nav-link:hover:not(.active) { border-color: #495057; }
|
||||
.hover-card:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0,0,0,0.5) !important; z-index: 1; }
|
||||
.nav-pills .nav-link { border: 1px solid transparent; }
|
||||
.border-secondary { border-color: rgba(255,255,255,0.1) !important; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -11,16 +11,7 @@
|
||||
<i class="fas fa-seedling season-icon {% if season_filter == 'spring' %}active text-success{% endif %}" title="Spring"></i>
|
||||
</a>
|
||||
<a href="?season=autumn" class="text-decoration-none d-flex align-items-center">
|
||||
<span class="season-icon {% if season_filter == 'autumn' %}active{% endif %}" title="Autumn" style="{% if season_filter == 'autumn' %}color: #8B4513; opacity: 1;{% endif %}">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L10 4L12 6L14 4L12 2Z" />
|
||||
<path d="M12 6V22" stroke="currentColor" stroke-width="2" />
|
||||
<path d="M12 10L8 8" stroke="currentColor" stroke-width="2" />
|
||||
<path d="M12 14L16 12" stroke="currentColor" stroke-width="2" />
|
||||
<path d="M12 18L7 16" stroke="currentColor" stroke-width="2" />
|
||||
<path d="M12 12L17 10" stroke="currentColor" stroke-width="2" />
|
||||
</svg>
|
||||
</span>
|
||||
<i class="fas fa-tree season-icon {% if season_filter == 'autumn' %}active{% endif %}" title="Autumn" style="{% if season_filter == 'autumn' %}color: #8B4513 !important; opacity: 1;{% endif %}"></i>
|
||||
</a>
|
||||
<a href="?season=winter" class="text-decoration-none">
|
||||
<i class="fas fa-snowflake season-icon {% if season_filter == 'winter' %}active text-info{% endif %}" title="Winter"></i>
|
||||
@ -30,12 +21,16 @@
|
||||
<div class="small text-uppercase tracking-wider text-dim" style="letter-spacing: 1px;">Total Items</div>
|
||||
<h3 class="mb-0 fw-bold accent-lavender">{{ total_items }}</h3>
|
||||
</div>
|
||||
<div class="avatar-selector">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center border border-2 border-secondary shadow-sm" style="width: 48px; height: 48px; background-color: #333; color: #999;">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 12C14.21 12 16 10.21 16 8C16 5.79 14.21 4 12 4C9.79 4 8 5.79 8 8C8 10.21 9.79 12 12 12ZM12 14C9.33 14 4 15.34 4 18V20H20V18C20 15.34 14.67 14 12 14Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="avatar-selector cursor-pointer" data-bs-toggle="modal" data-bs-target="#avatarModal">
|
||||
{% if profile.avatar %}
|
||||
<img src="{{ profile.avatar.url }}" class="rounded-circle border border-2 border-secondary shadow-sm" style="width: 48px; height: 48px; object-fit: cover;">
|
||||
{% else %}
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center border border-2 border-secondary shadow-sm" style="width: 48px; height: 48px; background-color: #333; color: #999;">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 12C14.21 12 16 10.21 16 8C16 5.79 14.21 4 12 4C9.79 4 8 5.79 8 8C8 10.21 9.79 12 12 12ZM12 14C9.33 14 4 15.34 4 18V20H20V18C20 15.34 14.67 14 12 14Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -47,39 +42,43 @@
|
||||
</div>
|
||||
|
||||
<!-- Row 1: Mon, Tue, Wed -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="row g-2 mb-2">
|
||||
{% for day in calendar_data|slice:":3" %}
|
||||
<div class="col-4">
|
||||
<div class="glass-card p-2 text-center h-100 d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<div class="small text-dim">{{ day.day }}</div>
|
||||
<div class="fw-bold {% if day.date == today %}accent-lavender{% endif %}">{{ day.date|date:"d" }}</div>
|
||||
<div class="small text-dim" style="font-size: 0.65rem;">{{ day.day }}</div>
|
||||
<div class="fw-bold {% if day.date == today %}accent-lavender{% endif %}" style="font-size: 0.9rem;">{{ day.date|date:"d" }}</div>
|
||||
</div>
|
||||
<div class="outfit-preview my-2 position-relative">
|
||||
<div class="outfit-preview my-1 position-relative">
|
||||
{% if day.outfit %}
|
||||
<form action="{% url 'remove_assignment' day.date|date:'Y-m-d' %}" method="post" class="position-absolute top-0 end-0 z-1">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-link text-danger p-0" style="line-height: 1; margin-top: -5px; margin-right: -5px;">
|
||||
<i class="fas fa-times-circle" style="font-size: 0.8rem;"></i>
|
||||
<button type="submit" class="btn btn-link text-danger p-0" style="line-height: 1; margin-top: -8px; margin-right: -8px;">
|
||||
<i class="fas fa-times-circle" style="font-size: 0.9rem; background: rgba(0,0,0,0.6); border-radius: 50%;"></i>
|
||||
</button>
|
||||
</form>
|
||||
<div class="rounded bg-secondary position-relative overflow-hidden" style="height: 60px;">
|
||||
{% with first_item=day.outfit.items.first %}
|
||||
{% if first_item %}
|
||||
<img src="{{ first_item.image.url }}" class="w-100 h-100 object-fit-cover" alt="">
|
||||
{% else %}
|
||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center bg-dark">
|
||||
<i class="fas fa-tshirt text-dim"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="rounded bg-secondary position-relative overflow-hidden d-flex align-items-center justify-content-center p-1" style="height: 100px;">
|
||||
<div class="d-flex align-items-center justify-content-center h-100 w-100 overflow-hidden" style="gap: 3px;">
|
||||
{% for item in day.outfit.items.all %}
|
||||
<img src="{{ item.image.url }}" class="outfit-item-square shadow-sm" alt="{{ item.name }}">
|
||||
{% endfor %}
|
||||
{% for acc in day.outfit.accessories.all %}
|
||||
<img src="{{ acc.image.url }}" class="outfit-item-square shadow-sm" alt="{{ acc.name }}">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if not day.outfit.items.exists and not day.outfit.accessories.exists %}
|
||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center bg-dark">
|
||||
<i class="fas fa-tshirt text-dim"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="small mt-1 text-truncate" style="font-size: 0.7rem;">{{ day.outfit.name|default:"Outfit" }}</div>
|
||||
<div class="small mt-1 text-truncate fw-bold" style="font-size: 0.7rem;">{{ day.outfit.name|default:"Outfit" }}</div>
|
||||
{% else %}
|
||||
<div class="rounded border border-dashed border-secondary d-flex align-items-center justify-content-center cursor-pointer"
|
||||
style="height: 60px; border-style: dashed !important; background: rgba(255,255,255,0.02);"
|
||||
style="height: 100px; border-style: dashed !important; background: rgba(255,255,255,0.02);"
|
||||
data-bs-toggle="modal" data-bs-target="#assignModal" data-date="{{ day.date|date:'Y-m-d' }}" data-day="{{ day.day }}">
|
||||
<i class="fas fa-plus text-dim" style="font-size: 0.8rem;"></i>
|
||||
<i class="fas fa-plus text-dim" style="font-size: 1rem;"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -89,39 +88,43 @@
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Thu, Fri -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="row g-2 mb-2">
|
||||
{% for day in calendar_data|slice:"3:5" %}
|
||||
<div class="col-6">
|
||||
<div class="glass-card p-3 text-center h-100 d-flex flex-column justify-content-between">
|
||||
<div class="glass-card p-2 text-center h-100 d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<div class="small text-dim">{{ day.day }}</div>
|
||||
<div class="fw-bold {% if day.date == today %}accent-lavender{% endif %}">{{ day.date|date:"d" }}</div>
|
||||
<div class="small text-dim" style="font-size: 0.65rem;">{{ day.day }}</div>
|
||||
<div class="fw-bold {% if day.date == today %}accent-lavender{% endif %}" style="font-size: 0.9rem;">{{ day.date|date:"d" }}</div>
|
||||
</div>
|
||||
<div class="outfit-preview my-2 position-relative">
|
||||
<div class="outfit-preview my-1 position-relative">
|
||||
{% if day.outfit %}
|
||||
<form action="{% url 'remove_assignment' day.date|date:'Y-m-d' %}" method="post" class="position-absolute top-0 end-0 z-1">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-link text-danger p-0" style="line-height: 1; margin-top: -5px; margin-right: -5px;">
|
||||
<i class="fas fa-times-circle" style="font-size: 0.9rem;"></i>
|
||||
<button type="submit" class="btn btn-link text-danger p-0" style="line-height: 1; margin-top: -8px; margin-right: -8px;">
|
||||
<i class="fas fa-times-circle" style="font-size: 0.9rem; background: rgba(0,0,0,0.6); border-radius: 50%;"></i>
|
||||
</button>
|
||||
</form>
|
||||
<div class="rounded bg-secondary position-relative overflow-hidden" style="height: 80px;">
|
||||
{% with first_item=day.outfit.items.first %}
|
||||
{% if first_item %}
|
||||
<img src="{{ first_item.image.url }}" class="w-100 h-100 object-fit-cover" alt="">
|
||||
{% else %}
|
||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center bg-dark">
|
||||
<i class="fas fa-tshirt text-dim"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="rounded bg-secondary position-relative overflow-hidden d-flex align-items-center justify-content-center p-1" style="height: 100px;">
|
||||
<div class="d-flex align-items-center justify-content-center h-100 w-100 overflow-hidden" style="gap: 4px;">
|
||||
{% for item in day.outfit.items.all %}
|
||||
<img src="{{ item.image.url }}" class="outfit-item-square shadow-sm" alt="{{ item.name }}">
|
||||
{% endfor %}
|
||||
{% for acc in day.outfit.accessories.all %}
|
||||
<img src="{{ acc.image.url }}" class="outfit-item-square shadow-sm" alt="{{ acc.name }}">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if not day.outfit.items.exists and not day.outfit.accessories.exists %}
|
||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center bg-dark">
|
||||
<i class="fas fa-tshirt text-dim"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="small mt-1 text-truncate">{{ day.outfit.name|default:"Outfit" }}</div>
|
||||
<div class="small mt-1 text-truncate fw-bold" style="font-size: 0.7rem;">{{ day.outfit.name|default:"Outfit" }}</div>
|
||||
{% else %}
|
||||
<div class="rounded border border-dashed border-secondary d-flex align-items-center justify-content-center cursor-pointer"
|
||||
style="height: 80px; border-style: dashed !important; background: rgba(255,255,255,0.02);"
|
||||
style="height: 100px; border-style: dashed !important; background: rgba(255,255,255,0.02);"
|
||||
data-bs-toggle="modal" data-bs-target="#assignModal" data-date="{{ day.date|date:'Y-m-d' }}" data-day="{{ day.day }}">
|
||||
<i class="fas fa-plus text-dim"></i>
|
||||
<i class="fas fa-plus text-dim" style="font-size: 1rem;"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -131,39 +134,43 @@
|
||||
</div>
|
||||
|
||||
<!-- Row 3: Sat, Sun -->
|
||||
<div class="row g-3">
|
||||
<div class="row g-2">
|
||||
{% for day in calendar_data|slice:"5:" %}
|
||||
<div class="col-6">
|
||||
<div class="glass-card p-3 text-center h-100 d-flex flex-column justify-content-between">
|
||||
<div class="glass-card p-2 text-center h-100 d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<div class="small text-dim">{{ day.day }}</div>
|
||||
<div class="fw-bold {% if day.date == today %}accent-lavender{% endif %}">{{ day.date|date:"d" }}</div>
|
||||
<div class="small text-dim" style="font-size: 0.65rem;">{{ day.day }}</div>
|
||||
<div class="fw-bold {% if day.date == today %}accent-lavender{% endif %}" style="font-size: 0.9rem;">{{ day.date|date:"d" }}</div>
|
||||
</div>
|
||||
<div class="outfit-preview my-2 position-relative">
|
||||
<div class="outfit-preview my-1 position-relative">
|
||||
{% if day.outfit %}
|
||||
<form action="{% url 'remove_assignment' day.date|date:'Y-m-d' %}" method="post" class="position-absolute top-0 end-0 z-1">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-link text-danger p-0" style="line-height: 1; margin-top: -5px; margin-right: -5px;">
|
||||
<i class="fas fa-times-circle" style="font-size: 0.9rem;"></i>
|
||||
<button type="submit" class="btn btn-link text-danger p-0" style="line-height: 1; margin-top: -8px; margin-right: -8px;">
|
||||
<i class="fas fa-times-circle" style="font-size: 0.9rem; background: rgba(0,0,0,0.6); border-radius: 50%;"></i>
|
||||
</button>
|
||||
</form>
|
||||
<div class="rounded bg-secondary position-relative overflow-hidden" style="height: 80px;">
|
||||
{% with first_item=day.outfit.items.first %}
|
||||
{% if first_item %}
|
||||
<img src="{{ first_item.image.url }}" class="w-100 h-100 object-fit-cover" alt="">
|
||||
{% else %}
|
||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center bg-dark">
|
||||
<i class="fas fa-tshirt text-dim"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="rounded bg-secondary position-relative overflow-hidden d-flex align-items-center justify-content-center p-1" style="height: 100px;">
|
||||
<div class="d-flex align-items-center justify-content-center h-100 w-100 overflow-hidden" style="gap: 4px;">
|
||||
{% for item in day.outfit.items.all %}
|
||||
<img src="{{ item.image.url }}" class="outfit-item-square shadow-sm" alt="{{ item.name }}">
|
||||
{% endfor %}
|
||||
{% for acc in day.outfit.accessories.all %}
|
||||
<img src="{{ acc.image.url }}" class="outfit-item-square shadow-sm" alt="{{ acc.name }}">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if not day.outfit.items.exists and not day.outfit.accessories.exists %}
|
||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center bg-dark">
|
||||
<i class="fas fa-tshirt text-dim"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="small mt-1 text-truncate">{{ day.outfit.name|default:"Outfit" }}</div>
|
||||
<div class="small mt-1 text-truncate fw-bold" style="font-size: 0.7rem;">{{ day.outfit.name|default:"Outfit" }}</div>
|
||||
{% else %}
|
||||
<div class="rounded border border-dashed border-secondary d-flex align-items-center justify-content-center cursor-pointer"
|
||||
style="height: 80px; border-style: dashed !important; background: rgba(255,255,255,0.02);"
|
||||
style="height: 100px; border-style: dashed !important; background: rgba(255,255,255,0.02);"
|
||||
data-bs-toggle="modal" data-bs-target="#assignModal" data-date="{{ day.date|date:'Y-m-d' }}" data-day="{{ day.day }}">
|
||||
<i class="fas fa-plus text-dim"></i>
|
||||
<i class="fas fa-plus text-dim" style="font-size: 1rem;"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -189,16 +196,16 @@
|
||||
<input type="hidden" name="outfit_id" value="{{ outfit.id }}">
|
||||
<input type="hidden" name="date" class="modal-date-input">
|
||||
<button type="submit" class="list-group-item list-group-item-action bg-dark text-white border-secondary d-flex align-items-center gap-3 py-3">
|
||||
<div class="rounded bg-secondary overflow-hidden" style="width: 50px; height: 50px; flex-shrink: 0;">
|
||||
{% with first_item=outfit.items.first %}
|
||||
{% if first_item %}
|
||||
<img src="{{ first_item.image.url }}" class="w-100 h-100 object-fit-cover" alt="">
|
||||
{% else %}
|
||||
<div class="rounded bg-secondary overflow-hidden d-flex align-items-center justify-content-center p-1" style="width: 80px; height: 50px; flex-shrink: 0;">
|
||||
<div class="d-flex align-items-center justify-content-center h-100 w-100 overflow-hidden" style="gap: 2px;">
|
||||
{% for item in outfit.items.all|slice:":3" %}
|
||||
<img src="{{ item.image.url }}" class="outfit-item-square" style="height: 70% !important;" alt="">
|
||||
{% empty %}
|
||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center bg-dark">
|
||||
<i class="fas fa-tshirt text-dim"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-bold">{{ outfit.name|default:"Unnamed Outfit" }}</div>
|
||||
@ -223,12 +230,41 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Avatar Update Modal -->
|
||||
<div class="modal fade" id="avatarModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content bg-dark text-white border-secondary">
|
||||
<div class="modal-header border-secondary">
|
||||
<h5 class="modal-title">Change Profile Picture</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{% url 'update_avatar' %}" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<div class="mb-3 text-center">
|
||||
{% if profile.avatar %}
|
||||
<img src="{{ profile.avatar.url }}" class="rounded-circle border border-2 border-secondary mb-3" style="width: 100px; height: 100px; object-fit: cover;">
|
||||
{% endif %}
|
||||
<div class="text-start">
|
||||
<label class="form-label small text-uppercase fw-bold text-dim">Select Image</label>
|
||||
{{ avatar_form.avatar }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-secondary">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-accent px-4">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.text-dim { color: var(--text-dim); }
|
||||
.object-fit-cover { object-fit: cover; }
|
||||
.tracking-wider { letter-spacing: 0.05em; }
|
||||
.cursor-pointer { cursor: pointer; }
|
||||
.btn-outline-lavender {
|
||||
@ -239,6 +275,22 @@
|
||||
background-color: var(--primary-accent);
|
||||
color: #000;
|
||||
}
|
||||
.season-icon { font-size: 1.5rem; transition: all 0.2s; opacity: 0.5; }
|
||||
.season-icon.active { opacity: 1; transform: scale(1.2); }
|
||||
|
||||
.outfit-item-square {
|
||||
height: 75%;
|
||||
max-width: 30%;
|
||||
aspect-ratio: 1/1;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
transition: transform 0.2s;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.outfit-item-square:hover {
|
||||
transform: scale(1.05);
|
||||
z-index: 5;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -1,110 +1,197 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3 class="mb-0">{{ title }}</h3>
|
||||
<a href="{% if date_str %}{% url 'home' %}{% else %}{% url 'outfit_list' %}{% endif %}" class="btn-close btn-close-white" aria-label="Close"></a>
|
||||
</div>
|
||||
<div class="container-fluid py-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0 fw-bold">{{ title }}</h4>
|
||||
<a href="{% if date_str %}{% url 'home' %}{% else %}{% url 'outfit_list' %}{% endif %}" class="btn-close btn-close-white" aria-label="Close"></a>
|
||||
</div>
|
||||
|
||||
<form method="post" id="outfit-form">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger">
|
||||
<ul class="mb-0">
|
||||
{% for field, errors in form.errors.items %}
|
||||
{% for error in errors %}
|
||||
<li>{{ field }}: {{ error }}</li>
|
||||
<form method="post" id="outfit-form" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger small py-2">
|
||||
<ul class="mb-0">
|
||||
{% for field, errors in form.errors.items %}
|
||||
{% for error in errors %}
|
||||
<li>{{ field }}: {{ error }}</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label text-dim small text-uppercase">Outfit Name (Optional)</label>
|
||||
{{ form.name }}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6">
|
||||
<label class="form-label text-dim small text-uppercase fw-bold">Outfit Name</label>
|
||||
{{ form.name }}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label text-dim small text-uppercase fw-bold">Season</label>
|
||||
{{ form.season }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label text-dim small text-uppercase">Season (Optional)</label>
|
||||
{{ form.season }}
|
||||
</div>
|
||||
<!-- View Toggle & Category Header -->
|
||||
<div class="d-flex justify-content-between align-items-end mb-2">
|
||||
<div>
|
||||
<label class="form-label text-dim small text-uppercase fw-bold mb-0">Select Items</label>
|
||||
<div class="small text-accent" id="item-count">0 selected</div>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm bg-secondary rounded-pill p-1 shadow-sm">
|
||||
<button type="button" class="btn btn-sm rounded-pill px-3 active" id="view-grid-img" title="Pictures">
|
||||
<i class="fas fa-th-large"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm rounded-pill px-3" id="view-grid-text" title="Names Only">
|
||||
<i class="fas fa-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label text-dim small text-uppercase d-flex justify-content-between">
|
||||
Select Items
|
||||
<span class="badge bg-accent text-dark" id="item-count">0 selected</span>
|
||||
</label>
|
||||
<div class="row g-2 overflow-y-auto no-scrollbar" style="max-height: 300px;">
|
||||
{% for item in wardrobe_items %}
|
||||
<div class="col-4">
|
||||
<div class="item-card position-relative rounded overflow-hidden border border-2 border-transparent"
|
||||
data-id="{{ item.id }}" data-type="item" style="cursor: pointer; height: 100px;">
|
||||
<img src="{{ item.image.url }}" class="w-100 h-100 object-fit-cover">
|
||||
<div class="position-absolute bottom-0 start-0 end-0 p-1 bg-dark bg-opacity-75" style="font-size: 0.6rem;">
|
||||
{{ item.name|default:"Item"|truncatechars:10 }}
|
||||
</div>
|
||||
<div class="check-overlay position-absolute top-0 end-0 p-1 d-none">
|
||||
<i class="fas fa-check-circle text-primary-accent"></i>
|
||||
<!-- Category Navigation -->
|
||||
<div class="category-nav mb-3">
|
||||
<div class="d-flex gap-2 overflow-x-auto no-scrollbar pb-2" id="main-cat-list">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary rounded-pill px-3 active-cat" data-cat-id="all">All</button>
|
||||
{% for cat in main_categories %}
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary rounded-pill px-3 main-cat-btn" data-cat-id="{{ cat.id }}">
|
||||
{{ cat.name }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="d-flex gap-2 overflow-x-auto no-scrollbar pb-2 mt-1 d-none" id="sub-cat-list">
|
||||
<!-- Subcategories will be injected here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wardrobe Item Grid -->
|
||||
<div class="item-grid-container glass-card p-3 mb-4" style="height: 350px; overflow-y: auto;">
|
||||
<div class="row g-3" id="wardrobe-items-grid">
|
||||
{% for item in wardrobe_items %}
|
||||
<div class="col-4 col-sm-3 col-md-2 item-wrapper"
|
||||
data-cat-id="{% if item.category %}{{ item.category.id }}{% else %}none{% endif %}"
|
||||
data-parent-cat-id="{% if item.category %}{% if item.category.parent %}{{ item.category.parent.id }}{% else %}{{ item.category.id }}{% endif %}{% else %}none{% endif %}">
|
||||
<div class="item-card position-relative rounded-2 d-flex flex-column align-items-center p-2"
|
||||
data-id="{{ item.id }}" data-real-id="{{ item.id }}" data-type="item" style="cursor: pointer; transition: background 0.2s;">
|
||||
<div class="item-img-container position-relative rounded-2 overflow-hidden bg-secondary w-100 shadow-sm" style="aspect-ratio: 1/1;">
|
||||
<img src="{{ item.image.url }}" class="w-100 h-100 object-fit-contain item-img">
|
||||
<div class="check-overlay position-absolute top-0 end-0 p-1 d-none" style="z-index: 5;">
|
||||
<i class="fas fa-check-circle text-primary-accent shadow-sm" style="background: white; border-radius: 50%;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-name-label mt-1 text-center small text-truncate w-100 px-1" style="font-size: 0.65rem; color: var(--text-main);">
|
||||
{{ item.name|default:"Item" }}
|
||||
</div>
|
||||
<div class="item-name-only d-none w-100 rounded-2 bg-secondary align-items-center justify-content-center p-2 text-center small fw-bold shadow-sm" style="aspect-ratio: 1/1; font-size: 0.7rem;">
|
||||
{{ item.name|default:"Item" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label text-dim small text-uppercase fw-bold mb-2">Select Accessories</label>
|
||||
<div class="item-grid-container glass-card p-3" style="max-height: 250px; overflow-y: auto;">
|
||||
<div class="row g-3" id="accessories-grid">
|
||||
{% for acc in accessories %}
|
||||
<div class="col-4 col-sm-3 col-md-2 accessory-wrapper">
|
||||
<div class="item-card position-relative rounded-2 d-flex flex-column align-items-center p-2"
|
||||
data-id="{{ acc.id }}" data-real-id="{{ acc.id }}" data-type="accessory" style="cursor: pointer; transition: background 0.2s;">
|
||||
<div class="item-img-container position-relative rounded-2 overflow-hidden bg-secondary w-100 shadow-sm" style="aspect-ratio: 1/1;">
|
||||
<img src="{{ acc.image.url }}" class="w-100 h-100 object-fit-contain item-img">
|
||||
<div class="check-overlay position-absolute top-0 end-0 p-1 d-none" style="z-index: 5;">
|
||||
<i class="fas fa-check-circle text-primary-accent shadow-sm" style="background: white; border-radius: 50%;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-name-label mt-1 text-center small text-truncate w-100 px-1" style="font-size: 0.65rem; color: var(--text-main);">
|
||||
{{ acc.name|default:"Acc" }}
|
||||
</div>
|
||||
<div class="item-name-only d-none w-100 rounded-2 bg-secondary align-items-center justify-content-center p-2 text-center small fw-bold shadow-sm" style="aspect-ratio: 1/1; font-size: 0.7rem;">
|
||||
{{ acc.name|default:"Acc" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label text-dim small text-uppercase fw-bold">Folder / Group (Optional)</label>
|
||||
{{ form.folder }}
|
||||
<div class="mt-2">
|
||||
<a class="text-accent small text-decoration-none" data-bs-toggle="collapse" href="#newFolderCollapse">
|
||||
<i class="fas fa-plus-circle me-1"></i> Add New Group
|
||||
</a>
|
||||
<div class="collapse mt-2" id="newFolderCollapse">
|
||||
{{ form.new_folder }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Selects -->
|
||||
<div class="d-none">
|
||||
{{ form.items }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label text-dim small text-uppercase">Select Accessories</label>
|
||||
<div class="d-flex gap-2 overflow-x-auto pb-2 no-scrollbar">
|
||||
{% for acc in accessories %}
|
||||
<div class="item-card position-relative rounded-circle overflow-hidden border border-2 border-transparent"
|
||||
data-id="{{ acc.id }}" data-type="accessory" style="cursor: pointer; width: 70px; height: 70px; flex-shrink: 0;">
|
||||
<img src="{{ acc.image.url }}" class="w-100 h-100 object-fit-cover">
|
||||
<div class="check-overlay position-absolute top-0 end-0 p-1 d-none">
|
||||
<i class="fas fa-check-circle text-primary-accent"></i>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="d-none">
|
||||
{{ form.accessories }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label text-dim small text-uppercase">Folder / Group (Optional)</label>
|
||||
{{ form.folder }}
|
||||
<div class="mt-2">
|
||||
<a class="text-accent small text-decoration-none" data-bs-toggle="collapse" href="#newFolderCollapse">
|
||||
<i class="fas fa-plus-circle me-1"></i> Add New Group
|
||||
</a>
|
||||
<div class="collapse mt-2" id="newFolderCollapse">
|
||||
{{ form.new_folder }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-accent w-100 py-3 fw-bold">SAVE OUTFIT</button>
|
||||
</form>
|
||||
<button type="submit" class="btn btn-accent w-100 py-3 fw-bold rounded-3 shadow">SAVE OUTFIT</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Windows Folder Grid Inspired Styling */
|
||||
.item-card:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.item-card.selected {
|
||||
border-color: var(--primary-accent) !important;
|
||||
background-color: rgba(var(--primary-accent-rgb, 187, 134, 252), 0.15) !important;
|
||||
}
|
||||
.item-card.selected .item-img-container,
|
||||
.item-card.selected .item-name-only {
|
||||
outline: 2px solid var(--primary-accent) !important;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.item-card.selected .check-overlay {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.border-transparent { border-color: transparent; }
|
||||
.text-primary-accent { color: var(--primary-accent); }
|
||||
.object-fit-cover { object-fit: cover; }
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
|
||||
.active-cat {
|
||||
background-color: var(--primary-accent) !important;
|
||||
color: #000 !important;
|
||||
border-color: var(--primary-accent) !important;
|
||||
}
|
||||
|
||||
.item-card.text-only .item-img-container,
|
||||
.item-card.text-only .item-name-label {
|
||||
display: none !important;
|
||||
}
|
||||
.item-card.text-only .item-name-only {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.object-fit-contain { object-fit: contain; }
|
||||
|
||||
.item-grid-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.item-grid-container::-webkit-scrollbar-track {
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.item-grid-container::-webkit-scrollbar-thumb {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@ -115,31 +202,139 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const itemsSelect = document.querySelector('select[name="items"]');
|
||||
const accsSelect = document.querySelector('select[name="accessories"]');
|
||||
const itemCountBadge = document.getElementById('item-count');
|
||||
|
||||
const mainCatBtns = document.querySelectorAll('.main-cat-btn');
|
||||
const allCatBtn = document.querySelector('[data-cat-id="all"]');
|
||||
const subCatList = document.getElementById('sub-cat-list');
|
||||
const itemWrappers = document.querySelectorAll('.item-wrapper');
|
||||
|
||||
const viewGridImg = document.getElementById('view-grid-img');
|
||||
const viewGridText = document.getElementById('view-grid-text');
|
||||
|
||||
function updateBadge() {
|
||||
const selectedCount = document.querySelectorAll('.item-card.selected').length;
|
||||
itemCountBadge.textContent = `${selectedCount} selected`;
|
||||
itemCountBadge.textContent = `${selectedCount} items selected`;
|
||||
}
|
||||
|
||||
// Item Selection Logic
|
||||
itemCards.forEach(card => {
|
||||
card.addEventListener('click', function() {
|
||||
const id = this.dataset.id;
|
||||
const id = this.dataset.realId || this.dataset.id;
|
||||
const type = this.dataset.type;
|
||||
const isSelected = this.classList.toggle('selected');
|
||||
|
||||
const select = (type === 'item') ? itemsSelect : accsSelect;
|
||||
|
||||
for (let option of select.options) {
|
||||
if (option.value === id) {
|
||||
option.selected = isSelected;
|
||||
break;
|
||||
if (select) {
|
||||
for (let option of select.options) {
|
||||
if (option.value === id) {
|
||||
option.selected = isSelected;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
updateBadge();
|
||||
});
|
||||
});
|
||||
|
||||
// View Toggle Logic
|
||||
if (viewGridImg && viewGridText) {
|
||||
viewGridImg.addEventListener('click', () => {
|
||||
viewGridImg.classList.add('active');
|
||||
viewGridText.classList.remove('active');
|
||||
document.querySelectorAll('.item-card').forEach(card => {
|
||||
card.classList.remove('text-only');
|
||||
});
|
||||
});
|
||||
|
||||
viewGridText.addEventListener('click', () => {
|
||||
viewGridText.classList.add('active');
|
||||
viewGridImg.classList.remove('active');
|
||||
document.querySelectorAll('.item-card').forEach(card => {
|
||||
card.classList.add('text-only');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Category Filtering Logic
|
||||
function filterItems(mainCatId, subCatId = null) {
|
||||
itemWrappers.forEach(wrapper => {
|
||||
const itemMainCatId = wrapper.dataset.parentCatId;
|
||||
const itemSubCatId = wrapper.dataset.catId;
|
||||
|
||||
if (mainCatId === 'all') {
|
||||
wrapper.classList.remove('d-none');
|
||||
} else {
|
||||
if (subCatId) {
|
||||
wrapper.classList.toggle('d-none', itemSubCatId !== subCatId);
|
||||
} else {
|
||||
wrapper.classList.toggle('d-none', itemMainCatId !== mainCatId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (allCatBtn) {
|
||||
allCatBtn.addEventListener('click', function() {
|
||||
document.querySelectorAll('.category-nav button').forEach(b => b.classList.remove('active-cat'));
|
||||
this.classList.add('active-cat');
|
||||
if (subCatList) subCatList.classList.add('d-none');
|
||||
filterItems('all');
|
||||
});
|
||||
}
|
||||
|
||||
mainCatBtns.forEach(btn => {
|
||||
btn.addEventListener('click', async function() {
|
||||
document.querySelectorAll('.category-nav button').forEach(b => b.classList.remove('active-cat'));
|
||||
this.classList.add('active-cat');
|
||||
|
||||
const catId = this.dataset.catId;
|
||||
|
||||
// Fetch subcategories
|
||||
try {
|
||||
const response = await fetch(`/ajax/get-subcategories/?parent_id=${catId}`);
|
||||
const subcats = await response.json();
|
||||
|
||||
if (subCatList) {
|
||||
if (subcats.length > 0) {
|
||||
subCatList.innerHTML = '';
|
||||
// Add "All [Parent]" option
|
||||
const allSubBtn = document.createElement('button');
|
||||
allSubBtn.type = 'button';
|
||||
allSubBtn.className = 'btn btn-xs btn-outline-accent rounded-pill px-3 active-cat';
|
||||
allSubBtn.textContent = 'All ' + this.textContent.trim();
|
||||
allSubBtn.addEventListener('click', function() {
|
||||
subCatList.querySelectorAll('button').forEach(b => b.classList.remove('active-cat'));
|
||||
this.classList.add('active-cat');
|
||||
filterItems(catId);
|
||||
});
|
||||
subCatList.appendChild(allSubBtn);
|
||||
|
||||
subcats.forEach(sc => {
|
||||
const scBtn = document.createElement('button');
|
||||
scBtn.type = 'button';
|
||||
scBtn.className = 'btn btn-xs btn-outline-accent rounded-pill px-3';
|
||||
scBtn.textContent = sc.name;
|
||||
scBtn.addEventListener('click', function() {
|
||||
subCatList.querySelectorAll('button').forEach(b => b.classList.remove('active-cat'));
|
||||
this.classList.add('active-cat');
|
||||
filterItems(catId, sc.id.toString());
|
||||
});
|
||||
subCatList.appendChild(scBtn);
|
||||
});
|
||||
subCatList.classList.remove('d-none');
|
||||
} else {
|
||||
subCatList.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching subcategories:', error);
|
||||
}
|
||||
|
||||
filterItems(catId);
|
||||
});
|
||||
});
|
||||
|
||||
// Initial badge update if some items are selected (e.g. on form error)
|
||||
updateBadge();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -2,156 +2,280 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="mb-0 fw-bold">{{ title }}</h4>
|
||||
<a href="{% url 'new_fit' %}" class="btn btn-sm btn-outline-lavender rounded-pill px-3">
|
||||
<i class="fas fa-plus me-1"></i> New Fit
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Category Tabs (Folders) -->
|
||||
<div class="category-tabs-container mb-4 overflow-auto">
|
||||
<ul class="nav nav-pills flex-nowrap pb-2 gap-2" id="folder-tabs" role="tablist" style="min-width: max-content;">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a href="{% url 'outfit_list' %}" class="nav-link rounded-pill {% if not current_folder %}active bg-lavender{% else %}bg-secondary text-white{% endif %} py-1 px-3 small">
|
||||
All Outfits
|
||||
</a>
|
||||
</li>
|
||||
{% for folder in folders %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<a href="{% url 'outfit_list' %}?folder={{ folder.id }}" class="nav-link rounded-pill {% if current_folder.id == folder.id %}active bg-lavender{% else %}bg-secondary text-white{% endif %} py-1 px-3 small">
|
||||
{{ folder.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<form method="GET" class="mb-4">
|
||||
{% if current_folder %}<input type="hidden" name="folder" value="{{ current_folder.id }}">{% endif %}
|
||||
<div class="input-group input-group-sm glass-card p-1">
|
||||
<span class="input-group-text bg-transparent border-0 text-dim"><i class="fas fa-search"></i></span>
|
||||
<input type="text" name="q" class="form-control bg-transparent border-0 text-white" placeholder="Search outfits..." value="{{ request.GET.q }}">
|
||||
<div class="container-fluid py-2">
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0 fw-bold">{{ title }}</h4>
|
||||
<a href="{% url 'new_fit' %}" class="btn btn-sm btn-outline-lavender rounded-pill px-3">
|
||||
<i class="fas fa-plus me-1"></i> <span class="d-none d-sm-inline">New Fit</span><span class="d-inline d-sm-none">New</span>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Outfit Grid -->
|
||||
<div class="row g-3">
|
||||
{% for outfit in outfits %}
|
||||
<div class="col-6 col-md-4">
|
||||
<div class="glass-card outfit-card h-100 overflow-hidden position-relative">
|
||||
<!-- Delete Button -->
|
||||
<form action="{% url 'delete_outfit' outfit.pk %}" method="post" class="position-absolute top-0 end-0 z-2 m-1">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-link text-danger p-0" onclick="return confirm('Delete this outfit?')">
|
||||
<i class="fas fa-times-circle shadow-sm" style="font-size: 1.1rem; background: rgba(0,0,0,0.5); border-radius: 50%;"></i>
|
||||
</button>
|
||||
</form>
|
||||
<!-- Filter Toggle and Sort Bars -->
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm rounded-pill px-3 mb-2" type="button" data-bs-toggle="collapse" data-bs-target="#filterCollapse" aria-expanded="false">
|
||||
<i class="bi bi-filter me-1"></i> Filter
|
||||
</button>
|
||||
|
||||
<div class="collapse {% if sort != 'recent' %}show{% endif %}" id="filterCollapse">
|
||||
<div class="d-flex flex-column gap-2 mt-2">
|
||||
<a href="?sort=recent{% if current_folder %}&folder={{ current_folder.id }}{% endif %}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}"
|
||||
class="btn {% if sort == 'recent' %}btn-lavender{% else %}btn-outline-secondary{% endif %} btn-sm py-2 text-start px-3 rounded-3 border-0 shadow-sm">
|
||||
Most Recently Added
|
||||
</a>
|
||||
<a href="?sort=alpha{% if current_folder %}&folder={{ current_folder.id }}{% endif %}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}"
|
||||
class="btn {% if sort == 'alpha' %}btn-lavender{% else %}btn-outline-secondary{% endif %} btn-sm py-2 text-start px-3 rounded-3 border-0 shadow-sm">
|
||||
A-Z
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="outfit-collage p-2" data-bs-toggle="modal" data-bs-target="#previewModal{{ outfit.id }}">
|
||||
<div class="rounded bg-secondary position-relative overflow-hidden mb-2" style="aspect-ratio: 1/1;">
|
||||
{% with first_item=outfit.items.first %}
|
||||
{% if first_item %}
|
||||
<img src="{{ first_item.image.url }}" class="w-100 h-100 object-fit-cover" alt="">
|
||||
{% else %}
|
||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center bg-dark">
|
||||
<i class="fas fa-tshirt text-dim fa-2x"></i>
|
||||
</div>
|
||||
<!-- Category Tabs (Folders) -->
|
||||
<div class="category-tabs-container mb-4 overflow-auto">
|
||||
<ul class="nav nav-pills flex-nowrap pb-2 gap-2" id="folder-tabs" role="tablist" style="min-width: max-content;">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a href="{% url 'outfit_list' %}?sort={{ sort }}" class="nav-link rounded-pill {% if not current_folder %}active bg-lavender{% else %}bg-secondary text-white{% endif %} py-1 px-3 small">
|
||||
All Outfits
|
||||
</a>
|
||||
</li>
|
||||
{% for folder in folders %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<a href="{% url 'outfit_list' %}?folder={{ folder.id }}&sort={{ sort }}" class="nav-link rounded-pill {% if current_folder.id == folder.id %}active bg-lavender{% else %}bg-secondary text-white{% endif %} py-1 px-3 small">
|
||||
{{ folder.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<form method="GET" class="mb-4">
|
||||
{% if current_folder %}<input type="hidden" name="folder" value="{{ current_folder.id }}">{% endif %}
|
||||
<input type="hidden" name="sort" value="{{ sort }}">
|
||||
<div class="input-group input-group-sm glass-card p-1">
|
||||
<span class="input-group-text bg-transparent border-0 text-dim"><i class="fas fa-search"></i></span>
|
||||
<input type="text" name="q" class="form-control bg-transparent border-0 text-white" placeholder="Search outfits..." value="{{ request.GET.q }}">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Outfit Grid -->
|
||||
<div class="row g-4">
|
||||
{% for outfit in outfits %}
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="glass-card outfit-card h-100 overflow-hidden position-relative border border-secondary shadow-lg long-press-target"
|
||||
data-outfit-id="{{ outfit.id }}" data-outfit-name="{{ outfit.name|default:'Unnamed Outfit' }}">
|
||||
|
||||
<div class="outfit-collage-container p-3" data-bs-toggle="modal" data-bs-target="#previewModal{{ outfit.id }}" style="cursor: pointer;">
|
||||
<div class="rounded bg-secondary position-relative overflow-hidden mb-3 shadow-inner d-flex align-items-center justify-content-center p-2" style="height: 180px;">
|
||||
<div class="d-flex align-items-center justify-content-center h-100 w-100 overflow-hidden" style="gap: 8px;">
|
||||
{% for item in outfit.items.all|slice:":4" %}
|
||||
<img src="{{ item.image.url }}" class="outfit-item-square shadow-sm" alt="{{ item.name }}">
|
||||
{% endfor %}
|
||||
{% if outfit.accessories.exists %}
|
||||
{% for acc in outfit.accessories.all|slice:":2" %}
|
||||
<img src="{{ acc.image.url }}" class="outfit-item-square rounded-circle shadow-sm" alt="{{ acc.name }}">
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not outfit.items.exists and not outfit.accessories.exists %}
|
||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center bg-dark">
|
||||
<i class="fas fa-tshirt text-dim fa-3x"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="small fw-bold text-truncate">{{ outfit.name|default:"Unnamed Outfit" }}</div>
|
||||
<div class="small text-dim">{{ outfit.items.count }} items</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Schedule Button -->
|
||||
<div class="p-2 pt-0">
|
||||
<button class="btn btn-sm btn-outline-secondary w-100 py-0" style="font-size: 0.7rem;" data-bs-toggle="modal" data-bs-target="#scheduleModal{{ outfit.id }}">
|
||||
<i class="far fa-calendar-alt me-1"></i> Schedule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Modal -->
|
||||
<div class="modal fade" id="previewModal{{ outfit.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content bg-dark text-white border-secondary">
|
||||
<div class="modal-header border-secondary">
|
||||
<h5 class="modal-title">{{ outfit.name|default:"Outfit Details" }}</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<div class="small text-dim mb-2 text-uppercase tracking-wider">Clothing Items</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for item in outfit.items.all %}
|
||||
<div class="rounded bg-secondary overflow-hidden" style="width: 60px; height: 60px;">
|
||||
<img src="{{ item.image.url }}" class="w-100 h-100 object-fit-cover" title="{{ item.name }}">
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-start pe-2">
|
||||
<div>
|
||||
<h6 class="fw-bold mb-1 text-white text-truncate" style="max-width: 200px;">{{ outfit.name|default:"Unnamed Outfit" }}</h6>
|
||||
<div class="small text-dim">
|
||||
<i class="fas fa-tshirt me-1"></i> {{ outfit.items.count }}
|
||||
{% if outfit.accessories.exists %}
|
||||
<i class="fas fa-gem ms-2 me-1"></i> {{ outfit.accessories.count }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% if outfit.accessories.exists %}
|
||||
<div class="mb-3">
|
||||
<div class="small text-dim mb-2 text-uppercase tracking-wider">Accessories</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for acc in outfit.accessories.all %}
|
||||
<div class="rounded bg-secondary overflow-hidden" style="width: 60px; height: 60px;">
|
||||
<img src="{{ acc.image.url }}" class="w-100 h-100 object-fit-cover" title="{{ acc.name }}">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-lavender w-100" data-bs-toggle="modal" data-bs-target="#scheduleModal{{ outfit.id }}">
|
||||
<i class="far fa-calendar-alt me-2"></i> Schedule for this week
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Schedule Button -->
|
||||
<div class="p-3 pt-0 border-top border-secondary mt-2">
|
||||
<button class="btn btn-sm btn-lavender w-100 py-2 mt-2" data-bs-toggle="modal" data-bs-target="#scheduleModal{{ outfit.id }}">
|
||||
<i class="far fa-calendar-alt me-2"></i> Schedule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedule Modal -->
|
||||
<div class="modal fade" id="scheduleModal{{ outfit.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content bg-dark text-white border-secondary">
|
||||
<div class="modal-header border-secondary">
|
||||
<h5 class="modal-title">Schedule Outfit</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for day in week_data %}
|
||||
<form action="{% url 'assign_outfit' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="outfit_id" value="{{ outfit.id }}">
|
||||
<input type="hidden" name="date" value="{{ day.date }}">
|
||||
<button type="submit" class="list-group-item list-group-item-action bg-dark text-white border-secondary d-flex justify-content-between align-items-center py-3">
|
||||
<div>
|
||||
<span class="fw-bold">{{ day.day }}</span>
|
||||
<span class="small text-dim ms-2">{{ day.date }}</span>
|
||||
<!-- Preview Modal -->
|
||||
<div class="modal fade" id="previewModal{{ outfit.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content bg-dark text-white border-secondary">
|
||||
<div class="modal-header border-secondary">
|
||||
<h5 class="modal-title fw-bold">{{ outfit.name|default:"Outfit Details" }}</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-7">
|
||||
<div class="small text-dim mb-3 text-uppercase tracking-wider fw-bold">Clothing Items</div>
|
||||
<div class="row g-2">
|
||||
{% for item in outfit.items.all %}
|
||||
<div class="col-4">
|
||||
<div class="rounded bg-secondary overflow-hidden shadow-sm" style="aspect-ratio: 1/1;">
|
||||
<img src="{{ item.image.url }}" class="w-100 h-100 object-fit-contain" title="{{ item.name }}">
|
||||
</div>
|
||||
<div class="small text-center mt-1 text-truncate" style="font-size: 0.7rem;">{{ item.name }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<i class="fas fa-calendar-plus text-lavender"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
{% if outfit.accessories.exists %}
|
||||
<div class="mb-4">
|
||||
<div class="small text-dim mb-3 text-uppercase tracking-wider fw-bold">Accessories</div>
|
||||
<div class="row g-2">
|
||||
{% for acc in outfit.accessories.all %}
|
||||
<div class="col-6">
|
||||
<div class="rounded-circle bg-secondary overflow-hidden shadow-sm" style="aspect-ratio: 1/1;">
|
||||
<img src="{{ acc.image.url }}" class="w-100 h-100 object-fit-cover" title="{{ acc.name }}">
|
||||
</div>
|
||||
<div class="small text-center mt-1 text-truncate" style="font-size: 0.7rem;">{{ acc.name }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card bg-secondary border-0 p-3 mb-3">
|
||||
<div class="small text-dim mb-1">Created on</div>
|
||||
<div class="fw-bold">{{ outfit.date_created|date:"M d, Y" }}</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-lavender w-100 py-3 fw-bold shadow" data-bs-toggle="modal" data-bs-target="#scheduleModal{{ outfit.id }}">
|
||||
<i class="far fa-calendar-alt me-2"></i> Schedule Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedule Modal -->
|
||||
<div class="modal fade" id="scheduleModal{{ outfit.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content bg-dark text-white border-secondary">
|
||||
<div class="modal-header border-secondary">
|
||||
<h5 class="modal-title fw-bold">Schedule Outfit</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for day in week_data %}
|
||||
<form action="{% url 'assign_outfit' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="outfit_id" value="{{ outfit.id }}">
|
||||
<input type="hidden" name="date" value="{{ day.date }}">
|
||||
<button type="submit" class="list-group-item list-group-item-action bg-dark text-white border-secondary d-flex justify-content-between align-items-center py-3">
|
||||
<div>
|
||||
<span class="fw-bold">{{ day.day }}</span>
|
||||
<span class="small text-dim ms-3">{{ day.date }}</span>
|
||||
</div>
|
||||
<i class="fas fa-calendar-plus text-lavender fa-lg"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="mb-3">
|
||||
<i class="fas fa-tshirt fa-4x text-dim opacity-25"></i>
|
||||
</div>
|
||||
<h5 class="text-white">No outfits found.</h5>
|
||||
<p class="text-dim">Create your first stylish combination!</p>
|
||||
<a href="{% url 'new_fit' %}" class="btn btn-lavender rounded-pill px-4">Create Outfit</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<p class="text-dim">No outfits found.</p>
|
||||
<a href="{% url 'new_fit' %}" class="btn btn-outline-lavender rounded-pill">Create Outfit</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Long Press Options Modal -->
|
||||
<div class="modal fade" id="optionsModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-sm">
|
||||
<div class="modal-content bg-dark text-white border-secondary">
|
||||
<div class="modal-body p-0">
|
||||
<div class="list-group list-group-flush">
|
||||
<button type="button" class="list-group-item list-group-item-action bg-dark text-white border-secondary py-3 text-center fw-bold" id="btn-categorize">
|
||||
<i class="fas fa-folder-plus me-2 text-lavender"></i> Categorize
|
||||
</button>
|
||||
<button type="button" class="list-group-item list-group-item-action bg-dark text-white border-secondary py-3 text-center fw-bold text-danger" id="btn-show-delete-confirm">
|
||||
<i class="fas fa-trash-alt me-2"></i> Delete
|
||||
</button>
|
||||
<button type="button" class="list-group-item list-group-item-action bg-dark text-white border-secondary py-3 text-center small text-dim" data-bs-dismiss="modal">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Categorize Modal -->
|
||||
<div class="modal fade" id="categorizeModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content bg-dark text-white border-secondary">
|
||||
<div class="modal-header border-secondary">
|
||||
<h5 class="modal-title fw-bold">Categorize Outfit</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="move-outfit-form" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-dim small text-uppercase">Select Group</label>
|
||||
<select name="folder_id" class="form-select bg-secondary text-white border-secondary">
|
||||
<option value="">No Group</option>
|
||||
{% for folder in all_folders %}
|
||||
<option value="{{ folder.id }}">{{ folder.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="text-center my-2 text-dim small">- OR -</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label text-dim small text-uppercase">New Group Name</label>
|
||||
<input type="text" name="new_folder_name" class="form-control bg-secondary text-white border-secondary" placeholder="e.g. Work, Vacation">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-lavender w-100 py-2">Save to Group</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content bg-dark text-white border-secondary p-3">
|
||||
<div class="modal-body text-center py-4">
|
||||
<h4 class="fw-bold mb-4">Are you sure?</h4>
|
||||
<div class="d-flex gap-3 mt-4">
|
||||
<button type="button" class="btn btn-outline-secondary flex-grow-1 py-3 rounded-3" data-bs-dismiss="modal">Back</button>
|
||||
<form id="delete-outfit-form" method="post" class="flex-grow-1">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger w-100 py-3 rounded-3 fw-bold" style="background-color: #dc3545 !important;">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
@ -161,10 +285,91 @@
|
||||
.btn-outline-lavender { color: var(--accent-lavender); border-color: var(--accent-lavender); }
|
||||
.btn-outline-lavender:hover { background-color: var(--accent-lavender); color: #fff; }
|
||||
.btn-lavender { background-color: var(--accent-lavender); color: #fff; border: none; }
|
||||
.btn-lavender:hover { opacity: 0.9; }
|
||||
.outfit-card { transition: transform 0.2s; cursor: pointer; }
|
||||
.outfit-card:hover { transform: translateY(-2px); }
|
||||
.object-fit-cover { object-fit: cover; }
|
||||
.z-2 { z-index: 2; }
|
||||
.btn-lavender:hover { opacity: 0.9; transform: translateY(-1px); }
|
||||
.outfit-card { transition: all 0.3s ease; border-radius: 15px !important; }
|
||||
.outfit-card:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0,0,0,0.3) !important; }
|
||||
.shadow-inner { box-shadow: inset 0 2px 4px rgba(0,0,0,0.5); }
|
||||
.border-secondary { border-color: rgba(255,255,255,0.1) !important; }
|
||||
|
||||
.outfit-item-square {
|
||||
height: 80%;
|
||||
max-width: 25%;
|
||||
aspect-ratio: 1/1;
|
||||
object-fit: contain;
|
||||
background-color: rgba(255,255,255,0.05);
|
||||
border-radius: 8px;
|
||||
transition: transform 0.2s;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.long-press-target {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let longPressTimer;
|
||||
const longPressDuration = 600; // ms
|
||||
let currentOutfitId = null;
|
||||
let currentOutfitName = '';
|
||||
|
||||
const optionsModal = new bootstrap.Modal(document.getElementById('optionsModal'));
|
||||
const categorizeModal = new bootstrap.Modal(document.getElementById('categorizeModal'));
|
||||
const deleteConfirmModal = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
|
||||
|
||||
const targets = document.querySelectorAll('.long-press-target');
|
||||
|
||||
targets.forEach(target => {
|
||||
function startPress(e) {
|
||||
longPressTimer = setTimeout(() => {
|
||||
currentOutfitId = target.dataset.outfitId;
|
||||
currentOutfitName = target.dataset.outfitName;
|
||||
optionsModal.show();
|
||||
// Add haptic feedback if available
|
||||
if (window.navigator && window.navigator.vibrate) {
|
||||
window.navigator.vibrate(50);
|
||||
}
|
||||
}, longPressDuration);
|
||||
}
|
||||
|
||||
function cancelPress() {
|
||||
clearTimeout(longPressTimer);
|
||||
}
|
||||
|
||||
target.addEventListener('touchstart', startPress);
|
||||
target.addEventListener('touchend', cancelPress);
|
||||
target.addEventListener('touchmove', cancelPress);
|
||||
target.addEventListener('mousedown', startPress);
|
||||
target.addEventListener('mouseup', cancelPress);
|
||||
target.addEventListener('mouseleave', cancelPress);
|
||||
|
||||
// Also prevent standard context menu on long press for mobile
|
||||
target.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('btn-categorize').addEventListener('click', function() {
|
||||
optionsModal.hide();
|
||||
const form = document.getElementById('move-outfit-form');
|
||||
form.action = `/outfits/${currentOutfitId}/move/`;
|
||||
categorizeModal.show();
|
||||
});
|
||||
|
||||
document.getElementById('btn-show-delete-confirm').addEventListener('click', function() {
|
||||
optionsModal.hide();
|
||||
const form = document.getElementById('delete-outfit-form');
|
||||
form.action = `/outfits/${currentOutfitId}/delete/`;
|
||||
deleteConfirmModal.show();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -2,38 +2,59 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h2 mb-0">Wardrobe</h1>
|
||||
<a href="{% url 'add_wardrobe_item' %}" class="btn btn-primary shadow-sm">
|
||||
<i class="bi bi-plus-lg"></i> Add Item
|
||||
<div class="container-fluid py-2">
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0 fw-bold">Wardrobe</h4>
|
||||
<a href="{% url 'add_wardrobe_item' %}" class="btn btn-primary btn-sm rounded-pill px-3 shadow-sm d-flex align-items-center">
|
||||
<i class="bi bi-plus-lg me-1"></i> <span class="d-none d-sm-inline">Add Item</span><span class="d-inline d-sm-none">Add</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Filter Toggle and Sort Bars -->
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm rounded-pill px-3 mb-2" type="button" data-bs-toggle="collapse" data-bs-target="#filterCollapse" aria-expanded="false">
|
||||
<i class="bi bi-filter me-1"></i> Filter
|
||||
</button>
|
||||
|
||||
<div class="collapse {% if sort != 'recent' %}show{% endif %}" id="filterCollapse">
|
||||
<div class="d-flex flex-column gap-2 mt-2">
|
||||
<a href="?sort=recent{% if current_main %}&main_category={{ current_main.id }}{% endif %}{% if current_sub %}&subcategory={{ current_sub.id }}{% endif %}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}"
|
||||
class="btn {% if sort == 'recent' %}btn-lavender{% else %}btn-outline-secondary{% endif %} btn-sm py-2 text-start px-3 rounded-3 border-0 shadow-sm">
|
||||
Most Recently Added
|
||||
</a>
|
||||
<a href="?sort=alpha{% if current_main %}&main_category={{ current_main.id }}{% endif %}{% if current_sub %}&subcategory={{ current_sub.id }}{% endif %}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}"
|
||||
class="btn {% if sort == 'alpha' %}btn-lavender{% else %}btn-outline-secondary{% endif %} btn-sm py-2 text-start px-3 rounded-3 border-0 shadow-sm">
|
||||
A-Z
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Categories Tabs -->
|
||||
<ul class="nav nav-pills mb-3 overflow-auto flex-nowrap pb-2" id="mainCatTabs" role="tablist">
|
||||
<ul class="nav nav-pills mb-3 overflow-auto flex-nowrap pb-2 gap-2" id="mainCatTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link {% if not current_main %}active{% endif %}" href="{% url 'wardrobe_list' %}">All</a>
|
||||
<a class="nav-link rounded-pill py-1 px-3 small {% if not current_main %}active bg-lavender{% else %}bg-secondary text-white{% endif %}" href="?sort={{ sort }}">All</a>
|
||||
</li>
|
||||
{% for main_cat in main_categories %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link {% if current_main.id == main_cat.id %}active{% endif %}"
|
||||
href="?main_category={{ main_cat.id }}">{{ main_cat.name }}</a>
|
||||
<a class="nav-link rounded-pill py-1 px-3 small {% if current_main.id == main_cat.id %}active bg-lavender{% else %}bg-secondary text-white{% endif %}"
|
||||
href="?main_category={{ main_cat.id }}&sort={{ sort }}">{{ main_cat.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<!-- Subcategories Tabs (if main category selected) -->
|
||||
<!-- Subcategories Tabs -->
|
||||
{% if current_main and subcategories %}
|
||||
<div class="mb-4">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a href="?main_category={{ current_main.id }}"
|
||||
class="btn btn-sm {% if not current_sub %}btn-secondary{% else %}btn-outline-secondary{% endif %} rounded-pill">
|
||||
<div class="mb-3 overflow-auto">
|
||||
<div class="d-flex flex-nowrap gap-2 pb-2">
|
||||
<a href="?main_category={{ current_main.id }}&sort={{ sort }}"
|
||||
class="btn btn-sm {% if not current_sub %}btn-secondary{% else %}btn-outline-secondary{% endif %} rounded-pill py-1 px-3 small">
|
||||
All {{ current_main.name }}
|
||||
</a>
|
||||
{% for sub in subcategories %}
|
||||
<a href="?main_category={{ current_main.id }}&subcategory={{ sub.id }}"
|
||||
class="btn btn-sm {% if current_sub.id == sub.id %}btn-secondary{% else %}btn-outline-secondary{% endif %} rounded-pill">
|
||||
<a href="?main_category={{ current_main.id }}&subcategory={{ sub.id }}&sort={{ sort }}"
|
||||
class="btn btn-sm {% if current_sub.id == sub.id %}btn-secondary{% else %}btn-outline-secondary{% endif %} rounded-pill py-1 px-3 small">
|
||||
{{ sub.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
@ -42,22 +63,21 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<form method="get" class="d-flex shadow-sm">
|
||||
{% if current_main %}<input type="hidden" name="main_category" value="{{ current_main.id }}">{% endif %}
|
||||
{% if current_sub %}<input type="hidden" name="subcategory" value="{{ current_sub.id }}">{% endif %}
|
||||
<input type="text" name="q" class="form-control bg-dark text-white border-secondary" placeholder="Search wardrobe..." value="{{ request.GET.q }}">
|
||||
<button class="btn btn-secondary border-secondary" type="submit"><i class="bi bi-search"></i></button>
|
||||
</form>
|
||||
<form method="get" class="mb-4">
|
||||
{% if current_main %}<input type="hidden" name="main_category" value="{{ current_main.id }}">{% endif %}
|
||||
{% if current_sub %}<input type="hidden" name="subcategory" value="{{ current_sub.id }}">{% endif %}
|
||||
<input type="hidden" name="sort" value="{{ sort }}">
|
||||
<div class="input-group input-group-sm glass-card p-1">
|
||||
<span class="input-group-text bg-transparent border-0 text-dim"><i class="fas fa-search"></i></span>
|
||||
<input type="text" name="q" class="form-control bg-transparent border-0 text-white" placeholder="Search wardrobe..." value="{{ request.GET.q }}">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Items Grid -->
|
||||
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 g-4">
|
||||
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 g-3">
|
||||
{% for item in items %}
|
||||
<div class="col">
|
||||
<div class="card h-100 bg-dark text-white border-secondary shadow-sm hover-card position-relative">
|
||||
<div class="card h-100 bg-dark text-white border-secondary shadow-sm hover-card position-relative overflow-hidden" style="border-radius: 12px;">
|
||||
<a href="{% url 'wardrobe_item_detail' item.pk %}" class="stretched-link"></a>
|
||||
{% if item.image %}
|
||||
<img src="{{ item.image.url }}" class="card-img-top" style="aspect-ratio: 3/4; object-fit: cover;" alt="{{ item.name }}">
|
||||
@ -67,28 +87,30 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-body p-2">
|
||||
<h6 class="card-title mb-0 text-truncate">{{ item.name|default:"Unnamed" }}</h6>
|
||||
<p class="card-text small text-muted mb-0">{{ item.category.name|default:"Uncategorized" }}</p>
|
||||
{% if item.season %}
|
||||
<span class="badge bg-primary position-absolute top-0 end-0 m-2">{{ item.get_season_display }}</span>
|
||||
{% endif %}
|
||||
<h6 class="card-title mb-0 text-truncate small fw-bold">{{ item.name|default:"Unnamed" }}</h6>
|
||||
<p class="card-text text-dim mb-0" style="font-size: 0.7rem;">{{ item.category.name|default:"Uncategorized" }}</p>
|
||||
</div>
|
||||
{% if item.season %}
|
||||
<span class="badge bg-primary position-absolute top-0 end-0 m-2" style="font-size: 0.6rem;">{{ item.get_season_display }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||||
<p class="mt-2">No items found in this category.</p>
|
||||
<i class="bi bi-inbox text-dim" style="font-size: 3rem;"></i>
|
||||
<p class="mt-2 text-dim">No items found.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bg-lavender { background-color: var(--accent-lavender) !important; color: #fff !important; }
|
||||
.btn-lavender { background-color: var(--accent-lavender); color: #fff; }
|
||||
.text-dim { color: var(--text-dim); }
|
||||
.hover-card { transition: transform 0.2s, box-shadow 0.2s; }
|
||||
.hover-card:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0,0,0,0.5) !important; z-index: 10; }
|
||||
.nav-pills .nav-link { color: #adb5bd; border: 1px solid transparent; margin-right: 5px; }
|
||||
.nav-pills .nav-link.active { background-color: #0d6efd; color: white; }
|
||||
.nav-pills .nav-link:hover:not(.active) { border-color: #495057; }
|
||||
.hover-card:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0,0,0,0.5) !important; z-index: 1; }
|
||||
.nav-pills .nav-link { border: 1px solid transparent; }
|
||||
.border-secondary { border-color: rgba(255,255,255,0.1) !important; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -14,7 +14,9 @@ urlpatterns = [
|
||||
path('outfits/', views.outfit_list, name='outfit_list'),
|
||||
path('outfits/new/', views.new_fit, name='new_fit'),
|
||||
path('outfits/<int:pk>/delete/', views.delete_outfit, name='delete_outfit'),
|
||||
path('outfits/<int:pk>/move/', views.move_outfit, name='move_outfit'),
|
||||
path('assign-outfit/', views.assign_outfit, name='assign_outfit'),
|
||||
path('remove-assignment/<str:date>/', views.remove_assignment, name='remove_assignment'),
|
||||
path('ajax/get-subcategories/', views.get_subcategories, name='get_subcategories'),
|
||||
]
|
||||
path('profile/update-avatar/', views.update_avatar, name='update_avatar'),
|
||||
]
|
||||
@ -4,8 +4,21 @@ from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.db.models import Count, Q
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.http import JsonResponse
|
||||
from .models import Category, WardrobeItem, Accessory, Outfit, OutfitFolder, CalendarAssignment
|
||||
from .forms import WardrobeItemForm, AccessoryForm, OutfitForm
|
||||
from django.contrib.auth.models import User
|
||||
from .models import Category, WardrobeItem, Accessory, Outfit, OutfitFolder, CalendarAssignment, UserProfile
|
||||
from .forms import WardrobeItemForm, AccessoryForm, OutfitForm, UserProfileForm
|
||||
|
||||
def get_current_user_profile(request):
|
||||
if request.user.is_authenticated:
|
||||
profile, _ = UserProfile.objects.get_or_create(user=request.user)
|
||||
return profile
|
||||
else:
|
||||
# Fallback to first user if not authenticated (common for simple dev setups)
|
||||
user = User.objects.first()
|
||||
if user:
|
||||
profile, _ = UserProfile.objects.get_or_create(user=user)
|
||||
return profile
|
||||
return None
|
||||
|
||||
def home(request):
|
||||
"""Dashboard view with current week calendar and total items count."""
|
||||
@ -33,6 +46,9 @@ def home(request):
|
||||
|
||||
# All outfits for the "quick add" modal on home screen
|
||||
all_outfits = Outfit.objects.all().order_by('-date_created')
|
||||
|
||||
profile = get_current_user_profile(request)
|
||||
avatar_form = UserProfileForm(instance=profile)
|
||||
|
||||
context = {
|
||||
'total_items': total_items,
|
||||
@ -42,9 +58,20 @@ def home(request):
|
||||
'all_outfits': all_outfits,
|
||||
'week_dates_json': json.dumps([d.strftime('%Y-%m-%d') for d in week_dates]),
|
||||
'day_names': day_names,
|
||||
'profile': profile,
|
||||
'avatar_form': avatar_form,
|
||||
}
|
||||
return render(request, 'core/index.html', context)
|
||||
|
||||
@require_POST
|
||||
def update_avatar(request):
|
||||
profile = get_current_user_profile(request)
|
||||
if profile:
|
||||
form = UserProfileForm(request.POST, request.FILES, instance=profile)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect('home')
|
||||
|
||||
@require_POST
|
||||
def remove_assignment(request, date):
|
||||
CalendarAssignment.objects.filter(date=date).delete()
|
||||
@ -70,6 +97,7 @@ def wardrobe_list(request):
|
||||
query = request.GET.get('q')
|
||||
main_category_id = request.GET.get('main_category')
|
||||
subcategory_id = request.GET.get('subcategory')
|
||||
sort = request.GET.get('sort', 'recent')
|
||||
|
||||
main_categories = Category.objects.filter(item_type='wardrobe', parent=None).annotate(
|
||||
num_items=Count('wardrobe_items', distinct=True),
|
||||
@ -80,7 +108,10 @@ def wardrobe_list(request):
|
||||
current_main = None
|
||||
current_sub = None
|
||||
|
||||
items = WardrobeItem.objects.all().order_by('-date_added')
|
||||
if sort == 'alpha':
|
||||
items = WardrobeItem.objects.all().order_by('name')
|
||||
else:
|
||||
items = WardrobeItem.objects.all().order_by('-date_added')
|
||||
|
||||
if main_category_id:
|
||||
current_main = get_object_or_404(Category, id=main_category_id, parent=None)
|
||||
@ -103,7 +134,8 @@ def wardrobe_list(request):
|
||||
'current_main': current_main,
|
||||
'current_sub': current_sub,
|
||||
'items': items,
|
||||
'title': 'Wardrobe'
|
||||
'title': 'Wardrobe',
|
||||
'sort': sort
|
||||
}
|
||||
return render(request, 'core/wardrobe_list.html', context)
|
||||
|
||||
@ -122,6 +154,7 @@ def accessory_list(request):
|
||||
query = request.GET.get('q')
|
||||
main_category_id = request.GET.get('main_category')
|
||||
subcategory_id = request.GET.get('subcategory')
|
||||
sort = request.GET.get('sort', 'recent')
|
||||
|
||||
main_categories = Category.objects.filter(item_type='accessory', parent=None).annotate(
|
||||
num_items=Count('accessories', distinct=True),
|
||||
@ -132,7 +165,10 @@ def accessory_list(request):
|
||||
current_main = None
|
||||
current_sub = None
|
||||
|
||||
items = Accessory.objects.all().order_by('-date_added')
|
||||
if sort == 'alpha':
|
||||
items = Accessory.objects.all().order_by('name')
|
||||
else:
|
||||
items = Accessory.objects.all().order_by('-date_added')
|
||||
|
||||
if main_category_id:
|
||||
current_main = get_object_or_404(Category, id=main_category_id, parent=None)
|
||||
@ -155,7 +191,8 @@ def accessory_list(request):
|
||||
'current_main': current_main,
|
||||
'current_sub': current_sub,
|
||||
'items': items,
|
||||
'title': 'Accessories'
|
||||
'title': 'Accessories',
|
||||
'sort': sort
|
||||
}
|
||||
return render(request, 'core/accessory_list.html', context)
|
||||
|
||||
@ -173,6 +210,7 @@ def add_accessory_item(request):
|
||||
def outfit_list(request):
|
||||
query = request.GET.get('q')
|
||||
folder_id = request.GET.get('folder')
|
||||
sort = request.GET.get('sort', 'recent')
|
||||
|
||||
def get_valid_folders(parent=None):
|
||||
return OutfitFolder.objects.filter(parent=parent).annotate(
|
||||
@ -181,7 +219,11 @@ def outfit_list(request):
|
||||
).filter(Q(is_preset=True) | Q(num_outfits__gt=0) | Q(num_child_outfits__gt=0))
|
||||
|
||||
folders = get_valid_folders(None)
|
||||
outfits = Outfit.objects.all().order_by('-date_created')
|
||||
|
||||
if sort == 'alpha':
|
||||
outfits = Outfit.objects.all().order_by('name')
|
||||
else:
|
||||
outfits = Outfit.objects.all().order_by('-date_created')
|
||||
|
||||
if folder_id:
|
||||
current_folder = get_object_or_404(OutfitFolder, id=folder_id)
|
||||
@ -202,12 +244,17 @@ def outfit_list(request):
|
||||
day_names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||
week_data = [{'day': day_names[i], 'date': week_dates[i].strftime('%Y-%m-%d')} for i in range(7)]
|
||||
|
||||
# All folders for categorization
|
||||
all_folders = OutfitFolder.objects.all().order_by('name')
|
||||
|
||||
context = {
|
||||
'outfits': outfits,
|
||||
'folders': folders,
|
||||
'all_folders': all_folders,
|
||||
'current_folder': current_folder,
|
||||
'title': 'Outfits',
|
||||
'week_data': week_data,
|
||||
'sort': sort
|
||||
}
|
||||
return render(request, 'core/outfit_list.html', context)
|
||||
|
||||
@ -231,14 +278,17 @@ def new_fit(request):
|
||||
else:
|
||||
form = OutfitForm()
|
||||
|
||||
wardrobe_items = WardrobeItem.objects.all()
|
||||
accessories = Accessory.objects.all()
|
||||
wardrobe_items = WardrobeItem.objects.all().order_by('-date_added')
|
||||
accessories = Accessory.objects.all().order_by('-date_added')
|
||||
|
||||
main_categories = Category.objects.filter(parent=None)
|
||||
|
||||
return render(request, 'core/new_fit.html', {
|
||||
'form': form,
|
||||
'title': f'New Fit for {date_str}' if date_str else 'New Fit',
|
||||
'wardrobe_items': wardrobe_items,
|
||||
'accessories': accessories,
|
||||
'main_categories': main_categories,
|
||||
'date_str': date_str
|
||||
})
|
||||
|
||||
@ -295,4 +345,25 @@ def delete_outfit(request, pk):
|
||||
folder = outfit.folder
|
||||
outfit.delete()
|
||||
cleanup_folder(folder)
|
||||
return redirect('outfit_list')
|
||||
return redirect('outfit_list')
|
||||
|
||||
@require_POST
|
||||
def move_outfit(request, pk):
|
||||
outfit = get_object_or_404(Outfit, pk=pk)
|
||||
folder_id = request.POST.get('folder_id')
|
||||
new_folder_name = request.POST.get('new_folder_name')
|
||||
|
||||
old_folder = outfit.folder
|
||||
|
||||
if new_folder_name:
|
||||
folder = OutfitFolder.objects.create(name=new_folder_name)
|
||||
outfit.folder = folder
|
||||
elif folder_id:
|
||||
folder = get_object_or_404(OutfitFolder, id=folder_id)
|
||||
outfit.folder = folder
|
||||
else:
|
||||
outfit.folder = None
|
||||
|
||||
outfit.save()
|
||||
cleanup_folder(old_folder)
|
||||
return redirect('outfit_list')
|
||||
|
||||
BIN
media/accessories/images_YIMWngy.jpg
Normal file
BIN
media/accessories/images_YIMWngy.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Loading…
x
Reference in New Issue
Block a user