diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d1866e9..e57113d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 449b88b..91accb2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b29af3f..e92770d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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; 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({ + 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() {

- 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!

-
+ 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 &&

{error}

} - + {errors.url && ( +

{errors.url.message}

+ )} + {errors.root && ( +

{errors.root.message}

+ )} +
@@ -106,7 +133,7 @@ export default function App() { )} - +

@@ -121,10 +148,13 @@ export default function App() {

-

Zero-Knowledge Encryption

+

+ Zero-Knowledge Encryption +

- 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.

@@ -136,7 +166,9 @@ export default function App() {

Secure Storage

- 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.

@@ -150,8 +182,8 @@ export default function App() {

Complete Anonymity

- 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.

@@ -163,7 +195,9 @@ export default function App() {

Security Process

- 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.

@@ -172,7 +206,9 @@ export default function App() {

- 🔒 Important: Make sure to Bookmark your tnyr.me links safely - there's no way to recover lost IDs or access links without them. + 🔒 Important: Make sure to + Bookmark your tnyr.me links safely - there's no way to recover + lost IDs or access links without them.

@@ -193,9 +229,9 @@ export default function App() { rel="noopener noreferrer" className="hover:text-slate-300 transition-colors" > - + ); -} \ No newline at end of file +}