Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down Expand Up @@ -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=="],
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
16 changes: 1 addition & 15 deletions frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
12 changes: 7 additions & 5 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ export default function Home() {
<header className="border-b border-border">
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-xl font-bold gradient-text">lnk</span>
<span className="text-xl font-bold text-[#30b6db]">lnk</span>
</div>
</div>
</header>

<main className="container mx-auto px-4 py-12 md:py-20">
<main className="container mx-auto px-4 py-12 md:py-20 mt-32">
<div className="text-center mb-12 max-w-4xl mx-auto">
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold mb-6 text-balance">
<span className="gradient-text">Shorten Your Loooong Links :)</span>
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold mb-6 ">
<span className="text-[#30b6db]">
Shorten Your Loooong Links :)
</span>
</h1>
<p className="text-lg text-muted-foreground text-pretty max-w-2xl mx-auto">
<p className="text-lg text-foreground text-pretty max-w-2xl mx-auto">
lnk is an efficient and easy-to-use URL shortening service that
streamlines your online experience.
</p>
Expand Down
89 changes: 89 additions & 0 deletions frontend/src/components/url-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
} from "@radix-ui/react-dialog";
import { CheckCircle2, Copy, QrCode } from "lucide-react";
import Image from "next/image";
import { useState } from "react";
import { Button } from "./ui/button";
import { DialogHeader } from "./ui/dialog";
import { Input } from "./ui/input";

export function UrlDialog() {
const [dialogOpen, setDialogOpen] = useState(false);
const [shortUrl, setShortUrl] = useState("");
const [url, setUrl] = useState("");
const [copied, setCopied] = useState(false);

return (
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<CheckCircle2 className="size-6 text-green-500" />
Your Short URL is Ready!
</DialogTitle>
<DialogDescription>
Share this shortened link anywhere
</DialogDescription>
</DialogHeader>

<div className="flex flex-col gap-4 mt-4">
<div className="flex items-center gap-2">
<Input readOnly value={shortUrl} className="font-mono bg-muted" />
<Button
size="icon"
variant="outline"
onClick={() => setCopied(true)}
className="shrink-0"
>
{copied ? (
<CheckCircle2 className="size-4 text-green-500" />
) : (
<Copy className="size-4" />
)}
</Button>
</div>

<div className="bg-muted/50 rounded-lg p-4 border border-border">
<div className="flex items-center gap-2 mb-3">
<QrCode className="size-4 text-muted-foreground" />
<div className="text-xs font-medium text-muted-foreground">
QR Code
</div>
</div>
<div className="flex justify-center bg-background rounded-lg p-4">
<Image
src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(shortUrl)}`}
alt="QR Code"
className="size-[200px]"
width={200}
height={200}
/>
</div>
</div>

<div className="bg-muted/50 rounded-lg p-4 border border-border">
<div className="text-xs text-muted-foreground mb-1">
Original URL
</div>
<div className="text-sm break-all">{url}</div>
</div>

<Button
onClick={() => {
setDialogOpen(false);
setUrl("");
setShortUrl("");
}}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
Shorten Another URL
</Button>
</div>
</DialogContent>
</Dialog>
);
}
78 changes: 78 additions & 0 deletions frontend/src/components/url-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { LinkIcon } from "lucide-react";
import { useForm } from "react-hook-form";
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,
formState: { isValid },
} = useForm<FormData>({
mode: "onChange",
defaultValues: {
url,
},
});

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://";
}
};

const { onChange, ...registerProps } = register("url", {
required: "URL is required",
validate: validateUrl,
});

return (
<form onSubmit={handleSubmit(onSubmit)} className="w-full px-4 sm:px-6">
<div className=" bg-gray-800/50 flex flex-col sm:flex-row items-stretch sm:items-center w-full max-w-2xl lg:max-w-3xl mx-auto min-h-[60px] sm:h-[76px] rounded-2xl sm:rounded-4xl gap-2 sm:gap-0 overflow-hidden">
<div className="flex items-center flex-1 h-full min-h-[56px] sm:min-h-0 bg-transparent">
<LinkIcon className="size-4 text-foreground shrink-0 ml-4 sm:ml-6" />
<Input
{...registerProps}
onChange={(e) => {
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"
/>
</div>
<Button
type="submit"
disabled={!isValid}
className="bg-[#30b6db] text-primary-foreground sm:rounded-4xl w-full sm:w-[178px] h-full sm:h-[60px] cursor-pointer hover:shadow-lg hover:bg-[#30b6db]/90 transition-all duration-300 sm:mr-0.5 disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
>
Shorten
</Button>
</div>
</form>
);
}
Loading
Loading