From 1ec40061828e90138f94a82f1967ff994ef595c5 Mon Sep 17 00:00:00 2001 From: Ahmed Nassar Date: Fri, 14 Feb 2025 08:46:05 +0200 Subject: [PATCH] feat: Implement particles.js background, optimize image loading, and add admin customization styles --- .hintrc | 15 + apps/stations/admin.py | 144 +++++--- apps/stations/models.py | 6 + egypt_metro/views.py | 140 +++++-- static/css/admin.css | 21 ++ static/css/animations.css | 135 +++++-- static/css/style.css | 347 +++++------------- .../images/{metro-logo.png => metro-map.png} | Bin static/js/animations.js | 36 ++ static/js/main.js | 53 ++- static/js/particles-config.js | 94 +++++ templates/base.html | 40 +- templates/home.html | 3 + templates/includes/footer.html | 65 ++-- templates/includes/header.html | 77 ++-- templates/includes/metro_map.html | 34 -- 16 files changed, 680 insertions(+), 530 deletions(-) create mode 100644 .hintrc create mode 100644 static/css/admin.css rename static/images/{metro-logo.png => metro-map.png} (100%) create mode 100644 static/js/animations.js create mode 100644 static/js/particles-config.js delete mode 100644 templates/includes/metro_map.html diff --git a/.hintrc b/.hintrc new file mode 100644 index 0000000..272b46c --- /dev/null +++ b/.hintrc @@ -0,0 +1,15 @@ +{ + "extends": [ + "development" + ], + "hints": { + "compat-api/css": [ + "default", + { + "ignore": [ + "-webkit-backdrop-filter" + ] + } + ] + } +} \ No newline at end of file diff --git a/apps/stations/admin.py b/apps/stations/admin.py index f53f8b8..40b11f7 100644 --- a/apps/stations/admin.py +++ b/apps/stations/admin.py @@ -1,12 +1,14 @@ # apps/stations/admin.py +from venv import logger from django.contrib import admin from django.utils.html import format_html from django.db.models import Count +import markupsafe + +from egypt_metro import settings from .models import Line, Station, LineStation -from apps.stations.management.commands.populate_metro_data import ( - Command as MetroDataCommand, -) +from apps.stations.management.commands.populate_metro_data import Command as MetroDataCommand # Get constants from MetroDataCommand LINE_OPERATIONS = MetroDataCommand.LINE_OPERATIONS @@ -90,7 +92,7 @@ class StationAdmin(admin.ModelAdmin): list_display = [ "name", "get_lines_display", - "is_interchange_display", + "is_interchange_display", # Use the modified method "get_coordinates", "get_connections", ] @@ -106,54 +108,83 @@ class StationAdmin(admin.ModelAdmin): ("Location", {"fields": ("latitude", "longitude"), "classes": ("collapse",)}), ) + def is_interchange_display(self, obj): + """Display interchange status""" + return obj.lines.count() > 1 + is_interchange_display.boolean = True + is_interchange_display.short_description = "Interchange" + def get_lines_display(self, obj): - """Display lines with their colors""" - return format_html( - " ".join( - f'' - f"{line.name}" - for line in obj.lines.all() - ) - ) + """Display lines without colors""" + return ", ".join([line.name for line in obj.lines.all()]) get_lines_display.short_description = "Lines" - def is_interchange_display(self, obj): - """Display interchange status with icon""" - is_interchange = obj.lines.count() > 1 - icon = "✓" if is_interchange else "✗" - color = "green" if is_interchange else "red" - return format_html(f'{icon}') + def is_interchange_status(self, obj): + """ + Custom method to display interchange status with a more robust approach + """ + try: + # Determine interchange status + is_interchange = obj.lines.count() > 1 + + # Use Django's built-in boolean icon method + return markupsafe( + '{}'.format( + f'/static/admin/img/icon-{"yes" if is_interchange else "no"}.svg', + 'Yes' if is_interchange else 'No' + ) + ) + except Exception as e: + if settings.DEBUG: + print(f"Error in is_interchange_status: {e}") + return markupsafe('Error') - is_interchange_display.short_description = "Interchange" - is_interchange_display.boolean = True + is_interchange_status.short_description = "Interchange" + is_interchange_status.boolean = True def get_coordinates(self, obj): """Display coordinates with link to map""" - if obj.latitude and obj.longitude: - return format_html( - '' - "{:.6f}, {:.6f}", - obj.latitude, - obj.longitude, - obj.latitude, - obj.longitude, - ) - return "N/A" + try: + # Check if coordinates exist and are valid + if (obj.latitude is not None and obj.longitude is not None and isinstance(obj.latitude, (int, float)) and isinstance(obj.longitude, (int, float))): + + # Safely convert to float and round + lat = round(float(obj.latitude), 6) + lon = round(float(obj.longitude), 6) + + # Validate coordinate ranges + if -90 <= lat <= 90 and -180 <= lon <= 180: + return format_html( + '{}, {}', + lat, lon, lat, lon + ) + + logger.warning(f"Invalid coordinates for station {obj.name}: Lat {obj.latitude}, Lon {obj.longitude}") + return "N/A" + except Exception as e: + # Log the full error + logger.error(f"Coordinate error for station {obj.name}: {e}", exc_info=True) + return "Invalid Coordinates" get_coordinates.short_description = "Location" def get_connections(self, obj): """Display connecting stations""" - if not obj.is_interchange(): - return "-" - connections = [] - for conn in CONNECTING_STATIONS: - if conn["name"] == obj.name: - connections.extend(conn["lines"]) - return " ↔ ".join(connections) if connections else "-" - + try: + if not obj.is_interchange(): + return "-" + + connections = [] + for conn in CONNECTING_STATIONS: + if conn["name"] == obj.name: + connections.extend(conn["lines"]) + + return " ↔ ".join(connections) if connections else "-" + except Exception as e: + if settings.DEBUG: + print(f"Error in get_connections: {e}") + return "Error" get_connections.short_description = "Connections" @@ -174,21 +205,28 @@ class LineStationAdmin(admin.ModelAdmin): def get_next_station(self, obj): """Display next station in sequence""" - next_station = LineStation.objects.filter( - line=obj.line, order=obj.order + 1 - ).first() - return next_station.station.name if next_station else "-" - - get_next_station.short_description = "Next Station" + try: + next_station = LineStation.objects.filter( + line=obj.line, order=obj.order + 1 + ).first() + return next_station.station.name if next_station else "-" + except Exception as e: + if settings.DEBUG: + print(f"Error in get_next_station: {e}") + return "Error" def get_distance_to_next(self, obj): """Display distance to next station""" - next_station = LineStation.objects.filter( - line=obj.line, order=obj.order + 1 - ).first() - if next_station: - distance = obj.station.distance_to(next_station.station) - return f"{distance / 1000:.2f} km" - return "-" - + try: + next_station = LineStation.objects.filter( + line=obj.line, order=obj.order + 1 + ).first() + if next_station: + distance = obj.station.distance_to(next_station.station) + return f"{distance / 1000:.2f} km" + return "-" + except Exception as e: + if settings.DEBUG: + print(f"Error in get_distance_to_next: {e}") + return "Error" get_distance_to_next.short_description = "Distance to Next" diff --git a/apps/stations/models.py b/apps/stations/models.py index 27f7589..e601dd6 100644 --- a/apps/stations/models.py +++ b/apps/stations/models.py @@ -94,6 +94,12 @@ def is_interchange(self) -> bool: """Checks if this is an interchange station.""" return self.lines.count() > 1 + def is_interchange_display(self, obj): + """Display interchange status""" + return obj.is_interchange + is_interchange_display.boolean = True + is_interchange_display.short_description = "Interchange" + def get_nearest_interchange( self, target_line: Line ) -> Optional[Tuple["Station", float]]: diff --git a/egypt_metro/views.py b/egypt_metro/views.py index 8726b7e..6a5789e 100644 --- a/egypt_metro/views.py +++ b/egypt_metro/views.py @@ -1,15 +1,13 @@ # egypt_metro/views.py import logging -from django.http import JsonResponse -from django.shortcuts import render +import os +from django.http import HttpResponse, JsonResponse from django.db import connection # from django.utils.timezone import now from datetime import datetime from django.views.decorators.csrf import csrf_exempt -from egypt_metro import settings - logger = logging.getLogger(__name__) # Define the API's start time globally (when the server starts) @@ -26,35 +24,113 @@ def log_session_data(request): @csrf_exempt def home(request): - context = { - 'version': '1.0.0', - 'environment': settings.ENVIRONMENT, - 'current_time': datetime.now().strftime('%d/%m/%Y %I:%M:%S %p'), - 'metro_lines': [ - { - 'name': 'First Line', - 'color': '#FF0000', - 'stations': 35, - 'start': 'Helwan', - 'end': 'New El-Marg' - }, - { - 'name': 'Second Line', - 'color': '#008000', - 'stations': 20, - 'start': 'El-Mounib', - 'end': 'Shubra El-Kheima' - }, - { - 'name': 'Third Line', - 'color': '#0000FF', - 'stations': 34, - 'start': 'Adly Mansour', - 'end': 'Rod al-Farag Axis' - } - ] + """ + Home endpoint that provides an overview of the API. + Includes links to key features like admin panel, documentation, health checks, and API routes. + """ + + # Get the current time + current_time = datetime.utcnow() + + # Format the current time with minutes and seconds + current_date_time = current_time.strftime("%d/%m/%Y %I:%M:%S %p") + + # Calculate uptime dynamically + uptime_delta = current_time - API_START_TIME + days, remainder = divmod(uptime_delta.total_seconds(), 86400) # 86400 seconds in a day + hours, remainder = divmod(remainder, 3600) # 3600 seconds in an hour + minutes, seconds = divmod(remainder, 60) # 60 seconds in a minute + + # Get the current environment (dev or prod) + environment = os.getenv("ENVIRONMENT", "dev") # Default to dev if not set + logger.debug(f"Current environment: {environment}") + + # Data to return as JSON response + data = { + "admin_panel": "/admin/", # Link to Django admin panel + "api_documentation": "/docs/", # Link to API documentation + "health_check": "/health/", # Health check endpoint + "swagger": "/swagger/", # Swagger API documentation + "redoc": "/redoc/", # Redoc API documentation + "version": "1.0.0", # Backend version + "current_date_time": current_date_time, # Current date and time with minutes and seconds + "environment": environment, # Current environment (dev or prod) + "api_routes": { + "users": "/api/users/", # User-related routes + "register": "/api/users/register/", # User registration + "login": "/api/users/login/", # User login + "profile": "/api/users/profile/", # User profile + "update_profile": "/api/users/profile/update/", # Update profile + "token_refresh": "/api/users/token/refresh/", # Refresh token + "stations": "/api/stations/", # Stations-related routes + "stations_list": "/api/stations/list/", # List stations + "trip_details": "/api/stations/trip///", # Trip details + "nearest_station": "/api/stations/nearest/", # Nearest station + "precomputed_route": "/api/routes/route///", # Precomputed route + "shortest_route": "/api/routes/shortest/", # Shortest route + }, } - return render(request, 'home.html', context) + + # Check if browser or API client + if "text/html" in request.META.get("HTTP_ACCEPT", ""): + html_content = f""" + + + Egypt Metro API + + + +

Welcome to Egypt Metro Backend

+

Version: {data['version']}

+

Date & Time: {data['current_date_time']}

+

Environment: {data['environment']}

+

Quick Links

+ +

API Routes

+
    + """ + for name, path in data["api_routes"].items(): + html_content += f"
  • {name}
  • " + html_content += "
" + + return HttpResponse(html_content) + + # Return the JSON response with status code 200 + return JsonResponse(data, status=200) def health_check(request): diff --git a/static/css/admin.css b/static/css/admin.css new file mode 100644 index 0000000..619f073 --- /dev/null +++ b/static/css/admin.css @@ -0,0 +1,21 @@ +/* Admin Customization */ +#header { + background: var(--primary); + color: white; +} + +.module h2, .module caption { + background: var(--primary-dark); +} + +div.breadcrumbs { + background: var(--primary); +} + +.submit-row input { + background: var(--primary); +} + +.submit-row input:hover { + background: var(--primary-dark); +} \ No newline at end of file diff --git a/static/css/animations.css b/static/css/animations.css index 3ffaaa7..b73a3d4 100644 --- a/static/css/animations.css +++ b/static/css/animations.css @@ -1,47 +1,124 @@ -/* static/css/animations.css */ -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes pulseHighlight { +/* Add to your existing animation.css */ + +/* Line Cards Animation */ +.line-card { + animation: fadeIn 0.5s ease-out; + transition: transform 0.3s ease; +} + +.line-card:hover { + transform: translateY(-5px); +} + +/* Developer Card Animation */ +.developer-card { + animation: scaleIn 0.8s ease-out; +} + +.developer-avatar { + animation: float 3s ease-in-out infinite; +} + +/* API Links Animation */ +.api-link { + animation: fadeIn 0.5s ease-out; +} + +.api-link:nth-child(1) { animation-delay: 0.1s; } +.api-link:nth-child(2) { animation-delay: 0.2s; } +.api-link:nth-child(3) { animation-delay: 0.3s; } + +/* Particle Interaction Animations */ +@keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } -.metro-map { - transition: transform 0.3s ease; +.interactive-element { + animation: pulse 2s infinite; } -.metro-map:hover { - transform: scale(1.02); +/* Hover Effects */ +.glass-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); + background: rgba(255, 255, 255, 0.15); } -.line-path { - animation: fadeInUp 0.5s ease-out forwards; +/* Content Fade In */ +.fade-in { opacity: 0; + transform: translateY(20px); + animation: fadeIn 0.8s ease forwards; } -.line-path:nth-child(1) { animation-delay: 0.2s; } -.line-path:nth-child(2) { animation-delay: 0.4s; } -.line-path:nth-child(3) { animation-delay: 0.6s; } +.delay-1 { animation-delay: 0.2s; } +.delay-2 { animation-delay: 0.4s; } +.delay-3 { animation-delay: 0.6s; } -.api-category { - animation: fadeInUp 0.5s ease-out forwards; +/* Image Animations and Effects */ +.responsive-img { opacity: 0; + transition: opacity 0.3s ease, transform 0.3s ease; } -.api-category:nth-child(1) { animation-delay: 0.3s; } -.api-category:nth-child(2) { animation-delay: 0.5s; } -.api-category:nth-child(3) { animation-delay: 0.7s; } +.responsive-img.loaded { + opacity: 1; +} + +.image-hover { + overflow: hidden; + position: relative; +} + +.image-hover img { + transition: transform 0.3s ease; +} + +.image-hover:hover img { + transform: scale(1.05); +} + +/* Skeleton Loading */ +.skeleton { + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0.1), + rgba(255, 255, 255, 0.2), + rgba(255, 255, 255, 0.1) + ); + background-size: 200% 100%; + animation: loading 1.5s infinite; +} + +@keyframes loading { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes scaleIn { + from { transform: scale(0.95); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +.fade-in { + animation: fadeIn 0.8s ease forwards; +} + +.scale-in { + animation: scaleIn 0.5s ease forwards; +} + +.hover-lift { + transition: transform 0.3s ease; +} -.interchange-station { - animation: pulseHighlight 2s infinite; +.hover-lift:hover { + transform: translateY(-5px); } \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css index 275dd52..d95ede1 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,309 +1,130 @@ -/* Base Styles and Variables */ :root { - --primary-color: #2563eb; - --secondary-color: #1e40af; - --background-color: #ffffff; - --text-color: #1f2937; - --hover-color: #3b82f6; - --shadow-color: rgba(0, 0, 0, 0.1); - --gray-100: #f3f4f6; - --gray-200: #e5e7eb; - --gray-300: #d1d5db; - --gray-400: #9ca3af; - --gray-500: #6b7280; - --success-color: #10b981; - --error-color: #ef4444; - --warning-color: #f59e0b; + /* Colors */ + --primary: #2196F3; + --secondary: #1976D2; + --text: #FFFFFF; + --glass-bg: rgba(255, 255, 255, 0.1); + --glass-border: rgba(255, 255, 255, 0.2); + + /* Typography */ + --h1-size: clamp(2.5rem, 5vw, 4rem); + --h2-size: clamp(2rem, 4vw, 3rem); + --h3-size: clamp(1.5rem, 3vw, 2rem); + --body-size: clamp(1rem, 2vw, 1.125rem); + + /* Spacing */ + --spacing-xs: 0.5rem; + --spacing-sm: 1rem; + --spacing-md: 2rem; + --spacing-lg: 4rem; + + /* Effects */ + --shadow-md: 0 4px 8px rgba(0,0,0,0.12); + --transition: all 0.3s ease; } -/* Reset and Base Styles */ * { margin: 0; padding: 0; box-sizing: border-box; - font-family: 'Poppins', sans-serif; } body { - background-color: var(--gray-100); + font-family: 'Inter', sans-serif; + font-size: var(--body-size); line-height: 1.6; - color: var(--text-color); - padding-top: 70px; /* Account for fixed header */ + color: var(--text); + background: linear-gradient(135deg, #1a237e, #0d47a1); + overflow-x: hidden; } -/* Container */ -.container { - max-width: 1200px; - margin: 0 auto; - padding: 0 2rem; -} - -/* Main Content Area */ -.main-content { - min-height: calc(100vh - 70px - 200px); /* Account for header and footer */ - padding: 2rem 0; -} - -/* Card Styles */ -.card { - background: var(--background-color); - border-radius: 12px; - box-shadow: 0 2px 10px var(--shadow-color); - padding: 1.5rem; - margin-bottom: 1.5rem; - transition: transform 0.3s ease; -} - -.card:hover { - transform: translateY(-5px); -} - -/* Grid System */ -.grid { - display: grid; - gap: 1.5rem; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); -} - -/* Station List */ -.station-list { - display: grid; - gap: 1rem; -} - -.station-item { - background: var(--background-color); - padding: 1rem; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: space-between; - box-shadow: 0 2px 4px var(--shadow-color); -} - -/* Route Map */ -.route-map { - background: var(--background-color); - padding: 2rem; - border-radius: 12px; - box-shadow: 0 4px 6px var(--shadow-color); - margin: 2rem 0; -} - -.route-map img { +#particles-js { + position: fixed; width: 100%; - height: auto; - border-radius: 8px; -} - -/* Buttons */ -.btn { - padding: 0.75rem 1.5rem; - border-radius: 8px; - border: none; - font-weight: 500; - cursor: pointer; - transition: all 0.3s ease; - display: inline-flex; - align-items: center; - gap: 0.5rem; -} - -.btn-primary { - background-color: var(--primary-color); - color: white; + height: 100%; + top: 0; + left: 0; + z-index: 1; } -.btn-primary:hover { - background-color: var(--secondary-color); - transform: translateY(-2px); -} - -.btn-secondary { - background-color: var(--gray-200); - color: var(--text-color); -} - -.btn-secondary:hover { - background-color: var(--gray-300); -} - -/* Forms */ -.form-group { - margin-bottom: 1.5rem; +.site-wrapper { + position: relative; + z-index: 2; + min-height: 100vh; + display: flex; + flex-direction: column; } -.form-label { - display: block; - margin-bottom: 0.5rem; - font-weight: 500; +.glass { + background: var(--glass-bg); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + border: 1px solid var(--glass-border); + box-shadow: var(--shadow-md); } -.form-input { +.main-header { + background: var(--glass-bg); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--glass-border); + position: fixed; width: 100%; - padding: 0.75rem; - border: 1px solid var(--gray-300); - border-radius: 8px; - transition: border-color 0.3s ease; -} - -.form-input:focus { - outline: none; - border-color: var(--primary-color); - box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1); + top: 0; + z-index: 1000; } -/* Footer */ -.footer { - background-color: var(--background-color); - padding: 3rem 0; - box-shadow: 0 -2px 10px var(--shadow-color); -} - -.footer-content { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 2rem; +.nav-container { max-width: 1200px; margin: 0 auto; - padding: 0 2rem; -} - -.footer-section { + padding: var(--spacing-sm) var(--spacing-md); display: flex; - flex-direction: column; - gap: 1rem; -} - -.footer-section h3 { - color: var(--text-color); - font-size: 1.2rem; - margin-bottom: 1rem; + justify-content: space-between; + align-items: center; } -.footer-links { - display: flex; - flex-direction: column; - gap: 0.5rem; +.main-content { + flex: 1; + padding: var(--spacing-lg) var(--spacing-md); + margin-top: 70px; } -.footer-links a { - color: var(--gray-500); - text-decoration: none; - transition: color 0.3s ease; +.container { + max-width: 1200px; + margin: 0 auto; } -.footer-links a:hover { - color: var(--primary-color); +.section { + margin: var(--spacing-lg) 0; } -.footer-bottom { - margin-top: 2rem; - padding-top: 2rem; - border-top: 1px solid var(--gray-200); +.section-title { + font-size: var(--h2-size); + margin-bottom: var(--spacing-md); text-align: center; - color: var(--gray-500); } -/* Alerts and Messages */ -.alert { - padding: 1rem; - border-radius: 8px; - margin-bottom: 1rem; -} - -.alert-success { - background-color: #d1fae5; - color: #065f46; - border: 1px solid #34d399; -} - -.alert-error { - background-color: #fee2e2; - color: #991b1b; - border: 1px solid #f87171; +.footer { + background: var(--glass-bg); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + padding: var(--spacing-md) 0; + margin-top: auto; } -/* Responsive Design */ @media (max-width: 768px) { - .container { - padding: 0 1rem; + .nav-container { + padding: var(--spacing-sm); } - .grid { - grid-template-columns: 1fr; + .section-title { + font-size: var(--h3-size); } - - .footer-content { - grid-template-columns: 1fr; - text-align: center; - } - - .footer-links { - align-items: center; - } -} - -/* Admin Customization */ -.admin-container { - padding: 2rem; - background: var(--background-color); - border-radius: 12px; - box-shadow: 0 2px 10px var(--shadow-color); -} - -.admin-header { - margin-bottom: 2rem; - padding-bottom: 1rem; - border-bottom: 2px solid var(--gray-200); } -/* Tables */ -.table-container { - overflow-x: auto; -} - -.table { - width: 100%; - border-collapse: collapse; - margin: 1rem 0; -} - -.table th, -.table td { - padding: 1rem; - text-align: left; - border-bottom: 1px solid var(--gray-200); -} - -.table th { - background-color: var(--gray-100); - font-weight: 600; -} - -.table tr:hover { - background-color: var(--gray-50); -} - -/* Badges */ -.badge { - padding: 0.25rem 0.75rem; - border-radius: 9999px; - font-size: 0.875rem; - font-weight: 500; -} - -.badge-success { - background-color: #d1fae5; - color: #065f46; -} - -.badge-warning { - background-color: #fef3c7; - color: #92400e; -} - -.badge-error { - background-color: #fee2e2; - color: #991b1b; +@media (prefers-reduced-motion: reduce) { + * { + animation: none !important; + transition: none !important; + } } \ No newline at end of file diff --git a/static/images/metro-logo.png b/static/images/metro-map.png similarity index 100% rename from static/images/metro-logo.png rename to static/images/metro-map.png diff --git a/static/js/animations.js b/static/js/animations.js new file mode 100644 index 0000000..6932626 --- /dev/null +++ b/static/js/animations.js @@ -0,0 +1,36 @@ +// Scroll Animations +document.addEventListener("DOMContentLoaded", function () { + const elements = document.querySelectorAll(".scroll-reveal"); + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add("visible"); + } + }); + }); + elements.forEach((element) => observer.observe(element)); +}); + +// Particles.js Background +document.addEventListener("DOMContentLoaded", function () { + particlesJS("particles-js", { + particles: { + number: { value: 80, density: { enable: true, value_area: 800 } }, + color: { value: "#2196F3" }, + shape: { type: "circle" }, + opacity: { value: 0.5, random: true }, + size: { value: 3, random: true }, + line_linked: { enable: true, distance: 150, color: "#2196F3", opacity: 0.4, width: 1 }, + move: { enable: true, speed: 3, direction: "none", random: true, straight: false, out_mode: "out" } + }, + interactivity: { + detect_on: "canvas", + events: { + onhover: { enable: true, mode: "repulse" }, + onclick: { enable: true, mode: "push" }, + resize: true + } + }, + retina_detect: true + }); +}); \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index 099021c..a08d14a 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,36 +1,35 @@ -// static/js/main.js +// Image loading optimization document.addEventListener('DOMContentLoaded', function() { - // Metro map interactivity - const map = document.querySelector('.metro-map'); - const stations = document.querySelectorAll('.station-point'); + // Lazy load images + const images = document.querySelectorAll('img[data-src]'); - stations.forEach(station => { - station.addEventListener('click', () => { - showStationInfo(station.dataset.stationId); + const imageObserver = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const img = entry.target; + img.src = img.dataset.src; + img.onload = () => img.classList.add('loaded'); + observer.unobserve(img); + } }); }); - // API documentation search - const searchInput = document.querySelector('.api-search'); - const apiEndpoints = document.querySelectorAll('.api-list li'); - - if (searchInput) { - searchInput.addEventListener('input', (e) => { - const searchTerm = e.target.value.toLowerCase(); - apiEndpoints.forEach(endpoint => { - const text = endpoint.textContent.toLowerCase(); - endpoint.style.display = text.includes(searchTerm) ? 'block' : 'none'; - }); + images.forEach(img => imageObserver.observe(img)); + + // Responsive image sizing + function handleResponsiveImages() { + const contentWidth = document.querySelector('.container').offsetWidth; + const images = document.querySelectorAll('.responsive-img'); + + images.forEach(img => { + if (contentWidth < 768) { + img.style.maxWidth = '100%'; + } else { + img.style.maxWidth = '75%'; + } }); } - // Smooth scrolling for navigation links - document.querySelectorAll('a[href^="#"]').forEach(anchor => { - anchor.addEventListener('click', function (e) { - e.preventDefault(); - document.querySelector(this.getAttribute('href')).scrollIntoView({ - behavior: 'smooth' - }); - }); - }); + window.addEventListener('resize', handleResponsiveImages); + handleResponsiveImages(); }); \ No newline at end of file diff --git a/static/js/particles-config.js b/static/js/particles-config.js new file mode 100644 index 0000000..78bf739 --- /dev/null +++ b/static/js/particles-config.js @@ -0,0 +1,94 @@ +// static/js/particles-config.js +document.addEventListener("DOMContentLoaded", function() { + particlesJS("particles-js", { + "particles": { + "number": { + "value": 100, + "density": { + "enable": true, + "value_area": 800 + } + }, + "color": { + "value": "#2196F3" + }, + "shape": { + "type": "circle", + "stroke": { + "width": 0, + "color": "#000000" + }, + "polygon": { + "nb_sides": 5 + } + }, + "opacity": { + "value": 0.3, + "random": true, + "anim": { + "enable": true, + "speed": 1, + "opacity_min": 0.1, + "sync": false + } + }, + "size": { + "value": 3, + "random": true, + "anim": { + "enable": true, + "speed": 2, + "size_min": 0.1, + "sync": false + } + }, + "line_linked": { + "enable": true, + "distance": 150, + "color": "#2196F3", + "opacity": 0.2, + "width": 1 + }, + "move": { + "enable": true, + "speed": 2, + "direction": "none", + "random": true, + "straight": false, + "out_mode": "out", + "bounce": false, + "attract": { + "enable": true, + "rotateX": 600, + "rotateY": 1200 + } + } + }, + "interactivity": { + "detect_on": "canvas", + "events": { + "onhover": { + "enable": true, + "mode": "grab" + }, + "onclick": { + "enable": true, + "mode": "push" + }, + "resize": true + }, + "modes": { + "grab": { + "distance": 140, + "line_linked": { + "opacity": 1 + } + }, + "push": { + "particles_nb": 4 + } + } + }, + "retina_detect": true + }); +}); \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index e576a5b..97bc75f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,30 +4,36 @@ - {% block title %}Egypt Metro System{% endblock %} + {% block title %}Egypt Metro{% endblock %} - - - - - - + + + - - + + - {% block extra_css %}{% endblock %} + + + - {% include 'includes/header.html' %} + +
-
- {% block content %}{% endblock %} -
+ +
+ {% include 'includes/header.html' %} - {% include 'includes/footer.html' %} - - +
+ {% block content %}{% endblock %} +
+ + {% include 'includes/footer.html' %} +
+ + + {% block extra_js %}{% endblock %} \ No newline at end of file diff --git a/templates/home.html b/templates/home.html index 84e6766..01933ba 100644 --- a/templates/home.html +++ b/templates/home.html @@ -2,6 +2,9 @@ {% load static %} {% block content %} + +
+

Egypt Metro System

diff --git a/templates/includes/footer.html b/templates/includes/footer.html index 386730f..4413e4b 100644 --- a/templates/includes/footer.html +++ b/templates/includes/footer.html @@ -1,36 +1,43 @@ -