From 173d216f86b9a808cb8df78930bfbae4974acffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Mon, 17 Nov 2025 12:01:08 -0300 Subject: [PATCH 1/3] feat: Implement URL input and dialog components for URL shortening functionality --- frontend/src/app/page.tsx | 8 +- frontend/src/components/url-dialog.tsx | 89 ++++++++++++ frontend/src/components/url-input.tsx | 19 +++ frontend/src/components/url-shortener.tsx | 168 +--------------------- 4 files changed, 117 insertions(+), 167 deletions(-) create mode 100644 frontend/src/components/url-dialog.tsx create mode 100644 frontend/src/components/url-input.tsx diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index bf3bcee..37446fa 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -6,15 +6,17 @@ export default function Home() {
- lnk + lnk
-

- Shorten Your Loooong Links :) +

+ + Shorten Your Loooong Links :) +

lnk is an efficient and easy-to-use URL shortening service that diff --git a/frontend/src/components/url-dialog.tsx b/frontend/src/components/url-dialog.tsx new file mode 100644 index 0000000..27cc403 --- /dev/null +++ b/frontend/src/components/url-dialog.tsx @@ -0,0 +1,89 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@radix-ui/react-dialog"; +import { CheckCircle2, Copy, QrCode } from "lucide-react"; +import { useState } from "react"; +import { DialogHeader } from "./ui/dialog"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import Image from "next/image"; + +export function UrlDialog() { + const [dialogOpen, setDialogOpen] = useState(false); + const [shortUrl, setShortUrl] = useState(""); + const [url, setUrl] = useState(""); + const [copied, setCopied] = useState(false); + + return ( +

+ + + + + Your Short URL is Ready! + + + Share this shortened link anywhere + + + +
+
+ + +
+ +
+
+ +
+ QR Code +
+
+
+ QR Code +
+
+ +
+
+ Original URL +
+
{url}
+
+ + +
+
+
+ ); +} diff --git a/frontend/src/components/url-input.tsx b/frontend/src/components/url-input.tsx new file mode 100644 index 0000000..86db225 --- /dev/null +++ b/frontend/src/components/url-input.tsx @@ -0,0 +1,19 @@ +import { LinkIcon } from "lucide-react"; +import { Input } from "./ui/input"; +import { Button } from "./ui/button"; + +export function UrlInput() { + return ( +
+ + + +
+ ); +} diff --git a/frontend/src/components/url-shortener.tsx b/frontend/src/components/url-shortener.tsx index dac7d08..1eb3d32 100644 --- a/frontend/src/components/url-shortener.tsx +++ b/frontend/src/components/url-shortener.tsx @@ -1,174 +1,14 @@ "use client"; -import { CheckCircle2, Copy, Link2, Loader2, QrCode } from "lucide-react"; -import Image from "next/image"; -import { useState } from "react"; -import { toast } from "sonner"; -import { shortenUrl } from "@/app/actions"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; +import { UrlDialog } from "./url-dialog"; +import { UrlInput } from "./url-input"; export function UrlShortener() { - const [url, setUrl] = useState(""); - const [shortUrl, setShortUrl] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [dialogOpen, setDialogOpen] = useState(false); - const [copied, setCopied] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - try { - new URL(url); - } catch { - toast.error("Invalid URL", { - description: - "Please enter a valid URL starting with http:// or https://", - }); - return; - } - - setIsLoading(true); - - try { - const result = await shortenUrl(url); - setShortUrl(result.shortUrl); - setDialogOpen(true); - setCopied(false); - } catch { - toast.error("Error", { - description: "Failed to shorten URL. Please try again.", - }); - } finally { - setIsLoading(false); - } - }; - - const handleCopy = async (text: string) => { - try { - await navigator.clipboard.writeText(text); - setCopied(true); - toast.success("Copied!", { - description: "Short URL copied to clipboard", - }); - setTimeout(() => setCopied(false), 2000); - } catch { - toast.error("Copy failed", { - description: "Please copy the URL manually", - }); - } - }; - return (
-
-
-
-
- - setUrl(e.target.value)} - className="pl-12 h-14 text-base bg-background border-2" - disabled={isLoading} - required - /> -
- -
-
-
- - - - - - - Your Short URL is Ready! - - - Share this shortened link anywhere - - - -
-
- - -
- -
-
- -
- QR Code -
-
-
- QR Code -
-
- -
-
- Original URL -
-
{url}
-
+ - -
-
-
+
); } From 9e72a792e389154832622e4dc7f098c1189450a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Mon, 17 Nov 2025 14:42:47 -0300 Subject: [PATCH 2/3] feat: Add react-hook-form for URL input validation and enhance URL shortening component --- frontend/bun.lock | 3 + frontend/package.json | 1 + frontend/src/app/page.tsx | 2 +- frontend/src/components/url-dialog.tsx | 4 +- frontend/src/components/url-input.tsx | 88 +++++++++++++++++++---- frontend/src/components/url-shortener.tsx | 10 ++- 6 files changed, 91 insertions(+), 17 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index 04ad32c..69a84ad 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -15,6 +15,7 @@ "next-themes": "^0.4.6", "react": "19.2.0", "react-dom": "19.2.0", + "react-hook-form": "^7.66.0", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", }, @@ -727,6 +728,8 @@ "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + "react-hook-form": ["react-hook-form@7.66.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw=="], + "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], diff --git a/frontend/package.json b/frontend/package.json index e888d91..b68ef2c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "next-themes": "^0.4.6", "react": "19.2.0", "react-dom": "19.2.0", + "react-hook-form": "^7.66.0", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0" }, diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 37446fa..9d01edd 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -11,7 +11,7 @@ export default function Home() {
-
+

diff --git a/frontend/src/components/url-dialog.tsx b/frontend/src/components/url-dialog.tsx index 27cc403..8dc351a 100644 --- a/frontend/src/components/url-dialog.tsx +++ b/frontend/src/components/url-dialog.tsx @@ -5,11 +5,11 @@ import { DialogTitle, } from "@radix-ui/react-dialog"; import { CheckCircle2, Copy, QrCode } from "lucide-react"; +import Image from "next/image"; import { useState } from "react"; -import { DialogHeader } from "./ui/dialog"; import { Button } from "./ui/button"; +import { DialogHeader } from "./ui/dialog"; import { Input } from "./ui/input"; -import Image from "next/image"; export function UrlDialog() { const [dialogOpen, setDialogOpen] = useState(false); diff --git a/frontend/src/components/url-input.tsx b/frontend/src/components/url-input.tsx index 86db225..b1bc4ac 100644 --- a/frontend/src/components/url-input.tsx +++ b/frontend/src/components/url-input.tsx @@ -1,19 +1,81 @@ +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; import { LinkIcon } from "lucide-react"; -import { Input } from "./ui/input"; import { Button } from "./ui/button"; +import { Input } from "./ui/input"; + +interface UrlInputProps { + url: string; + setUrl: (url: string) => void; + requestShorten: () => void; +} + +interface FormData { + url: string; +} + +export function UrlInput({ url, setUrl, requestShorten }: UrlInputProps) { + const { + register, + handleSubmit, + watch, + formState: { isValid }, + } = useForm({ + mode: "onChange", + defaultValues: { + url, + }, + values: { + url, + }, + }); + + const watchedUrl = watch("url"); + + useEffect(() => { + setUrl(watchedUrl); + }, [watchedUrl, setUrl]); + + const onSubmit = (data: FormData) => { + if (isValid) { + setUrl(data.url); + requestShorten(); + } + }; + + const validateUrl = (value: string) => { + if (!value) { + return "URL is required"; + } + try { + new URL(value); + return true; + } catch { + return "Please enter a valid URL starting with http:// or https://"; + } + }; -export function UrlInput() { return ( -
- - - -
+
+
+ + + +
+
); } diff --git a/frontend/src/components/url-shortener.tsx b/frontend/src/components/url-shortener.tsx index 1eb3d32..753248c 100644 --- a/frontend/src/components/url-shortener.tsx +++ b/frontend/src/components/url-shortener.tsx @@ -1,12 +1,20 @@ "use client"; +import { useState } from "react"; import { UrlDialog } from "./url-dialog"; import { UrlInput } from "./url-input"; export function UrlShortener() { + const [url, setUrl] = useState(""); + + const requestShorten = async () => { + // const response = await shortenUrl(url); + console.log("banana"); + }; + return (
- +
From 05bfeee8967d5d8c51812614f306f98f35271f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Mon, 17 Nov 2025 15:09:48 -0300 Subject: [PATCH 3/3] style: Update global styles and enhance URL input component for improved UI --- frontend/src/app/globals.css | 16 +-------- frontend/src/app/page.tsx | 2 +- frontend/src/components/url-input.tsx | 47 +++++++++++++-------------- 3 files changed, 24 insertions(+), 41 deletions(-) diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 89eefa3..ae1e47d 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -96,20 +96,6 @@ @apply border-border outline-ring/50; } body { - @apply bg-background text-foreground; - } - - .gradient-text { - background: linear-gradient( - 90deg, - #3b82f6 0%, - #8b5cf6 25%, - #ec4899 50%, - #8b5cf6 75%, - #3b82f6 100% - ); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + background-color: #0b101b; } } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 9d01edd..b558774 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -18,7 +18,7 @@ export default function Home() { Shorten Your Loooong Links :)

-

+

lnk is an efficient and easy-to-use URL shortening service that streamlines your online experience.

diff --git a/frontend/src/components/url-input.tsx b/frontend/src/components/url-input.tsx index b1bc4ac..bcc2251 100644 --- a/frontend/src/components/url-input.tsx +++ b/frontend/src/components/url-input.tsx @@ -1,6 +1,5 @@ -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; import { LinkIcon } from "lucide-react"; +import { useForm } from "react-hook-form"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; @@ -18,24 +17,14 @@ export function UrlInput({ url, setUrl, requestShorten }: UrlInputProps) { const { register, handleSubmit, - watch, formState: { isValid }, } = useForm({ mode: "onChange", defaultValues: { url, }, - values: { - url, - }, }); - const watchedUrl = watch("url"); - - useEffect(() => { - setUrl(watchedUrl); - }, [watchedUrl, setUrl]); - const onSubmit = (data: FormData) => { if (isValid) { setUrl(data.url); @@ -55,23 +44,31 @@ export function UrlInput({ url, setUrl, requestShorten }: UrlInputProps) { } }; + const { onChange, ...registerProps } = register("url", { + required: "URL is required", + validate: validateUrl, + }); + return ( -
-
- - + +
+
+ + { + onChange(e); + setUrl(e.target.value); + }} + className="bg-transparent dark:bg-transparent h-full text-foreground ml-3 sm:ml-4 outline-none border-none focus-visible:outline-none focus-visible:border-none focus-visible:ring-0 focus-visible:ring-offset-0 text-sm sm:text-base py-3 sm:py-0" + type="url" + placeholder="Enter the link here" + /> +