diff --git a/cleanup_expired.py b/cleanup_expired.py index 0e8b5c7..5508abd 100755 --- a/cleanup_expired.py +++ b/cleanup_expired.py @@ -9,12 +9,12 @@ import json from datetime import datetime, timezone from pathlib import Path -# Add src directory to Python path -src_path = Path(__file__).parent / "src" -sys.path.insert(0, str(src_path)) +# Add the project root to Python path +project_root = Path(__file__).parent +sys.path.append(str(project_root)) -from config import config -from storage import StorageManager +from src.config import config +from src.storage import StorageManager def cleanup_local_storage(storage_manager): diff --git a/src/app.py b/src/app.py index 32fdf70..5ab1885 100644 --- a/src/app.py +++ b/src/app.py @@ -7,12 +7,17 @@ import hashlib import re from datetime import datetime, timedelta from functools import wraps -from urllib.parse import urlparse from flask import Flask, render_template, request, jsonify, redirect, url_for, send_from_directory, session, Response +import sys +import os + +# Add project root to Python path +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(project_root) + +# Import modules from current directory and project root from config import config from storage import StorageManager -import sys -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from expiry_db import ExpiryDatabase # Flask app configuration @@ -110,52 +115,7 @@ except Exception as e: def generate_short_id(length=6): return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) -# URL Shortener utility functions -def generate_short_code(length=6): - """Generate a random short code for URL shortener""" - characters = string.ascii_letters + string.digits - while True: - code = ''.join(random.choices(characters, k=length)) - # Make sure it doesn't conflict with existing content IDs - content_type, _ = detect_content_type(code) - if content_type is None and not expiry_db.get_redirect(code): - return code -def is_valid_url(url): - """Validate if a URL is properly formatted""" - try: - result = urlparse(url) - return all([result.scheme, result.netloc]) and result.scheme in ['http', 'https'] - except: - return False - -def is_safe_url(url): - """Basic safety check for URLs (can be extended)""" - # Block localhost, private IPs, and suspicious TLDs - parsed = urlparse(url) - hostname = parsed.hostname - - if not hostname: - return False - - # Block localhost and private IPs - if hostname.lower() in ['localhost', '127.0.0.1', '0.0.0.0']: - return False - - # Block private IP ranges (basic check) - if hostname.startswith('192.168.') or hostname.startswith('10.') or hostname.startswith('172.'): - return False - - # Block suspicious patterns - suspicious_patterns = [ - r'bit\.ly', r'tinyurl', r'short\.link', r'malware', r'phishing' - ] - - for pattern in suspicious_patterns: - if re.search(pattern, url, re.IGNORECASE): - return False - - return True # Helper function to determine if an ID is a file or paste def detect_content_type(content_id): @@ -196,18 +156,11 @@ def index(): @check_maintenance def get_content(content_id): """ - Clean URL handler - checks for URL redirect first, then files/pastes + Clean URL handler for files and pastes Examples: sharey.org/ABC123, sharey.org/XYZ789.png """ print(f"🔍 Processing request for ID: {content_id}") - # First check if it's a URL redirect - redirect_url = expiry_db.get_redirect(content_id) - if redirect_url: - print(f"🔗 Redirecting {content_id} to {redirect_url}") - return redirect(redirect_url, code=302) - - # Not a redirect, try as file/paste # Skip certain paths that should not be treated as content IDs excluded_paths = ['admin', 'health', 'api', 'static', 'files', 'pastes', 'favicon.ico'] if content_id in excluded_paths: @@ -447,121 +400,7 @@ def view_paste_raw(paste_id): except Exception as e: return jsonify({'error': 'Paste not found'}), 404 -# URL Shortener API Routes -@app.route('/api/shorten', methods=['POST']) -@check_maintenance -def create_redirect(): - """Create a new URL redirect""" - try: - data = request.get_json() - - if not data or 'url' not in data: - return jsonify({'error': 'URL is required'}), 400 - - target_url = data['url'].strip() - custom_code = data.get('code', '').strip() - expires_in_hours = data.get('expires_in_hours') - - # Validate URL - if not is_valid_url(target_url): - return jsonify({'error': 'Invalid URL format'}), 400 - - if not is_safe_url(target_url): - return jsonify({'error': 'URL not allowed'}), 400 - - # Generate or validate short code - if custom_code: - # Custom code provided - if len(custom_code) < 3 or len(custom_code) > 20: - return jsonify({'error': 'Custom code must be 3-20 characters'}), 400 - - if not re.match(r'^[a-zA-Z0-9_-]+$', custom_code): - return jsonify({'error': 'Custom code can only contain letters, numbers, hyphens, and underscores'}), 400 - - # Check if code already exists - if expiry_db.get_redirect(custom_code) or detect_content_type(custom_code)[0]: - return jsonify({'error': 'Code already exists'}), 400 - - short_code = custom_code - else: - # Generate random code - short_code = generate_short_code() - - # Calculate expiry - expires_at = None - if expires_in_hours and expires_in_hours > 0: - expires_at = (datetime.utcnow() + timedelta(hours=expires_in_hours)).isoformat() + 'Z' - - # Get client IP for logging - client_ip = request.headers.get('X-Forwarded-For', request.remote_addr) - - # Create redirect - success = expiry_db.add_redirect( - short_code=short_code, - target_url=target_url, - expires_at=expires_at, - created_by_ip=client_ip - ) - - if not success: - return jsonify({'error': 'Failed to create redirect'}), 500 - - # Return short URL - short_url = url_for('get_content', content_id=short_code, _external=True) - - return jsonify({ - 'short_url': short_url, - 'short_code': short_code, - 'target_url': target_url, - 'expires_at': expires_at - }), 201 - - except Exception as e: - return jsonify({'error': f'Failed to create redirect: {str(e)}'}), 500 -@app.route('/api/redirect//info', methods=['GET']) -@check_maintenance -def redirect_info(short_code): - """Get information about a redirect (for preview)""" - try: - import sqlite3 - - # Get redirect info without incrementing click count - conn = sqlite3.connect(expiry_db.db_path) - cursor = conn.cursor() - - cursor.execute(''' - SELECT target_url, created_at, expires_at, click_count, is_active - FROM url_redirects - WHERE short_code = ? - ''', (short_code,)) - - result = cursor.fetchone() - conn.close() - - if not result: - return jsonify({'error': 'Redirect not found'}), 404 - - target_url, created_at, expires_at, click_count, is_active = result - - # Check if expired - is_expired = False - if expires_at: - current_time = datetime.utcnow().isoformat() + 'Z' - is_expired = expires_at <= current_time - - return jsonify({ - 'short_code': short_code, - 'target_url': target_url, - 'created_at': created_at, - 'expires_at': expires_at, - 'click_count': click_count, - 'is_active': bool(is_active), - 'is_expired': is_expired - }) - - except Exception as e: - return jsonify({'error': f'Failed to get redirect info: {str(e)}'}), 500 # Admin Panel Routes @app.route('/admin') diff --git a/src/static/script.js b/src/static/script.js index 6fccbcb..665f129 100644 --- a/src/static/script.js +++ b/src/static/script.js @@ -13,26 +13,16 @@ document.addEventListener("DOMContentLoaded", function() { const fileModeButton = document.getElementById('fileMode'); const pasteModeButton = document.getElementById('pasteMode'); - const urlModeButton = document.getElementById('urlMode'); const faqButton = document.getElementById('faqButton'); const fileSharingSection = document.getElementById('fileSharingSection'); const pastebinSection = document.getElementById('pastebinSection'); - const urlShortenerSection = document.getElementById('urlShortenerSection'); const faqSection = document.getElementById('faqSection'); const pasteContent = document.getElementById('pasteContent'); const submitPasteButton = document.getElementById('submitPaste'); const pasteResult = document.getElementById('pasteResult'); - // URL Shortener elements - const urlInput = document.getElementById('urlInput'); - const customCode = document.getElementById('customCode'); - const urlExpiry = document.getElementById('urlExpiry'); - const shortenUrlButton = document.getElementById('shortenUrl'); - const urlResult = document.getElementById('urlResult'); - const urlPreviewContainer = document.getElementById('urlPreviewContainer'); - // Mobile clipboard elements const mobileClipboardSection = document.getElementById('mobileClipboardSection'); const mobilePasteButton = document.getElementById('mobilePasteButton'); @@ -40,11 +30,9 @@ document.addEventListener("DOMContentLoaded", function() { let filesToUpload = []; let currentMode = 'file'; // Track current mode: 'file', 'paste', or 'faq' - // Expiry elements + // Expiry elements (shared for both files and pastes) const expirySelect = document.getElementById('expirySelect'); const customExpiry = document.getElementById('customExpiry'); - const pasteExpirySelect = document.getElementById('pasteExpirySelect'); - const customPasteExpiry = document.getElementById('customPasteExpiry'); // Helper function to check if file is an image function isImageFile(file) { @@ -65,29 +53,15 @@ document.addEventListener("DOMContentLoaded", function() { currentMode = 'paste'; showSection(pastebinSection); hideSection(fileSharingSection); - hideSection(urlShortenerSection); hideSection(faqSection); activateButton(pasteModeButton); }); - urlModeButton.addEventListener('click', () => { - currentMode = 'url'; - showSection(urlShortenerSection); - hideSection(fileSharingSection); - hideSection(pastebinSection); - hideSection(faqSection); - activateButton(urlModeButton); - - // Initialize URL shortener after section is visible - initializeUrlShortener(); - }); - faqButton.addEventListener('click', () => { currentMode = 'faq'; showSection(faqSection); hideSection(fileSharingSection); hideSection(pastebinSection); - hideSection(urlShortenerSection); activateButton(faqButton); }); @@ -103,7 +77,7 @@ document.addEventListener("DOMContentLoaded", function() { // Helper function to activate a button function activateButton(button) { - const buttons = [fileModeButton, pasteModeButton, urlModeButton, faqButton]; + const buttons = [fileModeButton, pasteModeButton, faqButton]; buttons.forEach(btn => btn.classList.remove('active')); button.classList.add('active'); } @@ -125,16 +99,7 @@ document.addEventListener("DOMContentLoaded", function() { }); } - if (pasteExpirySelect) { - pasteExpirySelect.addEventListener('change', (e) => { - if (e.target.value === 'custom') { - customPasteExpiry.style.display = 'block'; - customPasteExpiry.focus(); - } else { - customPasteExpiry.style.display = 'none'; - } - }); - } + // Helper function to calculate expiry datetime function calculateExpiryTime(expiryValue, customValue = null) { @@ -702,9 +667,9 @@ document.addEventListener("DOMContentLoaded", function() { try { pasteResult.innerHTML = '

📝 Uploading paste...

'; - // Calculate paste expiry - const expiryValue = pasteExpirySelect?.value || 'never'; - const customExpiryValue = customPasteExpiry?.value; + // Calculate paste expiry (using shared expiry selector) + const expiryValue = expirySelect?.value || 'never'; + const customExpiryValue = customExpiry?.value; const expiryTime = calculateExpiryTime(expiryValue, customExpiryValue); // Prepare request body @@ -769,9 +734,27 @@ document.addEventListener("DOMContentLoaded", function() { console.log('🔍 Theme toggle button found:', themeToggle ? 'YES' : 'NO'); if (themeToggle) { console.log('🎯 Adding click listener to theme button'); + + // Apply saved position immediately if available + if (window.shareyThemeButtonPosition) { + const pos = window.shareyThemeButtonPosition; + const maxX = window.innerWidth - themeToggle.offsetWidth; + const maxY = window.innerHeight - themeToggle.offsetHeight; + + const x = Math.max(0, Math.min(pos.x, maxX)); + const y = Math.max(0, Math.min(pos.y, maxY)); + + themeToggle.style.left = x + 'px'; + themeToggle.style.top = y + 'px'; + themeToggle.style.right = 'auto'; + themeToggle.style.bottom = 'auto'; + } + themeToggle.addEventListener('click', toggleTheme); // Show the current theme setting on button updateThemeButton(savedTheme); + // Make theme button draggable + makeDraggable(themeToggle); } else { console.log('⚠️ Theme toggle button not found on this page'); } @@ -791,6 +774,18 @@ document.addEventListener("DOMContentLoaded", function() { } console.log('🎨 Switching to theme:', newTheme); + + // Add animation to theme button + const themeToggle = document.getElementById('themeToggle'); + if (themeToggle) { + themeToggle.classList.add('switching'); + + // Remove animation class after animation completes + setTimeout(() => { + themeToggle.classList.remove('switching'); + }, 600); // Match animation duration + } + applyTheme(newTheme); setCookie('sharey-theme', newTheme, 365); // Save for 1 year updateThemeButton(newTheme); @@ -823,6 +818,138 @@ document.addEventListener("DOMContentLoaded", function() { } } + // Draggable functionality for theme button + function makeDraggable(element) { + let isDragging = false; + let hasMoved = false; + let dragOffset = { x: 0, y: 0 }; + let startPos = { x: 0, y: 0 }; + const moveThreshold = 5; // pixels - minimum movement to be considered a drag + + // Mouse events + element.addEventListener('mousedown', startDrag); + document.addEventListener('mousemove', drag); + document.addEventListener('mouseup', stopDrag); + + // Touch events for mobile + element.addEventListener('touchstart', startDrag, { passive: false }); + document.addEventListener('touchmove', drag, { passive: false }); + document.addEventListener('touchend', stopDrag); + + function startDrag(e) { + isDragging = true; + hasMoved = false; + + const clientX = e.clientX || (e.touches && e.touches[0].clientX); + const clientY = e.clientY || (e.touches && e.touches[0].clientY); + const rect = element.getBoundingClientRect(); + + startPos.x = clientX; + startPos.y = clientY; + dragOffset.x = clientX - rect.left; + dragOffset.y = clientY - rect.top; + + // Only prevent default for touch events to avoid issues + if (e.type.startsWith('touch')) { + e.preventDefault(); + } + } + + function drag(e) { + if (!isDragging) return; + + const clientX = e.clientX || (e.touches && e.touches[0].clientX); + const clientY = e.clientY || (e.touches && e.touches[0].clientY); + + // Check if we've moved enough to be considered a drag + const deltaX = Math.abs(clientX - startPos.x); + const deltaY = Math.abs(clientY - startPos.y); + + if (!hasMoved && (deltaX > moveThreshold || deltaY > moveThreshold)) { + hasMoved = true; + element.classList.add('dragging'); + } + + // Only move the element if we're actually dragging + if (hasMoved) { + let newX = clientX - dragOffset.x; + let newY = clientY - dragOffset.y; + + // Keep button within viewport bounds + const maxX = window.innerWidth - element.offsetWidth; + const maxY = window.innerHeight - element.offsetHeight; + + newX = Math.max(0, Math.min(newX, maxX)); + newY = Math.max(0, Math.min(newY, maxY)); + + element.style.left = newX + 'px'; + element.style.top = newY + 'px'; + element.style.right = 'auto'; + element.style.bottom = 'auto'; + + e.preventDefault(); + } + } + + function stopDrag(e) { + if (!isDragging) return; + + isDragging = false; + element.classList.remove('dragging'); + + if (hasMoved) { + // Save position to localStorage only if we actually moved + const rect = element.getBoundingClientRect(); + saveThemeButtonPosition(rect.left, rect.top); + + // Prevent the click event since this was a drag + e.preventDefault(); + e.stopPropagation(); + } + // If we didn't move, let the click event fire normally + } + + // Only prevent click if we actually dragged + element.addEventListener('click', function(e) { + if (hasMoved) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + // If we didn't drag, allow the click to proceed normally + }, true); + } + + // Save theme button position to localStorage + function saveThemeButtonPosition(x, y) { + const position = { x: x, y: y }; + localStorage.setItem('sharey-theme-button-position', JSON.stringify(position)); + } + + // Restore theme button position from localStorage + function restoreThemeButtonPosition(element) { + try { + const saved = localStorage.getItem('sharey-theme-button-position'); + if (saved) { + const position = JSON.parse(saved); + + // Validate position is within current viewport + const maxX = window.innerWidth - element.offsetWidth; + const maxY = window.innerHeight - element.offsetHeight; + + const x = Math.max(0, Math.min(position.x, maxX)); + const y = Math.max(0, Math.min(position.y, maxY)); + + element.style.left = x + 'px'; + element.style.top = y + 'px'; + element.style.right = 'auto'; + element.style.bottom = 'auto'; + } + } catch (error) { + console.log('Could not restore theme button position:', error); + } + } + // Cookie management functions function setCookie(name, value, days) { const expires = new Date(); @@ -1088,218 +1215,4 @@ function showToast(message, type = "success", duration = 3000) { }, duration); } -// URL Shortener initialization function -function initializeUrlShortener() { - console.log('🔗 Initializing URL shortener...'); - - // Re-query elements now that section is visible - const urlInput = document.getElementById('urlInput'); - const customCode = document.getElementById('customCode'); - const urlExpiry = document.getElementById('urlExpiry'); - const shortenUrlButton = document.getElementById('shortenUrl'); - const urlResult = document.getElementById('urlResult'); - const urlPreviewContainer = document.getElementById('urlPreviewContainer'); - - console.log('URL shortener elements:', { - urlInput: !!urlInput, - customCode: !!customCode, - urlExpiry: !!urlExpiry, - shortenUrlButton: !!shortenUrlButton, - urlResult: !!urlResult, - urlPreviewContainer: !!urlPreviewContainer - }); - - if (shortenUrlButton && urlInput && urlResult) { - console.log('✅ URL shortener elements found, adding event listeners'); - - // Remove any existing listeners to avoid duplicates - shortenUrlButton.replaceWith(shortenUrlButton.cloneNode(true)); - const newShortenButton = document.getElementById('shortenUrl'); - - newShortenButton.addEventListener('click', () => shortenUrl(urlInput, customCode, urlExpiry, urlResult, urlPreviewContainer, newShortenButton)); - - // Enter key support for URL input - urlInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - shortenUrl(urlInput, customCode, urlExpiry, urlResult, urlPreviewContainer, newShortenButton); - } - }); - - // Copy button functionality - document.addEventListener('click', (e) => { - if (e.target && e.target.id === 'copyUrlButton') { - const shortUrlInput = document.getElementById('shortUrlInput'); - if (shortUrlInput) { - shortUrlInput.select(); - shortUrlInput.setSelectionRange(0, 99999); // For mobile - - navigator.clipboard.writeText(shortUrlInput.value).then(() => { - showToast('Short URL copied to clipboard! 📋', 'success'); - }).catch(() => { - // Fallback for older browsers - document.execCommand('copy'); - showToast('Short URL copied to clipboard! �', 'success'); - }); - } - } - }); - } else { - console.log('❌ URL shortener elements not found'); - } -} -// URL Shortener functionality -console.log('�🔗 Setting up URL shortener...'); -console.log('shortenUrlButton:', shortenUrlButton); -console.log('urlInput:', urlInput); -console.log('urlResult:', urlResult); - -if (shortenUrlButton && urlInput && urlResult) { - console.log('✅ URL shortener elements found, adding event listeners'); - shortenUrlButton.addEventListener('click', shortenUrl); - - // Enter key support for URL input - urlInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - shortenUrl(); - } - }); - - // Copy button functionality - document.addEventListener('click', (e) => { - if (e.target && e.target.id === 'copyUrlButton') { - const shortUrlInput = document.getElementById('shortUrlInput'); - if (shortUrlInput) { - shortUrlInput.select(); - shortUrlInput.setSelectionRange(0, 99999); // For mobile - - navigator.clipboard.writeText(shortUrlInput.value).then(() => { - showToast('Short URL copied to clipboard! 📋', 'success'); - }).catch(() => { - // Fallback for older browsers - document.execCommand('copy'); - showToast('Short URL copied to clipboard! 📋', 'success'); - }); - } - } - }); -} else { - console.log('❌ URL shortener elements not found'); - console.log('Missing elements:', { - shortenUrlButton: !shortenUrlButton, - urlInput: !urlInput, - urlResult: !urlResult - }); -} - -async function shortenUrl(urlInputEl, customCodeEl, urlExpiryEl, urlResultEl, urlPreviewContainerEl, shortenButtonEl) { - // Use passed elements or fallback to global ones - const urlInput = urlInputEl || document.getElementById('urlInput'); - const customCode = customCodeEl || document.getElementById('customCode'); - const urlExpiry = urlExpiryEl || document.getElementById('urlExpiry'); - const urlResult = urlResultEl || document.getElementById('urlResult'); - const urlPreviewContainer = urlPreviewContainerEl || document.getElementById('urlPreviewContainer'); - const shortenUrlButton = shortenButtonEl || document.getElementById('shortenUrl'); - - console.log('🔗 shortenUrl function called'); - - const url = urlInput.value.trim(); - const code = customCode.value.trim(); - const expiryHours = urlExpiry.value === 'never' ? null : parseInt(urlExpiry.value); - - console.log('URL:', url, 'Code:', code, 'Expiry:', expiryHours); - - if (!url) { - showError('Please enter a URL to shorten', urlResult); - return; - } - - // Basic URL validation - try { - new URL(url); - } catch (e) { - showError('Please enter a valid URL (include http:// or https://)', urlResult); - return; - } - - // Show loading state - shortenUrlButton.disabled = true; - shortenUrlButton.textContent = '⏳ Shortening...'; - showInfo('Creating short URL...', urlResult); - - try { - const payload = { - url: url - }; - - if (code) { - payload.code = code; - } - - if (expiryHours) { - payload.expires_in_hours = expiryHours; - } - - const response = await fetch('/api/shorten', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload) - }); - - const data = await response.json(); - - if (response.ok) { - showUrlSuccess(data, urlResult, urlPreviewContainer, urlInput, customCode, urlExpiry); - } else { - showError(data.error || 'Failed to create short URL', urlResult); - } - } catch (error) { - console.error('URL shortening error:', error); - showError('Failed to create short URL. Please try again.', urlResult); - } finally { - // Reset button state - shortenUrlButton.disabled = false; - shortenUrlButton.textContent = '✨ Shorten URL'; - } -} - -function showUrlSuccess(data, urlResult, urlPreviewContainer, urlInput, customCode, urlExpiry) { - urlResult.className = 'result-message success'; - urlResult.textContent = '✅ Short URL created successfully!'; - - // Show preview container - urlPreviewContainer.style.display = 'block'; - - // Update preview elements - const shortUrlInput = document.getElementById('shortUrlInput'); - const targetUrl = document.getElementById('targetUrl'); - const clickCount = document.getElementById('clickCount'); - const expiryInfo = document.getElementById('expiryInfo'); - const expiryDate = document.getElementById('expiryDate'); - - if (shortUrlInput) { - shortUrlInput.value = data.short_url; - } - - if (targetUrl) { - targetUrl.textContent = data.target_url; - } - - if (clickCount) { - clickCount.textContent = '0'; - } - - if (data.expires_at && expiryInfo && expiryDate) { - expiryDate.textContent = new Date(data.expires_at).toLocaleString(); - expiryInfo.style.display = 'block'; - } else if (expiryInfo) { - expiryInfo.style.display = 'none'; - } - - // Clear form - urlInput.value = ''; - customCode.value = ''; - urlExpiry.value = '24'; -} diff --git a/src/static/style.css b/src/static/style.css index c02ad63..48106df 100644 --- a/src/static/style.css +++ b/src/static/style.css @@ -88,16 +88,16 @@ h1 { /* Theme toggle button */ .theme-toggle { - position: absolute; + position: fixed; top: clamp(15px, 3vw, 20px); right: clamp(15px, 3vw, 20px); background: var(--bg-secondary); border: 2px solid var(--border-color); border-radius: 50px; padding: clamp(10px, 2vw, 16px); - cursor: pointer; + cursor: grab; font-size: clamp(16px, 3vw, 18px); - transition: all 0.3s ease; + transition: box-shadow 0.3s ease, transform 0.3s ease; color: var(--text-primary); box-shadow: 0 2px 6px var(--shadow); min-width: 44px; /* Minimum touch target */ @@ -105,6 +105,61 @@ h1 { display: flex; align-items: center; justify-content: center; + z-index: 9999; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + touch-action: none; +} + +.theme-toggle:active { + cursor: grabbing; + transform: scale(1.05); +} + +.theme-toggle.dragging { + transition: none; + cursor: grabbing; + transform: scale(1.05); + box-shadow: 0 8px 25px var(--shadow); +} + +.theme-toggle.switching { + animation: themeSwitch 0.6s ease-in-out; +} + +@keyframes themeSwitch { + 0% { + transform: rotate(0deg) scale(1); + } + 25% { + transform: rotate(90deg) scale(0.8); + } + 50% { + transform: rotate(180deg) scale(0.6); + opacity: 0.7; + } + 75% { + transform: rotate(270deg) scale(0.8); + } + 100% { + transform: rotate(360deg) scale(1); + } +} + +/* Alternative spinning animation */ +@keyframes themeSpin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.theme-toggle.spinning { + animation: themeSpin 0.5s ease-in-out; } .theme-toggle:hover { @@ -1389,191 +1444,4 @@ pre { } } -/* URL Shortener Styles */ -.url-input-container { - display: flex; - flex-direction: column; - gap: 20px; - margin-bottom: 25px; -} -.url-label { - font-size: 18px; - font-weight: bold; - color: var(--text-primary); - margin-bottom: 8px; -} - -.url-input { - width: 100%; - padding: 15px; - font-size: 16px; - border: 2px solid var(--border-color); - border-radius: 8px; - background-color: var(--bg-secondary); - color: var(--text-primary); - transition: all 0.3s ease; - box-sizing: border-box; -} - -.url-input:focus { - outline: none; - border-color: #388e3c; - box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.2); -} - -.url-options { - display: flex; - flex-wrap: wrap; - gap: 20px; - margin-bottom: 20px; -} - -.option-group { - flex: 1; - min-width: 200px; -} - -.option-label { - display: block; - font-weight: bold; - color: var(--text-primary); - margin-bottom: 5px; - font-size: 14px; -} - -.custom-code-input { - width: 100%; - padding: 10px; - font-size: 14px; - border: 1px solid #ccc; - border-radius: 6px; - background-color: var(--bg-secondary); - color: var(--text-primary); - transition: all 0.3s ease; - box-sizing: border-box; -} - -.custom-code-input:focus { - outline: none; - border-color: var(--border-color); - box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2); -} - -.url-expiry-select { - width: 100%; - padding: 10px; - font-size: 14px; - border: 1px solid #ccc; - border-radius: 6px; - background-color: var(--bg-secondary); - color: var(--text-primary); - cursor: pointer; -} - -.option-hint { - font-size: 12px; - color: #666; - margin-top: 3px; - font-style: italic; -} - -/* URL Preview Container */ -.url-preview-container { - background-color: var(--bg-accent); - border: 2px solid var(--border-color); - border-radius: 12px; - padding: 20px; - margin-top: 20px; - animation: slideDown 0.3s ease-out; -} - -.url-preview h3 { - color: var(--text-primary); - margin-bottom: 15px; - font-size: 18px; -} - -.short-url-display { - display: flex; - gap: 10px; - margin-bottom: 15px; - align-items: center; -} - -.short-url-input { - flex: 1; - padding: 12px; - font-size: 16px; - border: 1px solid #ccc; - border-radius: 6px; - background-color: var(--bg-secondary); - color: var(--text-primary); - font-family: 'Courier New', monospace; - font-weight: bold; -} - -.copy-button { - padding: 12px 16px; - font-size: 16px; - background-color: var(--border-color); - color: white; - border: none; - border-radius: 6px; - cursor: pointer; - transition: all 0.3s ease; - white-space: nowrap; -} - -.copy-button:hover { - background-color: #388e3c; - transform: translateY(-2px); -} - -.url-details { - font-size: 14px; - color: var(--text-primary); - line-height: 1.6; -} - -.url-details p { - margin: 8px 0; -} - -.url-details strong { - color: var(--text-secondary); -} - -/* Responsive adjustments for URL shortener */ -@media only screen and (max-width: 768px) { - .url-options { - flex-direction: column; - gap: 15px; - } - - .option-group { - min-width: unset; - } - - .short-url-display { - flex-direction: column; - gap: 10px; - } - - .copy-button { - align-self: stretch; - text-align: center; - } -} - -/* Animation for URL preview */ -@keyframes slideDown { - from { - opacity: 0; - transform: translateY(-20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} diff --git a/src/templates/index.html b/src/templates/index.html index dafb867..7af321f 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -11,9 +11,9 @@ - + @@ -45,7 +61,6 @@
- @@ -103,71 +118,11 @@ - - + -
-

🔗 How do I shorten a URL?

-

Use the URL Shortener tab to create short links:

-
    -
  • Basic shortening: Enter any URL and get a short link like sharey.org/abc123
  • -
  • Custom codes: Create memorable links like sharey.org/my-project
  • -
  • Expiring links: Set links to expire automatically for security
  • -
  • Click tracking: See how many times your link has been clicked
  • -
-

API Usage:

-
curl -X POST https://sharey.org/api/shorten \
--H "Content-Type: application/json" \
--d '{"url": "https://example.com", "code": "my-link", "expires_in_hours": 24}'
-

Perfect for sharing long URLs, tracking clicks, or creating temporary access links!

-
+
diff --git a/src/templates/index_backup.html b/src/templates/index_backup.html deleted file mode 100644 index 09866fc..0000000 --- a/src/templates/index_backup.html +++ /dev/null @@ -1,102 +0,0 @@ - - -< - ad> - - - Sharey - - - - - - - - - - - -
-

Sharey~

- - -
- - - -
- - -
-
-

Drag & Drop Files Here or Click to Select

- -
- - -

0%

- -
- - - -
- - - - - - -
- - - - - -