mirror of
https://github.com/arkorty/Reduce.git
synced 2026-03-18 00:47:10 +00:00
overhaul
This commit is contained in:
16
frontend/src/App.tsx
Normal file
16
frontend/src/App.tsx
Normal 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
9
frontend/src/index.css
Normal 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
10
frontend/src/main.tsx
Normal 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
138
frontend/src/pages/Home.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
62
frontend/src/pages/Redirect.tsx
Normal file
62
frontend/src/pages/Redirect.tsx
Normal 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
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user