diff --git a/padelclub_backend/settings_app.py b/padelclub_backend/settings_app.py
index c34bc82..e69f82e 100644
--- a/padelclub_backend/settings_app.py
+++ b/padelclub_backend/settings_app.py
@@ -52,8 +52,7 @@ SYNC_APPS = {
STRIPE_CURRENCY = 'eur'
# Add managers who should receive internal emails
SHOP_MANAGERS = [
- ('Razmig Sarkissian', 'razmig@padelclub.app'),
- # ('Shop Admin', 'shop-admin@padelclub.app'),
+ ('Shop Admin', 'shop-admin@padelclub.app'),
# ('Laurent Morvillier', 'laurent@padelclub.app'),
# ('Xavier Rousset', 'xavier@padelclub.app'),
]
diff --git a/shop/cart.py b/shop/cart.py
index 76dcec6..18bb05c 100644
--- a/shop/cart.py
+++ b/shop/cart.py
@@ -74,3 +74,21 @@ def get_cart_item(request, item_id):
return CartItem.objects.get(id=item_id, session_id=cart_id)
except CartItem.DoesNotExist:
raise Exception("Cart item not found")
+
+def transfer_cart(request, old_session_key):
+ """
+ Transfer cart items from an anonymous session to an authenticated user's session
+ """
+ from django.contrib.sessions.models import Session
+ from django.contrib.sessions.backends.db import SessionStore
+
+ # Get the old session
+ try:
+ old_session = SessionStore(session_key=old_session_key)
+ # Check if there are cart items in the old session
+ if 'cart_items' in old_session:
+ # Transfer cart items to the new session
+ request.session['cart_items'] = old_session['cart_items']
+ request.session.modified = True
+ except Session.DoesNotExist:
+ pass
diff --git a/shop/signals.py b/shop/signals.py
index 83be688..70ed58a 100644
--- a/shop/signals.py
+++ b/shop/signals.py
@@ -5,6 +5,8 @@ from django.conf import settings
from django.urls import reverse
from .models import Order, OrderItem, OrderStatus
from django.db import transaction
+from django.contrib.auth.signals import user_logged_in
+from .cart import transfer_cart
@receiver([post_save, post_delete], sender=Order)
def send_order_notification(sender, instance, **kwargs):
@@ -307,3 +309,17 @@ Merci de votre confiance.
L'équipe PadelClub
"""
+
+@receiver(user_logged_in)
+def user_logged_in_handler(sender, request, user, **kwargs):
+ """
+ When a user logs in, transfer any cart items from their anonymous session
+ """
+ # Get the anonymous session key
+ if hasattr(request, 'session') and not request.session.is_empty():
+ anonymous_session_key = request.session.session_key
+
+ # After the user logs in, the session key changes
+ # So we transfer cart from the old session to the new session
+ if anonymous_session_key:
+ transfer_cart(request, anonymous_session_key)
diff --git a/shop/static/shop/images/products/noir_hat.png.avif b/shop/static/shop/images/products/noir_hat.png.avif
new file mode 100644
index 0000000..b451e1d
Binary files /dev/null and b/shop/static/shop/images/products/noir_hat.png.avif differ
diff --git a/shop/stripe_utils.py b/shop/stripe_utils.py
index 99fb354..a6a5934 100644
--- a/shop/stripe_utils.py
+++ b/shop/stripe_utils.py
@@ -25,12 +25,13 @@ class StripeService:
mode_str = "TEST" if self.is_test_mode else "LIVE"
logger.debug(f"Initialized StripeService in {mode_str} mode")
- def create_checkout_session(self, line_items, success_url, cancel_url, metadata=None):
+ def create_checkout_session(self, customer_email, line_items, success_url, cancel_url, metadata=None):
"""Create a Stripe Checkout Session for one-time payments"""
if self.is_test_mode:
logger.info(f"Creating checkout session in TEST mode with metadata: {metadata}")
session = stripe.checkout.Session.create(
+ customer_email=customer_email,
payment_method_types=['card'],
line_items=line_items,
mode='payment',
@@ -67,24 +68,3 @@ class StripeService:
# Create a singleton instance for import and use throughout the app
stripe_service = StripeService()
-
-# For backward compatibility, expose some functions directly
-def create_payment_intent(amount, currency=None, metadata=None):
- """Legacy function for backward compatibility"""
- if currency is None:
- currency = stripe_service.currency
-
- return stripe.PaymentIntent.create(
- amount=amount,
- currency=currency,
- metadata=metadata or {},
- )
-
-def create_checkout_session(line_items, success_url, cancel_url, metadata=None):
- """Legacy function for backward compatibility"""
- return stripe_service.create_checkout_session(
- line_items=line_items,
- success_url=success_url,
- cancel_url=cancel_url,
- metadata=metadata or {},
- )
diff --git a/shop/templates/shop/product_item.html b/shop/templates/shop/product_item.html
index e00c05e..047ec50 100644
--- a/shop/templates/shop/product_item.html
+++ b/shop/templates/shop/product_item.html
@@ -1,11 +1,15 @@
+{% load shop_extras %}
{% if product.image %}
-

+

{% else %}
No Image Available
{% endif %}
-
{{ product.price }} €
-
+
@@ -123,6 +128,88 @@ function selectColor(productId, colorId, colorName, element) {
// Add selected class to clicked color
element.classList.add('selected');
+
+ // Update product image based on selected color
+ const productImage = document.getElementById(`product-image-${productId}`);
+
+ if (productImage) {
+ const colorImage = element.getAttribute('data-color-image');
+ if (colorImage) {
+ productImage.src = colorImage;
+ }
+ }
}
+function addToCartAjax(productId) {
+ // Get the form
+ const form = document.getElementById(`cart-form-${productId}`);
+ const formData = new FormData(form);
+
+ // Add CSRF token
+ const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
+
+ // Create notification element
+ const notification = document.createElement('div');
+ notification.className = 'add-to-cart-notification';
+ notification.style.position = 'fixed';
+ notification.style.bottom = '20px';
+ notification.style.right = '20px';
+ notification.style.padding = '20px';
+ notification.style.backgroundColor = '#90ee90';
+ notification.style.color = '#707070';
+ notification.style.borderRadius = '12px';
+ notification.style.zIndex = '9999';
+ notification.style.opacity = '0';
+ notification.style.transition = 'opacity 0.3s';
+
+ // Send AJAX request
+ fetch(form.action, {
+ method: 'POST',
+ body: formData,
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'X-CSRFToken': csrftoken,
+ }
+ })
+ .then(response => response.json())
+ .then(data => {
+ // Show success message
+ notification.textContent = data.message;
+ document.body.appendChild(notification);
+
+ // Update cart total in the navigation
+ const cartTotalElements = document.querySelectorAll('.confirm-nav-button');
+ cartTotalElements.forEach(element => {
+ element.textContent = `Voir mon panier (${data.cart_total} €)`;
+ });
+
+ // Show notification
+ setTimeout(() => { notification.style.opacity = '1'; }, 100);
+
+ // Hide notification after 3 seconds
+ setTimeout(() => {
+ notification.style.opacity = '0';
+ setTimeout(() => {
+ document.body.removeChild(notification);
+ }, 300); // Wait for fade-out animation
+ }, 3000);
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ notification.textContent = "Erreur lors de l'ajout au panier";
+ notification.style.backgroundColor = '#F44336';
+ document.body.appendChild(notification);
+
+ // Show notification
+ setTimeout(() => { notification.style.opacity = '1'; }, 100);
+
+ // Hide notification after 3 seconds
+ setTimeout(() => {
+ notification.style.opacity = '0';
+ setTimeout(() => {
+ document.body.removeChild(notification);
+ }, 300); // Wait for fade-out animation
+ }, 3000);
+ });
+}
diff --git a/shop/templatetags/__init__.py b/shop/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/shop/templatetags/shop_extras.py b/shop/templatetags/shop_extras.py
new file mode 100644
index 0000000..c6f6406
--- /dev/null
+++ b/shop/templatetags/shop_extras.py
@@ -0,0 +1,82 @@
+from django import template
+import os
+
+register = template.Library()
+
+@register.filter
+def color_image_url(product_image, color_name):
+ """
+ Returns color-specific image URL with any supported extension.
+ Falls back to the original image if no color variant exists.
+ """
+ if not product_image or not color_name:
+ return product_image
+
+ # Generate color suffix
+ suffix = generate_color_suffix(color_name)
+
+ # Split path
+ directory, filename = os.path.split(product_image)
+ base_name, original_ext = os.path.splitext(filename)
+
+ # List of supported image extensions to check
+ supported_extensions = ['.png.avif', '.jpg', '.jpeg', '.png', '.gif', '.webp', '.avif']
+
+ # Check for the color image with original extension first
+ color_filename = f"{suffix}_{base_name}{original_ext}"
+ color_image = os.path.join(directory, color_filename)
+
+ # Extract the path after /static/
+ static_prefix = '/static/'
+ if color_image.startswith(static_prefix):
+ rel_path = color_image[len(static_prefix):]
+ else:
+ rel_path = color_image.lstrip('/')
+
+ # Check if file with original extension exists
+ from django.conf import settings
+ app_static_path = os.path.join(settings.BASE_DIR, 'shop', 'static', rel_path)
+
+ if os.path.exists(app_static_path):
+ return color_image
+
+ # If not found with original extension, try other extensions
+ for ext in supported_extensions:
+ if ext == original_ext:
+ continue # Skip the original extension as we already checked it
+
+ color_filename = f"{suffix}_{base_name}{ext}"
+ color_image = os.path.join(directory, color_filename)
+
+ if color_image.startswith(static_prefix):
+ rel_path = color_image[len(static_prefix):]
+ else:
+ rel_path = color_image.lstrip('/')
+
+ app_static_path = os.path.join(settings.BASE_DIR, 'shop', 'static', rel_path)
+
+ if os.path.exists(app_static_path):
+ return color_image
+
+ # If no color variant is found with any extension, return the original image
+ return product_image
+
+def generate_color_suffix(color_name):
+ """
+ Generates a URL-friendly suffix from a color name
+ Example: "Noir / Gris Foncé Chiné" becomes "noir_gris_fonce_chine"
+ """
+ import unicodedata
+ import re
+
+ # Convert to lowercase and replace accents
+ value = color_name.lower()
+ value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
+
+ # Replace slashes and spaces with underscores
+ value = re.sub(r'[/\s]+', '_', value)
+
+ # Remove any remaining non-alphanumeric characters
+ value = re.sub(r'[^\w_]', '', value)
+
+ return value
diff --git a/shop/views.py b/shop/views.py
index dcf6089..8ddd755 100644
--- a/shop/views.py
+++ b/shop/views.py
@@ -50,15 +50,24 @@ def _create_stripe_checkout_session(request, order, line_items):
'order_id': str(order.id),
}
+ # Set up customer information
+ customer_email = None
+
# Add user info to metadata if available
if request.user.is_authenticated:
metadata['user_id'] = str(request.user.id)
+ customer_email = request.user.email
+ elif order.guest_user:
+ metadata['guest_email'] = order.guest_user.email
+ customer_email = order.guest_user.email
elif 'guest_email' in request.session:
metadata['guest_email'] = request.session.get('guest_email', '')
+ customer_email = request.session.get('guest_email', '')
try:
# Use the service to create the session
checkout_session = stripe_service.create_checkout_session(
+ customer_email=customer_email,
line_items=line_items,
success_url=success_url,
cancel_url=cancel_url,
@@ -114,8 +123,21 @@ def add_to_cart_view(request, product_id):
size_id = request.POST.get('size')
cart_item = cart.add_to_cart(request, product_id, quantity, color_id, size_id)
- messages.success(request, f'{cart_item.quantity} x {product.title} added to your cart')
+ message = f'{cart_item.quantity} x {product.title} dans le panier',
+
+ # Check if this is an AJAX request
+ if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
+ cart_items = cart.get_cart_items(request)
+ total = cart.get_cart_total(request)
+ return JsonResponse({
+ 'success': True,
+ 'message': message,
+ 'cart_total': total,
+ 'cart_count': cart_items.count()
+ })
+ # For non-AJAX requests, fall back to the original behavior
+ messages.success(request, message)
return redirect('shop:product_list')
def update_cart_view(request, product_id):
diff --git a/tournaments/models/match.py b/tournaments/models/match.py
index c4f7e67..bb23a2e 100644
--- a/tournaments/models/match.py
+++ b/tournaments/models/match.py
@@ -424,13 +424,16 @@ class Match(SideStoreModel):
time_indication = self.time_indication()
court = self.court_name(self.court_index)
group_stage_name = None
+ bracket_name = None
if self.group_stage:
group_stage_name = self.group_stage.display_name()
+ else:
+ bracket_name = f"Match n˚{self.index_in_round() + 1}"
ended = self.end_date is not None
live_format = "Format " + FederalMatchCategory(self.format).format_label_short
- livematch = LiveMatch(self.index, title, date, time_indication, court, self.started(), ended, group_stage_name, live_format, self.start_date, self.court_index, self.disabled)
+ livematch = LiveMatch(self.index, title, date, time_indication, court, self.started(), ended, group_stage_name, live_format, self.start_date, self.court_index, self.disabled, bracket_name)
for team in self.live_teams():
livematch.add_team(team)
@@ -491,7 +494,7 @@ class Team:
}
class LiveMatch:
- def __init__(self, index, title, date, time_indication, court, started, ended, group_stage_name, format, start_date, court_index, disabled):
+ def __init__(self, index, title, date, time_indication, court, started, ended, group_stage_name, format, start_date, court_index, disabled, bracket_name):
self.index = index
self.title = title
self.date = date
@@ -506,6 +509,7 @@ class LiveMatch:
self.disabled = disabled
self.start_date = start_date
self.court_index = court_index
+ self.bracket_name = bracket_name
def add_team(self, team):
self.teams.append(team)
@@ -526,7 +530,8 @@ class LiveMatch:
"group_stage_name": self.group_stage_name,
"format": self.format,
"disabled": self.disabled,
- "court_index": self.court_index
+ "court_index": self.court_index,
+ "bracket_name": self.bracket_name
}
def show_time_indication(self):
diff --git a/tournaments/static/rules/padel-guide-cdc.pdf b/tournaments/static/rules/padel-guide-cdc.pdf
new file mode 100644
index 0000000..f45e9da
Binary files /dev/null and b/tournaments/static/rules/padel-guide-cdc.pdf differ
diff --git a/tournaments/static/rules/padel-guide-general.pdf b/tournaments/static/rules/padel-guide-general.pdf
new file mode 100644
index 0000000..693575e
Binary files /dev/null and b/tournaments/static/rules/padel-guide-general.pdf differ
diff --git a/tournaments/static/rules/padel-guide-rankings.pdf b/tournaments/static/rules/padel-guide-rankings.pdf
new file mode 100644
index 0000000..4a5ffc9
Binary files /dev/null and b/tournaments/static/rules/padel-guide-rankings.pdf differ
diff --git a/tournaments/static/rules/padel-rules-2024.pdf b/tournaments/static/rules/padel-rules-2024.pdf
deleted file mode 100644
index 5465bd4..0000000
Binary files a/tournaments/static/rules/padel-rules-2024.pdf and /dev/null differ
diff --git a/tournaments/static/rules/padel-rules.pdf b/tournaments/static/rules/padel-rules.pdf
new file mode 100644
index 0000000..62e691b
Binary files /dev/null and b/tournaments/static/rules/padel-rules.pdf differ
diff --git a/tournaments/static/tournaments/css/style.css b/tournaments/static/tournaments/css/style.css
index 205c41c..b287a0b 100644
--- a/tournaments/static/tournaments/css/style.css
+++ b/tournaments/static/tournaments/css/style.css
@@ -902,6 +902,23 @@ h-margin {
border-radius: 0 0 24px 24px; /* Match the bubble's bottom corners */
}
+.status-container-bracket {
+ margin: 0px -20px -20px -20px; /* Negative margin to counter the bubble padding, including bottom */
+ padding: 10px 20px 20px 20px; /* Add padding back to maintain text alignment, including bottom */
+ border-radius: 0 0 24px 24px; /* Match the bubble's bottom corners */
+}
+
+.status-container-bracket-header {
+ height: 30px;
+ text-align: left;
+}
+
+.status-container-bracket-header-bottom {
+ height: 30px;
+ text-align: left;
+}
+
+.status-container-bracket.running,
.status-container.running {
background-color: #90ee90; /* Light green color */
}
diff --git a/tournaments/static/tournaments/css/tournament_bracket.css b/tournaments/static/tournaments/css/tournament_bracket.css
index 1be8f50..ed1a1a4 100644
--- a/tournaments/static/tournaments/css/tournament_bracket.css
+++ b/tournaments/static/tournaments/css/tournament_bracket.css
@@ -43,7 +43,7 @@
transform: translateX(-50%); /* Center it exactly */
text-align: center;
font-weight: bold;
- width: auto; /* Change from 100% to auto */
+ width: 100%; /* Change from 100% to auto */
padding: 5px 10px;
white-space: nowrap; /* Prevent text from wrapping */
@@ -53,6 +53,10 @@
justify-content: center;
}
+.round-title.broadcast-mode {
+ width: auto; /* Change from 100% to auto */
+}
+
.round-name {
color: #707070;
font-size: 1.5em;
diff --git a/tournaments/static/tournaments/js/tournament_bracket.js b/tournaments/static/tournaments/js/tournament_bracket.js
index 3f610f1..bbf058d 100644
--- a/tournaments/static/tournaments/js/tournament_bracket.js
+++ b/tournaments/static/tournaments/js/tournament_bracket.js
@@ -66,7 +66,9 @@ function renderBracket(options) {
// Create title
const titleDiv = document.createElement("div");
titleDiv.className = "round-title";
-
+ if (isBroadcast) {
+ titleDiv.className = "round-title broadcast-mode";
+ }
// Get the match group name and format
const firstMatchTemplate = roundMatches[0].closest(".match-template");
const matchGroupName = firstMatchTemplate.dataset.matchGroupName;
diff --git a/tournaments/templates/registration/signup.html b/tournaments/templates/registration/signup.html
index a6a083f..d541d72 100644
--- a/tournaments/templates/registration/signup.html
+++ b/tournaments/templates/registration/signup.html
@@ -36,10 +36,6 @@
-
- {% for message in messages %}
- {{ message }}
- {% endfor %}
diff --git a/tournaments/templates/tournaments/bracket_match_cell.html b/tournaments/templates/tournaments/bracket_match_cell.html
index 6df8cb2..33a3bbc 100644
--- a/tournaments/templates/tournaments/bracket_match_cell.html
+++ b/tournaments/templates/tournaments/bracket_match_cell.html
@@ -2,6 +2,17 @@
+
+
+
+
{% for team in match.teams %}
@@ -53,5 +64,14 @@
{% endfor %}
+
diff --git a/tournaments/views.py b/tournaments/views.py
index 1679a84..75424ab 100644
--- a/tournaments/views.py
+++ b/tournaments/views.py
@@ -111,7 +111,11 @@ def index(request):
thirty_days_future = now + timedelta(days=30)
club_id = request.GET.get('club')
- tournaments = tournaments_query(Q(end_date__isnull=True, start_date__gte=thirty_days_ago, start_date__lte=thirty_days_future), club_id, True, 50)
+ if club_id:
+ tournaments = tournaments_query(Q(end_date__isnull=True), club_id, True, 50)
+ else:
+ tournaments = tournaments_query(Q(end_date__isnull=True, start_date__gte=thirty_days_ago, start_date__lte=thirty_days_future), club_id, True, 50)
+
display_tournament = [t for t in tournaments if t.display_tournament()]
live = []
future = []