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
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total Clicks
+ 0
+
+
+ Created
+ Just now
+
+
+ Expires
+ Never
+
+
+
+
+
+
+
+
+
+
+ Download QR Code
+
+
+
+
+
+
+
+
+
+ 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