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:
@@ -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):
|
||||
|
||||
179
src/app.py
179
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/<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
|
||||
@app.route('/admin')
|
||||
|
||||
@@ -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 = '<p>📝 Uploading paste...</p>';
|
||||
|
||||
// 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! <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';
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
<!-- 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 -->
|
||||
<!-- Immediate theme and position application to prevent flash -->
|
||||
<script>
|
||||
// Apply theme immediately before page renders
|
||||
// Apply theme and button position immediately before page renders
|
||||
(function() {
|
||||
function getCookie(name) {
|
||||
const nameEQ = name + "=";
|
||||
@@ -31,6 +31,22 @@
|
||||
if (savedTheme === '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>
|
||||
</head>
|
||||
@@ -45,7 +61,6 @@
|
||||
<div class="toggle-container">
|
||||
<button id="fileMode" class="toggle-button active">File Sharing</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>
|
||||
|
||||
<!-- File Expiry Selection (inline) -->
|
||||
@@ -103,71 +118,11 @@
|
||||
<div id="pastebinSection" class="section" style="display: none;">
|
||||
<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>
|
||||
<div id="pasteResult" class="result-message"></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) -->
|
||||
<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>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user