feat(frontend): implement Next.js frontend package foundation with complete API integration

This commit establishes the complete Next.js frontend foundation with comprehensive backend integration:

## Core Infrastructure 
- Next.js 14 with App Router and TypeScript configuration
- Tailwind CSS with custom design system and dark mode
- Complete project structure with proper imports and path aliases

## API Integration Layer 
- Full-featured API client with authentication, file upload, and WebSocket
- Comprehensive TypeScript type definitions for all API responses
- Axios-based HTTP client with interceptors and error handling
- Socket.io integration for real-time progress updates

## Authentication System 
- useAuth hook with Google OAuth integration
- JWT token management with automatic refresh
- Protected route handling and session persistence
- Login/logout flow with redirect management

## File Upload System 
- useUpload hook with drag & drop functionality
- File validation (size, type, duplicates)
- Progress tracking during upload
- Batch creation and image processing workflow

## WebSocket Integration 
- useWebSocket hook for real-time updates
- Progress subscription for batch processing
- Reconnection logic with exponential backoff
- Event-driven updates for batches, images, and user data

## UI Foundation 
- Responsive Header with user authentication state
- Professional Footer with proper navigation
- Error Boundary for graceful error handling
- Toast notification system with multiple variants
- Loading spinners and UI components

## Layout & Navigation 
- Main page component with authenticated/unauthenticated states
- Dynamic content switching between landing and dashboard
- Mobile-responsive design with proper accessibility

This provides the complete foundation for a production-ready frontend that integrates seamlessly with the existing backend APIs, supporting all core workflows from authentication to file processing.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DustyWalker 2025-08-05 19:04:51 +02:00
parent b198bfe3cf
commit 27db3d968f
20 changed files with 3200 additions and 0 deletions

View file

@ -0,0 +1,344 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@100;200;300;400;500;600;700;800&display=swap');
/* Base styles */
@layer base {
html {
@apply scroll-smooth;
}
body {
@apply bg-white text-secondary-900 antialiased;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
}
/* Dark mode */
.dark body {
@apply bg-secondary-900 text-secondary-100;
}
/* Focus styles */
*:focus {
@apply outline-none ring-2 ring-primary-500 ring-offset-2;
}
.dark *:focus {
@apply ring-offset-secondary-900;
}
/* Selection */
::selection {
@apply bg-primary-100 text-primary-900;
}
.dark ::selection {
@apply bg-primary-800 text-primary-100;
}
/* Scrollbar */
::-webkit-scrollbar {
@apply w-2;
}
::-webkit-scrollbar-track {
@apply bg-secondary-100;
}
::-webkit-scrollbar-thumb {
@apply bg-secondary-300 rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-secondary-400;
}
.dark ::-webkit-scrollbar-track {
@apply bg-secondary-800;
}
.dark ::-webkit-scrollbar-thumb {
@apply bg-secondary-600;
}
.dark ::-webkit-scrollbar-thumb:hover {
@apply bg-secondary-500;
}
}
/* Component styles */
@layer components {
/* Button variants */
.btn {
@apply inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-primary {
@apply bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500 shadow-sm;
}
.btn-secondary {
@apply bg-secondary-100 text-secondary-900 hover:bg-secondary-200 focus:ring-secondary-500 border border-secondary-200;
}
.btn-success {
@apply bg-success-600 text-white hover:bg-success-700 focus:ring-success-500 shadow-sm;
}
.btn-danger {
@apply bg-error-600 text-white hover:bg-error-700 focus:ring-error-500 shadow-sm;
}
.btn-outline {
@apply bg-transparent text-secondary-700 hover:bg-secondary-50 focus:ring-secondary-500 border border-secondary-300;
}
.btn-ghost {
@apply bg-transparent text-secondary-600 hover:bg-secondary-100 hover:text-secondary-900 focus:ring-secondary-500;
}
.btn-sm {
@apply px-3 py-1.5 text-xs;
}
.btn-lg {
@apply px-6 py-3 text-base;
}
.btn-xl {
@apply px-8 py-4 text-lg;
}
/* Dark mode button variants */
.dark .btn-secondary {
@apply bg-secondary-800 text-secondary-100 hover:bg-secondary-700 border-secondary-700;
}
.dark .btn-outline {
@apply text-secondary-300 hover:bg-secondary-800 border-secondary-600;
}
.dark .btn-ghost {
@apply text-secondary-400 hover:bg-secondary-800 hover:text-secondary-200;
}
/* Input styles */
.input {
@apply block w-full px-3 py-2 border border-secondary-300 rounded-lg text-secondary-900 placeholder-secondary-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 disabled:bg-secondary-50 disabled:cursor-not-allowed transition-colors;
}
.dark .input {
@apply bg-secondary-800 border-secondary-600 text-secondary-100 placeholder-secondary-400 focus:border-primary-400 disabled:bg-secondary-900;
}
/* Card styles */
.card {
@apply bg-white border border-secondary-200 rounded-xl shadow-soft;
}
.dark .card {
@apply bg-secondary-800 border-secondary-700;
}
/* Modal styles */
.modal-backdrop {
@apply fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm z-40;
}
.modal-content {
@apply fixed inset-x-4 top-1/2 -translate-y-1/2 max-w-lg mx-auto bg-white rounded-xl shadow-large z-50 max-h-[90vh] overflow-y-auto;
}
.dark .modal-content {
@apply bg-secondary-800;
}
/* Loading spinner */
.spinner {
@apply animate-spin h-5 w-5 border-2 border-secondary-300 border-t-primary-600 rounded-full;
}
/* Shimmer loading effect */
.shimmer {
@apply relative overflow-hidden bg-secondary-200 rounded;
}
.shimmer::after {
@apply absolute top-0 right-0 bottom-0 left-0 bg-gradient-to-r from-transparent via-white to-transparent;
content: '';
animation: shimmer 2s infinite;
}
.dark .shimmer {
@apply bg-secondary-700;
}
.dark .shimmer::after {
@apply via-secondary-600;
}
/* Upload area */
.upload-area {
@apply border-2 border-dashed border-secondary-300 rounded-xl p-8 text-center transition-colors hover:border-primary-400 hover:bg-primary-50;
}
.upload-area.active {
@apply border-primary-500 bg-primary-50;
}
.dark .upload-area {
@apply border-secondary-600 hover:border-primary-500 hover:bg-primary-900/10;
}
.dark .upload-area.active {
@apply border-primary-400 bg-primary-900/20;
}
/* Progress bar */
.progress-bar {
@apply w-full bg-secondary-200 rounded-full h-2 overflow-hidden;
}
.progress-fill {
@apply h-full bg-primary-600 transition-all duration-300 ease-in-out;
}
.dark .progress-bar {
@apply bg-secondary-700;
}
/* Toast styles */
.toast {
@apply flex items-center gap-3 p-4 bg-white border border-secondary-200 rounded-lg shadow-medium max-w-sm;
}
.toast-success {
@apply border-success-200 bg-success-50;
}
.toast-error {
@apply border-error-200 bg-error-50;
}
.toast-warning {
@apply border-warning-200 bg-warning-50;
}
.dark .toast {
@apply bg-secondary-800 border-secondary-700;
}
.dark .toast-success {
@apply border-success-800 bg-success-900/20;
}
.dark .toast-error {
@apply border-error-800 bg-error-900/20;
}
.dark .toast-warning {
@apply border-warning-800 bg-warning-900/20;
}
/* Badge styles */
.badge {
@apply inline-flex items-center gap-1 px-2.5 py-0.5 text-xs font-medium rounded-full;
}
.badge-primary {
@apply bg-primary-100 text-primary-800;
}
.badge-success {
@apply bg-success-100 text-success-800;
}
.badge-warning {
@apply bg-warning-100 text-warning-800;
}
.badge-error {
@apply bg-error-100 text-error-800;
}
.dark .badge-primary {
@apply bg-primary-900/30 text-primary-300;
}
.dark .badge-success {
@apply bg-success-900/30 text-success-300;
}
.dark .badge-warning {
@apply bg-warning-900/30 text-warning-300;
}
.dark .badge-error {
@apply bg-error-900/30 text-error-300;
}
}
/* Utility classes */
@layer utilities {
.text-balance {
text-wrap: balance;
}
.animation-delay-75 {
animation-delay: 75ms;
}
.animation-delay-100 {
animation-delay: 100ms;
}
.animation-delay-150 {
animation-delay: 150ms;
}
.animation-delay-200 {
animation-delay: 200ms;
}
.animation-delay-300 {
animation-delay: 300ms;
}
.animation-delay-500 {
animation-delay: 500ms;
}
.animation-delay-700 {
animation-delay: 700ms;
}
.animation-delay-1000 {
animation-delay: 1000ms;
}
/* Glass morphism effect */
.glass {
@apply bg-white/80 backdrop-blur-md border border-white/20;
}
.dark .glass {
@apply bg-secondary-900/80 border-secondary-700/50;
}
/* Gradient text */
.gradient-text {
@apply bg-gradient-to-r from-primary-600 to-primary-400 bg-clip-text text-transparent;
}
/* Safe area padding for mobile */
.safe-area-top {
padding-top: env(safe-area-inset-top);
}
.safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
}

View file

@ -0,0 +1,97 @@
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'SEO Image Renamer - AI-Powered Image SEO Tool',
description: 'Transform your image SEO workflow with AI that analyzes content and generates perfect filenames automatically. No more manual renaming - just upload, enhance, and download.',
keywords: ['SEO', 'image optimization', 'AI', 'filename generator', 'image renaming', 'bulk processing'],
authors: [{ name: 'SEO Image Renamer Team' }],
creator: 'SEO Image Renamer',
publisher: 'SEO Image Renamer',
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://seo-image-renamer.com',
title: 'SEO Image Renamer - AI-Powered Image SEO Tool',
description: 'Transform your image SEO workflow with AI that analyzes content and generates perfect filenames automatically.',
siteName: 'SEO Image Renamer',
images: [
{
url: '/og-image.png',
width: 1200,
height: 630,
alt: 'SEO Image Renamer - AI-Powered Image SEO Tool',
},
],
},
twitter: {
card: 'summary_large_image',
title: 'SEO Image Renamer - AI-Powered Image SEO Tool',
description: 'Transform your image SEO workflow with AI that analyzes content and generates perfect filenames automatically.',
images: ['/og-image.png'],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
viewport: {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
},
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#ffffff' },
{ media: '(prefers-color-scheme: dark)', color: '#0f172a' },
],
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var mode = localStorage.getItem('theme');
if (mode === 'dark' || (!mode && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
} catch (e) {}
})();
`,
}}
/>
</head>
<body className={`${inter.className} antialiased`}>
<div id="root">
{children}
</div>
<div id="modal-root" />
<div id="toast-root" />
</body>
</html>
);
}

View file

@ -0,0 +1,80 @@
'use client';
import { useEffect, useState } from 'react';
import { useAuth } from '@/hooks/useAuth';
import { useWebSocket } from '@/hooks/useWebSocket';
import { Header } from '@/components/Layout/Header';
import { Footer } from '@/components/Layout/Footer';
import { HeroSection } from '@/components/Landing/HeroSection';
import { FeaturesSection } from '@/components/Landing/FeaturesSection';
import { HowItWorksSection } from '@/components/Landing/HowItWorksSection';
import { PricingSection } from '@/components/Landing/PricingSection';
import { Dashboard } from '@/components/Dashboard/Dashboard';
import { WorkflowSection } from '@/components/Workflow/WorkflowSection';
import { LoadingSpinner } from '@/components/UI/LoadingSpinner';
import { ErrorBoundary } from '@/components/UI/ErrorBoundary';
import { ToastProvider } from '@/components/UI/ToastProvider';
export default function HomePage() {
const { user, isAuthenticated, isLoading } = useAuth();
const { connect } = useWebSocket();
const [showWorkflow, setShowWorkflow] = useState(false);
// Connect WebSocket when user is authenticated
useEffect(() => {
if (isAuthenticated && user) {
connect(user.id);
}
}, [isAuthenticated, user, connect]);
// Handle workflow visibility
const handleStartWorkflow = () => {
setShowWorkflow(true);
};
const handleWorkflowComplete = () => {
setShowWorkflow(false);
};
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<LoadingSpinner size="lg" />
</div>
);
}
return (
<ErrorBoundary>
<ToastProvider>
<div className="min-h-screen bg-white dark:bg-secondary-900">
<Header />
<main>
{isAuthenticated ? (
<>
{showWorkflow ? (
<WorkflowSection
onComplete={handleWorkflowComplete}
onCancel={() => setShowWorkflow(false)}
/>
) : (
<Dashboard onStartWorkflow={handleStartWorkflow} />
)}
</>
) : (
<>
<HeroSection onStartWorkflow={handleStartWorkflow} />
<FeaturesSection />
<HowItWorksSection />
<PricingSection />
</>
)}
</main>
<Footer />
</div>
</ToastProvider>
</ErrorBoundary>
);
}