diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,6 +1,6 @@ -# --- START OF FILE app (1) (11).py --- -from flask import Flask, render_template_string, request, redirect, url_for, send_file + +from flask import Flask, render_template_string, request, redirect, url_for, send_from_directory import json import os import logging @@ -13,93 +13,103 @@ from werkzeug.utils import secure_filename app = Flask(__name__) DATA_FILE = 'data.json' +UPLOAD_FOLDER = 'uploads' +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +os.makedirs(UPLOAD_FOLDER, exist_ok=True) + +# Настройки Hugging Face REPO_ID = "Kgshop/glasman" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") +# Ссылка на логотип LOGO_URL = "https://huggingface.co/spaces/GlasmanGL/shop/resolve/main/gl_glasman-20250423-0001.jpg" -logging.basicConfig(level=logging.INFO) # Changed to INFO for less verbosity in production +# Настройка логирования +logging.basicConfig(level=logging.INFO) # Changed level to INFO for production clarity def load_data(): try: - logging.info("Attempting to download database from Hugging Face...") - download_db_from_hf() - logging.info("Database downloaded successfully.") + # Ensure local file exists before attempting to load + if not os.path.exists(DATA_FILE): + logging.info(f"{DATA_FILE} not found locally, attempting download from HF.") + download_db_from_hf() # Download if it doesn't exist + + # Check again if download succeeded + if not os.path.exists(DATA_FILE): + logging.warning("Local database file still not found after download attempt. Starting with empty data.") + return {'products': [], 'categories': []} + with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) logging.info("Data successfully loaded from JSON") - # Basic validation - if not isinstance(data, dict): - logging.warning("Data is not a dictionary, attempting recovery.") - # Try to recover if it's a list of products (old format?) - if isinstance(data, list): - return {'products': data, 'categories': []} - else: # Otherwise, initialize fresh + if not isinstance(data, dict) or 'products' not in data or 'categories' not in data: + logging.warning("JSON structure is invalid or incomplete. Resetting to default structure.") + # Handle case where data is just a list (old format?) or missing keys + if isinstance(data, list): # Assuming list might be old product list + return {'products': data, 'categories': []} + else: return {'products': [], 'categories': []} - if 'products' not in data: - data['products'] = [] - if 'categories' not in data: - data['categories'] = [] - # Ensure products is a list and categories is a list + # Ensure products and categories are lists if not isinstance(data.get('products'), list): - logging.warning("Products data is not a list, resetting.") data['products'] = [] if not isinstance(data.get('categories'), list): - logging.warning("Categories data is not a list, resetting.") data['categories'] = [] - return data except FileNotFoundError: - logging.warning("Local database file not found even after download attempt. Initializing empty structure.") + # This case should ideally be handled by the initial check and download + logging.warning("Local database file not found. Starting with empty data.") return {'products': [], 'categories': []} except json.JSONDecodeError: - logging.error("Error: Could not decode JSON file. Initializing empty structure.") - return {'products': [], 'categories': []} + logging.error("Error: Unable to decode JSON file. File might be corrupted. Starting with empty data.") + # Optionally, try to rename the corrupted file and download again + try: + os.rename(DATA_FILE, f"{DATA_FILE}.corrupted_{datetime.now().strftime('%Y%m%d%H%M%S')}") + logging.info("Renamed corrupted data file.") + # Try downloading again + download_db_from_hf() + # Recursive call - be careful with recursion depth + return load_data() + except Exception as e_rename: + logging.error(f"Could not rename corrupted file or re-download: {e_rename}") + return {'products': [], 'categories': []} except RepositoryNotFoundError: - logging.error("Hugging Face repository not found. Initializing with local empty structure.") - # Create an empty file locally if it doesn't exist after repo error - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'products': [], 'categories': []}, f) + logging.error("Hugging Face repository not found. Cannot download initial database. Starting with empty local data.") return {'products': [], 'categories': []} except Exception as e: logging.error(f"An unexpected error occurred during data loading: {e}") - # Attempt to create a default structure if file exists but is problematic, or if other error occurred - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'products': [], 'categories': []}, f) return {'products': [], 'categories': []} - def save_data(data): + temp_file = f"{DATA_FILE}.tmp" try: - # Ensure the data structure is valid before saving - if not isinstance(data, dict) or 'products' not in data or 'categories' not in data: - logging.error("Invalid data structure provided to save_data. Aborting save.") - # Optionally raise an error or handle it differently - # raise ValueError("Invalid data structure for saving") - return # Prevent saving incorrect data - - with open(DATA_FILE, 'w', encoding='utf-8') as file: + # Write to a temporary file first + with open(temp_file, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) - logging.info("Data successfully saved to JSON file.") - upload_db_to_hf() + # Rename the temporary file to the actual data file (atomic operation on POSIX) + os.replace(temp_file, DATA_FILE) + logging.info("Data successfully saved to JSON") + # Schedule HF upload (consider doing it asynchronously or less frequently) + # Using threading here for simplicity, but a task queue (like Celery) is better for production + threading.Thread(target=upload_db_to_hf).start() except Exception as e: logging.error(f"Error saving data: {e}") - # Decide if you want to re-raise or just log - # raise - + # Clean up temp file if it exists + if os.path.exists(temp_file): + try: + os.remove(temp_file) + except OSError as e_remove: + logging.error(f"Could not remove temporary save file {temp_file}: {e_remove}") + raise # Re-raise the exception to indicate failure def upload_db_to_hf(): if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE is not set. Skipping upload to Hugging Face.") + logging.warning("HF_TOKEN_WRITE not set. Skipping Hugging Face upload.") return if not os.path.exists(DATA_FILE): - logging.warning(f"Data file {DATA_FILE} not found. Skipping upload.") - return - + logging.warning(f"Data file {DATA_FILE} not found. Skipping Hugging Face upload.") + return try: api = HfApi() api.upload_file( @@ -108,74 +118,59 @@ def upload_db_to_hf(): repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Automatic database backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + commit_message=f"Automated database backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) logging.info("JSON database backup successfully uploaded to Hugging Face.") + except RepositoryNotFoundError: + logging.error(f"Failed to upload backup: Repository '{REPO_ID}' not found.") except Exception as e: - logging.error(f"Error uploading backup to Hugging Face: {e}") - + logging.error(f"Error uploading backup: {e}") def download_db_from_hf(): if not HF_TOKEN_READ: - logging.warning("HF_TOKEN_READ is not set. Skipping download from Hugging Face.") - # Check if local file exists, if not, create a default empty one - if not os.path.exists(DATA_FILE): - logging.warning(f"Local data file {DATA_FILE} not found. Creating empty default.") - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'products': [], 'categories': []}, f) - return # Don't raise error if token is missing, proceed with local or empty - + logging.warning("HF_TOKEN_READ not set. Skipping Hugging Face download.") + # Raise an error if download is essential for startup without local file + # raise ValueError("HF Read Token not configured, cannot download database.") + return # Or just return if local fallback is acceptable try: - # Ensure the target directory exists - local_dir = os.path.dirname(DATA_FILE) or "." - os.makedirs(local_dir, exist_ok=True) - hf_hub_download( repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, - local_dir=local_dir, # Specify the directory - local_dir_use_symlinks=False, # Recommended setting - cache_dir=None, # Avoid using default cache if issues arise - force_download=True # Ensure fresh copy + local_dir=".", # Download to current directory + local_dir_use_symlinks=False, # Recommended for safety + force_download=True # Overwrite local file if it exists ) logging.info("JSON database successfully downloaded from Hugging Face.") - # Rename the downloaded file if it was placed in a subdirectory structure unintentionally by hf_hub_download - downloaded_path = os.path.join(local_dir, DATA_FILE) - if downloaded_path != DATA_FILE and os.path.exists(downloaded_path): - os.replace(downloaded_path, DATA_FILE) - logging.info(f"Moved downloaded file from {downloaded_path} to {DATA_FILE}") - - except RepositoryNotFoundError as e: - logging.error(f"Hugging Face repository not found: {e}. Will use/create local file.") - # If repo not found, ensure a local file exists - if not os.path.exists(DATA_FILE): - logging.warning(f"Local data file {DATA_FILE} not found after repo error. Creating empty default.") - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'products': [], 'categories': []}, f) - # Do not raise RepositoryNotFoundError, let the app start with local/empty data - except Exception as e: + except RepositoryNotFoundError: + logging.error(f"Repository '{REPO_ID}' not found on Hugging Face.") + raise # Re-raise to be handled by load_data + except Exception as e: # Catch more specific exceptions if needed (e.g., hf_hub_utils.HFValidationError) logging.error(f"Error downloading JSON database from Hugging Face: {e}") - # If download fails, ensure a local file exists - if not os.path.exists(DATA_FILE): - logging.warning(f"Local data file {DATA_FILE} not found after download error. Creating empty default.") - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'products': [], 'categories': []}, f) - # Do not raise general Exception, let the app try to proceed - + raise # Re-raise to be handled by load_data def periodic_backup(): + if not HF_TOKEN_WRITE: + logging.info("Periodic backup disabled: HF_TOKEN_WRITE not set.") + return + logging.info("Starting periodic backup thread.") while True: - time.sleep(800) # Sleep first to avoid immediate backup on start - logging.info("Starting periodic backup...") - upload_db_to_hf() + time.sleep(800) # Sleep for 13 minutes and 20 seconds + logging.info("Initiating periodic backup...") + try: + # Load current data before backup to ensure consistency? + # Or just upload the last saved state. Uploading last saved is simpler. + upload_db_to_hf() + except Exception as e: + logging.error(f"Error during periodic backup: {e}") + # Add error handling/retry logic if needed @app.route('/') def catalog(): data = load_data() - products = data.get('products', []) + products = data.get('products', []) # Use .get for safety categories = data.get('categories', []) catalog_html = ''' @@ -217,9 +212,9 @@ def catalog(): padding: 15px 0; border-bottom: 1px solid #e2e8f0; } - body.dark-mode .header { + body.dark-mode .header { border-bottom: 1px solid #4a5568; - } + } .header-logo { width: 60px; height: 60px; @@ -233,11 +228,11 @@ def catalog(): box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); } .header h1 { - font-size: 1.5rem; + font-size: 1.5rem; /* Adjusted for potentially longer titles */ font-weight: 600; margin-left: 15px; - flex-grow: 1; /* Allow title to take space */ - text-align: center; /* Center title */ + flex-grow: 1; /* Allow title to take space */ + text-align: center; /* Center title */ } .theme-toggle { background: none; @@ -251,11 +246,11 @@ def catalog(): color: #a0aec0; } .theme-toggle:hover { - color: #3b82f6; /* Blue */ + color: #3b82f6; /* Blue accent */ } body.dark-mode .theme-toggle:hover { - color: #63b3ed; /* Lighter blue */ - } + color: #63b3ed; /* Lighter blue in dark mode */ + } .filters-container { margin: 20px 0; display: flex; @@ -277,13 +272,11 @@ def catalog(): outline: none; box-shadow: 0 2px 5px rgba(0,0,0,0.05); transition: all 0.3s ease; - background-color: #fff; - color: #2d3748; } body.dark-mode #search-input { background-color: #2d3748; - color: #e2e8f0; border-color: #4a5568; + color: #e2e8f0; } #search-input:focus { border-color: #3b82f6; @@ -291,117 +284,115 @@ def catalog(): } .category-filter { padding: 8px 16px; - border: 1px solid #cbd5e0; /* Slightly darker border */ + border: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; + color: #4a5568; /* Default text color */ cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); font-size: 0.9rem; font-weight: 400; - color: #4a5568; /* Default text color */ } - body.dark-mode .category-filter { - background-color: #2d3748; - border-color: #4a5568; - color: #a0aec0; - } + body.dark-mode .category-filter { + background-color: #4a5568; + border-color: #718096; + color: #e2e8f0; + } .category-filter.active, .category-filter:hover { background-color: #3b82f6; color: white; border-color: #3b82f6; box-shadow: 0 2px 10px rgba(59, 130, 246, 0.3); } - body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover { - background-color: #63b3ed; /* Lighter blue for dark mode active/hover */ - color: #1a202c; /* Dark text on light blue */ + body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover { + background-color: #63b3ed; /* Lighter blue for active/hover in dark */ border-color: #63b3ed; + color: #1a202c; /* Dark text on light blue */ box-shadow: 0 2px 10px rgba(99, 179, 237, 0.3); - } + } .products-grid { display: grid; - /* Responsive grid */ - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); /* Smaller min size */ - gap: 15px; - padding: 10px; - } + /* Responsive columns: 1 on small, 2 on medium, 3 on large */ + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); /* Adjusted min width */ + gap: 20px; /* Increased gap */ + padding: 10px 0; /* Padding top/bottom */ + } + /* Media queries for better responsiveness */ @media (min-width: 600px) { - .products-grid { - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); /* Larger min size for wider screens */ - } - } + .products-grid { grid-template-columns: repeat(2, 1fr); } + } @media (min-width: 900px) { - .products-grid { - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - } - } - @media (min-width: 1200px) { - .products-grid { - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - } - } + .products-grid { grid-template-columns: repeat(3, 1fr); } + } + @media (min-width: 1200px) { + .products-grid { grid-template-columns: repeat(4, 1fr); } /* Maybe 4 columns on very wide screens */ + } + .product { background: #fff; border-radius: 15px; - padding: 15px; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + /* Removed fixed padding, using flex column for layout */ + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); /* Softer shadow */ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease; overflow: hidden; - display: flex; - flex-direction: column; /* Stack elements vertically */ - justify-content: space-between; /* Push buttons to bottom */ + display: flex; + flex-direction: column; /* Stack elements vertically */ } body.dark-mode .product { background: #2d3748; - color: #e2e8f0; /* Use body's dark mode color */ - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); /* Slightly stronger shadow */ + color: #e2e8f0; /* Ensure text is visible */ + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); /* Darker shadow */ } .product:hover { transform: translateY(-5px) scale(1.02); - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); } body.dark-mode .product:hover { box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); } - .product-image { + .product-image-wrapper { /* New wrapper for aspect ratio */ width: 100%; - aspect-ratio: 1 / 1; /* Make it square */ - background-color: #fff; /* White background for images */ - border-radius: 10px; + aspect-ratio: 1 / 1; /* Keep square aspect ratio */ + background-color: #fff; /* White background for image area */ overflow: hidden; display: flex; justify-content: center; align-items: center; - margin-bottom: 10px; /* Space below image */ + border-bottom: 1px solid #e2e8f0; /* Separator line */ } - .product-image img { + body.dark-mode .product-image-wrapper { + background-color: #4a5568; /* Darker background for image */ + border-bottom: 1px solid #718096; + } + .product-image-wrapper img { + display: block; /* Remove extra space below image */ max-width: 100%; max-height: 100%; - object-fit: contain; /* Changed from cover to contain */ + object-fit: contain; /* Changed to contain to see full image */ transition: transform 0.3s ease; - display: block; /* Remove extra space below img */ } - .product-image img:hover { - transform: scale(1.1); + .product-image-wrapper img:hover { + transform: scale(1.08); /* Slightly larger zoom */ } - /* Ensure dark mode images have a background if transparent */ - body.dark-mode .product-image { - /* background-color: #e2e8f0; /* Light gray background for images in dark mode */ - /* Or keep it white: */ - background-color: #fff; - } - .product h2 { /* Product Name */ + .product-info { /* Container for text and buttons */ + padding: 15px; + display: flex; + flex-direction: column; + flex-grow: 1; /* Allow this section to grow */ + justify-content: space-between; /* Push buttons to bottom */ + } + .product h2 { /* Name */ font-size: 1rem; font-weight: 600; - margin: 0 0 5px 0; /* Adjusted margin */ + margin-bottom: 5px; /* Reduced margin */ text-align: center; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: #2d3748; /* Explicit color */ + /* Allow wrapping */ + /* white-space: nowrap; */ + /* overflow: hidden; */ + /* text-overflow: ellipsis; */ + line-height: 1.3; /* Adjust line height */ + min-height: 2.6em; /* Reserve space for 2 lines */ } - body.dark-mode .product h2 { - color: #e2e8f0; - } .product-price { font-size: 1.1rem; color: #ef4444; /* Red price */ @@ -409,44 +400,47 @@ def catalog(): text-align: center; margin: 5px 0; } - body.dark-mode .product-price { - color: #f87171; /* Lighter red */ - } + body.dark-mode .product-price { + color: #fca5a5; /* Lighter red */ + } .product-description { font-size: 0.8rem; color: #718096; text-align: center; margin-bottom: 15px; - overflow: hidden; - text-overflow: ellipsis; - /* Limit to 2 lines using line-clamp */ - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - line-height: 1.4; /* Adjust line height for clamping */ - min-height: calc(0.8rem * 1.4 * 2); /* Reserve space for 2 lines */ + flex-grow: 1; /* Allow description to take available space */ + /* Allow wrapping for description */ + /* overflow: hidden; */ + /* text-overflow: ellipsis; */ + /* white-space: nowrap; */ + line-height: 1.4; } body.dark-mode .product-description { color: #a0aec0; } + .product-buttons { /* Group buttons */ + margin-top: auto; /* Push buttons to the bottom */ + display: flex; + flex-direction: column; /* Stack buttons */ + gap: 8px; /* Space between buttons */ + } .product-button { display: block; width: 100%; - padding: 8px; + padding: 10px; /* Slightly larger padding */ border: none; border-radius: 8px; background-color: #3b82f6; /* Blue */ color: white; - font-size: 0.8rem; + font-size: 0.85rem; /* Slightly larger font */ font-weight: 500; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - margin: 5px 0 0 0; /* Add top margin */ text-align: center; text-decoration: none; } .product-button:hover { - background-color: #2563eb; + background-color: #2563eb; /* Darker blue */ box-shadow: 0 4px 15px rgba(37, 99, 235, 0.4); transform: translateY(-2px); } @@ -454,7 +448,7 @@ def catalog(): background-color: #10b981; /* Green */ } .add-to-cart:hover { - background-color: #059669; + background-color: #059669; /* Darker green */ box-shadow: 0 4px 15px rgba(5, 150, 105, 0.4); } #cart-button { @@ -469,16 +463,24 @@ def catalog(): height: 55px; font-size: 1.5rem; /* Larger icon */ cursor: pointer; - display: none; /* Hidden by default */ - align-items: center; + display: flex; /* Use flex to center icon */ justify-content: center; + align-items: center; box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 1000; + display: none; /* Initially hidden */ } - #cart-button:hover { - background-color: #dc2626; /* Darker red */ - transform: scale(1.1); + #cart-button .cart-count { /* Badge for item count */ + position: absolute; + top: -5px; + right: -5px; + background-color: #3b82f6; /* Blue badge */ + color: white; + border-radius: 50%; + padding: 2px 6px; + font-size: 0.7rem; + font-weight: bold; } .modal { display: none; @@ -488,69 +490,64 @@ def catalog(): top: 0; width: 100%; height: 100%; - background-color: rgba(0,0,0,0.5); + overflow: auto; /* Enable scroll if needed */ + background-color: rgba(0,0,0,0.6); /* Darker overlay */ backdrop-filter: blur(5px); - overflow-y: auto; /* Allow modal scroll */ + -webkit-backdrop-filter: blur(5px); /* For Safari */ } .modal-content { background: #fff; - margin: 5% auto; /* Top margin */ + margin: 5% auto; /* Vertically centered */ padding: 25px; /* More padding */ border-radius: 15px; width: 90%; - max-width: 700px; /* Max width for product details */ + max-width: 700px; /* Consistent max width */ box-shadow: 0 10px 30px rgba(0,0,0,0.2); - animation: slideIn 0.3s ease-out; - position: relative; /* For close button positioning */ + animation: slideIn 0.4s ease-out; + position: relative; /* For close button positioning */ } body.dark-mode .modal-content { background: #2d3748; color: #e2e8f0; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + box-shadow: 0 10px 30px rgba(0,0,0,0.5); } - /* Smaller max-width for quantity modal */ - #quantityModal .modal-content, #cartModal .modal-content { - max-width: 500px; - } - @keyframes slideIn { - from { transform: translateY(-50px); opacity: 0; } - to { transform: translateY(0); opacity: 1; } + from { transform: translateY(-30px) scale(0.95); opacity: 0; } + to { transform: translateY(0) scale(1); opacity: 1; } } .close { - position: absolute; /* Position relative to modal-content */ - top: 10px; - right: 15px; - font-size: 1.8rem; /* Larger close icon */ - color: #718096; + position: absolute; /* Position relative to modal-content */ + top: 10px; + right: 15px; + font-size: 2rem; /* Larger close button */ + font-weight: bold; + color: #aaa; cursor: pointer; - transition: color 0.3s, transform 0.3s; - line-height: 1; /* Prevent extra spacing */ + transition: color 0.3s; + line-height: 1; /* Adjust line height for better positioning */ } - .close:hover { - color: #2d3748; - transform: scale(1.1); + .close:hover, .close:focus { + color: #333; /* Darker hover */ + text-decoration: none; } body.dark-mode .close { color: #a0aec0; } - body.dark-mode .close:hover { + body.dark-mode .close:hover, body.dark-mode .close:focus { color: #fff; } - /* Modal specific styles */ - #productModal .modal-content { - max-width: 700px; - } - #quantityModal .modal-content, #cartModal .modal-content { - max-width: 500px; + .modal-content h2 { + margin-bottom: 20px; + text-align: center; + font-size: 1.5rem; } - #quantityModal label, #cartModal h2 { - margin-top: 15px; - display: block; - font-weight: 500; - } - #quantityModal h2 { margin-top: 0; } - + /* Cart specific styles */ + #cartContent { + max-height: 40vh; /* Limit cart height */ + overflow-y: auto; /* Enable scrolling for cart items */ + margin-bottom: 20px; + padding-right: 10px; /* Space for scrollbar */ + } .cart-item { display: flex; justify-content: space-between; @@ -558,610 +555,731 @@ def catalog(): padding: 15px 0; border-bottom: 1px solid #e2e8f0; } - .cart-item:last-child { - border-bottom: none; /* Remove border for last item */ - } body.dark-mode .cart-item { border-bottom: 1px solid #4a5568; } + .cart-item:last-child { + border-bottom: none; /* No border for the last item */ + } .cart-item img { width: 60px; /* Larger image in cart */ height: 60px; object-fit: contain; border-radius: 8px; margin-right: 15px; - background-color: #fff; /* Ensure visibility */ + background-color: #fff; /* Ensure background for transparent images */ + } + body.dark-mode .cart-item img { + background-color: #4a5568; } .cart-item-details { - flex-grow: 1; /* Take remaining space */ + flex-grow: 1; /* Allow details to take space */ + font-size: 0.9rem; } .cart-item-details strong { - display: block; /* Name on its own line */ - margin-bottom: 3px; - } - .cart-item-details p { - font-size: 0.85rem; - color: #718096; + display: block; /* Name on its own line */ + margin-bottom: 5px; } - body.dark-mode .cart-item-details p { - color: #a0aec0; + .cart-item-details span { /* Price and quantity line */ + display: block; + color: #718096; + font-size: 0.8rem; } - .cart-item-price { - font-weight: 600; + body.dark-mode .cart-item-details span { + color: #a0aec0; + } + .cart-item-total { + font-weight: bold; font-size: 1rem; - min-width: 70px; /* Ensure space for price */ + min-width: 70px; /* Ensure alignment */ text-align: right; } + .cart-item-remove { /* Button to remove item */ + background: none; + border: none; + color: #ef4444; + font-size: 1.2rem; + cursor: pointer; + margin-left: 15px; + transition: color 0.3s; + } + .cart-item-remove:hover { + color: #dc2626; + } + body.dark-mode .cart-item-remove { + color: #fca5a5; + } + body.dark-mode .cart-item-remove:hover { + color: #f87171; + } + /* Quantity/Options Modal Styles */ + #quantityModal label { + display: block; + margin: 15px 0 5px; + font-weight: 500; + } .quantity-input, .color-select, .size-select, .pattern-select { width: 100%; - max-width: 100%; /* Full width within modal */ - padding: 10px; /* More padding */ - border: 1px solid #cbd5e0; /* Match category filter */ + padding: 10px; /* Consistent padding */ + border: 1px solid #e2e8f0; border-radius: 8px; font-size: 1rem; - margin: 5px 0 15px 0; /* Add bottom margin */ - background-color: #fff; - color: #2d3748; + margin-bottom: 10px; /* Space below each input/select */ } body.dark-mode .quantity-input, body.dark-mode .color-select, body.dark-mode .size-select, body.dark-mode .pattern-select { - background-color: #1a202c; /* Darker background */ - border-color: #4a5568; + background-color: #4a5568; + border-color: #718096; color: #e2e8f0; + } + #quantityModal .product-button { /* Button specific to this modal */ + margin-top: 20px; + width: 100%; /* Full width button */ } - - /* Modal buttons */ - .modal-buttons { - display: flex; - justify-content: flex-end; /* Align buttons to the right */ - gap: 10px; /* Space between buttons */ + /* Cart Summary and Actions */ + .cart-summary { margin-top: 20px; - padding-top: 20px; + padding-top: 15px; border-top: 1px solid #e2e8f0; + text-align: right; + } + body.dark-mode .cart-summary { + border-top: 1px solid #4a5568; } - body.dark-mode .modal-buttons { - border-top: 1px solid #4a5568; - } - .modal-buttons .product-button { - width: auto; /* Allow buttons to size naturally */ - padding: 10px 20px; /* Adjust padding */ - } - + .cart-summary strong { + font-size: 1.2rem; + margin-bottom: 15px; /* Space before buttons */ + display: block; /* Total on its own line */ + } + .cart-actions { + display: flex; + justify-content: flex-end; /* Align buttons right */ + gap: 10px; /* Space between buttons */ + margin-top: 15px; + } .clear-cart { background-color: #ef4444; /* Red */ } .clear-cart:hover { - background-color: #dc2626; + background-color: #dc2626; /* Darker red */ box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4); } .order-button { background-color: #10b981; /* Green */ } .order-button:hover { - background-color: #059669; + background-color: #059669; /* Darker green */ box-shadow: 0 4px 15px rgba(5, 150, 105, 0.4); } - /* Swiper styles override */ - .swiper-button-next, .swiper-button-prev { - color: #3b82f6; /* Blue navigation buttons */ - transition: color 0.3s; + /* Product Detail Modal Specifics */ + #productModal .swiper-container { + max-width: 450px; /* Larger image display */ + margin-bottom: 25px; } - .swiper-button-next:hover, .swiper-button-prev:hover { - color: #2563eb; + #productModal .swiper-slide { + background-color: #f9f9f9; /* Light background for slides */ + display: flex; + justify-content: center; + align-items: center; + height: 400px; /* Fixed height for swiper */ } - body.dark-mode .swiper-button-next, body.dark-mode .swiper-button-prev { - color: #63b3ed; /* Lighter blue in dark mode */ + body.dark-mode #productModal .swiper-slide { + background-color: #4a5568; } - body.dark-mode .swiper-button-next:hover, body.dark-mode .swiper-button-prev:hover { - color: #90cdf4; + #productModal .swiper-slide img { + max-width: 100%; + max-height: 100%; + object-fit: contain; } - .swiper-pagination-bullet { - background: #a0aec0; /* Gray bullets */ + #productModal .swiper-button-next, + #productModal .swiper-button-prev { + color: #3b82f6; /* Blue navigation arrows */ } - .swiper-pagination-bullet-active { - background: #3b82f6; /* Blue active bullet */ + body.dark-mode #productModal .swiper-button-next, + body.dark-mode #productModal .swiper-button-prev { + color: #63b3ed; /* Lighter blue */ + } + #productModal .swiper-pagination-bullet-active { + background: #3b82f6; /* Blue active pagination dot */ } - body.dark-mode .swiper-pagination-bullet-active { - background: #63b3ed; /* Lighter blue active bullet */ + body.dark-mode #productModal .swiper-pagination-bullet-active { + background: #63b3ed; + } + #modalContent p { + margin-bottom: 10px; /* Spacing for details */ + font-size: 1rem; + line-height: 1.5; } - .swiper-container { - border-radius: 10px; /* Rounded corners for swiper */ - overflow: hidden; + #modalContent p strong { + font-weight: 600; + margin-right: 8px; /* Space after label */ } - + /* Ensure Swiper elements are styled */ + .swiper-button-next::after, .swiper-button-prev::after { + font-size: 1.5rem !important; /* Adjust arrow size */ + } + /* Loading indicator (optional) */ + .loading { + text-align: center; + padding: 50px; + font-size: 1.2rem; + color: #718096; + } + body.dark-mode .loading { + color: #a0aec0; + }
-

Каталог

+

Каталог Товаров

- + {% for category in categories %} - + {% endfor %}
- +
- {% for product in products %} -
-
- {% if product.get('photos') and product['photos']|length > 0 %} -
- {{ product['name'] }} -
- {% else %} -
+ {% if products %} + {% for product in products %} +
+
+ {% if product.get('photos') and product['photos']|length > 0 %} + {{ product['name']|e }} + {% else %} No Image Available + {% endif %} +
+
+

{{ product['name']|e }}

+
{{ product['price'] }} ₸
+

{{ product['description'][:80]|e }}{% if product['description']|length > 80 %}...{% endif %}

+
+ + +
- {% endif %} -

{{ product['name'] }}

-
{{ product['price'] }} ₸
-

{{ product.get('description', 'Нет описания')[:80] }}{% if product.get('description', '')|length > 80 %}...{% endif %}

-
-
- -
-
- {% endfor %} - + {% endfor %} + {% else %} +

Нет товаров для отображения.

+ {% endif %}
- +