progress on conversion to tailwind
This commit is contained in:
parent
6a872124c6
commit
6e2843c60e
110 changed files with 4997 additions and 1691 deletions
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 %}
|
||||
14
theme/templates/account/email/password_reset_key_message.txt
Normal file
14
theme/templates/account/email/password_reset_key_message.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{% load i18n %}
|
||||
{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Hello from {{ site_name }}!
|
||||
|
||||
We've received a request to reset your password. If you didn't make this request, you can safely ignore this email. Otherwise, click the button below to reset your password.{% endblocktrans %}
|
||||
|
||||
{{ password_reset_url }}
|
||||
|
||||
{% if username %}{% blocktrans %}In case you forgot, your username is {{ username }}.{% endblocktrans %}
|
||||
|
||||
{% endif %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you for using {{ site_name }}!
|
||||
{{ site_domain }}{% endblocktrans %}
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
Password Reset E-mail
|
||||
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 %}
|
||||
89
theme/templates/trades/trade_offer_update.html
Normal file
89
theme/templates/trades/trade_offer_update.html
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Trade Offer Details & Update{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<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 bg-base-100 shadow-lg">
|
||||
<div class="card-body">
|
||||
<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>
|
||||
{% 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>
|
||||
{% if object.accepted_by %}
|
||||
{{ object.accepted_by }}
|
||||
{% else %}
|
||||
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 %}
|
||||
{% endfor %}<br>
|
||||
<strong>Cards You Want:</strong>
|
||||
{% for card in object.want_cards.all %}
|
||||
{{ card.name }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}<br>
|
||||
<strong>Current State:</strong> {{ object.get_state_display }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if form.fields %}
|
||||
<!-- Form Card -->
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body">
|
||||
<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 %}">
|
||||
{% if action == "accept" %}
|
||||
Accept Trade Offer
|
||||
{% else %}
|
||||
Submit
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
You are not authorized to perform any status changes on this trade offer.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if form and form.errors %}
|
||||
<div class="alert alert-error">
|
||||
<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 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-error">Delete Trade Offer</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
20
theme/templatetags/card_badge.html
Normal file
20
theme/templatetags/card_badge.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<div class="relative inline-block">
|
||||
{% if decks|length == 1 %}
|
||||
<div class="grid grid-cols-4 grid-rows-2 my-2 px-2 py-2 h-16 w-36 text-white shadow-md shadow-black/50" style="background-color: {{ decks.0.hex_color }};">
|
||||
{% elif decks|length == 2 %}
|
||||
<div class="grid grid-cols-4 grid-rows-2 my-2 px-2 py-2 h-16 w-36 text-white shadow-md shadow-black/50" style="background: linear-gradient(to right, {{ decks.0.hex_color }}, {{ decks.1.hex_color }});">
|
||||
{% elif decks|length >= 3 %}
|
||||
<div class="grid grid-cols-4 grid-rows-2 my-2 px-2 py-2 h-16 w-36 text-white shadow-md shadow-black/50" style="background: linear-gradient(to right, {{ decks.0.hex_color }}, {{ decks.1.hex_color }}, {{ decks.2.hex_color }});">
|
||||
{% else %}
|
||||
<div class="grid grid-cols-4 grid-rows-2 px-2 py-2 h-16 w-36 text-white shadow-md shadow-black/50" style="background-color: #cccccc; color: white;">
|
||||
{% endif %}
|
||||
<div class="row-span-1 col-span-4 truncate text-ellipsis self-start font-semibold leading-tight text-sm max-w-7/8">{{ card.name }}</div>
|
||||
<div class="row-start-2 col-span-2 truncate self-end align-bottom text-xs">{{ card.rarity.icons }}</div>
|
||||
<div class="row-start-2 col-start-3 col-span-2 text-right truncate self-end align-bottom font-semibold leading-none text-sm">{{ card.cardset.name }}</div>
|
||||
</div>
|
||||
{% if quantity != 1 %}
|
||||
<span class="card-quantity-badge absolute top-3.5 right-1 bg-gray-600 text-white text-xs font-semibold rounded-full px-2">
|
||||
{{ quantity }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
306
theme/templatetags/card_multiselect.html
Normal file
306
theme/templatetags/card_multiselect.html
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
{% load cache card_badge %}
|
||||
<label for="{{ field_id }}" class="label">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
</label>
|
||||
<select name="{{ field_name }}" id="{{ field_id }}" class="select select-bordered w-full card-multiselect" data-placeholder="{{ placeholder }}" multiple>
|
||||
{% cache cache_timeout cache_key selected_values|join:"," %}
|
||||
<option value="" disabled>{{ placeholder }}</option>
|
||||
{% for card in available_cards %}
|
||||
<option
|
||||
value="{{ card.pk }}"
|
||||
data-quantity="{% if card.pk|stringformat:"s" in selected_values %}{{ card.selected_quantity }}{% else %}1{% endif %}"
|
||||
{% if card.pk|stringformat:"s" in selected_values %}selected{% endif %}
|
||||
data-html-content='{% if card.selected_quantity %}{{ card|card_badge_inline:"__QUANTITY__" }}{% else %}{{ card|card_badge_inline }}{% endif %}'
|
||||
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 defer>
|
||||
if (!window.updateGlobalCardFilters) {
|
||||
window.updateGlobalCardFilters = function() {
|
||||
const selects = document.querySelectorAll('.card-multiselect');
|
||||
|
||||
// Gather all selected card IDs from every multiselect.
|
||||
const globalSelectedIds = [];
|
||||
selects.forEach(select => {
|
||||
Array.from(select.selectedOptions).forEach(option => {
|
||||
if (option.value) {
|
||||
globalSelectedIds.push(option.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Determine the global rarity based on the first found selected option.
|
||||
let globalRarity = null;
|
||||
for (const select of selects) {
|
||||
if (select.selectedOptions.length > 0) {
|
||||
globalRarity = select.selectedOptions[0].getAttribute('data-rarity');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to determine if the option for the given card is selected in a specific select.
|
||||
const isOptionSelected = (select, cardId) => {
|
||||
const option = select.querySelector(`option[value="${cardId}"]`);
|
||||
return option ? option.selected : false;
|
||||
};
|
||||
|
||||
selects.forEach(select => {
|
||||
// Update each option element in the select.
|
||||
select.querySelectorAll('option').forEach(function(option) {
|
||||
const cardId = option.value;
|
||||
const optionRarity = option.getAttribute('data-rarity');
|
||||
const isSelected = option.selected;
|
||||
|
||||
// 1. Global rarity filter: if any card is selected overall, then only allow options that are already
|
||||
// selected or that match the global rarity.
|
||||
const passesRarity = (!globalRarity) || isSelected || (optionRarity === globalRarity);
|
||||
// 2. Unique selection filter: if the card is selected anywhere globally (and not on this select), then disable it.
|
||||
const passesUnique = isSelected || !globalSelectedIds.includes(cardId);
|
||||
|
||||
option.disabled = !(passesRarity && passesUnique);
|
||||
});
|
||||
|
||||
// Update the Choices.js dropdown display as well.
|
||||
if (select.choicesInstance) {
|
||||
const dropdown = select.choicesInstance.dropdown.element;
|
||||
if (dropdown) {
|
||||
dropdown.querySelectorAll('[data-choice]').forEach(function(item) {
|
||||
const cardId = item.getAttribute('data-value');
|
||||
const itemRarity = item.getAttribute('data-rarity');
|
||||
const isSelected = isOptionSelected(select, cardId);
|
||||
|
||||
const passesRarity = (!globalRarity) || isSelected || (itemRarity === globalRarity);
|
||||
const passesUnique = isSelected || !globalSelectedIds.includes(cardId);
|
||||
|
||||
item.style.display = (passesRarity && passesUnique) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const selectField = document.getElementById('{{ field_id }}');
|
||||
const placeholder = selectField.getAttribute('data-placeholder') || '';
|
||||
|
||||
const choicesInstance = new Choices(selectField, {
|
||||
removeItemButton: false,
|
||||
placeholderValue: placeholder,
|
||||
searchEnabled: true,
|
||||
shouldSort: false,
|
||||
allowHTML: true,
|
||||
callbackOnCreateTemplates: function(template) {
|
||||
// Helper to get HTML content and do token replacement if necessary.
|
||||
const getCardContent = (data) => {
|
||||
let htmlContent = (data.element && data.element.getAttribute('data-html-content')) || data.label;
|
||||
let quantity = data.element ? data.element.getAttribute('data-quantity') : "1";
|
||||
// Replace placeholder token (__QUANTITY__) with the current quantity.
|
||||
if (htmlContent.includes('__QUANTITY__')) {
|
||||
htmlContent = htmlContent.replace(/__QUANTITY__/g, quantity);
|
||||
}
|
||||
return htmlContent;
|
||||
};
|
||||
|
||||
const renderCard = (classNames, data, type) => {
|
||||
const rarity = data.element ? data.element.getAttribute('data-rarity') : '';
|
||||
const content = getCardContent(data);
|
||||
if (type === 'item') {
|
||||
return template(`
|
||||
<div class="${classNames.item} mx-auto w-max ${data.highlighted ? classNames.highlightedState : ''} relative"
|
||||
data-id="${data.id}"
|
||||
data-value="${data.value}"
|
||||
data-quantity="${data.element ? data.element.getAttribute('data-quantity') : 1}"
|
||||
data-item
|
||||
data-rarity="${rarity}"
|
||||
aria-selected="true"
|
||||
style="cursor: pointer; padding: 1rem;">
|
||||
<button type="button" class="decrement absolute left-[-1.5rem] top-1/2 transform -translate-y-1/2 bg-gray-300 px-2">-</button>
|
||||
<button type="button" class="increment absolute right-[-1.5rem] top-1/2 transform -translate-y-1/2 bg-gray-300 px-2">+</button>
|
||||
<div class="card-content">${content}</div>
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
const extraAttributes = `data-select-text="${this.config.itemSelectText}" data-choice ${
|
||||
data.disabled ? 'data-choice-disabled aria-disabled="true"' : 'data-choice-selectable'
|
||||
}`;
|
||||
const extraClasses = classNames.itemChoice;
|
||||
return template(`
|
||||
<div class="${classNames.item} ${extraClasses} ${data.highlighted ? classNames.highlightedState : ''} mx-auto w-max"
|
||||
${extraAttributes}
|
||||
data-id="${data.id}"
|
||||
data-value="${data.value}"
|
||||
data-rarity="${rarity}"
|
||||
style="cursor: pointer;">
|
||||
${content}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
choice: function(classNames, data) {
|
||||
return renderCard(classNames, data, 'choice');
|
||||
},
|
||||
item: function(classNames, data) {
|
||||
return renderCard(classNames, data, 'item');
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Associate the Choices instance with the select.
|
||||
selectField.choicesInstance = choicesInstance;
|
||||
|
||||
if (!window.cardMultiselectInstances) {
|
||||
window.cardMultiselectInstances = [];
|
||||
}
|
||||
window.cardMultiselectInstances.push(selectField);
|
||||
|
||||
selectField.addEventListener('change', function() {
|
||||
if (window.updateGlobalCardFilters) {
|
||||
window.updateGlobalCardFilters();
|
||||
}
|
||||
});
|
||||
|
||||
// Scope the event listener to the closest .choices container.
|
||||
const choicesContainer = selectField.closest('.choices') || document;
|
||||
|
||||
choicesContainer.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('increment')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
const container = e.target.closest('[data-item]');
|
||||
if (container) {
|
||||
let quantityBadge = container.querySelector('.card-quantity-badge');
|
||||
if (!quantityBadge) {
|
||||
quantityBadge = document.createElement('span');
|
||||
quantityBadge.className = 'card-quantity-badge';
|
||||
quantityBadge.style.marginRight = '0.5rem';
|
||||
container.insertBefore(quantityBadge, container.firstChild);
|
||||
}
|
||||
let quantity = parseInt(container.getAttribute('data-quantity')) || 1;
|
||||
quantity = quantity + 1;
|
||||
quantityBadge.innerText = quantity;
|
||||
updateOptionQuantity(container, quantity);
|
||||
}
|
||||
}
|
||||
if (e.target.classList.contains('decrement')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
const container = e.target.closest('[data-item]');
|
||||
if (container) {
|
||||
let quantityBadge = container.querySelector('.card-quantity-badge');
|
||||
if (!quantityBadge) {
|
||||
quantityBadge = document.createElement('span');
|
||||
quantityBadge.className = 'card-quantity-badge';
|
||||
quantityBadge.style.marginRight = '0.5rem';
|
||||
container.insertBefore(quantityBadge, container.firstChild);
|
||||
}
|
||||
let quantity = parseInt(container.getAttribute('data-quantity')) || 1;
|
||||
if (quantity === 1) {
|
||||
const cardId = container.getAttribute('data-value');
|
||||
let removed = false;
|
||||
const activeValues = choicesInstance.getValue(true);
|
||||
for (let val of activeValues) {
|
||||
if (val === cardId) {
|
||||
choicesInstance.removeActiveItemsByValue(val);
|
||||
removed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!removed) {
|
||||
container.remove();
|
||||
const option = selectField.querySelector('option[value="' + cardId + '"]');
|
||||
if (option) {
|
||||
option.selected = false;
|
||||
option.setAttribute('data-quantity', '1');
|
||||
}
|
||||
}
|
||||
if (window.updateGlobalCardFilters) {
|
||||
window.updateGlobalCardFilters();
|
||||
}
|
||||
} else {
|
||||
quantity = quantity - 1;
|
||||
quantityBadge.innerText = quantity;
|
||||
updateOptionQuantity(container, quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (e.target.closest('[data-item]') && !e.target.classList.contains('increment') && !e.target.classList.contains('decrement')) {
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
});
|
||||
|
||||
// Helper: update only the data-quantity in both the UI container and its corresponding select option.
|
||||
function updateOptionQuantity(container, quantity) {
|
||||
container.setAttribute('data-quantity', quantity);
|
||||
const cardId = container.getAttribute('data-value') || container.getAttribute('data-id');
|
||||
const option = selectField.querySelector('option[value="' + cardId + '"]');
|
||||
if (option) {
|
||||
option.setAttribute('data-quantity', quantity);
|
||||
}
|
||||
}
|
||||
|
||||
// New Form Submission Handler: Before the form submits, create hidden inputs with "cardID:quantity".
|
||||
const formElement = selectField.closest('form');
|
||||
if (formElement) {
|
||||
formElement.addEventListener('submit', function(e) {
|
||||
const multiselects = formElement.querySelectorAll('.card-multiselect');
|
||||
multiselects.forEach((select) => {
|
||||
const fieldName = select.getAttribute('name');
|
||||
select.removeAttribute('name');
|
||||
Array.from(select.selectedOptions).forEach((option) => {
|
||||
const cardId = option.value;
|
||||
const quantity = option.getAttribute('data-quantity') || '1';
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = fieldName;
|
||||
hiddenInput.value = cardId + ':' + quantity;
|
||||
formElement.appendChild(hiddenInput);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initial filters update on page load.
|
||||
if (window.updateGlobalCardFilters) {
|
||||
window.updateGlobalCardFilters();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.choices.select {
|
||||
height: inherit;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
.choices.select[data-type*="select-one"]::after {
|
||||
display: none;
|
||||
}
|
||||
.choices__inner.bg-secondary {
|
||||
background-color: var(--color-secondary);
|
||||
border: none;
|
||||
z-index: 10;
|
||||
}
|
||||
.choices__item.mx-auto.w-max:hover {
|
||||
background-color: #e2e8f0;
|
||||
}
|
||||
.choices__input,
|
||||
.choices__input--cloned {
|
||||
width: 100% !important;
|
||||
}
|
||||
div.choices__list span.card-quantity-badge {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
253
theme/templatetags/trade_offer.html
Normal file
253
theme/templatetags/trade_offer.html
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
{% load gravatar card_badge %}
|
||||
|
||||
<div class="card border border-gray-200 shadow-lg w-96 md:w-80 lg:w-96"
|
||||
x-data="{ flipped: false, badgeExpanded: false, acceptanceExpanded: false }">
|
||||
<!-- Flip Container with Perspective -->
|
||||
<div class="flip-container">
|
||||
<!-- Flip Inner: rotates based on 'flipped' state -->
|
||||
<div class="flip-inner grid transform transition-transform duration-700 ease-in-out"
|
||||
:class="{'rotate-y-180': flipped}">
|
||||
|
||||
<!-- Front Side: Trade Offer -->
|
||||
<div class="flip-face front col-start-1 row-start-1">
|
||||
<a href="{% url 'trade_offer_detail' pk=offer.pk %}" class="no-underline block">
|
||||
<!-- Header: Has/Wants -->
|
||||
<div class="py-4 mx-2 sm:mx-4">
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<div class="flex justify-center items-center">
|
||||
<span class="text-sm font-semibold">Has</span>
|
||||
</div>
|
||||
<div class="flex justify-center items-center">
|
||||
<div class="avatar">
|
||||
<div class="w-10 rounded-full">
|
||||
{{ offer.initiated_by.user.email|gravatar:40 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center items-center">
|
||||
<span class="text-sm font-semibold">Wants</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main Trade Offer Row: First row of card badges -->
|
||||
<div class="px-2 pb-2 min-h-[80px]">
|
||||
<div class="grid grid-cols-2 gap-2 items-center border-t border-gray-300">
|
||||
<div class="flex flex-col items-center">
|
||||
{% if offer.trade_offer_have_cards.all %}
|
||||
{% with first_have=offer.trade_offer_have_cards.all.0 %}
|
||||
{% card_badge first_have.card first_have.quantity %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
{% if offer.trade_offer_want_cards.all %}
|
||||
{% with first_want=offer.trade_offer_want_cards.all.0 %}
|
||||
{% card_badge first_want.card first_want.quantity %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Extra Card Badges (Collapsible) -->
|
||||
<div x-show="badgeExpanded" x-collapse.duration.500ms class="px-2">
|
||||
<a href="{% url 'trade_offer_detail' pk=offer.pk %}" class="no-underline block">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<!-- Extra Has Cards Column -->
|
||||
<div class="flex flex-col items-center">
|
||||
{% for th in offer.trade_offer_have_cards.all|slice:"1:" %}
|
||||
{% card_badge th.card th.quantity %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<!-- Extra Wants Cards Column -->
|
||||
<div class="flex flex-col items-center">
|
||||
{% for th in offer.trade_offer_want_cards.all|slice:"1:" %}
|
||||
{% card_badge th.card th.quantity %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Extra Card Badges Expansion Toggle (shown only if extra cards exist) -->
|
||||
{% if offer.trade_offer_have_cards.all|length > 1 or offer.trade_offer_want_cards.all|length > 1 %}
|
||||
<div class="flex justify-center my-1">
|
||||
<svg @click="badgeExpanded = !badgeExpanded"
|
||||
x-bind:class="{ 'rotate-180': badgeExpanded }"
|
||||
class="transition-transform duration-500 h-5 w-5 cursor-pointer"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Bottom Icons on Front Side -->
|
||||
<div class="flex justify-between px-2 pb-2">
|
||||
<!-- Info Icon at Bottom Left -->
|
||||
<div class="text-gray-500 text-sm tooltip tooltip-right" data-tip="Trade ID: {{ offer.hash }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Flip Icon at Bottom Right: flips to acceptances view (back side) -->
|
||||
<div class="cursor-pointer"
|
||||
@click="badgeExpanded = false; $nextTick(() => { flipped = true })">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 4v1m0 14v1m8-8h1M4 12H3m15.364-6.364l.707.707M6.343 17.657l-.707.707m12.728 0l-.707-.707M6.343 6.343l-.707-.707" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Back Side: Acceptances View -->
|
||||
<div class="flip-face back col-start-1 row-start-1" style="transform: rotateY(180deg);">
|
||||
<a href="{% url 'trade_offer_detail' pk=offer.pk %}" class="no-underline block">
|
||||
<!-- Has/Wants Header -->
|
||||
<div class="py-4 mx-2 sm:mx-4">
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<div class="flex justify-center items-center">
|
||||
<span class="text-sm font-semibold">Has</span>
|
||||
</div>
|
||||
<div class="flex justify-center items-center">
|
||||
<div class="avatar">
|
||||
<div class="w-10 rounded-full">
|
||||
{{ offer.initiated_by.user.email|gravatar:40 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center items-center">
|
||||
<span class="text-sm font-semibold">Wants</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Acceptances Content Area -->
|
||||
<div class="px-2 pb-2 min-h-[80px]">
|
||||
<!-- Collapsed Acceptances: show only the first acceptance (if available) -->
|
||||
<div x-show="!acceptanceExpanded" class="overflow-hidden">
|
||||
{% if offer.acceptances.all|length > 0 %}
|
||||
<div class="space-y-3">
|
||||
{% for acceptance in offer.acceptances.all|slice:"0:1" %}
|
||||
<!-- Acceptance Card Pair -->
|
||||
<div class="grid grid-cols-2 gap-4 items-center border-t border-gray-300">
|
||||
<div>
|
||||
{% card_badge acceptance.requested_card %}
|
||||
</div>
|
||||
<div>
|
||||
{% card_badge acceptance.offered_card %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Acceptor Info -->
|
||||
<div class="flex items-center text-xs mt-2">
|
||||
<div class="w-8 h-8 mr-2">
|
||||
<div class="w-full h-full rounded-full overflow-hidden">
|
||||
{{ acceptance.accepted_by.user.email|gravatar:32 }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="leading-tight">{{ acceptance.accepted_by.user.username }}</span>
|
||||
<span class="ml-auto leading-tight">{{ acceptance.state }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Expanded Acceptances: displays all acceptances -->
|
||||
<div x-show="acceptanceExpanded" x-collapse.duration.500ms class="space-y-3">
|
||||
{% for acceptance in offer.acceptances.all %}
|
||||
<!-- Acceptance Card Pair -->
|
||||
<div class="grid grid-cols-2 gap-4 items-center border-t border-gray-300">
|
||||
<div>
|
||||
{% card_badge acceptance.requested_card %}
|
||||
</div>
|
||||
<div>
|
||||
{% card_badge acceptance.offered_card %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Acceptor Info -->
|
||||
<div class="flex items-center text-xs mt-2">
|
||||
<div class="w-8 h-8 mr-2">
|
||||
<div class="w-full h-full rounded-full overflow-hidden">
|
||||
{{ acceptance.accepted_by.user.email|gravatar:32 }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="leading-tight">{{ acceptance.accepted_by.user.username }}</span>
|
||||
<span class="ml-auto leading-tight">{{ acceptance.state }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Arrow for Acceptances Expansion -->
|
||||
<div class="flex justify-center my-1">
|
||||
<svg @click="acceptanceExpanded = !acceptanceExpanded"
|
||||
x-bind:class="{ 'rotate-180': acceptanceExpanded }"
|
||||
class="transition-transform duration-500 h-5 w-5 cursor-pointer"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Icons on Back Side -->
|
||||
<div class="flex justify-between px-2 pb-2">
|
||||
<!-- Info Icon at Bottom Left -->
|
||||
<div class="text-gray-500 text-sm tooltip tooltip-right" data-tip="Trade ID: {{ offer.hash }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Acceptances Header -->
|
||||
<div class="px-1 text-center ">
|
||||
<span class="text-sm font-semibold">
|
||||
Acceptances ({{ offer.acceptances.all|length }})
|
||||
</span>
|
||||
</div>
|
||||
<!-- Flip-Back Icon at Bottom Right: flips back to front side -->
|
||||
<div class="cursor-pointer"
|
||||
@click="acceptanceExpanded = false; $nextTick(() => { flipped = false })">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 20l9-8-9-8M3 12h18" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
/* Minimal custom CSS for the card flip effect */
|
||||
.flip-container {
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.flip-inner {
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.flip-face {
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* Explicitly set the front face to 0 rotation */
|
||||
.flip-face.front {
|
||||
transform: rotateY(0);
|
||||
}
|
||||
|
||||
/* This class is toggled by AlpineJS to rotate the card container */
|
||||
.rotate-y-180 {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue