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
540 changes: 524 additions & 16 deletions frontend/bun.lock

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions frontend/orval.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineConfig } from "orval";

export default defineConfig({
"lnk-file": {
input: {
target: "http://localhost:8080/swagger/doc.json",
validation: false,
},
output: {
target: "./src/api/lnk.ts",
client: "fetch",
override: {
mutator: {
path: "./src/api/undici-instance.ts",
name: "customInstance",
default: false,
},
},
},
},
});
7 changes: 5 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"start": "next start",
"lint": "biome check",
"lint:fix": "biome check --write",
"format": "biome format --write"
"format": "biome format --write",
"generate:api": "orval"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.15",
Expand All @@ -24,7 +25,8 @@
"react-dom": "19.2.0",
"react-hook-form": "^7.66.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0"
"tailwind-merge": "^3.4.0",
"undici": "^7.16.0"
},
"devDependencies": {
"@biomejs/biome": "2.2.0",
Expand All @@ -33,6 +35,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"babel-plugin-react-compiler": "1.0.0",
"orval": "^8.0.0-rc.2",
"shadcn": "^3.5.0",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
Expand Down
158 changes: 158 additions & 0 deletions frontend/src/api/lnk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* Generated by orval v8.0.0-rc.2 🍺
* Do not edit manually.
* LNK URL Shortener API
* A URL shortener service API
* OpenAPI spec version: 1.0
*/
import { customInstance } from './undici-instance';
export interface HandlersCreateURLRequest {
url: string;
}

export interface HandlersCreateURLResponse {
original_url?: string;
short_url?: string;
}

export interface HandlersErrorResponse {
error?: string;
}

export type GetHealth200 = {[key: string]: string};

export type GetShortUrl308 = {[key: string]: string};

export type GetShortUrl500 = {[key: string]: string};

/**
* Check if the API is running
* @summary Health check endpoint
*/
export type getHealthResponse200 = {
data: GetHealth200
status: 200
}

export type getHealthResponseSuccess = (getHealthResponse200) & {
headers: Headers;
};
;

export type getHealthResponse = (getHealthResponseSuccess)

export const getGetHealthUrl = () => {




return `/health`
}

export const getHealth = async ( options?: RequestInit): Promise<getHealthResponse> => {

return customInstance<getHealthResponse>(getGetHealthUrl(),
{
...options,
method: 'GET'


}
);}



/**
* Create a short URL from a long URL
* @summary Create a short URL
*/
export type postShortenResponse200 = {
data: HandlersCreateURLResponse
status: 200
}

export type postShortenResponse400 = {
data: HandlersErrorResponse
status: 400
}

export type postShortenResponse500 = {
data: HandlersErrorResponse
status: 500
}

export type postShortenResponseSuccess = (postShortenResponse200) & {
headers: Headers;
};
export type postShortenResponseError = (postShortenResponse400 | postShortenResponse500) & {
headers: Headers;
};

export type postShortenResponse = (postShortenResponseSuccess | postShortenResponseError)

export const getPostShortenUrl = () => {




return `/shorten`
}

export const postShorten = async (handlersCreateURLRequest: HandlersCreateURLRequest, options?: RequestInit): Promise<postShortenResponse> => {

return customInstance<postShortenResponse>(getPostShortenUrl(),
{
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(
handlersCreateURLRequest,)
}
);}



/**
* Get the original URL from a short URL
* @summary Get original URL by short URL
*/
export type getShortUrlResponse308 = {
data: GetShortUrl308
status: 308
}

export type getShortUrlResponse404 = {
data: HandlersErrorResponse
status: 404
}

export type getShortUrlResponse500 = {
data: GetShortUrl500
status: 500
}

;
export type getShortUrlResponseError = (getShortUrlResponse308 | getShortUrlResponse404 | getShortUrlResponse500) & {
headers: Headers;
};

export type getShortUrlResponse = (getShortUrlResponseError)

export const getGetShortUrlUrl = (shortUrl: string,) => {




return `/${shortUrl}`
}

export const getShortUrl = async (shortUrl: string, options?: RequestInit): Promise<getShortUrlResponse> => {

return customInstance<getShortUrlResponse>(getGetShortUrlUrl(shortUrl),
{
...options,
method: 'GET'


}
);}
53 changes: 53 additions & 0 deletions frontend/src/api/undici-instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const baseURL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8080";

const getFetch = async (): Promise<typeof fetch> => {
if (typeof window === "undefined") {
try {
const { fetch: undiciFetch } = await import("undici");
return undiciFetch as unknown as typeof fetch;
} catch {
return globalThis.fetch;
}
} else {
return window.fetch;
}
};

export const customInstance = async <T>(
url: string,
options?: RequestInit,
): Promise<{
data: T;
status: number;
statusText: string;
headers: Headers;
}> => {
const fullUrl = `${baseURL}${url}`;

const fetchImpl = await getFetch();
const response = await fetchImpl(fullUrl, {
...options,
method: options?.method || "GET",
});

let responseData: T;
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
responseData = await response.json();
} else {
const text = await response.text();
try {
responseData = JSON.parse(text) as T;
} catch {
responseData = text as unknown as T;
}
}

return {
data: responseData,
status: response.status,
statusText: response.statusText,
headers: response.headers,
};
};

56 changes: 56 additions & 0 deletions frontend/src/app/[shortUrl]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { NextResponse } from "next/server";
import { getShortUrl } from "@/api/lnk";

export async function GET(
_request: Request,
context: { params: Promise<{ shortUrl: string }> | { shortUrl: string } }
) {
const params = context.params;
const resolvedParams = params instanceof Promise ? await params : params;
const shortUrl = resolvedParams?.shortUrl;

if (!shortUrl) {
return NextResponse.json(
{ error: "Short URL parameter is required" },
{ status: 400 }
);
}

try {
const response = await getShortUrl(shortUrl);

console.log("response", response);

if (response.status === 308) {
const location = response.headers.get("Location");
if (location) {
return NextResponse.redirect(location, 308);
}

const data = response.data as { [key: string]: string };
const originalUrl = data.original_url || data.url || Object.values(data)[0];
if (originalUrl) {
return NextResponse.redirect(originalUrl, 308);
}
}

if (response.status === 404) {
return NextResponse.json(
{ error: "Short URL not found" },
{ status: 404 }
);
}

return NextResponse.json(
{ error: "Failed to redirect", status: response.status },
{ status: response.status }
);
} catch (error) {
console.error("Error fetching short URL:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

44 changes: 0 additions & 44 deletions frontend/src/app/actions.ts

This file was deleted.

6 changes: 3 additions & 3 deletions frontend/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function DialogOverlay({
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 backdrop-blur-sm",
className,
)}
{...props}
Expand All @@ -60,7 +60,7 @@ function DialogContent({
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-100 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className,
)}
{...props}
Expand All @@ -69,7 +69,7 @@ function DialogContent({
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
className="ring-offset-background focus:ring-ring text-foreground data-[state=open]:bg-accent data-[state=open]:text-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 hover:text-foreground focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
Expand Down
Loading
Loading