diff --git a/.bash_history b/.bash_history new file mode 100644 index 0000000..db0cb72 --- /dev/null +++ b/.bash_history @@ -0,0 +1,58 @@ +pip install django-crispy-forms +pip install crispy-tailwind +exit +npm i -D daisyui@beta +nano requirements.txt +cat requirements.txt +cd .. +exit +cd theme/static_src/ +npm run build:clean +npm run build:tailwind +npm run build:tailwind +npm run build:tailwind +npm run build:clean && npm run build:tailwind +npm run build:clean && npm run build:tailwind +npm run build:clean && npm run build:tailwind +ps ax +npm run dev +exit +cd theme/static_src/ +npm run dev +exit +pwd +cd /code +ls +cd theme/static_src/ +ls +pwd +exit +cd /code/theme/static_src && npm run dev +exit +python manage.py migrate notifications +exit +exit +python manage.py dumpdata --indent 2 trades.TradeOffer +python manage.py dumpdata --indent 2 trades.TradeOffer > seed/0007_TestTradeOffers.json +exit +python manage.py dumpdata --indent 2 trades.TradeOffer > seed/0007_TestTradeOffers.json +python manage.py dumpdata --indent 2 trades.TradeOfferAcceptances > seed/0008_TestTradeOffersAcceptances.json +python manage.py dumpdata --indent 2 trades.TradeOfferAcceptances > seed/0008_TestTradeOffersAcceptance.json +python manage.py dumpdata --indent 2 trades.TradeOfferAcceptance > seed/0008_TestTradeOffersAcceptance.json +python manage.py dumpdata --indent 2 trades.TradeAcceptance > seed/0008_TestTradeAcceptances.json +python manage.py dumpdata --indent 2 trades.TradeOfferWantCard > seed/0008_TestOfferWantCard.json +python manage.py dumpdata --indent 2 trades.TradeOfferHaveCard > seed/0009_TestOfferHaveCard.json +python manage.py dumpdata --indent 2 trades.TradeAcceptance > seed/0008_TestTradeAcceptances.json +exit +python manage.py shell +exit +python manage.py dumpdata trades.TradeOffer --indent 2 +python manage.py dumpdata trades.TradeOffer --indent 2 > seed/0007_TestTradeOffers.json +python manage.py dumpdata trades.TradeOfferHasCard --indent 2 +python manage.py dumpdata trades.TradeOfferHaveCard --indent 2 +python manage.py dumpdata trades.TradeOfferHaveCard --indent 2 > seed/0009_TestOfferHaveCard.json +python manage.py dumpdata trades.TradeOfferWantCard --indent 2 > seed/0009_TestOfferWantCard.json +python manage.py dumpdata trades.TradeOfferWantCard --indent 2 > seed/0008_TestOfferWantCard.json +rm seed/0009_TestOfferWantCard.json +cat seed/0008_TestOfferWantCard.json +exit diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..5e49a06 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,72 @@ +You are an expert in Python, Django, and scalable web application development. + +Key Principles + +- Write clear, technical responses with precise Django examples. +- Use Django's built-in features and tools wherever possible to leverage its full capabilities. +- Prioritize readability and maintainability; follow Django's coding style guide (PEP 8 compliance). +- Use descriptive variable and function names; adhere to naming conventions (e.g., lowercase with underscores for functions and variables). +- Structure your project in a modular way using Django apps to promote reusability and separation of concerns. + +Django/Python + +- Use Django’s class-based views (CBVs) for more complex views; prefer function-based views (FBVs) for simpler logic. +- Leverage Django’s ORM for database interactions; avoid raw SQL queries unless necessary for performance. +- Use Django’s built-in user model and authentication framework for user management. +- Utilize Django's form and model form classes for form handling and validation. +- Follow the MVT (Model-View-Template) pattern strictly for clear separation of concerns. +- Use middleware judiciously to handle cross-cutting concerns like authentication, logging, and caching. + +Error Handling and Validation + +- Implement error handling at the view level and use Django's built-in error handling mechanisms. +- Use Django's validation framework to validate form and model data. +- Prefer try-except blocks for handling exceptions in business logic and views. +- Customize error pages (e.g., 404, 500) to improve user experience and provide helpful information. +- Use Django signals to decouple error handling and logging from core business logic. + +Dependencies + +- Django +- Django REST Framework (for API development) +- Celery (for background tasks) +- Redis (for caching and task queues) +- PostgreSQL or MySQL (preferred databases for production) +- Tailwind CSS for the frontend +- Django Crispy Forms for the frontend +- Django Allauth for authentication +- Django DaisyUI for the frontend +- Django El Pagination for the frontend +- Django Widget Tweaks for the frontend +- Django Crispy Tailwind for the frontend + +Django-Specific Guidelines + +- Use Django templates for rendering HTML and DRF serializers for JSON responses. +- Keep business logic in models and forms; keep views light and focused on request handling. +- Use Django's URL dispatcher (urls.py) to define clear and RESTful URL patterns. +- Apply Django's security best practices (e.g., CSRF protection, SQL injection protection, XSS prevention). +- Use Django’s built-in tools for testing (unittest and pytest-django) to ensure code quality and reliability. +- Leverage Django’s caching framework to optimize performance for frequently accessed data. +- Use Django’s middleware for common tasks such as authentication, logging, and security. + +Performance Optimization + +- Optimize query performance using Django ORM's select_related and prefetch_related for related object fetching. +- Use Django’s cache framework with backend support (e.g., Redis or Memcached) to reduce database load. +- Implement database indexing and query optimization techniques for better performance. +- Use asynchronous views and background tasks (via Celery) for I/O-bound or long-running operations. +- Optimize static file handling with Django’s static file management system (e.g., WhiteNoise or CDN integration). + +Key Conventions + +1. Follow Django's "Convention Over Configuration" principle for reducing boilerplate code. +2. Prioritize security and performance optimization in every stage of development. +3. Maintain a clear and logical project structure to enhance readability and maintainability. + +Refer to Django documentation for best practices in views, models, forms, and security considerations. + +Use the following guidelines for the project: + +- Disable redis cache for now. +- Use PostgreSQL for the database. diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/.python_history b/.python_history new file mode 100644 index 0000000..fb310f7 --- /dev/null +++ b/.python_history @@ -0,0 +1,22 @@ +TradeOffer.objects.filter(initiated\_by=request.user.friend_code +TradeOffer.objects.filter(initiated_by=request.user.friend_code +TradeOffer.objects.filter(initiated_by=request.user.friend_code) +from .models import TradeOffer, TradeAcceptance; TradeOffer.objects.filter(initiated_by=request.user.friend_code) +exit +exit() + from django.contrib.auth.models import User + from trades.models import TradeOffer + # Replace 'your_username' with the username of the user you want to test. + user = User.objects.get(username="your_username") + friend_code = user.friend_code + # Verify the friend code: + print("Friend Code:", friend_code) +from django.contrib.auth.models import User +from trades.models import TradeOffer +# Replace 'your_username' with the username of the user you want to test. +user = User.objects.get(username="your_username") +friend_code = user.friend_code +# Verify the friend code: +print("Friend Code:", friend_code) +exit +exit() diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..7fbeb0a --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Reset DB, Make Migrations, And Seed Data", + "type": "shell", + "command": "./reset-db_make-migrations_seed-data.sh", + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared" + }, + "problemMatcher": [] + }, + { + "label": "Run app", + "type": "shell", + "command": "./entrypoint.sh", + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared" + }, + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/accounts/forms.py b/accounts/forms.py index 4a3063b..f8bdf73 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -1,15 +1,63 @@ from django import forms from django.contrib.auth.forms import UserCreationForm, UserChangeForm -from .models import CustomUser +from .models import CustomUser, FriendCode +from allauth.account.forms import SignupForm class CustomUserCreationForm(UserCreationForm): class Meta(UserCreationForm.Meta): model = CustomUser - fields = ('email', 'username',) + fields = ('email',) class CustomUserChangeForm(UserChangeForm): class Meta: model = CustomUser - fields = ('email', 'username',) \ No newline at end of file + fields = ('email',) + +class FriendCodeForm(forms.ModelForm): + class Meta: + model = FriendCode + fields = ["friend_code"] + + def clean_friend_code(self): + friend_code = self.cleaned_data.get("friend_code", "").strip() + # Remove any dashes from the input for validation. + friend_code_clean = friend_code.replace("-", "") + if len(friend_code_clean) != 16 or not friend_code_clean.isdigit(): + raise forms.ValidationError("Friend code must be exactly 16 digits long.") + # Format the friend code as: XXXX-XXXX-XXXX-XXXX. + friend_code_formatted = f"{friend_code_clean[:4]}-{friend_code_clean[4:8]}-{friend_code_clean[8:12]}-{friend_code_clean[12:16]}" + return friend_code_formatted + +class CustomSignupForm(SignupForm): + friend_code = forms.CharField( + max_length=19, + required=True, + label="Friend Code", + help_text="Enter your friend code in the format XXXX-XXXX-XXXX-XXXX.", + widget=forms.TextInput(attrs={'placeholder': 'XXXX-XXXX-XXXX-XXXX'}) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Remove the username field completely. + if "username" in self.fields: + del self.fields["username"] + + def clean_friend_code(self): + friend_code = self.cleaned_data.get("friend_code", "").strip().replace("-", "") + if len(friend_code) != 16 or not friend_code.isdigit(): + raise forms.ValidationError("Friend code must be exactly 16 digits long.") + formatted = f"{friend_code[:4]}-{friend_code[4:8]}-{friend_code[8:12]}-{friend_code[12:16]}" + return formatted + + def save(self, request): + # First, complete the normal signup process. + user = super().save(request) + # Create the associated FriendCode record. + FriendCode.objects.create( + friend_code=self.cleaned_data["friend_code"], + user=user + ) + return user \ No newline at end of file diff --git a/friend_codes/__init__.py b/accounts/management/__init__.py similarity index 100% rename from friend_codes/__init__.py rename to accounts/management/__init__.py diff --git a/friend_codes/migrations/__init__.py b/accounts/management/commands/__init__.py similarity index 100% rename from friend_codes/migrations/__init__.py rename to accounts/management/commands/__init__.py diff --git a/accounts/management/commands/seed_default_friend_codes.py b/accounts/management/commands/seed_default_friend_codes.py new file mode 100644 index 0000000..9514bc1 --- /dev/null +++ b/accounts/management/commands/seed_default_friend_codes.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand +from accounts.models import CustomUser, FriendCode + +class Command(BaseCommand): + help = "Seed default friend codes for TestUsers after friend codes have been loaded." + + def handle(self, *args, **options): + users_updated = 0 + for user in CustomUser.objects.all(): + # Automatically select the earliest friend code added for the user: + default_code = FriendCode.objects.filter(user=user).order_by('created_at').first() + if default_code: + user.default_friend_code = default_code + user.save(update_fields=["default_friend_code"]) + self.stdout.write(f"Set default friend code for user {user.username} to {default_code.friend_code}.") + users_updated += 1 + else: + self.stdout.write(f"No friend code found for user {user.username}.") + self.stdout.write(self.style.SUCCESS(f"Seeded default friend codes for {users_updated} user(s).")) diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..bfc095d --- /dev/null +++ b/accounts/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# Generated by Django 5.1.2 on 2025-03-07 01:04 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='CustomUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='FriendCode', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('friend_code', models.CharField(max_length=19)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='friend_codes', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='customuser', + name='default_friend_code', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.friendcode'), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index ff1ac10..ca2b979 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,8 +1,47 @@ from django.contrib.auth.models import AbstractUser from django.db import models +from django.core.exceptions import ValidationError class CustomUser(AbstractUser): - pass + default_friend_code = models.ForeignKey("FriendCode", on_delete=models.SET_NULL, null=True, blank=True) def __str__(self): - return self.email \ No newline at end of file + return self.email + + def set_default_friend_code(self, friend_code): + """Set a friend code as default if it belongs to the user.""" + if friend_code.user != self: + raise ValidationError("Friend code does not belong to this user.") + self.default_friend_code = friend_code + self.save(update_fields=["default_friend_code"]) + + def remove_default_friend_code(self, friend_code): + """ + If the given friend code is the current default, + assign another of the user's friend codes (if any) as default. + """ + if self.default_friend_code == friend_code: + other_codes = self.friend_codes.exclude(pk=friend_code.pk) + self.default_friend_code = other_codes.first() if other_codes.exists() else None + self.save(update_fields=["default_friend_code"]) + +class FriendCode(models.Model): + friend_code = models.CharField(max_length=19) + user = models.ForeignKey(CustomUser, on_delete=models.PROTECT, related_name='friend_codes') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def save(self, *args, **kwargs): + """ + When a new friend code is saved, + if the user has no default friend code yet, + automatically set this as the default. + """ + is_new = self.pk is None + super().save(*args, **kwargs) + if is_new and not self.user.default_friend_code: + self.user.default_friend_code = self + self.user.save(update_fields=["default_friend_code"]) + + def __str__(self): + return self.friend_code \ No newline at end of file diff --git a/accounts/templatetags/gravatar.py b/accounts/templatetags/gravatar.py index 9e57e00..b1dac76 100644 --- a/accounts/templatetags/gravatar.py +++ b/accounts/templatetags/gravatar.py @@ -26,6 +26,7 @@ def gravatar_url(email, size=20): params = urlencode({'d': default, 's': str(size)}) return f"https://www.gravatar.com/avatar/{email_hash}?{params}" +@register.filter def gravatar_profile_url(email=None): """ Returns the Gravatar Profile URL for a given email. diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..6bb99fd --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from .views import ListFriendCodesView, AddFriendCodeView, DeleteFriendCodeView, ChangeDefaultFriendCodeView + +urlpatterns = [ + # ... other account URLs ... + path("friend-codes/", ListFriendCodesView.as_view(), name="list_friend_codes"), + path("friend-codes/add/", AddFriendCodeView.as_view(), name="add_friend_code"), + path("friend-codes/delete//", DeleteFriendCodeView.as_view(), name="delete_friend_code"), + path("friend-codes/default//", ChangeDefaultFriendCodeView.as_view(), name="change_default_friend_code"), +] \ No newline at end of file diff --git a/accounts/views.py b/accounts/views.py index 91ea44a..aa1da74 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,3 +1,113 @@ -from django.shortcuts import render +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin +from django.urls import reverse_lazy +from django.shortcuts import redirect, get_object_or_404 +from django.views.generic import ListView, CreateView, DeleteView, View +from accounts.models import FriendCode +from accounts.forms import FriendCodeForm -# Create your views here. +class ListFriendCodesView(LoginRequiredMixin, ListView): + """ + Display the current user's friend codes. + """ + model = FriendCode + template_name = "friend_codes/list_friend_codes.html" + context_object_name = "friend_codes" + + def get_queryset(self): + return self.request.user.friend_codes.all() + +class AddFriendCodeView(LoginRequiredMixin, CreateView): + """ + Add a new friend code for the current user. If the user does not yet have a default, + the newly added code will automatically become the default. + """ + model = FriendCode + form_class = FriendCodeForm + template_name = "friend_codes/add_friend_code.html" + success_url = reverse_lazy("list_friend_codes") + + def form_valid(self, form): + form.instance.user = self.request.user + messages.success(self.request, "Friend code added successfully.") + return super().form_valid(form) + +class DeleteFriendCodeView(LoginRequiredMixin, DeleteView): + """ + Remove an existing friend code. + Prevent deletion if the friend code is bound to any trade offers. + Also, prevent deletion if the friend code is either the only one or + is set as the default friend code. + """ + model = FriendCode + template_name = "friend_codes/confirm_delete_friend_code.html" + context_object_name = "friend_code" + success_url = reverse_lazy("list_friend_codes") + + def get_queryset(self): + # Only allow deletion of friend codes owned by the current user. + return FriendCode.objects.filter(user=self.request.user) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + friend_code = self.get_object() + user = self.request.user + + # Determine if the deletion should be disabled. + disable_delete = False + error_message = None + + if user.friend_codes.count() == 1: + disable_delete = True + error_message = "Cannot delete your only friend code." + elif user.default_friend_code == friend_code: + disable_delete = True + error_message = ( + "Cannot delete your default friend code. " + "Please set a different default first." + ) + + context["disable_delete"] = disable_delete + context["error_message"] = error_message + return context + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + user = self.object.user + + # Check if the friend code is the only one; prevent deletion. + if user.friend_codes.count() == 1: + messages.error(request, "Cannot remove your only friend code.") + return redirect(self.success_url) + + # Check if the friend code is set as default; prevent deletion. + if user.default_friend_code == self.object: + messages.error( + request, + "Cannot delete your default friend code. Please set a different default first." + ) + return redirect(self.success_url) + + # Also check if this friend code is referenced by any trade offer. + if self.object.initiated_by.exists() or self.object.accepted_by.exists(): + messages.error( + request, + "Cannot remove this friend code because there are existing trade offers associated with it." + ) + return redirect(self.success_url) + + # Proceed to safe deletion. + self.object.delete() + messages.success(request, "Friend code removed successfully.") + return redirect(self.success_url + "?deleted=true") + +class ChangeDefaultFriendCodeView(LoginRequiredMixin, View): + """ + Change the default friend code for the current user. + """ + def post(self, request, *args, **kwargs): + friend_code_id = kwargs.get("pk") + friend_code = get_object_or_404(FriendCode, pk=friend_code_id, user=request.user) + request.user.set_default_friend_code(friend_code) + messages.success(request, "Default friend code updated successfully.") + return redirect("list_friend_codes") \ No newline at end of file diff --git a/cards/migrations/0001_initial.py b/cards/migrations/0001_initial.py new file mode 100644 index 0000000..aeb030c --- /dev/null +++ b/cards/migrations/0001_initial.py @@ -0,0 +1,92 @@ +# Generated by Django 5.1.2 on 2025-03-07 01:04 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Card', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('cardnum', models.IntegerField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='CardSet', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='Rarity', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('icons', models.CharField(max_length=64)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='CardNameTranslation', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('language', models.CharField(max_length=64)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('card', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='name_translations', to='cards.card')), + ], + ), + migrations.AddField( + model_name='card', + name='cardset', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='cards', to='cards.cardset'), + ), + migrations.CreateModel( + name='Deck', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('hex_color', models.CharField(max_length=9)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('cardset', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='decks', to='cards.cardset')), + ], + ), + migrations.AddField( + model_name='card', + name='decks', + field=models.ManyToManyField(to='cards.deck'), + ), + migrations.CreateModel( + name='DeckNameTranslation', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('language', models.CharField(max_length=64)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('deck', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='name_translations', to='cards.deck')), + ], + ), + migrations.AddField( + model_name='card', + name='rarity', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='cards', to='cards.rarity'), + ), + ] diff --git a/cards/models.py b/cards/models.py index 80fb1d9..a762ac8 100644 --- a/cards/models.py +++ b/cards/models.py @@ -52,6 +52,15 @@ class Rarity(models.Model): def __str__(self): return self.name + @property + def normalized_id(self): + """ + For trading equivalence: treat Special Art Rare (pk 7) and Super Rare (pk 6) as the same. + """ + if self.pk in (6, 7): + return 6 + return self.pk + class Card(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=64) @@ -63,4 +72,12 @@ class Card(models.Model): updated_at = models.DateTimeField(auto_now=True) def __str__(self): - return self.name + " " + self.rarity.icons + " " + self.cardset.name + # For display, we show the original rarity icons. + return f"{self.name} {self.rarity.icons} {self.cardset.name}" + + @property + def normalized_rarity(self): + """ + Returns the canonical rarity id for trade logic. + """ + return self.rarity.normalized_id diff --git a/cards/templatetags/card_badge.py b/cards/templatetags/card_badge.py index c014637..954a8ea 100644 --- a/cards/templatetags/card_badge.py +++ b/cards/templatetags/card_badge.py @@ -1,11 +1,28 @@ from django import template +from django.template.loader import render_to_string +from django.utils.safestring import mark_safe register = template.Library() -@register.inclusion_tag("includes/card_badge.html") -def card_badge(card): +@register.inclusion_tag("templatetags/card_badge.html") +def card_badge(card, quantity=1, show_single_count=True): return { 'card': card, + 'quantity': quantity, 'decks': card.decks.all() if card else None, - 'dropdown': card is None - } \ No newline at end of file + 'dropdown': card is None, + 'show_single_count': show_single_count, + } + +@register.filter +def card_badge_inline(card, quantity=1): + """ + Renders an inline card badge. + """ + html = render_to_string("templatetags/card_badge.html", { + 'card': card, + 'quantity': quantity, + 'decks': card.decks.all() if card else None, + 'dropdown': card is None, + }) + return mark_safe(html) \ No newline at end of file diff --git a/cards/templatetags/card_multiselect.py b/cards/templatetags/card_multiselect.py index 4d4029a..a9f4579 100644 --- a/cards/templatetags/card_multiselect.py +++ b/cards/templatetags/card_multiselect.py @@ -3,46 +3,69 @@ from cards.models import Card register = template.Library() -@register.inclusion_tag('includes/card_multiselect.html') -def card_multiselect(field_name, label, available_cards, placeholder, selected_values=None, cache_timeout=86400, cache_key="available_cards_options"): +@register.inclusion_tag('templatetags/card_multiselect.html') +def card_multiselect(field_name, label, placeholder, card_filter=None, selected_values=None, cache_timeout=86400, cache_key="available_cards_options"): """ - Renders a Select2 field for choosing cards. - + Renders a multiselect field for choosing cards, storing the card ID only as the option's value and + the quantity in a dedicated data attribute. + Parameters: - field_name: The name attribute for the select tag. - label: Label text to show above the selector. - - available_cards: A queryset or list of card objects that will populate the options. - placeholder: Placeholder text to show in the select. - - selected_values: (Optional) A list of selected card IDs (will be compared as strings). + - card_filter: (Optional) A dictionary of filter parameters to apply on the Card query. + - selected_values: (Optional) A list of selected card values; if a value includes a quantity + it should be in the format "card_id:quantity". - cache_timeout: (Optional) Cache timeout (in seconds) for the options block. - cache_key: (Optional) Cache key—by default both select fields use the same key so that caching is shared. """ if selected_values is None: selected_values = [] - # Normalize selected_values to strings. - selected_values = [str(val) for val in selected_values] + # Map the selected values into a dictionary: { card_id (str): quantity (str) } + selected_cards = {} + for val in selected_values: + parts = str(val).split(':') + card_id = parts[0] + quantity = parts[1] if len(parts) > 1 else "1" + selected_cards[card_id] = quantity + + # If a card_filter is provided, use it; otherwise retrieve all cards. + if card_filter: + available_cards_qs = Card.objects.filter(**card_filter) + else: + available_cards_qs = Card.objects.all() - # --- Available cards for the search form --- available_cards = list( - Card.objects.order_by("name", "rarity__pk") + available_cards_qs.order_by("name", "rarity__pk") .select_related("rarity", "cardset") .prefetch_related("decks") ) + for card in available_cards: - if card.decks.count() == 1: - card.style = "background-color: " + card.decks.all()[0].hex_color + "; color: white;" - elif card.decks.count() == 2: - card.style = "background: linear-gradient(to right, " + card.decks.all()[0].hex_color + ", " + card.decks.all()[1].hex_color + "); color: white;" - elif card.decks.count() >= 3: - card.style = "background: linear-gradient(to right, " + card.decks.all()[0].hex_color + ", " + card.decks.all()[1].hex_color + ", " + card.decks.all()[2].hex_color + "); color: white;" - + # Apply styling based on deck count. + deck_count = card.decks.count() + if deck_count == 1: + card.style = f"background-color: {card.decks.all()[0].hex_color}; color: white;" + elif deck_count == 2: + decks = card.decks.all() + card.style = f"background: linear-gradient(to right, {decks[0].hex_color}, {decks[1].hex_color}); color: white;" + elif deck_count >= 3: + decks = card.decks.all() + card.style = f"background: linear-gradient(to right, {decks[0].hex_color}, {decks[1].hex_color}, {decks[2].hex_color}); color: white;" + + # Attach selected_quantity only if the card is pre‑selected. + pk_str = str(card.pk) + if pk_str in selected_cards: + card.selected_quantity = selected_cards[pk_str] + return { 'field_name': field_name, 'field_id': field_name, # using the name as id for simplicity 'label': label, 'available_cards': available_cards, 'placeholder': placeholder, - 'selected_values': selected_values, + # For caching/selection checks, pass a list of the pre‑selected card IDs. + 'selected_values': list(selected_cards.keys()), 'cache_timeout': cache_timeout, 'cache_key': cache_key, } \ No newline at end of file diff --git a/django_project/settings.py b/django_project/settings.py index add996b..551063f 100644 --- a/django_project/settings.py +++ b/django_project/settings.py @@ -3,7 +3,6 @@ from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ @@ -11,13 +10,15 @@ BASE_DIR = Path(__file__).resolve().parent.parent # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "django-insecure-0peo@#x9jur3!h$ryje!$879xww8y1y66jx!%*#ymhg&jkozs2" +# Resend API Key +RESEND_API_KEY = "re_BBXJWctP_8gb4iNpfaHuau7Na95mc3feu" + # https://docs.djangoproject.com/en/dev/ref/settings/#debug # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts -ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1", "pocket-trade-dev.fly.dev"] - +ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1", "pkmntradeclub.fly.dev", "pkmntrade.club"] # Application definition # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps @@ -35,7 +36,7 @@ INSTALLED_APPS = [ "allauth.account", 'allauth.socialaccount.providers.google', "crispy_forms", - "crispy_bootstrap5", + "crispy_tailwind", "debug_toolbar", "el_pagination", "tailwind", @@ -45,7 +46,7 @@ INSTALLED_APPS = [ "cards", "home", "trades.apps.TradesConfig", - "friend_codes" + "widget_tweaks", ] TAILWIND_APP_NAME = 'theme' @@ -67,7 +68,7 @@ MIDDLEWARE = [ ] DAISY_SETTINGS = { - 'SITE_TITLE': 'Pocket.Trade Admin', + 'SITE_TITLE': 'PKMN Trade Club Admin', 'DONT_SUPPORT_ME': True, } @@ -81,8 +82,7 @@ WSGI_APPLICATION = "django_project.wsgi.application" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - #"DIRS": [BASE_DIR / "templates"], - "DIRS": [BASE_DIR / "theme", BASE_DIR / "templates"], + "DIRS": [BASE_DIR / "theme/templates", BASE_DIR / "theme"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -105,25 +105,25 @@ TEMPLATES = [ # For Docker/PostgreSQL usage uncomment this and comment the DATABASES config above DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql", - "NAME": "postgres", - "USER": "postgres", - "PASSWORD": "postgres", - "HOST": "db", # set in docker-compose.yml - "PORT": 5432, # default postgres port - }, - "neon": { - "ENGINE": "django.db.backends.postgresql", - "NAME": "pocket-trade", - "USER": "pocket_trade_owner", - "PASSWORD": "npg_f1lTpOX7Rnvb", - "HOST": "ep-cool-cake-a6zvgu85-pooler.us-west-2.aws.neon.tech", # set in docker-compose.yml - "PORT": 5432, # default postgres port - "OPTIONS": { - "sslmode": "require" - }, - } + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "postgres", + "USER": "postgres", + "PASSWORD": "", + "HOST": "db", # set in docker-compose.yml + "PORT": 5432, # default postgres port + }, + "neon": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "pocket-trade", + "USER": "pocket_trade_owner", + "PASSWORD": "npg_f1lTpOX7Rnvb", + "HOST": "ep-cool-cake-a6zvgu85-pooler.us-west-2.aws.neon.tech", # set in docker-compose.yml + "PORT": 5432, # default postgres port + "OPTIONS": { + "sslmode": "require" + }, + } } # Password validation @@ -189,14 +189,19 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # django-crispy-forms # https://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs -CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap5' -CRISPY_TEMPLATE_PACK = "bootstrap5" +CRISPY_ALLOWED_TEMPLATE_PACKS = 'tailwind'#'bootstrap5' +CRISPY_TEMPLATE_PACK = "tailwind"#"bootstrap5" # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" +EMAIL_HOST = "smtp.resend.com" +EMAIL_PORT = 587 +EMAIL_HOST_USER = "resend" +EMAIL_HOST_PASSWORD = RESEND_API_KEY +EMAIL_USE_TLS = True # https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email -DEFAULT_FROM_EMAIL = "root@localhost" +DEFAULT_FROM_EMAIL = "noreply@pkmntrade.club" # django-debug-toolbar # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html @@ -226,14 +231,17 @@ AUTHENTICATION_BACKENDS = ( # https://django-allauth.readthedocs.io/en/latest/configuration.html ACCOUNT_SESSION_REMEMBER = True ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False -ACCOUNT_USERNAME_REQUIRED = False ACCOUNT_AUTHENTICATION_METHOD = "email" ACCOUNT_EMAIL_REQUIRED = True #ACCOUNT_EMAIL_VERIFICATION = "mandatory" ACCOUNT_EMAIL_VERIFICATION = "none" ACCOUNT_CHANGE_EMAIL = True ACCOUNT_UNIQUE_EMAIL = True -ACCOUNT_SIGNUP_FORM_HONEYPOT_FIELD = "friend_code" +ACCOUNT_SIGNUP_FORM_HONEYPOT_FIELD = "in_game_username" +ACCOUNT_USERNAME_REQUIRED = False +ACCOUNT_FORMS = { + "signup": "accounts.forms.CustomSignupForm", +} SOCIALACCOUNT_EMAIL_AUTHENTICATION = True SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True SOCIALACCOUNT_ONLY = False diff --git a/django_project/urls.py b/django_project/urls.py index 6e45c60..505ad30 100644 --- a/django_project/urls.py +++ b/django_project/urls.py @@ -7,7 +7,7 @@ urlpatterns = [ path("accounts/", include("allauth.urls")), path("", include("home.urls")), path("cards/", include("cards.urls")), - path('friend_codes/', include('friend_codes.urls')), + path('account/', include('accounts.urls')), path("trades/", include("trades.urls")), path("__reload__/", include("django_browser_reload.urls")), ] diff --git a/docker-compose.yml b/docker-compose.yml index 5ed66e3..f8541e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: web: build: . - command: python /code/manage.py runserver 0.0.0.0:8000 &; python /code/manage.py tailwind start; fg + command: python /code/manage.py runserver 0.0.0.0:8000 volumes: - .:/code:z ports: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..34dba49 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Make the script exit when a command fails. +set -e + +# Define a cleanup function to handle CTRL-C (SIGINT) +cleanup() { + echo "CTRL-C caught! Shutting down Docker Compose services..." + docker compose down + exit 1 +} + +# Set trap to call cleanup() when SIGINT (Ctrl-C) is received. +trap cleanup SIGINT + +# Restart compose services. +echo "Restarting compose services..." +docker compose down +docker compose up -d + +docker compose exec web bash -c "cd /code/theme/static_src && npm run dev" || true + +docker compose down +echo "Done!" \ No newline at end of file diff --git a/friend_codes/admin.py b/friend_codes/admin.py deleted file mode 100644 index bceaa40..0000000 --- a/friend_codes/admin.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.contrib import admin -from .models import FriendCode - -# Register your models here. -admin.site.register(FriendCode) \ No newline at end of file diff --git a/friend_codes/apps.py b/friend_codes/apps.py deleted file mode 100644 index b930ceb..0000000 --- a/friend_codes/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class FriendCodesConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'friend_codes' diff --git a/friend_codes/forms.py b/friend_codes/forms.py deleted file mode 100644 index 4dba12c..0000000 --- a/friend_codes/forms.py +++ /dev/null @@ -1,22 +0,0 @@ -from django import forms -from .models import FriendCode - -class FriendCodeForm(forms.ModelForm): - class Meta: - model = FriendCode - fields = ["friend_code"] - - def clean_friend_code(self): - friend_code = self.cleaned_data.get("friend_code", "").strip() - - # Remove any dashes from the input so we can validate the digits only. - friend_code_clean = friend_code.replace("-", "") - - # Ensure that the cleaned friend code is exactly 16 digits. - if len(friend_code_clean) != 16 or not friend_code_clean.isdigit(): - raise forms.ValidationError("Friend code must be exactly 16 digits long.") - - # Format the friend code with dashes: XXXX-XXXX-XXXX-XXXX. - friend_code_formatted = f"{friend_code_clean[:4]}-{friend_code_clean[4:8]}-{friend_code_clean[8:12]}-{friend_code_clean[12:16]}" - - return friend_code_formatted \ No newline at end of file diff --git a/friend_codes/migrations/0001_initial.py b/friend_codes/migrations/0001_initial.py deleted file mode 100644 index d6ea7e9..0000000 --- a/friend_codes/migrations/0001_initial.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.1.2 on 2025-02-20 02:54 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='FriendCode', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('friend_code', models.CharField(max_length=16)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='friend_codes', to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/friend_codes/migrations/0002_alter_friendcode_friend_code.py b/friend_codes/migrations/0002_alter_friendcode_friend_code.py deleted file mode 100644 index 31efc70..0000000 --- a/friend_codes/migrations/0002_alter_friendcode_friend_code.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.2 on 2025-02-20 03:06 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('friend_codes', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='friendcode', - name='friend_code', - field=models.CharField(max_length=19), - ), - ] diff --git a/friend_codes/models.py b/friend_codes/models.py deleted file mode 100644 index 8acb089..0000000 --- a/friend_codes/models.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.db import models -from django.conf import settings -from accounts.models import CustomUser - -class FriendCode(models.Model): - id = models.AutoField(primary_key=True) - friend_code = models.CharField(max_length=19) - user = models.ForeignKey("accounts.CustomUser", on_delete=models.PROTECT, related_name='friend_codes') - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return self.friend_code \ No newline at end of file diff --git a/friend_codes/tests.py b/friend_codes/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/friend_codes/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/friend_codes/urls.py b/friend_codes/urls.py deleted file mode 100644 index ae07366..0000000 --- a/friend_codes/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.urls import path -from .views import ListFriendCodesView, AddFriendCodeView, DeleteFriendCodeView - -urlpatterns = [ - path('', ListFriendCodesView.as_view(), name='list_friend_codes'), - path('add/', AddFriendCodeView.as_view(), name='add_friend_code'), - path('delete//', DeleteFriendCodeView.as_view(), name='delete_friend_code'), -] \ No newline at end of file diff --git a/friend_codes/views.py b/friend_codes/views.py deleted file mode 100644 index ca2ca7b..0000000 --- a/friend_codes/views.py +++ /dev/null @@ -1,67 +0,0 @@ -from django.shortcuts import render, redirect -from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin -from django.urls import reverse_lazy -from django.views.generic import ListView, CreateView, DeleteView - -from .models import FriendCode -from .forms import FriendCodeForm - - -class ListFriendCodesView(LoginRequiredMixin, ListView): - """ - Display the current user's friend codes. - """ - model = FriendCode - template_name = "friend_codes/list_friend_codes.html" - context_object_name = "friend_codes" - - def get_queryset(self): - # Only display friend codes that belong to the current user. - return self.request.user.friend_codes.all() - - -class AddFriendCodeView(LoginRequiredMixin, CreateView): - """ - Add a new friend code for the current user. - """ - model = FriendCode - form_class = FriendCodeForm - template_name = "friend_codes/add_friend_code.html" - success_url = reverse_lazy('list_friend_codes') - - def form_valid(self, form): - # Set the friend code's user to the current user before saving. - form.instance.user = self.request.user - messages.success(self.request, "Friend code added successfully.") - return super().form_valid(form) - - -class DeleteFriendCodeView(LoginRequiredMixin, DeleteView): - """ - Remove an existing friend code. - The friend code will not be removed if it is referenced by trade offers via - either the initiated_by or accepted_by relationships. - """ - model = FriendCode - template_name = "friend_codes/confirm_delete_friend_code.html" - context_object_name = "friend_code" - success_url = reverse_lazy('list_friend_codes') - - def get_queryset(self): - # Ensure the friend code belongs to the current user. - return FriendCode.objects.filter(user=self.request.user) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - # Check if there are any trade offers associated with this friend code. - if self.object.initiated_by.exists() or self.object.accepted_by.exists(): - messages.error( - request, - "Cannot remove this friend code because there are existing trade offers associated with it." - ) - return redirect(self.success_url) - else: - self.object.delete() - messages.success(request, "Friend code removed successfully.") - return redirect(self.success_url) diff --git a/home/views.py b/home/views.py index eeb5d09..1c7c032 100644 --- a/home/views.py +++ b/home/views.py @@ -1,22 +1,22 @@ from collections import defaultdict from django.views.generic import TemplateView from django.urls import reverse_lazy -from django.db.models import Count, Q +from django.db.models import Count, Q, Prefetch, Sum from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from trades.models import TradeOffer -from cards.models import Card, CardSet +from cards.models import Card, CardSet, Rarity from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page from django.template.response import TemplateResponse -@method_decorator(cache_page(60), name='get') # Cache view for 60 seconds (smallest cache time in the template) +@method_decorator(cache_page(60), name='get') # Cache view for 60 seconds class HomePageView(TemplateView): template_name = "home/home.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - # Use POST data if available, fallback to GET + # Use POST data if available, else fallback to GET request_data = self.request.POST if self.request.method == "POST" else self.request.GET # --- Search form logic --- @@ -25,22 +25,36 @@ class HomePageView(TemplateView): context["offered_cards"] = offered_cards context["wanted_cards"] = wanted_cards + # Define prefetch objects ordered by number of associated trade offers ascending, + # and by id secondarily. + have_cards_prefetch = Prefetch( + 'have_cards', + queryset=Card.objects.annotate( + trade_offer_count=Count("trade_offers_have") + ).order_by("trade_offer_count", "id") + ) + want_cards_prefetch = Prefetch( + 'want_cards', + queryset=Card.objects.annotate( + trade_offer_count=Count("trade_offers_want") + ).order_by("trade_offer_count", "id") + ) + search_results = None if offered_cards or wanted_cards: - qs = TradeOffer.objects.filter( - state=TradeOffer.State.INITIATED - ).prefetch_related( - "have_cards", + # Instead of filtering by a 'state' field (which no longer exists), + # we fetch all offers. You may later add logic to filter only "open" offers. + qs = TradeOffer.objects.all().prefetch_related( + have_cards_prefetch, "have_cards__decks", "have_cards__rarity", "have_cards__cardset", - "want_cards", + want_cards_prefetch, "want_cards__decks", "want_cards__rarity", "want_cards__cardset" ).select_related( - "initiated_by__user", - "accepted_by__user" + "initiated_by__user" ) if offered_cards: try: @@ -56,72 +70,90 @@ class HomePageView(TemplateView): qs = qs.none() else: qs = qs.filter(have_cards__id__in=wanted_card_ids) - # Pagination: 3 results per page + page_number = request_data.get("page", 1) - paginator = Paginator(qs, 3) + paginator = Paginator(qs, 6) try: search_results = paginator.page(page_number) except PageNotAnInteger: search_results = paginator.page(1) except EmptyPage: search_results = paginator.page(paginator.num_pages) + context["search_results"] = search_results # --- Recently posted offers (latest 5, newest first) --- context["recent_offers"] = TradeOffer.objects.order_by("-created_at").prefetch_related( - "have_cards", + have_cards_prefetch, "have_cards__decks", "have_cards__rarity", "have_cards__cardset", - "want_cards", + want_cards_prefetch, "want_cards__decks", "want_cards__rarity", "want_cards__cardset" ).select_related( - "initiated_by__user", - "accepted_by__user" + "initiated_by__user" )[:5] # --- Most offered cards --- - context["most_offered_cards"] = Card.objects.annotate( - offer_count=Count("trade_offers_have") + context["most_offered_cards"] = Card.objects.filter( + tradeofferhavecard__isnull=False + ).annotate( + offer_count=Sum("tradeofferhavecard__quantity") ).order_by("-offer_count").select_related("rarity", "cardset").prefetch_related("decks")[:5] - + # --- Most wanted cards --- - context["most_wanted_cards"] = Card.objects.annotate( - offer_count=Count("trade_offers_want") + context["most_wanted_cards"] = Card.objects.filter( + tradeofferwantcard__isnull=False + ).annotate( + offer_count=Sum("tradeofferwantcard__quantity") ).order_by("-offer_count").select_related("rarity", "cardset").prefetch_related("decks")[:5] - - # --- Featured offers grouped by cardset --- + + # --- Least offered cards --- + context["least_offered_cards"] = Card.objects.annotate( + offer_count=Sum("tradeofferhavecard__quantity") + ).order_by("offer_count", "?")[:5] + + # --- Featured offers grouped by rarity (using card.rarity.icon for tab names) --- featured = {} all_offers = list( TradeOffer.objects.order_by("created_at").prefetch_related( - "have_cards", + have_cards_prefetch, "have_cards__decks", "have_cards__rarity", "have_cards__cardset", - "want_cards", + want_cards_prefetch, "want_cards__decks", "want_cards__rarity", "want_cards__cardset" ).select_related( - "initiated_by__user", - "accepted_by__user" + "initiated_by__user" ) ) featured["All"] = all_offers[:5] - + + # Group offers by normalized rarity id from their have_cards grouped = defaultdict(list) for offer in all_offers: - cardsets_in_offer = set() + normalized_ids = set() for card in offer.have_cards.all(): - cardsets_in_offer.add(card.cardset.name) - for card in offer.want_cards.all(): - cardsets_in_offer.add(card.cardset.name) - for cs_name in cardsets_in_offer: - grouped[cs_name].append(offer) - for cs_name, offers in grouped.items(): - featured[cs_name] = offers[:5] + if card.rarity: + normalized_ids.add(card.rarity.normalized_id) + for norm in normalized_ids: + grouped[norm].append(offer) + + # Map each normalized rarity id to a representative icon + norm_ids_available = list(grouped.keys()) + rareness_qs = Rarity.objects.filter(pk__in=[6] + [nid for nid in norm_ids_available if nid != 6]) + rarity_map = {rarity.pk: rarity.icons for rarity in rareness_qs} + + # Order groups by descending normalized rarity id + for norm in sorted(grouped.keys(), reverse=True): + offers = grouped[norm] + icon_label = rarity_map.get(norm) + if icon_label: + featured[icon_label] = offers[:5] context["featured_offers"] = featured diff --git a/manage.py b/manage.py index fac3788..34f0809 100755 --- a/manage.py +++ b/manage.py @@ -1,4 +1,40 @@ #!/usr/bin/env python +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "asgiref==3.8.1", +# "certifi==2022.12.7", +# "cffi==1.17.1", +# "charset-normalizer==3.0.1", +# "cookiecutter==2.6.0", +# "crispy-tailwind==1.0.3", +# "cryptography==39.0.1", +# "defusedxml==0.7.1", +# "django==5.1.2", +# "django-allauth==65.0.2", +# "django-browser-reload==1.17.0", +# "django-crispy-forms==2.3", +# "django-daisy==1.0.13", +# "django-debug-toolbar==4.4.6", +# "django-el-pagination==4.1.2", +# "django-tailwind-4[reload]==0.1.4", +# "gunicorn==23.0.0", +# "idna==3.4", +# "oauthlib==3.2.2", +# "packaging==23.1", +# "psycopg==3.2.3", +# "psycopg-binary==3.2.3", +# "pycparser==2.21", +# "pyjwt==2.6.0", +# "python3-openid==3.2.0", +# "requests==2.28.2", +# "requests-oauthlib==1.3.1", +# "sqlparse==0.4.3", +# "typing-extensions==4.9.0", +# "urllib3==1.26.14", +# "whitenoise==6.7.0", +# ] +# /// """Django's command-line utility for administrative tasks.""" import os import sys diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..81cc685 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,21 @@ +{ + "name": "code", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "daisyui": "^5.0.0-beta.9" + } + }, + "node_modules/daisyui": { + "version": "5.0.0-beta.9", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.0-beta.9.tgz", + "integrity": "sha512-V+To8o1O8AaxSgdk9QrjXyq/e1AhdW1Z6oUI5iwrOjPs8avM7VQNqoTDCAE5rM0NcMbUfmFgQH8h8guiQ5QPOA==", + "dev": true, + "funding": { + "url": "https://github.com/saadeghi/daisyui?sponsor=1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ded448a --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "daisyui": "^5.0.0-beta.9" + } +} diff --git a/requirements.txt b/requirements.txt index 3d1f861..aaff3c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,20 @@ asgiref==3.8.1 certifi==2022.12.7 -cffi==1.15.1 +cffi==1.17.1 charset-normalizer==3.0.1 cookiecutter==2.6.0 -crispy-bootstrap5==2024.10 +crispy-tailwind==1.0.3 cryptography==39.0.1 defusedxml==0.7.1 Django==5.1.2 django-allauth==65.0.2 django-browser-reload==1.17.0 django-crispy-forms==2.3 -django-daisy==1.0.11 +django-daisy==1.0.13 django-debug-toolbar==4.4.6 django-el-pagination==4.1.2 django-tailwind-4[reload]==0.1.4 +django-widget-tweaks==1.5.0 gunicorn==23.0.0 idna==3.4 oauthlib==3.2.2 diff --git a/reset-db_make-migrations_seed-data.sh b/reset-db_make-migrations_seed-data.sh new file mode 100755 index 0000000..56cb74c --- /dev/null +++ b/reset-db_make-migrations_seed-data.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Exit immediately if any command exits with a non-zero status. +set -e + +# Reset the database and migrations. +echo "Resetting database and migrations..." +docker compose down \ + && docker volume prune -af \ + && find . -path "*/migrations/00*.py" -delete \ + && docker compose up -d + +# Wait for the database to be ready. +echo "Waiting for the database to be ready..." +sleep 10 + +echo "Running makemigrations..." +docker compose exec web bash -c "python manage.py makemigrations" + +echo "Running migrations..." +docker compose exec web bash -c "python manage.py migrate" + +echo "Loading seed data..." +docker compose exec web bash -c "python manage.py loaddata seed/0*" + +echo "Seeding default friend codes..." +docker compose exec web bash -c "python manage.py seed_default_friend_codes" + +./entrypoint.sh \ No newline at end of file diff --git a/seed/0006_TestFriendCodes.json b/seed/0006_TestFriendCodes.json index 388041a..b1011a7 100644 --- a/seed/0006_TestFriendCodes.json +++ b/seed/0006_TestFriendCodes.json @@ -1,6 +1,6 @@ [ { - "model": "friend_codes.friendcode", + "model": "accounts.friendcode", "pk": 1, "fields": { "friend_code": "3595-6375-9151-8459", @@ -10,7 +10,7 @@ } }, { - "model": "friend_codes.friendcode", + "model": "accounts.friendcode", "pk": 2, "fields": { "friend_code": "4863-0754-2764-1890", @@ -20,7 +20,7 @@ } }, { - "model": "friend_codes.friendcode", + "model": "accounts.friendcode", "pk": 3, "fields": { "friend_code": "1020-0576-9371-6042", @@ -30,7 +30,7 @@ } }, { - "model": "friend_codes.friendcode", + "model": "accounts.friendcode", "pk": 4, "fields": { "friend_code": "8358-5883-3807-6654", diff --git a/seed/0007_TestTradeOffers.json b/seed/0007_TestTradeOffers.json index 76a09c9..c88152b 100644 --- a/seed/0007_TestTradeOffers.json +++ b/seed/0007_TestTradeOffers.json @@ -3,232 +3,110 @@ "model": "trades.tradeoffer", "pk": 1, "fields": { + "manually_closed": false, "hash": "c4ca4238", - "initiated_by": 1, - "accepted_by": null, - "created_at": "2025-02-26T06:26:31.024Z", - "updated_at": "2025-02-26T06:26:31.024Z", - "state": "INITIATED", - "want_cards": [ - 188, - 312, - 501 - ], - "have_cards": [ - 55, - 117, - 481 - ] + "initiated_by": 3, + "created_at": "2025-03-07T00:21:33.089Z", + "updated_at": "2025-03-07T00:21:33.089Z" } }, { "model": "trades.tradeoffer", "pk": 2, "fields": { + "manually_closed": false, "hash": "c81e728d", - "initiated_by": 2, - "accepted_by": null, - "created_at": "2025-02-26T06:29:06.154Z", - "updated_at": "2025-02-26T06:29:06.154Z", - "state": "INITIATED", - "want_cards": [ - 17, - 417 - ], - "have_cards": [ - 91, - 524 - ] + "initiated_by": 4, + "created_at": "2025-03-07T00:24:21.664Z", + "updated_at": "2025-03-07T00:24:21.664Z" } }, { "model": "trades.tradeoffer", "pk": 3, "fields": { + "manually_closed": false, "hash": "eccbc87e", "initiated_by": 3, - "accepted_by": null, - "created_at": "2025-02-26T06:29:55.322Z", - "updated_at": "2025-02-26T06:50:58.181Z", - "state": "INITIATED", - "want_cards": [ - 275, - 370 - ], - "have_cards": [ - 575 - ] + "created_at": "2025-03-07T00:27:36.345Z", + "updated_at": "2025-03-07T00:27:36.345Z" } }, { "model": "trades.tradeoffer", "pk": 4, "fields": { + "manually_closed": false, "hash": "a87ff679", - "initiated_by": 3, - "accepted_by": null, - "created_at": "2025-02-26T06:30:45.876Z", - "updated_at": "2025-02-26T06:30:45.876Z", - "state": "INITIATED", - "want_cards": [ - 367, - 558 - ], - "have_cards": [ - 256, - 258, - 559 - ] + "initiated_by": 4, + "created_at": "2025-03-07T00:28:57.655Z", + "updated_at": "2025-03-07T00:28:57.655Z" } }, { "model": "trades.tradeoffer", "pk": 5, "fields": { + "manually_closed": false, "hash": "e4da3b7f", "initiated_by": 4, - "accepted_by": null, - "created_at": "2025-02-26T06:32:37.741Z", - "updated_at": "2025-02-26T06:32:37.741Z", - "state": "INITIATED", - "want_cards": [ - 136, - 165, - 224, - 321, - 375, - 417, - 489 - ], - "have_cards": [ - 15, - 75, - 106, - 116, - 200, - 383, - 424, - 447, - 485, - 512 - ] + "created_at": "2025-03-07T00:30:53.491Z", + "updated_at": "2025-03-07T00:30:53.491Z" } }, { "model": "trades.tradeoffer", "pk": 6, "fields": { + "manually_closed": false, "hash": "1679091c", "initiated_by": 1, - "accepted_by": null, - "created_at": "2025-02-26T06:52:14.287Z", - "updated_at": "2025-02-26T06:52:14.287Z", - "state": "INITIATED", - "want_cards": [ - 16, - 382 - ], - "have_cards": [ - 503, - 517 - ] + "created_at": "2025-03-07T00:21:33.089Z", + "updated_at": "2025-03-07T00:21:33.089Z" } }, { "model": "trades.tradeoffer", "pk": 7, "fields": { + "manually_closed": false, "hash": "8f14e45f", "initiated_by": 2, - "accepted_by": null, - "created_at": "2025-02-26T06:53:25.694Z", - "updated_at": "2025-02-26T06:53:25.694Z", - "state": "INITIATED", - "want_cards": [ - 202, - 375, - 391 - ], - "have_cards": [ - 180, - 321, - 489 - ] + "created_at": "2025-03-07T00:24:21.664Z", + "updated_at": "2025-03-07T00:24:21.664Z" } }, { "model": "trades.tradeoffer", "pk": 8, "fields": { + "manually_closed": false, "hash": "c9f0f895", - "initiated_by": 3, - "accepted_by": null, - "created_at": "2025-02-26T06:55:19.117Z", - "updated_at": "2025-02-26T06:55:19.117Z", - "state": "INITIATED", - "want_cards": [ - 284, - 579 - ], - "have_cards": [ - 285, - 578 - ] + "initiated_by": 1, + "created_at": "2025-03-07T00:27:36.345Z", + "updated_at": "2025-03-07T00:27:36.345Z" } }, { "model": "trades.tradeoffer", "pk": 9, "fields": { + "manually_closed": false, "hash": "45c48cce", - "initiated_by": 4, - "accepted_by": null, - "created_at": "2025-02-26T06:55:39.531Z", - "updated_at": "2025-02-26T06:55:39.531Z", - "state": "INITIATED", - "want_cards": [ - 507 - ], - "have_cards": [ - 115 - ] + "initiated_by": 2, + "created_at": "2025-03-07T00:28:57.655Z", + "updated_at": "2025-03-07T00:28:57.655Z" } }, { "model": "trades.tradeoffer", "pk": 10, "fields": { + "manually_closed": false, "hash": "d3d94468", - "initiated_by": 2, - "accepted_by": null, - "created_at": "2025-02-26T06:55:56.621Z", - "updated_at": "2025-02-26T06:55:56.621Z", - "state": "INITIATED", - "want_cards": [ - 136, - 184 - ], - "have_cards": [ - 91 - ] - } -}, -{ - "model": "trades.tradeoffer", - "pk": 11, - "fields": { - "hash": "6512bd43", - "initiated_by": 3, - "accepted_by": null, - "created_at": "2025-02-26T07:05:16.870Z", - "updated_at": "2025-02-26T07:05:16.870Z", - "state": "INITIATED", - "want_cards": [ - 370 - ], - "have_cards": [ - 367 - ] + "initiated_by": 1, + "created_at": "2025-03-07T00:30:53.491Z", + "updated_at": "2025-03-07T00:30:53.491Z" } } ] diff --git a/seed/0008_TestOfferWantCard.json b/seed/0008_TestOfferWantCard.json new file mode 100644 index 0000000..b453f0d --- /dev/null +++ b/seed/0008_TestOfferWantCard.json @@ -0,0 +1,632 @@ +[ +{ + "model": "trades.tradeofferwantcard", + "pk": 1, + "fields": { + "trade_offer": 1, + "card": 113, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 2, + "fields": { + "trade_offer": 1, + "card": 479, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 3, + "fields": { + "trade_offer": 1, + "card": 206, + "quantity": 8 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 4, + "fields": { + "trade_offer": 1, + "card": 414, + "quantity": 7 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 5, + "fields": { + "trade_offer": 1, + "card": 329, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 6, + "fields": { + "trade_offer": 1, + "card": 395, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 7, + "fields": { + "trade_offer": 1, + "card": 42, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 8, + "fields": { + "trade_offer": 1, + "card": 8, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 9, + "fields": { + "trade_offer": 2, + "card": 165, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 10, + "fields": { + "trade_offer": 2, + "card": 65, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 11, + "fields": { + "trade_offer": 2, + "card": 309, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 12, + "fields": { + "trade_offer": 2, + "card": 219, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 13, + "fields": { + "trade_offer": 2, + "card": 413, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 14, + "fields": { + "trade_offer": 2, + "card": 173, + "quantity": 7 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 15, + "fields": { + "trade_offer": 2, + "card": 469, + "quantity": 8 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 16, + "fields": { + "trade_offer": 2, + "card": 424, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 17, + "fields": { + "trade_offer": 3, + "card": 394, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 18, + "fields": { + "trade_offer": 3, + "card": 437, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 19, + "fields": { + "trade_offer": 3, + "card": 384, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 20, + "fields": { + "trade_offer": 3, + "card": 305, + "quantity": 7 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 21, + "fields": { + "trade_offer": 3, + "card": 13, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 22, + "fields": { + "trade_offer": 3, + "card": 177, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 23, + "fields": { + "trade_offer": 3, + "card": 103, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 24, + "fields": { + "trade_offer": 4, + "card": 345, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 25, + "fields": { + "trade_offer": 4, + "card": 76, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 26, + "fields": { + "trade_offer": 4, + "card": 4, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 27, + "fields": { + "trade_offer": 4, + "card": 471, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 28, + "fields": { + "trade_offer": 4, + "card": 379, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 29, + "fields": { + "trade_offer": 4, + "card": 104, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 30, + "fields": { + "trade_offer": 5, + "card": 230, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 31, + "fields": { + "trade_offer": 5, + "card": 529, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 32, + "fields": { + "trade_offer": 5, + "card": 540, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 33, + "fields": { + "trade_offer": 5, + "card": 239, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 34, + "fields": { + "trade_offer": 5, + "card": 248, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 35, + "fields": { + "trade_offer": 5, + "card": 355, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 36, + "fields": { + "trade_offer": 6, + "card": 115, + "quantity": 8 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 37, + "fields": { + "trade_offer": 6, + "card": 502, + "quantity": 7 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 38, + "fields": { + "trade_offer": 6, + "card": 517, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 39, + "fields": { + "trade_offer": 6, + "card": 105, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 40, + "fields": { + "trade_offer": 6, + "card": 151, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 41, + "fields": { + "trade_offer": 6, + "card": 442, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 42, + "fields": { + "trade_offer": 6, + "card": 287, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 43, + "fields": { + "trade_offer": 6, + "card": 194, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 44, + "fields": { + "trade_offer": 7, + "card": 417, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 45, + "fields": { + "trade_offer": 7, + "card": 321, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 46, + "fields": { + "trade_offer": 7, + "card": 34, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 47, + "fields": { + "trade_offer": 7, + "card": 524, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 48, + "fields": { + "trade_offer": 7, + "card": 108, + "quantity": 7 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 49, + "fields": { + "trade_offer": 7, + "card": 30, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 50, + "fields": { + "trade_offer": 7, + "card": 431, + "quantity": 8 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 51, + "fields": { + "trade_offer": 7, + "card": 150, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 52, + "fields": { + "trade_offer": 8, + "card": 210, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 53, + "fields": { + "trade_offer": 8, + "card": 117, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 54, + "fields": { + "trade_offer": 8, + "card": 40, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 55, + "fields": { + "trade_offer": 8, + "card": 486, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 56, + "fields": { + "trade_offer": 8, + "card": 481, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 57, + "fields": { + "trade_offer": 8, + "card": 425, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 58, + "fields": { + "trade_offer": 8, + "card": 300, + "quantity": 7 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 59, + "fields": { + "trade_offer": 9, + "card": 332, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 60, + "fields": { + "trade_offer": 9, + "card": 41, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 61, + "fields": { + "trade_offer": 9, + "card": 84, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 62, + "fields": { + "trade_offer": 9, + "card": 36, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 63, + "fields": { + "trade_offer": 9, + "card": 482, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 64, + "fields": { + "trade_offer": 9, + "card": 401, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 65, + "fields": { + "trade_offer": 10, + "card": 236, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 66, + "fields": { + "trade_offer": 10, + "card": 549, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 67, + "fields": { + "trade_offer": 10, + "card": 227, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 68, + "fields": { + "trade_offer": 10, + "card": 530, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 69, + "fields": { + "trade_offer": 10, + "card": 359, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferwantcard", + "pk": 70, + "fields": { + "trade_offer": 10, + "card": 238, + "quantity": 6 + } +} +] diff --git a/seed/0009_TestOfferHaveCard.json b/seed/0009_TestOfferHaveCard.json new file mode 100644 index 0000000..422df6d --- /dev/null +++ b/seed/0009_TestOfferHaveCard.json @@ -0,0 +1,632 @@ +[ +{ + "model": "trades.tradeofferhavecard", + "pk": 9, + "fields": { + "trade_offer": 1, + "card": 115, + "quantity": 8 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 2, + "fields": { + "trade_offer": 1, + "card": 502, + "quantity": 7 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 3, + "fields": { + "trade_offer": 1, + "card": 517, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 4, + "fields": { + "trade_offer": 1, + "card": 105, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 5, + "fields": { + "trade_offer": 1, + "card": 151, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 6, + "fields": { + "trade_offer": 1, + "card": 442, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 7, + "fields": { + "trade_offer": 1, + "card": 287, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 8, + "fields": { + "trade_offer": 1, + "card": 194, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 1, + "fields": { + "trade_offer": 2, + "card": 417, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 10, + "fields": { + "trade_offer": 2, + "card": 321, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 11, + "fields": { + "trade_offer": 2, + "card": 34, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 12, + "fields": { + "trade_offer": 2, + "card": 524, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 13, + "fields": { + "trade_offer": 2, + "card": 108, + "quantity": 7 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 14, + "fields": { + "trade_offer": 2, + "card": 30, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 15, + "fields": { + "trade_offer": 2, + "card": 431, + "quantity": 8 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 16, + "fields": { + "trade_offer": 2, + "card": 150, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 17, + "fields": { + "trade_offer": 3, + "card": 210, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 18, + "fields": { + "trade_offer": 3, + "card": 117, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 19, + "fields": { + "trade_offer": 3, + "card": 40, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 20, + "fields": { + "trade_offer": 3, + "card": 486, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 21, + "fields": { + "trade_offer": 3, + "card": 481, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 22, + "fields": { + "trade_offer": 3, + "card": 425, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 23, + "fields": { + "trade_offer": 3, + "card": 300, + "quantity": 7 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 24, + "fields": { + "trade_offer": 4, + "card": 332, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 25, + "fields": { + "trade_offer": 4, + "card": 41, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 26, + "fields": { + "trade_offer": 4, + "card": 84, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 27, + "fields": { + "trade_offer": 4, + "card": 36, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 28, + "fields": { + "trade_offer": 4, + "card": 482, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 29, + "fields": { + "trade_offer": 4, + "card": 401, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 30, + "fields": { + "trade_offer": 5, + "card": 236, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 31, + "fields": { + "trade_offer": 5, + "card": 549, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 32, + "fields": { + "trade_offer": 5, + "card": 227, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 33, + "fields": { + "trade_offer": 5, + "card": 530, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 34, + "fields": { + "trade_offer": 5, + "card": 359, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 35, + "fields": { + "trade_offer": 5, + "card": 238, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 36, + "fields": { + "trade_offer": 6, + "card": 113, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 37, + "fields": { + "trade_offer": 6, + "card": 479, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 38, + "fields": { + "trade_offer": 6, + "card": 206, + "quantity": 8 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 39, + "fields": { + "trade_offer": 6, + "card": 414, + "quantity": 7 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 40, + "fields": { + "trade_offer": 6, + "card": 329, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 41, + "fields": { + "trade_offer": 6, + "card": 395, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 42, + "fields": { + "trade_offer": 6, + "card": 42, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 43, + "fields": { + "trade_offer": 6, + "card": 8, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 44, + "fields": { + "trade_offer": 7, + "card": 165, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 45, + "fields": { + "trade_offer": 7, + "card": 65, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 46, + "fields": { + "trade_offer": 7, + "card": 309, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 47, + "fields": { + "trade_offer": 7, + "card": 219, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 48, + "fields": { + "trade_offer": 7, + "card": 413, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 49, + "fields": { + "trade_offer": 7, + "card": 173, + "quantity": 7 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 50, + "fields": { + "trade_offer": 7, + "card": 469, + "quantity": 8 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 51, + "fields": { + "trade_offer": 7, + "card": 424, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 52, + "fields": { + "trade_offer": 8, + "card": 394, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 53, + "fields": { + "trade_offer": 8, + "card": 437, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 54, + "fields": { + "trade_offer": 8, + "card": 384, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 55, + "fields": { + "trade_offer": 8, + "card": 305, + "quantity": 7 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 56, + "fields": { + "trade_offer": 8, + "card": 13, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 57, + "fields": { + "trade_offer": 8, + "card": 177, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 58, + "fields": { + "trade_offer": 8, + "card": 103, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 59, + "fields": { + "trade_offer": 9, + "card": 345, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 60, + "fields": { + "trade_offer": 9, + "card": 76, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 61, + "fields": { + "trade_offer": 9, + "card": 4, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 62, + "fields": { + "trade_offer": 9, + "card": 471, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 63, + "fields": { + "trade_offer": 9, + "card": 379, + "quantity": 5 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 64, + "fields": { + "trade_offer": 9, + "card": 104, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 65, + "fields": { + "trade_offer": 10, + "card": 230, + "quantity": 1 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 66, + "fields": { + "trade_offer": 10, + "card": 529, + "quantity": 2 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 67, + "fields": { + "trade_offer": 10, + "card": 540, + "quantity": 4 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 68, + "fields": { + "trade_offer": 10, + "card": 239, + "quantity": 3 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 69, + "fields": { + "trade_offer": 10, + "card": 248, + "quantity": 6 + } +}, +{ + "model": "trades.tradeofferhavecard", + "pk": 70, + "fields": { + "trade_offer": 10, + "card": 355, + "quantity": 5 + } +} +] diff --git a/static/css/base.css b/static/css/base.css index dddd1d3..b57274c 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -1,12 +1,12 @@ -/* Helper classes +/* /* Helper classes -------------------------------------------------- */ -.min-width-fit-content { +/* .min-width-fit-content { min-width: fit-content; -} +} */ /* Sticky footer styles -------------------------------------------------- */ -html { +/* html { position: relative; min-height: 100%; font-size: 14px; @@ -18,7 +18,7 @@ html { } body { - margin-bottom: 60px; /* Margin bottom by footer height */ + margin-bottom: 60px; /* Margin bottom by footer height } .container { @@ -33,10 +33,10 @@ body { position: absolute; bottom: 0; width: 100%; - height: 60px; /* Set the fixed height of the footer here */ - line-height: 60px; /* Vertically center the text there */ + height: 60px; /* Set the fixed height of the footer here + line-height: 60px; /* Vertically center the text there background-color: #f5f5f5; -} +} */ /* Trade Offer -------------------------------------------------- */ .trade-offer-grid { @@ -77,52 +77,6 @@ body { /* Card Badge -------------------------------------------------- */ -span:has(> .card-badge-grid) { - display: block; -} - -.card-badge-grid { - display: grid; - grid-template-columns: 1fr auto; - grid-template-rows: 1fr 1fr; - gap: 0.2rem; - padding: 0.3rem 0.5rem; - border-radius: 0.25rem; - font-size: 0.9rem; - min-width: 150px; -} - -.card-badge-name { - grid-column: 1 / span 2; - grid-row: 1; - text-align: left; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.card-badge-rarity { - grid-column: 1; - grid-row: 2; - text-align: left; - min-width: 0; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - display: inline-block; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1.2; -} - -.card-badge-cardset { - grid-column: 2; - grid-row: 2; - text-align: right; - font-size: 0.75rem; - opacity: 0.9; -} - /* Responsive: On narrow viewports, stack the Has and Wants sections */ @media (max-width: 576px) { .trade-offer-grid { @@ -181,4 +135,4 @@ button.select2-selection__choice__remove { display: block !important; position: relative; opacity: 1; -} +} \ No newline at end of file diff --git a/static/js/base.js b/static/js/base.js index ee4656a..2b634e1 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -1,33 +1,26 @@ -$(document).ready(function () { - // Initialize Gravatar - Gravatar.init(); +const $ = x => Array.from(document.querySelectorAll(x)); +const $$ = x => Array.from(document.querySelector(x)); - // Initialize tooltips - $('[data-bs-toggle="tooltip"]').each(function () { - new bootstrap.Tooltip(this); - }); - - // Initialize select2 fields +document.addEventListener('DOMContentLoaded', function() { + // Initialize Gravatar if available + if (typeof Gravatar !== 'undefined' && typeof Gravatar.init === 'function') { + Gravatar.init(); + } - // Updated slider functionality for tab content - $("button[data-bs-toggle='tab']").on("click", function(e) { - e.preventDefault(); // Prevent default Bootstrap behavior - - // Get the target pane selector from the button attribute - var targetSelector = $(this).attr("data-bs-target"); - var $targetPane = $(targetSelector); - - // Update active class on the nav buttons - $(this).closest("ul").find("button").removeClass("active"); - $(this).addClass("active"); - - // Compute the offset of the target pane relative to the grid container - // Using the DOM property offsetLeft ensures any grid gap is taken into account - var offset = $targetPane[0].offsetLeft; - - // Slide the grid: translate the container to align the target pane with the viewport - $("#cardsetTabsContent").css("transform", "translateX(-" + offset + "px)"); - }); -}); - + const themeToggleBtn = document.getElementById('theme-toggle-btn'); + if (themeToggleBtn) { + themeToggleBtn.addEventListener('click', function() { + const root = document.documentElement; + if (root.classList.contains("dark")) { + root.classList.remove("dark"); + root.setAttribute("data-theme", "light"); + localStorage.setItem("theme", "light"); + } else { + root.classList.add("dark"); + root.setAttribute("data-theme", "dark"); + localStorage.setItem("theme", "dark"); + } + }); + } +}); \ No newline at end of file diff --git a/templates/403_csrf.html b/templates/403_csrf.html deleted file mode 100644 index 46d63f0..0000000 --- a/templates/403_csrf.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends '_base.html' %} - -{% block title %}Forbidden (403){% endblock title %} - -{% block content %} -

Forbidden (403)

-

CSRF verification failed. Request aborted.

-{% endblock content %} \ No newline at end of file diff --git a/templates/404.html b/templates/404.html deleted file mode 100644 index 91110b3..0000000 --- a/templates/404.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends '_base.html' %} - -{% block title %}404 Page not found{% endblock %} - -{% block content %} -

Page not found

-{% endblock content %} \ No newline at end of file diff --git a/templates/500.html b/templates/500.html deleted file mode 100644 index ae82ffd..0000000 --- a/templates/500.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends '_base.html' %} - -{% block title %}500 Server Error{% endblock %} - -{% block content %} -

500 Server Error

-

Looks like something went wrong!

-{% endblock content %} \ No newline at end of file diff --git a/templates/_base.html b/templates/_base.html deleted file mode 100644 index 43d2638..0000000 --- a/templates/_base.html +++ /dev/null @@ -1,135 +0,0 @@ -{% load static card_badge %} - - - - - - - - {% block title %}Pocket.Trade{% endblock title %} - - - - - - - - - - - - {% block css %} - {% endblock %} - - - - - - - - - - - - - - - - - - {% block javascript %} - {% endblock javascript %} - - - - - -
- {% block content %} -

Default content...

- {% endblock content %} -
- -
-
- Footer... -
-
- - - \ No newline at end of file diff --git a/templates/account/login.html b/templates/account/login.html deleted file mode 100644 index 10fe7bf..0000000 --- a/templates/account/login.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends '_base.html' %} -{% load crispy_forms_tags %} - -{% block title %}Log in{% endblock %} - -{% block content %} -

Log in

-
- {% csrf_token %} - {{ form|crispy }} - -
-{% endblock content %} diff --git a/templates/account/logout.html b/templates/account/logout.html deleted file mode 100644 index 5eb9747..0000000 --- a/templates/account/logout.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends '_base.html' %} -{% load crispy_forms_tags %} - -{% block title %}Log out{% endblock %} - - -{% block content %} -

Sign Out

- -

Are you sure you want to sign out?

- -
- {% csrf_token %} - {{ form|crispy }} - -
- -{% endblock content %} \ No newline at end of file diff --git a/templates/account/password_change.html b/templates/account/password_change.html deleted file mode 100644 index 937c99a..0000000 --- a/templates/account/password_change.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends '_base.html' %} -{% load crispy_forms_tags %} - -{% block title %}Change Password{% endblock %} - -{% block content %} -

Change Password

-
- {% csrf_token %} - {{ form|crispy }} - -
-{% endblock content %} diff --git a/templates/account/password_reset.html b/templates/account/password_reset.html deleted file mode 100644 index be2c91d..0000000 --- a/templates/account/password_reset.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends '_base.html' %} -{% load crispy_forms_tags %} - -{% block title %}Password Reset{% endblock %} - -{% block content %} -

Forgot your password?

-
- {% csrf_token %} - {{ form | crispy }} - -
-{% endblock content %} diff --git a/templates/account/password_reset_done.html b/templates/account/password_reset_done.html deleted file mode 100644 index 2e6f3d3..0000000 --- a/templates/account/password_reset_done.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends '_base.html' %} -{% load crispy_forms_tags %} - -{% block title %}Password Reset Done{% endblock %} - -{% block content %} -

Password Reset

-

We have sent you an e-mail. Please contact us if you do not receive it in a few minutes.

-{% endblock content %} diff --git a/templates/account/password_reset_from_key.html b/templates/account/password_reset_from_key.html deleted file mode 100644 index fb9eeb3..0000000 --- a/templates/account/password_reset_from_key.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends '_base.html' %} -{% load crispy_forms_tags %} - -{% block title %}Change Password{% endblock title %} - -{% block content %} -

{% if token_fail %}Bad Token{% else %}Change Password{% endif %}

- - {% if token_fail %} -

The password reset link was invalid. Perhaps it has already been used? Please request a new password reset.

- {% else %} - {% if form %} -
- {% csrf_token %} - {{ form|crispy }} - -
- {% else %} -

Your password is now changed.

- {% endif %} - {% endif %} -{% endblock content%} diff --git a/templates/account/password_reset_from_key_done.html b/templates/account/password_reset_from_key_done.html deleted file mode 100644 index 58c4094..0000000 --- a/templates/account/password_reset_from_key_done.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends '_base.html' %} -{% load crispy_forms_tags %} - -{% block title %}Change Password Done{% endblock title %} - -{% block content %} -

Password Change Done

-

Your password has been changed.

-{% endblock content %} \ No newline at end of file diff --git a/templates/account/password_set.html b/templates/account/password_set.html deleted file mode 100644 index a46b8f1..0000000 --- a/templates/account/password_set.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends '_base.html' %} -{% load crispy_forms_tags %} - -{% block title %}Set Password{% endblock title %} - -{% block content %} -
- {% csrf_token %} - {{ form | crispy }} -
- -
-
-{% endblock content %} diff --git a/templates/account/signup.html b/templates/account/signup.html deleted file mode 100644 index da021a9..0000000 --- a/templates/account/signup.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends '_base.html' %} -{% load crispy_forms_tags %} - -{% block title %}Sign up{% endblock %} - -{% block content %} -

Sign up

-
- {% csrf_token %} - {{ form|crispy }} - -
-{% endblock content %} \ No newline at end of file diff --git a/templates/friend_codes/add_friend_code.html b/templates/friend_codes/add_friend_code.html deleted file mode 100644 index 7afb0f0..0000000 --- a/templates/friend_codes/add_friend_code.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends '_base.html' %} -{% load crispy_forms_tags %} - -{% block title %}Add Friend Code{% endblock %} - -{% block content %} -

Add Friend Code

-
- {% csrf_token %} - {{ form|crispy }} - -
- -

- Back to Friend Codes -

-{% endblock %} \ No newline at end of file diff --git a/templates/friend_codes/confirm_delete_friend_code.html b/templates/friend_codes/confirm_delete_friend_code.html deleted file mode 100644 index dada659..0000000 --- a/templates/friend_codes/confirm_delete_friend_code.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends '_base.html' %} -{% load crispy_forms_tags %} - -{% block title %}Log in{% endblock %} - -{% block content %} -

Are you sure you want to delete friend code: {{ friend_code.friend_code }}?

-
- {% csrf_token %} - - Cancel -
-{% endblock content %} \ No newline at end of file diff --git a/templates/friend_codes/list_friend_codes.html b/templates/friend_codes/list_friend_codes.html deleted file mode 100644 index 0a5e499..0000000 --- a/templates/friend_codes/list_friend_codes.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends '_base.html' %} - -{% block title %}My Friend Codes{% endblock %} - -{% block content %} -

My Friend Codes

- - {% if friend_codes %} -
    - {% for code in friend_codes %} -
  • - {{ code.friend_code }} - - Delete -
  • - {% endfor %} -
- {% else %} -

You do not have any friend codes added yet.

- {% endif %} - -

- Add a New Friend Code -

-{% endblock %} \ No newline at end of file diff --git a/templates/home/_search_results.html b/templates/home/_search_results.html deleted file mode 100644 index 8d5b172..0000000 --- a/templates/home/_search_results.html +++ /dev/null @@ -1,42 +0,0 @@ -{% load trade_offer_tags %} -{% if offered_cards or wanted_cards %} -
-

Results

- {% if search_results and search_results.object_list %} - - - - {% else %} -
No trade offers found.
- {% endif %} -{% endif %} \ No newline at end of file diff --git a/templates/home/home.html b/templates/home/home.html deleted file mode 100644 index d226673..0000000 --- a/templates/home/home.html +++ /dev/null @@ -1,239 +0,0 @@ -{% extends '_base.html' %} -{% load static %} -{% load trade_offer_tags card_badge %} -{% load cache %} -{% load card_multiselect %} - -{% block content %} -
-

Welcome to Pocket.Trade

- - - - -
- {% include "home/_search_results.html" %} -
- - -
-

Market Stats

-
- -
-
Most Offered Cards
-
-
- {% cache 3600 most_offered_cards %} - {% if most_offered_cards %} -
- {% for card in most_offered_cards %} - {% if card.offer_count > 0 %} - - {% card_badge card %} - {{ card.offer_count }} - - {% endif %} - {% endfor %} -
- {% else %} -

No cards found

- {% endif %} - {% endcache %} -
-
-
- - -
-
Most Wanted Cards
-
-
- {% cache 3600 most_wanted_cards %} - {% if most_wanted_cards %} -
- {% for card in most_wanted_cards %} - {% if card.offer_count > 0 %} - - {% card_badge card %} - {{ card.offer_count }} - - {% endif %} - {% endfor %} -
- {% else %} -

No cards found

- {% endif %} - {% endcache %} -
-
-
-
-
- - -
- -
- {% cache 86400 featured_offers %} -
-
-
Featured Offers
- -
-
-
- -
- {% if featured_offers.All %} -
- {% for offer in featured_offers.All %} - - {% render_trade_offer offer %} - - {% endfor %} -
- {% else %} -

No featured offers available.

- {% endif %} -
- - {% for cardset, offers in featured_offers.items %} - {% if cardset != "All" %} -
- {% if offers %} -
- {% for offer in offers %} - - {% render_trade_offer offer %} - - {% endfor %} -
- {% else %} -

No featured offers for {{ cardset }}.

- {% endif %} -
- {% endif %} - {% endfor %} -
-
-
- {% endcache %} -
- - -
- {% cache 60 recent_offers %} -
-
-
Recent Offers
-
- {% for offer in recent_offers %} - - {% render_trade_offer offer %} - - {% empty %} -
No offers available
- {% endfor %} -
-
-
- {% endcache %} -
-
-
-{% endblock content %} - -{% block javascript %} - - -{% endblock %} diff --git a/templates/includes/card_badge.html b/templates/includes/card_badge.html deleted file mode 100644 index 3044fc4..0000000 --- a/templates/includes/card_badge.html +++ /dev/null @@ -1,13 +0,0 @@ -{% if decks|length == 1 %} - {% if dropdown %}'{% endif %}{% if dropdown %}' + {% endif %} -{% elif decks|length == 2 %} - {% if dropdown %}'{% endif %}{% if dropdown %}' + {% endif %} -{% elif decks|length >= 3 %} - {% if dropdown %}'{% endif %}{% if dropdown %}' + {% endif %} -{% else %} - {% if dropdown %}'{% endif %}{% if dropdown %}' + {% endif %} -{% endif %} - {% if dropdown %}'{% endif %}{% if dropdown %}'+ cardName +'{% else %}{{ card.name }}{% endif %}{% if dropdown %}' + {% endif %} - {% if dropdown %}'{% endif %}{% if dropdown %}'+ rarity +'{% else %}{{ card.rarity.icons }}{% endif %}{% if dropdown %}' + {% endif %} - {% if dropdown %}'{% endif %}{% if dropdown %}'+ cardset +'{% else %}{{ card.cardset.name }}{% endif %}{% if dropdown %}' + {% endif %} - {% if dropdown %}'{% endif %}{% if dropdown %}'{% endif %} \ No newline at end of file diff --git a/templates/includes/card_multiselect.html b/templates/includes/card_multiselect.html deleted file mode 100644 index 4a92c7a..0000000 --- a/templates/includes/card_multiselect.html +++ /dev/null @@ -1,29 +0,0 @@ -{% load cache card_badge %} - - - - \ No newline at end of file diff --git a/templates/includes/trade_offer.html b/templates/includes/trade_offer.html deleted file mode 100644 index 1bc254c..0000000 --- a/templates/includes/trade_offer.html +++ /dev/null @@ -1,62 +0,0 @@ -{% load gravatar card_badge %} - -
-
- -
- -
- {% if offer.initiated_by and offer.initiated_by.user.email %} - -
- {{ offer.initiated_by.user.email|gravatar:40 }} -
- {% endif %} - -
-
Has
-
-
- -
- {% if offer.accepted_by and offer.accepted_by.user.email %} - -
- {{ offer.accepted_by.user.email|gravatar:40 }} -
- {% endif %} - -
-
Wants
-
-
-
- - -
-
-
- {% if offer.have_cards.all %} - {% for card in offer.have_cards.all %} - {% card_badge card %} - {% endfor %} - {% endif %} -
-
-
-
- {% if offer.want_cards.all %} - {% for card in offer.want_cards.all %} - {% card_badge card %} - {% endfor %} - {% endif %} -
-
-
- - - - - -
-
\ No newline at end of file diff --git a/templates/trades/trade_offer_create.html b/templates/trades/trade_offer_create.html deleted file mode 100644 index f5a9a71..0000000 --- a/templates/trades/trade_offer_create.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends '_base.html' %} -{% load static %} -{% load card_multiselect %} - -{% block title %}Create Trade Offer{% endblock title %} - -{% block content %} -

Create a Trade Offer

-
- {% csrf_token %} - {# Render the non–Select2 field normally (e.g. initiated_by) #} -
- - {{ form.initiated_by }} -
-
- {% card_multiselect "have_cards" "Have:" available_cards "Select one or more cards..." form.have_cards.value %} -
-
- {% card_multiselect "want_cards" "Want:" available_cards "Select one or more cards..." form.want_cards.value %} -
- - -
- -{% if form.errors %} -
- Please correct the errors below: -
    - {% for field in form %} - {% for error in field.errors %} -
  • {{ error }}
  • - {% endfor %} - {% endfor %} - {% for error in form.non_field_errors %} -
  • {{ error }}
  • - {% endfor %} -
-
-{% endif %} -{% endblock content %} \ No newline at end of file diff --git a/templates/trades/trade_offer_delete.html b/templates/trades/trade_offer_delete.html deleted file mode 100644 index 5386358..0000000 --- a/templates/trades/trade_offer_delete.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends '_base.html' %} -{% load static %} - -{% block title %}Delete Trade Offer{% endblock title %} - -{% block content %} -

Delete Trade Offer

-

Are you sure you want to delete this trade offer?

-
- {% csrf_token %} - - Cancel -
-{% endblock content %} diff --git a/templates/trades/trade_offer_list.html b/templates/trades/trade_offer_list.html deleted file mode 100644 index 80e49be..0000000 --- a/templates/trades/trade_offer_list.html +++ /dev/null @@ -1,62 +0,0 @@ -{% extends '_base.html' %} -{% load static %} -{% load el_pagination_tags %} - -{% block title %}Trade Offer List{% endblock title %} - -{% block content %} -
-
-
- - -
- -
-
- -

Trade Offers

- - - - - - - - - - {% paginate 10 object_list as paginated_offers %} - {% for offer in paginated_offers %} - - - - - - {% empty %} - - - - {% endfor %} - -
OfferStateUpdated At
- -
- FT: {% for card in offer.cards_ft.all %} - {{ card.name }}{% if not forloop.last %}, {% endif %} - {% endfor %} -
-
-
- LF: {% for card in offer.cards_lf.all %} - {{ card.name }}{% if not forloop.last %}, {% endif %} - {% endfor %} -
-
-
{{ offer.get_state_display }}{{ offer.updated_at }}
No trade offers available.
- -Create New Offer -{% endblock content %} diff --git a/theme/static_src/package-lock.json b/theme/static_src/package-lock.json index 2a1fa2c..ef1591d 100644 --- a/theme/static_src/package-lock.json +++ b/theme/static_src/package-lock.json @@ -14,6 +14,7 @@ "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.16", "cross-env": "^7.0.3", + "daisyui": "^5.0.0-beta.9", "postcss": "^8.5.1", "postcss-import": "^16.1.0", "postcss-nested": "^7.0.2", @@ -711,6 +712,15 @@ "node": ">=4" } }, + "node_modules/daisyui": { + "version": "5.0.0-beta.9", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.0-beta.9.tgz", + "integrity": "sha512-V+To8o1O8AaxSgdk9QrjXyq/e1AhdW1Z6oUI5iwrOjPs8avM7VQNqoTDCAE5rM0NcMbUfmFgQH8h8guiQ5QPOA==", + "dev": true, + "funding": { + "url": "https://github.com/saadeghi/daisyui?sponsor=1" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", diff --git a/theme/static_src/package.json b/theme/static_src/package.json index 07d6a44..0c509d8 100644 --- a/theme/static_src/package.json +++ b/theme/static_src/package.json @@ -15,15 +15,16 @@ "license": "MIT", "devDependencies": { "@tailwindcss/aspect-ratio": "^0.4.2", + "@tailwindcss/cli": "^4.0.0", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.16", "cross-env": "^7.0.3", + "daisyui": "^5.0.0-beta.9", "postcss": "^8.5.1", "postcss-import": "^16.1.0", "postcss-nested": "^7.0.2", "postcss-simple-vars": "^7.0.1", "rimraf": "^6.0.1", - "tailwindcss": "^4.0.0", - "@tailwindcss/cli": "^4.0.0" + "tailwindcss": "^4.0.0" } } diff --git a/theme/static_src/src/styles.css b/theme/static_src/src/styles.css index f8e9e36..eb690bb 100644 --- a/theme/static_src/src/styles.css +++ b/theme/static_src/src/styles.css @@ -14,8 +14,7 @@ * @source "../../../templates"; */ -@import "tailwindcss" source("../../../"); - +@import "tailwindcss" source("../../"); /* * If you would like to customise you theme, you can do that here too. @@ -38,38 +37,78 @@ @plugin "@tailwindcss/typography"; @plugin "@tailwindcss/aspect-ratio"; +@tailwind base; +@tailwind components; +@tailwind utilities; -@plugin "daisyui/theme" { - name: "acid"; - default: false; - prefersdark: true; - color-scheme: "dark"; - --color-base-100: oklch(98% 0 0); - --color-base-200: oklch(92% 0 0); - --color-base-300: oklch(87% 0 0); - --color-base-content: oklch(0% 0 0); +@custom-variant dark (&:where(.dark, .dark *)); + +@plugin "daisyui"; +/* @plugin "daisyui/theme" { + name: "light"; + default: true; + prefersdark: false; + color-scheme: light; + --color-base-100: oklch(100% 0 0); + --color-base-200: oklch(98% 0 0); + --color-base-300: oklch(95% 0 0); + --color-base-content: oklch(21% 0.006 285.885); --color-primary: #CF36E0; - --color-primary-content: oklch(98% 0.003 247.858); + --color-primary-content: oklch(100% 0 0); --color-secondary: #8040E0; - --color-secondary-content: oklch(98% 0.003 247.858); - --color-accent: #1070EB; - --color-accent-content: oklch(18.556% 0.052 122.962); - --color-neutral: oklch(43% 0 0); - --color-neutral-content: oklch(98% 0.003 247.858); - --color-info: #302FD9; - --color-info-content: oklch(98% 0.003 247.858); + --color-secondary-content: oklch(100% 0 0); + --color-accent: #302FD9; + --color-accent-content: oklch(100% 0 0); + --color-neutral: oklch(37% 0 0); + --color-neutral-content: oklch(100% 0 0); + --color-info: #1070EB; + --color-info-content: oklch(100% 0 0); --color-success: #20AA80; - --color-success-content: oklch(12% 0.042 264.695); - --color-warning: #EB8600; - --color-warning-content: oklch(18.202% 0.042 100.5); + --color-success-content: oklch(100% 0 0); + --color-warning: #EA8200; + --color-warning-content: oklch(100% 0 0); --color-error: #E00202; - --color-error-content: oklch(98% 0.003 247.858); - --radius-selector: 0rem; + --color-error-content: oklch(100% 0 0); + --radius-selector: 0.5rem; --radius-field: 0rem; --radius-box: 0rem; --size-selector: 0.3125rem; --size-field: 0.3125rem; - --border: 1.5px; + --border: 1px; --depth: 1; --noise: 0; } +@plugin "daisyui/theme" { + name: "dark"; + default: false; + prefersdark: false; + color-scheme: dark; + --color-base-100: oklch(25.33% 0.016 252.42); + --color-base-200: oklch(23.26% 0.014 253.1); + --color-base-300: oklch(21.15% 0.012 254.09); + --color-base-content: oklch(97.807% 0.029 256.847); + --color-primary: #CF36E0; + --color-primary-content: oklch(100% 0 0); + --color-secondary: #8040E0; + --color-secondary-content: oklch(100% 0 0); + --color-accent: #302FD9; + --color-accent-content: oklch(100% 0 0); + --color-neutral: oklch(37% 0 0); + --color-neutral-content: oklch(100% 0 0); + --color-info: #1070EB; + --color-info-content: oklch(100% 0 0); + --color-success: #20AA80; + --color-success-content: oklch(100% 0 0); + --color-warning: #EA8200; + --color-warning-content: oklch(100% 0 0); + --color-error: #E00202; + --color-error-content: oklch(100% 0 0); + --radius-selector: 0.5rem; + --radius-field: 0rem; + --radius-box: 0rem; + --size-selector: 0.3125rem; + --size-field: 0.3125rem; + --border: 1px; + --depth: 1; + --noise: 0; +} */ \ No newline at end of file diff --git a/theme/static_src/tailwind.config.js b/theme/static_src/tailwind.config.js index 9485bc6..6e97ede 100644 --- a/theme/static_src/tailwind.config.js +++ b/theme/static_src/tailwind.config.js @@ -54,4 +54,5 @@ module.exports = { require('@tailwindcss/typography'), require('@tailwindcss/aspect-ratio'), ], + darkMode: 'class', } diff --git a/theme/templates/403_csrf.html b/theme/templates/403_csrf.html new file mode 100644 index 0000000..a2743b1 --- /dev/null +++ b/theme/templates/403_csrf.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} +{% block title %}Forbidden (403){% endblock title %} + +{% block content %} +
+

Forbidden (403)

+

CSRF verification failed. Request aborted.

+ Return Home +
+{% endblock content %} \ No newline at end of file diff --git a/theme/templates/404.html b/theme/templates/404.html new file mode 100644 index 0000000..4f7876c --- /dev/null +++ b/theme/templates/404.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} +{% block title %}404 Page not found{% endblock title %} + +{% block content %} +
+

404

+

Page not found

+ Return Home +
+{% endblock content %} \ No newline at end of file diff --git a/theme/templates/500.html b/theme/templates/500.html new file mode 100644 index 0000000..eb003e8 --- /dev/null +++ b/theme/templates/500.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} +{% block title %}500 Server Error{% endblock title %} + +{% block content %} +
+

500

+

Looks like something went wrong!

+ Return Home +
+{% endblock content %} \ No newline at end of file diff --git a/templates/account/email/password_reset_key_message.txt b/theme/templates/account/email/password_reset_key_message.txt similarity index 100% rename from templates/account/email/password_reset_key_message.txt rename to theme/templates/account/email/password_reset_key_message.txt diff --git a/templates/account/email/password_reset_key_subject.txt b/theme/templates/account/email/password_reset_key_subject.txt similarity index 100% rename from templates/account/email/password_reset_key_subject.txt rename to theme/templates/account/email/password_reset_key_subject.txt diff --git a/theme/templates/account/login.html b/theme/templates/account/login.html new file mode 100644 index 0000000..b6eb2aa --- /dev/null +++ b/theme/templates/account/login.html @@ -0,0 +1,34 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags i18n widget_tweaks %} + +{% block head_title %}{% trans "Log In" %}{% endblock %} + +{% block content %} +
+

{% trans "Log In" %}

+
+ {% csrf_token %} + {{ form.non_field_errors }} +
+ + {{ form.login|add_class:"input input-bordered w-full" }} + {{ form.login.errors }} +
+
+ + {{ form.password|add_class:"input input-bordered w-full" }} + {{ form.password.errors }} +
+ {% if form.remember %} +
+ {{ form.remember }} + +
+ {% endif %} + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/theme/templates/account/logout.html b/theme/templates/account/logout.html new file mode 100644 index 0000000..9d9be74 --- /dev/null +++ b/theme/templates/account/logout.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags i18n %} + +{% block head_title %}{% trans "Log Out" %}{% endblock %} + +{% block content %} +
+

{% trans "Sign Out" %}

+

{% trans "Are you sure you want to sign out?" %}

+
+ {% csrf_token %} + {{ form|crispy }} + +
+
+{% endblock %} \ No newline at end of file diff --git a/theme/templates/account/password_change.html b/theme/templates/account/password_change.html new file mode 100644 index 0000000..af8389a --- /dev/null +++ b/theme/templates/account/password_change.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags i18n %} + +{% block head_title %}{% trans "Change Password" %}{% endblock %} + +{% block content %} +
+

{% trans "Change Password" %}

+
+ {% csrf_token %} + {{ form|crispy }} + +
+
+{% endblock %} \ No newline at end of file diff --git a/theme/templates/account/password_reset.html b/theme/templates/account/password_reset.html new file mode 100644 index 0000000..501086d --- /dev/null +++ b/theme/templates/account/password_reset.html @@ -0,0 +1,21 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags i18n widget_tweaks %} + +{% block head_title %}{% trans "Reset Password" %}{% endblock %} + +{% block content %} +
+

{% trans "Reset Password" %}

+

{% trans "Enter your email address and we'll send you a link to reset your password." %}

+
+ {% csrf_token %} + {{ form.non_field_errors }} +
+ + {{ form.email|add_class:"input input-bordered w-full" }} + {{ form.email.errors }} +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/theme/templates/account/password_reset_done.html b/theme/templates/account/password_reset_done.html new file mode 100644 index 0000000..eaa25b2 --- /dev/null +++ b/theme/templates/account/password_reset_done.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags i18n %} + +{% block head_title %}{% trans "Password Reset Done" %}{% endblock %} + +{% block content %} +
+

{% trans "Password Reset" %}

+

{% trans "We have sent you an e-mail. Please contact us if you do not receive it in a few minutes." %}

+
+{% endblock %} \ No newline at end of file diff --git a/theme/templates/account/password_reset_from_key.html b/theme/templates/account/password_reset_from_key.html new file mode 100644 index 0000000..c380e63 --- /dev/null +++ b/theme/templates/account/password_reset_from_key.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags i18n %} + +{% block head_title %}{% trans "Change Password" %}{% endblock %} + +{% block content %} +
+ {% if token_fail %} +

{% trans "Bad Token" %}

+

+ {% trans "The password reset link was invalid. Perhaps it has already been used? Please request a" %} + {% trans "new password reset" %}. +

+ {% else %} + {% if form %} +

{% trans "Change Password" %}

+
+ {% csrf_token %} + {{ form|crispy }} + +
+ {% else %} +

{% trans "Password Changed" %}

+

{% trans "Your password is now changed." %}

+ {% endif %} + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/theme/templates/account/password_reset_from_key_done.html b/theme/templates/account/password_reset_from_key_done.html new file mode 100644 index 0000000..c615e40 --- /dev/null +++ b/theme/templates/account/password_reset_from_key_done.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags i18n %} + +{% block head_title %}{% trans "Password Change Done" %}{% endblock %} + +{% block content %} +
+

{% trans "Password Change Done" %}

+

{% trans "Your password has been changed." %}

+
+{% endblock %} \ No newline at end of file diff --git a/theme/templates/account/signup.html b/theme/templates/account/signup.html new file mode 100644 index 0000000..c52e941 --- /dev/null +++ b/theme/templates/account/signup.html @@ -0,0 +1,43 @@ +{% extends 'base.html' %} +{% load i18n widget_tweaks %} + +{% block head_title %}{% trans "Sign Up" %}{% endblock %} + +{% block content %} +
+

{% trans "Sign Up" %}

+
+ {% csrf_token %} + {{ form.non_field_errors }} +
+ + {{ form.username|add_class:"input input-bordered w-full" }} + {{ form.username.errors }} +
+
+ + {{ form.email|add_class:"input input-bordered w-full" }} + {{ form.email.errors }} +
+
+ + {{ form.password1|add_class:"input input-bordered w-full" }} + {{ form.password1.errors }} +
+
+ + {{ form.password2|add_class:"input input-bordered w-full" }} + {{ form.password2.errors }} +
+
+ + {{ form.friend_code|add_class:"input input-bordered w-full" }} + {{ form.friend_code.errors }} +
+ +
+
+

{% trans "Already have an account?" %} {% trans "Log In" %}

+
+
+{% endblock %} \ No newline at end of file diff --git a/theme/templates/base.html b/theme/templates/base.html index d2bf435..562cf2d 100644 --- a/theme/templates/base.html +++ b/theme/templates/base.html @@ -1 +1,173 @@ -{% load static tailwind_tags %} +{% load static tailwind_tags gravatar %} + + + + + + + + + + {% block title %}Pkmn Trade Club{% endblock title %} + + + + + + {% tailwind_css %} + + {% block css %}{% endblock %} + + {% block javascript_head %}{% endblock %} + + + + + + + +
+ {% block content %}{% endblock %} +
+ + +
+
+

© {% now "Y" %} PKMNTrade.Club. All rights reserved.

+
+
+ + +
+ + + + + + + +
+ + + + + + + + + {% block javascript %}{% endblock %} + + \ No newline at end of file diff --git a/theme/templates/friend_codes/add_friend_code.html b/theme/templates/friend_codes/add_friend_code.html new file mode 100644 index 0000000..d793480 --- /dev/null +++ b/theme/templates/friend_codes/add_friend_code.html @@ -0,0 +1,33 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags %} + +{% block title %}Add Friend Code{% endblock %} + +{% block content %} +
+

Add Friend Code

+
+ {% csrf_token %} + {{ form|crispy }} + +
+ + +
+ + + + +{% endblock %} \ No newline at end of file diff --git a/theme/templates/friend_codes/confirm_delete_friend_code.html b/theme/templates/friend_codes/confirm_delete_friend_code.html new file mode 100644 index 0000000..98574af --- /dev/null +++ b/theme/templates/friend_codes/confirm_delete_friend_code.html @@ -0,0 +1,33 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags %} + +{% block title %}Delete Friend Code{% endblock %} + +{% block content %} +
+

Delete Friend Code

+

+ Are you sure you want to delete friend code: + {{ friend_code.friend_code }}? +

+ + {% if error_message %} +
+ {{ error_message }} +
+ {% endif %} + +
+ {% csrf_token %} + + Cancel +
+
+{% endblock %} \ No newline at end of file diff --git a/theme/templates/friend_codes/list_friend_codes.html b/theme/templates/friend_codes/list_friend_codes.html new file mode 100644 index 0000000..d7736f4 --- /dev/null +++ b/theme/templates/friend_codes/list_friend_codes.html @@ -0,0 +1,50 @@ +{% extends 'base.html' %} + +{% block title %}My Friend Codes{% endblock %} + +{% block content %} +
+ {# Display messages if there are any. #} + {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + +

My Friend Codes

+ + {% if friend_codes %} +
    + {% for code in friend_codes %} +
  • +
    + {{ code.friend_code }} + {% if user.default_friend_code and code.id == user.default_friend_code.id %} + Default + {% endif %} +
    +
    + {% if user.default_friend_code and code.id == user.default_friend_code.id %} + + {% else %} +
    + {% csrf_token %} + +
    + {% endif %} + Delete +
    +
  • + {% endfor %} +
+ {% else %} +

You do not have any friend codes added yet.

+ {% endif %} + + +
+{% endblock %} \ No newline at end of file diff --git a/theme/templates/home/_card_list.html b/theme/templates/home/_card_list.html new file mode 100644 index 0000000..b2f84ac --- /dev/null +++ b/theme/templates/home/_card_list.html @@ -0,0 +1,26 @@ +{% load card_badge %} +{% comment %} + This partial expects: + - cards: a list of card objects + - mode: a string that determines the render style. + It should be "offered" for Most Offered Cards and "wanted" for Most Wanted Cards. + - Optional 'show_zero' flag (default False): if True, also display cards with 0 offers. +{% endcomment %} +{% if cards %} +
+ {% for card in cards %} + {% if show_zero|default:False or card.offer_count > 0 %} + {% if mode == "offered" %} + + {% card_badge card card.offer_count %} + + {% endif %} + {% endfor %} +
+{% else %} +

No cards found

+{% endif %} \ No newline at end of file diff --git a/theme/templates/home/_search_results.html b/theme/templates/home/_search_results.html new file mode 100644 index 0000000..5cf762f --- /dev/null +++ b/theme/templates/home/_search_results.html @@ -0,0 +1,10 @@ +{% load trade_offer_tags %} +{% if offered_cards or wanted_cards %} +
+

Results

+ {% if search_results and search_results.object_list %} + {% include "trades/_trade_offer_list.html" with offers=search_results %} + {% else %} +
No trade offers found.
+ {% endif %} +{% endif %} \ No newline at end of file diff --git a/theme/templates/home/home.html b/theme/templates/home/home.html new file mode 100644 index 0000000..1acd465 --- /dev/null +++ b/theme/templates/home/home.html @@ -0,0 +1,331 @@ +{% extends 'base.html' %} +{% load static trade_offer_tags card_badge cache card_multiselect %} + +{% block content %} +

+ + Welcome to Pokemon Trade Club +

+ + + + + +
+ {% include "home/_search_results.html" %} +
+ + +
+

Market Stats

+
+ +
+
+
+
Most Offered Cards
+
+
+ {% cache 3600 most_offered_cards %} + {% include "home/_card_list.html" with cards=most_offered_cards mode="wanted" %} + {% endcache %} +
+
+
+ +
+
+
+
Most Wanted Cards
+
+
+ {% cache 3600 most_wanted_cards %} + {% include "home/_card_list.html" with cards=most_wanted_cards mode="offered" %} + {% endcache %} +
+
+
+ +
+
+
+
Least Offered Cards
+
+
+ {% cache 3600 least_offered_cards %} + {% include "home/_card_list.html" with cards=least_offered_cards mode="wanted" show_zero=True %} + {% endcache %} +
+
+
+
+
+ + +
+
+ +
+ {% cache 86400 featured_offers %} +
+
+
Featured Offers
+
+
+ + +
+
+ {% endcache %} +
+ + +
+ {% cache 60 recent_offers %} +
+
+
Recent Offers
+
+
+
+ {% for offer in recent_offers %} + {% render_trade_offer offer %} + {% empty %} +

No recent offers available.

+ {% endfor %} +
+
+
+ {% endcache %} +
+
+
+{% endblock content %} + +{% block css %} + +{% endblock %} + +{% block javascript %} + +{% endblock %} \ No newline at end of file diff --git a/theme/templates/trades/_friend_code_select.html b/theme/templates/trades/_friend_code_select.html new file mode 100644 index 0000000..c7f3953 --- /dev/null +++ b/theme/templates/trades/_friend_code_select.html @@ -0,0 +1,27 @@ +{% comment %} +This fragment renders a friend code selector used for filtering or form submissions. +Expected variables: + - friend_codes: A list or QuerySet of FriendCode objects. + - selected_friend_code: The currently selected FriendCode. + - field_name (optional): The name/id for the input element (default "friend_code"). + - label (optional): The label text (default "Friend Code"). +{% endcomment %} + +{% with field_name=field_name|default:"friend_code" label=label|default:"Friend Code" %} + {% if friend_codes|length > 1 %} +
+ + +
+ {% else %} + + {% endif %} +{% endwith %} \ No newline at end of file diff --git a/theme/templates/trades/_trade_offer_list.html b/theme/templates/trades/_trade_offer_list.html new file mode 100644 index 0000000..453c34a --- /dev/null +++ b/theme/templates/trades/_trade_offer_list.html @@ -0,0 +1,58 @@ +{% load trade_offer_tags %} +{% comment %} + This snippet renders a grid of trade offer cards along with pagination controls, + using the trade_offer templatetag (i.e. {% render_trade_offer offer %}). + + It expects a context variable: + - offers: an iterable or a paginated page of TradeOffer objects. +{% endcomment %} + +
+ {% for offer in offers %} + + {% empty %} +
No trade offers available.
+ {% endfor %} +
+ +{% if offers.has_other_pages %} + +{% endif %} \ No newline at end of file diff --git a/theme/templates/trades/trade_acceptance_create.html b/theme/templates/trades/trade_acceptance_create.html new file mode 100644 index 0000000..5b581b8 --- /dev/null +++ b/theme/templates/trades/trade_acceptance_create.html @@ -0,0 +1,32 @@ +{% extends 'base.html' %} + +{% block title %}Accept Trade Offer{% endblock title %} + +{% block content %} +
+

Accept Trade Offer

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ {% if form.errors %} +
+ Please correct the errors below: +
    + {% for field in form %} + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} + {% endfor %} + {% for error in form.non_field_errors %} +
  • {{ error }}
  • + {% endfor %} +
+
+ {% endif %} + +
+{% endblock content %} \ No newline at end of file diff --git a/theme/templates/trades/trade_acceptance_update.html b/theme/templates/trades/trade_acceptance_update.html new file mode 100644 index 0000000..cb870a1 --- /dev/null +++ b/theme/templates/trades/trade_acceptance_update.html @@ -0,0 +1,32 @@ +{% extends 'base.html' %} + +{% block title %}Update Trade Acceptance{% endblock title %} + +{% block content %} +
+

Update Trade Acceptance

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ {% if form.errors %} +
+ Please correct the errors below: +
    + {% for field in form %} + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} + {% endfor %} + {% for error in form.non_field_errors %} +
  • {{ error }}
  • + {% endfor %} +
+
+ {% endif %} + +
+{% endblock content %} \ No newline at end of file diff --git a/theme/templates/trades/trade_offer_create.html b/theme/templates/trades/trade_offer_create.html new file mode 100644 index 0000000..050781a --- /dev/null +++ b/theme/templates/trades/trade_offer_create.html @@ -0,0 +1,77 @@ +{% extends 'base.html' %} +{% load card_multiselect %} + +{% block title %}Create Trade Offer{% endblock title %} + +{% block content %} +
+

Create a Trade Offer

+
+ {% csrf_token %} + + {# Use the DRY friend code selector fragment #} + {% include "trades/_friend_code_select.html" with friend_codes=friend_codes selected_friend_code=selected_friend_code field_name=form.initiated_by.html_name label="Initiated by" %} + + +
+
+ {% card_multiselect "have_cards" "Have:" "Select one or more cards..." available_cards form.initial.have_cards %} +
+
+ {% card_multiselect "want_cards" "Want:" "Select one or more cards..." available_cards form.initial.want_cards %} +
+
+ + +
+ {% if form.errors %} +
+ Please correct the errors below: +
    + {% for field in form %} + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} + {% endfor %} + {% for error in form.non_field_errors %} +
  • {{ error }}
  • + {% endfor %} +
+
+ {% endif %} +
+ +{% endblock content %} \ No newline at end of file diff --git a/theme/templates/trades/trade_offer_delete.html b/theme/templates/trades/trade_offer_delete.html new file mode 100644 index 0000000..aecec59 --- /dev/null +++ b/theme/templates/trades/trade_offer_delete.html @@ -0,0 +1,47 @@ +{% extends 'base.html' %} + +{% block title %}Delete or Close Trade Offer{% endblock title %} + +{% block content %} +
+

+ {% if action == 'delete' %} + Delete Trade Offer + {% elif action == 'close' %} + Close Trade Offer + {% else %} + Delete/Close Trade Offer + {% endif %} +

+ + {% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + +

+ {% if action == 'delete' %} + Are you sure you want to delete this trade offer? This will permanently remove the offer. + {% elif action == 'close' %} + Are you sure you want to close this trade offer? It will remain in the system as closed. + {% else %} + This trade offer cannot be deleted or closed because there are active acceptances. + {% endif %} +

+ +
+ {% csrf_token %} + {% if action %} + {% if action == 'delete' %} + + {% elif action == 'close' %} + + {% endif %} + {% else %} + + {% endif %} + Cancel +
+
+{% endblock content %} \ No newline at end of file diff --git a/theme/templates/trades/trade_offer_detail.html b/theme/templates/trades/trade_offer_detail.html new file mode 100644 index 0000000..99217b3 --- /dev/null +++ b/theme/templates/trades/trade_offer_detail.html @@ -0,0 +1,64 @@ +{% extends 'base.html' %} + +{% block title %}Trade Offer Detail{% endblock title %} + +{% block content %} +
+

Trade Offer Details

+
+

+ Hash: {{ object.hash }}
+ Initiated By: {{ object.initiated_by }}
+ Cards You Have (Offer): + {% for through in object.trade_offer_have_cards.all %} + {{ through.card.name }} x{{ through.quantity }}{% if not forloop.last %}, {% endif %} + {% endfor %}
+ Cards You Want: + {% for through in object.trade_offer_want_cards.all %} + {{ through.card.name }} x{{ through.quantity }}{% if not forloop.last %}, {% endif %} + {% endfor %}
+ Created At: {{ object.created_at|date:"M d, Y H:i" }}
+ Updated At: {{ object.updated_at|date:"M d, Y H:i" }}
+ Status: {% if object.is_closed %}Closed{% else %}Open{% endif %} +

+
+ +

Acceptances

+ {% if acceptances %} +
    + {% for acceptance in acceptances %} +
  • +

    + Accepted By: {{ acceptance.accepted_by }}
    + Requested Card: {{ acceptance.requested_card.name }}
    + Offered Card: {{ acceptance.offered_card.name }}
    + State: {{ acceptance.get_state_display }} +

    + Update +
  • + {% endfor %} +
+ {% else %} +

No acceptances yet.

+ {% endif %} + + {% if acceptance_form %} +

Accept This Offer

+
+
+ {% csrf_token %} + {{ acceptance_form.as_p }} + +
+
+ {% endif %} + +
+ + {% if is_initiator %} + Delete/Close Trade Offer + {% endif %} + Back to Trade Offers +
+
+{% endblock content %} \ No newline at end of file diff --git a/theme/templates/trades/trade_offer_list.html b/theme/templates/trades/trade_offer_list.html new file mode 100644 index 0000000..cdca7bb --- /dev/null +++ b/theme/templates/trades/trade_offer_list.html @@ -0,0 +1,94 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}Trade Offer & Acceptance List{% endblock title %} + +{% block content %} +
+ +
+
+ {% include "trades/_friend_code_select.html" with friend_codes=friend_codes selected_friend_code=selected_friend_code field_name="friend_code" label="Filter by Friend Code" %} + + + +
+
+ + +
+

My Trade Offers

+ {% if my_trade_offers_paginated.object_list %} + {% include "trades/_trade_offer_list.html" with offers=my_trade_offers_paginated %} +
+ {% if my_trade_offers_paginated.has_previous %} + Previous + {% else %} + + {% endif %} + Page {{ my_trade_offers_paginated.number }} of {{ my_trade_offers_paginated.paginator.num_pages }} + {% if my_trade_offers_paginated.has_next %} + Next + {% else %} + + {% endif %} +
+ {% else %} +

No trade offers found.

+ {% endif %} +
+ + +
+

Trade Acceptances Waiting For Your Response

+ {% if trade_acceptances_waiting_paginated.object_list %} + {% include "trades/_trade_offer_list.html" with offers=trade_acceptances_waiting_paginated %} +
+ {% if trade_acceptances_waiting_paginated.has_previous %} + Previous + {% else %} + + {% endif %} + Page {{ trade_acceptances_waiting_paginated.number }} of {{ trade_acceptances_waiting_paginated.paginator.num_pages }} + {% if trade_acceptances_waiting_paginated.has_next %} + Next + {% else %} + + {% endif %} +
+ {% else %} +

No pending acceptances at this time.

+ {% endif %} +
+ + +
+

Other Trade Acceptances

+ {% if other_trade_acceptances_paginated.object_list %} + {% include "trades/_trade_offer_list.html" with offers=other_trade_acceptances_paginated %} +
+ {% if other_trade_acceptances_paginated.has_previous %} + Previous + {% else %} + + {% endif %} + Page {{ other_trade_acceptances_paginated.number }} of {{ other_trade_acceptances_paginated.paginator.num_pages }} + {% if other_trade_acceptances_paginated.has_next %} + Next + {% else %} + + {% endif %} +
+ {% else %} +

No other acceptances found.

+ {% endif %} +
+ + +
+{% endblock content %} \ No newline at end of file diff --git a/templates/trades/trade_offer_update.html b/theme/templates/trades/trade_offer_update.html similarity index 74% rename from templates/trades/trade_offer_update.html rename to theme/templates/trades/trade_offer_update.html index 673ec29..ee7a43a 100644 --- a/templates/trades/trade_offer_update.html +++ b/theme/templates/trades/trade_offer_update.html @@ -1,26 +1,16 @@ -{% extends '_base.html' %} -{% load static %} +{% extends 'base.html' %} {% block title %}Trade Offer Details & Update{% endblock title %} {% block content %} -
-

Trade Offer Details

- +
+

Trade Offer Details

-
-
- Offer Information -
+
-

+

Created At: {{ object.created_at|date:"M d, Y H:i" }}
Updated At: {{ object.updated_at|date:"M d, Y H:i" }}
- - {% comment %} - Only display these fields if the current user is associated with the initiating friend code - or (if available) with the accepted friend code. - {% endcomment %} {% if object.initiated_by.user == request.user or object.accepted_by and object.accepted_by.user == request.user %} Initiated By: {{ object.initiated_by }}
Accepted By: @@ -30,7 +20,6 @@ Not yet accepted {% endif %}
{% endif %} - Cards You Have: {% for card in object.have_cards.all %} {{ card.name }}{% if not forloop.last %}, {% endif %} @@ -46,16 +35,16 @@ {% if form.fields %} -

-
- {% if action == "accept" %} - Accept Trade Offer - {% else %} - Update Trade Offer - {% endif %} -
+
-
+
+ {% if action == "accept" %} + Accept Trade Offer + {% else %} + Update Trade Offer + {% endif %} +
+ {% csrf_token %} {{ form.as_p }}