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
43 changes: 43 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

.cursor/

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
37 changes: 37 additions & 0 deletions frontend/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": true,
"includes": ["**", "!node_modules", "!.next", "!dist", "!build"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noUnknownAtRules": "off"
}
},
"domains": {
"next": "recommended",
"react": "recommended"
}
},
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
974 changes: 974 additions & 0 deletions frontend/bun.lock

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions frontend/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "gray",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
16 changes: 16 additions & 0 deletions frontend/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
reactCompiler: true,
images: {
remotePatterns: [
{
protocol: "https",
hostname: "api.qrserver.com",
},
],
},
};

export default nextConfig;
40 changes: 40 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "lnk",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "biome check",
"lint:fix": "biome check --write",
"format": "biome format --write"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-toast": "^1.2.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.553.0",
"next": "16.0.3",
"next-themes": "^0.4.6",
"react": "19.2.0",
"react-dom": "19.2.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@biomejs/biome": "2.2.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"babel-plugin-react-compiler": "1.0.0",
"shadcn": "^3.5.0",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
}
7 changes: 7 additions & 0 deletions frontend/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};

export default config;
1 change: 1 addition & 0 deletions frontend/public/file.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/public/globe.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/public/next.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/public/vercel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/public/window.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions frontend/src/app/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use server";

// Simple URL shortener using a hash function
// In production, you'd want to use a database to store mappings
function generateShortCode(url: string): string {
let hash = 0;
const timestamp = Date.now().toString(36);

for (let i = 0; i < url.length; i++) {
const char = url.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}

const shortCode =
Math.abs(hash).toString(36).substring(0, 6) + timestamp.substring(0, 3);
return shortCode;
}

export async function shortenUrl(url: string) {
// Validate URL
try {
new URL(url);
} catch {
throw new Error("Invalid URL");
}

// Generate short code
const shortCode = generateShortCode(url);

// In production, you would save this to a database
// For now, we'll just return a mock shortened URL
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://lnk.sh";
const shortUrl = `${baseUrl}/${shortCode}`;

// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 500));

return {
shortUrl,
originalUrl: url,
shortCode,
};
}
Binary file added frontend/src/app/favicon.ico
Binary file not shown.
115 changes: 115 additions & 0 deletions frontend/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
@import "tailwindcss";
@import "tw-animate-css";

@custom-variant dark (&:is(.dark *));

:root {
--background: oklch(0.98 0 0);
--foreground: oklch(0.15 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.15 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.15 0 0);
--primary: oklch(0.45 0.15 264);
--primary-foreground: oklch(0.98 0 0);
--secondary: oklch(0.94 0 0);
--secondary-foreground: oklch(0.15 0 0);
--muted: oklch(0.96 0 0);
--muted-foreground: oklch(0.5 0 0);
--accent: oklch(0.94 0 0);
--accent-foreground: oklch(0.15 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.9 0 0);
--input: oklch(0.9 0 0);
--ring: oklch(0.45 0.15 264);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.75rem;
}

.dark {
--background: oklch(0.12 0 0);
--foreground: oklch(0.98 0 0);
--card: oklch(0.16 0 0);
--card-foreground: oklch(0.98 0 0);
--popover: oklch(0.16 0 0);
--popover-foreground: oklch(0.98 0 0);
--primary: oklch(0.6 0.18 264);
--primary-foreground: oklch(0.98 0 0);
--secondary: oklch(0.22 0 0);
--secondary-foreground: oklch(0.98 0 0);
--muted: oklch(0.22 0 0);
--muted-foreground: oklch(0.6 0 0);
--accent: oklch(0.22 0 0);
--accent-foreground: oklch(0.98 0 0);
--destructive: oklch(0.5 0.2 27);
--destructive-foreground: oklch(0.98 0 0);
--border: oklch(0.22 0 0);
--input: oklch(0.22 0 0);
--ring: oklch(0.6 0.18 264);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
}

@theme inline {
--font-sans: "Geist", "Geist Fallback";
--font-mono: "Geist Mono", "Geist Mono Fallback";
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}

@layer base {
* {
@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;
}
}
46 changes: 46 additions & 0 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Toaster } from "@/components/ui/sonner";

const _geist = Geist({ subsets: ["latin"] });
const _geistMono = Geist_Mono({ subsets: ["latin"] });

export const metadata: Metadata = {
title: "lnk - Fast & Simple URL Shortener",
description:
"Shorten your links instantly with our modern, easy-to-use URL shortener",
generator: "v0.app",
icons: {
icon: [
{
url: "/icon-light-32x32.png",
media: "(prefers-color-scheme: light)",
},
{
url: "/icon-dark-32x32.png",
media: "(prefers-color-scheme: dark)",
},
{
url: "/icon.svg",
type: "image/svg+xml",
},
],
apple: "/apple-icon.png",
},
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className="dark">
<body className={`font-sans antialiased`}>
{children}
<Toaster />
</body>
</html>
);
}
Loading
Loading