From 27db3d968f2f5346f958e52eb54c296b6b730357 Mon Sep 17 00:00:00 2001 From: DustyWalker Date: Tue, 5 Aug 2025 19:04:51 +0200 Subject: [PATCH 1/7] feat(frontend): implement Next.js frontend package foundation with complete API integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/frontend/next.config.js | 135 +++++++ packages/frontend/package.json | 92 +++++ packages/frontend/postcss.config.js | 6 + packages/frontend/src/app/globals.css | 344 +++++++++++++++++ packages/frontend/src/app/layout.tsx | 97 +++++ packages/frontend/src/app/page.tsx | 80 ++++ .../src/components/Auth/LoginButton.tsx | 80 ++++ .../frontend/src/components/Layout/Footer.tsx | 125 ++++++ .../frontend/src/components/Layout/Header.tsx | 143 +++++++ .../src/components/UI/ErrorBoundary.tsx | 114 ++++++ .../src/components/UI/LoadingSpinner.tsx | 42 ++ .../src/components/UI/ToastProvider.tsx | 211 ++++++++++ packages/frontend/src/hooks/useAuth.ts | 191 +++++++++ packages/frontend/src/hooks/useUpload.ts | 317 +++++++++++++++ packages/frontend/src/hooks/useWebSocket.ts | 297 ++++++++++++++ packages/frontend/src/lib/api-client.ts | 340 +++++++++++++++++ packages/frontend/src/types/api.ts | 361 ++++++++++++++++++ packages/frontend/src/types/index.ts | 34 ++ packages/frontend/tailwind.config.js | 142 +++++++ packages/frontend/tsconfig.json | 49 +++ 20 files changed, 3200 insertions(+) create mode 100644 packages/frontend/next.config.js create mode 100644 packages/frontend/package.json create mode 100644 packages/frontend/postcss.config.js create mode 100644 packages/frontend/src/app/globals.css create mode 100644 packages/frontend/src/app/layout.tsx create mode 100644 packages/frontend/src/app/page.tsx create mode 100644 packages/frontend/src/components/Auth/LoginButton.tsx create mode 100644 packages/frontend/src/components/Layout/Footer.tsx create mode 100644 packages/frontend/src/components/Layout/Header.tsx create mode 100644 packages/frontend/src/components/UI/ErrorBoundary.tsx create mode 100644 packages/frontend/src/components/UI/LoadingSpinner.tsx create mode 100644 packages/frontend/src/components/UI/ToastProvider.tsx create mode 100644 packages/frontend/src/hooks/useAuth.ts create mode 100644 packages/frontend/src/hooks/useUpload.ts create mode 100644 packages/frontend/src/hooks/useWebSocket.ts create mode 100644 packages/frontend/src/lib/api-client.ts create mode 100644 packages/frontend/src/types/api.ts create mode 100644 packages/frontend/src/types/index.ts create mode 100644 packages/frontend/tailwind.config.js create mode 100644 packages/frontend/tsconfig.json diff --git a/packages/frontend/next.config.js b/packages/frontend/next.config.js new file mode 100644 index 0000000..4047df0 --- /dev/null +++ b/packages/frontend/next.config.js @@ -0,0 +1,135 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', + experimental: { + appDir: true, + }, + + // Environment variables + env: { + NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001', + NEXT_PUBLIC_WS_URL: process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:3001', + NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, + NEXT_PUBLIC_GOOGLE_CLIENT_ID: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID, + }, + + // Image configuration for external sources + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'lh3.googleusercontent.com', + port: '', + pathname: '/a/**', + }, + { + protocol: 'http', + hostname: 'localhost', + port: '3001', + pathname: '/api/images/**', + }, + ], + dangerouslyAllowSVG: true, + contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", + }, + + // Headers for security + async headers() { + return [ + { + source: '/(.*)', + headers: [ + { + key: 'X-Frame-Options', + value: 'DENY', + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin', + }, + { + key: 'X-XSS-Protection', + value: '1; mode=block', + }, + ], + }, + ]; + }, + + // Rewrites for API proxy in development + async rewrites() { + if (process.env.NODE_ENV === 'development') { + return [ + { + source: '/api/:path*', + destination: `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/api/:path*`, + }, + ]; + } + return []; + }, + + // Webpack configuration + webpack: (config, { dev, isServer }) => { + // Optimization for production + if (!dev && !isServer) { + config.optimization.splitChunks.cacheGroups = { + ...config.optimization.splitChunks.cacheGroups, + vendor: { + test: /[\\/]node_modules[\\/]/, + name: 'vendors', + chunks: 'all', + priority: 10, + }, + common: { + name: 'common', + minChunks: 2, + chunks: 'all', + priority: 5, + reuseExistingChunk: true, + }, + }; + } + + return config; + }, + + // TypeScript configuration + typescript: { + ignoreBuildErrors: false, + }, + + // ESLint configuration + eslint: { + ignoreDuringBuilds: false, + }, + + // Compression and optimization + compress: true, + poweredByHeader: false, + generateEtags: true, + + // Redirects + async redirects() { + return [ + { + source: '/dashboard', + destination: '/', + permanent: false, + has: [ + { + type: 'cookie', + key: 'authenticated', + value: undefined, + }, + ], + }, + ]; + }, +}; + +module.exports = nextConfig; \ No newline at end of file diff --git a/packages/frontend/package.json b/packages/frontend/package.json new file mode 100644 index 0000000..6572364 --- /dev/null +++ b/packages/frontend/package.json @@ -0,0 +1,92 @@ +{ + "name": "@seo-image-renamer/frontend", + "version": "1.0.0", + "description": "Next.js frontend for SEO Image Renamer with complete backend integration", + "private": true, + "scripts": { + "dev": "next dev -p 3000", + "build": "next build", + "start": "next start -p 3000", + "lint": "next lint", + "type-check": "tsc --noEmit", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "dependencies": { + "next": "^14.0.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "@types/node": "^20.10.5", + "@types/react": "^18.2.45", + "@types/react-dom": "^18.2.18", + "typescript": "^5.3.3", + "tailwindcss": "^3.3.6", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "@tailwindcss/forms": "^0.5.7", + "@tailwindcss/typography": "^0.5.10", + "@headlessui/react": "^1.7.17", + "@heroicons/react": "^2.0.18", + "socket.io-client": "^4.7.4", + "axios": "^1.6.2", + "@stripe/stripe-js": "^2.4.0", + "react-dropzone": "^14.2.3", + "react-hook-form": "^7.48.2", + "react-hot-toast": "^2.4.1", + "clsx": "^2.0.0", + "class-variance-authority": "^0.7.0", + "lucide-react": "^0.298.0", + "next-themes": "^0.2.1", + "zustand": "^4.4.7", + "jszip": "^3.10.1", + "file-saver": "^2.0.5", + "@hookform/resolvers": "^3.3.2", + "zod": "^3.22.4", + "react-query": "^3.39.3", + "framer-motion": "^10.16.16" + }, + "devDependencies": { + "@types/file-saver": "^2.0.7", + "@types/jszip": "^3.4.1", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "eslint": "^8.55.0", + "eslint-config-next": "^14.0.4", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "@testing-library/react": "^14.1.2", + "@testing-library/jest-dom": "^6.1.5", + "@testing-library/user-event": "^14.5.1", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "@storybook/addon-essentials": "^7.6.6", + "@storybook/addon-interactions": "^7.6.6", + "@storybook/addon-links": "^7.6.6", + "@storybook/blocks": "^7.6.6", + "@storybook/nextjs": "^7.6.6", + "@storybook/react": "^7.6.6", + "@storybook/testing-library": "^0.2.2", + "storybook": "^7.6.6", + "prettier": "^3.1.1", + "prettier-plugin-tailwindcss": "^0.5.9" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} \ No newline at end of file diff --git a/packages/frontend/postcss.config.js b/packages/frontend/postcss.config.js new file mode 100644 index 0000000..8567b4c --- /dev/null +++ b/packages/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; \ No newline at end of file diff --git a/packages/frontend/src/app/globals.css b/packages/frontend/src/app/globals.css new file mode 100644 index 0000000..98a81cf --- /dev/null +++ b/packages/frontend/src/app/globals.css @@ -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); + } +} \ No newline at end of file diff --git a/packages/frontend/src/app/layout.tsx b/packages/frontend/src/app/layout.tsx new file mode 100644 index 0000000..c7fe967 --- /dev/null +++ b/packages/frontend/src/app/layout.tsx @@ -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 ( + + + + + + + + + +