Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
/target
**/*.rs.bk
.something/
node_modules/
frontend/node_modules/
*.lock
40 changes: 40 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "url-shortener-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.18.0",
"react-helmet-async": "^2.0.1",
"@tanstack/react-query": "^5.8.0",
"zustand": "^4.4.6",
"axios": "^1.6.0"
},
"devDependencies": {
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16",
"daisyui": "^4.0.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"typescript": "^5.2.2",
"vite": "^5.0.0",
"vitest": "^1.0.0",
"@testing-library/react": "^14.1.0",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/user-event": "^14.5.1",
"jsdom": "^22.1.0",
"msw": "^2.0.0"
}
}
14 changes: 14 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</BrowserRouter>
);
}

export default App;
12 changes: 12 additions & 0 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { HelmetProvider } from 'react-helmet-async';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<HelmetProvider>
<App />
</HelmetProvider>
</React.StrictMode>
);
175 changes: 175 additions & 0 deletions frontend/src/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { Helmet } from 'react-helmet-async';
import type { SEOConfig } from '../types/home';

const SEO_CONFIG: SEOConfig = {
title: 'Shorten Links, Track Clicks | URL Shortener',
description: 'Free URL shortener with analytics. Create short links, track clicks, and measure performance. Get started in seconds.',
canonicalUrl: 'https://urlshortener.example.com',
ogImage: 'https://urlshortener.example.com/og-image.png',
};

const STRUCTURED_DATA = {
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: 'URL Shortener',
description: SEO_CONFIG.description,
url: SEO_CONFIG.canonicalUrl,
applicationCategory: 'UtilityApplication',
operatingSystem: 'Web',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
};

function Home() {
return (
<>
<Helmet>
<title>{SEO_CONFIG.title}</title>
<meta name="description" content={SEO_CONFIG.description} />
<link rel="canonical" href={SEO_CONFIG.canonicalUrl} />

{/* Open Graph tags */}
<meta property="og:title" content={SEO_CONFIG.title} />
<meta property="og:description" content={SEO_CONFIG.description} />
<meta property="og:image" content={SEO_CONFIG.ogImage} />
<meta property="og:url" content={SEO_CONFIG.canonicalUrl} />
<meta property="og:type" content="website" />

{/* Twitter Card tags */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={SEO_CONFIG.title} />
<meta name="twitter:description" content={SEO_CONFIG.description} />
<meta name="twitter:image" content={SEO_CONFIG.ogImage} />

{/* Structured data */}
<script type="application/ld+json">
{JSON.stringify(STRUCTURED_DATA)}
</script>
</Helmet>

<div className="min-h-screen flex flex-col">
<header role="banner">
<nav role="navigation" aria-label="Main navigation" className="navbar bg-base-100 shadow-lg">
<div className="flex-1">
<a href="/" className="btn btn-ghost text-xl">URL Shortener</a>
</div>
<div className="flex-none">
<ul className="menu menu-horizontal px-1">
<li><a href="/login">Login</a></li>
<li><a href="/register">Register</a></li>
</ul>
</div>
</nav>
</header>

<main id="main-content" role="main" className="flex-grow">
<section aria-labelledby="hero-heading" className="hero min-h-[60vh] bg-gradient-to-br from-primary to-secondary">
<div className="hero-content text-center">
<div className="max-w-2xl">
<h1 id="hero-heading" className="text-5xl font-bold text-white">
Shorten Links, Track Clicks
</h1>
<p className="py-6 text-lg text-white/90">
Create short, memorable links and track their performance with powerful analytics.
</p>
<div className="flex gap-4 justify-center">
<a href="/register" className="btn btn-primary btn-lg">Get Started Free</a>
<a href="#features" className="btn btn-outline btn-lg text-white border-white hover:bg-white hover:text-primary">
Learn More
</a>
</div>
</div>
</div>
</section>

<section id="features" aria-labelledby="features-heading" className="py-20 px-4">
<div className="max-w-6xl mx-auto">
<h2 id="features-heading" className="text-3xl font-bold text-center mb-12">
Why Choose Us?
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<article className="card bg-base-200 shadow-xl">
<div className="card-body">
<h3 className="card-title">URL Shortening</h3>
<p>Create short, shareable links instantly.</p>
</div>
</article>
<article className="card bg-base-200 shadow-xl">
<div className="card-body">
<h3 className="card-title">Analytics</h3>
<p>Track clicks, locations, and devices.</p>
</div>
</article>
<article className="card bg-base-200 shadow-xl">
<div className="card-body">
<h3 className="card-title">Dashboard</h3>
<p>Manage all your links in one place.</p>
</div>
</article>
<article className="card bg-base-200 shadow-xl">
<div className="card-body">
<h3 className="card-title">Security</h3>
<p>Enterprise-grade security for your links.</p>
</div>
</article>
</div>
</div>
</section>

<section aria-labelledby="how-it-works-heading" className="py-20 px-4 bg-base-200">
<div className="max-w-4xl mx-auto text-center">
<h2 id="how-it-works-heading" className="text-3xl font-bold mb-12">
How It Works
</h2>
<div className="flex flex-col md:flex-row gap-8 justify-center">
<div className="flex-1">
<div className="text-4xl mb-4">1</div>
<h3 className="font-bold text-xl mb-2">Paste</h3>
<p>Enter your long URL</p>
</div>
<div className="flex-1">
<div className="text-4xl mb-4">2</div>
<h3 className="font-bold text-xl mb-2">Shorten</h3>
<p>Get a short link instantly</p>
</div>
<div className="flex-1">
<div className="text-4xl mb-4">3</div>
<h3 className="font-bold text-xl mb-2">Share</h3>
<p>Share and track clicks</p>
</div>
</div>
</div>
</section>
</main>

<footer role="contentinfo" className="footer p-10 bg-neutral text-neutral-content">
<nav aria-label="Footer navigation">
<h4 className="footer-title">Product</h4>
<a href="/features" className="link link-hover">Features</a>
<a href="/pricing" className="link link-hover">Pricing</a>
<a href="/api" className="link link-hover">API</a>
</nav>
<nav aria-label="Legal">
<h4 className="footer-title">Legal</h4>
<a href="/terms" className="link link-hover">Terms of Service</a>
<a href="/privacy" className="link link-hover">Privacy Policy</a>
</nav>
<nav aria-label="Contact">
<h4 className="footer-title">Contact</h4>
<a href="mailto:support@urlshortener.example.com" className="link link-hover">Support</a>
</nav>
<div>
<p>&copy; 2024 URL Shortener. All rights reserved.</p>
</div>
</footer>
</div>
</>
);
}

export default Home;

export { SEO_CONFIG, STRUCTURED_DATA };
11 changes: 11 additions & 0 deletions frontend/src/types/custom.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// <reference types="vite/client" />

declare module '*.svg' {
const content: string;
export default content;
}

declare module '*.png' {
const content: string;
export default content;
}
45 changes: 45 additions & 0 deletions frontend/src/types/home.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Homepage-specific type definitions.
* Owner: First scenario builder
*/

export interface Feature {
id: string;
icon: string;
title: string;
description: string;
}

export interface Testimonial {
id: string;
quote: string;
author: string;
role: string;
company?: string;
avatar?: string;
}

export interface FAQItem {
id: string;
question: string;
answer: string;
}

export interface PublicStats {
total_urls: number;
total_clicks: number;
active_users?: number;
}

export interface ShortenResponse {
short_code: string;
short_url: string;
original_url: string;
}

export interface SEOConfig {
title: string;
description: string;
canonicalUrl: string;
ogImage?: string;
}
Loading