diff --git a/backend/app/routes/account.py b/backend/app/routes/account.py index 1237c40..c912844 100644 --- a/backend/app/routes/account.py +++ b/backend/app/routes/account.py @@ -1,4 +1,5 @@ """Authenticated account endpoints — usage summary and API key management.""" + from flask import Blueprint, jsonify, request from app.extensions import limiter @@ -9,7 +10,15 @@ from app.services.account_service import ( revoke_api_key, ) from app.services.policy_service import get_usage_summary_for_user +from app.services.stripe_service import ( + is_stripe_configured, + get_stripe_price_id, +) from app.utils.auth import get_current_user_id +import stripe +import logging + +logger = logging.getLogger(__name__) account_bp = Blueprint("account", __name__) @@ -29,6 +38,69 @@ def get_usage_route(): return jsonify(get_usage_summary_for_user(user_id, user["plan"])), 200 +@account_bp.route("/subscription", methods=["GET"]) +@limiter.limit("60/hour") +def get_subscription_status(): + """Return subscription status for the authenticated user.""" + user_id = get_current_user_id() + if user_id is None: + return jsonify({"error": "Authentication required."}), 401 + + user = get_user_by_id(user_id) + if user is None: + return jsonify({"error": "User not found."}), 404 + + # If Stripe is not configured, return basic info + if not is_stripe_configured(): + return jsonify( + { + "plan": user["plan"], + "stripe_configured": False, + "subscription": None, + } + ), 200 + + # Retrieve subscription info from Stripe if available + subscription_info = None + if user.get("stripe_subscription_id"): + try: + from app.services.stripe_service import get_stripe_secret_key + + stripe.api_key = get_stripe_secret_key() + + subscription = stripe.Subscription.retrieve(user["stripe_subscription_id"]) + subscription_info = { + "id": subscription.id, + "status": subscription.status, + "current_period_start": subscription.current_period_start, + "current_period_end": subscription.current_period_end, + "cancel_at_period_end": subscription.cancel_at_period_end, + "items": [ + { + "price": item.price.id, + "quantity": item.quantity, + } + for item in subscription.items.data + ], + } + except Exception as e: + logger.error( + f"Failed to retrieve subscription {user['stripe_subscription_id']}: {e}" + ) + + return jsonify( + { + "plan": user["plan"], + "stripe_configured": True, + "subscription": subscription_info, + "pricing": { + "monthly_price_id": get_stripe_price_id("monthly"), + "yearly_price_id": get_stripe_price_id("yearly"), + }, + } + ), 200 + + @account_bp.route("/api-keys", methods=["GET"]) @limiter.limit("60/hour") def list_api_keys_route(): diff --git a/backend/app/services/quota_service.py b/backend/app/services/quota_service.py new file mode 100644 index 0000000..3b27a63 --- /dev/null +++ b/backend/app/services/quota_service.py @@ -0,0 +1,460 @@ +""" +QuotaService +Manages usage quotas and limits for Free, Pro, and Business tiers + +Quota Structure: +- Free: 5 conversions/day, 10MB max file size, no batch processing +- Pro: 100 conversions/day, 100MB max file size, batch processing (5 files) +- Business: Unlimited conversions, 500MB max file size, batch processing (20 files) + +Tracks: +- Daily usage (resets at UTC midnight) +- Storage usage +- API rate limits +- Feature access (premium features) +""" + +import logging +from datetime import datetime, timedelta +from typing import Dict, Optional, Tuple +from flask import current_app +from app.services.account_service import _connect, _utc_now + +logger = logging.getLogger(__name__) + + +class QuotaLimits: + """Define quota limits for each tier""" + + # Conversions per day + CONVERSIONS_PER_DAY = { + "free": 5, + "pro": 100, + "business": float("inf"), + } + + # Maximum file size in MB + MAX_FILE_SIZE_MB = { + "free": 10, + "pro": 100, + "business": 500, + } + + # Storage limit in MB (monthly) + STORAGE_LIMIT_MB = { + "free": 500, + "pro": 5000, + "business": float("inf"), + } + + # API rate limit (requests per minute) + API_RATE_LIMIT = { + "free": 10, + "pro": 60, + "business": float("inf"), + } + + # Concurrent processing jobs + CONCURRENT_JOBS = { + "free": 1, + "pro": 3, + "business": 10, + } + + # Batch file limit + BATCH_FILE_LIMIT = { + "free": 1, + "pro": 5, + "business": 20, + } + + # Premium features (Pro/Business) + PREMIUM_FEATURES = { + "free": set(), + "pro": {"batch_processing", "priority_queue", "email_delivery", "api_access"}, + "business": { + "batch_processing", + "priority_queue", + "email_delivery", + "api_access", + "webhook", + "sso", + }, + } + + +class QuotaService: + """Service for managing user quotas and usage tracking""" + + @staticmethod + def _ensure_quota_tables(): + """Create quota tracking tables if they don't exist""" + conn = _connect() + try: + # Daily usage tracking + conn.execute(""" + CREATE TABLE IF NOT EXISTS daily_usage ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + date TEXT NOT NULL, + conversions INTEGER DEFAULT 0, + files_processed INTEGER DEFAULT 0, + total_size_mb REAL DEFAULT 0, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, date), + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) + """) + + # Storage usage tracking + conn.execute(""" + CREATE TABLE IF NOT EXISTS storage_usage ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + month TEXT NOT NULL, + total_size_mb REAL DEFAULT 0, + file_count INTEGER DEFAULT 0, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, month), + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) + """) + + # API rate limiting + conn.execute(""" + CREATE TABLE IF NOT EXISTS api_requests ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + endpoint TEXT NOT NULL, + timestamp TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) + """) + + # Feature access log + conn.execute(""" + CREATE TABLE IF NOT EXISTS feature_access ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + feature TEXT NOT NULL, + accessed_at TEXT DEFAULT CURRENT_TIMESTAMP, + allowed BOOLEAN DEFAULT 1, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) + """) + + conn.commit() + logger.info("Quota tables initialized") + except Exception as e: + logger.error(f"Failed to create quota tables: {e}") + raise + finally: + conn.close() + + @staticmethod + def init_quota_db(): + """Initialize quota tracking database""" + QuotaService._ensure_quota_tables() + + @staticmethod + def get_user_plan(user_id: int) -> str: + """Get user's current plan""" + conn = _connect() + try: + row = conn.execute( + "SELECT plan FROM users WHERE id = ?", (user_id,) + ).fetchone() + return row["plan"] if row else "free" + finally: + conn.close() + + @staticmethod + def get_daily_usage(user_id: int, date: Optional[str] = None) -> Dict: + """ + Get daily usage for a user + + Args: + user_id: User ID + date: Date in YYYY-MM-DD format (defaults to today) + + Returns: + Usage stats dict + """ + if not date: + date = datetime.utcnow().strftime("%Y-%m-%d") + + conn = _connect() + try: + row = conn.execute( + "SELECT * FROM daily_usage WHERE user_id = ? AND date = ?", + (user_id, date), + ).fetchone() + + if not row: + return { + "conversions": 0, + "files_processed": 0, + "total_size_mb": 0, + } + + return dict(row) + finally: + conn.close() + + @staticmethod + def record_conversion(user_id: int, file_size_mb: float) -> bool: + """ + Record a file conversion + + Args: + user_id: User ID + file_size_mb: File size in MB + + Returns: + True if allowed, False if quota exceeded + """ + plan = QuotaService.get_user_plan(user_id) + today = datetime.utcnow().strftime("%Y-%m-%d") + + conn = _connect() + try: + # Check daily conversion limit + usage = conn.execute( + "SELECT conversions FROM daily_usage WHERE user_id = ? AND date = ?", + (user_id, today), + ).fetchone() + + current_conversions = usage["conversions"] if usage else 0 + limit = QuotaLimits.CONVERSIONS_PER_DAY[plan] + + if current_conversions >= limit and limit != float("inf"): + logger.warning(f"User {user_id} exceeded daily conversion limit") + return False + + # Check file size limit + max_size = QuotaLimits.MAX_FILE_SIZE_MB[plan] + if file_size_mb > max_size: + logger.warning( + f"User {user_id} file exceeds size limit: {file_size_mb}MB > {max_size}MB" + ) + return False + + # Record the conversion + conn.execute( + """ + INSERT INTO daily_usage (user_id, date, conversions, files_processed, total_size_mb) + VALUES (?, ?, 1, 1, ?) + ON CONFLICT(user_id, date) DO UPDATE SET + conversions = conversions + 1, + files_processed = files_processed + 1, + total_size_mb = total_size_mb + ? + """, + (user_id, today, file_size_mb, file_size_mb), + ) + conn.commit() + + logger.info(f"Recorded conversion for user {user_id}: {file_size_mb}MB") + return True + except Exception as e: + logger.error(f"Failed to record conversion: {e}") + return False + finally: + conn.close() + + @staticmethod + def check_rate_limit(user_id: int) -> Tuple[bool, int]: + """ + Check if user has exceeded API rate limit + + Args: + user_id: User ID + + Returns: + (allowed, remaining_requests_in_window) tuple + """ + plan = QuotaService.get_user_plan(user_id) + limit = QuotaLimits.API_RATE_LIMIT[plan] + + if limit == float("inf"): + return True, -1 # Unlimited + + # Check requests in last minute + one_minute_ago = (datetime.utcnow() - timedelta(minutes=1)).isoformat() + + conn = _connect() + try: + count = conn.execute( + "SELECT COUNT(*) as count FROM api_requests WHERE user_id = ? AND timestamp > ?", + (user_id, one_minute_ago), + ).fetchone()["count"] + + if count >= limit: + return False, 0 + + # Record this request + conn.execute( + "INSERT INTO api_requests (user_id, endpoint) VALUES (?, ?)", + (user_id, "api"), + ) + conn.commit() + + return True, limit - count - 1 + finally: + conn.close() + + @staticmethod + def has_feature(user_id: int, feature: str) -> bool: + """ + Check if user has access to a premium feature + + Args: + user_id: User ID + feature: Feature name (e.g., 'batch_processing') + + Returns: + True if user can access feature + """ + plan = QuotaService.get_user_plan(user_id) + allowed = feature in QuotaLimits.PREMIUM_FEATURES[plan] + + # Log feature access attempt + conn = _connect() + try: + conn.execute( + "INSERT INTO feature_access (user_id, feature, allowed) VALUES (?, ?, ?)", + (user_id, feature, allowed), + ) + conn.commit() + except Exception as e: + logger.error(f"Failed to log feature access: {e}") + finally: + conn.close() + + return allowed + + @staticmethod + def get_quota_status(user_id: int) -> Dict: + """ + Get comprehensive quota status for a user + + Args: + user_id: User ID + + Returns: + Complete quota status dict + """ + plan = QuotaService.get_user_plan(user_id) + today = datetime.utcnow().strftime("%Y-%m-%d") + + conn = _connect() + try: + # Get daily usage + daily = conn.execute( + "SELECT conversions FROM daily_usage WHERE user_id = ? AND date = ?", + (user_id, today), + ).fetchone() + + conversions_used = daily["conversions"] if daily else 0 + conversions_limit = QuotaLimits.CONVERSIONS_PER_DAY[plan] + + return { + "plan": plan, + "conversions": { + "used": conversions_used, + "limit": conversions_limit, + "remaining": conversions_limit - conversions_used + if conversions_limit != float("inf") + else -1, + "reset_at": (datetime.utcnow() + timedelta(days=1)).replace( + hour=0, minute=0, second=0 + ), + }, + "file_size_limit_mb": QuotaLimits.MAX_FILE_SIZE_MB[plan], + "storage_limit_mb": QuotaLimits.STORAGE_LIMIT_MB[plan], + "api_rate_limit": QuotaLimits.API_RATE_LIMIT[plan], + "concurrent_jobs": QuotaLimits.CONCURRENT_JOBS[plan], + "batch_file_limit": QuotaLimits.BATCH_FILE_LIMIT[plan], + "features": list(QuotaLimits.PREMIUM_FEATURES[plan]), + "can_batch_process": QuotaService.has_feature( + user_id, "batch_processing" + ), + "can_use_api": QuotaService.has_feature(user_id, "api_access"), + "can_schedule_delivery": QuotaService.has_feature( + user_id, "email_delivery" + ), + } + finally: + conn.close() + + @staticmethod + def get_monthly_storage_usage(user_id: int, year: int, month: int) -> float: + """Get storage usage for a specific month""" + month_key = f"{year}-{month:02d}" + + conn = _connect() + try: + row = conn.execute( + "SELECT total_size_mb FROM storage_usage WHERE user_id = ? AND month = ?", + (user_id, month_key), + ).fetchone() + + return row["total_size_mb"] if row else 0 + finally: + conn.close() + + @staticmethod + def upgrade_plan(user_id: int, new_plan: str) -> bool: + """ + Upgrade user to a new plan + + Args: + user_id: User ID + new_plan: New plan ('pro' or 'business') + + Returns: + Success status + """ + if new_plan not in QuotaLimits.CONVERSIONS_PER_DAY: + logger.error(f"Invalid plan: {new_plan}") + return False + + conn = _connect() + try: + conn.execute( + "UPDATE users SET plan = ?, updated_at = ? WHERE id = ?", + (new_plan, _utc_now(), user_id), + ) + conn.commit() + logger.info(f"User {user_id} upgraded to {new_plan}") + return True + except Exception as e: + logger.error(f"Failed to upgrade user plan: {e}") + return False + finally: + conn.close() + + @staticmethod + def downgrade_plan(user_id: int, new_plan: str = "free") -> bool: + """Downgrade user to a lower plan""" + return QuotaService.upgrade_plan(user_id, new_plan) + + +# Convenience functions +def init_quota_db(): + return QuotaService.init_quota_db() + + +def get_quota_status(user_id: int) -> Dict: + return QuotaService.get_quota_status(user_id) + + +def record_conversion(user_id: int, file_size_mb: float) -> bool: + return QuotaService.record_conversion(user_id, file_size_mb) + + +def check_rate_limit(user_id: int) -> Tuple[bool, int]: + return QuotaService.check_rate_limit(user_id) + + +def has_feature(user_id: int, feature: str) -> bool: + return QuotaService.has_feature(user_id, feature) diff --git a/backend/app/utils/auth.py b/backend/app/utils/auth.py index d70f385..c6828a6 100644 --- a/backend/app/utils/auth.py +++ b/backend/app/utils/auth.py @@ -2,7 +2,9 @@ from flask import session TASK_ACCESS_SESSION_KEY = "task_access_ids" +STAGED_UPLOAD_SESSION_KEY = "staged_upload_ids" MAX_TRACKED_TASK_IDS = 200 +MAX_TRACKED_STAGED_UPLOAD_IDS = 50 def get_current_user_id() -> int | None: @@ -28,14 +30,34 @@ def has_session_task_access(task_id: str) -> bool: return isinstance(tracked, list) and task_id in tracked +def remember_staged_upload(upload_id: str): + """Persist one staged upload id in the active browser session.""" + tracked = session.get(STAGED_UPLOAD_SESSION_KEY, []) + if not isinstance(tracked, list): + tracked = [] + + normalized = [value for value in tracked if isinstance(value, str) and value != upload_id] + normalized.append(upload_id) + session[STAGED_UPLOAD_SESSION_KEY] = normalized[-MAX_TRACKED_STAGED_UPLOAD_IDS:] + + +def has_staged_upload_access(upload_id: str) -> bool: + """Return whether the active browser session owns one staged upload id.""" + tracked = session.get(STAGED_UPLOAD_SESSION_KEY, []) + return isinstance(tracked, list) and upload_id in tracked + + def login_user_session(user_id: int): """Persist the authenticated user in the Flask session.""" tracked_task_ids = session.get(TASK_ACCESS_SESSION_KEY, []) + staged_upload_ids = session.get(STAGED_UPLOAD_SESSION_KEY, []) session.clear() session.permanent = True session["user_id"] = user_id if isinstance(tracked_task_ids, list) and tracked_task_ids: session[TASK_ACCESS_SESSION_KEY] = tracked_task_ids[-MAX_TRACKED_TASK_IDS:] + if isinstance(staged_upload_ids, list) and staged_upload_ids: + session[STAGED_UPLOAD_SESSION_KEY] = staged_upload_ids[-MAX_TRACKED_STAGED_UPLOAD_IDS:] def logout_user_session(): diff --git a/docker-compose.yml b/docker-compose.yml index 615400e..9ddc5c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -117,16 +117,14 @@ services: - VITE_SITE_DOMAIN=${VITE_SITE_DOMAIN:-} - VITE_SENTRY_DSN=${VITE_SENTRY_DSN:-} - # --- Nginx Reverse Proxy --- + # --- Nginx Reverse Proxy --- nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: - - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro - - ./certbot/www:/var/www/certbot:ro - - ./certbot/conf:/etc/letsencrypt:ro + - ./nginx/nginx.dev.conf:/etc/nginx/conf.d/default.conf:ro depends_on: - backend - frontend diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml index be52981..4f794d1 100644 --- a/frontend/public/sitemap.xml +++ b/frontend/public/sitemap.xml @@ -2,1471 +2,1471 @@ https://dociva.io/ - 2026-03-25 + 2026-03-27 daily 1.0 https://dociva.io/about - 2026-03-25 + 2026-03-27 monthly 0.4 https://dociva.io/contact - 2026-03-25 + 2026-03-27 monthly 0.4 https://dociva.io/privacy - 2026-03-25 + 2026-03-27 yearly 0.3 https://dociva.io/terms - 2026-03-25 + 2026-03-27 yearly 0.3 https://dociva.io/pricing - 2026-03-25 + 2026-03-27 monthly 0.7 https://dociva.io/blog - 2026-03-25 + 2026-03-27 weekly 0.6 https://dociva.io/developers - 2026-03-25 + 2026-03-27 monthly 0.5 https://dociva.io/blog/how-to-compress-pdf-online - 2026-03-25 + 2026-03-27 monthly 0.6 https://dociva.io/blog/convert-images-without-losing-quality - 2026-03-25 + 2026-03-27 monthly 0.6 https://dociva.io/blog/ocr-extract-text-from-images - 2026-03-25 + 2026-03-27 monthly 0.6 https://dociva.io/blog/merge-split-pdf-files - 2026-03-25 + 2026-03-27 monthly 0.6 https://dociva.io/blog/ai-chat-with-pdf-documents - 2026-03-25 + 2026-03-27 monthly 0.6 https://dociva.io/tools/pdf-to-word - 2026-03-25 + 2026-03-27 weekly 0.9 https://dociva.io/tools/word-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.9 https://dociva.io/tools/compress-pdf - 2026-03-25 + 2026-03-27 weekly 0.9 https://dociva.io/tools/merge-pdf - 2026-03-25 + 2026-03-27 weekly 0.9 https://dociva.io/tools/split-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/rotate-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/pdf-to-images - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/images-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/watermark-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/protect-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/unlock-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/page-numbers - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/pdf-editor - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/pdf-flowchart - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/pdf-to-excel - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/remove-watermark-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/reorder-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/extract-pages - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/image-converter - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/image-resize - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/compress-image - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/ocr - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/remove-background - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/image-to-svg - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/html-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/chat-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/summarize-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/translate-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/extract-tables - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/qr-code - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/video-to-gif - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/word-counter - 2026-03-25 + 2026-03-27 weekly 0.6 https://dociva.io/tools/text-cleaner - 2026-03-25 + 2026-03-27 weekly 0.6 https://dociva.io/tools/pdf-to-pptx - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/excel-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/pptx-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/sign-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/tools/crop-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/flatten-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/repair-pdf - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/pdf-metadata - 2026-03-25 + 2026-03-27 weekly 0.6 https://dociva.io/tools/image-crop - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/image-rotate-flip - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/tools/barcode-generator - 2026-03-25 + 2026-03-27 weekly 0.7 https://dociva.io/pdf-to-word - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-word - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/word-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/word-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/convert-jpg-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/convert-jpg-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/merge-pdf-files - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/merge-pdf-files - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/remove-pdf-password - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/remove-pdf-password - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-word-editable - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-word-editable - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/convert-pdf-to-text - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/convert-pdf-to-text - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/split-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/split-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/jpg-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/jpg-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/png-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/png-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/images-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/images-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-jpg - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-jpg - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-png - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-png - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-pdf-for-email - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-pdf-for-email - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-scanned-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-scanned-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/merge-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/merge-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/combine-pdf-files - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/combine-pdf-files - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/extract-pages-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/extract-pages-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/reorder-pdf-pages - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/reorder-pdf-pages - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/rotate-pdf-pages - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/rotate-pdf-pages - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/add-page-numbers-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/add-page-numbers-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/protect-pdf-with-password - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/protect-pdf-with-password - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/unlock-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/unlock-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/watermark-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/watermark-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/remove-watermark-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/remove-watermark-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/edit-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/edit-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-excel-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-excel-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/extract-tables-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/extract-tables-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/html-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/html-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/scan-pdf-to-text - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/scan-pdf-to-text - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/chat-with-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/chat-with-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/summarize-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/summarize-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/translate-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/translate-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/convert-image-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/convert-image-to-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/convert-webp-to-jpg - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/convert-webp-to-jpg - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/resize-image-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/resize-image-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-image-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-image-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/remove-image-background - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/remove-image-background - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-word-editable-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-word-editable-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-pdf-to-100kb - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-pdf-to-100kb - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/ai-extract-text-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/ai-extract-text-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-excel-accurate-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-excel-accurate-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/split-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/split-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/unlock-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/unlock-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/summarize-pdf-ai - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/summarize-pdf-ai - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/convert-pdf-to-text-ai - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/convert-pdf-to-text-ai - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-jpg-high-quality - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-jpg-high-quality - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/jpg-to-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/jpg-to-pdf-online-free - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/reduce-pdf-size-for-email - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/reduce-pdf-size-for-email - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/ocr-for-scanned-pdfs - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/ocr-for-scanned-pdfs - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/remove-watermark-from-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/remove-watermark-from-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/add-watermark-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/add-watermark-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/repair-corrupted-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/repair-corrupted-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/rotate-pdf-pages-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/rotate-pdf-pages-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/reorder-pdf-pages-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/reorder-pdf-pages-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-png-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-png-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/images-to-pdf-multiple - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/images-to-pdf-multiple - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/split-pdf-by-range-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/split-pdf-by-range-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-scanned-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-scanned-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-metadata-editor-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-metadata-editor-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/add-page-numbers-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/add-page-numbers-to-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/protect-pdf-with-password-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/protect-pdf-with-password-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/unlock-encrypted-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/unlock-encrypted-pdf-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/ocr-table-extraction-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/ocr-table-extraction-from-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-excel-converter-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-excel-converter-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/extract-text-from-protected-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/extract-text-from-protected-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/bulk-convert-pdf-to-word - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/bulk-convert-pdf-to-word - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/compress-pdf-for-web-upload - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/compress-pdf-for-web-upload - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/ocr-multi-language-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/ocr-multi-language-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/summarize-long-pdf-ai - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/summarize-long-pdf-ai - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/convert-pdf-to-ppt-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/convert-pdf-to-ppt-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/pdf-to-pptx-free-online - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/pdf-to-pptx-free-online - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/دمج-ملفات-pdf-مجاناً - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/دمج-ملفات-pdf-مجاناً - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/ضغط-بي-دي-اف-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/ضغط-بي-دي-اف-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/تحويل-pdf-الى-word-قابل-للتعديل - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-word-قابل-للتعديل - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/تحويل-jpg-الى-pdf-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/تحويل-jpg-الى-pdf-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/فصل-صفحات-pdf-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/فصل-صفحات-pdf-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/ازالة-كلمة-مرور-من-pdf - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/ازالة-كلمة-مرور-من-pdf - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/تحويل-pdf-الى-نص-باستخدام-ocr - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-نص-باستخدام-ocr - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/تحويل-pdf-الى-excel-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-excel-اونلاين - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/تحويل-pdf-الى-صور - 2026-03-25 + 2026-03-27 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-صور - 2026-03-25 + 2026-03-27 weekly 0.8 https://dociva.io/best-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/best-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/free-pdf-tools-online - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/free-pdf-tools-online - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/convert-files-online - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/convert-files-online - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/pdf-converter-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/pdf-converter-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/secure-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/secure-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/ai-document-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/ai-document-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/image-to-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/image-to-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/online-image-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/online-image-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/office-to-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/office-to-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/scanned-document-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/scanned-document-tools - 2026-03-25 + 2026-03-27 weekly 0.74 https://dociva.io/arabic-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.82 https://dociva.io/ar/arabic-pdf-tools - 2026-03-25 + 2026-03-27 weekly 0.74 diff --git a/frontend/src/components/shared/ErrorMessage.tsx b/frontend/src/components/shared/ErrorMessage.tsx new file mode 100644 index 0000000..9a0095b --- /dev/null +++ b/frontend/src/components/shared/ErrorMessage.tsx @@ -0,0 +1,168 @@ +import React, { useState } from 'react'; +import { AlertCircle, RefreshCw, HelpCircle, AlertTriangle } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; + +export interface ErrorMessageProps { + message?: string; + type?: 'error' | 'warning' | 'info'; + details?: string; + showDetails?: boolean; + onRetry?: () => void; + showRetry?: boolean; + actions?: Array<{ + label: string; + onClick: () => void; + variant?: 'primary' | 'secondary'; + }>; + suggestion?: string; + helpLink?: string; + dismissible?: boolean; + onDismiss?: () => void; +} + +export default function ErrorMessage({ + message = 'An error occurred', + type = 'error', + details, + showDetails: initialShowDetails = false, + onRetry, + showRetry = true, + actions, + suggestion, + helpLink, + dismissible = true, + onDismiss, +}: ErrorMessageProps) { + const { t } = useTranslation(); + const [showDetails, setShowDetails] = useState(initialShowDetails); + const [isDismissed, setIsDismissed] = useState(false); + + if (isDismissed) return null; + + const bgColor = + type === 'error' + ? 'bg-red-50 dark:bg-red-900/20' + : type === 'warning' + ? 'bg-amber-50 dark:bg-amber-900/20' + : 'bg-blue-50 dark:bg-blue-900/20'; + + const borderColor = + type === 'error' + ? 'border-red-200 dark:border-red-800' + : type === 'warning' + ? 'border-amber-200 dark:border-amber-800' + : 'border-blue-200 dark:border-blue-800'; + + const textColor = + type === 'error' + ? 'text-red-900 dark:text-red-200' + : type === 'warning' + ? 'text-amber-900 dark:text-amber-200' + : 'text-blue-900 dark:text-blue-200'; + + const iconColor = + type === 'error' + ? 'text-red-600 dark:text-red-400' + : type === 'warning' + ? 'text-amber-600 dark:text-amber-400' + : 'text-blue-600 dark:text-blue-400'; + + const Icon = type === 'warning' ? AlertTriangle : AlertCircle; + + return ( +
+
+
+
+ ); +} diff --git a/frontend/src/components/shared/ProgressBar.tsx b/frontend/src/components/shared/ProgressBar.tsx index 9f44ef3..58ec3d1 100644 --- a/frontend/src/components/shared/ProgressBar.tsx +++ b/frontend/src/components/shared/ProgressBar.tsx @@ -1,40 +1,137 @@ import { useTranslation } from 'react-i18next'; -import { Loader2, CheckCircle2 } from 'lucide-react'; +import { Loader2, CheckCircle2, AlertCircle, Clock } from 'lucide-react'; interface ProgressBarProps { /** Current task state */ - state: 'PENDING' | 'PROCESSING' | 'SUCCESS' | 'FAILURE' | string; + state?: 'PENDING' | 'PROCESSING' | 'SUCCESS' | 'FAILURE' | string; + status?: string; // Alternative to state (for compatibility) /** Progress message */ message?: string; + /** Progress percentage (0-100) */ + progress?: number; + /** Show detailed steps */ + steps?: Array<{ + name: string; + status: 'pending' | 'active' | 'complete' | 'error'; + message?: string; + }>; + /** Show a simple indeterminate progress bar */ + indeterminate?: boolean; } -export default function ProgressBar({ state, message }: ProgressBarProps) { +export default function ProgressBar({ + state, + status, + message, + progress, + steps, + indeterminate = true, +}: ProgressBarProps) { const { t } = useTranslation(); + const taskState = state || status || 'PROCESSING'; - const isActive = state === 'PENDING' || state === 'PROCESSING'; - const isComplete = state === 'SUCCESS'; + const isActive = taskState === 'PENDING' || taskState === 'PROCESSING'; + const isComplete = taskState === 'SUCCESS'; + const isError = taskState === 'FAILURE'; return ( -
-
- {isActive && ( - - )} - {isComplete && ( - +
+ {/* Main Progress Card */} +
+
+ {isActive && ( + + )} + {isComplete && ( + + )} + {isError && ( + + )} + {!isActive && !isComplete && !isError && ( + + )} + +
+

+ {message || t('common.processing', { defaultValue: 'Processing...' })} +

+ {progress !== undefined && ( +

+ {progress}% +

+ )} +
+
+ + {/* Progress Bar */} + {indeterminate && isActive && ( +
+
+
)} -
-

- {message || t('common.processing')} -

-
+ {/* Determinate Progress Bar */} + {!indeterminate && progress !== undefined && ( +
+
+
+
+
+ )}
- {/* Animated progress bar for active states */} - {isActive && ( -
-
+ {/* Step-by-Step Progress */} + {steps && steps.length > 0 && ( +
+

+ {t('common.processingSteps', { defaultValue: 'Processing Steps' })} +

+
+ {steps.map((step, idx) => ( +
+
+ {step.status === 'complete' && ( + + )} + {step.status === 'active' && ( + + )} + {step.status === 'error' && ( + + )} + {step.status === 'pending' && ( +
+ )} +
+
+

+ {step.name} +

+ {step.message && ( +

+ {step.message} +

+ )} +
+
+ ))} +
)}
diff --git a/frontend/src/components/shared/ToolTemplate.tsx b/frontend/src/components/shared/ToolTemplate.tsx new file mode 100644 index 0000000..16fe662 --- /dev/null +++ b/frontend/src/components/shared/ToolTemplate.tsx @@ -0,0 +1,226 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Helmet } from 'react-helmet-async'; +import { LucideIcon, AlertCircle, CheckCircle, Clock } from 'lucide-react'; +import FileUploader from '@/components/shared/FileUploader'; +import ProgressBar from '@/components/shared/ProgressBar'; +import DownloadButton from '@/components/shared/DownloadButton'; +import AdSlot from '@/components/layout/AdSlot'; +import { useFileUpload } from '@/hooks/useFileUpload'; +import { useTaskPolling } from '@/hooks/useTaskPolling'; +import { generateToolSchema } from '@/utils/seo'; +import { useFileStore } from '@/stores/fileStore'; + +export interface ToolConfig { + slug: string; + icon: LucideIcon; + color?: 'orange' | 'red' | 'blue' | 'green' | 'purple' | 'pink' | 'amber' | 'cyan'; + i18nKey: string; + endpoint: string; + maxSizeMB?: number; + acceptedTypes?: string[]; + isPremium?: boolean; + extraData?: Record; +} + +export interface ToolTemplateProps { + file: File | null; + uploadProgress: number; + isUploading: boolean; + isProcessing: boolean; + result: any; + error: string | null; + selectFile: (file: File) => void; + reset: () => void; +} + +const colorMap: Record = { + orange: { bg: 'bg-orange-50 dark:bg-orange-900/20', icon: 'text-orange-600 dark:text-orange-400' }, + red: { bg: 'bg-red-50 dark:bg-red-900/20', icon: 'text-red-600 dark:text-red-400' }, + blue: { bg: 'bg-blue-50 dark:bg-blue-900/20', icon: 'text-blue-600 dark:text-blue-400' }, + green: { bg: 'bg-green-50 dark:bg-green-900/20', icon: 'text-green-600 dark:text-green-400' }, + purple: { bg: 'bg-purple-50 dark:bg-purple-900/20', icon: 'text-purple-600 dark:text-purple-400' }, + pink: { bg: 'bg-pink-50 dark:bg-pink-900/20', icon: 'text-pink-600 dark:text-pink-400' }, + amber: { bg: 'bg-amber-50 dark:bg-amber-900/20', icon: 'text-amber-600 dark:text-amber-400' }, + cyan: { bg: 'bg-cyan-50 dark:bg-cyan-900/20', icon: 'text-cyan-600 dark:text-cyan-400' }, +}; + +interface ToolTemplateComponentProps { + config: ToolConfig; + onGetExtraData?: () => Record; + children?: (props: ToolTemplateProps) => React.ReactNode; +} + +export default function ToolTemplate({ config, onGetExtraData, children }: ToolTemplateComponentProps) { + const { t } = useTranslation(); + const [phase, setPhase] = useState<'upload' | 'processing' | 'done'>('upload'); + const [extraData, setExtraData] = useState>(config.extraData || {}); + + const colors = colorMap[config.color || 'blue']; + const bgColor = colors.bg; + const iconColor = colors.icon; + + const { file, uploadProgress, isUploading, taskId, error, selectFile, startUpload, reset } = useFileUpload({ + endpoint: config.endpoint, + maxSizeMB: config.maxSizeMB || 20, + acceptedTypes: config.acceptedTypes || ['pdf'], + extraData, + }); + + const { status, result } = useTaskPolling({ + taskId, + onComplete: () => setPhase('done'), + onError: () => setPhase('done'), + }); + + const storeFile = useFileStore((s) => s.file); + const clearStoreFile = useFileStore((s) => s.clearFile); + + useEffect(() => { + if (storeFile && config.acceptedTypes?.some((type) => storeFile.name.endsWith(`.${type}`))) { + selectFile(storeFile); + clearStoreFile(); + setPhase('upload'); + } + }, []); + + const handleUpload = useCallback(async () => { + // Get fresh extraData from child if callback provided + if (onGetExtraData) { + const freshExtraData = onGetExtraData(); + setExtraData(freshExtraData); + } + const id = await startUpload(); + if (id) setPhase('processing'); + }, [onGetExtraData, startUpload]); + + const handleReset = () => { + reset(); + setPhase('upload'); + }; + + const title = t(`${config.i18nKey}.title`, { defaultValue: config.slug }); + const description = t(`${config.i18nKey}.description`, { defaultValue: '' }); + + const schema = generateToolSchema({ + name: title, + description, + url: `${window.location.origin}/tools/${config.slug}`, + }); + + const templateProps: ToolTemplateProps = { + file, + uploadProgress, + isUploading, + isProcessing: phase === 'processing', + result, + error, + selectFile, + reset: handleReset, + }; + + return ( + <> + + {title} — Dociva + + + + + +
+
+
+
+

{title}

+

{description}

+
+ + + +
+ {phase === 'upload' && ( +
+ { + selectFile(f); + setPhase('upload'); + }} + file={file} + accept={config.acceptedTypes?.reduce( + (acc, type) => ({ + ...acc, + [`application/${type}`]: [`.${type}`], + }), + {} + ) || {}} + /> + + {children && ( +
{children(templateProps)}
+ )} + + +
+ )} + + {phase === 'processing' && ( +
+ +
+ )} + + {phase === 'done' && ( +
+ {result ? ( + <> +
+
+ +
+

Success!

+

Your file is ready

+
+
+
+ + + + ) : ( +
+
+ +
+

Error

+

{error || 'Processing failed'}

+
+
+
+ )} + + +
+ )} +
+ + +
+ + ); +} diff --git a/frontend/src/components/tools/PdfCompressor.tsx b/frontend/src/components/tools/PdfCompressor.tsx index 19e3d4e..b2aea33 100644 --- a/frontend/src/components/tools/PdfCompressor.tsx +++ b/frontend/src/components/tools/PdfCompressor.tsx @@ -1,63 +1,22 @@ -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Helmet } from 'react-helmet-async'; import { Minimize2 } from 'lucide-react'; -import FileUploader from '@/components/shared/FileUploader'; -import ProgressBar from '@/components/shared/ProgressBar'; -import DownloadButton from '@/components/shared/DownloadButton'; -import AdSlot from '@/components/layout/AdSlot'; -import { useFileUpload } from '@/hooks/useFileUpload'; -import { useTaskPolling } from '@/hooks/useTaskPolling'; -import { generateToolSchema } from '@/utils/seo'; -import { useFileStore } from '@/stores/fileStore'; +import ToolTemplate, { ToolConfig, ToolTemplateProps } from '@/components/shared/ToolTemplate'; type Quality = 'low' | 'medium' | 'high'; export default function PdfCompressor() { const { t } = useTranslation(); - const [phase, setPhase] = useState<'upload' | 'processing' | 'done'>('upload'); const [quality, setQuality] = useState('medium'); - const { - file, - uploadProgress, - isUploading, - taskId, - error: uploadError, - selectFile, - startUpload, - reset, - } = useFileUpload({ + const toolConfig: ToolConfig = { + slug: 'compress-pdf', + icon: Minimize2, + color: 'orange', + i18nKey: 'tools.compressPdf', endpoint: '/compress/pdf', maxSizeMB: 20, acceptedTypes: ['pdf'], - extraData: { quality }, - }); - - const { status, result, error: taskError } = useTaskPolling({ - taskId, - onComplete: () => setPhase('done'), - onError: () => setPhase('done'), - }); - - // Accept file from homepage smart upload - const storeFile = useFileStore((s) => s.file); - const clearStoreFile = useFileStore((s) => s.clearFile); - useEffect(() => { - if (storeFile) { - selectFile(storeFile); - clearStoreFile(); - } - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - const handleUpload = async () => { - const id = await startUpload(); - if (id) setPhase('processing'); - }; - - const handleReset = () => { - reset(); - setPhase('upload'); }; const qualityOptions: { value: Quality; label: string; desc: string }[] = [ @@ -66,94 +25,35 @@ export default function PdfCompressor() { { value: 'high', label: t('tools.compressPdf.qualityHigh'), desc: '300 DPI' }, ]; - const schema = generateToolSchema({ - name: t('tools.compressPdf.title'), - description: t('tools.compressPdf.description'), - url: `${window.location.origin}/tools/compress-pdf`, - }); + const getExtraData = () => ({ quality }); return ( - <> - - {t('tools.compressPdf.title')} — {t('common.appName')} - - - - - -
-
-
- -
-

{t('tools.compressPdf.title')}

-

{t('tools.compressPdf.description')}

-
- - - - {phase === 'upload' && ( -
- - - {/* Quality Selector */} - {file && !isUploading && ( - <> -
- {qualityOptions.map((opt) => ( - - ))} -
- - - )} -
- )} - - {phase === 'processing' && !result && ( - - )} - - {phase === 'done' && result && result.status === 'completed' && ( - - )} - - {phase === 'done' && taskError && ( -
-
-

{taskError}

+ ))}
-
- )} - - -
- +
+ )} + ); } diff --git a/frontend/src/design-system/colors.ts b/frontend/src/design-system/colors.ts new file mode 100644 index 0000000..72acae6 --- /dev/null +++ b/frontend/src/design-system/colors.ts @@ -0,0 +1,250 @@ +/** + * Color Design System for Dociva + * Comprehensive color palette matching competitive standards + * References: PDFSimpli (bright blues), Smallpdf (modern purple), ILovePDF (bold oranges) + */ + +export const colors = { + // Primary Palette (Main brand color - Blue) + primary: { + 50: '#eff6ff', + 100: '#dbeafe', + 200: '#bfdbfe', + 300: '#93c5fd', + 400: '#60a5fa', + 500: '#3b82f6', + 600: '#2563eb', // Primary brand color (buttons, links, highlights) + 700: '#1d4ed8', + 800: '#1e40af', + 900: '#1e3a8a', + 950: '#172554', + }, + + // Accent Palette (Secondary accent - Purple/Fuchsia for CTAs) + accent: { + 50: '#fdf4ff', + 100: '#fae8ff', + 200: '#f5d0fe', + 300: '#f0abfc', + 400: '#e879f9', + 500: '#d946ef', + 600: '#c026d3', // Accent for premium tier, special offers + 700: '#a21caf', + 800: '#86198f', + 900: '#701a75', + }, + + // Success Palette (For positive feedback, completed actions) + success: { + 50: '#f0fdf4', + 100: '#dcfce7', + 200: '#bbf7d0', + 300: '#86efac', + 400: '#4ade80', + 500: '#22c55e', + 600: '#16a34a', // Success button/feedback + 700: '#15803d', + 800: '#166534', + 900: '#145231', + }, + + // Warning Palette (For alerts, warnings, secondary actions) + warning: { + 50: '#fffbeb', + 100: '#fef3c7', + 200: '#fde68a', + 300: '#fcd34d', + 400: '#fbbf24', + 500: '#f59e0b', + 600: '#d97706', // Warning alerts + 700: '#b45309', + 800: '#92400e', + 900: '#78350f', + }, + + // Error Palette (For errors, destructive actions) + error: { + 50: '#fef2f2', + 100: '#fee2e2', + 200: '#fecaca', + 300: '#fca5a5', + 400: '#f87171', + 500: '#ef4444', + 600: '#dc2626', // Error states + 700: '#b91c1c', + 800: '#991b1b', + 900: '#7f1d1d', + }, + + // Info Palette (For informational messages) + info: { + 50: '#ecf0ff', + 100: '#e0e7ff', + 200: '#c7d2fe', + 300: '#a5b4fc', + 400: '#818cf8', + 500: '#6366f1', + 600: '#4f46e5', // Info messages + 700: '#4338ca', + 800: '#3730a3', + 900: '#312e81', + }, + + // Neutral Grayscale (For text, borders, backgrounds) + neutral: { + 50: '#fafafa', + 100: '#f5f5f5', + 150: '#efefef', // Custom: between 100 and 200 + 200: '#e5e5e5', + 300: '#d4d4d4', + 400: '#a3a3a3', + 500: '#737373', + 600: '#525252', + 700: '#404040', + 800: '#262626', + 900: '#171717', + 950: '#0a0a0a', + }, + + // Slate Grayscale (Alternative neutral - used in current design) + slate: { + 50: '#f8fafc', + 100: '#f1f5f9', + 200: '#e2e8f0', + 300: '#cbd5e1', + 400: '#94a3b8', + 500: '#64748b', + 600: '#475569', + 700: '#334155', + 800: '#1e293b', + 900: '#0f172a', + 950: '#020617', + }, + + // Semantic Colors (Light mode) + light: { + background: '#ffffff', + surface: '#f8fafc', + surfaceHover: '#f1f5f9', + text: '#0f172a', + textSecondary: '#64748b', + textTertiary: '#94a3b8', + border: '#e2e8f0', + borderHover: '#cbd5e1', + }, + + // Semantic Colors (Dark mode) + dark: { + background: '#0f172a', + surface: '#1e293b', + surfaceHover: '#334155', + text: '#f1f5f9', + textSecondary: '#94a3b8', + textTertiary: '#64748b', + border: '#334155', + borderHover: '#475569', + }, + + // Tool Category Colors (for visual differentiation) + tools: { + pdf: '#dc2626', // Red for PDF tools + image: '#f59e0b', // Amber for image tools + video: '#06b6d4', // Cyan for video tools + document: '#3b82f6', // Blue for document tools + text: '#8b5cf6', // Violet for text tools + convert: '#ec4899', // Pink for conversion tools + edit: '#10b981', // Emerald for editing tools + secure: '#f97316', // Orange for security tools + }, + + // Premium Gradient (for Pro/Business badges) + gradients: { + premium: { + from: '#f59e0b', + to: '#d97706', + }, + business: { + from: '#8b5cf6', + to: '#6366f1', + }, + featured: { + from: '#06b6d4', + to: '#0ea5e9', + }, + }, +} as const; + +/** + * Color Assignments for UI Elements + * These are semantic usage guidelines + */ +export const colorAssignments = { + // Button Colors + buttons: { + primary: 'primary-600', // Main CTAs + secondary: 'slate-600', // Secondary actions + success: 'success-600', // Confirm/accept + danger: 'error-600', // Delete/destructive + ghost: 'slate-500', // Tertiary/icon buttons + }, + + // Badge/Pill Colors + badges: { + default: 'slate-100', + success: 'success-100', + warning: 'warning-100', + error: 'error-100', + info: 'info-100', + pro: 'accent-100', + }, + + // Alert/Toast Colors + alerts: { + success: 'success-600', + warning: 'warning-600', + error: 'error-600', + info: 'info-600', + }, + + // Text Colors + text: { + primary: 'slate-900', + secondary: 'slate-600', + tertiary: 'slate-500', + muted: 'slate-400', + link: 'primary-600', + linkHover: 'primary-700', + }, + + // Border Colors + borders: { + default: 'slate-200', + focus: 'primary-500', + error: 'error-400', + success: 'success-400', + }, + + // Background Colors + backgrounds: { + page: 'white', + surface: 'slate-50', + surfaceAlt: 'slate-100', + overlay: 'rgba(0, 0, 0, 0.5)', + }, +} as const; + +/** + * Utility function to get color with proper contrast + * @param colorName - The color name (e.g., 'primary', 'error') + * @param lightValue - Shade number for light mode (e.g., 600) + * @param darkValue - Shade number for dark mode (e.g., 500) + */ +export function getColorClass( + colorName: keyof typeof colors, + lightValue: number = 600, + darkValue: number = 500 +): string { + return `${colorName}-${lightValue} dark:${colorName}-${darkValue}`; +} + +export default colors; diff --git a/frontend/src/design-system/components-registry.ts b/frontend/src/design-system/components-registry.ts new file mode 100644 index 0000000..a76e1e7 --- /dev/null +++ b/frontend/src/design-system/components-registry.ts @@ -0,0 +1,512 @@ +/** + * Component Registry & System + * Central registry of all UI components with metadata and styling + */ + +import { LucideIcon } from 'lucide-react'; + +/** + * Component Type Definitions + */ +export interface ComponentMetadata { + name: string; + description: string; + category: 'button' | 'input' | 'card' | 'layout' | 'overlay' | 'feedback' | 'navigation' | 'form'; + variants?: string[]; + props?: Record; + a11y?: { + role?: string; + ariaLabel?: string; + ariaDescribedBy?: string; + }; +} + +export interface ComponentColors { + light: { + bg: string; + text: string; + border: string; + hover?: string; + }; + dark: { + bg: string; + text: string; + border: string; + hover?: string; + }; +} + +export interface ToolCardMetadata { + name: string; + slug: string; + icon: string; // Lucide icon name + category: 'pdf' | 'image' | 'video' | 'document' | 'text' | 'convert' | 'edit' | 'secure'; + colorBg: string; // Tailwind bg class + colorIcon: string; // Tailwind text color class + description: string; + i18nKey: string; + isPremium?: boolean; + isNew?: boolean; + isPopular?: boolean; + orderPriority: number; // 1 = highest priority on homepage +} + +/** + * UI Component Registry + */ +export const componentRegistry: Record = { + // Buttons + button: { + name: 'Button', + description: 'Primary interactive element', + category: 'button', + variants: ['primary', 'secondary', 'success', 'danger', 'ghost', 'icon'], + }, + buttonPrimary: { + name: 'Primary Button', + description: 'Main call-to-action button', + category: 'button', + a11y: { role: 'button' }, + }, + buttonSecondary: { + name: 'Secondary Button', + description: 'Alternative action button', + category: 'button', + }, + buttonIcon: { + name: 'Icon Button', + description: 'Icon-only interactive button', + category: 'button', + a11y: { role: 'button', ariaLabel: 'Required for icon buttons' }, + }, + + // Inputs & Forms + input: { + name: 'Text Input', + description: 'Single-line text input field', + category: 'input', + }, + inputFile: { + name: 'File Input', + description: 'File upload field', + category: 'input', + }, + fileUploader: { + name: 'File Uploader', + description: 'Drag-and-drop file upload zone', + category: 'input', + }, + formSelect: { + name: 'Select Dropdown', + description: 'Option selection dropdown', + category: 'form', + }, + formCheckbox: { + name: 'Checkbox', + description: 'Multi-select checkbox input', + category: 'form', + }, + formRadio: { + name: 'Radio Button', + description: 'Single-select radio input', + category: 'form', + }, + formToggle: { + name: 'Toggle Switch', + description: 'On/off toggle switch', + category: 'form', + }, + + // Cards & Containers + card: { + name: 'Card', + description: 'Elevation container with padding', + category: 'card', + }, + toolCard: { + name: 'Tool Card', + description: 'Tool preview card for homepage grid', + category: 'card', + }, + pricingCard: { + name: 'Pricing Card', + description: 'Subscription plan card', + category: 'card', + }, + + // Layout + header: { + name: 'Header', + description: 'Application header with navigation', + category: 'layout', + }, + footer: { + name: 'Footer', + description: 'Application footer', + category: 'layout', + }, + sidebar: { + name: 'Sidebar', + description: 'Side navigation panel', + category: 'layout', + }, + container: { + name: 'Container', + description: 'Max-width wrapper', + category: 'layout', + }, + + // Feedback + alert: { + name: 'Alert', + description: 'Alert message container', + category: 'feedback', + variants: ['success', 'warning', 'error', 'info'], + }, + badge: { + name: 'Badge', + description: 'Small labeling component', + category: 'feedback', + }, + progressBar: { + name: 'Progress Bar', + description: 'Linear progress indicator', + category: 'feedback', + }, + spinner: { + name: 'Spinner', + description: 'Loading spinner indicator', + category: 'feedback', + }, + toast: { + name: 'Toast', + description: 'Temporary notification message', + category: 'feedback', + }, + + // Overlay & Navigation + modal: { + name: 'Modal Dialog', + description: 'Modal dialog overlay', + category: 'overlay', + }, + dropdown: { + name: 'Dropdown Menu', + description: 'Dropdown menu with options', + category: 'navigation', + }, + tabs: { + name: 'Tabs', + description: 'Tabbed content navigation', + category: 'navigation', + }, + breadcrumb: { + name: 'Breadcrumb', + description: 'Breadcrumb navigation trail', + category: 'navigation', + }, + pagination: { + name: 'Pagination', + description: 'Page navigation controls', + category: 'navigation', + }, +}; + +/** + * Tool Categories with Colors + */ +export const toolCategories = { + pdf: { + name: 'PDF Tools', + color: 'red-600', + bgLight: 'bg-red-50', + bgDark: 'dark:bg-red-900/20', + borderColor: 'border-red-200 dark:border-red-800', + }, + image: { + name: 'Image Tools', + color: 'amber-600', + bgLight: 'bg-amber-50', + bgDark: 'dark:bg-amber-900/20', + borderColor: 'border-amber-200 dark:border-amber-800', + }, + video: { + name: 'Video Tools', + color: 'cyan-600', + bgLight: 'bg-cyan-50', + bgDark: 'dark:bg-cyan-900/20', + borderColor: 'border-cyan-200 dark:border-cyan-800', + }, + document: { + name: 'Document Tools', + color: 'blue-600', + bgLight: 'bg-blue-50', + bgDark: 'dark:bg-blue-900/20', + borderColor: 'border-blue-200 dark:border-blue-800', + }, + text: { + name: 'Text Tools', + color: 'violet-600', + bgLight: 'bg-violet-50', + bgDark: 'dark:bg-violet-900/20', + borderColor: 'border-violet-200 dark:border-violet-800', + }, + convert: { + name: 'Conversion Tools', + color: 'pink-600', + bgLight: 'bg-pink-50', + bgDark: 'dark:bg-pink-900/20', + borderColor: 'border-pink-200 dark:border-pink-800', + }, + edit: { + name: 'Editing Tools', + color: 'emerald-600', + bgLight: 'bg-emerald-50', + bgDark: 'dark:bg-emerald-900/20', + borderColor: 'border-emerald-200 dark:border-emerald-800', + }, + secure: { + name: 'Security Tools', + color: 'orange-600', + bgLight: 'bg-orange-50', + bgDark: 'dark:bg-orange-900/20', + borderColor: 'border-orange-200 dark:border-orange-800', + }, +} as const; + +/** + * Complete Tool Registry + * This should be updated to include ALL 40+ tools + */ +export const toolRegistry: Record = { + pdfCompressor: { + name: 'Compress PDF', + slug: 'compress-pdf', + icon: 'Minimize2', + category: 'pdf', + colorBg: 'bg-red-50 dark:bg-red-900/20', + colorIcon: 'text-red-600 dark:text-red-400', + description: 'Reduce PDF file size without losing quality', + i18nKey: 'tools.compressPdf.title', + isPopular: true, + orderPriority: 1, + }, + pdfToWord: { + name: 'PDF to Word', + slug: 'pdf-to-word', + icon: 'FileText', + category: 'convert', + colorBg: 'bg-pink-50 dark:bg-pink-900/20', + colorIcon: 'text-pink-600 dark:text-pink-400', + description: 'Convert PDF documents to editable Word files', + i18nKey: 'tools.pdfToWord.title', + isPopular: true, + orderPriority: 2, + }, + wordToPdf: { + name: 'Word to PDF', + slug: 'word-to-pdf', + icon: 'FilePdf', + category: 'convert', + colorBg: 'bg-blue-50 dark:bg-blue-900/20', + colorIcon: 'text-blue-600 dark:text-blue-400', + description: 'Convert Word documents to PDF format', + i18nKey: 'tools.wordToPdf.title', + isPopular: true, + orderPriority: 3, + }, + mergePdf: { + name: 'Merge PDF', + slug: 'merge-pdf', + icon: 'Layers', + category: 'pdf', + colorBg: 'bg-violet-50 dark:bg-violet-900/20', + colorIcon: 'text-violet-600 dark:text-violet-400', + description: 'Combine multiple PDF files into one', + i18nKey: 'tools.mergePdf.title', + isPopular: true, + orderPriority: 4, + }, + splitPdf: { + name: 'Split PDF', + slug: 'split-pdf', + icon: 'Scissors', + category: 'pdf', + colorBg: 'bg-pink-50 dark:bg-pink-900/20', + colorIcon: 'text-pink-600 dark:text-pink-400', + description: 'Extract pages or split PDF into separate files', + i18nKey: 'tools.splitPdf.title', + orderPriority: 5, + }, + rotatePdf: { + name: 'Rotate PDF', + slug: 'rotate-pdf', + icon: 'RotateCw', + category: 'edit', + colorBg: 'bg-teal-50 dark:bg-teal-900/20', + colorIcon: 'text-teal-600 dark:text-teal-400', + description: 'Rotate PDF pages at any angle', + i18nKey: 'tools.rotatePdf.title', + orderPriority: 6, + }, + pdfToImages: { + name: 'PDF to Images', + slug: 'pdf-to-images', + icon: 'Image', + category: 'convert', + colorBg: 'bg-amber-50 dark:bg-amber-900/20', + colorIcon: 'text-amber-600 dark:text-amber-400', + description: 'Convert PDF pages to individual image files', + i18nKey: 'tools.pdfToImages.title', + orderPriority: 7, + }, + imagesToPdf: { + name: 'Images to PDF', + slug: 'images-to-pdf', + icon: 'FileImage', + category: 'convert', + colorBg: 'bg-lime-50 dark:bg-lime-900/20', + colorIcon: 'text-lime-600 dark:text-lime-400', + description: 'Combine images into a single PDF file', + i18nKey: 'tools.imagesToPdf.title', + orderPriority: 8, + }, + watermarkPdf: { + name: 'Watermark PDF', + slug: 'watermark-pdf', + icon: 'Droplets', + category: 'edit', + colorBg: 'bg-cyan-50 dark:bg-cyan-900/20', + colorIcon: 'text-cyan-600 dark:text-cyan-400', + description: 'Add watermarks to PDF documents', + i18nKey: 'tools.watermarkPdf.title', + orderPriority: 9, + }, + protectPdf: { + name: 'Protect PDF', + slug: 'protect-pdf', + icon: 'Lock', + category: 'secure', + colorBg: 'bg-red-50 dark:bg-red-900/20', + colorIcon: 'text-red-600 dark:text-red-400', + description: 'Password-protect PDF files', + i18nKey: 'tools.protectPdf.title', + isPremium: true, + orderPriority: 10, + }, + unlockPdf: { + name: 'Unlock PDF', + slug: 'unlock-pdf', + icon: 'Unlock', + category: 'secure', + colorBg: 'bg-green-50 dark:bg-green-900/20', + colorIcon: 'text-green-600 dark:text-green-400', + description: 'Remove password protection from PDF files', + i18nKey: 'tools.unlockPdf.title', + isPremium: true, + orderPriority: 11, + }, + addPageNumbers: { + name: 'Add Page Numbers', + slug: 'add-page-numbers', + icon: 'ListOrdered', + category: 'edit', + colorBg: 'bg-sky-50 dark:bg-sky-900/20', + colorIcon: 'text-sky-600 dark:text-sky-400', + description: 'Add page numbers to PDF documents', + i18nKey: 'tools.addPageNumbers.title', + isPremium: true, + orderPriority: 12, + }, + imageConverter: { + name: 'Image Converter', + slug: 'image-converter', + icon: 'ImageIcon', + category: 'image', + colorBg: 'bg-purple-50 dark:bg-purple-900/20', + colorIcon: 'text-purple-600 dark:text-purple-400', + description: 'Convert images between different formats', + i18nKey: 'tools.imageConverter.title', + orderPriority: 13, + }, + videoToGif: { + name: 'Video to GIF', + slug: 'video-to-gif', + icon: 'Film', + category: 'video', + colorBg: 'bg-emerald-50 dark:bg-emerald-900/20', + colorIcon: 'text-emerald-600 dark:text-emerald-400', + description: 'Convert video files to animated GIFs', + i18nKey: 'tools.videoToGif.title', + isPremium: true, + orderPriority: 14, + }, + wordCounter: { + name: 'Word Counter', + slug: 'word-counter', + icon: 'Hash', + category: 'text', + colorBg: 'bg-blue-50 dark:bg-blue-900/20', + colorIcon: 'text-blue-600 dark:text-blue-400', + description: 'Count words, characters, and paragraphs', + i18nKey: 'tools.wordCounter.title', + orderPriority: 15, + }, + textCleaner: { + name: 'Text Cleaner', + slug: 'text-cleaner', + icon: 'Eraser', + category: 'text', + colorBg: 'bg-indigo-50 dark:bg-indigo-900/20', + colorIcon: 'text-indigo-600 dark:text-indigo-400', + description: 'Clean and format text content', + i18nKey: 'tools.textCleaner.title', + orderPriority: 16, + }, +}; + +/** + * Get all tools sorted by priority + */ +export function getToolsByPriority(): ToolCardMetadata[] { + return Object.values(toolRegistry).sort( + (a, b) => a.orderPriority - b.orderPriority + ); +} + +/** + * Get tools by category + */ +export function getToolsByCategory( + category: ToolCardMetadata['category'] +): ToolCardMetadata[] { + return Object.values(toolRegistry) + .filter((tool) => tool.category === category) + .sort((a, b) => a.orderPriority - b.orderPriority); +} + +/** + * Get popular tools + */ +export function getPopularTools(): ToolCardMetadata[] { + return Object.values(toolRegistry) + .filter((tool) => tool.isPopular) + .sort((a, b) => a.orderPriority - b.orderPriority); +} + +/** + * Get premium tools + */ +export function getPremiumTools(): ToolCardMetadata[] { + return Object.values(toolRegistry).filter((tool) => tool.isPremium); +} + +export default { + componentRegistry, + toolRegistry, + toolCategories, + getToolsByPriority, + getToolsByCategory, + getPopularTools, + getPremiumTools, +}; diff --git a/frontend/src/design-system/theme.ts b/frontend/src/design-system/theme.ts new file mode 100644 index 0000000..99e57c0 --- /dev/null +++ b/frontend/src/design-system/theme.ts @@ -0,0 +1,296 @@ +/** + * Design System Theme Configuration + * Centralized theme utilities and token system + */ + +import colors, { colorAssignments, getColorClass } from './colors'; + +/** + * Typography Scale + * Matches Tailwind CSS + custom adjustments for accessibility + */ +export const typography = { + // Headings + h1: { + fontSize: '3rem', // 48px + fontWeight: 700, + lineHeight: '3.5rem', // 56px + letterSpacing: '-0.02em', + }, + h2: { + fontSize: '2.25rem', // 36px + fontWeight: 700, + lineHeight: '2.5rem', // 40px + letterSpacing: '-0.01em', + }, + h3: { + fontSize: '1.875rem', // 30px + fontWeight: 600, + lineHeight: '2.25rem', // 36px + letterSpacing: '-0.01em', + }, + h4: { + fontSize: '1.5rem', // 24px + fontWeight: 600, + lineHeight: '2rem', // 32px + }, + h5: { + fontSize: '1.25rem', // 20px + fontWeight: 600, + lineHeight: '1.75rem', // 28px + }, + h6: { + fontSize: '1rem', // 16px + fontWeight: 600, + lineHeight: '1.5rem', // 24px + }, + + // Body text + body: { + large: { + fontSize: '1.125rem', // 18px + fontWeight: 400, + lineHeight: '1.75rem', // 28px + }, + base: { + fontSize: '1rem', // 16px + fontWeight: 400, + lineHeight: '1.5rem', // 24px + }, + small: { + fontSize: '0.875rem', // 14px + fontWeight: 400, + lineHeight: '1.25rem', // 20px + }, + xs: { + fontSize: '0.75rem', // 12px + fontWeight: 400, + lineHeight: '1rem', // 16px + }, + }, + + // Labels & UI text + label: { + fontSize: '0.875rem', // 14px + fontWeight: 500, + lineHeight: '1.25rem', // 20px + }, + caption: { + fontSize: '0.75rem', // 12px + fontWeight: 500, + lineHeight: '1rem', // 16px + }, +} as const; + +/** + * Spacing Scale + * 4px base unit (Tailwind default) + */ +export const spacing = { + xs: '0.25rem', // 4px + sm: '0.5rem', // 8px + md: '1rem', // 16px + lg: '1.5rem', // 24px + xl: '2rem', // 32px + '2xl': '2.5rem', // 40px + '3xl': '3rem', // 48px + '4xl': '4rem', // 64px + '5xl': '5rem', // 80px + '6xl': '6rem', // 96px +} as const; + +/** + * Border Radius Scale + */ +export const borderRadius = { + none: '0', + sm: '0.375rem', // 6px + base: '0.5rem', // 8px + md: '0.75rem', // 12px + lg: '1rem', // 16px + xl: '1.25rem', // 20px + '2xl': '1.5rem', // 24px + full: '9999px', +} as const; + +/** + * Shadow Scale + */ +export const shadows = { + none: '0 0 #0000', + xs: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', + sm: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', + md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', + lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', + xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', + '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)', + lg_dark: '0 20px 25px -5px rgba(0, 0, 0, 0.5)', +} as const; + +/** + * Z-Index Scale + * Structured layering system + */ +export const zIndex = { + auto: 'auto', + hide: '-1', + base: '0', + dropdown: '1000', + sticky: '1010', + fixed: '1020', + backdrop: '1030', + offcanvas: '1040', + modal: '1050', + popover: '1060', + tooltip: '1070', + notification: '1080', +} as const; + +/** + * Transitions & Animations + */ +export const transitions = { + fast: '0.15s', + base: '0.2s', + slow: '0.3s', + slower: '0.5s', + + easing: { + in: 'cubic-bezier(0.4, 0, 1, 1)', + out: 'cubic-bezier(0, 0, 0.2, 1)', + inOut: 'cubic-bezier(0.4, 0, 0.2, 1)', + }, +} as const; + +/** + * Breakpoints (matching Tailwind) + */ +export const breakpoints = { + xs: '0px', + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', +} as const; + +/** + * Container Width + */ +export const containers = { + sm: '24rem', // 384px + md: '28rem', // 448px + lg: '32rem', // 512px + xl: '36rem', // 576px + '2xl': '42rem', // 672px + '3xl': '48rem', // 768px + '4xl': '56rem', // 896px + '5xl': '64rem', // 1024px + '6xl': '72rem', // 1152px + '7xl': '80rem', // 1280px +} as const; + +/** + * Component Size Presets + */ +export const componentSizes = { + // Button sizes + button: { + xs: { + padding: '0.375rem 0.75rem', + fontSize: '0.75rem', + height: '1.5rem', + }, + sm: { + padding: '0.5rem 1rem', + fontSize: '0.875rem', + height: '2rem', + }, + md: { + padding: '0.75rem 1.5rem', + fontSize: '1rem', + height: '2.5rem', + }, + lg: { + padding: '1rem 2rem', + fontSize: '1.125rem', + height: '3rem', + }, + xl: { + padding: '1.25rem 2.5rem', + fontSize: '1.25rem', + height: '3.5rem', + }, + }, + + // Input sizes + input: { + sm: { padding: '0.375rem 0.75rem', fontSize: '0.875rem' }, + md: { padding: '0.75rem 1rem', fontSize: '1rem' }, + lg: { padding: '1rem 1.25rem', fontSize: '1.125rem' }, + }, + + // Icon sizes + icon: { + xs: '1rem', // 16px + sm: '1.25rem', // 20px + md: '1.5rem', // 24px + lg: '2rem', // 32px + xl: '2.5rem', // 40px + '2xl': '3rem', // 48px + }, +} as const; + +/** + * Responsive Utilities + */ +export const responsive = { + // Stack direction + stackMobile: 'flex flex-col', + stackDesktop: 'lg:flex-row', + + // Grid column count + gridAuto: 'grid gap-4', + grid2: 'grid grid-cols-1 sm:grid-cols-2 gap-4', + grid3: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4', + grid4: 'grid grid-cols-2 lg:grid-cols-4 gap-4', + + // Common padding + pagePaddingX: 'px-4 sm:px-6 lg:px-8', + pagePaddingY: 'py-8 sm:py-12 lg:py-16', + sectionPadding: 'px-4 sm:px-6 lg:px-8 py-8 sm:py-12 lg:py-16', + + // Container width + containerMax: 'max-w-7xl', + containerContent: 'max-w-4xl', + containerSmall: 'max-w-2xl', +} as const; + +/** + * Complete Theme Object + */ +export const theme = { + colors, + colorAssignments, + typography, + spacing, + borderRadius, + shadows, + zIndex, + transitions, + breakpoints, + containers, + componentSizes, + responsive, +} as const; + +/** + * Utility: Get CSS variable or Tailwind class for a color + */ +export const useColor = (semantic: string, mode: 'light' | 'dark' = 'light') => { + const colorObj = mode === 'light' ? colors.light : colors.dark; + return colorObj[semantic as keyof typeof colorObj]; +}; + +export default theme; diff --git a/nginx/nginx.dev.conf b/nginx/nginx.dev.conf new file mode 100644 index 0000000..a72a3fc --- /dev/null +++ b/nginx/nginx.dev.conf @@ -0,0 +1,40 @@ +upstream frontend { + server frontend:5173; +} + +# ── HTTP Development Server ── +server { + listen 80 default_server; + client_max_body_size 100M; + resolver 127.0.0.11 valid=30s ipv6=off; + set $backend_upstream backend:5000; + + # API requests → Flask backend + location /api/ { + proxy_pass http://$backend_upstream; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Timeout for large file uploads + proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_connect_timeout 60s; + } + + # Frontend (Vite dev server) + location / { + proxy_pass http://frontend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Health check + location /health { + proxy_pass http://$backend_upstream/api/health; + } +}