diff --git a/apps/blog/static/js/blog-comments.js b/apps/blog/static/js/blog-comments.js index e650d80..cfd00a1 100644 --- a/apps/blog/static/js/blog-comments.js +++ b/apps/blog/static/js/blog-comments.js @@ -174,14 +174,7 @@ function submitFormAjax(form, submitBtn, originalText) { form.classList.remove('form-submitting'); if (data.success) { - // Show success notification - if (window.notifications) { - if (data.user_authenticated) { - window.notifications.success(data.message, 5000); - } else { - window.notifications.info(data.message, 8000); - } - } + toast(data.message, data.user_authenticated ? "success" : "info"); // Clear form form.reset(); @@ -192,10 +185,7 @@ function submitFormAjax(form, submitBtn, originalText) { }, 1500); } else { - // Show error notification - if (window.notifications) { - window.notifications.error(data.message || 'An error occurred', 10000); - } + toast(data.message || 'An error occurred?', "error"); // Show field errors if any if (data.errors) { @@ -222,10 +212,7 @@ function submitFormAjax(form, submitBtn, originalText) { submitBtn.classList.remove('btn-loading'); form.classList.remove('form-submitting'); - // Show error notification - if (window.notifications) { - window.notifications.error('Network error. Please try again.', 10000); - } + toast('Network error. Please try again!', "error"); // Fallback to regular form submission setTimeout(() => { diff --git a/apps/blog/static/js/blog-dashboard.js b/apps/blog/static/js/blog-dashboard.js index 9c81ef4..c6b4321 100644 --- a/apps/blog/static/js/blog-dashboard.js +++ b/apps/blog/static/js/blog-dashboard.js @@ -2,11 +2,8 @@ document.addEventListener('DOMContentLoaded', function () { const pendingComments = document.querySelectorAll('[data-moderation-type="pending"] .comment-row'); - if (pendingComments.length > 0 && window.notifications) { - window.notifications.info( - `${pendingComments.length} comment(s) to check`, - 8000 - ); + if (pendingComments.length > 0) { + toast(`${pendingComments.length} comment(s) to check`, "info"); } document.addEventListener('click', function (e) { @@ -37,9 +34,7 @@ function handleModerationAction(button) { button.textContent = 'Processing...'; button.classList.add('btn-loading'); - if (window.notifications) { - window.notifications.info(`Processing ${action} action...`, 3000); - } + toast(`Processing ${action} action...`, "info"); fetch(`/blog/comment/${commentId}/${action}/`, { method: 'GET', @@ -50,16 +45,12 @@ function handleModerationAction(button) { .then(response => response.json()) .then(data => { if (!data.success) { - if (window.notifications) { - window.notifications.error(data.message || 'Action failed', 10000); - } + toast(data.message || 'Action failed', "error"); resetButton(button, originalText); return; } - if (window.notifications) { - window.notifications.success(data.message, 5000); - } + toast(data.message, "success"); const commentRow = document.querySelector(`[data-comment-id="${commentId}"]`); if (commentRow) { @@ -77,9 +68,7 @@ function handleModerationAction(button) { }) .catch(error => { console.error('Moderation action error:', error); - if (window.notifications) { - window.notifications.error('Network error. Please try again.', 10000); - } + toast('Network error. Please try again.', "error"); resetButton(button, originalText); }); } diff --git a/apps/blog/test_settings.py b/apps/blog/test_settings.py index 9371aa9..38e7621 100644 --- a/apps/blog/test_settings.py +++ b/apps/blog/test_settings.py @@ -69,7 +69,6 @@ def __getitem__(self, item): "django.contrib.staticfiles", "apps.blog", "apps.users", - "apps.notifications", ] # Test user model diff --git a/apps/blog/views.py b/apps/blog/views.py index 4a04418..6e8d1ff 100644 --- a/apps/blog/views.py +++ b/apps/blog/views.py @@ -26,11 +26,6 @@ from .uploads import clean_upload_filename from .attachments import render_attachments from .forms import BlogPostForm, BlogPostCreateForm, TagForm, CommentForm -from apps.notifications.utils import ( - show_success_notification, - show_error_notification, - show_info_notification, -) class BlogListView(ListView): @@ -123,138 +118,87 @@ def build_comment_tree(self, comments): return root_comments +def _reply(request, redirect_url, success, message, **extra): + """JSON for AJAX requests, otherwise a Django message + redirect.""" + if request.headers.get("X-Requested-With") == "XMLHttpRequest": + return JsonResponse({"success": success, "message": message, **extra}) + (messages.success if success else messages.error)(request, message) + return redirect(redirect_url) + + @require_POST def submit_comment(request, post_id): post = get_object_or_404(BlogPost, id=post_id) + url = post.get_absolute_url() - # Check if comments are allowed (you could add a field to BlogPost for this) if post.status != "published": - if request.headers.get("X-Requested-With") == "XMLHttpRequest": - return JsonResponse( - { - "success": False, - "message": "Comments are only allowed on published posts.", - } - ) - show_error_notification( - request, "Comments are only allowed on published posts." + return _reply( + request, + url, + False, + "I dont have a way yet for you to comment on unpublished posts!", ) - return redirect(post.get_absolute_url()) form = CommentForm(request.POST, user=request.user, post=post, request=request) if form.is_valid(): comment = form.save() - - # Prepare response data - response_data = { - "success": True, - "user_authenticated": request.user.is_authenticated, - "comment_id": comment.id, - } - if request.user.is_authenticated: - response_data["message"] = "Comment posted." - show_success_notification(request, "Comment posted.") - else: - response_data["message"] = "Got it. I'll check it before it shows up." - show_success_notification( - request, "Got it. I'll check it before it shows up." - ) - - # Return JSON for AJAX requests, redirect for regular requests - if request.headers.get("X-Requested-With") == "XMLHttpRequest": - return JsonResponse(response_data) + message = "Comment posted." else: - return redirect(post.get_absolute_url()) - else: - # Store form errors in messages with better context - if form.errors: - error_message = "Check the comment and try again." - - # Prepare error response data - response_data = { - "success": False, - "message": error_message, - "errors": form.errors, - } - - # Add specific field errors - for field, errors in form.errors.items(): - field_name = field.replace("_", " ").title() - for error in errors: - if field == "content": - show_error_notification(request, f"Comment: {error}") - elif field == "author_name": - show_error_notification(request, f"Name: {error}") - elif field == "author_email": - show_error_notification(request, f"Email: {error}") - elif field == "author_website": - show_error_notification(request, f"Website: {error}") - else: - show_error_notification(request, f"{field_name}: {error}") - else: - error_message = "Something went wrong. Try again." - response_data = {"success": False, "message": error_message} - show_error_notification(request, "Something went wrong. Try again.") + message = "Got it. I'll check it before it shows up." + return _reply( + request, + url, + True, + message, + user_authenticated=request.user.is_authenticated, + comment_id=comment.id, + ) - # Return JSON for AJAX requests, redirect for regular requests - if request.headers.get("X-Requested-With") == "XMLHttpRequest": - return JsonResponse(response_data) - else: - return redirect(post.get_absolute_url()) + # If its non ajax, one message per, otherwise... + if request.headers.get("X-Requested-With") != "XMLHttpRequest": + labels = { + "content": "Comment", + "author_name": "Name", + "author_email": "Email", + "author_website": "Website", + } + for field, errors in form.errors.items(): + label = labels.get(field, field.replace("_", " ").title()) + for error in errors: + messages.error(request, f"{label}: {error}") + return redirect(url) + + return _reply( + request, url, False, "Check the comment and try again!", errors=form.errors + ) @login_required def moderate_comment(request, comment_id, action): comment = get_object_or_404(Comment, id=comment_id) + url = comment.post.get_absolute_url() - # Check if user has permission to moderate if not (request.user.is_staff or request.user == comment.post.author): - if request.headers.get("X-Requested-With") == "XMLHttpRequest": - return JsonResponse( - { - "success": False, - "message": "You don't have permission to moderate comments.", - } - ) - show_error_notification( - request, "You don't have permission to moderate comments." + return _reply( + request, url, False, "You don't have permission to moderate comments." ) - return redirect(comment.post.get_absolute_url()) - - # Prepare response data - response_data = {"success": True} - - if action == "approve": - comment.status = "approved" - response_data["message"] = "Comment approved." - show_success_notification(request, "Comment approved.") - elif action == "reject": - comment.status = "rejected" - response_data["message"] = "Comment rejected." - show_success_notification(request, "Comment rejected.") - elif action == "spam": - comment.status = "spam" - response_data["message"] = "Comment marked as spam." - show_success_notification(request, "Comment marked as spam.") - else: - response_data = {"success": False, "message": "Invalid moderation action."} - if request.headers.get("X-Requested-With") == "XMLHttpRequest": - return JsonResponse(response_data) - show_error_notification(request, "Invalid moderation action.") - return redirect(comment.post.get_absolute_url()) - - # Set moderation metadata + + results = { + "approve": ("approved", "Comment approved."), + "reject": ("rejected", "Comment rejected."), + "spam": ("spam", "Comment marked as spam."), + } + if action not in results: + return _reply(request, url, False, "Invalid moderation action.") + + comment.status, message = results[action] comment.moderated_at = timezone.now() comment.moderated_by = request.user comment.save() - # Return JSON for AJAX requests, redirect for regular requests - if request.headers.get("X-Requested-With") == "XMLHttpRequest": - return JsonResponse(response_data) - else: - return redirect(comment.post.get_absolute_url()) + return _reply(request, url, True, message) class BlogEditorMixin: @@ -274,7 +218,7 @@ def form_valid(self, form): form.instance.author = self.request.user response = super().form_valid(form) - show_success_notification(self.request, "Blog post created successfully!") + messages.success(self.request, "Blog post created successfully!") return response def get_form_kwargs(self): @@ -296,7 +240,7 @@ def test_func(self): def form_valid(self, form): response = super().form_valid(form) - show_success_notification(self.request, "Blog post updated successfully!") + messages.success(self.request, "Blog post updated successfully!") return response def get_form_kwargs(self): @@ -315,7 +259,7 @@ def test_func(self): return self.request.user == post.author or self.request.user.is_staff def delete(self, request, *args, **kwargs): - show_success_notification(request, "Blog post deleted successfully!") + messages.success(request, "Blog post deleted successfully!") return super().delete(request, *args, **kwargs) diff --git a/apps/notifications/README.md b/apps/notifications/README.md deleted file mode 100644 index 88cd71f..0000000 --- a/apps/notifications/README.md +++ /dev/null @@ -1,183 +0,0 @@ -# Centralized Notification System - -A beautiful, consistent notification system that any Django app can use to show user feedback. - -## Features - -- đ¨ **Beautiful Design**: Gradient backgrounds, smooth animations, responsive layout -- đ§ **Easy to Use**: Simple API for both Python and JavaScript -- đą **Mobile Friendly**: Responsive design that works on all devices -- ⥠**Auto-hide**: Smart timing for different notification types -- đ¯ **Centralized**: Consistent styling and behavior across the entire site - -## Quick Start - -### 1. JavaScript Usage (Any Template) - -```javascript -// Show different types of notifications -window.notifications.success("Your comment was posted!"); -window.notifications.error("Something went wrong!"); -window.notifications.warning("Please check your input"); -window.notifications.info("Processing your request..."); - -// Custom duration (in milliseconds) -window.notifications.success("Quick success!", 3000); - -// Custom options -window.notifications.show("Custom message", "info", 5000, { - className: "my-custom-notification" -}); -``` - -### 2. Django Template Tags - -```html -{% load notification_tags %} - - -{% show_success_notification "Comment posted successfully!" %} -{% show_error_notification "Please fix the errors below" %} -{% show_warning_notification "Your session expires soon" %} -{% show_info_notification "Processing your request..." %} - - -{% show_notification "Quick message" "success" 3000 %} -``` - -### 3. Python Views - -```python -from apps.notifications.utils import show_success_notification, show_error_notification - -def my_view(request): - if form.is_valid(): - form.save() - show_success_notification(request, "Data saved successfully!") - else: - show_error_notification(request, "Please fix the errors below") - - return render(request, "template.html") -``` - -## Notification Types - -| Type | Default Duration | Use Case | -|------|------------------|----------| -| `success` | 5 seconds | Confirmations, successful actions | -| `error` | 15 seconds | Errors, validation failures | -| `warning` | 10 seconds | Warnings, important notices | -| `info` | 8 seconds | General information, tips | - -## API Reference - -### JavaScript API - -```javascript -// Main methods -window.notifications.show(message, type, duration, options) -window.notifications.success(message, duration, options) -window.notifications.error(message, duration, options) -window.notifications.warning(message, duration, options) -window.notifications.info(message, duration, options) - -// Utility methods -window.notifications.hide(notification) // Hide specific notification -window.notifications.hideAll() // Hide all notifications -``` - -### Django Template Tags - -```html -{% show_notification message type duration %} -{% show_success_notification message duration %} -{% show_error_notification message duration %} -{% show_warning_notification message duration %} -{% show_info_notification message duration %} -``` - -### Python Utils - -```python -from apps.notifications.utils import ( - show_notification, - show_success_notification, - show_error_notification, - show_warning_notification, - show_info_notification -) - -# All functions take: (request, message, duration=default) -show_success_notification(request, "Success!") -show_error_notification(request, "Error occurred", 20000) -``` - -## Examples - -### Blog Comment Success - -```python -# In your view -if comment_form.is_valid(): - comment = comment_form.save() - if request.user.is_authenticated: - show_success_notification(request, "Comment posted successfully!") - else: - show_info_notification(request, "Comment submitted for moderation") -``` - -### Form Validation Errors - -```python -# In your view -if not form.is_valid(): - show_error_notification(request, "Please fix the errors below") - for field, errors in form.errors.items(): - for error in errors: - show_error_notification(request, f"{field}: {error}") -``` - -### User Action Confirmation - -```javascript -// In your JavaScript -function deleteItem() { - if (confirm("Are you sure?")) { - // Delete logic here - window.notifications.success("Item deleted successfully!"); - } -} -``` - -## Styling - -The notification system uses CSS custom properties for easy theming: - -```css -:root { - --success-color: #28a745; - --error-color: #dc3545; - --warning-color: #ffc107; - --info-color: #17a2b8; -} -``` - -## Browser Support - -- Modern browsers (Chrome, Firefox, Safari, Edge) -- Mobile browsers (iOS Safari, Chrome Mobile) -- Graceful fallback for older browsers - -## Contributing - -To add new notification types or features: - -1. Update `notifications.js` with new functionality -2. Add corresponding CSS classes -3. Update template tags and Python utils -4. Test across different devices and browsers -5. Update this README - -## License - -This notification system is part of the Snowsune site and follows the same license terms. \ No newline at end of file diff --git a/apps/notifications/context_processors.py b/apps/notifications/context_processors.py deleted file mode 100644 index b377f6d..0000000 --- a/apps/notifications/context_processors.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Context processor for notifications -Makes notifications available in all templates -""" - - -def notifications_processor(request): - """ - Add notifications to template context - - This allows any template to access notifications from the request - """ - notifications = getattr(request, "notifications", []) - - return {"notifications": notifications, "has_notifications": len(notifications) > 0} diff --git a/apps/notifications/static/notifications/js/notifications.js b/apps/notifications/static/notifications/js/notifications.js deleted file mode 100644 index d9c33f3..0000000 --- a/apps/notifications/static/notifications/js/notifications.js +++ /dev/null @@ -1,237 +0,0 @@ -/** - * Centralized Notification System - * Provides a consistent way for all apps to show notifications - */ - -class NotificationSystem { - constructor() { - this.container = null; - this.init(); - } - - init() { - // Create notification container if it doesn't exist - this.createContainer(); - - // Set up auto-hide for existing messages - this.setupAutoHide(); - } - - createContainer() { - // Check if container already exists - this.container = document.querySelector('.notifications-container'); - - if (!this.container) { - this.container = document.createElement('div'); - this.container.className = 'notifications-container'; - document.body.appendChild(this.container); - } - } - - /** - * Show a notification - * @param {string} message - The message to display - * @param {string} type - 'success', 'error', 'warning', 'info' - * @param {number} duration - Auto-hide duration in milliseconds (0 = no auto-hide) - * @param {Object} options - Additional options - */ - show(message, type = 'info', duration = 0, options = {}) { - const notification = this.createNotification(message, type, options); - - // Add to container - this.container.appendChild(notification); - - // Trigger entrance animation - setTimeout(() => { - notification.classList.add('show'); - }, 10); - - // Auto-hide if duration is set - if (duration > 0) { - setTimeout(() => { - this.hide(notification); - }, duration); - } - - return notification; - } - - /** - * Show a success notification - */ - success(message, duration = 5000, options = {}) { - return this.show(message, 'success', duration, options); - } - - /** - * Show an error notification - */ - error(message, duration = 15000, options = {}) { - return this.show(message, 'error', duration, options); - } - - /** - * Show a warning notification - */ - warning(message, duration = 10000, options = {}) { - return this.show(message, 'warning', duration, options); - } - - /** - * Show an info notification - */ - info(message, duration = 8000, options = {}) { - return this.show(message, 'info', duration, options); - } - - /** - * Hide a specific notification - */ - hide(notification) { - notification.classList.add('hiding'); - setTimeout(() => { - if (notification.parentNode) { - notification.parentNode.removeChild(notification); - } - }, 300); - } - - /** - * Hide all notifications - */ - hideAll() { - const notifications = this.container.querySelectorAll('.notification'); - notifications.forEach(notification => this.hide(notification)); - } - - /** - * Create notification element - */ - createNotification(message, type, options = {}) { - const notification = document.createElement('div'); - notification.className = `notification notification-${type}`; - notification.setAttribute('role', 'alert'); - - // Add custom classes if provided - if (options.className) { - notification.classList.add(options.className); - } - - // Create notification content - const content = document.createElement('div'); - content.className = 'notification-content'; - - // Icon - const icon = document.createElement('span'); - icon.className = 'notification-icon'; - const stickerSrc = this.getStickerSrc(type); - if (stickerSrc) { - const img = document.createElement('img'); - img.className = 'notification-sticker'; - img.src = stickerSrc; - img.alt = ''; - img.decoding = 'async'; - icon.appendChild(img); - } else { - icon.innerHTML = this.getIcon(type); - } - - // Message - const messageSpan = document.createElement('span'); - messageSpan.className = 'notification-text'; - messageSpan.textContent = message; - - // Close button - const closeBtn = document.createElement('button'); - closeBtn.className = 'notification-close'; - closeBtn.innerHTML = 'Ã'; - closeBtn.setAttribute('aria-label', 'Close notification'); - closeBtn.onclick = () => this.hide(notification); - - // Assemble notification - content.appendChild(icon); - content.appendChild(messageSpan); - content.appendChild(closeBtn); - notification.appendChild(content); - - return notification; - } - - /** - * Static sticker URLs (matches {% static 'stickers/âĻ' %} with STATIC_URL /static/) - */ - getStickerSrc(type) { - const base = '/static/stickers/'; - if (type === 'success') { - return `${base}bep_bounce.gif`; - } - if (type === 'error') { - return `${base}foxi-sticker-ERROR.png`; - } - return null; - } - - /** - * Fallback emoji icon for notification types without stickers - */ - getIcon(type) { - const icons = { - success: 'â ', - error: 'â', - warning: 'â ī¸', - info: 'âšī¸' - }; - return icons[type] || 'đŦ'; - } - - /** - * Set up auto-hide for existing Django messages - */ - setupAutoHide() { - const messages = document.querySelectorAll('.message'); - messages.forEach(message => { - const type = this.getMessageType(message); - const duration = this.getAutoHideDuration(type); - - if (duration > 0) { - setTimeout(() => { - message.style.animation = 'slideOutRight 0.3s ease-in forwards'; - setTimeout(() => message.remove(), 300); - }, duration); - } - }); - } - - /** - * Get message type from Django message classes - */ - getMessageType(message) { - if (message.classList.contains('message-success')) return 'success'; - if (message.classList.contains('message-error')) return 'error'; - if (message.classList.contains('message-warning')) return 'warning'; - if (message.classList.contains('message-info')) return 'info'; - if (message.classList.contains('message-debug')) return 'info'; - return 'info'; - } - - /** - * Get auto-hide duration for message type - */ - getAutoHideDuration(type) { - const durations = { - success: 5000, - info: 8000, - warning: 10000, - error: 15000 - }; - return durations[type] || 8000; - } -} - -// Create global notification system instance -window.notifications = new NotificationSystem(); - -// Export for module systems -if (typeof module !== 'undefined' && module.exports) { - module.exports = NotificationSystem; -} \ No newline at end of file diff --git a/apps/notifications/templatetags/__init__.py b/apps/notifications/templatetags/__init__.py deleted file mode 100644 index 1c678fc..0000000 --- a/apps/notifications/templatetags/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Django template tags for notifications diff --git a/apps/notifications/templatetags/notification_tags.py b/apps/notifications/templatetags/notification_tags.py deleted file mode 100644 index dbd9d10..0000000 --- a/apps/notifications/templatetags/notification_tags.py +++ /dev/null @@ -1,56 +0,0 @@ -from django import template -from django.utils.safestring import mark_safe - -register = template.Library() - - -@register.simple_tag -def show_notification(message, type="info", duration=0): - """ - Show a notification from a Django template - - Usage: - {% show_notification "Your comment was posted!" "success" 5000 %} - {% show_notification "Something went wrong" "error" %} - """ - if duration == 0: - duration = get_default_duration(type) - - script = f""" - - """ - return mark_safe(script) - - -@register.simple_tag -def show_success_notification(message, duration=5000): - """Show a success notification""" - return show_notification(message, "success", duration) - - -@register.simple_tag -def show_error_notification(message, duration=15000): - """Show an error notification""" - return show_notification(message, "error", duration) - - -@register.simple_tag -def show_warning_notification(message, duration=10000): - """Show a warning notification""" - return show_notification(message, "warning", duration) - - -@register.simple_tag -def show_info_notification(message, duration=8000): - """Show an info notification""" - return show_notification(message, "info", duration) - - -def get_default_duration(type): - """Get default duration for notification type""" - durations = {"success": 5000, "error": 15000, "warning": 10000, "info": 8000} - return durations.get(type, 8000) diff --git a/apps/notifications/utils.py b/apps/notifications/utils.py deleted file mode 100644 index f1ba2a2..0000000 --- a/apps/notifications/utils.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Notification utilities for Django apps -Provides easy ways to show notifications from Python code -""" - - -def show_notification(request, message, type="info", duration=0): - """ - Show a notification by adding it to the request session - - Usage: - from apps.notifications.utils import show_notification - - def my_view(request): - show_notification(request, "Success!", "success") - return render(request, "template.html") - """ - if not hasattr(request, "notifications"): - request.notifications = [] - - if duration == 0: - duration = get_default_duration(type) - - notification = {"message": message, "type": type, "duration": duration} - - request.notifications.append(notification) - - -def show_success_notification(request, message, duration=5000): - """Show a success notification""" - show_notification(request, message, "success", duration) - - -def show_error_notification(request, message, duration=15000): - """Show an error notification""" - show_notification(request, message, "error", duration) - - -def show_warning_notification(request, message, duration=10000): - """Show a warning notification""" - show_notification(request, message, "warning", duration) - - -def show_info_notification(request, message, duration=8000): - """Show an info notification""" - show_notification(request, message, "info", duration) - - -def get_default_duration(type): - """Get default duration for notification type""" - durations = {"success": 5000, "error": 15000, "warning": 10000, "info": 8000} - return durations.get(type, 8000) diff --git a/snowsune/settings.py b/snowsune/settings.py index d0e325e..79d9889 100644 --- a/snowsune/settings.py +++ b/snowsune/settings.py @@ -76,7 +76,6 @@ "apps.comics", "apps.characters", "apps.commorganizer", # Commission Organizer app - "apps.notifications", # Centralized notification system "apps.thank_yous", # Thank yous management app "apps.quotes", # Quotes scroller app "apps.bot_manager", # Fops Bot management app @@ -127,7 +126,6 @@ "snowsune.context_processors.ko_fi_url", "snowsune.context_processors.google_analytics_id", "snowsune.context_processors.seasonal_css", - "apps.notifications.context_processors.notifications_processor", ], }, }, diff --git a/static/css/base.css b/static/css/base.css index 477e089..23ac11c 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -955,151 +955,12 @@ a:visited { } } -/* Centralized Notification System Styling */ -.notifications-container { - position: fixed; - top: 20px; - right: 20px; - z-index: 1000; - max-width: 400px; - width: 100%; -} - -.notification { - margin-bottom: 10px; - border-radius: var(--radius-lg); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - animation: slideInRight 0.3s ease-out; - overflow: hidden; - transform: translateX(100%); - opacity: 0; - transition: all 0.3s ease; -} - -.notification.show { - transform: translateX(0); - opacity: 1; -} - -.notification.hiding { - transform: translateX(100%); - opacity: 0; -} - -.notification-content { - display: flex; - align-items: center; - padding: 12px 16px; - gap: 12px; -} - -.notification-icon { - font-size: 1.2em; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; -} - -.notification-sticker { - width: 44px; - height: 44px; - object-fit: contain; - display: block; - flex-shrink: 0; -} - -.notification-text { - flex: 1; - font-size: 0.95em; - line-height: 1.4; -} - -.notification-close { - background: none; - border: none; - font-size: 1.5em; - cursor: pointer; - padding: 0; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-lg); - transition: background-color 0.2s ease; - flex-shrink: 0; -} - -.notification-close:hover { - background-color: rgba(0, 0, 0, 0.1); -} - -/* Notification Types */ -.notification-success { - background: linear-gradient(135deg, #28a745, #20c997); - color: white; -} - -.notification-error { - background: linear-gradient(135deg, #dc3545, #e83e8c); - color: white; -} - -.notification-warning { - background: linear-gradient(135deg, #ffc107, #fd7e14); - color: #212529; -} - -.notification-info { - background: linear-gradient(135deg, #17a2b8, #6f42c1); - color: white; -} - -/* Notification Animations */ -@keyframes slideInRight { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -@keyframes slideOutRight { - from { - transform: translateX(0); - opacity: 1; - } - to { - transform: translateX(100%); - opacity: 0; - } -} - -/* Responsive Notifications */ -@media (max-width: 768px) { - .notifications-container { - left: 20px; - right: 20px; - max-width: none; - } - - .notification { - margin-bottom: 8px; - } - - .notification-content { - padding: 10px 14px; - gap: 10px; - } - - .notification-text { - font-size: 0.9em; - } -} +/* Toast popups (Toastify, see base.html) */ +.toastify { border-radius: var(--radius-lg); } +.toastify-avatar { width: 44px; height: 44px; object-fit: contain; } +.toast-success { background: linear-gradient(135deg, #28a745, #20c997); } +.toast-error { background: linear-gradient(135deg, #dc3545, #e83e8c); } +.toast-warning { background: linear-gradient(135deg, #ffc107, #fd7e14); color: #212529; } /* Terms of Service page */ .tos-page { diff --git a/templates/base.html b/templates/base.html index be30b27..a629edb 100644 --- a/templates/base.html +++ b/templates/base.html @@ -76,6 +76,7 @@ + {% if seasonal_css %} @@ -180,8 +181,6 @@ title="Matrix join widget" loading="lazy" scrolling="no"> - {% include "includes/django_messages.html" %} - {% if user.is_authenticated and not user.email_verified %}
- + + + diff --git a/templates/includes/django_messages.html b/templates/includes/django_messages.html deleted file mode 100644 index bd06f1c..0000000 --- a/templates/includes/django_messages.html +++ /dev/null @@ -1,10 +0,0 @@ -{% if messages %} -{# Consumed by base.html â notifications.js as toasts; kept off-screen until then #} - -{% endif %}