This commit is contained in:
Arkaprabha Chakraborty
2025-12-14 05:33:05 +05:30
parent 25ead1d8bd
commit 43c516a005
37 changed files with 1037 additions and 523 deletions

16
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import Redirect from './pages/Redirect';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/:code" element={<Redirect />} />
</Routes>
</BrowserRouter>
);
}
export default App;

9
frontend/src/index.css Normal file
View File

@@ -0,0 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
@apply bg-zinc-950 text-zinc-300 font-mono antialiased;
}
}

10
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

138
frontend/src/pages/Home.tsx Normal file
View File

@@ -0,0 +1,138 @@
import { useState } from 'react';
import axios from 'axios';
import { MdContentCopy, MdCheck, MdRefresh } from 'react-icons/md';
import QRCode from 'react-qr-code';
export default function Home() {
const [longUrl, setLongUrl] = useState("");
const [shortUrl, setShortUrl] = useState("");
const [copied, setCopied] = useState(false);
const [prevLongUrl, setPrevLongUrl] = useState("");
const [status, setStatus] = useState<{ type: 'error' | 'success' | 'idle', msg: string }>({ type: 'idle', msg: '' });
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus({ type: 'idle', msg: '' });
if (longUrl === prevLongUrl && shortUrl) {
return;
}
if (longUrl.trim() === "") {
return;
}
try {
new URL(longUrl);
} catch (_) {
setStatus({ type: 'error', msg: 'Invalid URL' });
return;
}
const baseURL = window.location.origin;
const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8080';
try {
const response = await axios.post(
`${backendUrl}/reduce/shorten`,
{
lurl: longUrl,
base_url: baseURL,
},
);
setShortUrl(response.data.surl);
setPrevLongUrl(longUrl);
setStatus({ type: 'success', msg: '' });
} catch (error) {
console.error("Error shortening URL:", error);
setStatus({ type: 'error', msg: 'Error' });
}
};
const handleCopy = () => {
navigator.clipboard.writeText(shortUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const handleReset = () => {
setLongUrl("");
setShortUrl("");
setPrevLongUrl("");
setStatus({ type: 'idle', msg: '' });
setCopied(false);
};
return (
<div className="flex flex-col items-center justify-center min-h-screen p-4">
<div className="w-full max-w-md">
{/* Header */}
<header className="mb-8 text-center">
<h1 className="text-3xl font-bold tracking-widest text-zinc-100 uppercase">
Reduce
</h1>
</header>
{/* Main Interface */}
<div className="bg-zinc-900 border border-zinc-800 p-6 md:p-8">
{!shortUrl ? (
<>
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<input
type="text"
placeholder="https://..."
value={longUrl}
onChange={(e) => setLongUrl(e.target.value)}
className="w-full bg-zinc-950 border border-zinc-700 p-3 text-zinc-300 placeholder-zinc-700 focus:outline-none focus:border-zinc-400 focus:ring-1 focus:ring-zinc-400 transition-all font-mono text-sm"
/>
<button
type="submit"
className="w-full bg-zinc-200 text-zinc-950 font-bold uppercase tracking-widest py-3 hover:bg-white transition-colors border border-transparent active:border-zinc-400 active:scale-[0.99]"
>
Reduce
</button>
</form>
{/* Error Indicator */}
{status.msg && status.type === 'error' && (
<div className="mt-4 text-xs font-mono uppercase tracking-wide border-l-2 pl-3 py-1 border-red-500 text-red-400">
{status.msg}
</div>
)}
</>
) : (
<div className="flex flex-col gap-6">
<div className="flex gap-2">
<input
readOnly
value={shortUrl}
className="w-full bg-zinc-950 border border-zinc-700 p-3 text-emerald-400 font-mono text-sm focus:outline-none"
/>
<button
onClick={handleCopy}
className="bg-zinc-800 border border-zinc-700 text-zinc-300 p-3 hover:bg-zinc-700 hover:text-white transition-colors"
aria-label="Copy"
>
{copied ? <MdCheck size={20} /> : <MdContentCopy size={20} />}
</button>
</div>
<div className="flex justify-center bg-white p-4">
<QRCode value={shortUrl} size={150} style={{ height: "auto", maxWidth: "100%", width: "100%" }} />
</div>
<button
onClick={handleReset}
className="w-full bg-zinc-800 text-zinc-300 font-bold uppercase tracking-widest py-3 hover:bg-zinc-700 hover:text-white transition-colors border border-transparent flex items-center justify-center gap-2"
>
<MdRefresh size={20} /> Use Again
</button>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,62 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import axios from 'axios';
import { MdErrorOutline } from "react-icons/md";
export default function Redirect() {
const { code } = useParams();
const [error, setError] = useState(false);
useEffect(() => {
const fetchUrl = async () => {
if (!code) return;
const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8080';
try {
const response = await axios.get(
`${backendUrl}/reduce/${code}`
);
if (response.status === 200 && response.data.lurl) {
window.location.replace(response.data.lurl);
} else {
setError(true);
}
} catch (err) {
console.error("Redirect error:", err);
setError(true);
}
};
fetchUrl();
}, [code]);
if (error) {
return (
<div className="flex items-center justify-center min-h-screen p-4">
<div className="bg-zinc-900 border border-zinc-800 p-8 max-w-md w-full text-center">
<div className="flex justify-center mb-6">
<MdErrorOutline className="text-red-500 text-5xl" />
</div>
<h1 className="text-xl font-bold mb-4 text-zinc-100 uppercase tracking-widest">404 Not Found</h1>
<p className="text-zinc-500 font-mono text-sm mb-8">
Link invalid or expired.
</p>
<a href="/" className="inline-block w-full">
<span className="block bg-zinc-200 text-zinc-900 font-bold uppercase tracking-widest py-3 hover:bg-white transition-colors border border-transparent">
Go Home
</span>
</a>
</div>
</div>
);
}
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<div className="font-mono text-zinc-400 text-sm tracking-widest animate-pulse">
Redirecting...
</div>
</div>
);
}

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />