diff --git a/css/components/url-shortener.css b/css/components/url-shortener.css new file mode 100644 index 0000000..0ef5907 --- /dev/null +++ b/css/components/url-shortener.css @@ -0,0 +1,309 @@ +.url-shortener { + max-width: 800px; + margin: 0 auto; + padding: 2rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.input-section { + margin-bottom: 2rem; +} + +.url-input-container { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.url-input-container input { + flex: 1; + padding: 1rem; + border: 2px solid var(--border-color); + border-radius: 5px; + background: var(--background-light); + color: var(--text-light); + font-size: 1rem; + transition: var(--transition); +} + +.url-input-container input:focus { + border-color: var(--primary-color); + outline: none; + box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.2); +} + +.options-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; +} + +.custom-alias { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.custom-alias input { + padding: 0.75rem; + border: 2px solid var(--border-color); + border-radius: 5px; + background: var(--background-light); + color: var(--text-light); + font-size: 0.9rem; +} + +.hint { + font-size: 0.8rem; + color: var(--text-light); + opacity: 0.7; +} + +.expiry-options { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.expiry-options select, +.expiry-options input[type="date"] { + padding: 0.75rem; + border: 2px solid var(--border-color); + border-radius: 5px; + background: var(--background-light); + color: var(--text-light); + font-size: 0.9rem; +} + +.result-section { + padding: 2rem; + background: rgba(var(--primary-color-rgb), 0.1); + border-radius: 10px; + margin-top: 2rem; +} + +.shortened-url-container { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.shortened-url-container input { + flex: 1; + padding: 1rem; + border: 2px solid var(--border-color); + border-radius: 5px; + background: var(--background-light); + color: var(--text-light); + font-size: 1rem; + cursor: text; +} + +.stats-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + margin-top: 2rem; + padding-top: 2rem; + border-top: 1px solid var(--border-color); +} + +.stat { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.stat-label { + font-size: 0.9rem; + color: var(--text-light); + opacity: 0.7; + margin-bottom: 0.5rem; +} + +.stat-value { + font-size: 1.5rem; + font-weight: 700; + color: var(--primary-color); +} + +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background: var(--background-light); + padding: 2rem; + border-radius: 10px; + max-width: 400px; + width: 90%; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.close-button { + background: none; + border: none; + font-size: 1.5rem; + color: var(--text-light); + cursor: pointer; + padding: 0.5rem; +} + +#qr-code { + display: flex; + justify-content: center; + margin: 2rem 0; +} + +.features-section { + margin-top: 4rem; +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; + margin-top: 2rem; +} + +.feature-card { + background: rgba(255, 255, 255, 0.05); + padding: 2rem; + border-radius: 10px; + text-align: center; + transition: var(--transition); +} + +.feature-card:hover { + transform: translateY(-5px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); +} + +.feature-card i { + font-size: 2rem; + color: var(--primary-color); + margin-bottom: 1rem; +} + +.feature-card h3 { + margin-bottom: 1rem; + color: var(--text-light); +} + +.feature-card p { + color: var(--text-light); + opacity: 0.8; + font-size: 0.9rem; +} + +.hidden { + display: none; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .url-input-container { + flex-direction: column; + } + + .options-container { + grid-template-columns: 1fr; + } + + .stats-container { + grid-template-columns: 1fr; + gap: 1rem; + } + + .shortened-url-container { + flex-direction: column; + } + + .shortened-url-container button { + width: 100%; + } +} + +.history-section { + max-width: 800px; + margin: 2rem auto; + padding: 2rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.history-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.history-item { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 1rem; + padding: 1rem; + background: rgba(255, 255, 255, 0.03); + border-radius: 5px; + align-items: center; +} + +.history-item .url-info { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.history-item .short-url { + font-weight: 700; + color: var(--primary-color); +} + +.history-item .long-url { + font-size: 0.9rem; + color: var(--text-light); + opacity: 0.8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.history-item .meta-info { + font-size: 0.8rem; + color: var(--text-light); + opacity: 0.7; +} + +.history-item .action-buttons { + display: flex; + gap: 0.5rem; +} + +@media (max-width: 768px) { + .history-item { + grid-template-columns: 1fr; + } + + .history-item .action-buttons { + justify-content: flex-end; + } +} \ No newline at end of file diff --git a/index.html b/index.html index 661ec39..2ac729c 100644 --- a/index.html +++ b/index.html @@ -100,6 +100,18 @@

Password Generator

Strength meter + +
+
+ +
+

URL Shortener

+

Create short, memorable links for easy sharing and tracking.

+
+ Click analytics + Custom aliases +
+
diff --git a/js/features/url-shortener.js b/js/features/url-shortener.js new file mode 100644 index 0000000..a0659ad --- /dev/null +++ b/js/features/url-shortener.js @@ -0,0 +1,266 @@ +import { BaseTool } from './base-tool.js'; +import { notifications } from '../utils/ui.js'; +import utils from '../utils/helpers.js'; + +class URLShortener extends BaseTool { + constructor() { + super(); + this.initializeElements(); + this.setupEventListeners(); + this.loadSavedUrls(); + this.loadQRCodeLibrary(); + } + + initializeElements() { + this.elements = { + longUrlInput: document.getElementById('long-url'), + shortenButton: document.getElementById('shorten-button'), + customAlias: document.getElementById('custom-alias'), + expiryTime: document.getElementById('expiry-time'), + customExpiry: document.getElementById('custom-expiry'), + resultSection: document.getElementById('result-section'), + shortenedUrl: document.getElementById('shortened-url'), + copyButton: document.getElementById('copy-button'), + qrButton: document.getElementById('qr-button'), + qrModal: document.getElementById('qr-modal'), + closeModal: document.querySelector('.close-button'), + downloadQr: document.getElementById('download-qr'), + qrCode: document.getElementById('qr-code'), + clickCount: document.getElementById('click-count'), + createdDate: document.getElementById('created-date'), + expiryDate: document.getElementById('expiry-date'), + historySection: document.getElementById('history-section'), + historyList: document.querySelector('.history-list') + }; + } + + setupEventListeners() { + this.elements.shortenButton.addEventListener('click', () => this.shortenUrl()); + this.elements.copyButton.addEventListener('click', () => this.copyToClipboard()); + this.elements.qrButton.addEventListener('click', () => this.showQRCode()); + this.elements.closeModal.addEventListener('click', () => this.hideQRCode()); + this.elements.downloadQr.addEventListener('click', () => this.downloadQRCode()); + this.elements.expiryTime.addEventListener('change', () => this.toggleCustomExpiry()); + + // Handle Enter key in URL input + this.elements.longUrlInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + this.shortenUrl(); + } + }); + } + + async shortenUrl() { + const longUrl = this.elements.longUrlInput.value.trim(); + const customAlias = this.elements.customAlias.value.trim(); + + if (!this.validateUrl(longUrl)) { + notifications.error('Please enter a valid URL'); + return; + } + + try { + // In a real implementation, this would call an API + const shortUrl = await this.generateShortUrl(longUrl, customAlias); + this.displayResult(shortUrl); + this.saveUrl(shortUrl, longUrl); + notifications.success('URL shortened successfully!'); + } catch (error) { + notifications.error('Failed to shorten URL. Please try again.'); + console.error('Error shortening URL:', error); + } + } + + validateUrl(url) { + try { + new URL(url); + return true; + } catch { + return false; + } + } + + async generateShortUrl(longUrl, customAlias) { + // In a real implementation, this would use a URL shortening service + // For demo purposes, we'll create a mock short URL + const baseUrl = 'https://short.dsh/'; + const alias = customAlias || this.generateRandomAlias(); + return baseUrl + alias; + } + + generateRandomAlias() { + return Math.random().toString(36).substring(2, 8); + } + + displayResult(shortUrl) { + this.elements.resultSection.classList.remove('hidden'); + this.elements.shortenedUrl.value = shortUrl; + + // Update stats + this.elements.clickCount.textContent = '0'; + this.elements.createdDate.textContent = new Date().toLocaleDateString(); + + const expiry = this.getExpiryDate(); + this.elements.expiryDate.textContent = expiry ? expiry.toLocaleDateString() : 'Never'; + } + + getExpiryDate() { + const expiryValue = this.elements.expiryTime.value; + if (expiryValue === 'never') return null; + if (expiryValue === 'custom') return new Date(this.elements.customExpiry.value); + + const now = new Date(); + switch (expiryValue) { + case '24h': return new Date(now.getTime() + 24 * 60 * 60 * 1000); + case '7d': return new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); + case '30d': return new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); + default: return null; + } + } + + toggleCustomExpiry() { + const isCustom = this.elements.expiryTime.value === 'custom'; + this.elements.customExpiry.classList.toggle('hidden', !isCustom); + } + + async copyToClipboard() { + try { + await navigator.clipboard.writeText(this.elements.shortenedUrl.value); + notifications.success('URL copied to clipboard!'); + } catch (error) { + notifications.error('Failed to copy URL'); + console.error('Error copying to clipboard:', error); + } + } + + showQRCode() { + const shortUrl = this.elements.shortenedUrl.value; + if (!shortUrl) return; + + if (window.QRCode) { + // Clear previous QR code + this.elements.qrCode.innerHTML = ''; + + // Generate new QR code + QRCode.toCanvas(this.elements.qrCode, shortUrl, { + width: 256, + margin: 2, + color: { + dark: getComputedStyle(document.documentElement) + .getPropertyValue('--primary-color') + .trim(), + light: '#ffffff' + } + }, (error) => { + if (error) { + notifications.error('Failed to generate QR code'); + console.error('Error generating QR code:', error); + } + }); + } + + this.elements.qrModal.classList.remove('hidden'); + } + + hideQRCode() { + this.elements.qrModal.classList.add('hidden'); + } + + downloadQRCode() { + const canvas = this.elements.qrCode.querySelector('canvas'); + if (!canvas) { + notifications.error('No QR code to download'); + return; + } + + try { + const link = document.createElement('a'); + link.download = 'qr-code.png'; + link.href = canvas.toDataURL('image/png'); + link.click(); + notifications.success('QR code downloaded successfully!'); + } catch (error) { + notifications.error('Failed to download QR code'); + console.error('Error downloading QR code:', error); + } + } + + saveUrl(shortUrl, longUrl) { + const savedUrls = this.getSavedUrls(); + savedUrls.unshift({ + shortUrl, + longUrl, + created: new Date().toISOString(), + clicks: 0 + }); + + // Keep only the last 10 URLs + if (savedUrls.length > 10) savedUrls.pop(); + + localStorage.setItem('shortened_urls', JSON.stringify(savedUrls)); + } + + getSavedUrls() { + try { + return JSON.parse(localStorage.getItem('shortened_urls')) || []; + } catch { + return []; + } + } + + loadSavedUrls() { + const savedUrls = this.getSavedUrls(); + if (savedUrls.length > 0) { + this.elements.historySection.classList.remove('hidden'); + this.displayUrlHistory(savedUrls); + } + } + + displayUrlHistory(urls) { + this.elements.historyList.innerHTML = urls.map(url => ` +
+
+
${utils.sanitizeHTML(url.shortUrl)}
+
${utils.sanitizeHTML(url.longUrl)}
+
+ Created: ${new Date(url.created).toLocaleDateString()} | + Clicks: ${url.clicks} +
+
+
+ + +
+
+ `).join(''); + } + + copyHistoryUrl(url) { + navigator.clipboard.writeText(url) + .then(() => notifications.success('URL copied to clipboard!')) + .catch(() => notifications.error('Failed to copy URL')); + } + + showHistoryQR(url) { + this.elements.shortenedUrl.value = url; + this.showQRCode(); + } + + loadQRCodeLibrary() { + // Load QRCode.js dynamically + const script = document.createElement('script'); + script.src = 'https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js'; + script.async = true; + document.head.appendChild(script); + } +} + +// Initialize the URL shortener +const urlShortener = new URLShortener(); \ No newline at end of file diff --git a/pages/about.html b/pages/about.html index 6700997..e79e755 100644 --- a/pages/about.html +++ b/pages/about.html @@ -38,7 +38,7 @@

Our Mission

Digital Services Hub is dedicated to providing free, accessible, and powerful web-based tools for everyday digital tasks. We believe that quality digital tools should be available to everyone, regardless of technical expertise or budget.

- 6+ + 7+ Tools
@@ -110,6 +110,15 @@

Password Generator

Create strong, secure passwords with advanced customization and strength indicators.

Try It
+ +
+
+ +
+

URL Shortener

+

Create short, memorable links for easy sharing with click analytics and custom aliases.

+ Try It +
diff --git a/pages/url-shortener.html b/pages/url-shortener.html new file mode 100644 index 0000000..7047815 --- /dev/null +++ b/pages/url-shortener.html @@ -0,0 +1,173 @@ + + + + + + URL Shortener - Digital Services Hub + + + + + + + +
+ +
+

URL Shortener

+

Create short, memorable links instantly

+
+
+ +
+
+
+
+ + +
+ +
+
+ + Create a memorable custom link (optional) +
+ +
+ + +
+
+
+ + + + +
+ + + +
+

Features

+
+
+ +

Click Analytics

+

Track the performance of your shortened URLs with detailed click statistics.

+
+
+ +

QR Code Generation

+

Generate QR codes for your shortened URLs instantly.

+
+
+ +

Custom Expiry

+

Set custom expiration dates for your shortened URLs.

+
+
+ +

Custom Aliases

+

Create memorable custom aliases for your links.

+
+
+
+
+ + + + + + \ No newline at end of file