Major UI improvements and cleanup

 Features:
- Add movable theme button with drag functionality
- Theme button rotation animation on toggle
- Position persistence across page reloads
- Remove flash positioning on page load

🗑️ Cleanup:
- Remove URL shortener functionality completely
- Unify expiry dropdown (remove duplicate from pastebin)
- Fix import issues in cleanup scripts and app.py
- Remove unused CSS and JavaScript code

🔧 Technical:
- Improve drag vs click detection
- Add viewport boundary constraints for theme button
- Clean up JavaScript event handlers
- Optimize CSS animations and transitions
This commit is contained in:
2025-09-27 18:13:16 +01:00
parent b73af5bf11
commit 9ebd1331fb
6 changed files with 260 additions and 801 deletions

View File

@@ -9,12 +9,12 @@ import json
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
# Add src directory to Python path # Add the project root to Python path
src_path = Path(__file__).parent / "src" project_root = Path(__file__).parent
sys.path.insert(0, str(src_path)) sys.path.append(str(project_root))
from config import config from src.config import config
from storage import StorageManager from src.storage import StorageManager
def cleanup_local_storage(storage_manager): def cleanup_local_storage(storage_manager):

View File

@@ -7,12 +7,17 @@ import hashlib
import re import re
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import wraps 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 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 config import config
from storage import StorageManager from storage import StorageManager
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from expiry_db import ExpiryDatabase from expiry_db import ExpiryDatabase
# Flask app configuration # Flask app configuration
@@ -110,52 +115,7 @@ except Exception as e:
def generate_short_id(length=6): def generate_short_id(length=6):
return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) 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 # Helper function to determine if an ID is a file or paste
def detect_content_type(content_id): def detect_content_type(content_id):
@@ -196,18 +156,11 @@ def index():
@check_maintenance @check_maintenance
def get_content(content_id): 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 Examples: sharey.org/ABC123, sharey.org/XYZ789.png
""" """
print(f"🔍 Processing request for ID: {content_id}") 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 # Skip certain paths that should not be treated as content IDs
excluded_paths = ['admin', 'health', 'api', 'static', 'files', 'pastes', 'favicon.ico'] excluded_paths = ['admin', 'health', 'api', 'static', 'files', 'pastes', 'favicon.ico']
if content_id in excluded_paths: if content_id in excluded_paths:
@@ -447,121 +400,7 @@ def view_paste_raw(paste_id):
except Exception as e: except Exception as e:
return jsonify({'error': 'Paste not found'}), 404 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/<short_code>/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 # Admin Panel Routes
@app.route('/admin') @app.route('/admin')

View File

@@ -13,26 +13,16 @@ document.addEventListener("DOMContentLoaded", function() {
const fileModeButton = document.getElementById('fileMode'); const fileModeButton = document.getElementById('fileMode');
const pasteModeButton = document.getElementById('pasteMode'); const pasteModeButton = document.getElementById('pasteMode');
const urlModeButton = document.getElementById('urlMode');
const faqButton = document.getElementById('faqButton'); const faqButton = document.getElementById('faqButton');
const fileSharingSection = document.getElementById('fileSharingSection'); const fileSharingSection = document.getElementById('fileSharingSection');
const pastebinSection = document.getElementById('pastebinSection'); const pastebinSection = document.getElementById('pastebinSection');
const urlShortenerSection = document.getElementById('urlShortenerSection');
const faqSection = document.getElementById('faqSection'); const faqSection = document.getElementById('faqSection');
const pasteContent = document.getElementById('pasteContent'); const pasteContent = document.getElementById('pasteContent');
const submitPasteButton = document.getElementById('submitPaste'); const submitPasteButton = document.getElementById('submitPaste');
const pasteResult = document.getElementById('pasteResult'); 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 // Mobile clipboard elements
const mobileClipboardSection = document.getElementById('mobileClipboardSection'); const mobileClipboardSection = document.getElementById('mobileClipboardSection');
const mobilePasteButton = document.getElementById('mobilePasteButton'); const mobilePasteButton = document.getElementById('mobilePasteButton');
@@ -40,11 +30,9 @@ document.addEventListener("DOMContentLoaded", function() {
let filesToUpload = []; let filesToUpload = [];
let currentMode = 'file'; // Track current mode: 'file', 'paste', or 'faq' 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 expirySelect = document.getElementById('expirySelect');
const customExpiry = document.getElementById('customExpiry'); const customExpiry = document.getElementById('customExpiry');
const pasteExpirySelect = document.getElementById('pasteExpirySelect');
const customPasteExpiry = document.getElementById('customPasteExpiry');
// Helper function to check if file is an image // Helper function to check if file is an image
function isImageFile(file) { function isImageFile(file) {
@@ -65,29 +53,15 @@ document.addEventListener("DOMContentLoaded", function() {
currentMode = 'paste'; currentMode = 'paste';
showSection(pastebinSection); showSection(pastebinSection);
hideSection(fileSharingSection); hideSection(fileSharingSection);
hideSection(urlShortenerSection);
hideSection(faqSection); hideSection(faqSection);
activateButton(pasteModeButton); 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', () => { faqButton.addEventListener('click', () => {
currentMode = 'faq'; currentMode = 'faq';
showSection(faqSection); showSection(faqSection);
hideSection(fileSharingSection); hideSection(fileSharingSection);
hideSection(pastebinSection); hideSection(pastebinSection);
hideSection(urlShortenerSection);
activateButton(faqButton); activateButton(faqButton);
}); });
@@ -103,7 +77,7 @@ document.addEventListener("DOMContentLoaded", function() {
// Helper function to activate a button // Helper function to activate a button
function activateButton(button) { function activateButton(button) {
const buttons = [fileModeButton, pasteModeButton, urlModeButton, faqButton]; const buttons = [fileModeButton, pasteModeButton, faqButton];
buttons.forEach(btn => btn.classList.remove('active')); buttons.forEach(btn => btn.classList.remove('active'));
button.classList.add('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 // Helper function to calculate expiry datetime
function calculateExpiryTime(expiryValue, customValue = null) { function calculateExpiryTime(expiryValue, customValue = null) {
@@ -702,9 +667,9 @@ document.addEventListener("DOMContentLoaded", function() {
try { try {
pasteResult.innerHTML = '<p>📝 Uploading paste...</p>'; pasteResult.innerHTML = '<p>📝 Uploading paste...</p>';
// Calculate paste expiry // Calculate paste expiry (using shared expiry selector)
const expiryValue = pasteExpirySelect?.value || 'never'; const expiryValue = expirySelect?.value || 'never';
const customExpiryValue = customPasteExpiry?.value; const customExpiryValue = customExpiry?.value;
const expiryTime = calculateExpiryTime(expiryValue, customExpiryValue); const expiryTime = calculateExpiryTime(expiryValue, customExpiryValue);
// Prepare request body // Prepare request body
@@ -769,9 +734,27 @@ document.addEventListener("DOMContentLoaded", function() {
console.log('🔍 Theme toggle button found:', themeToggle ? 'YES' : 'NO'); console.log('🔍 Theme toggle button found:', themeToggle ? 'YES' : 'NO');
if (themeToggle) { if (themeToggle) {
console.log('🎯 Adding click listener to theme button'); 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); themeToggle.addEventListener('click', toggleTheme);
// Show the current theme setting on button // Show the current theme setting on button
updateThemeButton(savedTheme); updateThemeButton(savedTheme);
// Make theme button draggable
makeDraggable(themeToggle);
} else { } else {
console.log('⚠️ Theme toggle button not found on this page'); console.log('⚠️ Theme toggle button not found on this page');
} }
@@ -791,6 +774,18 @@ document.addEventListener("DOMContentLoaded", function() {
} }
console.log('🎨 Switching to theme:', newTheme); 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); applyTheme(newTheme);
setCookie('sharey-theme', newTheme, 365); // Save for 1 year setCookie('sharey-theme', newTheme, 365); // Save for 1 year
updateThemeButton(newTheme); 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 // Cookie management functions
function setCookie(name, value, days) { function setCookie(name, value, days) {
const expires = new Date(); const expires = new Date();
@@ -1088,218 +1215,4 @@ function showToast(message, type = "success", duration = 3000) {
}, duration); }, 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! <20>', 'success');
});
}
}
});
} else {
console.log('❌ URL shortener elements not found');
}
}
// URL Shortener functionality
console.log('<27>🔗 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';
}

View File

@@ -88,16 +88,16 @@ h1 {
/* Theme toggle button */ /* Theme toggle button */
.theme-toggle { .theme-toggle {
position: absolute; position: fixed;
top: clamp(15px, 3vw, 20px); top: clamp(15px, 3vw, 20px);
right: clamp(15px, 3vw, 20px); right: clamp(15px, 3vw, 20px);
background: var(--bg-secondary); background: var(--bg-secondary);
border: 2px solid var(--border-color); border: 2px solid var(--border-color);
border-radius: 50px; border-radius: 50px;
padding: clamp(10px, 2vw, 16px); padding: clamp(10px, 2vw, 16px);
cursor: pointer; cursor: grab;
font-size: clamp(16px, 3vw, 18px); 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); color: var(--text-primary);
box-shadow: 0 2px 6px var(--shadow); box-shadow: 0 2px 6px var(--shadow);
min-width: 44px; /* Minimum touch target */ min-width: 44px; /* Minimum touch target */
@@ -105,6 +105,61 @@ h1 {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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 { .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);
}
}

View File

@@ -11,9 +11,9 @@
<!-- QR Code generation library --> <!-- QR Code generation library -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<!-- Immediate theme application to prevent flash --> <!-- Immediate theme and position application to prevent flash -->
<script> <script>
// Apply theme immediately before page renders // Apply theme and button position immediately before page renders
(function() { (function() {
function getCookie(name) { function getCookie(name) {
const nameEQ = name + "="; const nameEQ = name + "=";
@@ -31,6 +31,22 @@
if (savedTheme === 'dark') { if (savedTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark'); document.documentElement.setAttribute('data-theme', 'dark');
} }
// Restore theme button position immediately
try {
const saved = localStorage.getItem('sharey-theme-button-position');
if (saved) {
const position = JSON.parse(saved);
// Store position data to be applied when DOM loads
window.shareyThemeButtonPosition = {
x: position.x,
y: position.y
};
}
} catch (error) {
// Silently fail if there's an issue with saved position
}
})(); })();
</script> </script>
</head> </head>
@@ -45,7 +61,6 @@
<div class="toggle-container"> <div class="toggle-container">
<button id="fileMode" class="toggle-button active">File Sharing</button> <button id="fileMode" class="toggle-button active">File Sharing</button>
<button id="pasteMode" class="toggle-button">Pastebin</button> <button id="pasteMode" class="toggle-button">Pastebin</button>
<button id="urlMode" class="toggle-button">URL Shortener</button>
<button id="faqButton" class="toggle-button">FAQ</button> <button id="faqButton" class="toggle-button">FAQ</button>
<!-- File Expiry Selection (inline) --> <!-- File Expiry Selection (inline) -->
@@ -103,71 +118,11 @@
<div id="pastebinSection" class="section" style="display: none;"> <div id="pastebinSection" class="section" style="display: none;">
<textarea id="pasteContent" placeholder="Enter your text here..." rows="10" cols="50"></textarea> <textarea id="pasteContent" placeholder="Enter your text here..." rows="10" cols="50"></textarea>
<!-- Paste Expiry Selection -->
<div class="expiry-section">
<label for="pasteExpirySelect" class="expiry-label">⏰ Paste Expiry:</label>
<select id="pasteExpirySelect" class="expiry-select">
<option value="never">Never expires</option>
<option value="1h">1 Hour</option>
<option value="24h" selected>24 Hours</option>
<option value="7d">7 Days</option>
<option value="30d">30 Days</option>
<option value="custom">Custom...</option>
</select>
<input type="datetime-local" id="customPasteExpiry" class="custom-expiry" style="display: none;">
<p class="expiry-hint">Pastes will be automatically deleted after the expiry time</p>
</div>
<button id="submitPaste" class="submit-button">Submit Paste</button> <button id="submitPaste" class="submit-button">Submit Paste</button>
<div id="pasteResult" class="result-message"></div> <div id="pasteResult" class="result-message"></div>
</div> </div>
<!-- URL Shortener Area (hidden by default) -->
<div id="urlShortenerSection" class="section" style="display: none;">
<div class="url-input-container">
<label for="urlInput" class="url-label">🔗 Enter URL to shorten:</label>
<input type="url" id="urlInput" class="url-input" placeholder="https://example.com/very-long-url" required>
<div class="url-options">
<div class="option-group">
<label for="customCode" class="option-label">Custom code (optional):</label>
<input type="text" id="customCode" class="custom-code-input" placeholder="my-link" maxlength="20">
<small class="option-hint">3-20 characters, letters, numbers, hyphens, underscores only</small>
</div>
<div class="option-group">
<label for="urlExpiry" class="option-label">⏰ Expires in:</label>
<select id="urlExpiry" class="url-expiry-select">
<option value="never">Never</option>
<option value="1">1 Hour</option>
<option value="24" selected>24 Hours</option>
<option value="168">7 Days</option>
<option value="720">30 Days</option>
</select>
</div>
</div>
<button id="shortenUrl" class="submit-button">✨ Shorten URL</button>
</div>
<div id="urlResult" class="result-message"></div>
<!-- URL Preview Container -->
<div id="urlPreviewContainer" class="url-preview-container" style="display: none;">
<h3>🔗 Short URL Created:</h3>
<div id="urlPreview" class="url-preview">
<div class="short-url-display">
<input type="text" id="shortUrlInput" class="short-url-input" readonly>
<button id="copyUrlButton" class="copy-button" title="Copy to clipboard">📋</button>
</div>
<div class="url-details">
<p><strong>Target:</strong> <span id="targetUrl"></span></p>
<p><strong>Clicks:</strong> <span id="clickCount">0</span></p>
<p id="expiryInfo" style="display: none;"><strong>Expires:</strong> <span id="expiryDate"></span></p>
</div>
</div>
</div>
</div>
<!-- FAQ Section (hidden by default) --> <!-- FAQ Section (hidden by default) -->
<div id="faqSection" class="section" style="display: none;"> <div id="faqSection" class="section" style="display: none;">
@@ -263,21 +218,7 @@
<p><em>Perfect for temporary shares or sensitive content that shouldn't persist!</em></p> <p><em>Perfect for temporary shares or sensitive content that shouldn't persist!</em></p>
</div> </div>
<div class="faq-section">
<h3>🔗 How do I shorten a URL?</h3>
<p>Use the URL Shortener tab to create short links:</p>
<ul>
<li><strong>Basic shortening:</strong> Enter any URL and get a short link like <code>sharey.org/abc123</code></li>
<li><strong>Custom codes:</strong> Create memorable links like <code>sharey.org/my-project</code></li>
<li><strong>Expiring links:</strong> Set links to expire automatically for security</li>
<li><strong>Click tracking:</strong> See how many times your link has been clicked</li>
</ul>
<p><strong>API Usage:</strong></p>
<pre><code>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}'</code></pre>
<p><em>Perfect for sharing long URLs, tracking clicks, or creating temporary access links!</em></p>
</div>
</div> </div>
</div> </div>

View File

@@ -1,102 +0,0 @@
<!DOCTYPE html>
<html lang="en">
< <!-- Theme toggle button -->
<button id="themeToggle" class="theme-toggle" title="Switch to dark mode">🌙</button>ad>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sharey</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<!-- QR Code generation library -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<!-- Immediate theme application to prevent flash -->
<script>
// Apply theme immediately before page renders
(function() {
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
const savedTheme = getCookie('sharey-theme') || 'light';
if (savedTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
</head>
<body>
<!-- Theme toggle button -->
<button id="themeToggle" class="theme-toggle" title="Switch to dark mode"><EFBFBD></button>
<div class="container">
<h1>Sharey~</h1>
<!-- Toggle button to switch between modes -->
<div class="toggle-container">
<button id="fileMode" class="toggle-button active">File Sharing</button>
<button id="pasteMode" class="toggle-button">Pastebin</button>
<button id="faqButton" class="toggle-button">FAQ</button> <!-- Updated FAQ button -->
</div>
<!-- File Sharing Area -->
<div id="fileSharingSection" class="section">
<div id="drop-area" class="drop-area">
<p>Drag & Drop Files Here or Click to Select</p>
<input type="file" id="fileInput" multiple style="display: none;">
</div>
<div id="progressContainer" class="progress-bar" style="display: none;">
<div id="progressBar" class="progress"></div>
</div>
<p id="progressText">0%</p>
<div id="result" class="result-message"></div>
<!-- File preview container -->
<div id="filePreviewContainer" class="file-preview-container" style="display: none;">
<h3>File Previews:</h3>
<div id="filePreviews" class="file-previews"></div>
</div>
</div>
<!-- Pastebin Area (hidden by default) -->
<div id="pastebinSection" class="section" style="display: none;">
<textarea id="pasteContent" placeholder="Enter your text here..." rows="10" cols="50"></textarea>
<button id="submitPaste" class="submit-button">Submit Paste</button>
<div id="pasteResult" class="result-message"></div>
</div>
<!-- FAQ Section (hidden by default) -->
<div id="faqSection" class="section" style="display: none;">
<h2>Frequently Asked Questions</h2>
<div class="faq-section">
<h3>How do I upload a file?</h3>
<p>To upload a file, send a POST request to the /api/upload endpoint. Example:</p>
<pre><code>curl -X POST https://sharey.org/api/upload \
-F "files[]=@/path/to/your/file.txt"</code></pre>
</div>
<div class="faq-section">
<h3>How do I create a paste?</h3>
<p>To create a paste, send a POST request to the /api/paste endpoint. Example:</p>
<pre><code>curl -X POST https://sharey.org/api/paste \
-H "Content-Type: application/json" \
-d '{"content": "This is the content of my paste."}'</code></pre>
</div>
<!-- Add more FAQ items as needed -->
</div>
</div>
<!-- QR Code library for sharing -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>