progress on conversion to tailwind
This commit is contained in:
parent
6a872124c6
commit
6e2843c60e
110 changed files with 4997 additions and 1691 deletions
58
.bash_history
Normal file
58
.bash_history
Normal file
|
|
@ -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
|
||||
72
.cursorrules
Normal file
72
.cursorrules
Normal file
|
|
@ -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.
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
3.13
|
||||
22
.python_history
Normal file
22
.python_history
Normal file
|
|
@ -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()
|
||||
29
.vscode/tasks.json
vendored
Normal file
29
.vscode/tasks.json
vendored
Normal file
|
|
@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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',)
|
||||
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
|
||||
19
accounts/management/commands/seed_default_friend_codes.py
Normal file
19
accounts/management/commands/seed_default_friend_codes.py
Normal file
|
|
@ -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)."))
|
||||
61
accounts/migrations/0001_initial.py
Normal file
61
accounts/migrations/0001_initial.py
Normal file
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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
|
||||
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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
10
accounts/urls.py
Normal file
10
accounts/urls.py
Normal file
|
|
@ -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/<int:pk>/", DeleteFriendCodeView.as_view(), name="delete_friend_code"),
|
||||
path("friend-codes/default/<int:pk>/", ChangeDefaultFriendCodeView.as_view(), name="change_default_friend_code"),
|
||||
]
|
||||
|
|
@ -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")
|
||||
92
cards/migrations/0001_initial.py
Normal file
92
cards/migrations/0001_initial.py
Normal file
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
'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)
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
23
entrypoint.sh
Executable file
23
entrypoint.sh
Executable file
|
|
@ -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!"
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from .models import FriendCode
|
||||
|
||||
# Register your models here.
|
||||
admin.site.register(FriendCode)
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FriendCodesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'friend_codes'
|
||||
|
|
@ -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
|
||||
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -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/<int:pk>/', DeleteFriendCodeView.as_view(), name='delete_friend_code'),
|
||||
]
|
||||
|
|
@ -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)
|
||||
106
home/views.py
106
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
|
||||
|
||||
|
|
|
|||
36
manage.py
36
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
|
||||
|
|
|
|||
21
package-lock.json
generated
Normal file
21
package-lock.json
generated
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
package.json
Normal file
5
package.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"daisyui": "^5.0.0-beta.9"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
28
reset-db_make-migrations_seed-data.sh
Executable file
28
reset-db_make-migrations_seed-data.sh
Executable file
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
632
seed/0008_TestOfferWantCard.json
Normal file
632
seed/0008_TestOfferWantCard.json
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
632
seed/0009_TestOfferHaveCard.json
Normal file
632
seed/0009_TestOfferHaveCard.json
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
|
||||
{% block title %}Forbidden (403){% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Forbidden (403)</h1>
|
||||
<p>CSRF verification failed. Request aborted.</p>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
|
||||
{% block title %}404 Page not found{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Page not found</h1>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
|
||||
{% block title %}500 Server Error{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>500 Server Error</h1>
|
||||
<p>Looks like something went wrong!</p>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
{% load static card_badge %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="light">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
|
||||
<title>{% block title %}Pocket.Trade{% endblock title %}</title>
|
||||
<meta name="description" content="A framework for launching new Django projects quickly.">
|
||||
<meta name="author" content="">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="{% static 'images/favicon.ico' %}">
|
||||
|
||||
<!-- DaisyUI (disabled for now)-->
|
||||
<!-- <link href="https://cdn.jsdelivr.net/npm/daisyui@5.0.0-beta.8/daisyui.min.css" rel="stylesheet"> -->
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="{% static 'css/base.css' %}">
|
||||
{% block css %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<!-- Bootstrap JavaScript -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Tailwind CSS (disabled for now)-->
|
||||
<!-- <script src="https://unpkg.com/@tailwindcss/browser@4"></script> -->
|
||||
<!-- DaisyUI (disabled for now)-->
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/daisyui@5.0.0-beta.8/index.min.js"></!-->
|
||||
<!-- Select2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<!-- Gravatar -->
|
||||
<script src="https://www.gravatar.com/js/hovercards/hovercards.min.js"></script>
|
||||
<!-- Project JS -->
|
||||
<script src="{% static 'js/base.js' %}"></script>
|
||||
<script>
|
||||
function formatOption(option) {
|
||||
if (!option.id) return option.text;
|
||||
var $option = $(option.element);
|
||||
var cardName = $option.data('name');
|
||||
var rarity = $option.data('rarity');
|
||||
var cardset = $option.data('cardset');
|
||||
var style = $option.data('style');
|
||||
var $container = $(
|
||||
{% card_badge None %}
|
||||
);
|
||||
return $container;
|
||||
}
|
||||
</script>
|
||||
{% block javascript %}
|
||||
{% endblock javascript %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg bg-light">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{% url 'home' %}">Pocket.Trade</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="{% url 'home' %}">Home</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="tradeOffersDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Trade Offers
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="tradeOffersDropdown">
|
||||
<li><a class="dropdown-item" href="{% url 'trade_offer_list' %}">All Offers</a></li>
|
||||
{% if user.is_authenticated %}<li><a class="dropdown-item" href="{% url 'trade_offer_list' %}?my_trades=true">My Trades</a></li>{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
{% if user.is_authenticated %}
|
||||
<div class="mr-auto">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item"><a href="{% url 'trade_offer_create' %}" class="btn btn-primary me-2 mb-2">
|
||||
Create Trade Offer
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mr-auto">
|
||||
<div class="navbar-nav">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
Settings
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="#">{{ user.email }}</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="{% url 'list_friend_codes' %}">My Friend Codes</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'account_change_password' %}">Change password</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'account_logout' %}">Sign out</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mr-auto">
|
||||
<form class="form d-flex">
|
||||
<a href="{% url 'account_login' %}" class="btn btn-outline-secondary">Log in</a>
|
||||
<a href="{% url 'account_signup' %}" class="btn btn-primary ms-2">Sign up</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
{% block content %}
|
||||
<p>Default content...</p>
|
||||
{% endblock content %}
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<span class="text-muted">Footer...</span>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Log in{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Log in</h2>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-success" type="submit">Log in</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Log out{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<h1>Sign Out</h1>
|
||||
|
||||
<p>Are you sure you want to sign out?</p>
|
||||
|
||||
<form method="post" action="{% url 'account_logout' %}">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-danger" type="submit">Sign Out</button>
|
||||
</form>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Change Password{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Change Password</h2>
|
||||
<form method="post" action="{% url 'account_change_password' %}">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-success" type="submit">Change Password</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Password Reset{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Forgot your password? </h2>
|
||||
<form method="POST" action="{% url 'account_reset_password' %}" class="password_reset">
|
||||
{% csrf_token %}
|
||||
{{ form | crispy }}
|
||||
<button class="btn btn-primary" type="submit">Reset Password</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Password Reset Done{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Password Reset</h1>
|
||||
<p>We have sent you an e-mail. Please contact us if you do not receive it in a few minutes.</p>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Change Password{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% if token_fail %}Bad Token{% else %}Change Password{% endif %}</h1>
|
||||
|
||||
{% if token_fail %}
|
||||
<p>The password reset link was invalid. Perhaps it has already been used? Please request a <a href="{% url 'account_reset_password' %}">new password reset</a>.</p>
|
||||
{% else %}
|
||||
{% if form %}
|
||||
<form method="POST" action=".">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-primary" type="submit">Change Password</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>Your password is now changed.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock content%}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Change Password Done{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Password Change Done</h1>
|
||||
<p>Your password has been changed.</p>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Set Password{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<form method="POST" action="" class="password_set">
|
||||
{% csrf_token %}
|
||||
{{ form | crispy }}
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit" name="action" value="Set Password">Change
|
||||
Password</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Sign up{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Sign up</h2>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-success" type="submit">Sign up</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Add Friend Code{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Add Friend Code</h1>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button type="submit">Add Friend Code</button>
|
||||
</form>
|
||||
|
||||
<p>
|
||||
<a href="{% url 'list_friend_codes' %}">Back to Friend Codes</a>
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Log in{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Are you sure you want to delete friend code: {{ friend_code.friend_code }}?</h1>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit">Confirm Delete</button>
|
||||
<a href="{% url 'list_friend_codes' %}">Cancel</a>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
|
||||
{% block title %}My Friend Codes{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>My Friend Codes</h1>
|
||||
|
||||
{% if friend_codes %}
|
||||
<ul>
|
||||
{% for code in friend_codes %}
|
||||
<li>
|
||||
{{ code.friend_code }}
|
||||
<!-- Link to the delete confirmation page for this friend code -->
|
||||
<a href="{% url 'delete_friend_code' code.id %}">Delete</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>You do not have any friend codes added yet.</p>
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
<a href="{% url 'add_friend_code' %}">Add a New Friend Code</a>
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
{% load trade_offer_tags %}
|
||||
{% if offered_cards or wanted_cards %}
|
||||
<hr class="my-5">
|
||||
<h2 class="mb-4">Results</h2>
|
||||
{% if search_results and search_results.object_list %}
|
||||
<ul class="list-group">
|
||||
{% for offer in search_results %}
|
||||
<li class="list-group-item border-0">
|
||||
<a href="{% url 'trade_offer_update' offer.pk %}" class="d-flex align-items-center text-decoration-none">
|
||||
{% render_trade_offer offer %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<!-- Pagination Controls -->
|
||||
<nav aria-label="Search results pagination" class="mt-4">
|
||||
<ul class="pagination">
|
||||
{% if search_results.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link ajax-page-link" data-page="{{ search_results.previous_page_number }}" href="#">Previous</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">Previous</span></li>
|
||||
{% endif %}
|
||||
{% for num in search_results.paginator.page_range %}
|
||||
<li class="page-item {% if search_results.number == num %}active{% endif %}">
|
||||
<a class="page-link ajax-page-link" data-page="{{ num }}" href="#">{{ num }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if search_results.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link ajax-page-link" data-page="{{ search_results.next_page_number }}" href="#">Next</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">Next</span></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% else %}
|
||||
<div class="alert alert-info">No trade offers found.</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
@ -1,239 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
{% load trade_offer_tags card_badge %}
|
||||
{% load cache %}
|
||||
{% load card_multiselect %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container my-5">
|
||||
<h1 class="text-center mb-5">Welcome to Pocket.Trade</h1>
|
||||
<!-- Search Form Section -->
|
||||
<section id="trade-search" class="mb-5">
|
||||
<form method="post" action=".">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
{% card_multiselect "offered_cards" "Have:" available_cards "Select zero or more cards..." offered_cards %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
{% card_multiselect "wanted_cards" "Want:" available_cards "Select zero or more cards..." wanted_cards %}
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Find a Trade Offer</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Search Results Section -->
|
||||
<section id="search-results">
|
||||
{% include "home/_search_results.html" %}
|
||||
</section>
|
||||
|
||||
<!-- Market Stats Section -->
|
||||
<section aria-labelledby="stats-heading" class="mb-5">
|
||||
<h2 id="stats-heading" class="mb-4">Market Stats</h2>
|
||||
<div class="row gx-5">
|
||||
<!-- Most Offered Cards (cached for 3600 seconds / 1 hour) -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<h5 class="mb-3">Most Offered Cards</h5>
|
||||
<div class="card h-100 shadow border-0">
|
||||
<div class="card-body">
|
||||
{% cache 3600 most_offered_cards %}
|
||||
{% if most_offered_cards %}
|
||||
<div class="d-flex flex-column gap-3">
|
||||
{% for card in most_offered_cards %}
|
||||
{% if card.offer_count > 0 %}
|
||||
<a href="?wanted_cards={{ card.id }}"
|
||||
class="d-flex justify-content-between align-items-center text-decoration-none text-primary">
|
||||
{% card_badge card %}
|
||||
<span>{{ card.offer_count }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No cards found</p>
|
||||
{% endif %}
|
||||
{% endcache %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Most Wanted Cards (cached for 3600 seconds / 1 hour) -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<h5 class="mb-3">Most Wanted Cards</h5>
|
||||
<div class="card h-100 shadow border-0">
|
||||
<div class="card-body">
|
||||
{% cache 3600 most_wanted_cards %}
|
||||
{% if most_wanted_cards %}
|
||||
<div class="d-flex flex-column gap-3">
|
||||
{% for card in most_wanted_cards %}
|
||||
{% if card.offer_count > 0 %}
|
||||
<a href="?offered_cards={{ card.id }}"
|
||||
class="d-flex justify-content-between align-items-center text-decoration-none text-primary">
|
||||
{% card_badge card %}
|
||||
<span>{{ card.offer_count }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No cards found</p>
|
||||
{% endif %}
|
||||
{% endcache %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Offers and Recent Offers Section -->
|
||||
<div class="row mb-5">
|
||||
<!-- Featured Offers Card (cached for 86400 seconds / 1 day) -->
|
||||
<div class="col-md-6 mb-3">
|
||||
{% cache 86400 featured_offers %}
|
||||
<div class="card h-100 border-0">
|
||||
<div class="card-header border-0 bg-transparent">
|
||||
<h5 class="card-title mb-0">Featured Offers</h5>
|
||||
<ul class="nav nav-tabs card-header-tabs mt-3" id="cardsetTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="all-tab" data-bs-toggle="tab" data-bs-target="#all"
|
||||
type="button" role="tab" aria-controls="all" aria-selected="true">All</button>
|
||||
</li>
|
||||
{% for cardset, offers in featured_offers.items %}
|
||||
{% if cardset != "All" %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="{{ cardset|slugify }}-tab" data-bs-toggle="tab" data-bs-target="#{{ cardset|slugify }}"
|
||||
type="button" role="tab" aria-controls="{{ cardset|slugify }}" aria-selected="false">
|
||||
{{ cardset }}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="tab-content" id="cardsetTabsContent">
|
||||
<!-- All Offers Tab Pane -->
|
||||
<div class="tab-pane fade show active" id="all" role="tabpanel" aria-labelledby="all-tab">
|
||||
{% if featured_offers.All %}
|
||||
<div class="d-flex flex-column gap-3">
|
||||
{% for offer in featured_offers.All %}
|
||||
<a href="{% url 'trade_offer_update' offer.pk %}" class="d-flex align-items-center text-decoration-none">
|
||||
{% render_trade_offer offer %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No featured offers available.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Other Cardset Tab Panes -->
|
||||
{% for cardset, offers in featured_offers.items %}
|
||||
{% if cardset != "All" %}
|
||||
<div class="tab-pane fade" id="{{ cardset|slugify }}" role="tabpanel" aria-labelledby="{{ cardset|slugify }}-tab">
|
||||
{% if offers %}
|
||||
<div class="d-flex flex-column gap-3">
|
||||
{% for offer in offers %}
|
||||
<a href="{% url 'trade_offer_update' offer.pk %}" class="d-flex align-items-center text-decoration-none">
|
||||
{% render_trade_offer offer %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No featured offers for {{ cardset }}.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endcache %}
|
||||
</div>
|
||||
|
||||
<!-- Recent Offers Card (cached for 60 seconds) -->
|
||||
<div class="col-md-6 mb-3">
|
||||
{% cache 60 recent_offers %}
|
||||
<div class="card h-100 border-0">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Recent Offers</h5>
|
||||
<div class="d-flex flex-column gap-3">
|
||||
{% for offer in recent_offers %}
|
||||
<a href="{% url 'trade_offer_update' offer.pk %}" class="text-decoration-none">
|
||||
{% render_trade_offer offer %}
|
||||
</a>
|
||||
{% empty %}
|
||||
<div>No offers available</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endcache %}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascript %}
|
||||
<!-- <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script> -->
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// function formatOption(option) {
|
||||
// if (!option.id) return option.text;
|
||||
// var $option = $(option.element);
|
||||
// var cardName = $option.data('name');
|
||||
// var rarity = $option.data('rarity');
|
||||
// var cardset = $option.data('cardset');
|
||||
// var style = $option.data('style');
|
||||
// return $('<span>').text(cardName + " " + rarity + " " + cardset).attr('style', style);
|
||||
// }
|
||||
|
||||
// $('.select2-field').select2({
|
||||
// placeholder: function() {
|
||||
// return $(this).data('placeholder');
|
||||
// },
|
||||
// templateResult: formatOption,
|
||||
// templateSelection: formatOption,
|
||||
// width: '100%',
|
||||
// dropdownAutoWidth: true,
|
||||
// allowClear: true
|
||||
// });
|
||||
|
||||
// AJAX form submission for trade search
|
||||
$("#trade-search form").on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
type: $(this).attr("method"),
|
||||
url: $(this).attr("action"),
|
||||
data: $(this).serialize(),
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||
success: function(data) {
|
||||
$("#search-results").html(data);
|
||||
},
|
||||
error: function() {
|
||||
alert("There was an error processing your search.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// AJAX pagination for search results
|
||||
$(document).on('click', '.ajax-page-link', function(e){
|
||||
e.preventDefault();
|
||||
var page = $(this).data('page');
|
||||
if($("#page").length) {
|
||||
$("#page").val(page);
|
||||
} else {
|
||||
$("<input>").attr({
|
||||
type: "hidden",
|
||||
id: "page",
|
||||
name: "page",
|
||||
value: page
|
||||
}).appendTo("#trade-search form");
|
||||
}
|
||||
$("#trade-search form").submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{% if decks|length == 1 %}
|
||||
{% if dropdown %}'{% endif %}<span class="badge card-badge-grid m-1" style="{% if dropdown %}'+ style +'{% else %}background-color: {{ decks.0.hex_color }}; color: white;{% endif %}">{% if dropdown %}' + {% endif %}
|
||||
{% elif decks|length == 2 %}
|
||||
{% if dropdown %}'{% endif %}<span class="badge card-badge-grid m-1" style="{% if dropdown %}'+ style +'{% else %}background: linear-gradient(to right, {{ decks.0.hex_color }}, {{ decks.1.hex_color }}); color: white;{% endif %}">{% if dropdown %}' + {% endif %}
|
||||
{% elif decks|length >= 3 %}
|
||||
{% if dropdown %}'{% endif %}<span class="badge card-badge-grid m-1" style="{% if dropdown %}'+ style +'{% else %}background: linear-gradient(to right, {{ decks.0.hex_color }}, {{ decks.1.hex_color }}, {{ decks.2.hex_color }}); color: white;{% endif %}">{% if dropdown %}' + {% endif %}
|
||||
{% else %}
|
||||
{% if dropdown %}'{% endif %}<span class="badge card-badge-grid m-1" style="{% if dropdown %}'+ style +'{% else %}background-color: #cccccc; color: white;{% endif %}">{% if dropdown %}' + {% endif %}
|
||||
{% endif %}
|
||||
{% if dropdown %}'{% endif %}<span class="card-badge-name">{% if dropdown %}'+ cardName +'{% else %}{{ card.name }}{% endif %}</span>{% if dropdown %}' + {% endif %}
|
||||
{% if dropdown %}'{% endif %}<span class="card-badge-rarity">{% if dropdown %}'+ rarity +'{% else %}{{ card.rarity.icons }}{% endif %}</span>{% if dropdown %}' + {% endif %}
|
||||
{% if dropdown %}'{% endif %}<span class="card-badge-cardset">{% if dropdown %}'+ cardset +'{% else %}{{ card.cardset.name }}{% endif %}</span>{% if dropdown %}' + {% endif %}
|
||||
{% if dropdown %}'{% endif %}</span>{% if dropdown %}'{% endif %}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
{% load cache card_badge %}
|
||||
<label for="{{ field_id }}" class="form-label">{{ label }}</label>
|
||||
<select name="{{ field_name }}" id="{{ field_id }}" class="form-select select2-field" data-placeholder="{{ placeholder }}" multiple="multiple">
|
||||
{% cache cache_timeout cache_key %}
|
||||
<option value="" disabled="disabled">{{ placeholder }}</option>
|
||||
{% for card in available_cards %}
|
||||
<option value="{{ card.pk }}"
|
||||
data-name="{{ card.name }}"
|
||||
data-rarity="{{ card.rarity.icons }}"
|
||||
data-cardset="{{ card.cardset.name }}"
|
||||
data-style="{{ card.style }}"
|
||||
{{ card.name }} {{ card.rarity.icons }} {{ card.cardset.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
{% endcache %}
|
||||
</select>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('#{{ field_id }}').select2({
|
||||
placeholder: $('#{{ field_id }}').data('placeholder'),
|
||||
templateResult: formatOption,
|
||||
templateSelection: formatOption,
|
||||
width: '100%',
|
||||
dropdownAutoWidth: true,
|
||||
allowClear: true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
{% load gravatar card_badge %}
|
||||
|
||||
<div class="card trade-offer mb-3 mx-auto shadow-lg unified-card" style="border: none;">
|
||||
<div class="card-body trade-offer-body">
|
||||
<!-- Header Row: Using Grid, with relative positioning for avatar placement -->
|
||||
<div class="row no-gutters">
|
||||
<!-- Has Side -->
|
||||
<div class="col-6 position-relative" style="padding: 1rem;">
|
||||
{% if offer.initiated_by and offer.initiated_by.user.email %}
|
||||
<!-- Positioned to the left -->
|
||||
<div class="avatar position-absolute" style="left: 1rem; top: 50%; transform: translateY(-50%);">
|
||||
{{ offer.initiated_by.user.email|gravatar:40 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Centered text remains in the normal flow -->
|
||||
<div class="text-center">
|
||||
<h6 class="card-subtitle text-muted mb-0">Has</h6>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Wants Side -->
|
||||
<div class="col-6 position-relative" style="padding: 1rem;">
|
||||
{% if offer.accepted_by and offer.accepted_by.user.email %}
|
||||
<!-- Positioned to the right -->
|
||||
<div class="avatar position-absolute" style="right: 1rem; top: 50%; transform: translateY(-50%);">
|
||||
{{ offer.accepted_by.user.email|gravatar:40 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Centered text remains in the normal flow -->
|
||||
<div class="text-center">
|
||||
<h6 class="card-subtitle text-muted mb-0">Wants</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body Row: Using Grid, no separators; badge spacing is consistent -->
|
||||
<div class="row no-gutters">
|
||||
<div class="col-6" style="padding: 1rem;">
|
||||
<div class="trade-offer-cards d-flex flex-wrap justify-content-center gap-2">
|
||||
{% if offer.have_cards.all %}
|
||||
{% for card in offer.have_cards.all %}
|
||||
{% card_badge card %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6" style="padding: 1rem;">
|
||||
<div class="trade-off-offer-cards d-flex flex-wrap justify-content-center gap-2">
|
||||
{% if offer.want_cards.all %}
|
||||
{% for card in offer.want_cards.all %}
|
||||
{% card_badge card %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trade ID Footer with Info Icon -->
|
||||
<small class="text-muted mt-auto d-block text-end pe-2">
|
||||
<i class="bi bi-info-circle-fill" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="Trade ID: {{ offer.hash }}" style="cursor: pointer;"></i>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
{% load card_multiselect %}
|
||||
|
||||
{% block title %}Create Trade Offer{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Create a Trade Offer</h2>
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
{# Render the non–Select2 field normally (e.g. initiated_by) #}
|
||||
<div class="mb-3">
|
||||
<label for="initiated_by" class="form-label">Initiated by</label>
|
||||
{{ form.initiated_by }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{% card_multiselect "have_cards" "Have:" available_cards "Select one or more cards..." form.have_cards.value %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{% card_multiselect "want_cards" "Want:" available_cards "Select one or more cards..." form.want_cards.value %}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger">
|
||||
<strong>Please correct the errors below:</strong>
|
||||
<ul>
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Delete Trade Offer{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Delete Trade Offer</h2>
|
||||
<p>Are you sure you want to delete this trade offer?</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">Confirm Delete</button>
|
||||
<a href="{% url 'trade_offer_list' %}" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
{% load el_pagination_tags %}
|
||||
|
||||
{% block title %}Trade Offer List{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<form method="get" class="d-flex align-items-center">
|
||||
<div class="form-check me-3">
|
||||
<input class="form-check-input" type="checkbox" name="show_completed" id="show_completed" value="true" {% if show_completed %}checked{% endif %}>
|
||||
<label class="form-check-label" for="show_completed">
|
||||
Only Completed
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Apply</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h2>Trade Offers</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Offer</th>
|
||||
<th>State</th>
|
||||
<th>Updated At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% paginate 10 object_list as paginated_offers %}
|
||||
{% for offer in paginated_offers %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'trade_offer_update' offer.id %}" class="d-flex align-items-center text-decoration-none">
|
||||
<div class="flex-grow-1 text-start">
|
||||
FT: {% for card in offer.cards_ft.all %}
|
||||
{{ card.name }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="px-2 text-center" style="min-width: 50px;">⟶</div>
|
||||
<div class="flex-grow-1 text-end">
|
||||
LF: {% for card in offer.cards_lf.all %}
|
||||
{{ card.name }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ offer.get_state_display }}</td>
|
||||
<td>{{ offer.updated_at }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3">No trade offers available.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination">
|
||||
{% show_pages %}
|
||||
</div>
|
||||
<a href="{% url 'trade_offer_create' %}" class="btn btn-success">Create New Offer</a>
|
||||
{% endblock content %}
|
||||
10
theme/static_src/package-lock.json
generated
10
theme/static_src/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
} */
|
||||
|
|
@ -54,4 +54,5 @@ module.exports = {
|
|||
require('@tailwindcss/typography'),
|
||||
require('@tailwindcss/aspect-ratio'),
|
||||
],
|
||||
darkMode: 'class',
|
||||
}
|
||||
|
|
|
|||
10
theme/templates/403_csrf.html
Normal file
10
theme/templates/403_csrf.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block title %}Forbidden (403){% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-md mt-12 text-center">
|
||||
<h1 class="text-5xl font-bold mb-4">Forbidden (403)</h1>
|
||||
<p class="text-xl mb-4">CSRF verification failed. Request aborted.</p>
|
||||
<a href="{% url 'home' %}" class="btn btn-primary">Return Home</a>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
10
theme/templates/404.html
Normal file
10
theme/templates/404.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block title %}404 Page not found{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-md mt-12 text-center">
|
||||
<h1 class="text-5xl font-bold mb-4">404</h1>
|
||||
<p class="text-xl mb-4">Page not found</p>
|
||||
<a href="{% url 'home' %}" class="btn btn-primary">Return Home</a>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
10
theme/templates/500.html
Normal file
10
theme/templates/500.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block title %}500 Server Error{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-md mt-12 text-center">
|
||||
<h1 class="text-5xl font-bold mb-4">500</h1>
|
||||
<p class="text-xl mb-4">Looks like something went wrong!</p>
|
||||
<a href="{% url 'home' %}" class="btn btn-primary">Return Home</a>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
34
theme/templates/account/login.html
Normal file
34
theme/templates/account/login.html
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags i18n widget_tweaks %}
|
||||
|
||||
{% block head_title %}{% trans "Log In" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-md mt-6">
|
||||
<h1 class="text-3xl font-bold text-center mb-6">{% trans "Log In" %}</h1>
|
||||
<form method="post" action="{% url 'account_login' %}" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
<div>
|
||||
<label for="{{ form.login.id_for_label }}" class="block font-medium text-gray-700">{{ form.login.label }}</label>
|
||||
{{ form.login|add_class:"input input-bordered w-full" }}
|
||||
{{ form.login.errors }}
|
||||
</div>
|
||||
<div>
|
||||
<label for="{{ form.password.id_for_label }}" class="block font-medium text-gray-700">{{ form.password.label }}</label>
|
||||
{{ form.password|add_class:"input input-bordered w-full" }}
|
||||
{{ form.password.errors }}
|
||||
</div>
|
||||
{% if form.remember %}
|
||||
<div class="flex items-center">
|
||||
{{ form.remember }}
|
||||
<label for="{{ form.remember.id_for_label }}" class="ml-2">{% trans "Remember Me" %}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-primary w-full">{% trans "Log In" %}</button>
|
||||
</form>
|
||||
<div class="mt-4 text-center">
|
||||
<a href="{% url 'account_reset_password' %}" class="text-primary underline">{% trans "Forgot Password?" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
16
theme/templates/account/logout.html
Normal file
16
theme/templates/account/logout.html
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags i18n %}
|
||||
|
||||
{% block head_title %}{% trans "Log Out" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-md mt-6">
|
||||
<h2 class="text-3xl font-bold text-center mb-6">{% trans "Sign Out" %}</h2>
|
||||
<p class="text-center mb-6">{% trans "Are you sure you want to sign out?" %}</p>
|
||||
<form method="post" action="{% url 'account_logout' %}" class="space-y-4 text-center">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-error w-full" type="submit">{% trans "Sign Out" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
15
theme/templates/account/password_change.html
Normal file
15
theme/templates/account/password_change.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags i18n %}
|
||||
|
||||
{% block head_title %}{% trans "Change Password" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-md mt-6">
|
||||
<h2 class="text-3xl font-bold text-center mb-6">{% trans "Change Password" %}</h2>
|
||||
<form method="post" action="{% url 'account_change_password' %}" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-success w-full" type="submit">{% trans "Change Password" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
21
theme/templates/account/password_reset.html
Normal file
21
theme/templates/account/password_reset.html
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags i18n widget_tweaks %}
|
||||
|
||||
{% block head_title %}{% trans "Reset Password" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-md mt-6">
|
||||
<h2 class="text-3xl font-bold text-center mb-6">{% trans "Reset Password" %}</h2>
|
||||
<p class="mb-4 text-center">{% trans "Enter your email address and we'll send you a link to reset your password." %}</p>
|
||||
<form method="post" action="{% url 'account_reset_password' %}" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
<div>
|
||||
<label for="{{ form.email.id_for_label }}" class="block font-medium text-gray-700">{{ form.email.label }}</label>
|
||||
{{ form.email|add_class:"input input-bordered w-full" }}
|
||||
{{ form.email.errors }}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-full">{% trans "Reset Password" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
11
theme/templates/account/password_reset_done.html
Normal file
11
theme/templates/account/password_reset_done.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags i18n %}
|
||||
|
||||
{% block head_title %}{% trans "Password Reset Done" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-md mt-6 text-center">
|
||||
<h2 class="text-3xl font-bold mb-4">{% trans "Password Reset" %}</h2>
|
||||
<p>{% trans "We have sent you an e-mail. Please contact us if you do not receive it in a few minutes." %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
28
theme/templates/account/password_reset_from_key.html
Normal file
28
theme/templates/account/password_reset_from_key.html
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags i18n %}
|
||||
|
||||
{% block head_title %}{% trans "Change Password" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-md mt-6">
|
||||
{% if token_fail %}
|
||||
<h2 class="text-3xl font-bold text-center mb-4">{% trans "Bad Token" %}</h2>
|
||||
<p class="mb-4 text-center">
|
||||
{% trans "The password reset link was invalid. Perhaps it has already been used? Please request a" %}
|
||||
<a href="{% url 'account_reset_password' %}" class="text-primary underline">{% trans "new password reset" %}</a>.
|
||||
</p>
|
||||
{% else %}
|
||||
{% if form %}
|
||||
<h2 class="text-3xl font-bold text-center mb-6">{% trans "Change Password" %}</h2>
|
||||
<form method="POST" action="." class="space-y-4">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-primary w-full" type="submit">{% trans "Change Password" %}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<h2 class="text-3xl font-bold text-center mb-4">{% trans "Password Changed" %}</h2>
|
||||
<p class="text-center">{% trans "Your password is now changed." %}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
11
theme/templates/account/password_reset_from_key_done.html
Normal file
11
theme/templates/account/password_reset_from_key_done.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags i18n %}
|
||||
|
||||
{% block head_title %}{% trans "Password Change Done" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-md mt-6 text-center">
|
||||
<h2 class="text-3xl font-bold mb-4">{% trans "Password Change Done" %}</h2>
|
||||
<p>{% trans "Your password has been changed." %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
43
theme/templates/account/signup.html
Normal file
43
theme/templates/account/signup.html
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n widget_tweaks %}
|
||||
|
||||
{% block head_title %}{% trans "Sign Up" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-md mt-6">
|
||||
<h1 class="text-3xl font-bold text-center mb-6">{% trans "Sign Up" %}</h1>
|
||||
<form method="post" action="{% url 'account_signup' %}" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
<div>
|
||||
<label for="{{ form.username.id_for_label }}" class="block font-medium text-gray-700">{{ form.username.label }}</label>
|
||||
{{ form.username|add_class:"input input-bordered w-full" }}
|
||||
{{ form.username.errors }}
|
||||
</div>
|
||||
<div>
|
||||
<label for="{{ form.email.id_for_label }}" class="block font-medium text-gray-700">{{ form.email.label }}</label>
|
||||
{{ form.email|add_class:"input input-bordered w-full" }}
|
||||
{{ form.email.errors }}
|
||||
</div>
|
||||
<div>
|
||||
<label for="{{ form.password1.id_for_label }}" class="block font-medium text-gray-700">{{ form.password1.label }}</label>
|
||||
{{ form.password1|add_class:"input input-bordered w-full" }}
|
||||
{{ form.password1.errors }}
|
||||
</div>
|
||||
<div>
|
||||
<label for="{{ form.password2.id_for_label }}" class="block font-medium text-gray-700">{{ form.password2.label }}</label>
|
||||
{{ form.password2|add_class:"input input-bordered w-full" }}
|
||||
{{ form.password2.errors }}
|
||||
</div>
|
||||
<div>
|
||||
<label for="{{ form.friend_code.id_for_label }}" class="block font-medium text-gray-700">{{ form.friend_code.label }}</label>
|
||||
{{ form.friend_code|add_class:"input input-bordered w-full" }}
|
||||
{{ form.friend_code.errors }}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-full">{% trans "Sign Up" %}</button>
|
||||
</form>
|
||||
<div class="mt-4 text-center">
|
||||
<p>{% trans "Already have an account?" %} <a href="{% url 'account_login' %}" class="text-primary underline">{% trans "Log In" %}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1 +1,173 @@
|
|||
{% load static tailwind_tags %}
|
||||
{% load static tailwind_tags gravatar %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- Inline script to set the theme before rendering -->
|
||||
<script>
|
||||
(function () {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<title>{% block title %}Pkmn Trade Club{% endblock title %}</title>
|
||||
<link rel="shortcut icon" href="{% static 'images/favicon.ico' %}">
|
||||
<!-- Choices.js -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js@11.0.6/public/assets/styles/choices.min.css" />
|
||||
<script async src="https://cdn.jsdelivr.net/npm/choices.js@11.0.6/public/assets/scripts/choices.min.js"></script>
|
||||
<!-- Tailwind CSS and Base stylesheet -->
|
||||
{% tailwind_css %}
|
||||
<link rel="stylesheet" href="{% static 'css/base.css' %}">
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block javascript_head %}{% endblock %}
|
||||
</head>
|
||||
<body class="min-h-screen bg-base-200">
|
||||
<!-- Header and Navigation -->
|
||||
<div class="navbar bg-base-100 shadow-sm">
|
||||
<div class="navbar-start">
|
||||
<div class="dropdown">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost hidden sm:flex md:hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" /> </svg>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
|
||||
<li><a href="{% url 'home' %}">Home</a></li>
|
||||
<li>
|
||||
<a>Trade</a>
|
||||
<ul class="p-2">
|
||||
<li><a href="{% url 'trade_offer_list' %}">All Offers</a></li>
|
||||
<li><a href="{% url 'trade_offer_list' %}?my_trades=true">My Trades</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a class="btn btn-ghost text-xl" href="{% url 'home' %}">
|
||||
<span aria-hidden="true">
|
||||
<sup class="inline-block relative left-2">P</sup>
|
||||
<sub class="inline-block relative">K</sub>
|
||||
<sup class="inline-block relative -left-2">M</sup>
|
||||
<sub class="inline-block relative -left-4">N</sub>
|
||||
<span class="inline-block relative -left-4">Trade Club</span>
|
||||
</span>
|
||||
<span aria-hidden="false" class="sr-only">Pokemon Trade Club</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-center hidden md:flex">
|
||||
<ul class="menu menu-horizontal px-1">
|
||||
<li><a href="{% url 'home' %}">Home</a></li>
|
||||
<li>
|
||||
<details>
|
||||
<summary>Trade</summary>
|
||||
<ul class="p-2 w-32 z-10">
|
||||
<li><a href="{% url 'trade_offer_list' %}">All Offers</a></li>
|
||||
<li><a href="{% url 'trade_offer_list' %}?my_trades=true">My Trades</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<!-- <button class="btn btn-ghost btn-circle hidden sm:flex">
|
||||
<div class="indicator">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /> </svg>
|
||||
<div aria-label="success" class="status status-success"></div>
|
||||
</div>
|
||||
</button> -->
|
||||
|
||||
<button id="theme-toggle-btn" class="btn btn-ghost btn-circle me-2" title="Toggle Theme">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 dark:hidden">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 hidden dark:block">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
|
||||
</svg>
|
||||
</button>
|
||||
{% if user.is_authenticated %}
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar">
|
||||
<div class="w-10 rounded-full">
|
||||
{{ user.email|gravatar:40 }}
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-32 p-2 shadow">
|
||||
<li>
|
||||
<a class="justify-between" href="https://www.gravatar.com/profile/" target="_blank" rel="noopener noreferrer">
|
||||
Profile
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="justify-between" href="{% url 'list_friend_codes' %}">
|
||||
Friend Codes
|
||||
</a>
|
||||
</li>
|
||||
<li><a href="{% url 'account_logout' %}">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex gap-2">
|
||||
<a class="btn btn-primary" href="{% url 'account_login' %}">Login</a>
|
||||
<a class="btn btn-secondary" href="{% url 'account_signup' %}">Sign Up</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto p-4 sm:w-4/5 md:w-full xl:w-256">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-base-200 text-base-content p-4">
|
||||
<div class="container mx-auto text-center">
|
||||
<p>© {% now "Y" %} PKMNTrade.Club. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Dock -->
|
||||
<div class="dock bg-neutral text-neutral-content sm:hidden">
|
||||
<button>
|
||||
<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor" stroke-linejoin="miter" stroke-linecap="butt"><polyline points="1 11 12 2 23 11" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="2"></polyline><path d="m5,13v7c0,1.105.895,2,2,2h10c1.105,0,2-.895,2-2v-7" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></path><line x1="12" y1="22" x2="12" y2="18" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></line></g></svg>
|
||||
<span class="dock-label">Home</span>
|
||||
</button>
|
||||
|
||||
<button class="dock-active">
|
||||
<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor" stroke-linejoin="miter" stroke-linecap="butt"><polyline points="3 14 9 14 9 17 15 17 15 14 21 14" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="2"></polyline><rect x="3" y="3" width="18" height="18" rx="2" ry="2" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></rect></g></svg>
|
||||
<span class="dock-label">Trades</span>
|
||||
</button>
|
||||
|
||||
<button>
|
||||
<svg class="size-[1.5em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor" stroke-linejoin="miter" stroke-linecap="butt"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /> </g></svg>
|
||||
<span class="dock-label">Notifications</span>
|
||||
</button>
|
||||
|
||||
<button>
|
||||
{% if user.is_authenticated %}<div tabindex="0" role="button" class="avatar"><div class="w-6 rounded-full">{{ user.email|gravatar:40 }}</div></div>{% else %}<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor" stroke-linejoin="miter" stroke-linecap="butt"><circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></circle><path d="m22,13.25v-2.5l-2.318-.966c-.167-.581-.395-1.135-.682-1.654l.954-2.318-1.768-1.768-2.318.954c-.518-.287-1.073-.515-1.654-.682l-.966-2.318h-2.5l-.966,2.318c-.581.167-1.135.395-1.654.682l-2.318-.954-1.768,1.768.954,2.318c-.287.518-.515,1.073-.682,1.654l-2.318.966v2.5l2.318.966c.167.581.395,1.135.682,1.654l-.954,2.318,1.768,1.768,2.318-.954c.518.287,1.073.515,1.654.682l.966,2.318h2.5l.966-2.318c.581-.167,1.135-.395,1.654-.682l2.318.954,1.768-1.768-.954-2.318c.287-.518.515-1.073.682-1.654l2.318-.966Z" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></path></g></svg>{% endif %}
|
||||
<span class="dock-label">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Alpine Plugins -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.14.8/dist/cdn.min.js"></script>
|
||||
|
||||
<!-- Alpine Core -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js"></script>
|
||||
|
||||
<script defer src="{% static 'js/base.js' %}"></script>
|
||||
{% block javascript %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
33
theme/templates/friend_codes/add_friend_code.html
Normal file
33
theme/templates/friend_codes/add_friend_code.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Add Friend Code{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-md mt-6">
|
||||
<h1 class="text-3xl font-bold mb-4">Add Friend Code</h1>
|
||||
<form method="post" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button type="submit" class="btn btn-primary w-full">Add Friend Code</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{% url 'list_friend_codes' %}" class="btn btn-secondary">Back to Friend Codes</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Cleave Zen from a CDN -->
|
||||
<script src="https://unpkg.com/cleave-zen@0.0.17/dist/cleave-zen.umd.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
// Initialize Cleave Zen on the friend code input field.
|
||||
// Make sure that the input ID is correct (e.g., provided by Django's widget rendering).
|
||||
new CleaveZen('#id_friend_code', {
|
||||
delimiters: ['-', '-', '-'], // Inserts dashes between the blocks.
|
||||
blocks: [4, 4, 4, 4],
|
||||
numericOnly: true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
33
theme/templates/friend_codes/confirm_delete_friend_code.html
Normal file
33
theme/templates/friend_codes/confirm_delete_friend_code.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Delete Friend Code{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-md mt-6">
|
||||
<h1 class="text-3xl font-bold mb-4">Delete Friend Code</h1>
|
||||
<p class="mb-4">
|
||||
Are you sure you want to delete friend code:
|
||||
<span class="font-mono">{{ friend_code.friend_code }}</span>?
|
||||
</p>
|
||||
|
||||
{% if error_message %}
|
||||
<div class="alert alert-warning mb-4">
|
||||
{{ error_message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="flex space-x-4">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-error"
|
||||
{% if disable_delete %} disabled {% endif %}>
|
||||
{% if disable_delete %}
|
||||
Delete Not Allowed
|
||||
{% else %}
|
||||
Confirm Delete
|
||||
{% endif %}
|
||||
</button>
|
||||
<a href="{% url 'list_friend_codes' %}" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
50
theme/templates/friend_codes/list_friend_codes.html
Normal file
50
theme/templates/friend_codes/list_friend_codes.html
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}My Friend Codes{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-xl mt-6">
|
||||
{# Display messages if there are any. #}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} mb-4">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<h1 class="text-3xl font-bold mb-4">My Friend Codes</h1>
|
||||
|
||||
{% if friend_codes %}
|
||||
<ul class="space-y-2">
|
||||
{% for code in friend_codes %}
|
||||
<li class="flex items-center justify-between {% if user.default_friend_code and code.id == user.default_friend_code.id %}bg-green-100{% else %}bg-base-100{% endif %} p-4 rounded shadow">
|
||||
<div>
|
||||
<span class="font-mono">{{ code.friend_code }}</span>
|
||||
{% if user.default_friend_code and code.id == user.default_friend_code.id %}
|
||||
<span class="badge badge-success ml-2">Default</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
{% if user.default_friend_code and code.id == user.default_friend_code.id %}
|
||||
<button type="button" class="btn btn-secondary btn-sm" disabled>Set as Default</button>
|
||||
{% else %}
|
||||
<form method="post" action="{% url 'change_default_friend_code' code.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-secondary btn-sm">Set as Default</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<a href="{% url 'delete_friend_code' code.id %}" class="btn btn-error btn-sm">Delete</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>You do not have any friend codes added yet.</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{% url 'add_friend_code' %}" class="btn btn-primary">Add a New Friend Code</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
26
theme/templates/home/_card_list.html
Normal file
26
theme/templates/home/_card_list.html
Normal file
|
|
@ -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 %}
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
{% for card in cards %}
|
||||
{% if show_zero|default:False or card.offer_count > 0 %}
|
||||
{% if mode == "offered" %}
|
||||
<a href="?offered_cards={{ card.id }}"
|
||||
{% else %}
|
||||
<a href="?wanted_cards={{ card.id }}"
|
||||
{% endif %}
|
||||
class="flex justify-between items-center text-primary no-underline">
|
||||
{% card_badge card card.offer_count %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center">No cards found</p>
|
||||
{% endif %}
|
||||
10
theme/templates/home/_search_results.html
Normal file
10
theme/templates/home/_search_results.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{% load trade_offer_tags %}
|
||||
{% if offered_cards or wanted_cards %}
|
||||
<hr class="my-8 border-t border-gray-200">
|
||||
<h2 class="text-2xl font-bold mb-4">Results</h2>
|
||||
{% if search_results and search_results.object_list %}
|
||||
{% include "trades/_trade_offer_list.html" with offers=search_results %}
|
||||
{% else %}
|
||||
<div class="alert alert-info mt-4">No trade offers found.</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
331
theme/templates/home/home.html
Normal file
331
theme/templates/home/home.html
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static trade_offer_tags card_badge cache card_multiselect %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-center text-4xl font-bold mb-8 pt-4">
|
||||
<span aria-hidden="true">
|
||||
<span class="inline-block relative left-2 text-4xl">Welcome to</span>
|
||||
<sup class="inline-block relative left-4 text-4xl">P</sup>
|
||||
<sub class="inline-block relative text-4xl">K</sub>
|
||||
<sup class="inline-block relative -left-2 text-4xl">M</sup>
|
||||
<sub class="inline-block relative -left-4 text-4xl">N</sub>
|
||||
<span class="inline-block relative -left-2 text-4xl">Trade Club</span>
|
||||
</span>
|
||||
<span aria-hidden="false" class="sr-only">Welcome to Pokemon Trade Club</span>
|
||||
</h1>
|
||||
|
||||
<!-- Search Form Section -->
|
||||
<section id="trade-search" class="mb-8">
|
||||
<form method="post" action="." class="space-y-4">
|
||||
{% csrf_token %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
{% card_multiselect "have_cards" "Have:" "Select zero or more cards..." available_cards have_cards %}
|
||||
</div>
|
||||
<div>
|
||||
{% card_multiselect "want_cards" "Want:" "Select zero or more cards..." available_cards want_cards %}
|
||||
</div>
|
||||
</div>
|
||||
{% if user.is_authenticated %}
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<button type="submit" class="btn btn-primary flex-1">Find a Trade Offer</button>
|
||||
<a href="{% url 'trade_offer_create' %}" id="createTradeOfferBtn" class="btn btn-secondary flex-1 text-center">Create Trade Offer</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary w-full">Find a Trade Offer</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Search Results Section -->
|
||||
<section id="search-results" class="mb-8">
|
||||
{% include "home/_search_results.html" %}
|
||||
</section>
|
||||
|
||||
<!-- Market Stats Section -->
|
||||
<section aria-labelledby="stats-heading" class="mb-8">
|
||||
<h2 id="stats-heading" class="text-2xl font-semibold mb-4">Market Stats</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<!-- Most Offered Cards -->
|
||||
<div>
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-header text-base-content p-4">
|
||||
<h5 class="text-xl text-center font-semibold whitespace-nowrap truncate mb-0">Most Offered Cards</h5>
|
||||
</div>
|
||||
<div class="card-body my-4 p-0">
|
||||
{% cache 3600 most_offered_cards %}
|
||||
{% include "home/_card_list.html" with cards=most_offered_cards mode="wanted" %}
|
||||
{% endcache %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Most Wanted Cards -->
|
||||
<div>
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-header text-base-content p-4">
|
||||
<h5 class="text-xl text-center font-semibold whitespace-nowrap truncate mb-0">Most Wanted Cards</h5>
|
||||
</div>
|
||||
<div class="card-body my-4 p-0">
|
||||
{% cache 3600 most_wanted_cards %}
|
||||
{% include "home/_card_list.html" with cards=most_wanted_cards mode="offered" %}
|
||||
{% endcache %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Least Offered Cards -->
|
||||
<div>
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-header text-base-content p-4">
|
||||
<h5 class="text-xl text-center font-semibold whitespace-nowrap truncate mb-0">Least Offered Cards</h5>
|
||||
</div>
|
||||
<div class="card-body my-4 p-0">
|
||||
{% cache 3600 least_offered_cards %}
|
||||
{% include "home/_card_list.html" with cards=least_offered_cards mode="wanted" show_zero=True %}
|
||||
{% endcache %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Offers and Recent Offers Section -->
|
||||
<section class="mb-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Featured Offers -->
|
||||
<div>
|
||||
{% cache 86400 featured_offers %}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-header text-base-content p-4">
|
||||
<h5 class="text-xl text-center font-semibold whitespace-nowrap truncate mb-0">Featured Offers</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<!-- New pure-CSS tabs for Featured Offers -->
|
||||
<div class="featured-offers-tabs">
|
||||
<!-- Radio inputs for all tabs -->
|
||||
<input type="radio" name="featured_offers_tabs" id="tab-all" class="hidden" checked>
|
||||
{% for rarity, offers in featured_offers.items %}
|
||||
{% if rarity != "All" %}
|
||||
<input type="radio" name="featured_offers_tabs" id="tab-{{ forloop.counter }}" class="hidden">
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<!-- Tab navigation: all tab labels appear together -->
|
||||
<div class="tabs tabs-box grid grid-cols-3 gap-2">
|
||||
<label for="tab-all" class="tab text-xs md:text-base">All</label>
|
||||
{% for rarity, offers in featured_offers.items %}
|
||||
{% if rarity != "All" %}
|
||||
<label for="tab-{{ forloop.counter }}" class="tab text-xs md:text-base">{{ rarity }}</label>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- All tab content panels are placed in one content container -->
|
||||
<div class="tab-contents">
|
||||
<!-- Panel for All offers -->
|
||||
<div class="tab-content" id="content-tab-all">
|
||||
{% if featured_offers.All %}
|
||||
<div class="flex flex-col items-center gap-3 w-auto mx-auto">
|
||||
{% for offer in featured_offers.All %}
|
||||
{% render_trade_offer offer %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center">No featured offers available.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Panels for each additional rarity -->
|
||||
{% for rarity, offers in featured_offers.items %}
|
||||
{% if rarity != "All" %}
|
||||
<div class="tab-content" id="content-tab-{{ forloop.counter }}">
|
||||
{% if offers %}
|
||||
<div class="flex flex-col items-center gap-3 w-auto mx-auto">
|
||||
{% for offer in offers %}
|
||||
{% render_trade_offer offer %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center">No featured offers for {{ rarity }}.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endcache %}
|
||||
</div>
|
||||
|
||||
<!-- Recent Offers -->
|
||||
<div>
|
||||
{% cache 60 recent_offers %}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-header text-center text-base-content p-4">
|
||||
<h5 class="text-xl font-semibold whitespace-nowrap truncate mb-0">Recent Offers</h5>
|
||||
</div>
|
||||
<div class="card-body my-4 p-4">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
{% for offer in recent_offers %}
|
||||
{% render_trade_offer offer %}
|
||||
{% empty %}
|
||||
<p>No recent offers available.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endcache %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
||||
{% block css %}
|
||||
<style>
|
||||
/* Hide the hidden radio inputs */
|
||||
.featured-offers-tabs input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Styles for the tabs navigation */
|
||||
.tabs.tabs-box {
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabs.tabs-box .tab {
|
||||
flex: 1; /* Each tab will equally expand */
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid transparent;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.tabs.tabs-box .tab:hover {
|
||||
border-color: currentColor;
|
||||
}
|
||||
|
||||
/* Active tab styling based on the radio input state */
|
||||
#tab-all:checked ~ .tabs.tabs-box label[for="tab-all"] {
|
||||
border-color: #2563eb; /* Example blue highlight */
|
||||
}
|
||||
{% for rarity, offers in featured_offers.items %}
|
||||
{% if rarity != "All" %}
|
||||
#tab-{{ forloop.counter }}:checked ~ .tabs.tabs-box label[for="tab-{{ forloop.counter }}"] {
|
||||
border-color: #2563eb;
|
||||
font-weight: bold;
|
||||
}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
/* Hide all content panels by default */
|
||||
.featured-offers-tabs .tab-contents > .tab-content {
|
||||
display: none;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Display the panel corresponding to the checked radio input */
|
||||
#tab-all:checked ~ .tab-contents #content-tab-all {
|
||||
display: block;
|
||||
}
|
||||
{% for rarity, offers in featured_offers.items %}
|
||||
{% if rarity != "All" %}
|
||||
#tab-{{ forloop.counter }}:checked ~ .tab-contents #content-tab-{{ forloop.counter }} {
|
||||
display: block;
|
||||
}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script defer>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// AJAX trade search form submission with vanilla JavaScript
|
||||
const tradeSearchForm = document.querySelector('#trade-search form');
|
||||
if (tradeSearchForm) {
|
||||
tradeSearchForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(tradeSearchForm);
|
||||
fetch(tradeSearchForm.action, {
|
||||
method: tradeSearchForm.method,
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(data => {
|
||||
document.querySelector('#search-results').innerHTML = data;
|
||||
})
|
||||
.catch(error => {
|
||||
alert("There was an error processing your search.");
|
||||
console.error('Error:', error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// AJAX pagination click handling
|
||||
document.addEventListener('click', function(e) {
|
||||
const target = e.target.closest('.ajax-page-link');
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
const page = target.getAttribute('data-page');
|
||||
let pageInput = document.getElementById('page');
|
||||
if (pageInput) {
|
||||
pageInput.value = page;
|
||||
} else {
|
||||
pageInput = document.createElement('input');
|
||||
pageInput.type = 'hidden';
|
||||
pageInput.id = 'page';
|
||||
pageInput.name = 'page';
|
||||
pageInput.value = page;
|
||||
tradeSearchForm.appendChild(pageInput);
|
||||
}
|
||||
tradeSearchForm.dispatchEvent(new Event('submit'));
|
||||
}
|
||||
});
|
||||
|
||||
// Updated: JS to carry over selections (including quantities) to the Create Trade Offer page.
|
||||
const createBtn = document.getElementById('createTradeOfferBtn');
|
||||
if (createBtn) {
|
||||
createBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
// Use the standardized field names for both "have_cards" and "want_cards"
|
||||
const haveSelect = document.querySelector('select[name="have_cards"]');
|
||||
const wantSelect = document.querySelector('select[name="want_cards"]');
|
||||
const url = new URL(createBtn.href, window.location.origin);
|
||||
|
||||
if (haveSelect) {
|
||||
// For each selected option, include the quantity from data-quantity (defaulting to "1")
|
||||
const selectedHave = Array.from(haveSelect.selectedOptions).map(opt => {
|
||||
const cardId = opt.value;
|
||||
const quantity = opt.getAttribute('data-quantity') || '1';
|
||||
return cardId + ':' + quantity;
|
||||
});
|
||||
selectedHave.forEach(val => url.searchParams.append('have_cards', val));
|
||||
}
|
||||
|
||||
if (wantSelect) {
|
||||
const selectedWant = Array.from(wantSelect.selectedOptions).map(opt => {
|
||||
const cardId = opt.value;
|
||||
const quantity = opt.getAttribute('data-quantity') || '1';
|
||||
return cardId + ':' + quantity;
|
||||
});
|
||||
selectedWant.forEach(val => url.searchParams.append('want_cards', val));
|
||||
}
|
||||
|
||||
window.location.href = url.href;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
27
theme/templates/trades/_friend_code_select.html
Normal file
27
theme/templates/trades/_friend_code_select.html
Normal file
|
|
@ -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 %}
|
||||
<div class="form-control">
|
||||
<label for="{{ field_name }}" class="label">
|
||||
<span class="label-text p-2 rounded">{{ label }}</span>
|
||||
</label>
|
||||
<select id="{{ field_name }}" name="{{ field_name }}" class="select select-bordered w-full bg-secondary text-white">
|
||||
{% for code in friend_codes %}
|
||||
<option value="{{ code.pk }}" {% if code.pk|stringformat:"s" == selected_friend_code.pk|stringformat:"s" %}selected{% endif %}>
|
||||
{{ code.friend_code }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% else %}
|
||||
<input type="hidden" name="{{ field_name }}" value="{{ friend_codes.0.pk }}">
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
58
theme/templates/trades/_trade_offer_list.html
Normal file
58
theme/templates/trades/_trade_offer_list.html
Normal file
|
|
@ -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 %}
|
||||
|
||||
<div class="flex flex-row gap-4 flex-wrap justify-center items-start">
|
||||
{% for offer in offers %}
|
||||
<div class="flex flex-none">
|
||||
<a href="{% url 'trade_offer_detail' pk=offer.pk %}" class="no-underline">
|
||||
{% render_trade_offer offer %}
|
||||
</a>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div>No trade offers available.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if offers.has_other_pages %}
|
||||
<nav aria-label="Trade offers pagination" class="mt-6">
|
||||
<ul class="flex justify-center space-x-2">
|
||||
{% if offers.has_previous %}
|
||||
<li>
|
||||
<a class="btn btn-outline ajax-page-link" data-page="{{ offers.previous_page_number }}" href="#">
|
||||
Previous
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<span class="btn btn-outline btn-disabled">Previous</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in offers.paginator.page_range %}
|
||||
<li>
|
||||
<a class="btn btn-outline ajax-page-link {% if offers.number == num %}btn-active{% endif %}" data-page="{{ num }}" href="#">
|
||||
{{ num }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if offers.has_next %}
|
||||
<li>
|
||||
<a class="btn btn-outline ajax-page-link" data-page="{{ offers.next_page_number }}" href="#">
|
||||
Next
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<span class="btn btn-outline btn-disabled">Next</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
32
theme/templates/trades/trade_acceptance_create.html
Normal file
32
theme/templates/trades/trade_acceptance_create.html
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Accept Trade Offer{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-xl mt-6">
|
||||
<h2 class="text-2xl font-bold">Accept Trade Offer</h2>
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">Submit Acceptance</button>
|
||||
</form>
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-error mt-4">
|
||||
<strong>Please correct the errors below:</strong>
|
||||
<ul>
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mt-6">
|
||||
<a href="{% url 'trade_offer_detail' pk=trade_offer.pk %}" class="btn btn-secondary">Back to Offer Details</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
32
theme/templates/trades/trade_acceptance_update.html
Normal file
32
theme/templates/trades/trade_acceptance_update.html
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Update Trade Acceptance{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-xl mt-6">
|
||||
<h2 class="text-2xl font-bold">Update Trade Acceptance</h2>
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">Update</button>
|
||||
</form>
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-error mt-4">
|
||||
<strong>Please correct the errors below:</strong>
|
||||
<ul>
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mt-6">
|
||||
<a href="{% url 'trade_offer_detail' pk=object.trade_offer.pk %}" class="btn btn-secondary">Back to Offer Details</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
77
theme/templates/trades/trade_offer_create.html
Normal file
77
theme/templates/trades/trade_offer_create.html
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load card_multiselect %}
|
||||
|
||||
{% block title %}Create Trade Offer{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-xl mt-6">
|
||||
<h2 class="text-2xl font-bold mb-4">Create a Trade Offer</h2>
|
||||
<form method="post" novalidate class="space-y-4">
|
||||
{% 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" %}
|
||||
|
||||
<!-- Grid layout for Card Selectors: "Have" and "Want" -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
{% card_multiselect "have_cards" "Have:" "Select one or more cards..." available_cards form.initial.have_cards %}
|
||||
</div>
|
||||
<div class="form-control">
|
||||
{% card_multiselect "want_cards" "Want:" "Select one or more cards..." available_cards form.initial.want_cards %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-full">Submit</button>
|
||||
</form>
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-error mt-4">
|
||||
<strong>Please correct the errors below:</strong>
|
||||
<ul class="mt-2">
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<script defer>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const initiatedBySelect = document.getElementById('{{ form.initiated_by.html_name }}');
|
||||
if (initiatedBySelect) {
|
||||
const choicesInstance = new Choices(initiatedBySelect, {
|
||||
searchEnabled: false,
|
||||
classNames: {
|
||||
containerOuter: 'choices',
|
||||
containerInner: 'choices__inner',
|
||||
input: 'choices__input',
|
||||
},
|
||||
callbackOnCreateTemplates: function(template) {
|
||||
return {
|
||||
choice: (classNames, data) => {
|
||||
return template(`
|
||||
<div class="${classNames.item} ${classNames.itemChoice} bg-accent text-white"
|
||||
data-select-text="${this.config.itemSelectText}"
|
||||
data-choice ${data.disabled ? 'data-choice-disabled aria-disabled="true"' : 'data-choice-selectable'}
|
||||
data-id="${data.id}" data-value="${data.value}"
|
||||
${data.groupId > 0 ? 'role="treeitem"' : 'role="option"'}>
|
||||
${data.label}
|
||||
</div>
|
||||
`);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Style the Choices control as needed
|
||||
choicesInstance.containerOuter.element.classList.add('bg-secondary', 'select', 'select-bordered', 'w-full');
|
||||
choicesInstance.containerInner.element.classList.add('bg-secondary', 'text-white');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock content %}
|
||||
47
theme/templates/trades/trade_offer_delete.html
Normal file
47
theme/templates/trades/trade_offer_delete.html
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Delete or Close Trade Offer{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-md mt-6">
|
||||
<h2 class="text-2xl font-bold mb-4">
|
||||
{% if action == 'delete' %}
|
||||
Delete Trade Offer
|
||||
{% elif action == 'close' %}
|
||||
Close Trade Offer
|
||||
{% else %}
|
||||
Delete/Close Trade Offer
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert {{ message.tags }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<p class="mb-4">
|
||||
{% 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 %}
|
||||
</p>
|
||||
|
||||
<form method="post" class="space-x-4">
|
||||
{% csrf_token %}
|
||||
{% if action %}
|
||||
{% if action == 'delete' %}
|
||||
<button type="submit" class="btn btn-error">Confirm Delete</button>
|
||||
{% elif action == 'close' %}
|
||||
<button type="submit" class="btn btn-warning">Confirm Close Trade Offer</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<button type="submit" class="btn btn-error" disabled>Cannot Delete/Close Trade Offer</button>
|
||||
{% endif %}
|
||||
<a href="{% url 'trade_offer_list' %}" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
64
theme/templates/trades/trade_offer_detail.html
Normal file
64
theme/templates/trades/trade_offer_detail.html
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Trade Offer Detail{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-2xl mt-6">
|
||||
<h2 class="text-2xl font-bold">Trade Offer Details</h2>
|
||||
<div class="card bg-base-100 shadow-lg p-4">
|
||||
<p>
|
||||
<strong>Hash:</strong> {{ object.hash }}<br>
|
||||
<strong>Initiated By:</strong> {{ object.initiated_by }}<br>
|
||||
<strong>Cards You Have (Offer):</strong>
|
||||
{% for through in object.trade_offer_have_cards.all %}
|
||||
{{ through.card.name }} x{{ through.quantity }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}<br>
|
||||
<strong>Cards You Want:</strong>
|
||||
{% for through in object.trade_offer_want_cards.all %}
|
||||
{{ through.card.name }} x{{ through.quantity }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}<br>
|
||||
<strong>Created At:</strong> {{ object.created_at|date:"M d, Y H:i" }}<br>
|
||||
<strong>Updated At:</strong> {{ object.updated_at|date:"M d, Y H:i" }}<br>
|
||||
<strong>Status:</strong> {% if object.is_closed %}Closed{% else %}Open{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 class="text-xl font-semibold mt-6">Acceptances</h3>
|
||||
{% if acceptances %}
|
||||
<ul class="space-y-2">
|
||||
{% for acceptance in acceptances %}
|
||||
<li class="card p-4">
|
||||
<p>
|
||||
<strong>Accepted By:</strong> {{ acceptance.accepted_by }}<br>
|
||||
<strong>Requested Card:</strong> {{ acceptance.requested_card.name }}<br>
|
||||
<strong>Offered Card:</strong> {{ acceptance.offered_card.name }}<br>
|
||||
<strong>State:</strong> {{ acceptance.get_state_display }}
|
||||
</p>
|
||||
<a href="{% url 'trade_acceptance_update' acceptance.pk %}" class="btn btn-sm">Update</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No acceptances yet.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if acceptance_form %}
|
||||
<h3 class="text-xl font-semibold mt-6">Accept This Offer</h3>
|
||||
<div class="card p-4">
|
||||
<form method="post" action="{% url 'trade_acceptance_create' offer_pk=object.pk %}">
|
||||
{% csrf_token %}
|
||||
{{ acceptance_form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">Submit Acceptance</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-6">
|
||||
<!-- Show delete/close button for the initiator -->
|
||||
{% if is_initiator %}
|
||||
<a href="{{ delete_close_url }}" class="btn btn-danger">Delete/Close Trade Offer</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'trade_offer_list' %}" class="btn btn-secondary">Back to Trade Offers</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
94
theme/templates/trades/trade_offer_list.html
Normal file
94
theme/templates/trades/trade_offer_list.html
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Trade Offer & Acceptance List{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto max-w-4xl mt-6">
|
||||
<!-- Filter Form: Friend Code Selector + Toggle for Completed view -->
|
||||
<div class="flex justify-end mb-4">
|
||||
<form method="get" class="flex items-center space-x-4">
|
||||
{% 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" %}
|
||||
|
||||
<label class="cursor-pointer flex items-center space-x-2">
|
||||
<span class="font-medium">Only Completed</span>
|
||||
<input type="checkbox" name="show_completed" value="true" class="toggle toggle-primary" {% if show_completed %}checked{% endif %}>
|
||||
</label>
|
||||
<button type="submit" class="btn btn-primary">Apply</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Section 1: My Trade Offers -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-4">My Trade Offers</h2>
|
||||
{% if my_trade_offers_paginated.object_list %}
|
||||
{% include "trades/_trade_offer_list.html" with offers=my_trade_offers_paginated %}
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
{% if my_trade_offers_paginated.has_previous %}
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'offers_page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}offers_page={{ my_trade_offers_paginated.previous_page_number }}" class="btn btn-sm">Previous</a>
|
||||
{% else %}
|
||||
<span></span>
|
||||
{% endif %}
|
||||
<span>Page {{ my_trade_offers_paginated.number }} of {{ my_trade_offers_paginated.paginator.num_pages }}</span>
|
||||
{% if my_trade_offers_paginated.has_next %}
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'offers_page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}offers_page={{ my_trade_offers_paginated.next_page_number }}" class="btn btn-sm">Next</a>
|
||||
{% else %}
|
||||
<span></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No trade offers found.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- Section 2: Trade Acceptances Waiting For Your Response -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-4">Trade Acceptances Waiting For Your Response</h2>
|
||||
{% if trade_acceptances_waiting_paginated.object_list %}
|
||||
{% include "trades/_trade_offer_list.html" with offers=trade_acceptances_waiting_paginated %}
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
{% if trade_acceptances_waiting_paginated.has_previous %}
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'waiting_page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}waiting_page={{ trade_acceptances_waiting_paginated.previous_page_number }}" class="btn btn-sm">Previous</a>
|
||||
{% else %}
|
||||
<span></span>
|
||||
{% endif %}
|
||||
<span>Page {{ trade_acceptances_waiting_paginated.number }} of {{ trade_acceptances_waiting_paginated.paginator.num_pages }}</span>
|
||||
{% if trade_acceptances_waiting_paginated.has_next %}
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'waiting_page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}waiting_page={{ trade_acceptances_waiting_paginated.next_page_number }}" class="btn btn-sm">Next</a>
|
||||
{% else %}
|
||||
<span></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No pending acceptances at this time.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- Section 3: Other Trade Acceptances -->
|
||||
<section>
|
||||
<h2 class="text-2xl font-bold mb-4">Other Trade Acceptances</h2>
|
||||
{% if other_trade_acceptances_paginated.object_list %}
|
||||
{% include "trades/_trade_offer_list.html" with offers=other_trade_acceptances_paginated %}
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
{% if other_trade_acceptances_paginated.has_previous %}
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'other_page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}other_page={{ other_trade_acceptances_paginated.previous_page_number }}" class="btn btn-sm">Previous</a>
|
||||
{% else %}
|
||||
<span></span>
|
||||
{% endif %}
|
||||
<span>Page {{ other_trade_acceptances_paginated.number }} of {{ other_trade_acceptances_paginated.paginator.num_pages }}</span>
|
||||
{% if other_trade_acceptances_paginated.has_next %}
|
||||
<a href="?{% for key, value in request.GET.items %}{% if key != 'other_page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}other_page={{ other_trade_acceptances_paginated.next_page_number }}" class="btn btn-sm">Next</a>
|
||||
{% else %}
|
||||
<span></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No other acceptances found.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<div class="mt-6">
|
||||
<a href="{% url 'trade_offer_create' %}" class="btn btn-success">Create New Offer</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
@ -1,26 +1,16 @@
|
|||
{% extends '_base.html' %}
|
||||
{% load static %}
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Trade Offer Details & Update{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-4">
|
||||
<h2 class="mb-4">Trade Offer Details</h2>
|
||||
|
||||
<div class="container mx-auto max-w-2xl mt-6 space-y-6">
|
||||
<h2 class="text-2xl font-bold">Trade Offer Details</h2>
|
||||
<!-- Offer Details Card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
Offer Information
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
<p class="text-gray-700">
|
||||
<strong>Created At:</strong> {{ object.created_at|date:"M d, Y H:i" }}<br>
|
||||
<strong>Updated At:</strong> {{ object.updated_at|date:"M d, Y H:i" }}<br>
|
||||
|
||||
{% 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 %}
|
||||
<strong>Initiated By:</strong> {{ object.initiated_by }}<br>
|
||||
<strong>Accepted By:</strong>
|
||||
|
|
@ -30,7 +20,6 @@
|
|||
Not yet accepted
|
||||
{% endif %}<br>
|
||||
{% endif %}
|
||||
|
||||
<strong>Cards You Have:</strong>
|
||||
{% for card in object.have_cards.all %}
|
||||
{{ card.name }}{% if not forloop.last %}, {% endif %}
|
||||
|
|
@ -46,16 +35,16 @@
|
|||
|
||||
{% if form.fields %}
|
||||
<!-- Form Card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
{% if action == "accept" %}
|
||||
Accept Trade Offer
|
||||
{% else %}
|
||||
Update Trade Offer
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body">
|
||||
<form method="post" novalidate>
|
||||
<div class="mb-4 font-semibold text-lg">
|
||||
{% if action == "accept" %}
|
||||
Accept Trade Offer
|
||||
{% else %}
|
||||
Update Trade Offer
|
||||
{% endif %}
|
||||
</div>
|
||||
<form method="post" novalidate class="space-y-4">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn {% if action == 'accept' %}btn-success{% else %}btn-primary{% endif %}">
|
||||
|
|
@ -75,9 +64,9 @@
|
|||
{% endif %}
|
||||
|
||||
{% if form and form.errors %}
|
||||
<div class="alert alert-danger mt-3">
|
||||
<div class="alert alert-error">
|
||||
<strong>Please correct the errors below:</strong>
|
||||
<ul class="mb-0">
|
||||
<ul class="mt-2">
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<li>{{ error }}</li>
|
||||
|
|
@ -89,12 +78,12 @@
|
|||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-3">
|
||||
|
||||
<div class="flex space-x-4">
|
||||
<a href="{% url 'trade_offer_list' %}" class="btn btn-secondary">Back to Trade Offers</a>
|
||||
{% if can_delete %}
|
||||
<a href="{% url 'trade_offer_delete' object.pk %}" class="btn btn-danger ms-2">Delete Trade Offer</a>
|
||||
<a href="{% url 'trade_offer_delete' object.pk %}" class="btn btn-error">Delete Trade Offer</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
{% endblock content %}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue