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:
parent
b198bfe3cf
commit
27db3d968f
20 changed files with 3200 additions and 0 deletions
344
packages/frontend/src/app/globals.css
Normal file
344
packages/frontend/src/app/globals.css
Normal 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);
|
||||
}
|
||||
}
|
97
packages/frontend/src/app/layout.tsx
Normal file
97
packages/frontend/src/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
80
packages/frontend/src/app/page.tsx
Normal file
80
packages/frontend/src/app/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue