anycoder-bc3e2d05 / components /URLShortener.jsx
KVRT's picture
Upload components/URLShortener.jsx with huggingface_hub
b5caa3a verified
import { useState, useEffect } from 'react'
import {
Link,
Copy,
Check,
AlertTriangle,
Clock,
Globe,
Sparkles,
Shield,
ChevronDown,
ChevronUp,
Loader2,
ExternalLink,
BarChart3,
Eye,
MousePointerClick
} from 'lucide-react'
export default function URLShortener() {
const [url, setUrl] = useState('')
const [customSlug, setCustomSlug] = useState('')
const [expiresAt, setExpiresAt] = useState('')
const [showAdvanced, setShowAdvanced] = useState(false)
const [result, setResult] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(false)
const [copied, setCopied] = useState(false)
const [recentLinks, setRecentLinks] = useState([])
const [mounted, setMounted] = useState(false)
const [showWarning, setShowWarning] = useState(null)
useEffect(() => {
setMounted(true)
try {
const stored = localStorage.getItem('sh0rtl_recent_links')
if (stored) setRecentLinks(JSON.parse(stored))
} catch {}
}, [])
useEffect(() => {
if (recentLinks.length > 0) {
try {
localStorage.setItem('sh0rtl_recent_links', JSON.stringify(recentLinks.slice(0, 10)))
} catch {}
}
}, [recentLinks])
const handleSubmit = async (e) => {
e.preventDefault()
if (!url.trim()) return
setLoading(true)
setError(null)
setResult(null)
setShowWarning(null)
try {
const res = await fetch('/api/shorten', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: url.trim(), customSlug: customSlug.trim() || undefined, expiresAt: expiresAt || undefined })
})
const data = await res.json()
if (!res.ok) {
setError(data.error || 'Failed to shorten URL')
setLoading(false)
return
}
if (data.warning) {
setShowWarning(data.warning)
}
setResult(data)
setRecentLinks(prev => [data, ...prev])
setUrl('')
setCustomSlug('')
setExpiresAt('')
} catch (err) {
setError('Network error. Please try again.')
} finally {
setLoading(false)
}
}
const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch {
setError('Failed to copy to clipboard')
}
}
const getHostname = () => {
if (typeof window !== 'undefined') return window.location.origin
return 'https://sh0rtl.ink'
}
if (!mounted) return null
return (
<section id="shorten" className="relative py-20">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl sm:text-4xl font-bold mb-4">Shorten Your URL</h2>
<p className="text-slate-400">Paste a long URL and get a clean, trackable short link in seconds.</p>
</div>
<div className="bg-slate-900/80 backdrop-blur-sm border border-slate-800 rounded-2xl p-6 sm:p-8 shadow-2xl shadow-slate-950/50">
<form onSubmit={handleSubmit} className="space-y-5">
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<Link className="w-5 h-5 text-slate-500" />
</div>
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="Paste your long URL here..."
required
className="w-full pl-12 pr-4 py-4 bg-slate-950 border border-slate-700 rounded-xl text-slate-100 placeholder-slate-500 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500/50 transition-all"
/>
</div>
<button
type="button"
onClick={() => setShowAdvanced(!showAdvanced)}
className="flex items-center gap-2 text-sm text-slate-400 hover:text-emerald-400 transition-colors"
>
{showAdvanced ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
Advanced Options
</button>
{showAdvanced && (
<div className="grid sm:grid-cols-2 gap-4 animate-slide-up">
<div>
<label className="block text-sm font-medium text-slate-400 mb-2">
Custom Slug <span className="text-slate-600">(optional)</span>
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Sparkles className="w-4 h-4 text-slate-500" />
</div>
<input
type="text"
value={customSlug}
onChange={(e) => setCustomSlug(e.target.value.replace(/[^a-zA-Z0-9_-]/g, ''))}
placeholder="my-link"
maxLength={32}
className="w-full pl-10 pr-4 py-3 bg-slate-950 border border-slate-700 rounded-xl text-slate-100 placeholder-slate-600 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500/50 transition-all"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-400 mb-2">
Expires At <span className="text-slate-600">(optional)</span>
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Clock className="w-4 h-4 text-slate-500" />
</div>
<input
type="datetime-local"
value={expiresAt}
onChange={(e) => setExpiresAt(e.target.value)}
className="w-full pl-10 pr-4 py-3 bg-slate-950 border border-slate-700 rounded-xl text-slate-100 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500/50 transition-all"
/>
</div>
</div>
</div>
)}
<button
type="submit"
disabled={loading || !url.trim()}
className="w-full py-4 rounded-xl bg-gradient-to-r from-emerald-600 to-teal-600 text-white font-semibold shadow-lg shadow-emerald-500/20 hover:shadow-emerald-500/40 disabled:opacity-50 disabled:cursor-not-allowed hover:scale-[1.01] transition-all flex items-center justify-center gap-2"
>
{loading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Shortening...
</>
) : (
<>
<Sparkles className="w-5 h-5" />
Shorten URL
</>
)}
</button>
</form>
{showWarning && (
<div className="mt-5 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-start gap-3 animate-fade-in">
<AlertTriangle className="w-5 h-5 text-amber-400 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-amber-400">Security Warning</p>
<p className="text-sm text-amber-300/80 mt-1">{showWarning}</p>
</div>
</div>
)}
{error && (
<div className="mt-5 p-4 rounded-xl bg-red-500/10 border border-red-500/20 flex items-start gap-3 animate-fade-in">
<AlertTriangle className="w-5 h-5 text-red-400 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-red-400">Error</p>
<p className="text-sm text-red-300/80 mt-1">{error}</p>
</div>
</div>
)}
{result && (
<div className="mt-5 p-5 rounded-xl bg-emerald-500/5 border border-emerald-500/20 animate-fade-in">
<div className="flex items-center gap-2 mb-3">
<Shield className="w-4 h-4 text-emerald-400" />
<span className="text-sm font-medium text-emerald-400">URL Shortened Successfully</span>
</div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
<div className="flex-1 min-w-0">
<p className="text-sm text-slate-500 mb-1">Short Link</p>
<a
href={result.shortUrl}
target="_blank"
rel="noopener noreferrer"
className="font-mono text-lg text-emerald-400 hover:text-emerald-300 transition-colors truncate block"
>
{result.shortUrl}
</a>
</div>
<button
onClick={() => copyToClipboard(result.shortUrl)}
className="shrink-0 px-4 py-2.5 rounded-lg bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-300 transition-all flex items-center justify-center gap-2"
>
{copied ? <Check className="w-4 h-4 text-emerald-400" /> : <Copy className="w-4 h-4" />}
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
<div className="mt-3 pt-3 border-t border-slate-800">
<p className="text-xs text-slate-500 truncate">
Original: <span className="text-slate-400">{result.originalUrl}</span>
</p>
</div>
</div>
)}
</div>
{recentLinks.length > 0 && (
<div className="mt-8">
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Clock className="w-5 h-5 text-slate-400" />
Recent Links
</h3>
<div className="space-y-3">
{recentLinks.slice(0, 5).map((link, idx) => (
<div
key={link.slug + idx}
className="bg-slate-900/60 border border-slate-800 rounded-xl p-4 hover:border-slate-700 transition-colors group"
>
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<div className="flex-1 min-w-0">
<a
href={link.shortUrl}
target="_blank"
rel="noopener noreferrer"
className="font-mono text-sm text-emerald-400 hover:text-emerald-300 transition-colors flex items-center gap-1"
>
{link.shortUrl}
<ExternalLink className="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity" />
</a>
<p className="text-xs text-slate-500 truncate mt-1">{link.originalUrl}</p>
</div>
<div className="flex items-center gap-4 shrink-0">
<div className="flex items-center gap-1.5 text-xs text-slate-400">
<MousePointerClick className="w-3.5 h-3.5" />
<span>{link.clicks} clicks</span>
</div>
{link.expiresAt && (
<div className="flex items-center gap-1.5 text-xs text-amber-400">
<Clock className="w-3.5 h-3.5" />
<span>Expires</span>
</div>
)}
<button
onClick={() => copyToClipboard(link.shortUrl)}
className="p-1.5 rounded-md hover:bg-slate-800 text-slate-500 hover:text-slate-300 transition-colors"
title="Copy link"
>
<Copy className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</section>
)
}