mirror of
https://github.com/Sevi-py/tnyr.me.git
synced 2026-02-19 11:54:45 -05:00
Merge pull request #4 from ayequill/fix/enhanced-form-and-optimize-error-handling
Update dependencies and refactor URL handling in App component
This commit is contained in:
44
frontend/package-lock.json
generated
44
frontend/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "tnyr-frontend",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@icons-pack/react-simple-icons": "^11.2.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
@@ -18,8 +19,10 @@
|
||||
"lucide-react": "^0.474.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.25.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
@@ -891,6 +894,17 @@
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.0.1.tgz",
|
||||
"integrity": "sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==",
|
||||
"dependencies": {
|
||||
"@standard-schema/utils": "^0.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-hook-form": "^7.55.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -1804,6 +1818,11 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -3854,6 +3873,21 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.56.4",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.4.tgz",
|
||||
"integrity": "sha512-Rob7Ftz2vyZ/ZGsQZPaRdIefkgOSrQSPXfqBdvOPwJfoGnjwRJUs7EM7Kc1mcoDv3NOtqBzPGbcMB8CGn9CKgw==",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-hook-form"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||
@@ -4690,6 +4724,14 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.32",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.32.tgz",
|
||||
"integrity": "sha512-OSm2xTIRfW8CV5/QKgngwmQW/8aPfGdaQFlrGoErlgg/Epm7cjb6K6VEyExfe65a3VybUOnu381edLb0dfJl0g==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@icons-pack/react-simple-icons": "^11.2.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
@@ -20,8 +21,10 @@
|
||||
"lucide-react": "^0.474.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.25.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
|
||||
@@ -1,44 +1,63 @@
|
||||
import { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { Input } from './components/ui/input';
|
||||
import { Button } from './components/ui/button';
|
||||
import { Shield, Key, Hash, Lock, Copy, EyeOff, Github } from 'lucide-react';
|
||||
import { SiBuymeacoffee } from '@icons-pack/react-simple-icons';
|
||||
import { useState } from "react";
|
||||
import axios from "axios";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { Input } from "./components/ui/input";
|
||||
import { Button } from "./components/ui/button";
|
||||
import { Shield, Key, Hash, Lock, Copy, EyeOff, Github } from "lucide-react";
|
||||
import { SiBuymeacoffee } from "@icons-pack/react-simple-icons";
|
||||
|
||||
const urlSchema = z.object({
|
||||
url: z
|
||||
.string()
|
||||
.min(1, "URL is required")
|
||||
.url("Please enter a valid URL")
|
||||
.refine((url) => {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
return (
|
||||
parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:"
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, "URL must start with http:// or https://"),
|
||||
});
|
||||
|
||||
type UrlFormData = z.infer<typeof urlSchema>;
|
||||
|
||||
export default function App() {
|
||||
const [url, setUrl] = useState('');
|
||||
const [shortened, setShortened] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [shortened, setShortened] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// const isValidUrl = (string) => {
|
||||
// try {
|
||||
// new URL(string);
|
||||
// return true;
|
||||
// } catch (_) {
|
||||
// return false;
|
||||
// }
|
||||
// };
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setError,
|
||||
clearErrors,
|
||||
} = useForm<UrlFormData>({
|
||||
resolver: zodResolver(urlSchema),
|
||||
mode: "onChange", // Validate on change for better UX
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
// if (!isValidUrl(url)) {
|
||||
// setError('Please enter a valid URL');
|
||||
// return;
|
||||
// }
|
||||
|
||||
const onSubmit = async (data: UrlFormData) => {
|
||||
setLoading(true);
|
||||
clearErrors();
|
||||
|
||||
try {
|
||||
const response = await axios.post('/shorten', {
|
||||
url: url
|
||||
const response = await axios.post("/shorten", {
|
||||
url: data.url,
|
||||
});
|
||||
const shortUrl = `tnyr.me/${response.data.id}`;
|
||||
setShortened(shortUrl);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
setError('Error shortening URL. Please try again.');
|
||||
} catch {
|
||||
// Simplified error handling - all errors go to root
|
||||
setError("root", { message: "Error shortening URL. Please try again." });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const copyToClipboard = () => {
|
||||
@@ -60,27 +79,35 @@ export default function App() {
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<Lock className="w-5 h-5" />
|
||||
<p className="text-center">
|
||||
Your links are encrypted - we can't see your destination URLs or share your links!
|
||||
Your links are encrypted - we can't see your destination URLs or
|
||||
share your links!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col space-y-4">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
{...register("url")}
|
||||
placeholder="Enter your long URL here"
|
||||
className="bg-slate-700/50 border-slate-600 text-lg h-14 rounded-xl"
|
||||
/>
|
||||
{error && <p className="text-red-400 text-sm">{error}</p>}
|
||||
|
||||
{errors.url && (
|
||||
<p className="text-red-400 text-sm">{errors.url.message}</p>
|
||||
)}
|
||||
{errors.root && (
|
||||
<p className="text-red-400 text-sm">{errors.root.message}</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="h-12 text-lg rounded-xl bg-indigo-600 hover:bg-indigo-700 transition-colors"
|
||||
>
|
||||
{loading ? 'Shortening...' : 'Create Secure Link'}
|
||||
{loading ? "Shortening..." : "Create Secure Link"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -106,7 +133,7 @@ export default function App() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="w-full max-w-3xl mt-12 mb-8">
|
||||
<div className="bg-slate-800/50 backdrop-blur-sm rounded-xl p-6 shadow-xl border border-slate-700/30">
|
||||
<h2 className="text-2xl font-semibold mb-6 flex items-center gap-2">
|
||||
@@ -121,10 +148,13 @@ export default function App() {
|
||||
<Shield className="w-5 h-5 text-indigo-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">Zero-Knowledge Encryption</h3>
|
||||
<h3 className="font-medium mb-1">
|
||||
Zero-Knowledge Encryption
|
||||
</h3>
|
||||
<p className="text-slate-400 text-sm">
|
||||
Your URL is encrypted using AES-256 with a key derived from your unique link ID.
|
||||
Not even we can decrypt or view your original URL.
|
||||
Your URL is encrypted using AES-256 with a key derived from
|
||||
your unique link ID. Not even we can decrypt or view your
|
||||
original URL.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,7 +166,9 @@ export default function App() {
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">Secure Storage</h3>
|
||||
<p className="text-slate-400 text-sm">
|
||||
We generate two separate hashes - one for identification and another for encrypting the destination. Without the exact ID, the link is completely inaccessible.
|
||||
We generate two separate hashes - one for identification and
|
||||
another for encrypting the destination. Without the exact
|
||||
ID, the link is completely inaccessible.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,8 +182,8 @@ export default function App() {
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">Complete Anonymity</h3>
|
||||
<p className="text-slate-400 text-sm">
|
||||
There's no way to discover or list existing links. Each URL exists
|
||||
only for those who possess the unique ID.
|
||||
There's no way to discover or list existing links. Each URL
|
||||
exists only for those who possess the unique ID.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -163,7 +195,9 @@ export default function App() {
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">Security Process</h3>
|
||||
<p className="text-slate-400 text-sm">
|
||||
We never log IP addresses, track users, or use cookies. Each request is completely anonymous - your browsing activity leaves no trace in our systems.
|
||||
We never log IP addresses, track users, or use cookies. Each
|
||||
request is completely anonymous - your browsing activity
|
||||
leaves no trace in our systems.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,7 +206,9 @@ export default function App() {
|
||||
|
||||
<div className="mt-6 p-4 bg-slate-700/30 rounded-lg border border-slate-700/50">
|
||||
<p className="text-sm text-slate-400">
|
||||
🔒 <span className="font-medium">Important:</span> Make sure to Bookmark your tnyr.me links safely - there's no way to recover lost IDs or access links without them.
|
||||
🔒 <span className="font-medium">Important:</span> Make sure to
|
||||
Bookmark your tnyr.me links safely - there's no way to recover
|
||||
lost IDs or access links without them.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,9 +229,9 @@ export default function App() {
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-slate-300 transition-colors"
|
||||
>
|
||||
<SiBuymeacoffee className="w-8 h-8" />
|
||||
<SiBuymeacoffee className="w-8 h-8" />
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user