Autosave: 20260204-231352

This commit is contained in:
Flatlogic Bot 2026-02-04 23:13:52 +00:00
parent 88ba420f9e
commit 77d103ab40
17 changed files with 1022 additions and 399 deletions

View File

@ -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

View 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)),
],
),
]

View File

@ -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}"

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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'),
]

View File

@ -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')

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB