diff --git a/backend/src/main/java/services/shay/shortlink/ShortLinkController.java b/backend/src/main/java/services/shay/shortlink/ShortLinkController.java index a60e561..216341a 100644 --- a/backend/src/main/java/services/shay/shortlink/ShortLinkController.java +++ b/backend/src/main/java/services/shay/shortlink/ShortLinkController.java @@ -40,6 +40,15 @@ public class ShortLinkController { return savedShortLink; } + @GetMapping("/info/{id}") + public ResponseEntity getShortLink(@PathVariable UUID id) { + ShortLink sl = shortLinkRepository.findById(id).orElse(null); + if (sl == null) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(sl); + } + @DeleteMapping("/{id}") public void deleteShortLink(@PathVariable UUID id) { shortLinkRepository.deleteById(id); diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index fb37add..2a1ae4d 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -13,7 +13,7 @@ export default function RootLayout({ }>) { return ( - + {children} diff --git a/frontend/app/manage/[id]/page.tsx b/frontend/app/manage/[id]/page.tsx new file mode 100644 index 0000000..14ff2c4 --- /dev/null +++ b/frontend/app/manage/[id]/page.tsx @@ -0,0 +1,122 @@ +'use client'; + +import { deleteLink, getLinkInfo } from "@/app/utils/api"; +import { Clipboard } from "lucide-react"; +import { useParams } from "next/navigation"; +import { useEffect, useState } from "react"; + +export default function Home() { + const params = useParams(); + + const [errorMessage, setErrorMessage] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const [loading, setLoading] = useState(false); + + const [shortUrl, setShortUrl] = useState(null); + const [manageUrl, setManageUrl] = useState(null); + + const [linkInfo, setLinkInfo] = useState<{ + shortUrl: string; + manageUrl: string; + title: string; + createdAt: string; + clicks: number; + } | null>(null); + + async function handleDelete(event: React.FormEvent) { + event.preventDefault(); + setLoading(true); + try { + await deleteLink(params.id?.toString() || ""); + setSuccessMessage("Link deleted!"); + setErrorMessage(null); + } catch (err) { + console.error(err); + setErrorMessage("Failed to delete the link. Please try again."); + setSuccessMessage(null); + } finally { + setLoading(false); + } + } + + function copyToClipboard(text: string, title: string) { + navigator.clipboard.writeText(text).then( + () => { + setSuccessMessage(`Copied ${title} to clipboard!`); + }, + () => { + setErrorMessage(`Failed to copy ${title} to clipboard.`); + } + ); + } + + useEffect(() => { + if (!params.id) return; + getLinkInfo(params.id?.toString()).then(setLinkInfo).catch((err) => { + console.error(err); + setErrorMessage("Failed to retrieve link info. Please check the link ID."); + }); + }, [params.id]); + + return ( +
+

halflink

+

Shorten a link with ease.

+ + {errorMessage && ( +

{errorMessage}

+ )} + {successMessage && ( +

{successMessage}

+ )} + + {linkInfo && ( +
+ +

Shareable short link:

+
+ + +
+ +

Created at:

+

{new Date(linkInfo.createdAt).toLocaleString()}

+ +

Total clicks:

+

{linkInfo.clicks}

+ +
+ +
+ +

View analytics and manage your short link at:

+ {linkInfo.manageUrl} +
+ )} + +

+ Secret link ID:
{params.id} +

+
+ ); +} diff --git a/frontend/app/manage/page.tsx b/frontend/app/manage/page.tsx deleted file mode 100644 index 9ad6c4a..0000000 --- a/frontend/app/manage/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -export default function Home() { - return ( -
-

halflink

-

Shorten a link with ease.

- -

Manage your link

- -
- - -
-
- ); - } - \ No newline at end of file diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 6208240..1e37bbc 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -79,7 +79,7 @@ export default function Home() { type="text" value={shortUrl} readOnly - className="bg-white w-full max-w-md border border-stone-400 rounded-l text-center pl-20 pr-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" + className="bg-white w-full max-w-md border border-stone-400 rounded-l px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" /> - + {manageUrl} )} diff --git a/frontend/app/utils/api.ts b/frontend/app/utils/api.ts index 62010a1..eaf40f7 100644 --- a/frontend/app/utils/api.ts +++ b/frontend/app/utils/api.ts @@ -1,6 +1,7 @@ import axios from "axios"; const API_BASE_URL = "http://localhost:8080"; +const FRONTEND_BASE_URL = "http://localhost:3000"; export async function shortenLink(longUrl: string): Promise<{ shortUrl: string; @@ -18,10 +19,41 @@ export async function shortenLink(longUrl: string): Promise<{ title: string; createdAt: string; clicks: number; - } = res.data + } = res.data; return { shortUrl: API_BASE_URL + "/l/" + sl.code, - manageUrl: API_BASE_URL + "/l/" + sl.code + "/manage", + manageUrl: FRONTEND_BASE_URL + "/manage/" + sl.id, } +} + +export async function getLinkInfo(id: string): Promise<{ + shortUrl: string; + manageUrl: string; + title: string; + createdAt: string; + clicks: number; +}> { + let res = await axios.get(API_BASE_URL + "/l/info/" + id); + + let sl: { + id: string; + url: string; + code: string; + title: string; + createdAt: string; + clicks: number; + } = res.data; + + return { + title: sl.title, + shortUrl: API_BASE_URL + "/l/" + sl.code, + manageUrl: FRONTEND_BASE_URL + "/manage/" + sl.id, + createdAt: sl.createdAt, + clicks: sl.clicks, + } +} + +export async function deleteLink(id: string): Promise { + await axios.delete(API_BASE_URL + "/l/" + id); } \ No newline at end of file