├── .nvmrc ├── meme-bot ├── .gitignore ├── requirements.txt ├── fonts │ └── DejaVuSans-Bold.ttf ├── test_generator.py ├── batch_meme_generator.py ├── dynamic_meme_generator.py └── app.py ├── public └── icons8-doge-16.png ├── favicon └── icons8-doge-16.png ├── postcss.config.js ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── tailwind.config.js ├── .env.example ├── .gitignore ├── src ├── main.jsx ├── components │ ├── MemeSearch.jsx │ ├── Toast.jsx │ ├── NewMeme.jsx │ ├── __tests__ │ │ └── ExportFlow.integration.test.jsx │ ├── About.jsx │ ├── Dynamicmeme.css │ ├── Footer.jsx │ ├── FormatSelector.jsx │ ├── QualitySlider.jsx │ ├── ExportButton.jsx │ ├── ExportOptionsPanel.css │ ├── ErrorBoundary.jsx │ ├── Home.jsx │ └── LoadingOverlay.jsx ├── utils │ ├── exportUtils.js │ ├── simpleMemeCanvas.js │ ├── debugMeme.js │ ├── socialShare.js │ ├── test-canvas-meme.html │ ├── memeCanvas.js │ ├── test-export-panel.html │ ├── __tests__ │ │ └── CanvasConverter.unit.test.js │ └── test-export-panel-standalone.html ├── App.jsx ├── ThemeContext.jsx ├── contexts │ └── ToastContext.jsx ├── Temp.jsx ├── index.css ├── test │ └── setup.js ├── memesMeta.js └── style.css ├── .gitpod.yml ├── .eslintrc.cjs ├── vercel.json ├── .eslintrc.js ├── vite.config.js ├── index.html ├── SETUP.md ├── package.json ├── api └── caption.js ├── dev-server.js ├── DEPLOYMENT.md ├── README.md └── CONTRIBUTING.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /meme-bot/.gitignore: -------------------------------------------------------------------------------- 1 | memes/ 2 | __pycache__/ 3 | logs/ 4 | venv/ 5 | *.pyc 6 | .env -------------------------------------------------------------------------------- /meme-bot/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dotenv 2 | groq 3 | pillow 4 | requests 5 | flask 6 | flask-cors -------------------------------------------------------------------------------- /public/icons8-doge-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avinash201199/MemeGenerator/HEAD/public/icons8-doge-16.png -------------------------------------------------------------------------------- /favicon/icons8-doge-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avinash201199/MemeGenerator/HEAD/favicon/icons8-doge-16.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /meme-bot/fonts/DejaVuSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avinash201199/MemeGenerator/HEAD/meme-bot/fonts/DejaVuSans-Bold.ttf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [avinash201199] 4 | 5 | custom: ["https://paypal.me/Avinash425"] 6 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Imgflip API Credentials 2 | # Get these from https://imgflip.com/signup (free account) 3 | # Then go to https://imgflip.com/api to get your credentials 4 | IMGFLIP_USERNAME=your_imgflip_username_here 5 | IMGFLIP_PASSWORD=your_imgflip_password_here 6 | 7 | # Optional: Backend API URL for AI meme generator 8 | VITE_API_BASE_URL=http://localhost:5000 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import { BrowserRouter as Router} from "react-router-dom"; 5 | import { ToastProvider } from "./contexts/ToastContext"; 6 | 7 | ReactDOM.createRoot(document.getElementById("root")).render( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was automatically generated by Gitpod. 2 | # Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) 3 | # and commit this file to your remote git repository to share the goodness with others. 4 | 5 | # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart 6 | 7 | tasks: 8 | - init: npm install && npm run build 9 | command: npm run dev 10 | 11 | 12 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ master, main, embedded-feature ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Use Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: '22' 19 | cache: 'npm' 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Lint (non-blocking) 25 | run: | 26 | npm run lint || echo "Lint warnings/errors ignored for CI pass" 27 | 28 | - name: Build 29 | run: npm run build 30 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "api/caption.js": { 4 | "runtime": "nodejs18.x" 5 | } 6 | }, 7 | "rewrites": [ 8 | { 9 | "source": "/api/(.*)", 10 | "destination": "/api/$1" 11 | } 12 | ], 13 | "headers": [ 14 | { 15 | "source": "/api/(.*)", 16 | "headers": [ 17 | { 18 | "key": "Access-Control-Allow-Origin", 19 | "value": "*" 20 | }, 21 | { 22 | "key": "Access-Control-Allow-Methods", 23 | "value": "GET, POST, PUT, DELETE, OPTIONS" 24 | }, 25 | { 26 | "key": "Access-Control-Allow-Headers", 27 | "value": "Content-Type, Authorization" 28 | } 29 | ] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2020: true, 6 | node: true 7 | }, 8 | extends: [ 9 | 'eslint:recommended', 10 | '@typescript-eslint/recommended', 11 | 'plugin:react-hooks/recommended', 12 | ], 13 | ignorePatterns: ['dist', '.eslintrc.cjs'], 14 | parser: '@typescript-eslint/parser', 15 | plugins: ['react-refresh'], 16 | rules: { 17 | 'react-refresh/only-export-components': [ 18 | 'warn', 19 | { allowConstantExport: true }, 20 | ], 21 | 'no-undef': 'off', // Allow process, global, etc. 22 | 'no-unused-vars': 'warn', // Make unused vars warnings instead of errors 23 | 'react/prop-types': 'off', // Disable prop-types validation 24 | }, 25 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | test: { 8 | environment: 'jsdom', 9 | globals: true, 10 | setupFiles: ['./src/test/setup.js'] 11 | }, 12 | server: { 13 | proxy: { 14 | '/api': { 15 | target: 'http://localhost:3001', 16 | changeOrigin: true, 17 | configure: (proxy, options) => { 18 | // Fallback for development - direct Imgflip API call 19 | proxy.on('error', (err, req, res) => { 20 | console.log('API proxy error, using direct Imgflip call'); 21 | }); 22 | } 23 | } 24 | } 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /meme-bot/test_generator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Simple test script for the meme generator 5 | """ 6 | 7 | try: 8 | print("Starting test...") 9 | from meme_generator_clean import MemeGeneratorClean 10 | print("Import successful") 11 | 12 | generator = MemeGeneratorClean() 13 | print("Generator initialized successfully!") 14 | 15 | print("Available categories:", generator.get_topic_categories()) 16 | 17 | print("Testing text generation...") 18 | text = generator.generate_meme_text("Gaming addiction") 19 | print(f"Generated text: {text}") 20 | 21 | print("Test completed successfully!") 22 | 23 | except Exception as e: 24 | print(f"Error during test: {e}") 25 | import traceback 26 | traceback.print_exc() -------------------------------------------------------------------------------- /src/components/MemeSearch.jsx: -------------------------------------------------------------------------------- 1 | // MemeSearch.js 2 | import React from "react"; 3 | import "../style.css"; 4 | import "../index.css"; 5 | 6 | const MemeSearch = ({ searchQuery, setSearchQuery }) => { 7 | const handleSearchChange = (event) => { 8 | // Update the search query state in the parent component 9 | setSearchQuery(event.target.value); 10 | }; 11 | 12 | return ( 13 |
14 | 21 |
22 | ); 23 | }; 24 | 25 | export default MemeSearch; 26 | -------------------------------------------------------------------------------- /src/utils/exportUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Export Utilities Index 3 | * Centralized exports for all export-related utilities 4 | */ 5 | 6 | import CanvasConverter from './CanvasConverter.js'; 7 | import ExportProcessor from './ExportProcessor.js'; 8 | import BrowserCompatibility from './BrowserCompatibility.js'; 9 | 10 | // Export individual classes 11 | export { CanvasConverter, ExportProcessor, BrowserCompatibility }; 12 | 13 | // Export as default object for convenience 14 | export default { 15 | CanvasConverter, 16 | ExportProcessor, 17 | BrowserCompatibility 18 | }; 19 | 20 | // Convenience functions for common operations 21 | export const convertImageToCanvas = CanvasConverter.imageToCanvas; 22 | export const convertUrlToCanvas = CanvasConverter.urlToCanvas; 23 | export const exportMeme = ExportProcessor.exportMeme; 24 | export const checkWebPSupport = BrowserCompatibility.checkWebPSupport; 25 | export const getCompatibilityReport = BrowserCompatibility.getCompatibilityReport; -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | // App.jsx 3 | import React from "react"; 4 | import { Route, Routes } from "react-router-dom"; 5 | import { ThemeProvider } from "./ThemeContext"; 6 | import Home from "./components/Home"; 7 | import "./style.css"; 8 | import About from "./components/About"; 9 | import History from "./components/History"; 10 | import Dynamicmeme from "./components/Dynamicmeme"; 11 | import NewMeme from "./components/NewMeme"; 12 | 13 | const App = () => { 14 | return ( 15 | 16 | 17 | } /> 18 | } /> 19 | } /> 20 | } /> 21 | } /> 22 | 23 | {/* Define other routes here */} 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default App; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Meme Generator 9 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 40 | 41 | -------------------------------------------------------------------------------- /src/ThemeContext.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState, useEffect } from 'react'; 2 | 3 | const ThemeContext = createContext(); 4 | 5 | export const useTheme = () => { 6 | const context = useContext(ThemeContext); 7 | if (!context) { 8 | throw new Error('useTheme must be used within a ThemeProvider'); 9 | } 10 | return context; 11 | }; 12 | 13 | export const ThemeProvider = ({ children }) => { 14 | const [isDarkTheme, setIsDarkTheme] = useState(true); 15 | 16 | useEffect(() => { 17 | const savedTheme = localStorage.getItem('theme'); 18 | const prefersDark = savedTheme ? savedTheme === 'dark' : true; 19 | setIsDarkTheme(prefersDark); 20 | }, []); 21 | 22 | useEffect(() => { 23 | if (isDarkTheme) { 24 | document.body.classList.add('dark-theme'); 25 | document.body.classList.remove('light-theme'); 26 | } else { 27 | document.body.classList.add('light-theme'); 28 | document.body.classList.remove('dark-theme'); 29 | } 30 | localStorage.setItem('theme', isDarkTheme ? 'dark' : 'light'); 31 | }, [isDarkTheme]); // Re-run this effect whenever isDarkTheme changes 32 | 33 | const toggleTheme = () => { 34 | setIsDarkTheme(prev => !prev); 35 | }; 36 | 37 | const value = { 38 | isDarkTheme, 39 | toggleTheme, 40 | }; 41 | 42 | return ( 43 | 44 | {children} 45 | 46 | ); 47 | }; -------------------------------------------------------------------------------- /SETUP.md: -------------------------------------------------------------------------------- 1 | # Meme Generator Setup Guide 2 | 3 | ## ✅ Current Status: WORKING IN DEMO MODE 4 | 5 | The meme generator is now running with demo mode enabled! You can test it immediately. 6 | 7 | ## 🚀 Quick Start 8 | 9 | 1. **Run the project:** 10 | ```bash 11 | npm run dev:full 12 | ``` 13 | 14 | 2. **Open your browser:** http://localhost:5173/ 15 | 16 | 3. **Test it:** Select a meme template, add captions, and click "Generate Meme" 17 | 18 | ## 🎯 Demo Mode vs Real Mode 19 | 20 | ### Demo Mode (Current) 21 | - ✅ Works immediately without setup 22 | - ✅ Shows placeholder images with your text 23 | - ✅ Perfect for testing the interface 24 | - ⚠️ Uses placeholder images instead of real memes 25 | 26 | ### Real Mode (Optional) 27 | To generate actual memes with Imgflip: 28 | 29 | 1. **Get Imgflip Credentials (Free):** 30 | - Sign up at [imgflip.com/signup](https://imgflip.com/signup) 31 | - Your username and password are your API credentials 32 | 33 | 2. **Update `.env.local`:** 34 | ``` 35 | IMGFLIP_USERNAME=your_actual_username 36 | IMGFLIP_PASSWORD=your_actual_password 37 | ``` 38 | 39 | 3. **Restart the server:** 40 | ```bash 41 | npm run dev:full 42 | ``` 43 | 44 | ## 🛠 Development Commands 45 | 46 | - `npm run dev` - Frontend only 47 | - `npm run dev:api` - API server only 48 | - `npm run dev:full` - Both servers (recommended) 49 | 50 | ## 🎉 What's Working 51 | 52 | - ✅ Frontend running on http://localhost:5173/ 53 | - ✅ API server running on http://localhost:3001/ 54 | - ✅ Demo mode for immediate testing 55 | - ✅ Real Imgflip integration ready when you add credentials -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "engines": { 7 | "node": "22.x" 8 | }, 9 | "scripts": { 10 | "dev": "vite", 11 | "dev:api": "node dev-server.js", 12 | "dev:full": "concurrently \"npm run dev:api\" \"npm run dev\"", 13 | "build": "vite build", 14 | "build:prod": "vite build --mode production", 15 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 16 | "lint:fix": "eslint . --ext js,jsx --fix", 17 | "preview": "vite preview", 18 | "test": "vitest --run", 19 | "test:watch": "vitest", 20 | "test:ui": "vitest --ui" 21 | }, 22 | "dependencies": { 23 | "gsap": "^3.12.2", 24 | "react": "^18.2.0", 25 | "react-dom": "^18.2.0", 26 | "react-icons": "^5.5.0", 27 | "react-router-dom": "^6.17.0" 28 | }, 29 | "devDependencies": { 30 | "@testing-library/jest-dom": "^6.9.1", 31 | "@testing-library/react": "^16.3.0", 32 | "@testing-library/user-event": "^14.6.1", 33 | "@types/react": "^18.2.15", 34 | "@types/react-dom": "^18.2.7", 35 | "@vitejs/plugin-react": "^4.0.3", 36 | "@vitest/ui": "^4.0.3", 37 | "autoprefixer": "^10.4.16", 38 | "concurrently": "^9.2.1", 39 | "cors": "^2.8.5", 40 | "dotenv": "^17.2.3", 41 | "eslint": "^8.45.0", 42 | "eslint-plugin-react": "^7.32.2", 43 | "eslint-plugin-react-hooks": "^4.6.0", 44 | "eslint-plugin-react-refresh": "^0.4.3", 45 | "express": "^5.1.0", 46 | "jsdom": "^27.0.1", 47 | "node-fetch": "^3.3.2", 48 | "postcss": "^8.4.31", 49 | "tailwindcss": "^3.3.5", 50 | "vite": "^4.5.14", 51 | "vitest": "^4.0.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Toast.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * Toast Notification Component 6 | * Displays temporary notification messages with different variants 7 | */ 8 | const Toast = ({ message, type = 'info', duration = 3000, onClose }) => { 9 | useEffect(() => { 10 | if (duration > 0) { 11 | const timer = setTimeout(() => { 12 | onClose(); 13 | }, duration); 14 | 15 | return () => clearTimeout(timer); 16 | } 17 | }, [duration, onClose]); 18 | 19 | const getToastStyles = () => { 20 | const baseStyles = "fixed bottom-4 right-4 z-50 px-6 py-4 rounded-lg shadow-2xl flex items-center gap-3 animate-slide-in-right max-w-md"; 21 | 22 | const variants = { 23 | success: "bg-green-600 text-white border-l-4 border-green-400", 24 | error: "bg-red-600 text-white border-l-4 border-red-400", 25 | warning: "bg-yellow-600 text-white border-l-4 border-yellow-400", 26 | info: "bg-blue-600 text-white border-l-4 border-blue-400", 27 | }; 28 | 29 | return `${baseStyles} ${variants[type] || variants.info}`; 30 | }; 31 | 32 | const getIcon = () => { 33 | const icons = { 34 | success: "✅", 35 | error: "❌", 36 | warning: "⚠️", 37 | info: "ℹ️", 38 | }; 39 | return icons[type] || icons.info; 40 | }; 41 | 42 | return ( 43 |
44 | {getIcon()} 45 |

{message}

46 | 53 |
54 | ); 55 | }; 56 | 57 | Toast.propTypes = { 58 | message: PropTypes.string.isRequired, 59 | type: PropTypes.oneOf(['success', 'error', 'warning', 'info']), 60 | duration: PropTypes.number, 61 | onClose: PropTypes.func.isRequired, 62 | }; 63 | 64 | export default Toast; 65 | 66 | -------------------------------------------------------------------------------- /api/caption.js: -------------------------------------------------------------------------------- 1 | // Vercel Serverless Function: Proxy Imgflip caption_image securely 2 | // Expects POST JSON: { template_id: string, boxes: Array<{ text: string }>, username?: string, password?: string } 3 | // Uses env vars IMGFLIP_USERNAME and IMGFLIP_PASSWORD by default. 4 | 5 | export default async function handler(req, res) { 6 | if (req.method !== 'POST') { 7 | return res.status(405).json({ success: false, error: 'Method not allowed' }); 8 | } 9 | 10 | try { 11 | const { template_id, boxes = [], username, password } = req.body || {}; 12 | 13 | if (!template_id) { 14 | return res.status(400).json({ success: false, error: 'template_id is required' }); 15 | } 16 | 17 | const user = username || process.env.IMGFLIP_USERNAME; 18 | const pass = password || process.env.IMGFLIP_PASSWORD; 19 | 20 | if (!user || !pass) { 21 | return res.status(500).json({ success: false, error: 'Server not configured: missing IMGFLIP credentials' }); 22 | } 23 | 24 | const params = new URLSearchParams(); 25 | params.append('template_id', String(template_id)); 26 | params.append('username', user); 27 | params.append('password', pass); 28 | 29 | (boxes || []).forEach((box, i) => { 30 | const val = box && typeof box.text === 'string' ? box.text : ''; 31 | params.append(`boxes[${i}][text]`, val); 32 | }); 33 | 34 | const resp = await fetch('https://api.imgflip.com/caption_image', { 35 | method: 'POST', 36 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 37 | body: params.toString(), 38 | }); 39 | 40 | const data = await resp.json(); 41 | 42 | // Normalize response 43 | if (data && data.success) { 44 | return res.status(200).json({ success: true, data: data.data }); 45 | } 46 | 47 | return res.status(400).json({ success: false, error: data?.error_message || 'Imgflip error' }); 48 | } catch (err) { 49 | console.error('caption proxy error:', err); 50 | return res.status(500).json({ success: false, error: 'Internal server error' }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/contexts/ToastContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState, useCallback } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Toast from '../components/Toast'; 4 | 5 | const ToastContext = createContext(); 6 | 7 | export const useToast = () => { 8 | const context = useContext(ToastContext); 9 | if (!context) { 10 | throw new Error('useToast must be used within a ToastProvider'); 11 | } 12 | return context; 13 | }; 14 | 15 | export const ToastProvider = ({ children }) => { 16 | const [toasts, setToasts] = useState([]); 17 | 18 | const addToast = useCallback((message, type = 'info', duration = 3000) => { 19 | const id = Date.now() + Math.random(); 20 | const newToast = { id, message, type, duration }; 21 | 22 | setToasts((prev) => [...prev, newToast]); 23 | 24 | return id; 25 | }, []); 26 | 27 | const removeToast = useCallback((id) => { 28 | setToasts((prev) => prev.filter((toast) => toast.id !== id)); 29 | }, []); 30 | 31 | // Convenience methods for different toast types 32 | const toast = { 33 | success: (message, duration) => addToast(message, 'success', duration), 34 | error: (message, duration) => addToast(message, 'error', duration), 35 | warning: (message, duration) => addToast(message, 'warning', duration), 36 | info: (message, duration) => addToast(message, 'info', duration), 37 | }; 38 | 39 | return ( 40 | 41 | {children} 42 |
43 | {toasts.map((toast, index) => ( 44 |
53 | removeToast(toast.id)} 58 | /> 59 |
60 | ))} 61 |
62 |
63 | ); 64 | }; 65 | 66 | ToastProvider.propTypes = { 67 | children: PropTypes.node.isRequired, 68 | }; 69 | 70 | export default ToastContext; 71 | 72 | -------------------------------------------------------------------------------- /src/components/NewMeme.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React, { useState } from 'react'; 3 | 4 | export default function NewMeme() { 5 | const [templateId, setTemplateId] = useState(''); 6 | const [topText, setTopText] = useState(''); 7 | const [bottomText, setBottomText] = useState(''); 8 | const [imageURL, setImageURL] = useState(''); 9 | const [loading, setLoading] = useState(false); 10 | 11 | const handleGenerate = async (e) => { 12 | e.preventDefault(); 13 | setLoading(true); 14 | try { 15 | const res = await fetch('/api/caption', { 16 | method: 'POST', 17 | headers: { 'Content-Type': 'application/json' }, 18 | body: JSON.stringify({ 19 | template_id: templateId, 20 | boxes: [ 21 | { text: topText }, 22 | { text: bottomText } 23 | ] 24 | }) 25 | }); 26 | const data = await res.json(); 27 | setImageURL(data.url); 28 | } catch (error) { 29 | alert('Failed to generate meme!'); 30 | } finally { 31 | setLoading(false); 32 | } 33 | }; 34 | 35 | return ( 36 |
37 |

Create a New Meme

38 |
39 | setTemplateId(e.target.value)} /> 41 | setTopText(e.target.value)} /> 43 | setBottomText(e.target.value)} /> 45 | 52 |
53 | 54 | {imageURL && ( 55 |
56 |

Generated Meme:

57 | Meme 58 |
59 | )} 60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/Temp.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import React, { useRef, useLayoutEffect } from "react"; 3 | import gsap from "gsap"; 4 | import memesMeta from "./memesMeta"; 5 | 6 | const Temp = ({ temp, setMeme }) => { 7 | const row1 = useRef(null); 8 | const row2 = useRef(null); 9 | const row3 = useRef(null); 10 | 11 | useLayoutEffect(() => { 12 | const ctx1 = gsap.context(() => { 13 | animateRow(row1.current, "right"); 14 | }, row1.current); 15 | 16 | const ctx2 = gsap.context(() => { 17 | animateRow(row2.current, "left"); 18 | }, row2.current); 19 | 20 | const ctx3 = gsap.context(() => { 21 | animateRow(row3.current, "bottom"); 22 | }, row3.current); 23 | 24 | return () => { 25 | ctx1.revert(); 26 | ctx2.revert(); 27 | ctx3.revert(); 28 | }; 29 | }, [temp]); 30 | 31 | const animateRow = (rowRef, direction) => { 32 | gsap.from(rowRef.children, { 33 | opacity: 0, 34 | duration: 1, 35 | ease: "power4.out", 36 | stagger: { 37 | amount: 0.2, 38 | each: 0.1, 39 | }, 40 | x: direction === "right" ? 100 : direction === "left" ? -100 : 0, 41 | y: direction === "bottom" ? 100 : 0, 42 | }); 43 | }; 44 | 45 | const renderTemplate = (temps) => ( 46 |
setMeme(temps)} 51 | // 1. Add aria-label to the clickable container 52 | aria-label={`Select meme template: ${temps.name}`} 53 | role="button" // Indicates it is an interactive element 54 | > 55 |
62 | 63 | {/* Caption overlay */} 64 |
65 | {memesMeta[temps.name]?.captions?.join(", ")} 66 |
67 |
68 | ); 69 | 70 | return ( 71 |
72 |
73 | {temp.slice(0, 3).map(renderTemplate)} 74 |
75 | 76 |
77 | {temp.slice(3, 6).map(renderTemplate)} 78 |
79 | 80 |
81 | {temp.slice(6, 9).map(renderTemplate)} 82 |
83 |
84 | ); 85 | }; 86 | 87 | export default Temp; 88 | -------------------------------------------------------------------------------- /src/utils/simpleMemeCanvas.js: -------------------------------------------------------------------------------- 1 | // Simple and reliable meme canvas generator 2 | export const generateSimpleMeme = async (imageUrl, texts) => { 3 | return new Promise((resolve, reject) => { 4 | const canvas = document.createElement('canvas'); 5 | const ctx = canvas.getContext('2d'); 6 | const img = new Image(); 7 | 8 | // Don't set crossOrigin for blob URLs or same-origin images 9 | if (imageUrl.startsWith('https://') && !imageUrl.startsWith(window.location.origin)) { 10 | img.crossOrigin = 'anonymous'; 11 | } 12 | 13 | img.onload = () => { 14 | try { 15 | // Set canvas size 16 | canvas.width = img.width; 17 | canvas.height = img.height; 18 | 19 | // Draw the image 20 | ctx.drawImage(img, 0, 0); 21 | 22 | // Calculate font size based on image size 23 | const fontSize = Math.max(24, Math.min(img.width / 12, img.height / 12)); 24 | 25 | // Set text properties 26 | ctx.font = `bold ${fontSize}px Arial, sans-serif`; 27 | ctx.textAlign = 'center'; 28 | ctx.textBaseline = 'middle'; 29 | ctx.fillStyle = '#FFFFFF'; 30 | ctx.strokeStyle = '#000000'; 31 | ctx.lineWidth = Math.max(2, fontSize / 12); 32 | 33 | // Draw top text 34 | if (texts[0] && texts[0].trim()) { 35 | const topText = texts[0].toUpperCase(); 36 | const topY = fontSize + 10; 37 | 38 | // Draw outline 39 | ctx.strokeText(topText, canvas.width / 2, topY); 40 | // Draw fill 41 | ctx.fillText(topText, canvas.width / 2, topY); 42 | } 43 | 44 | // Draw bottom text 45 | if (texts[1] && texts[1].trim()) { 46 | const bottomText = texts[1].toUpperCase(); 47 | const bottomY = canvas.height - fontSize - 10; 48 | 49 | // Draw outline 50 | ctx.strokeText(bottomText, canvas.width / 2, bottomY); 51 | // Draw fill 52 | ctx.fillText(bottomText, canvas.width / 2, bottomY); 53 | } 54 | 55 | // Convert to blob 56 | canvas.toBlob((blob) => { 57 | if (blob) { 58 | const url = URL.createObjectURL(blob); 59 | resolve(url); 60 | } else { 61 | reject(new Error('Failed to create blob')); 62 | } 63 | }, 'image/png', 1.0); 64 | 65 | } catch (error) { 66 | reject(error); 67 | } 68 | }; 69 | 70 | img.onerror = () => { 71 | reject(new Error('Failed to load image')); 72 | }; 73 | 74 | // Set timeout 75 | setTimeout(() => { 76 | reject(new Error('Image loading timeout')); 77 | }, 5000); 78 | 79 | img.src = imageUrl; 80 | }); 81 | }; -------------------------------------------------------------------------------- /dev-server.js: -------------------------------------------------------------------------------- 1 | // Development server to handle API calls locally 2 | import express from 'express'; 3 | import cors from 'cors'; 4 | import fetch from 'node-fetch'; 5 | import dotenv from 'dotenv'; 6 | 7 | // Load environment variables 8 | dotenv.config({ path: '.env.local' }); 9 | 10 | const app = express(); 11 | const PORT = 3001; 12 | 13 | app.use(cors()); 14 | app.use(express.json()); 15 | 16 | // Caption API endpoint - same logic as api/caption.js 17 | app.post('/api/caption', async (req, res) => { 18 | try { 19 | const { template_id, boxes = [], username, password } = req.body || {}; 20 | 21 | if (!template_id) { 22 | return res.status(400).json({ success: false, error: 'template_id is required' }); 23 | } 24 | 25 | const user = username || process.env.IMGFLIP_USERNAME; 26 | const pass = password || process.env.IMGFLIP_PASSWORD; 27 | 28 | // Demo mode - if no credentials or demo credentials, return a mock response 29 | if (!user || !pass || user === 'demo_user' || user === 'your_imgflip_username_here') { 30 | console.log('Demo mode: returning mock meme response'); 31 | 32 | // Create a demo response with a placeholder image that shows the text 33 | const demoText = boxes.map(box => box.text || '').join(' / '); 34 | const encodedText = encodeURIComponent(`Demo Meme: ${demoText}`); 35 | 36 | return res.status(200).json({ 37 | success: true, 38 | data: { 39 | url: `https://via.placeholder.com/400x400/FF6B6B/FFFFFF?text=${encodedText}`, 40 | page_url: 'https://imgflip.com/demo' 41 | } 42 | }); 43 | } 44 | 45 | const params = new URLSearchParams(); 46 | params.append('template_id', String(template_id)); 47 | params.append('username', user); 48 | params.append('password', pass); 49 | 50 | (boxes || []).forEach((box, i) => { 51 | const val = box && typeof box.text === 'string' ? box.text : ''; 52 | params.append(`boxes[${i}][text]`, val); 53 | }); 54 | 55 | const resp = await fetch('https://api.imgflip.com/caption_image', { 56 | method: 'POST', 57 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 58 | body: params.toString(), 59 | }); 60 | 61 | const data = await resp.json(); 62 | 63 | // Normalize response 64 | if (data && data.success) { 65 | return res.status(200).json({ success: true, data: data.data }); 66 | } 67 | 68 | return res.status(400).json({ success: false, error: data?.error_message || 'Imgflip error' }); 69 | } catch (err) { 70 | console.error('caption proxy error:', err); 71 | return res.status(500).json({ success: false, error: 'Internal server error' }); 72 | } 73 | }); 74 | 75 | app.listen(PORT, () => { 76 | console.log(`Development API server running on http://localhost:${PORT}`); 77 | console.log('Make sure your .env.local file has IMGFLIP_USERNAME and IMGFLIP_PASSWORD set'); 78 | }); -------------------------------------------------------------------------------- /DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # Vercel Deployment Guide 2 | 3 | ## ✅ Vercel Deployment Ready! 4 | 5 | This MemeGenerator project is fully configured for Vercel deployment. 6 | 7 | ### 🚀 Quick Deploy Steps: 8 | 9 | 1. **Push to GitHub**: 10 | ```bash 11 | git add . 12 | git commit -m "Ready for Vercel deployment" 13 | git push origin main 14 | ``` 15 | 16 | 2. **Deploy on Vercel**: 17 | - Go to [vercel.com](https://vercel.com) 18 | - Import your GitHub repository 19 | - Vercel will auto-detect it as a Vite project 20 | 21 | 3. **Set Environment Variables** (in Vercel Dashboard): 22 | ``` 23 | IMGFLIP_USERNAME=your_imgflip_username 24 | IMGFLIP_PASSWORD=your_imgflip_password 25 | ``` 26 | 27 | ### 🎯 What's Included: 28 | 29 | #### ✅ Frontend (Vite + React): 30 | - **Build Command**: `npm run build` 31 | - **Output Directory**: `dist` 32 | - **Node Version**: 22.x (specified in package.json) 33 | 34 | #### ✅ API Functions: 35 | - **Serverless Function**: `/api/caption.js` 36 | - **Runtime**: Node.js 18.x 37 | - **CORS Headers**: Configured for cross-origin requests 38 | 39 | #### ✅ Canvas Generation: 40 | - **Client-side**: Works in browser without server dependencies 41 | - **Fallback**: API integration with Imgflip 42 | - **No External Dependencies**: All canvas utilities are self-contained 43 | 44 | ### 🔧 Vercel Configuration: 45 | 46 | The `vercel.json` file includes: 47 | - **Function runtime** specification 48 | - **API rewrites** for proper routing 49 | - **CORS headers** for API endpoints 50 | 51 | ### 🎨 Features That Work on Vercel: 52 | 53 | 1. **Meme Templates**: Fetched from Imgflip API 54 | 2. **Canvas Generation**: Client-side text overlay 55 | 3. **API Fallback**: Server-side meme generation 56 | 4. **Social Sharing**: All sharing features 57 | 5. **Download**: Meme download functionality 58 | 6. **History**: Local storage meme history 59 | 60 | ### 🛠 Build Process: 61 | 62 | Vercel will automatically: 63 | 1. **Install dependencies**: `npm install` 64 | 2. **Build frontend**: `npm run build` 65 | 3. **Deploy API functions**: Auto-deploy `/api/caption.js` 66 | 4. **Serve static files**: From `dist` folder 67 | 68 | ### 🌐 Post-Deployment: 69 | 70 | After deployment, your app will have: 71 | - **Frontend**: `https://your-app.vercel.app` 72 | - **API Endpoint**: `https://your-app.vercel.app/api/caption` 73 | - **Canvas Generation**: Works immediately 74 | - **Imgflip Integration**: Works with environment variables 75 | 76 | ### 🔍 Troubleshooting: 77 | 78 | If deployment fails: 79 | 1. **Check build logs** in Vercel dashboard 80 | 2. **Verify environment variables** are set 81 | 3. **Test API endpoint** manually 82 | 4. **Check function logs** for errors 83 | 84 | ### 📊 Performance: 85 | 86 | - **Frontend**: Static files served via Vercel CDN 87 | - **API**: Serverless functions with global edge network 88 | - **Canvas**: Client-side processing (no server load) 89 | - **Images**: Cached and optimized by Vercel 90 | 91 | ## 🎉 Ready to Deploy! 92 | 93 | Your MemeGenerator is production-ready for Vercel deployment! -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* =========================================================== 6 | ✨ Dynamic Meme Generator – Custom Styles & Animations 7 | Author: Divjot Singh Arora 8 | =========================================================== */ 9 | 10 | /* ---------- 🎞️ Animations ---------- */ 11 | @keyframes spin { 12 | 0% { transform: rotate(0deg); } 13 | 100% { transform: rotate(360deg); } 14 | } 15 | 16 | @keyframes slide-in-right { 17 | from { 18 | transform: translateX(100%); 19 | opacity: 0; 20 | } 21 | to { 22 | transform: translateX(0); 23 | opacity: 1; 24 | } 25 | } 26 | 27 | @keyframes fade-in { 28 | from { 29 | opacity: 0; 30 | } 31 | to { 32 | opacity: 1; 33 | } 34 | } 35 | 36 | .animate-spin-slow { 37 | animation: spin 2s linear infinite; 38 | } 39 | 40 | .animate-slide-in-right { 41 | animation: slide-in-right 0.3s ease-out; 42 | } 43 | 44 | .animate-fade-in { 45 | animation: fade-in 0.3s ease-in; 46 | } 47 | 48 | /* ---------- 📱 Responsive Layouts ---------- */ 49 | @media (max-width: 768px) { 50 | .dynamic-meme-main-content { 51 | grid-template-columns: 1fr !important; 52 | } 53 | 54 | .dynamic-meme-header h1 { 55 | font-size: 2rem !important; 56 | } 57 | 58 | .dynamic-meme-category-grid { 59 | grid-template-columns: 1fr !important; 60 | } 61 | 62 | .dynamic-meme-btn-group { 63 | flex-direction: column !important; 64 | gap: 0.75rem !important; 65 | } 66 | } 67 | 68 | /* ---------- ✋ Hover Effects ---------- */ 69 | .dynamic-meme-category-btn:hover { 70 | border-color: #667eea !important; 71 | background: #f7fafc !important; 72 | transition: all 0.3s ease; 73 | } 74 | 75 | .dynamic-meme-btn:hover:not(:disabled) { 76 | transform: translateY(-2px); 77 | box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); 78 | transition: all 0.3s ease; 79 | } 80 | 81 | .dynamic-meme-example-item:hover { 82 | background: #edf2f7 !important; 83 | border-color: #667eea !important; 84 | transition: background 0.3s ease, border-color 0.3s ease; 85 | } 86 | 87 | /* ---------- 🎯 Focus Styles ---------- */ 88 | .dynamic-meme-input:focus { 89 | outline: none !important; 90 | border-color: #667eea !important; 91 | box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important; 92 | transition: box-shadow 0.2s ease, border-color 0.2s ease; 93 | } 94 | 95 | /* ---------- 🚫 Disabled State ---------- */ 96 | .dynamic-meme-btn:disabled { 97 | opacity: 0.6; 98 | cursor: not-allowed; 99 | transform: none !important; 100 | box-shadow: none !important; 101 | } 102 | 103 | /* ---------- 🧩 Smooth Transitions ---------- */ 104 | .dynamic-meme-btn, 105 | .dynamic-meme-category-btn, 106 | .dynamic-meme-input { 107 | transition: all 0.25s ease-in-out; 108 | } 109 | 110 | /* History heading spacing to avoid overlap with fixed navbar */ 111 | .history-heading { 112 | margin-top: 24px; /* default spacing for small screens */ 113 | } 114 | 115 | @media (min-width: 670px) { 116 | /* For viewports >= 670px ensure the heading sits below the fixed navbar */ 117 | .history-heading { 118 | margin-top: 80px; /* large enough to clear the navbar and some breathing room */ 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Meme Generator [Link](https://meme-generator-three-psi.vercel.app/) 3 | 4 | ## Table of Contents 5 | 1. Introduction 6 | 2. Features 7 | 3. Getting Started 8 | 4. Prerequisites 9 | 5. Installation 10 | 6. Contributing 11 | 7. License 12 | 13 | 14 | # Introduction 15 | Welcome to the Meme Generator Website! This documentation will guide you through the setup, usage, and customization of our React-based meme generator. This web application allows users to create, view, and edit memes with ease. 16 | 17 | 18 | 19 | ## Screenshots 20 | 21 | ![image](https://github.com/avinash201199/MemeGenerator/assets/61057666/b80b2277-8d3b-4f4f-bca4-9a7e810d51bb) 22 | 23 | 24 | 25 | 26 | 27 | ## Installation 28 | 29 | 1. Fork the repo 30 | 31 | 2. Clone the repos 32 | ```bash 33 | git clone https://github.com//MemeGenerator.git 34 | ``` 35 | 3. Go the folder 36 | 37 | ```bash 38 | cd MemeGenerator 39 | npm install 40 | ``` 41 | 42 | 4. Add the git upstream 43 | ```bash 44 | git remote add upstream https://github.com/avinash201199/MemeGenerator.git 45 | ``` 46 | 47 | 5. Make your own branch 48 | ``` bash 49 | git checkout -b 50 | ``` 51 | 52 | 6. Add your changes 53 | ```bash 54 | git add . 55 | ``` 56 | 7. Commit your changes 57 | ```bash 58 | git commit -m 59 | ``` 60 | 8. Push your changes 61 | ```bash 62 | git push 63 | ``` 64 | 65 | ## Secure Imgflip caption API (new) 66 | 67 | To avoid exposing Imgflip credentials in the client, the app now uses a serverless function at `/api/caption`. 68 | 69 | - Copy `.env.example` to `.env.local` (for local dev) and set these variables: 70 | 71 | ``` 72 | IMGFLIP_USERNAME=your_username 73 | IMGFLIP_PASSWORD=your_password 74 | ``` 75 | 76 | - On Vercel, set the same values in Project Settings → Environment Variables. 77 | 78 | Local dev: 79 | 80 | ``` 81 | npm run dev 82 | ``` 83 | 84 | Deploy on Vercel as usual; the function lives at `api/caption.js` and will be auto-deployed. 85 | 86 | The frontend `src/Meme.jsx` now posts to `/api/caption` with `template_id` and `boxes` and does not include credentials in the browser. 87 | 88 | ## Dynamic AI Meme Generator backend 89 | 90 | The AI meme generator page (`/dynamic`) talks to a Python Flask backend in `meme-bot/`. 91 | 92 | Configure the frontend API base URL: 93 | 94 | ``` 95 | # .env.local 96 | VITE_API_BASE_URL=https://your-flask-backend.example.com 97 | ``` 98 | 99 | Local dev for backend: 100 | 101 | ``` 102 | cd meme-bot 103 | pip install -r requirements.txt 104 | setx GROQ_API_KEY "your_groq_key" # Windows PowerShell: set for current user 105 | python app.py 106 | ``` 107 | 108 | Then in another terminal: 109 | 110 | ``` 111 | VITE_API_BASE_URL=http://localhost:5000 npm run dev 112 | ``` 113 | 114 | Deploy ideas for the backend: 115 | - Render/Fly.io/Railway (free/low-cost) or any VPS. 116 | - Ensure the service exposes `/api/categories`, `/api/generate`, `/api/random`, `/api/view/:filename`, `/api/download/:filename`. 117 | 118 | ## Contributing 119 | 120 | Contributions are always welcome! 121 | 122 | See `contributing.md` for ways to get started. 123 | 124 | Please adhere to this project's `code of conduct`. 125 | 126 | ## License 127 | 128 | [MIT](https://choosealicense.com/licenses/mit/) 129 | -------------------------------------------------------------------------------- /src/components/__tests__/ExportFlow.integration.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import ExportOptionsPanel from '../ExportOptionsPanel'; 5 | 6 | // Mock the utility modules 7 | vi.mock('../../utils/CanvasConverter', () => ({ 8 | default: { 9 | convertImageToCanvas: vi.fn(), 10 | exportCanvas: vi.fn(), 11 | getSupportedFormats: vi.fn(), 12 | clearCache: vi.fn(), 13 | initializeCacheCleanup: vi.fn() 14 | } 15 | })); 16 | 17 | vi.mock('../../utils/ExportProcessor', () => ({ 18 | default: { 19 | exportMeme: vi.fn(), 20 | generateFilename: vi.fn(), 21 | initializeBlobUrlCleanup: vi.fn() 22 | } 23 | })); 24 | 25 | vi.mock('../../utils/ErrorHandler', () => ({ 26 | default: { 27 | checkCompatibilityIssues: vi.fn(), 28 | handleExportError: vi.fn(), 29 | SEVERITY_LEVELS: { 30 | CRITICAL: 'critical', 31 | HIGH: 'high' 32 | } 33 | } 34 | })); 35 | 36 | vi.mock('../../utils/PerformanceMonitor', () => ({ 37 | default: { 38 | startMonitoring: vi.fn(), 39 | stopMonitoring: vi.fn() 40 | } 41 | })); 42 | 43 | vi.mock('../../utils/PerformanceOptimizer', () => ({ 44 | default: { 45 | initializeMonitoring: vi.fn(), 46 | stopMonitoring: vi.fn() 47 | } 48 | })); 49 | 50 | // Mock URL.createObjectURL and URL.revokeObjectURL 51 | global.URL.createObjectURL = vi.fn(() => 'mock-blob-url'); 52 | global.URL.revokeObjectURL = vi.fn(); 53 | 54 | // Mock canvas and image elements 55 | const mockCanvas = { 56 | toDataURL: vi.fn(() => 'data:image/png;base64,mockdata'), 57 | getContext: vi.fn(() => ({ 58 | drawImage: vi.fn(), 59 | canvas: { width: 500, height: 500 } 60 | })) 61 | }; 62 | 63 | global.HTMLCanvasElement.prototype.toDataURL = mockCanvas.toDataURL; 64 | global.HTMLCanvasElement.prototype.getContext = mockCanvas.getContext; 65 | 66 | describe('Export Flow Integration Tests', () => { 67 | const mockMemeImageSrc = 'data:image/png;base64,mockImageData'; 68 | 69 | test('should render export options panel with meme image', () => { 70 | render( 71 | 75 | ); 76 | 77 | // Verify core components are rendered 78 | expect(screen.getByText('Export Options')).toBeInTheDocument(); 79 | expect(screen.getByText('Export Format')).toBeInTheDocument(); 80 | expect(screen.getByText('Estimated File Size')).toBeInTheDocument(); 81 | }); 82 | 83 | test('should show placeholder when no meme image provided', () => { 84 | render( 85 | 89 | ); 90 | 91 | // Should show placeholder message 92 | expect(screen.getByText('Generate a meme to access export options')).toBeInTheDocument(); 93 | }); 94 | 95 | test('should not render when not visible', () => { 96 | render( 97 | 101 | ); 102 | 103 | // Should not render when not visible 104 | expect(screen.queryByText('Export Options')).not.toBeInTheDocument(); 105 | }); 106 | }); -------------------------------------------------------------------------------- /src/test/setup.js: -------------------------------------------------------------------------------- 1 | // Test setup file for Vitest 2 | import { vi } from 'vitest'; 3 | import '@testing-library/jest-dom'; 4 | 5 | // Mock DOM APIs that might not be available in test environment 6 | global.URL = global.URL || { 7 | createObjectURL: vi.fn(() => 'mock-object-url'), 8 | revokeObjectURL: vi.fn() 9 | }; 10 | 11 | global.Blob = global.Blob || class MockBlob { 12 | constructor(parts, options) { 13 | this.parts = parts || []; 14 | this.type = options?.type || ''; 15 | this.size = parts ? parts.reduce((size, part) => size + (part.length || 0), 0) : 0; 16 | } 17 | }; 18 | 19 | // Mock FileReader 20 | global.FileReader = global.FileReader || class MockFileReader { 21 | constructor() { 22 | this.result = null; 23 | this.onload = null; 24 | this.onerror = null; 25 | } 26 | 27 | readAsDataURL(blob) { 28 | setTimeout(() => { 29 | this.result = 'data:image/png;base64,mock-base64-data'; 30 | if (this.onload) this.onload(); 31 | }, 0); 32 | } 33 | }; 34 | 35 | // Mock Canvas API 36 | global.HTMLCanvasElement = global.HTMLCanvasElement || class MockCanvas { 37 | constructor() { 38 | this.width = 0; 39 | this.height = 0; 40 | } 41 | 42 | getContext() { 43 | return { 44 | drawImage: vi.fn(), 45 | clearRect: vi.fn(), 46 | getImageData: vi.fn(() => ({ data: new Uint8Array(4) })), 47 | imageSmoothingEnabled: true, 48 | imageSmoothingQuality: 'high' 49 | }; 50 | } 51 | 52 | toDataURL(type, quality) { 53 | return `data:${type || 'image/png'};base64,mock-canvas-data`; 54 | } 55 | }; 56 | 57 | // Mock Image constructor 58 | global.Image = global.Image || class MockImage { 59 | constructor() { 60 | this.src = ''; 61 | this.width = 100; 62 | this.height = 100; 63 | this.naturalWidth = 100; 64 | this.naturalHeight = 100; 65 | this.complete = false; 66 | this.onload = null; 67 | this.onerror = null; 68 | } 69 | 70 | set src(value) { 71 | this._src = value; 72 | setTimeout(() => { 73 | this.complete = true; 74 | if (this.onload) this.onload(); 75 | }, 0); 76 | } 77 | 78 | get src() { 79 | return this._src; 80 | } 81 | }; 82 | 83 | // Mock performance API 84 | global.performance = global.performance || { 85 | memory: { 86 | usedJSHeapSize: 1000000, 87 | jsHeapSizeLimit: 10000000 88 | }, 89 | now: () => Date.now() 90 | }; 91 | 92 | // Mock navigator 93 | global.navigator = global.navigator || { 94 | userAgent: 'test-user-agent', 95 | deviceMemory: 4, 96 | onLine: true 97 | }; 98 | 99 | // Mock document methods 100 | if (typeof document !== 'undefined') { 101 | document.createElement = document.createElement || vi.fn((tagName) => { 102 | const element = { 103 | tagName: tagName.toUpperCase(), 104 | style: {}, 105 | setAttribute: vi.fn(), 106 | getAttribute: vi.fn(), 107 | addEventListener: vi.fn(), 108 | removeEventListener: vi.fn(), 109 | click: vi.fn(), 110 | appendChild: vi.fn(), 111 | removeChild: vi.fn(), 112 | parentNode: null 113 | }; 114 | 115 | if (tagName === 'canvas') { 116 | Object.assign(element, new global.HTMLCanvasElement()); 117 | } 118 | 119 | if (tagName === 'a') { 120 | element.href = ''; 121 | element.download = ''; 122 | } 123 | 124 | return element; 125 | }); 126 | 127 | document.body = document.body || { 128 | appendChild: vi.fn(), 129 | removeChild: vi.fn() 130 | }; 131 | } -------------------------------------------------------------------------------- /src/utils/debugMeme.js: -------------------------------------------------------------------------------- 1 | // Debug version to test meme generation step by step 2 | export const debugMemeGeneration = async (imageUrl, texts) => { 3 | console.log('🔍 Starting debug meme generation'); 4 | console.log('📷 Image URL:', imageUrl); 5 | console.log('📝 Texts:', texts); 6 | 7 | return new Promise((resolve, reject) => { 8 | const canvas = document.createElement('canvas'); 9 | const ctx = canvas.getContext('2d'); 10 | const img = new Image(); 11 | 12 | console.log('🎨 Canvas and context created'); 13 | 14 | img.onload = () => { 15 | console.log('✅ Image loaded successfully'); 16 | console.log('📐 Image dimensions:', img.width, 'x', img.height); 17 | 18 | // Set canvas size 19 | canvas.width = img.width; 20 | canvas.height = img.height; 21 | console.log('📐 Canvas size set to:', canvas.width, 'x', canvas.height); 22 | 23 | // Draw the image 24 | ctx.drawImage(img, 0, 0); 25 | console.log('🖼️ Base image drawn'); 26 | 27 | // Calculate font size 28 | const fontSize = Math.max(30, Math.min(img.width / 10, img.height / 10)); 29 | console.log('🔤 Font size calculated:', fontSize); 30 | 31 | // Set text properties 32 | ctx.font = `bold ${fontSize}px Arial`; 33 | ctx.textAlign = 'center'; 34 | ctx.fillStyle = '#FFFFFF'; 35 | ctx.strokeStyle = '#000000'; 36 | ctx.lineWidth = 4; 37 | 38 | console.log('🎨 Text style configured'); 39 | 40 | // Draw top text with high visibility 41 | if (texts[0] && texts[0].trim()) { 42 | const topText = texts[0].toUpperCase(); 43 | const topY = fontSize + 20; 44 | 45 | console.log('📝 Drawing top text:', topText, 'at position:', canvas.width / 2, topY); 46 | 47 | // Draw thick black outline 48 | ctx.strokeText(topText, canvas.width / 2, topY); 49 | // Draw white fill 50 | ctx.fillText(topText, canvas.width / 2, topY); 51 | 52 | console.log('✅ Top text drawn'); 53 | } 54 | 55 | // Draw bottom text 56 | if (texts[1] && texts[1].trim()) { 57 | const bottomText = texts[1].toUpperCase(); 58 | const bottomY = canvas.height - fontSize - 20; 59 | 60 | console.log('📝 Drawing bottom text:', bottomText, 'at position:', canvas.width / 2, bottomY); 61 | 62 | // Draw thick black outline 63 | ctx.strokeText(bottomText, canvas.width / 2, bottomY); 64 | // Draw white fill 65 | ctx.fillText(bottomText, canvas.width / 2, bottomY); 66 | 67 | console.log('✅ Bottom text drawn'); 68 | } 69 | 70 | // Add a test rectangle to verify canvas is working 71 | ctx.fillStyle = 'red'; 72 | ctx.fillRect(10, 10, 50, 50); 73 | console.log('🔴 Test red rectangle added'); 74 | 75 | // Convert to blob 76 | canvas.toBlob((blob) => { 77 | if (blob) { 78 | const url = URL.createObjectURL(blob); 79 | console.log('✅ Blob created successfully, URL:', url); 80 | console.log('📊 Blob size:', blob.size, 'bytes'); 81 | resolve(url); 82 | } else { 83 | console.error('❌ Failed to create blob'); 84 | reject(new Error('Failed to create blob')); 85 | } 86 | }, 'image/png', 1.0); 87 | 88 | }; 89 | 90 | img.onerror = (error) => { 91 | console.error('❌ Image load error:', error); 92 | reject(new Error('Failed to load image')); 93 | }; 94 | 95 | console.log('🔄 Starting image load...'); 96 | img.src = imageUrl; 97 | }); 98 | }; -------------------------------------------------------------------------------- /src/memesMeta.js: -------------------------------------------------------------------------------- 1 | // src/memesMeta.js 2 | const memesMeta = { 3 | "Distracted Boyfriend": { 4 | description: "Man distracted by something", 5 | keywords: ["boyfriend", "distracted"], 6 | captions: ["Me vs my responsibilities", "The new phone vs old phone"] 7 | }, 8 | "Drake Hotline Bling": { 9 | description: "Drake showing preference", 10 | keywords: ["Drake", "hotline bling"], 11 | captions: ["Not this", "Yes this"] 12 | }, 13 | "Two Buttons": { 14 | description: "Person choosing between two options", 15 | keywords: ["buttons", "choice"], 16 | captions: ["Option 1", "Option 2"] 17 | }, 18 | "Change My Mind": { 19 | description: "Man at table with sign", 20 | keywords: ["table", "sign", "change my mind"], 21 | captions: ["Change my mind about coding", "Coffee vs Tea", "Coding is life", "Coffee > Tea"] 22 | }, 23 | "Expanding Brain": { 24 | description: "Increasingly enlightened brain levels", 25 | keywords: ["brain", "expanding"], 26 | captions: ["Level 1", "Level 2", "Level 3", "Level 4"] 27 | }, 28 | "Woman Yelling at Cat": { 29 | description: "Woman yelling at a confused cat", 30 | keywords: ["cat", "yelling", "woman"], 31 | captions: ["Me yelling at my responsibilities", "Cat doing nothing wrong"] 32 | }, 33 | "Mocking Spongebob": { 34 | description: "Spongebob mocking tone", 35 | keywords: ["Spongebob", "mocking"], 36 | captions: ["Me: Do your work", "Also me: Do your work"] 37 | }, 38 | "Surprised Pikachu": { 39 | description: "Pikachu shocked face", 40 | keywords: ["Pikachu", "surprised"], 41 | captions: ["Me not studying", "Me surprised by exam"] 42 | }, 43 | "Is This a Pigeon?": { 44 | description: "Man pointing at butterfly, misidentifying it", 45 | keywords: ["pigeon", "butterfly", "confused"], 46 | captions: ["Is this a good idea?", "Is this coding?"] 47 | }, 48 | "Epic Handshake": { 49 | description: "Two people shaking hands", 50 | keywords: ["handshake", "agreement", "epic"], 51 | captions: ["Teamwork", "Coding & Debugging"] 52 | }, 53 | "Left Exit 12 Off Ramp": { 54 | description: "Car swerving off highway exit", 55 | keywords: ["exit", "car", "swerve"], 56 | captions: ["Doing something unexpected", "Following the plan"] 57 | }, 58 | "Leonardo DiCaprio Cheers": { 59 | description: "DiCaprio raising a glass", 60 | keywords: ["cheers", "DiCaprio", "toast"], 61 | captions: ["Success!", "Finally done with coding"] 62 | }, 63 | "Gru's Plan": { 64 | description: "Gru showing plan on a board", 65 | keywords: ["plan", "Gru", "schemes"], 66 | captions: ["Step 1", "Step 2", "Step 3", "Step 4: Fail"] 67 | }, 68 | "Roll Safe": { 69 | description: "Man tapping his head", 70 | keywords: ["thinking", "smart", "plan"], 71 | captions: ["Can't fail if you don't try", "Use logic"] 72 | }, 73 | "This Is Fine": { 74 | description: "Dog sitting in fire saying 'This is fine'", 75 | keywords: ["dog", "fire", "fine"], 76 | captions: ["This is fine", "Everything is burning"] 77 | }, 78 | "Drakeposting": { 79 | description: "Drake disapproving then approving", 80 | keywords: ["Drake", "disapprove", "approve"], 81 | captions: ["Not this", "Yes this"] 82 | }, 83 | "Baby Yoda": { 84 | description: "Baby Yoda sipping soup", 85 | keywords: ["Baby Yoda", "Grogu", "cute"], 86 | captions: ["Me watching drama unfold", "Me minding my business"] 87 | }, 88 | "Programmer Debugging": { 89 | description: "Developer happily fixing one bug and creating three new ones", 90 | keywords: ["programmer", "debugging", "code"], 91 | captions: [ 92 | "When you fix one bug but five more appear", 93 | "It worked yesterday... what changed?" 94 | ] 95 | } 96 | }; 97 | 98 | export default memesMeta; 99 | -------------------------------------------------------------------------------- /src/components/About.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import Navbar from "./Navbar"; 3 | import Footer from "./Footer"; 4 | import "../style.css"; 5 | import "../index.css"; 6 | 7 | const About = () => { 8 | const [showButton, setShowButton] = useState(false); 9 | 10 | useEffect(() => { 11 | const handleScroll = () => { 12 | setShowButton(window.scrollY > 300); 13 | }; 14 | window.addEventListener("scroll", handleScroll); 15 | return () => window.removeEventListener("scroll", handleScroll); 16 | }, []); 17 | 18 | const scrollToTop = () => { 19 | window.scrollTo({ top: 0, behavior: "smooth" }); 20 | }; 21 | 22 | return ( 23 |
24 | 25 | 26 | {/* Main Section */} 27 |
28 |

29 | About Us 30 |

31 | 32 |
33 | {/* Text Section */} 34 |
35 |

36 | Welcome to our Meme Generator — 37 | where creativity meets laughter! We’re passionate about crafting memes 38 | that spark smiles, spread joy, and make the internet a happier place. 39 |

40 | 41 |

42 | Whether you’re a seasoned meme creator or a casual scroller, 43 | our platform gives you the tools and freedom 44 | to design memes that match your humor and personality. 45 |

46 | 47 |

48 | We believe memes are more than jokes — they’re a universal language. 49 | They connect people, lighten moods, and tell stories in seconds. 50 | That’s why we’re here: to make meme creation simple, fun, and accessible for everyone. 51 |

52 | 53 |

54 | Have feedback or new ideas? We’d{" "} 55 | 56 | love to hear from you! 57 | 58 |

59 |
60 | 61 | {/* Image Section */} 62 |
63 | Funny Meme Illustration { 67 | e.target.onerror = null; 68 | e.target.src = 69 | "https://cdn-icons-png.flaticon.com/512/904/904124.png"; // fallback image 70 | }} 71 | className="w-72 md:w-96 rounded-full shadow-[0_0_40px_#ff00ff40] hover:scale-105 transition-transform duration-300 bg-white p-4" 72 | /> 73 |
74 |
75 |
76 | 77 | {/* Scroll-to-top button */} 78 | {showButton && ( 79 | 86 | )} 87 | 88 |
89 |
90 | ); 91 | }; 92 | 93 | export default About; 94 | -------------------------------------------------------------------------------- /meme-bot/batch_meme_generator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Batch Meme Generator 4 | Generate multiple memes from predefined popular topics for testing and demonstration. 5 | """ 6 | 7 | from meme_generator_clean import MemeGeneratorClean 8 | import time 9 | 10 | def generate_sample_memes(): 11 | """Generate sample memes from various categories.""" 12 | 13 | sample_topics = [ 14 | # Youth and Modern Life 15 | {"topic": "Gen Z explaining crypto to parents", "context": "youth and technology"}, 16 | {"topic": "Online classes vs sleeping schedule", "context": "education and lifestyle"}, 17 | 18 | # Technology and AI 19 | {"topic": "ChatGPT vs Google vs actually knowing stuff", "context": "AI and knowledge"}, 20 | {"topic": "Social media break vs checking phone every 5 minutes", "context": "technology addiction"}, 21 | 22 | # Indian Culture and Society 23 | {"topic": "Indian parents asking about salary in family functions", "context": "Indian family culture"}, 24 | {"topic": "Using English vs Hindi in Indian office", "context": "language and workplace"}, 25 | 26 | # World Events and Current Affairs 27 | {"topic": "Climate change awareness vs AC usage in summer", "context": "environmental consciousness"}, 28 | {"topic": "Remote work productivity vs home distractions", "context": "work from home culture"}, 29 | 30 | # Relationships and Dating 31 | {"topic": "Dating apps vs arranged marriage suggestions", "context": "modern relationships vs tradition"}, 32 | {"topic": "Social media couple goals vs real relationship", "context": "social media reality"}, 33 | ] 34 | 35 | print("🎭 Batch Meme Generator Starting...") 36 | print(f"📊 Will generate {len(sample_topics)} sample memes") 37 | print("=" * 60) 38 | 39 | try: 40 | generator = MemeGeneratorClean() 41 | successful_memes = [] 42 | failed_memes = [] 43 | 44 | for i, topic_data in enumerate(sample_topics, 1): 45 | topic = topic_data["topic"] 46 | context = topic_data["context"] 47 | 48 | print(f"\n[{i}/{len(sample_topics)}] 🎨 Generating: '{topic}'") 49 | print(f"📝 Context: {context}") 50 | 51 | try: 52 | filename = generator.create_meme(topic, context) 53 | 54 | if filename: 55 | successful_memes.append({ 56 | 'topic': topic, 57 | 'filename': filename, 58 | 'context': context 59 | }) 60 | print(f"✅ Success: {filename}") 61 | else: 62 | failed_memes.append(topic) 63 | print(f"❌ Failed to generate meme") 64 | 65 | # Small delay to avoid overwhelming the API 66 | time.sleep(2) 67 | 68 | except Exception as e: 69 | failed_memes.append(topic) 70 | print(f"❌ Error: {e}") 71 | 72 | # Print summary 73 | print("\n" + "=" * 60) 74 | print("📊 BATCH GENERATION SUMMARY") 75 | print("=" * 60) 76 | print(f"✅ Successfully generated: {len(successful_memes)} memes") 77 | print(f"❌ Failed to generate: {len(failed_memes)} memes") 78 | 79 | if successful_memes: 80 | print("\n🎉 Generated Memes:") 81 | for meme in successful_memes: 82 | print(f" 📁 {meme['filename']} - {meme['topic'][:50]}...") 83 | 84 | if failed_memes: 85 | print("\n💔 Failed Topics:") 86 | for topic in failed_memes: 87 | print(f" - {topic[:50]}...") 88 | 89 | print(f"\n📂 Check the 'memes' folder for your generated memes!") 90 | print("🎭 Batch generation completed!") 91 | 92 | except Exception as e: 93 | print(f"❌ Error initializing batch generator: {e}") 94 | 95 | if __name__ == "__main__": 96 | generate_sample_memes() -------------------------------------------------------------------------------- /src/components/Dynamicmeme.css: -------------------------------------------------------------------------------- 1 | /* =========================================================== 2 | ⚡ Dynamic Meme Generator – Responsive & Interactive Styles 3 | Author: Divjot Singh Arora 4 | =========================================================== */ 5 | 6 | /* ---------- 🧩 Main Content Layout ---------- */ 7 | .dynamic-meme-main-content { 8 | display: grid !important; 9 | grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)) !important; 10 | gap: 30px !important; 11 | align-items: stretch !important; 12 | } 13 | 14 | /* Ensure equal section heights */ 15 | .dynamic-meme-main-content > div { 16 | height: 100% !important; 17 | min-height: 500px !important; 18 | } 19 | 20 | /* ---------- 🗂️ Category Grid ---------- */ 21 | .dynamic-meme-category-grid { 22 | display: grid !important; 23 | grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)) !important; 24 | gap: 10px !important; 25 | } 26 | 27 | /* ---------- 🎛️ Button Groups ---------- */ 28 | .dynamic-meme-btn-group { 29 | display: flex !important; 30 | flex-wrap: wrap !important; 31 | gap: 15px !important; 32 | justify-content: center !important; 33 | } 34 | 35 | /* ---------- ✏️ Inputs ---------- */ 36 | .dynamic-meme-input { 37 | width: 100% !important; 38 | box-sizing: border-box !important; 39 | border-radius: 8px !important; 40 | transition: all 0.25s ease-in-out !important; 41 | } 42 | 43 | /* ---------- 🔘 Buttons ---------- */ 44 | .dynamic-meme-btn { 45 | min-width: 120px !important; 46 | flex: 1 1 auto !important; 47 | border-radius: 10px !important; 48 | transition: all 0.3s ease !important; 49 | } 50 | 51 | /* Hover animations for buttons */ 52 | .dynamic-meme-btn:hover:not(:disabled) { 53 | transform: translateY(-2px) !important; 54 | box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4) !important; 55 | } 56 | 57 | /* Disabled button styling */ 58 | .dynamic-meme-btn:disabled { 59 | opacity: 0.6 !important; 60 | cursor: not-allowed !important; 61 | box-shadow: none !important; 62 | transform: none !important; 63 | } 64 | 65 | /* ---------- 🧱 Category Buttons ---------- */ 66 | .dynamic-meme-category-btn { 67 | min-height: 40px !important; 68 | display: flex !important; 69 | align-items: center !important; 70 | justify-content: center !important; 71 | border-radius: 8px !important; 72 | transition: all 0.3s ease !important; 73 | } 74 | 75 | .dynamic-meme-category-btn:hover { 76 | transform: translateY(-2px) !important; 77 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; 78 | border-color: #667eea !important; 79 | background-color: #f7fafc !important; 80 | } 81 | 82 | /* ---------- 🧩 Example Items ---------- */ 83 | .dynamic-meme-example-item { 84 | transition: all 0.3s ease !important; 85 | border-radius: 8px !important; 86 | } 87 | 88 | .dynamic-meme-example-item:hover { 89 | background-color: #f0f0f0 !important; 90 | transform: translateX(5px) !important; 91 | border-color: #667eea !important; 92 | } 93 | 94 | /* ---------- 📱 Responsive Breakpoints ---------- */ 95 | 96 | /* Mobile (≤768px) */ 97 | @media (max-width: 768px) { 98 | .dynamic-meme-main-content { 99 | grid-template-columns: 1fr !important; 100 | gap: 20px !important; 101 | } 102 | 103 | .dynamic-meme-category-grid { 104 | grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)) !important; 105 | } 106 | 107 | .dynamic-meme-btn-group { 108 | flex-direction: column !important; 109 | } 110 | 111 | .dynamic-meme-btn { 112 | width: 100% !important; 113 | min-width: unset !important; 114 | } 115 | } 116 | 117 | /* Tablet (≤1024px) */ 118 | @media (max-width: 1024px) { 119 | .dynamic-meme-main-content { 120 | grid-template-columns: 1fr !important; 121 | max-width: 800px !important; 122 | margin: 0 auto !important; 123 | } 124 | } 125 | 126 | /* Small Mobile (≤480px) */ 127 | @media (max-width: 480px) { 128 | .dynamic-meme-category-grid { 129 | grid-template-columns: 1fr !important; 130 | } 131 | 132 | .dynamic-meme-category-btn { 133 | min-height: 50px !important; 134 | font-size: 16px !important; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/utils/socialShare.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Social Media Share Utilities 3 | * Reusable functions for sharing memes across various social platforms 4 | */ 5 | 6 | /** 7 | * Share meme to Twitter/X 8 | * @param {string} memeUrl - URL of the meme image 9 | * @param {string} text - Optional custom text (default: "Check out this meme I made!") 10 | */ 11 | export const shareToTwitter = (memeUrl, text = "Check out this meme I made!") => { 12 | const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(memeUrl)}`; 13 | window.open(url, '_blank', 'noopener,noreferrer'); 14 | }; 15 | 16 | /** 17 | * Share meme to Facebook 18 | * @param {string} memeUrl - URL of the meme image 19 | */ 20 | export const shareToFacebook = (memeUrl) => { 21 | const url = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(memeUrl)}`; 22 | window.open(url, '_blank', 'noopener,noreferrer'); 23 | }; 24 | 25 | /** 26 | * Share meme to Reddit 27 | * @param {string} memeUrl - URL of the meme image 28 | * @param {string} title - Optional custom title (default: "Check out this meme I made!") 29 | */ 30 | export const shareToReddit = (memeUrl, title = "Check out this meme I made!") => { 31 | const url = `https://reddit.com/submit?url=${encodeURIComponent(memeUrl)}&title=${encodeURIComponent(title)}`; 32 | window.open(url, '_blank', 'noopener,noreferrer'); 33 | }; 34 | 35 | /** 36 | * Share meme to Instagram (copies URL to clipboard) 37 | * Instagram doesn't support direct URL sharing, so we copy the URL for users to paste 38 | * @param {string} memeUrl - URL of the meme image 39 | * @returns {Promise} - True if successful, false otherwise 40 | */ 41 | export const shareToInstagram = async (memeUrl) => { 42 | try { 43 | await navigator.clipboard.writeText(memeUrl); 44 | alert("Meme URL copied! You can now paste it in Instagram or download the image to share as a story/post."); 45 | return true; 46 | } catch (error) { 47 | alert("Failed to copy URL. Please manually copy the meme URL to share on Instagram."); 48 | return false; 49 | } 50 | }; 51 | 52 | /** 53 | * Share meme to WhatsApp 54 | * @param {string} memeUrl - URL of the meme image 55 | * @param {string} text - Optional custom text (default: "Check out this meme I made!") 56 | */ 57 | export const shareToWhatsApp = (memeUrl, text = "Check out this meme I made!") => { 58 | const message = `${text} ${memeUrl}`; 59 | const url = `https://wa.me/?text=${encodeURIComponent(message)}`; 60 | window.open(url, '_blank', 'noopener,noreferrer'); 61 | }; 62 | 63 | /** 64 | * Copy meme URL to clipboard 65 | * @param {string} memeUrl - URL of the meme image 66 | * @returns {Promise} - True if successful, false otherwise 67 | */ 68 | export const copyToClipboard = async (memeUrl) => { 69 | try { 70 | await navigator.clipboard.writeText(memeUrl); 71 | alert("Meme URL copied to clipboard!"); 72 | return true; 73 | } catch (error) { 74 | alert("Failed to copy URL"); 75 | return false; 76 | } 77 | }; 78 | 79 | /** 80 | * Download meme image 81 | * @param {string} memeUrl - URL of the meme image 82 | * @param {string|number} filename - Filename for the downloaded meme (default: "meme") 83 | */ 84 | export const downloadMeme = (memeUrl, filename = "meme") => { 85 | const xhr = new XMLHttpRequest(); 86 | xhr.open("GET", memeUrl, true); 87 | xhr.responseType = "blob"; 88 | xhr.onload = function () { 89 | const urlCreator = window.URL || window.webkitURL; 90 | const imageUrl = urlCreator.createObjectURL(this.response); 91 | const tag = document.createElement('a'); 92 | tag.href = imageUrl; 93 | tag.download = filename; 94 | document.body.appendChild(tag); 95 | tag.click(); 96 | document.body.removeChild(tag); 97 | // Clean up the blob URL 98 | urlCreator.revokeObjectURL(imageUrl); 99 | }; 100 | xhr.onerror = function () { 101 | alert("Failed to download meme. Please try again."); 102 | }; 103 | xhr.send(); 104 | }; 105 | 106 | /** 107 | * Get all share functions as an object 108 | * Useful for passing to components 109 | */ 110 | export const socialShareUtils = { 111 | shareToTwitter, 112 | shareToFacebook, 113 | shareToReddit, 114 | shareToInstagram, 115 | shareToWhatsApp, 116 | copyToClipboard, 117 | downloadMeme 118 | }; 119 | 120 | export default socialShareUtils; 121 | 122 | -------------------------------------------------------------------------------- /src/utils/test-canvas-meme.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test Canvas Meme Generation 5 | 13 | 14 | 15 |
16 |

Canvas Meme Generator Test

17 | 18 |
19 | 20 | 21 | 22 |
23 | 24 |
25 |

Original Image:

26 | Drake meme template 27 |
28 | 29 |
30 |

Generated Meme:

31 | 32 |
33 |
34 | 35 | 110 | 111 | -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import "../index.css"; 3 | import { FaGithub, FaInstagram, FaLinkedin } from "react-icons/fa"; 4 | import { gsap } from "gsap"; 5 | 6 | const Footer = () => { 7 | const currentYear = new Date().getFullYear(); 8 | const footerRef = useRef(null); 9 | 10 | useEffect(() => { 11 | // Animate footer on mount 12 | gsap.fromTo( 13 | footerRef.current, 14 | { y: 50, opacity: 0 }, 15 | { y: 0, opacity: 1, duration: 1.5, ease: "power3.out" } 16 | ); 17 | 18 | // Animate social icons hover 19 | const icons = footerRef.current.querySelectorAll(".social-icon"); 20 | icons.forEach((icon) => { 21 | icon.addEventListener("mouseenter", () => { 22 | gsap.to(icon, { scale: 1.3, rotation: 15, duration: 0.3 }); 23 | }); 24 | icon.addEventListener("mouseleave", () => { 25 | gsap.to(icon, { scale: 1, rotation: 0, duration: 0.3 }); 26 | }); 27 | }); 28 | }, []); 29 | 30 | return ( 31 |
35 |
36 | {/* Glass container */} 37 |
38 |
39 | {/* Left - Copyright */} 40 |
41 |

© {currentYear} Meme Generator

42 |

All rights reserved.

43 |
44 | 45 | {/* Center - Brand */} 46 |
47 |

48 | 🎭 Meme Generator 49 |

50 |

51 | Creating laughter, one meme at a time 52 |

53 |
54 | 55 | {/* Right - Socials */} 56 | 82 |
83 | 84 | {/* Links section */} 85 |
86 |
87 |

Quick Links

88 | 94 |
95 |
96 |

Resources

97 | 103 |
104 |
105 | 106 | {/* Bottom */} 107 |
108 |
109 |

110 | Built with ❤️ for HacktoberFest {currentYear} | Made with React, TailwindCSS & GSAP 111 |

112 |
113 |
114 |
115 |
116 |
117 | ); 118 | }; 119 | 120 | export default Footer; 121 | -------------------------------------------------------------------------------- /src/components/FormatSelector.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FormatSelector = ({ selectedFormat, onFormatChange, disabled = false }) => { 4 | const formats = [ 5 | { 6 | value: 'png', 7 | label: 'PNG', 8 | description: 'Lossless, best quality', 9 | icon: '🖼️' 10 | }, 11 | { 12 | value: 'jpeg', 13 | label: 'JPEG', 14 | description: 'Smaller size, adjustable quality', 15 | icon: '📷' 16 | }, 17 | { 18 | value: 'webp', 19 | label: 'WebP', 20 | description: 'Modern format, best compression', 21 | icon: '🚀' 22 | } 23 | ]; 24 | 25 | const handleFormatClick = (format) => { 26 | if (!disabled) { 27 | onFormatChange(format); 28 | } 29 | }; 30 | 31 | // CSS for responsive design 32 | const responsiveCSS = ` 33 | .format-selector-container { 34 | margin-bottom: 20px; 35 | } 36 | 37 | .format-selector-label { 38 | display: block; 39 | margin-bottom: 12px; 40 | font-weight: 600; 41 | color: #4a5568; 42 | font-size: 14px; 43 | } 44 | 45 | .format-selector-grid { 46 | display: grid; 47 | grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); 48 | gap: 12px; 49 | } 50 | 51 | .format-selector-button { 52 | padding: 16px 12px; 53 | border: 2px solid #e2e8f0; 54 | background: white; 55 | border-radius: 12px; 56 | cursor: pointer; 57 | transition: all 0.3s ease; 58 | text-align: center; 59 | font-size: 14px; 60 | display: flex; 61 | flex-direction: column; 62 | align-items: center; 63 | gap: 8px; 64 | min-height: 100px; 65 | font-family: inherit; 66 | } 67 | 68 | .format-selector-button:hover:not(.format-selector-button-disabled):not(.format-selector-button-selected) { 69 | border-color: #cbd5e0; 70 | transform: translateY(-2px); 71 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 72 | background-color: #f7fafc; 73 | } 74 | 75 | .format-selector-button-selected { 76 | border-color: #667eea; 77 | background: linear-gradient(135deg, #f7faff 0%, #edf2ff 100%); 78 | box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15); 79 | transform: translateY(-1px); 80 | } 81 | 82 | .format-selector-button-disabled { 83 | opacity: 0.5; 84 | cursor: not-allowed; 85 | } 86 | 87 | .format-selector-icon { 88 | font-size: 24px; 89 | margin-bottom: 4px; 90 | } 91 | 92 | .format-selector-label-text { 93 | font-weight: 600; 94 | color: #2d3748; 95 | margin-bottom: 4px; 96 | } 97 | 98 | .format-selector-description { 99 | font-size: 12px; 100 | color: #718096; 101 | line-height: 1.3; 102 | } 103 | 104 | /* Responsive breakpoints */ 105 | @media (max-width: 768px) { 106 | .format-selector-grid { 107 | grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); 108 | gap: 10px; 109 | } 110 | 111 | .format-selector-button { 112 | padding: 14px 10px; 113 | min-height: 90px; 114 | font-size: 13px; 115 | } 116 | 117 | .format-selector-icon { 118 | font-size: 20px; 119 | } 120 | 121 | .format-selector-description { 122 | font-size: 11px; 123 | } 124 | } 125 | 126 | @media (max-width: 480px) { 127 | .format-selector-grid { 128 | grid-template-columns: 1fr; 129 | gap: 8px; 130 | } 131 | 132 | .format-selector-button { 133 | flex-direction: row; 134 | text-align: left; 135 | min-height: 60px; 136 | padding: 12px 16px; 137 | gap: 12px; 138 | } 139 | 140 | .format-selector-icon { 141 | font-size: 24px; 142 | margin-bottom: 0; 143 | flex-shrink: 0; 144 | } 145 | 146 | .format-selector-content { 147 | display: flex; 148 | flex-direction: column; 149 | gap: 2px; 150 | } 151 | 152 | .format-selector-label-text { 153 | margin-bottom: 2px; 154 | font-size: 14px; 155 | } 156 | 157 | .format-selector-description { 158 | font-size: 12px; 159 | } 160 | } 161 | `; 162 | 163 | const styles = { 164 | container: { 165 | marginBottom: '20px' 166 | }, 167 | label: { 168 | display: 'block', 169 | marginBottom: '12px', 170 | fontWeight: '600', 171 | color: '#4a5568', 172 | fontSize: '14px' 173 | } 174 | }; 175 | 176 | return ( 177 | <> 178 | 179 |
180 | 183 |
184 | {formats.map((format) => { 185 | const isSelected = selectedFormat === format.value; 186 | const buttonClasses = [ 187 | 'format-selector-button', 188 | isSelected ? 'format-selector-button-selected' : '', 189 | disabled ? 'format-selector-button-disabled' : '' 190 | ].filter(Boolean).join(' '); 191 | 192 | return ( 193 |
handleFormatClick(format.value)} 197 | role="button" 198 | tabIndex={disabled ? -1 : 0} 199 | onKeyDown={(e) => { 200 | if ((e.key === 'Enter' || e.key === ' ') && !disabled) { 201 | e.preventDefault(); 202 | handleFormatClick(format.value); 203 | } 204 | }} 205 | aria-pressed={isSelected} 206 | aria-disabled={disabled} 207 | > 208 |
{format.icon}
209 |
210 |
{format.label}
211 |
{format.description}
212 |
213 |
214 | ); 215 | })} 216 |
217 |
218 | 219 | ); 220 | }; 221 | 222 | export default FormatSelector; -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Meme Generator 2 | 3 | Thank you for your interest in contributing to Meme Generator! We appreciate contributions from everyone, whether you're fixing bugs, adding features, or improving documentation. 4 | 5 | ## Table of Contents 6 | 7 | - [Code of Conduct](#code-of-conduct) 8 | - [Getting Started](#getting-started) 9 | - [Development Workflow](#development-workflow) 10 | - [Coding Standards](#coding-standards) 11 | - [Submitting Changes](#submitting-changes) 12 | - [Reporting Bugs](#reporting-bugs) 13 | - [Requesting Features](#requesting-features) 14 | 15 | ## Code of Conduct 16 | 17 | We are committed to providing a welcoming and inclusive environment. Please be respectful and considerate in all interactions. 18 | 19 | **Expected behavior:** 20 | - Be respectful of different viewpoints and experiences 21 | - Accept constructive criticism gracefully 22 | - Focus on what's best for the community 23 | - Show empathy towards others 24 | 25 | **Unacceptable behavior:** 26 | - Harassment, trolling, or discriminatory comments 27 | - Personal attacks or insults 28 | - Publishing others' private information 29 | - Any conduct inappropriate in a professional setting 30 | 31 | ## Getting Started 32 | 33 | ### Prerequisites 34 | 35 | - Node.js 22.x (check `.nvmrc` file) 36 | - npm (comes with Node.js) 37 | - Git 38 | 39 | ### Installation 40 | 41 | Follow the installation steps in the [README](README.md). Make sure you: 42 | 43 | 1. Fork and clone the repository 44 | 2. Install dependencies with `npm install` 45 | 3. Set up environment variables if needed (see README for details) 46 | 4. Start the dev server with `npm run dev` 47 | 48 | ### Development Commands 49 | 50 | ```bash 51 | npm run dev # Start development server 52 | npm test # Run tests once 53 | npm run test:watch # Run tests in watch mode 54 | npm run lint # Check for linting errors 55 | npm run build # Build for production 56 | ``` 57 | 58 | ## Development Workflow 59 | 60 | ### Before You Start 61 | 62 | 1. Check existing issues to avoid duplicate work 63 | 2. For major changes, open an issue first to discuss your approach 64 | 3. Comment on the issue you want to work on so others know it's taken 65 | 66 | ### Creating a Branch 67 | 68 | Use descriptive branch names: 69 | 70 | ```bash 71 | git checkout -b feat/add-meme-search 72 | git checkout -b fix/pagination-bug 73 | git checkout -b docs/update-readme 74 | ``` 75 | 76 | Branch prefixes: 77 | - `feature/` - New features 78 | - `fix/` - Bug fixes 79 | - `docs/` - Documentation 80 | - `refactor/` - Code refactoring 81 | - `test/` - Adding tests 82 | 83 | ### Making Changes 84 | 85 | 1. Write clean, readable code 86 | 2. Add tests for new features or bug fixes 87 | 3. Update documentation if needed 88 | 4. Keep commits focused and atomic 89 | 5. Test your changes thoroughly 90 | 91 | ### Commit Messages 92 | 93 | Follow conventional commit format: 94 | 95 | ``` 96 | type: brief description 97 | 98 | Longer explanation if needed 99 | ``` 100 | 101 | Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` 102 | 103 | Examples: 104 | ```bash 105 | git commit -m "feat: add keyboard navigation to templates" 106 | git commit -m "fix: resolve mobile pagination issue" 107 | git commit -m "docs: update API documentation" 108 | ``` 109 | 110 | ## Coding Standards 111 | 112 | ### JavaScript/React 113 | 114 | - Use ES6+ features (arrow functions, destructuring, etc.) 115 | - Prefer functional components with hooks 116 | - Use meaningful variable and function names 117 | - Keep functions small and focused 118 | - Add comments for complex logic 119 | 120 | ### Code Style 121 | 122 | - Indentation: 2 spaces 123 | - Quotes: Single quotes for JS, double for JSX 124 | - Semicolons: Required 125 | - Max line length: 100 characters (soft limit) 126 | 127 | ### CSS/Styling 128 | 129 | - Use TailwindCSS utility classes when possible 130 | - Follow mobile-first responsive design 131 | - Ensure dark mode compatibility 132 | - Maintain WCAG AA color contrast standards 133 | 134 | ### Accessibility 135 | 136 | - Use semantic HTML elements 137 | - Add ARIA labels where appropriate 138 | - Ensure keyboard navigation works 139 | - Test with screen readers when possible 140 | 141 | ## Submitting Changes 142 | 143 | ### Pull Request Checklist 144 | 145 | Before submitting, ensure: 146 | 147 | - [ ] Code follows project style guidelines 148 | - [ ] All tests pass (`npm test`) 149 | - [ ] No linting errors (`npm run lint`) 150 | - [ ] Build succeeds (`npm run build`) 151 | - [ ] Self-reviewed your code 152 | - [ ] Added/updated tests if needed 153 | - [ ] Updated documentation if needed 154 | - [ ] Tested in multiple browsers (for UI changes) 155 | 156 | ### Creating a Pull Request 157 | 158 | 1. Push your branch to your fork 159 | 2. Open a PR against the main repository 160 | 3. Fill out the PR template with: 161 | - Clear description of changes 162 | - Related issue number (if applicable) 163 | - Screenshots for UI changes 164 | - Testing steps 165 | 166 | ### PR Review Process 167 | 168 | - Maintainers will review within a few days 169 | - Address any feedback by pushing new commits 170 | - Once approved, your PR will be merged 171 | - Your contribution will be recognized in the project 172 | 173 | ## Reporting Bugs 174 | 175 | Before reporting a bug: 176 | 1. Check if it's already reported in [issues](https://github.com/avinash201199/MemeGenerator/issues) 177 | 2. Try the latest version to see if it's fixed 178 | 3. Gather relevant information 179 | 180 | When reporting, include: 181 | - Clear, descriptive title 182 | - Steps to reproduce 183 | - Expected vs actual behavior 184 | - Screenshots if applicable 185 | - Environment details (OS, browser, Node version) 186 | - Console errors if any 187 | 188 | ## Requesting Features 189 | 190 | We welcome feature suggestions! When requesting: 191 | 192 | - Check if it's already suggested 193 | - Explain the problem it solves 194 | - Describe your proposed solution 195 | - Consider alternative approaches 196 | - Add mockups or examples if helpful 197 | 198 | Open an issue with the `enhancement` label. 199 | 200 | ## Finding Issues to Work On 201 | 202 | Look for issues labeled: 203 | - `good first issue` - Great for newcomers 204 | - `help wanted` - Contributions welcome 205 | - `documentation` - Documentation improvements 206 | 207 | ## Getting Help 208 | 209 | Need assistance? 210 | - Comment on the issue you're working on 211 | - Check existing documentation and issues 212 | - Reach out to [@avinash201199](https://github.com/avinash201199) 213 | 214 | ## Recognition 215 | 216 | All contributors are recognized in: 217 | - GitHub contributors list 218 | - Project README (for significant contributions) 219 | - Release notes 220 | 221 | ## Resources 222 | 223 | - [Project README](README.md) 224 | - [GitHub Repository](https://github.com/avinash201199/MemeGenerator) 225 | - [Live Demo](https://meme-generator-three-psi.vercel.app/) 226 | - [React Documentation](https://react.dev/) 227 | - [TailwindCSS Docs](https://tailwindcss.com/docs) 228 | 229 | ## License 230 | 231 | By contributing, you agree that your contributions will be licensed under the MIT License. 232 | 233 | --- 234 | 235 | Thank you for contributing to Meme Generator! Your efforts help make this project better for everyone. 236 | -------------------------------------------------------------------------------- /src/components/QualitySlider.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | 3 | const QualitySlider = ({ 4 | quality = 80, 5 | onQualityChange, 6 | visible = true, 7 | disabled = false 8 | }) => { 9 | const [localQuality, setLocalQuality] = useState(quality); 10 | 11 | // Debounced callback to prevent excessive updates 12 | const debouncedOnQualityChange = useCallback( 13 | debounce((value) => { 14 | onQualityChange(value); 15 | }, 300), 16 | [onQualityChange] 17 | ); 18 | 19 | // Update local state when prop changes 20 | useEffect(() => { 21 | setLocalQuality(quality); 22 | }, [quality]); 23 | 24 | const handleSliderChange = (e) => { 25 | const value = parseInt(e.target.value, 10); 26 | setLocalQuality(value); 27 | debouncedOnQualityChange(value); 28 | }; 29 | 30 | const getQualityLabel = (value) => { 31 | if (value >= 90) return 'Excellent'; 32 | if (value >= 80) return 'High'; 33 | if (value >= 60) return 'Good'; 34 | if (value >= 40) return 'Medium'; 35 | if (value >= 20) return 'Low'; 36 | if (value > 0) return 'Very Low'; 37 | return 'Minimum'; 38 | }; 39 | 40 | const getQualityColor = (value) => { 41 | if (value >= 80) return '#48bb78'; 42 | if (value >= 60) return '#ed8936'; 43 | if (value >= 40) return '#ecc94b'; 44 | return '#f56565'; 45 | }; 46 | 47 | // CSS for responsive design 48 | const responsiveCSS = ` 49 | .quality-slider-container { 50 | margin-bottom: 20px; 51 | transition: opacity 0.3s ease, visibility 0.3s ease, max-height 0.3s ease; 52 | overflow: hidden; 53 | } 54 | 55 | .quality-slider-container.hidden { 56 | opacity: 0; 57 | visibility: hidden; 58 | max-height: 0; 59 | margin-bottom: 0; 60 | } 61 | 62 | .quality-slider-container.visible { 63 | opacity: 1; 64 | visibility: visible; 65 | max-height: 200px; 66 | } 67 | 68 | .quality-slider-label { 69 | display: block; 70 | margin-bottom: 12px; 71 | font-weight: 600; 72 | color: #4a5568; 73 | font-size: 14px; 74 | } 75 | 76 | .quality-slider-input-container { 77 | position: relative; 78 | margin-bottom: 8px; 79 | } 80 | 81 | .quality-slider-input { 82 | width: 100%; 83 | height: 8px; 84 | border-radius: 4px; 85 | background: linear-gradient(to right, 86 | #f56565 0%, 87 | #ecc94b 25%, 88 | #ed8936 50%, 89 | #48bb78 75%, 90 | #48bb78 100%); 91 | outline: none; 92 | cursor: pointer; 93 | transition: opacity 0.3s ease; 94 | -webkit-appearance: none; 95 | appearance: none; 96 | } 97 | 98 | .quality-slider-input:disabled { 99 | opacity: 0.5; 100 | cursor: not-allowed; 101 | } 102 | 103 | .quality-slider-input::-webkit-slider-thumb { 104 | -webkit-appearance: none; 105 | appearance: none; 106 | height: 20px; 107 | width: 20px; 108 | border-radius: 50%; 109 | background: #667eea; 110 | cursor: pointer; 111 | border: 2px solid white; 112 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); 113 | transition: transform 0.2s ease; 114 | } 115 | 116 | .quality-slider-input::-webkit-slider-thumb:hover { 117 | transform: scale(1.1); 118 | } 119 | 120 | .quality-slider-input::-webkit-slider-thumb:active { 121 | transform: scale(0.95); 122 | } 123 | 124 | .quality-slider-input::-moz-range-thumb { 125 | height: 20px; 126 | width: 20px; 127 | border-radius: 50%; 128 | background: #667eea; 129 | cursor: pointer; 130 | border: 2px solid white; 131 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); 132 | transition: transform 0.2s ease; 133 | } 134 | 135 | .quality-slider-input::-moz-range-thumb:hover { 136 | transform: scale(1.1); 137 | } 138 | 139 | .quality-slider-input::-moz-range-thumb:active { 140 | transform: scale(0.95); 141 | } 142 | 143 | .quality-slider-info { 144 | display: flex; 145 | justify-content: space-between; 146 | align-items: center; 147 | font-size: 12px; 148 | color: #718096; 149 | margin-bottom: 4px; 150 | } 151 | 152 | .quality-slider-value { 153 | font-weight: 600; 154 | font-size: 14px; 155 | } 156 | 157 | .quality-slider-labels { 158 | display: flex; 159 | justify-content: space-between; 160 | font-size: 10px; 161 | color: #a0aec0; 162 | } 163 | 164 | /* Responsive breakpoints */ 165 | @media (max-width: 768px) { 166 | .quality-slider-info { 167 | flex-direction: column; 168 | align-items: flex-start; 169 | gap: 4px; 170 | } 171 | 172 | .quality-slider-value { 173 | font-size: 13px; 174 | } 175 | 176 | .quality-slider-labels { 177 | font-size: 9px; 178 | } 179 | } 180 | 181 | @media (max-width: 480px) { 182 | .quality-slider-input::-webkit-slider-thumb { 183 | height: 24px; 184 | width: 24px; 185 | } 186 | 187 | .quality-slider-input::-moz-range-thumb { 188 | height: 24px; 189 | width: 24px; 190 | } 191 | 192 | .quality-slider-info { 193 | font-size: 11px; 194 | } 195 | 196 | .quality-slider-value { 197 | font-size: 12px; 198 | } 199 | } 200 | `; 201 | 202 | const styles = { 203 | container: { 204 | marginBottom: '20px' 205 | }, 206 | label: { 207 | display: 'block', 208 | marginBottom: '12px', 209 | fontWeight: '600', 210 | color: '#4a5568', 211 | fontSize: '14px' 212 | } 213 | }; 214 | 215 | return ( 216 | <> 217 | 218 |
219 | 222 |
223 | 233 |
234 |
235 | Quality: {localQuality}% ({getQualityLabel(localQuality)}) 236 | Size vs Quality 237 |
238 |
239 | Smaller 240 | Larger 241 |
242 |
243 | 244 | ); 245 | }; 246 | 247 | // Debounce utility function 248 | function debounce(func, wait) { 249 | let timeout; 250 | return function executedFunction(...args) { 251 | const later = () => { 252 | clearTimeout(timeout); 253 | func(...args); 254 | }; 255 | clearTimeout(timeout); 256 | timeout = setTimeout(later, wait); 257 | }; 258 | } 259 | 260 | export default QualitySlider; -------------------------------------------------------------------------------- /src/utils/memeCanvas.js: -------------------------------------------------------------------------------- 1 | // Client-side meme generation using HTML5 Canvas 2 | export const generateMemeWithCanvas = async (imageUrl, texts, templateBoxCount = 2) => { 3 | return new Promise((resolve, reject) => { 4 | const canvas = document.createElement('canvas'); 5 | const ctx = canvas.getContext('2d'); 6 | const img = new Image(); 7 | 8 | img.crossOrigin = 'anonymous'; 9 | 10 | img.onload = () => { 11 | // Set canvas size to match image 12 | canvas.width = img.width; 13 | canvas.height = img.height; 14 | 15 | // Draw the base image 16 | ctx.drawImage(img, 0, 0); 17 | 18 | // Configure text style 19 | ctx.fillStyle = 'white'; 20 | ctx.strokeStyle = 'black'; 21 | ctx.lineWidth = 3; 22 | ctx.textAlign = 'center'; 23 | ctx.font = 'bold 40px Impact, Arial Black, sans-serif'; 24 | 25 | // Add text based on template box count 26 | if (templateBoxCount === 2) { 27 | // Top and bottom text (classic meme format) 28 | if (texts[0]) { 29 | const topText = texts[0].toUpperCase(); 30 | ctx.strokeText(topText, canvas.width / 2, 50); 31 | ctx.fillText(topText, canvas.width / 2, 50); 32 | } 33 | 34 | if (texts[1]) { 35 | const bottomText = texts[1].toUpperCase(); 36 | ctx.strokeText(bottomText, canvas.width / 2, canvas.height - 20); 37 | ctx.fillText(bottomText, canvas.width / 2, canvas.height - 20); 38 | } 39 | } else { 40 | // Multiple text boxes - distribute vertically 41 | texts.forEach((text, index) => { 42 | if (text) { 43 | const y = (canvas.height / (templateBoxCount + 1)) * (index + 1); 44 | const displayText = text.toUpperCase(); 45 | ctx.strokeText(displayText, canvas.width / 2, y); 46 | ctx.fillText(displayText, canvas.width / 2, y); 47 | } 48 | }); 49 | } 50 | 51 | // Convert canvas to blob URL 52 | canvas.toBlob((blob) => { 53 | const url = URL.createObjectURL(blob); 54 | resolve(url); 55 | }, 'image/jpeg', 0.9); 56 | }; 57 | 58 | img.onerror = () => { 59 | reject(new Error('Failed to load image')); 60 | }; 61 | 62 | img.src = imageUrl; 63 | }); 64 | }; 65 | 66 | // Helper function to wrap text for better display 67 | export const wrapText = (ctx, text, maxWidth) => { 68 | const words = text.split(' '); 69 | const lines = []; 70 | let currentLine = words[0]; 71 | 72 | for (let i = 1; i < words.length; i++) { 73 | const word = words[i]; 74 | const width = ctx.measureText(currentLine + ' ' + word).width; 75 | if (width < maxWidth) { 76 | currentLine += ' ' + word; 77 | } else { 78 | lines.push(currentLine); 79 | currentLine = word; 80 | } 81 | } 82 | lines.push(currentLine); 83 | return lines; 84 | }; 85 | 86 | // Enhanced version with text wrapping 87 | export const generateMemeWithWrapping = async (imageUrl, texts, templateBoxCount = 2) => { 88 | return new Promise((resolve, reject) => { 89 | const canvas = document.createElement('canvas'); 90 | const ctx = canvas.getContext('2d'); 91 | const img = new Image(); 92 | 93 | // Handle CORS for external images 94 | if (imageUrl.includes('imgflip.com')) { 95 | img.crossOrigin = 'anonymous'; 96 | } 97 | 98 | img.onload = () => { 99 | canvas.width = img.width; 100 | canvas.height = img.height; 101 | 102 | // Draw the base image 103 | ctx.drawImage(img, 0, 0); 104 | 105 | // Calculate responsive font size based on image dimensions 106 | const baseFontSize = Math.max(20, Math.min(canvas.width / 10, canvas.height / 15)); 107 | 108 | // Configure text style with better visibility 109 | ctx.fillStyle = '#FFFFFF'; 110 | ctx.strokeStyle = '#000000'; 111 | ctx.lineWidth = Math.max(2, baseFontSize / 15); 112 | ctx.textAlign = 'center'; 113 | ctx.textBaseline = 'middle'; 114 | ctx.font = `bold ${baseFontSize}px Impact, "Arial Black", Arial, sans-serif`; 115 | 116 | // Add shadow for better text visibility 117 | ctx.shadowColor = 'rgba(0, 0, 0, 0.8)'; 118 | ctx.shadowBlur = 4; 119 | ctx.shadowOffsetX = 2; 120 | ctx.shadowOffsetY = 2; 121 | 122 | const maxWidth = canvas.width * 0.85; 123 | 124 | console.log('Drawing text on canvas:', { texts, templateBoxCount, canvasSize: { width: canvas.width, height: canvas.height } }); 125 | 126 | if (templateBoxCount === 2) { 127 | // Top text 128 | if (texts[0] && texts[0].trim()) { 129 | const topText = texts[0].toUpperCase(); 130 | const topY = baseFontSize + 20; 131 | 132 | // Draw stroke first, then fill 133 | ctx.strokeText(topText, canvas.width / 2, topY); 134 | ctx.fillText(topText, canvas.width / 2, topY); 135 | 136 | console.log('Drew top text:', topText, 'at position:', canvas.width / 2, topY); 137 | } 138 | 139 | // Bottom text 140 | if (texts[1] && texts[1].trim()) { 141 | const bottomText = texts[1].toUpperCase(); 142 | const bottomY = canvas.height - baseFontSize - 20; 143 | 144 | // Draw stroke first, then fill 145 | ctx.strokeText(bottomText, canvas.width / 2, bottomY); 146 | ctx.fillText(bottomText, canvas.width / 2, bottomY); 147 | 148 | console.log('Drew bottom text:', bottomText, 'at position:', canvas.width / 2, bottomY); 149 | } 150 | } else { 151 | // Multiple text boxes - distribute evenly 152 | texts.forEach((text, index) => { 153 | if (text && text.trim()) { 154 | const displayText = text.toUpperCase(); 155 | const sectionHeight = canvas.height / (templateBoxCount + 1); 156 | const y = sectionHeight * (index + 1); 157 | 158 | // Draw stroke first, then fill 159 | ctx.strokeText(displayText, canvas.width / 2, y); 160 | ctx.fillText(displayText, canvas.width / 2, y); 161 | 162 | console.log(`Drew text ${index + 1}:`, displayText, 'at position:', canvas.width / 2, y); 163 | } 164 | }); 165 | } 166 | 167 | // Reset shadow 168 | ctx.shadowColor = 'transparent'; 169 | ctx.shadowBlur = 0; 170 | ctx.shadowOffsetX = 0; 171 | ctx.shadowOffsetY = 0; 172 | 173 | // Convert canvas to blob URL 174 | canvas.toBlob((blob) => { 175 | if (blob) { 176 | const url = URL.createObjectURL(blob); 177 | console.log('Canvas meme generated successfully'); 178 | resolve(url); 179 | } else { 180 | reject(new Error('Failed to create blob from canvas')); 181 | } 182 | }, 'image/jpeg', 0.9); 183 | }; 184 | 185 | img.onerror = (error) => { 186 | console.error('Image load error:', error); 187 | reject(new Error('Failed to load image: ' + imageUrl)); 188 | }; 189 | 190 | // Add timeout for image loading 191 | setTimeout(() => { 192 | if (!img.complete) { 193 | reject(new Error('Image loading timeout')); 194 | } 195 | }, 10000); 196 | 197 | img.src = imageUrl; 198 | }); 199 | }; -------------------------------------------------------------------------------- /src/utils/test-export-panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ExportOptionsPanel Test 7 | 8 | 51 | 52 | 53 |
54 |

ExportOptionsPanel Component Test

55 | 56 |
57 |

Task 3.3: Responsive Design & Styling ✅ COMPLETED

58 |

✅ Responsive Layout: Works on desktop, tablet, and mobile devices

59 |

✅ Consistent Styling: Matches existing component design patterns

60 |

✅ Proper Spacing: Typography and visual hierarchy implemented

61 |

✅ CSS Media Queries: Breakpoints at 1024px, 768px, and 480px

62 |

✅ Mobile Optimization: Touch-friendly buttons and improved layouts

63 |
64 | 65 |
66 |

Component Structure Test

67 |

✅ Component Created: ExportOptionsPanel.jsx

68 |

✅ State Management: Format, quality, and export status

69 |

✅ Sub-components Integration: FormatSelector, QualitySlider, FileSizeEstimator, ExportButton

70 |

✅ Responsive Design: Mobile-friendly layout with CSS media queries

71 |

✅ Error Handling: Export error states and user feedback

72 |
73 | 74 |
75 |

Responsive Design Features

76 |
    77 |
  • Desktop (>1024px): Full layout with optimal spacing and typography
  • 78 |
  • Tablet (768px-1024px): Adjusted padding and font sizes
  • 79 |
  • Mobile (480px-768px): Stacked layouts and touch-friendly controls
  • 80 |
  • Small Mobile (<480px): Compact design with horizontal format buttons
  • 81 |
  • Typography Scale: Responsive font sizes from 1.5rem to 1.125rem
  • 82 |
  • Spacing System: Consistent margins and padding across breakpoints
  • 83 |
  • Touch Targets: Minimum 44px height for mobile interactions
  • 84 |
  • Visual Hierarchy: Clear information architecture at all sizes
  • 85 |
86 |
87 | 88 |
89 |

Features Implemented

90 |
    91 |
  • Container Component: Main orchestrator for all export sub-components
  • 92 |
  • State Synchronization: Proper data flow between all components
  • 93 |
  • Conditional Rendering: Quality slider hidden for PNG format
  • 94 |
  • Responsive Layout: Works on desktop and mobile devices
  • 95 |
  • Visual Hierarchy: Consistent styling matching existing patterns
  • 96 |
  • Component Lifecycle: Proper cleanup and state management
  • 97 |
  • Error States: User-friendly error messages and recovery
  • 98 |
  • Loading States: Disabled overlay during export process
  • 99 |
100 |
101 | 102 |
103 |

Requirements Coverage

104 |

Requirement 4.1: ✅ Export options displayed in clearly visible section

105 |

Requirement 4.3: ✅ Export controls visible when meme is generated

106 |

Requirement 4.4: ✅ Export controls disabled when no meme available

107 |

Requirement 1.4: ✅ UI updates when format is selected

108 |

Requirement 2.5: ✅ Real-time quality updates

109 |

Requirement 3.1: ✅ File size updates with format/quality changes

110 |
111 | 112 |
113 |

Component Props Interface

114 |
115 | interface ExportOptionsPanelProps {
116 |   memeImageSrc: string | null;  // Image source for export
117 |   isVisible: boolean;           // Panel visibility control
118 |   onExport: function | null;    // Callback for export completion
119 | }
120 |             
121 |
122 | 123 |
124 |

Mock Visual Preview

125 |
126 |
Sample Meme Image
127 |
128 |

129 | 130 | Export Options 131 |

132 |
133 |

📷 Export Format: PNG | JPEG | WebP

134 |

🎚️ Quality: 80% (Hidden for PNG)

135 |

⚖️ Estimated Size: 245 KB

136 |

⬇️ Export Button: Export as PNG

137 |
138 |
139 |
140 |
141 |
142 | 143 | -------------------------------------------------------------------------------- /src/components/ExportButton.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const ExportButton = ({ 4 | onExport, 5 | disabled = false, 6 | format = 'png', 7 | quality = 80, 8 | isExporting = false 9 | }) => { 10 | const [exportStatus, setExportStatus] = useState('idle'); // idle, exporting, success, error 11 | const [errorMessage, setErrorMessage] = useState(''); 12 | 13 | const handleExportClick = async () => { 14 | if (disabled || isExporting) return; 15 | 16 | try { 17 | setExportStatus('exporting'); 18 | setErrorMessage(''); 19 | 20 | await onExport(format, quality); 21 | 22 | setExportStatus('success'); 23 | 24 | // Reset to idle after showing success 25 | setTimeout(() => { 26 | setExportStatus('idle'); 27 | }, 2000); 28 | 29 | } catch (error) { 30 | console.error('Export failed:', error); 31 | setExportStatus('error'); 32 | 33 | // Don't show error message here since parent component handles notifications 34 | // Just show brief error state 35 | setErrorMessage('Export failed - see notification above'); 36 | 37 | // Reset to idle after showing error 38 | setTimeout(() => { 39 | setExportStatus('idle'); 40 | setErrorMessage(''); 41 | }, 3000); 42 | } 43 | }; 44 | 45 | const getButtonText = () => { 46 | switch (exportStatus) { 47 | case 'exporting': 48 | return 'Exporting...'; 49 | case 'success': 50 | return 'Downloaded!'; 51 | case 'error': 52 | return 'Export Failed'; 53 | default: 54 | return `Export as ${format.toUpperCase()}`; 55 | } 56 | }; 57 | 58 | const getButtonIcon = () => { 59 | switch (exportStatus) { 60 | case 'exporting': 61 | return 'fas fa-spinner fa-spin'; 62 | case 'success': 63 | return 'fas fa-check'; 64 | case 'error': 65 | return 'fas fa-exclamation-triangle'; 66 | default: 67 | return 'fas fa-download'; 68 | } 69 | }; 70 | 71 | const getButtonColor = () => { 72 | switch (exportStatus) { 73 | case 'success': 74 | return { 75 | background: 'linear-gradient(135deg, #48bb78 0%, #38a169 100%)', 76 | borderColor: '#38a169' 77 | }; 78 | case 'error': 79 | return { 80 | background: 'linear-gradient(135deg, #f56565 0%, #e53e3e 100%)', 81 | borderColor: '#e53e3e' 82 | }; 83 | default: 84 | return { 85 | background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', 86 | borderColor: '#667eea' 87 | }; 88 | } 89 | }; 90 | 91 | // CSS for responsive design 92 | const responsiveCSS = ` 93 | .export-button-container { 94 | margin-bottom: 16px; 95 | } 96 | 97 | .export-button { 98 | color: white; 99 | border: 2px solid; 100 | padding: 14px 24px; 101 | border-radius: 12px; 102 | cursor: pointer; 103 | font-size: 16px; 104 | font-weight: 600; 105 | transition: all 0.3s ease; 106 | display: flex; 107 | align-items: center; 108 | justify-content: center; 109 | gap: 10px; 110 | width: 100%; 111 | min-height: 52px; 112 | font-family: inherit; 113 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 114 | } 115 | 116 | .export-button:hover:not(.export-button-disabled) { 117 | transform: translateY(-2px); 118 | box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4); 119 | } 120 | 121 | .export-button:active:not(.export-button-disabled) { 122 | transform: translateY(0); 123 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); 124 | } 125 | 126 | .export-button-disabled { 127 | opacity: 0.6; 128 | cursor: not-allowed; 129 | box-shadow: none; 130 | transform: none; 131 | } 132 | 133 | .export-button-default { 134 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 135 | border-color: #667eea; 136 | } 137 | 138 | .export-button-success { 139 | background: linear-gradient(135deg, #48bb78 0%, #38a169 100%); 140 | border-color: #38a169; 141 | } 142 | 143 | .export-button-error { 144 | background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%); 145 | border-color: #e53e3e; 146 | } 147 | 148 | .export-button-content { 149 | display: flex; 150 | flex-direction: column; 151 | align-items: center; 152 | gap: 2px; 153 | } 154 | 155 | .export-button-quality-info { 156 | font-size: 12px; 157 | color: rgba(255, 255, 255, 0.8); 158 | } 159 | 160 | .export-button-error-message { 161 | margin-top: 8px; 162 | padding: 12px; 163 | background: #fed7d7; 164 | color: #c53030; 165 | border-radius: 6px; 166 | font-size: 14px; 167 | display: flex; 168 | align-items: center; 169 | gap: 8px; 170 | } 171 | 172 | /* Responsive breakpoints */ 173 | @media (max-width: 768px) { 174 | .export-button { 175 | padding: 12px 20px; 176 | font-size: 15px; 177 | min-height: 48px; 178 | gap: 8px; 179 | } 180 | 181 | .export-button-quality-info { 182 | font-size: 11px; 183 | } 184 | 185 | .export-button-error-message { 186 | padding: 10px; 187 | font-size: 13px; 188 | gap: 6px; 189 | } 190 | } 191 | 192 | @media (max-width: 480px) { 193 | .export-button { 194 | padding: 12px 16px; 195 | font-size: 14px; 196 | min-height: 44px; 197 | border-radius: 10px; 198 | } 199 | 200 | .export-button-content { 201 | gap: 1px; 202 | } 203 | 204 | .export-button-quality-info { 205 | font-size: 10px; 206 | } 207 | 208 | .export-button-error-message { 209 | padding: 8px 10px; 210 | font-size: 12px; 211 | gap: 4px; 212 | } 213 | } 214 | `; 215 | 216 | const getButtonClasses = () => { 217 | const baseClasses = ['export-button']; 218 | 219 | if (disabled || isExporting) { 220 | baseClasses.push('export-button-disabled'); 221 | } 222 | 223 | switch (exportStatus) { 224 | case 'success': 225 | baseClasses.push('export-button-success'); 226 | break; 227 | case 'error': 228 | baseClasses.push('export-button-error'); 229 | break; 230 | default: 231 | baseClasses.push('export-button-default'); 232 | break; 233 | } 234 | 235 | return baseClasses.join(' '); 236 | }; 237 | 238 | const styles = { 239 | container: { 240 | marginBottom: '16px' 241 | } 242 | }; 243 | 244 | return ( 245 | <> 246 | 247 |
248 | 262 | 263 | {errorMessage && ( 264 |
265 | 266 | {errorMessage} 267 |
268 | )} 269 |
270 | 271 | ); 272 | }; 273 | 274 | export default ExportButton; -------------------------------------------------------------------------------- /src/components/ExportOptionsPanel.css: -------------------------------------------------------------------------------- 1 | /* =========================================================== 2 | 🎨 Export Options Panel – Responsive Design & Styling 3 | Task 3.3: Add responsive design and styling 4 | =========================================================== */ 5 | 6 | /* ---------- 📦 Main Panel Container ---------- */ 7 | .export-options-panel { 8 | background: white; 9 | border-radius: 20px; 10 | padding: 30px; 11 | box-shadow: 0 10px 30px rgba(0,0,0,0.2); 12 | border: 1px solid #e2e8f0; 13 | margin-top: 20px; 14 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 15 | transition: all 0.3s ease; 16 | } 17 | 18 | .export-options-header { 19 | display: flex; 20 | align-items: center; 21 | margin-bottom: 24px; 22 | padding-bottom: 16px; 23 | border-bottom: 2px solid #e2e8f0; 24 | } 25 | 26 | .export-options-header-icon { 27 | font-size: 20px; 28 | color: #667eea; 29 | margin-right: 12px; 30 | } 31 | 32 | .export-options-header-title { 33 | font-size: 1.5rem; 34 | font-weight: 600; 35 | color: #4a5568; 36 | margin: 0; 37 | } 38 | 39 | /* ---------- 🎯 Format Selector Styles ---------- */ 40 | .format-selector-grid { 41 | display: grid; 42 | grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); 43 | gap: 12px; 44 | } 45 | 46 | .format-selector-button { 47 | padding: 16px 12px; 48 | border: 2px solid #e2e8f0; 49 | background: white; 50 | border-radius: 12px; 51 | cursor: pointer; 52 | transition: all 0.3s ease; 53 | text-align: center; 54 | font-size: 14px; 55 | display: flex; 56 | flex-direction: column; 57 | align-items: center; 58 | gap: 8px; 59 | min-height: 100px; 60 | font-family: inherit; 61 | } 62 | 63 | .format-selector-button:hover:not(.format-selector-button-disabled):not(.format-selector-button-selected) { 64 | border-color: #cbd5e0; 65 | transform: translateY(-2px); 66 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 67 | background-color: #f7fafc; 68 | } 69 | 70 | .format-selector-button-selected { 71 | border-color: #667eea; 72 | background: linear-gradient(135deg, #f7faff 0%, #edf2ff 100%); 73 | box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15); 74 | transform: translateY(-1px); 75 | } 76 | 77 | /* ---------- 🎚️ Quality Slider Styles ---------- */ 78 | .quality-slider-input { 79 | width: 100%; 80 | height: 8px; 81 | border-radius: 4px; 82 | background: linear-gradient(to right, 83 | #f56565 0%, 84 | #ecc94b 25%, 85 | #ed8936 50%, 86 | #48bb78 75%, 87 | #48bb78 100%); 88 | outline: none; 89 | cursor: pointer; 90 | transition: opacity 0.3s ease; 91 | -webkit-appearance: none; 92 | appearance: none; 93 | } 94 | 95 | .quality-slider-input::-webkit-slider-thumb { 96 | -webkit-appearance: none; 97 | appearance: none; 98 | height: 20px; 99 | width: 20px; 100 | border-radius: 50%; 101 | background: #667eea; 102 | cursor: pointer; 103 | border: 2px solid white; 104 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); 105 | transition: transform 0.2s ease; 106 | } 107 | 108 | .quality-slider-input::-webkit-slider-thumb:hover { 109 | transform: scale(1.1); 110 | } 111 | 112 | /* ---------- 📊 File Size Estimator Styles ---------- */ 113 | .file-size-estimator-display { 114 | padding: 12px 16px; 115 | background: #f7fafc; 116 | border: 2px solid #e2e8f0; 117 | border-radius: 8px; 118 | display: flex; 119 | justify-content: space-between; 120 | align-items: center; 121 | transition: all 0.3s ease; 122 | } 123 | 124 | /* ---------- 🔘 Export Button Styles ---------- */ 125 | .export-button { 126 | color: white; 127 | border: 2px solid; 128 | padding: 14px 24px; 129 | border-radius: 12px; 130 | cursor: pointer; 131 | font-size: 16px; 132 | font-weight: 600; 133 | transition: all 0.3s ease; 134 | display: flex; 135 | align-items: center; 136 | justify-content: center; 137 | gap: 10px; 138 | width: 100%; 139 | min-height: 52px; 140 | font-family: inherit; 141 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 142 | } 143 | 144 | .export-button:hover:not(.export-button-disabled) { 145 | transform: translateY(-2px); 146 | box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4); 147 | } 148 | 149 | .export-button-default { 150 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 151 | border-color: #667eea; 152 | } 153 | 154 | /* ---------- 📱 Responsive Breakpoints ---------- */ 155 | 156 | /* Tablet (≤1024px) */ 157 | @media (max-width: 1024px) { 158 | .export-options-panel { 159 | max-width: 800px; 160 | margin: 20px auto 0; 161 | } 162 | } 163 | 164 | /* Mobile (≤768px) */ 165 | @media (max-width: 768px) { 166 | .export-options-panel { 167 | padding: 20px; 168 | border-radius: 16px; 169 | margin-top: 16px; 170 | } 171 | 172 | .export-options-header-title { 173 | font-size: 1.25rem; 174 | } 175 | 176 | .format-selector-grid { 177 | grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); 178 | gap: 10px; 179 | } 180 | 181 | .format-selector-button { 182 | padding: 14px 10px; 183 | min-height: 90px; 184 | font-size: 13px; 185 | } 186 | 187 | .file-size-estimator-display { 188 | flex-direction: column; 189 | align-items: flex-start; 190 | gap: 8px; 191 | padding: 14px 16px; 192 | } 193 | 194 | .export-button { 195 | padding: 12px 20px; 196 | font-size: 15px; 197 | min-height: 48px; 198 | gap: 8px; 199 | } 200 | } 201 | 202 | /* Small Mobile (≤480px) */ 203 | @media (max-width: 480px) { 204 | .export-options-panel { 205 | padding: 16px; 206 | border-radius: 12px; 207 | margin: 16px 10px 0; 208 | } 209 | 210 | .export-options-header { 211 | margin-bottom: 20px; 212 | padding-bottom: 12px; 213 | } 214 | 215 | .export-options-header-icon { 216 | font-size: 18px; 217 | margin-right: 8px; 218 | } 219 | 220 | .export-options-header-title { 221 | font-size: 1.125rem; 222 | } 223 | 224 | .format-selector-grid { 225 | grid-template-columns: 1fr; 226 | gap: 8px; 227 | } 228 | 229 | .format-selector-button { 230 | flex-direction: row; 231 | text-align: left; 232 | min-height: 60px; 233 | padding: 12px 16px; 234 | gap: 12px; 235 | } 236 | 237 | .quality-slider-input::-webkit-slider-thumb { 238 | height: 24px; 239 | width: 24px; 240 | } 241 | 242 | .export-button { 243 | padding: 12px 16px; 244 | font-size: 14px; 245 | min-height: 44px; 246 | border-radius: 10px; 247 | } 248 | } 249 | 250 | /* ---------- 🎨 Visual Enhancements ---------- */ 251 | 252 | /* Smooth transitions for better UX */ 253 | .export-options-panel * { 254 | transition: all 0.3s ease; 255 | } 256 | 257 | /* Loading and disabled states */ 258 | .export-options-disabled { 259 | position: relative; 260 | opacity: 0.6; 261 | pointer-events: none; 262 | transition: opacity 0.3s ease; 263 | } 264 | 265 | /* Error message styling */ 266 | .export-options-error { 267 | background: #fed7d7; 268 | color: #c53030; 269 | padding: 15px; 270 | border-radius: 8px; 271 | margin-bottom: 16px; 272 | display: flex; 273 | align-items: center; 274 | gap: 8px; 275 | font-size: 14px; 276 | } 277 | 278 | /* Placeholder styling */ 279 | .export-options-placeholder { 280 | text-align: center; 281 | padding: 40px 20px; 282 | color: #a0aec0; 283 | font-size: 1.1rem; 284 | font-style: italic; 285 | border: 3px dashed #e2e8f0; 286 | border-radius: 12px; 287 | } 288 | 289 | /* ---------- 🔧 Utility Classes ---------- */ 290 | 291 | .export-options-content { 292 | display: flex; 293 | flex-direction: column; 294 | gap: 0; 295 | } 296 | 297 | /* Spinner animation */ 298 | @keyframes spin { 299 | 0% { transform: rotate(0deg); } 300 | 100% { transform: rotate(360deg); } 301 | } 302 | 303 | .file-size-estimator-spinner { 304 | width: 16px; 305 | height: 16px; 306 | border: 2px solid #e2e8f0; 307 | border-top: 2px solid #667eea; 308 | border-radius: 50%; 309 | animation: spin 1s linear infinite; 310 | } -------------------------------------------------------------------------------- /src/utils/__tests__/CanvasConverter.unit.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; 2 | import CanvasConverter from '../CanvasConverter.js'; 3 | 4 | describe('CanvasConverter - Unit Tests', () => { 5 | beforeEach(() => { 6 | vi.clearAllMocks(); 7 | CanvasConverter.clearCache(); 8 | }); 9 | 10 | afterEach(() => { 11 | CanvasConverter.clearCache(); 12 | }); 13 | 14 | describe('Format Support Detection', () => { 15 | it('should detect PNG format support', () => { 16 | const isSupported = CanvasConverter.isFormatSupported('png'); 17 | expect(typeof isSupported).toBe('boolean'); 18 | }); 19 | 20 | it('should detect JPEG format support', () => { 21 | const isSupported = CanvasConverter.isFormatSupported('jpeg'); 22 | expect(typeof isSupported).toBe('boolean'); 23 | }); 24 | 25 | it('should detect WebP format support', () => { 26 | const isSupported = CanvasConverter.isFormatSupported('webp'); 27 | expect(typeof isSupported).toBe('boolean'); 28 | }); 29 | 30 | it('should return false for unsupported formats', () => { 31 | const isSupported = CanvasConverter.isFormatSupported('gif'); 32 | expect(isSupported).toBe(false); 33 | }); 34 | 35 | it('should handle case insensitive format names', () => { 36 | const pngSupported = CanvasConverter.isFormatSupported('PNG'); 37 | const jpegSupported = CanvasConverter.isFormatSupported('JPEG'); 38 | 39 | expect(typeof pngSupported).toBe('boolean'); 40 | expect(typeof jpegSupported).toBe('boolean'); 41 | }); 42 | 43 | it('should handle invalid format inputs', () => { 44 | expect(CanvasConverter.isFormatSupported(null)).toBe(false); 45 | expect(CanvasConverter.isFormatSupported(undefined)).toBe(false); 46 | expect(CanvasConverter.isFormatSupported('')).toBe(false); 47 | expect(CanvasConverter.isFormatSupported(123)).toBe(false); 48 | }); 49 | }); 50 | 51 | describe('Supported Formats List', () => { 52 | it('should return array of supported formats', () => { 53 | const formats = CanvasConverter.getSupportedFormats(); 54 | 55 | expect(Array.isArray(formats)).toBe(true); 56 | expect(formats.length).toBeGreaterThanOrEqual(0); 57 | 58 | // Should contain at least PNG in most environments 59 | if (formats.length > 0) { 60 | expect(formats).toContain('png'); 61 | } 62 | }); 63 | }); 64 | 65 | describe('File Size Estimation', () => { 66 | it('should estimate file size from valid data URL', () => { 67 | const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='; 68 | const size = CanvasConverter.estimateFileSize(dataUrl); 69 | 70 | expect(typeof size).toBe('number'); 71 | expect(size).toBeGreaterThan(0); 72 | }); 73 | 74 | it('should throw error for invalid data URL', () => { 75 | expect(() => CanvasConverter.estimateFileSize('invalid-url')).toThrow(); 76 | expect(() => CanvasConverter.estimateFileSize('')).toThrow(); 77 | }); 78 | 79 | it('should handle data URL without base64 data', () => { 80 | expect(() => CanvasConverter.estimateFileSize('data:image/png;base64,')).toThrow(); 81 | }); 82 | 83 | it('should calculate size correctly for different data URLs', () => { 84 | const smallDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='; 85 | const largerDataUrl = 'data:image/png;base64,' + 'A'.repeat(100); 86 | 87 | const smallSize = CanvasConverter.estimateFileSize(smallDataUrl); 88 | const largerSize = CanvasConverter.estimateFileSize(largerDataUrl); 89 | 90 | expect(largerSize).toBeGreaterThan(smallSize); 91 | }); 92 | }); 93 | 94 | describe('Cache Management', () => { 95 | it('should provide cache statistics', () => { 96 | const stats = CanvasConverter.getCacheStats(); 97 | 98 | expect(stats).toHaveProperty('totalEntries'); 99 | expect(stats).toHaveProperty('memoryUsage'); 100 | expect(stats).toHaveProperty('entries'); 101 | expect(Array.isArray(stats.entries)).toBe(true); 102 | expect(typeof stats.totalEntries).toBe('number'); 103 | expect(typeof stats.memoryUsage).toBe('number'); 104 | }); 105 | 106 | it('should clear cache and return count', () => { 107 | const clearedCount = CanvasConverter.clearCache(); 108 | expect(typeof clearedCount).toBe('number'); 109 | expect(clearedCount).toBeGreaterThanOrEqual(0); 110 | }); 111 | 112 | it('should cleanup expired cache entries', () => { 113 | const cleanedCount = CanvasConverter.cleanupExpiredCache(); 114 | expect(typeof cleanedCount).toBe('number'); 115 | expect(cleanedCount).toBeGreaterThanOrEqual(0); 116 | }); 117 | 118 | it('should initialize cache cleanup without errors', () => { 119 | expect(() => CanvasConverter.initializeCacheCleanup(1000)).not.toThrow(); 120 | }); 121 | 122 | it('should track memory usage in cache stats', () => { 123 | const stats = CanvasConverter.getCacheStats(); 124 | 125 | expect(stats).toHaveProperty('memoryUsage'); 126 | expect(stats).toHaveProperty('maxMemoryUsage'); 127 | expect(stats).toHaveProperty('memoryUtilization'); 128 | expect(typeof stats.memoryUsage).toBe('number'); 129 | expect(typeof stats.maxMemoryUsage).toBe('number'); 130 | expect(typeof stats.memoryUtilization).toBe('number'); 131 | }); 132 | }); 133 | 134 | describe('Canvas Disposal', () => { 135 | it('should dispose canvas without errors', () => { 136 | const mockCanvas = { 137 | width: 100, 138 | height: 100, 139 | getContext: vi.fn(() => ({ clearRect: vi.fn() })) 140 | }; 141 | expect(() => CanvasConverter.disposeCanvas(mockCanvas)).not.toThrow(); 142 | }); 143 | 144 | it('should dispose multiple canvases without errors', () => { 145 | const canvases = [ 146 | { width: 100, height: 100, getContext: () => ({ clearRect: vi.fn() }) }, 147 | { width: 200, height: 200, getContext: () => ({ clearRect: vi.fn() }) } 148 | ]; 149 | expect(() => CanvasConverter.disposeCanvases(canvases)).not.toThrow(); 150 | }); 151 | 152 | it('should handle null canvas disposal gracefully', () => { 153 | expect(() => CanvasConverter.disposeCanvas(null)).not.toThrow(); 154 | expect(() => CanvasConverter.disposeCanvas(undefined)).not.toThrow(); 155 | expect(() => CanvasConverter.disposeCanvases([null, undefined])).not.toThrow(); 156 | }); 157 | }); 158 | 159 | describe('Error Handling', () => { 160 | it('should handle invalid input gracefully in format detection', () => { 161 | expect(() => CanvasConverter.isFormatSupported(null)).not.toThrow(); 162 | expect(() => CanvasConverter.isFormatSupported(undefined)).not.toThrow(); 163 | expect(() => CanvasConverter.isFormatSupported('')).not.toThrow(); 164 | }); 165 | 166 | it('should return false for invalid format inputs', () => { 167 | expect(CanvasConverter.isFormatSupported(null)).toBe(false); 168 | expect(CanvasConverter.isFormatSupported(undefined)).toBe(false); 169 | expect(CanvasConverter.isFormatSupported('')).toBe(false); 170 | expect(CanvasConverter.isFormatSupported(123)).toBe(false); 171 | }); 172 | }); 173 | 174 | describe('Performance Optimizations', () => { 175 | it('should cleanup cache when memory limit is reached', () => { 176 | const cleaned = CanvasConverter.cleanupExpiredCache(true); // Force cleanup 177 | expect(typeof cleaned).toBe('number'); 178 | }); 179 | 180 | it('should initialize cache cleanup system', () => { 181 | expect(() => CanvasConverter.initializeCacheCleanup(1000)).not.toThrow(); 182 | }); 183 | 184 | it('should provide cache statistics with detailed entries', () => { 185 | const stats = CanvasConverter.getCacheStats(); 186 | 187 | expect(Array.isArray(stats.entries)).toBe(true); 188 | expect(typeof stats.totalEntries).toBe('number'); 189 | expect(typeof stats.activeCanvases).toBe('number'); 190 | }); 191 | }); 192 | }); -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* ====== BREAKPOINTS ====== */ 3 | --bp-max-xs: 575px; 4 | --bp-min-sm: 576px; 5 | --bp-min-md: 768px; 6 | --bp-min-lg: 992px; 7 | --bp-min-xl: 1200px; 8 | --bp-min-xxl: 1400px; 9 | 10 | /* ====== COLORS - DARK THEME ====== */ 11 | --color-background: #000000ec; 12 | --color-background-navbar: #000000ec; 13 | --color-primary: #6fb7d7; 14 | --color-secondary: #8bf710ec; 15 | --color-tertiary: #0C356A; 16 | --color-text: #fff; 17 | --color-navbar-shadow-inset: rgba(63, 64, 65, 0.363); 18 | --color-navbar-shadow-glow: rgba(124, 215, 221, 0.22); 19 | 20 | /* ====== COLORS - LIGHT THEME ====== */ 21 | --color-background-light: #ffffff; 22 | --color-text-light: #000000; 23 | --color-primary-light: #2563eb; 24 | --color-secondary-light: #000000; 25 | --color-tertiary-light: #4a8cf7; 26 | --color-navbar-shadow-inset-light: rgba(0, 0, 0, 0.1); 27 | --color-navbar-shadow-glow-light: rgba(37, 99, 235, 0.4); 28 | 29 | /* ====== TYPOGRAPHY ====== */ 30 | --font-family-body: "Oxanium", cursive; 31 | --font-size-em: 1.2em; 32 | --font-size-mammoth: 2.488rem; 33 | --font-size-xhuge: 2.074rem; 34 | --font-size-huge: 1.728rem; 35 | --font-size-xlarge: 1.44rem; 36 | --font-size-large: 1.2rem; 37 | --font-size-normal: 1rem; 38 | --font-size-tiny: 0.579rem; 39 | 40 | --line-height: 1.5; 41 | --line-height-thin: 1.25; 42 | --font-weight-bold: 600; 43 | --font-weight-semibold: 500; 44 | --font-weight-normal: 400; 45 | --font-weight-light: 300; 46 | --font-weight-thin: 200; 47 | 48 | /* ====== TEMPLATE WIDTHS ====== */ 49 | --template-width-sm: 33.3333%; 50 | --template-width-md: 50%; 51 | --template-width-lg: 100%; 52 | } 53 | 54 | /* ====== LIGHT THEME ====== */ 55 | .light-theme { 56 | --color-background: var(--color-background-light); 57 | --color-text: var(--color-text-light); 58 | --color-primary: var(--color-primary-light); 59 | --color-secondary: var(--color-secondary-light); 60 | --color-tertiary: var(--color-tertiary-light); 61 | --color-navbar-shadow-inset: var(--color-navbar-shadow-inset-light); 62 | --color-navbar-shadow-glow: var(--color-navbar-shadow-glow-light); 63 | } 64 | 65 | /* ====== GLOBAL STYLES ====== */ 66 | * { 67 | margin: 0; 68 | padding: 0; 69 | box-sizing: border-box; 70 | } 71 | 72 | body { 73 | font-family: var(--font-family-body); 74 | background-color: var(--color-background); 75 | color: var(--color-text); 76 | transition: background-color 0.3s ease, color 0.3s ease; 77 | } 78 | 79 | h2 { 80 | font-size: var(--font-size-xhuge); 81 | color: var(--color-text); 82 | text-align: center; 83 | margin: 10px; 84 | } 85 | 86 | /* ====== TEMPLATE (MEME CARD) ====== */ 87 | .template { 88 | width: var(--template-width-sm); 89 | display: inline-block; 90 | border: 20px solid transparent; 91 | position: relative; 92 | overflow: hidden; 93 | border-radius: 10px; 94 | transition: transform 0.3s ease; 95 | } 96 | 97 | .template .meme { 98 | width: 100%; 99 | height: 350px; 100 | background-size: cover; 101 | background-position: center; 102 | border-radius: 10px; 103 | cursor: pointer; 104 | transition: all 0.3s ease; 105 | } 106 | 107 | .template .meme:hover { 108 | filter: grayscale(100%); 109 | transform: scale(1.05); 110 | } 111 | 112 | /* Hover caption overlay */ 113 | .hover-caption { 114 | position: absolute; 115 | bottom: 0; 116 | left: 0; 117 | width: 100%; 118 | padding: 8px; 119 | background: rgba(0, 0, 0, 0.6); 120 | color: var(--color-text); 121 | text-align: center; 122 | font-size: var(--font-size-large); 123 | opacity: 0; 124 | transition: opacity 0.3s ease; 125 | } 126 | 127 | .template:hover .hover-caption { 128 | opacity: 1; 129 | } 130 | 131 | /* ====== NAVBAR ====== */ 132 | .navbar { 133 | background-color: var(--color-background-navbar); 134 | display: flex; 135 | justify-content: space-between; 136 | align-items: center; 137 | padding: 16px 40px; 138 | margin: 15px auto 40px; 139 | width: 90%; 140 | border-radius: 50px; 141 | box-shadow: 142 | inset var(--color-navbar-shadow-inset) -10px -14px 16px, 143 | var(--color-navbar-shadow-glow) 1px -2px 20px 20px; 144 | color: var(--color-text); 145 | position: sticky; 146 | top: 0; 147 | z-index: 100; 148 | transition: all 0.3s ease; 149 | 150 | } 151 | 152 | .logo { 153 | font-weight: bold; 154 | font-size: 2rem; 155 | color: var(--color-text); 156 | } 157 | 158 | .nav-links { 159 | list-style: none; 160 | display: flex; 161 | align-items: center; 162 | gap: 25px; 163 | } 164 | 165 | .nav-links a { 166 | text-decoration: none; 167 | color: var(--color-text); 168 | font-size: var(--font-size-large); 169 | transition: color 0.2s ease; 170 | } 171 | 172 | .nav-links a:hover { 173 | color: var(--color-primary); 174 | } 175 | 176 | /* ====== THEME TOGGLE ====== */ 177 | .theme-toggle { 178 | background: transparent; 179 | border: 2px solid var(--color-text); 180 | color: var(--color-text); 181 | padding: 8px 16px; 182 | border-radius: 20px; 183 | cursor: pointer; 184 | font-size: var(--font-size-normal); 185 | font-weight: var(--font-weight-semibold); 186 | transition: all 0.3s ease; 187 | min-width: 100px; 188 | text-align: center; 189 | } 190 | 191 | .theme-toggle:hover { 192 | background-color: var(--color-primary); 193 | color: var(--color-background); 194 | transform: scale(1.05); 195 | } 196 | 197 | /* ====== INPUT & BUTTONS ====== */ 198 | input { 199 | width: 250px; 200 | height: 40px; 201 | margin: 10px 5px; 202 | background: transparent; 203 | border: none; 204 | border-bottom: 1px solid var(--color-text); 205 | color: var(--color-text); 206 | font-size: var(--font-size-large); 207 | text-align: center; 208 | transition: border-color 0.3s ease; 209 | } 210 | 211 | input:focus { 212 | outline: none; 213 | border-bottom: 2px solid var(--color-secondary); 214 | } 215 | 216 | ::placeholder { 217 | color: var(--color-secondary); 218 | opacity: 0.7; 219 | } 220 | 221 | .generatebutton, 222 | .backbtn { 223 | font-size: var(--font-size-large); 224 | padding: 10px 20px; 225 | border-radius: 10px; 226 | color: var(--color-secondary); 227 | background-color: var(--color-tertiary); 228 | border: 1px solid var(--color-secondary); 229 | cursor: pointer; 230 | transition: all 0.3s ease; 231 | } 232 | 233 | .generatebutton:hover, 234 | .backbtn:hover { 235 | background-color: var(--color-secondary); 236 | color: var(--color-tertiary); 237 | transform: translateY(-2px); 238 | } 239 | 240 | /* ====== PAGINATION ====== */ 241 | .pagination { 242 | display: flex; 243 | align-items: center; 244 | justify-content: center; 245 | margin: 2rem 0 5rem; 246 | flex-wrap: wrap; 247 | gap: 5px; 248 | } 249 | 250 | .pagination button { 251 | background: var(--color-background); 252 | color: var(--color-text); 253 | padding: 6px 12px; 254 | border: 1px solid var(--color-text); 255 | border-radius: 6px; 256 | cursor: pointer; 257 | transition: all 0.3s ease; 258 | } 259 | 260 | .pagination button:hover { 261 | background: var(--color-primary); 262 | color: var(--color-background); 263 | transform: scale(1.05); 264 | } 265 | 266 | /* ====== FOOTER ====== */ 267 | .footer { 268 | background-color: var(--color-background); 269 | color: var(--color-text); 270 | padding: 12px 0; 271 | text-align: center; 272 | transition: all 0.3s ease; 273 | } 274 | 275 | /* ====== RESPONSIVE ====== */ 276 | @media (max-width: 1200px) { 277 | .template { 278 | width: 50%; 279 | } 280 | } 281 | 282 | @media (max-width: 768px) { 283 | .template { 284 | width: 100%; 285 | border: 10px solid transparent; 286 | } 287 | 288 | .navbar { 289 | flex-direction: column; 290 | width: 95%; 291 | padding: 12px 20px; 292 | } 293 | 294 | .logo { 295 | display: none; 296 | } 297 | 298 | .nav-links { 299 | gap: 15px; 300 | flex-wrap: wrap; 301 | justify-content: center; 302 | } 303 | 304 | .theme-toggle { 305 | font-size: var(--font-size-tiny); 306 | min-width: 80px; 307 | padding: 6px 10px; 308 | } 309 | 310 | .memebnao img { 311 | width: 100%; 312 | height: auto; 313 | max-height: 300px; 314 | } 315 | 316 | .btns { 317 | flex-direction: column; 318 | gap: 12px; 319 | } 320 | 321 | .generatebutton, .backbtn { 322 | width: 90%; 323 | max-width: 200px; 324 | } 325 | 326 | .pagination { 327 | margin-bottom: 20px; 328 | } 329 | } 330 | 331 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * Error Boundary Component 5 | * Catches JavaScript errors in the export system and provides fallback UI 6 | */ 7 | class ErrorBoundary extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | hasError: false, 12 | error: null, 13 | errorInfo: null, 14 | errorId: null 15 | }; 16 | } 17 | 18 | static getDerivedStateFromError(error) { 19 | // Update state so the next render will show the fallback UI 20 | return { 21 | hasError: true, 22 | errorId: Date.now() + Math.random() 23 | }; 24 | } 25 | 26 | componentDidCatch(error, errorInfo) { 27 | // Log error details 28 | console.error('Export system error caught by boundary:', error, errorInfo); 29 | 30 | this.setState({ 31 | error: error, 32 | errorInfo: errorInfo 33 | }); 34 | 35 | // Report error to monitoring service if available 36 | if (window.reportError) { 37 | window.reportError(error, { 38 | component: 'ExportSystem', 39 | errorInfo: errorInfo, 40 | timestamp: new Date().toISOString() 41 | }); 42 | } 43 | } 44 | 45 | handleRetry = () => { 46 | this.setState({ 47 | hasError: false, 48 | error: null, 49 | errorInfo: null, 50 | errorId: null 51 | }); 52 | }; 53 | 54 | handleReload = () => { 55 | window.location.reload(); 56 | }; 57 | 58 | render() { 59 | if (this.state.hasError) { 60 | const errorBoundaryCSS = ` 61 | .error-boundary { 62 | background: white; 63 | border-radius: 20px; 64 | padding: 40px; 65 | text-align: center; 66 | box-shadow: 0 10px 30px rgba(0,0,0,0.2); 67 | border: 1px solid #fed7d7; 68 | margin-top: 20px; 69 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 70 | } 71 | 72 | .error-boundary-icon { 73 | font-size: 48px; 74 | color: #e53e3e; 75 | margin-bottom: 20px; 76 | } 77 | 78 | .error-boundary-title { 79 | font-size: 24px; 80 | font-weight: 600; 81 | color: #2d3748; 82 | margin: 0 0 16px 0; 83 | } 84 | 85 | .error-boundary-message { 86 | font-size: 16px; 87 | color: #4a5568; 88 | margin: 0 0 24px 0; 89 | line-height: 1.6; 90 | } 91 | 92 | .error-boundary-actions { 93 | display: flex; 94 | gap: 12px; 95 | justify-content: center; 96 | flex-wrap: wrap; 97 | } 98 | 99 | .error-boundary-button { 100 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 101 | color: white; 102 | border: none; 103 | padding: 12px 24px; 104 | border-radius: 10px; 105 | cursor: pointer; 106 | font-size: 14px; 107 | font-weight: 500; 108 | transition: all 0.3s ease; 109 | font-family: inherit; 110 | } 111 | 112 | .error-boundary-button:hover { 113 | transform: translateY(-2px); 114 | box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4); 115 | } 116 | 117 | .error-boundary-button-secondary { 118 | background: none; 119 | color: #4a5568; 120 | border: 2px solid #e2e8f0; 121 | } 122 | 123 | .error-boundary-button-secondary:hover { 124 | background: #f7fafc; 125 | border-color: #cbd5e0; 126 | box-shadow: none; 127 | } 128 | 129 | .error-boundary-details { 130 | margin-top: 24px; 131 | padding: 16px; 132 | background: #f7fafc; 133 | border-radius: 8px; 134 | text-align: left; 135 | font-size: 12px; 136 | color: #718096; 137 | max-height: 200px; 138 | overflow-y: auto; 139 | } 140 | 141 | .error-boundary-details-toggle { 142 | background: none; 143 | border: none; 144 | color: #667eea; 145 | cursor: pointer; 146 | font-size: 14px; 147 | text-decoration: underline; 148 | margin-top: 16px; 149 | } 150 | 151 | .error-boundary-suggestions { 152 | background: #e6fffa; 153 | border: 1px solid #81e6d9; 154 | border-radius: 8px; 155 | padding: 16px; 156 | margin-top: 20px; 157 | text-align: left; 158 | } 159 | 160 | .error-boundary-suggestions-title { 161 | font-weight: 600; 162 | color: #234e52; 163 | margin: 0 0 8px 0; 164 | font-size: 14px; 165 | } 166 | 167 | .error-boundary-suggestions-list { 168 | margin: 0; 169 | padding-left: 20px; 170 | color: #285e61; 171 | font-size: 13px; 172 | } 173 | 174 | .error-boundary-suggestions-list li { 175 | margin-bottom: 4px; 176 | } 177 | 178 | /* Responsive design */ 179 | @media (max-width: 768px) { 180 | .error-boundary { 181 | padding: 30px 20px; 182 | border-radius: 16px; 183 | } 184 | 185 | .error-boundary-icon { 186 | font-size: 40px; 187 | } 188 | 189 | .error-boundary-title { 190 | font-size: 20px; 191 | } 192 | 193 | .error-boundary-message { 194 | font-size: 15px; 195 | } 196 | 197 | .error-boundary-actions { 198 | flex-direction: column; 199 | align-items: center; 200 | } 201 | 202 | .error-boundary-button { 203 | width: 100%; 204 | max-width: 200px; 205 | } 206 | } 207 | 208 | @media (max-width: 480px) { 209 | .error-boundary { 210 | padding: 24px 16px; 211 | margin: 16px 10px 0; 212 | } 213 | 214 | .error-boundary-icon { 215 | font-size: 36px; 216 | } 217 | 218 | .error-boundary-title { 219 | font-size: 18px; 220 | } 221 | 222 | .error-boundary-message { 223 | font-size: 14px; 224 | } 225 | } 226 | `; 227 | 228 | return ( 229 | <> 230 | 231 |
232 |
233 | 234 |
235 | 236 |

237 | Export System Error 238 |

239 | 240 |

241 | Something went wrong with the export system. This is usually a temporary issue. 242 |

243 | 244 |
245 |
246 | Try these solutions: 247 |
248 |
    249 |
  • Click "Try Again" to retry the operation
  • 250 |
  • Refresh the page if the problem persists
  • 251 |
  • Try using a different browser or device
  • 252 |
  • Check your internet connection
  • 253 |
  • Clear your browser cache and cookies
  • 254 |
255 |
256 | 257 |
258 | 264 | 265 | 271 |
272 | 273 | {process.env.NODE_ENV === 'development' && ( 274 | <> 275 | 281 | 282 | {this.state.showDetails && ( 283 |
284 | Error: {this.state.error && this.state.error.toString()} 285 |

286 | Stack Trace: 287 |
{this.state.errorInfo.componentStack}
288 |
289 | )} 290 | 291 | )} 292 |
293 | 294 | ); 295 | } 296 | 297 | return this.props.children; 298 | } 299 | } 300 | 301 | /** 302 | * Higher-order component to wrap components with error boundary 303 | */ 304 | export const withErrorBoundary = (Component, errorBoundaryProps = {}) => { 305 | const WrappedComponent = (props) => ( 306 | 307 | 308 | 309 | ); 310 | 311 | WrappedComponent.displayName = `withErrorBoundary(${Component.displayName || Component.name})`; 312 | 313 | return WrappedComponent; 314 | }; 315 | 316 | export default ErrorBoundary; -------------------------------------------------------------------------------- /meme-bot/dynamic_meme_generator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Dynamic Meme Generator 4 | A user-friendly script to generate memes on various topics without needing JSON files. 5 | Supports diverse topics including youth issues, world events, technology, and more. 6 | """ 7 | 8 | from meme_generator_clean import MemeGeneratorClean 9 | import random 10 | 11 | def display_welcome(): 12 | """Display welcome message and instructions.""" 13 | print("\n" + "="*60) 14 | print("🎭 DYNAMIC MEME GENERATOR 🎭") 15 | print("="*60) 16 | print("Generate hilarious memes on ANY topic!") 17 | print("From youth problems to world events, technology to relationships!") 18 | print("="*60) 19 | 20 | def get_topic_categories(): 21 | """Return predefined topic categories with examples.""" 22 | return { 23 | "1": { 24 | "name": "Youth & Gen Z Issues", 25 | "examples": [ 26 | "Gen Z job interview expectations vs reality", 27 | "Social media addiction among youth", 28 | "Online classes vs offline experience", 29 | "Gig economy and side hustles struggle", 30 | "Mental health awareness in young generation" 31 | ] 32 | }, 33 | "2": { 34 | "name": "World Current Events", 35 | "examples": [ 36 | "AI taking over jobs but can't cook like mom", 37 | "Climate change activists vs daily lifestyle", 38 | "Cryptocurrency investment vs traditional savings", 39 | "Remote work culture post-pandemic", 40 | "Social media influencing real-world events" 41 | ] 42 | }, 43 | "3": { 44 | "name": "Indian Society & Culture", 45 | "examples": [ 46 | "Traditional Indian parents vs modern kids", 47 | "Festival celebrations in metro cities vs villages", 48 | "Arranged marriage in digital age", 49 | "Regional language vs English preference", 50 | "Indian food culture vs fast food adoption" 51 | ] 52 | }, 53 | "4": { 54 | "name": "Technology & AI", 55 | "examples": [ 56 | "ChatGPT helping with homework", 57 | "Instagram reality vs actual life", 58 | "Online shopping vs physical store experience", 59 | "Smartphone addiction and real conversations", 60 | "Video call meetings and technical difficulties" 61 | ] 62 | }, 63 | "5": { 64 | "name": "Relationships & Dating", 65 | "examples": [ 66 | "Modern dating apps vs traditional meetings", 67 | "Long distance relationships in digital age", 68 | "Social media affecting real relationships", 69 | "Friendship in online vs offline world", 70 | "Dating expectations vs reality in 2024" 71 | ] 72 | }, 73 | "6": { 74 | "name": "Education & Career", 75 | "examples": [ 76 | "College placement season stress", 77 | "Skill development vs degree importance", 78 | "Internship expectations vs reality", 79 | "Work from home vs office culture", 80 | "Student loan burden and career choices" 81 | ] 82 | }, 83 | "7": { 84 | "name": "Finance & Economy", 85 | "examples": [ 86 | "Salary expectations vs actual paycheck", 87 | "Inflation affecting daily life choices", 88 | "Investment advice vs actual market reality", 89 | "EMI culture and financial planning", 90 | "Savings goals vs online shopping temptations" 91 | ] 92 | }, 93 | "8": { 94 | "name": "Health & Lifestyle", 95 | "examples": [ 96 | "Fitness influencer advice vs gym reality", 97 | "Diet plans vs food delivery apps", 98 | "Mental health awareness vs societal stigma", 99 | "Work-life balance in modern times", 100 | "Health consciousness vs junk food addiction" 101 | ] 102 | } 103 | } 104 | 105 | def display_categories(): 106 | """Display all available topic categories.""" 107 | categories = get_topic_categories() 108 | print("\n📱 Available Topic Categories:") 109 | print("-" * 40) 110 | 111 | for key, category in categories.items(): 112 | print(f"{key}. {category['name']}") 113 | 114 | print("9. Custom Topic (Enter your own)") 115 | print("0. Random Selection (Surprise me!)") 116 | 117 | def get_user_choice(): 118 | """Get and validate user's category choice.""" 119 | categories = get_topic_categories() 120 | 121 | while True: 122 | try: 123 | choice = input("\nSelect a category (0-9): ").strip() 124 | 125 | if choice in categories: 126 | return choice, categories[choice] 127 | elif choice == "9": 128 | return "9", {"name": "Custom Topic", "examples": []} 129 | elif choice == "0": 130 | return "0", {"name": "Random Selection", "examples": []} 131 | else: 132 | print("❌ Invalid choice! Please select a number between 0-9.") 133 | 134 | except KeyboardInterrupt: 135 | print("\n\nGoodbye! 👋") 136 | return None, None 137 | 138 | def get_topic_from_category(category_data): 139 | """Get specific topic from selected category.""" 140 | if not category_data: 141 | return None, None 142 | 143 | category_name = category_data["name"] 144 | examples = category_data["examples"] 145 | 146 | if category_name == "Custom Topic": 147 | topic = input("\n💭 Enter your custom meme topic: ").strip() 148 | context = input("📝 Enter additional context (optional): ").strip() 149 | return topic if topic else "Random daily life", context if context else None 150 | 151 | elif category_name == "Random Selection": 152 | all_categories = get_topic_categories() 153 | random_category = random.choice(list(all_categories.values())) 154 | random_topic = random.choice(random_category["examples"]) 155 | context = f"{random_category['name'].lower()}" 156 | print(f"\n🎲 Random topic selected: {random_topic}") 157 | return random_topic, context 158 | 159 | else: 160 | print(f"\n📋 {category_name} - Example Topics:") 161 | print("-" * 50) 162 | for i, example in enumerate(examples, 1): 163 | print(f"{i}. {example}") 164 | 165 | print(f"{len(examples) + 1}. Enter my own topic for this category") 166 | 167 | try: 168 | topic_choice = input(f"\nSelect a topic (1-{len(examples) + 1}): ").strip() 169 | 170 | if topic_choice.isdigit(): 171 | choice_num = int(topic_choice) 172 | if 1 <= choice_num <= len(examples): 173 | selected_topic = examples[choice_num - 1] 174 | context = category_name.lower() 175 | return selected_topic, context 176 | elif choice_num == len(examples) + 1: 177 | custom_topic = input(f"\n💭 Enter your {category_name.lower()} topic: ").strip() 178 | context = category_name.lower() 179 | return custom_topic if custom_topic else examples[0], context 180 | 181 | # If invalid input, return first example 182 | print("⚠️ Invalid selection, using first example topic.") 183 | return examples[0], category_name.lower() 184 | 185 | except (ValueError, IndexError): 186 | print("⚠️ Invalid input, using first example topic.") 187 | return examples[0], category_name.lower() 188 | 189 | def generate_meme_interactive(): 190 | """Main interactive meme generation function.""" 191 | display_welcome() 192 | 193 | try: 194 | generator = MemeGeneratorClean() 195 | print("✅ Meme generator initialized successfully!") 196 | 197 | while True: 198 | display_categories() 199 | choice, category_data = get_user_choice() 200 | 201 | if not choice: # User pressed Ctrl+C 202 | break 203 | 204 | topic, context = get_topic_from_category(category_data) 205 | if not topic: 206 | continue 207 | 208 | print(f"\n🎨 Generating meme for: '{topic}'") 209 | if context: 210 | print(f"📝 Context: {context}") 211 | 212 | print("\n⏳ Creating your meme... (this may take a few seconds)") 213 | 214 | filename = generator.create_meme(topic, context) 215 | 216 | if filename: 217 | print("\n🎉 SUCCESS! Your meme has been generated!") 218 | print(f"📁 Saved as: {filename}") 219 | print("✨ Check the 'memes' folder to view your creation!") 220 | else: 221 | print("\n❌ Oops! Failed to generate meme. Please try again.") 222 | 223 | # Ask if user wants to generate another meme 224 | try: 225 | another = input("\n🔄 Generate another meme? (y/n): ").strip().lower() 226 | if another not in ['y', 'yes']: 227 | break 228 | except KeyboardInterrupt: 229 | break 230 | 231 | print("\n👋 Thanks for using the Dynamic Meme Generator!") 232 | print("🎭 Keep creating and sharing awesome memes!") 233 | 234 | except Exception as e: 235 | print(f"\n❌ Error initializing meme generator: {e}") 236 | print("Please check your environment setup and try again.") 237 | 238 | if __name__ == "__main__": 239 | generate_meme_interactive() -------------------------------------------------------------------------------- /meme-bot/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify, send_file, render_template 2 | from flask_cors import CORS 3 | import os 4 | import json 5 | from meme_generator_clean import MemeGeneratorClean 6 | import random 7 | from datetime import datetime 8 | 9 | # ----------------------------- 10 | # Initialize Flask application 11 | # ----------------------------- 12 | app = Flask(__name__) 13 | 14 | # Enable Cross-Origin Resource Sharing (CORS) for all routes. 15 | # This allows your frontend (running on another domain/port) to call these APIs. 16 | CORS(app) 17 | 18 | # ----------------------------- 19 | # Global Meme Generator 20 | # ----------------------------- 21 | meme_generator = None # Will hold an instance of MemeGeneratorClean 22 | 23 | def initialize_generator(): 24 | """ 25 | Initialize the meme generator with error handling. 26 | Returns True if initialization succeeds, False otherwise. 27 | """ 28 | global meme_generator 29 | try: 30 | meme_generator = MemeGeneratorClean() # Create meme generator instance 31 | return True 32 | except Exception as e: 33 | # Print error to console (helps debugging initialization issues) 34 | print(f"Error initializing meme generator: {e}") 35 | return False 36 | 37 | # ----------------------------- 38 | # Routes 39 | # ----------------------------- 40 | 41 | @app.route('/') 42 | def index(): 43 | """ 44 | Serve the main web interface. 45 | Renders index.html from the templates folder. 46 | """ 47 | return render_template('index.html') 48 | 49 | @app.route('/api/categories', methods=['GET']) 50 | def get_categories(): 51 | """ 52 | API endpoint to get all available meme topic categories. 53 | Returns a JSON object containing categories or an error message. 54 | """ 55 | try: 56 | # Ensure meme generator is initialized 57 | if not meme_generator: 58 | if not initialize_generator(): 59 | return jsonify({"error": "Failed to initialize meme generator"}), 500 60 | 61 | categories = meme_generator.get_topic_categories() 62 | return jsonify({"success": True, "categories": categories}) 63 | except Exception as e: 64 | return jsonify({"error": str(e)}), 500 65 | 66 | @app.route('/api/generate', methods=['POST']) 67 | def generate_meme(): 68 | """ 69 | API endpoint to generate a meme based on user input. 70 | Request JSON should include: 71 | - topic (str): meme topic 72 | - context (str, optional): extra context for AI text generation 73 | - custom_top_text, custom_bottom_text (str, optional): user-defined meme texts 74 | - template_url, template_id (str, optional): custom meme template 75 | """ 76 | try: 77 | # Ensure meme generator is initialized 78 | if not meme_generator: 79 | if not initialize_generator(): 80 | return jsonify({"error": "Failed to initialize meme generator"}), 500 81 | 82 | data = request.get_json() # Parse JSON body 83 | 84 | if not data: 85 | return jsonify({"error": "No data provided"}), 400 86 | 87 | # Extract input parameters 88 | topic = data.get('topic', '').strip() 89 | context = data.get('context', '').strip() 90 | custom_top_text = data.get('custom_top_text', '').strip() 91 | custom_bottom_text = data.get('custom_bottom_text', '').strip() 92 | template_url = data.get('template_url', '').strip() 93 | template_id = data.get('template_id', '').strip() 94 | 95 | # Require either a topic or both custom texts 96 | if not topic and not (custom_top_text and custom_bottom_text): 97 | return jsonify({"error": "Topic is required or both custom texts must be provided"}), 400 98 | 99 | # Generate meme 100 | if custom_top_text and custom_bottom_text: 101 | # Use user-provided custom text 102 | result = meme_generator.create_meme_with_custom_text( 103 | topic if topic else "Custom Meme", # fallback topic 104 | custom_top_text, 105 | custom_bottom_text, 106 | context, 107 | template_url if template_url else None, 108 | template_id if template_id else None 109 | ) 110 | else: 111 | # Use AI to generate meme text 112 | result = meme_generator.create_meme(topic, context if context else None) 113 | 114 | if result['success']: 115 | # Successful response with meme details 116 | return jsonify({ 117 | "success": True, 118 | "filename": os.path.basename(result['filename']), 119 | "filepath": result['filename'], 120 | "top_text": result['top_text'], 121 | "bottom_text": result['bottom_text'], 122 | "topic": result['topic'], 123 | "context": result.get('context'), 124 | "timestamp": result['timestamp'] 125 | }) 126 | else: 127 | return jsonify({ 128 | "success": False, 129 | "error": result['error'] 130 | }), 500 131 | except Exception as e: 132 | return jsonify({"error": str(e)}), 500 133 | 134 | @app.route('/api/random', methods=['POST']) 135 | def generate_random_meme(): 136 | """ 137 | API endpoint to generate a random meme from predefined topics. 138 | Randomly selects a category and topic to generate the meme. 139 | """ 140 | try: 141 | # Ensure meme generator is initialized 142 | if not meme_generator: 143 | if not initialize_generator(): 144 | return jsonify({"error": "Failed to initialize meme generator"}), 500 145 | 146 | # Get categories and select random topic 147 | categories = meme_generator.get_topic_categories() 148 | random_category_key = random.choice(list(categories.keys())) 149 | random_category = categories[random_category_key] 150 | random_topic = random.choice(random_category['examples']) 151 | 152 | # Generate meme 153 | result = meme_generator.create_meme(random_topic, random_category['name'].lower()) 154 | 155 | if result['success']: 156 | return jsonify({ 157 | "success": True, 158 | "filename": os.path.basename(result['filename']), 159 | "filepath": result['filename'], 160 | "top_text": result['top_text'], 161 | "bottom_text": result['bottom_text'], 162 | "topic": result['topic'], 163 | "context": result.get('context'), 164 | "timestamp": result['timestamp'], 165 | "category": random_category['name'] 166 | }) 167 | else: 168 | return jsonify({ 169 | "success": False, 170 | "error": result['error'] 171 | }), 500 172 | except Exception as e: 173 | return jsonify({"error": str(e)}), 500 174 | 175 | @app.route('/api/download/') 176 | def download_meme(filename): 177 | """ 178 | API endpoint to download a generated meme as an image file. 179 | Security check ensures only files from the 'memes' directory are accessed. 180 | """ 181 | try: 182 | safe_filename = os.path.basename(filename) # prevent directory traversal 183 | file_path = os.path.join('memes', safe_filename) 184 | 185 | if not os.path.exists(file_path): 186 | return jsonify({"error": "File not found"}), 404 187 | 188 | return send_file( 189 | file_path, 190 | as_attachment=True, 191 | download_name=safe_filename, 192 | mimetype='image/png' 193 | ) 194 | except Exception as e: 195 | return jsonify({"error": str(e)}), 500 196 | 197 | @app.route('/api/view/') 198 | def view_meme(filename): 199 | """ 200 | API endpoint to view a generated meme directly in the browser. 201 | Security check ensures only files from the 'memes' directory are accessed. 202 | """ 203 | try: 204 | safe_filename = os.path.basename(filename) # prevent directory traversal 205 | file_path = os.path.join('memes', safe_filename) 206 | 207 | if not os.path.exists(file_path): 208 | return jsonify({"error": "File not found"}), 404 209 | 210 | return send_file(file_path, mimetype='image/png') 211 | except Exception as e: 212 | return jsonify({"error": str(e)}), 500 213 | 214 | @app.route('/api/health', methods=['GET']) 215 | def health_check(): 216 | """ 217 | Health check endpoint to verify the server and meme generator status. 218 | Useful for monitoring or automated uptime checks. 219 | """ 220 | try: 221 | if not meme_generator: 222 | if not initialize_generator(): 223 | return jsonify({ 224 | "status": "unhealthy", 225 | "error": "Meme generator not initialized" 226 | }), 500 227 | 228 | return jsonify({ 229 | "status": "healthy", 230 | "timestamp": datetime.now().isoformat(), 231 | "generator_ready": meme_generator is not None 232 | }) 233 | except Exception as e: 234 | return jsonify({ 235 | "status": "unhealthy", 236 | "error": str(e) 237 | }), 500 238 | 239 | # ----------------------------- 240 | # Error Handlers 241 | # ----------------------------- 242 | 243 | @app.errorhandler(404) 244 | def not_found(error): 245 | """Handle 404 errors for undefined endpoints.""" 246 | return jsonify({"error": "Endpoint not found"}), 404 247 | 248 | @app.errorhandler(500) 249 | def internal_error(error): 250 | """Handle 500 errors for server exceptions.""" 251 | return jsonify({"error": "Internal server error"}), 500 252 | 253 | # ----------------------------- 254 | # App Entry Point 255 | # ----------------------------- 256 | if __name__ == '__main__': 257 | # Print startup messages 258 | print("🚀 Starting Meme Generator Web App...") 259 | 260 | if initialize_generator(): 261 | print("✅ Meme generator initialized successfully!") 262 | else: 263 | print("❌ Failed to initialize meme generator. Check your environment setup.") 264 | 265 | print("🌐 Starting Flask server...") 266 | print("📱 Open your browser and go to: http://localhost:5000") 267 | 268 | # Start Flask server 269 | app.run(debug=True, host='0.0.0.0', port=5000) 270 | -------------------------------------------------------------------------------- /src/components/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | import Navbar from "./Navbar"; 4 | import Temp from "../Temp"; 5 | import Meme from "../Meme"; 6 | import Footer from "./Footer"; 7 | import "../style.css"; 8 | import "../index.css"; 9 | 10 | const Home = () => { 11 | const [temp, setTemp] = useState([]); 12 | const [meme, setMeme] = useState(null); 13 | const [searchQuery, setSearchQuery] = useState(""); 14 | const [currentPage, setCurrentPage] = useState(1); 15 | const [selectedCategory, setSelectedCategory] = useState('all'); 16 | const [isLoading, setIsLoading] = useState(true); 17 | const [error, setError] = useState(null); 18 | 19 | const [itemsPerPage] = useState(18); // Fixed 18 items per page 20 | 21 | 22 | 23 | useEffect(() => { 24 | setIsLoading(true); 25 | fetch("https://api.imgflip.com/get_memes") 26 | .then((res) => { 27 | if (!res.ok) throw new Error('Failed to load memes'); 28 | return res.json(); 29 | }) 30 | .then((data) => { 31 | setTemp(data.data.memes); 32 | setIsLoading(false); 33 | }) 34 | .catch((err) => { 35 | setError(err.message); 36 | setIsLoading(false); 37 | }); 38 | }, []); 39 | 40 | // Enhanced filtering with categories 41 | const filteredMemes = temp.filter((meme) => { 42 | const matchesSearch = meme.name.toLowerCase().includes(searchQuery.toLowerCase()); 43 | 44 | if (selectedCategory === 'all') return matchesSearch; 45 | 46 | const categoryKeywords = { 47 | reaction: ['surprised', 'pikachu', 'yelling', 'woman', 'cat'], 48 | funny: ['spongebob', 'mocking', 'fine', 'dog'], 49 | choice: ['drake', 'buttons', 'two buttons', 'exit'], 50 | success: ['cheers', 'dicaprio', 'handshake'], 51 | programming: ['debugging', 'brain', 'expanding'] 52 | }; 53 | 54 | const keywords = categoryKeywords[selectedCategory] || []; 55 | const matchesCategory = keywords.some(keyword => 56 | meme.name.toLowerCase().includes(keyword) 57 | ); 58 | 59 | return matchesSearch && matchesCategory; 60 | }); 61 | 62 | // Calculate the index of the last and first item on the current page 63 | const indexOfLastItem = currentPage * itemsPerPage; 64 | const indexOfFirstItem = indexOfLastItem - itemsPerPage; 65 | const currentMemes = filteredMemes.slice(indexOfFirstItem, indexOfLastItem); 66 | 67 | // Function to change the current page 68 | const paginate = (pageNumber) => { 69 | setCurrentPage(pageNumber); 70 | }; 71 | 72 | const prevPage = () => { 73 | if (currentPage > 1) { 74 | setCurrentPage(currentPage - 1); 75 | } 76 | }; 77 | 78 | const nextPage = () => { 79 | if (currentPage < Math.ceil(filteredMemes.length / itemsPerPage)) { 80 | setCurrentPage(currentPage + 1); 81 | } 82 | }; 83 | 84 | // Pagination logic for compact display 85 | const renderPagination = () => { 86 | const totalPages = Math.ceil(filteredMemes.length / itemsPerPage); 87 | const pages = []; 88 | const maxVisiblePages = 7; 89 | 90 | if (totalPages <= maxVisiblePages) { 91 | // Show all pages if total is small 92 | for (let i = 1; i <= totalPages; i++) { 93 | pages.push(i); 94 | } 95 | } else { 96 | // Complex pagination with dots 97 | if (currentPage <= 4) { 98 | // Show: 1 2 3 4 5 ... last 99 | for (let i = 1; i <= 5; i++) { 100 | pages.push(i); 101 | } 102 | pages.push('...', totalPages); 103 | } else if (currentPage >= totalPages - 3) { 104 | // Show: 1 ... (last-4) (last-3) (last-2) (last-1) last 105 | pages.push(1, '...'); 106 | for (let i = totalPages - 4; i <= totalPages; i++) { 107 | pages.push(i); 108 | } 109 | } else { 110 | // Show: 1 ... (current-1) current (current+1) ... last 111 | pages.push(1, '...', currentPage - 1, currentPage, currentPage + 1, '...', totalPages); 112 | } 113 | } 114 | 115 | return pages; 116 | }; 117 | 118 | return ( 119 |
120 | 125 | 126 |
127 | {meme === null ? ( 128 | <> 129 | {/* Categories */} 130 |
131 |
132 | {[ 133 | { id: 'all', name: 'All', icon: '🎭' }, 134 | { id: 'reaction', name: 'Reaction', icon: '😱' }, 135 | { id: 'funny', name: 'Funny', icon: '😂' }, 136 | { id: 'choice', name: 'Choice', icon: '🤔' }, 137 | { id: 'success', name: 'Success', icon: '🎉' }, 138 | { id: 'programming', name: 'Code', icon: '💻' } 139 | ].map(category => ( 140 | 151 | ))} 152 |
153 |

154 | {filteredMemes.length} memes found 155 | {selectedCategory !== 'all' && ` in ${selectedCategory}`} 156 |

157 |
158 | 159 | {/* Error Message */} 160 | {error && ( 161 |
162 |
163 | ⚠️ 164 |
165 |

{error}

166 | 172 |
173 |
174 |
175 | )} 176 | 177 | {/* Loading State */} 178 | {isLoading ? ( 179 |
180 | {[...Array(18)].map((_, i) => ( 181 |
182 |
183 |
184 |
185 |
186 | ))} 187 |
188 | ) : filteredMemes.length === 0 ? ( 189 |
190 |
😅
191 |

No memes found

192 |

193 | {searchQuery ? `No results for "${searchQuery}"` : 'No memes in this category'} 194 |

195 | 204 |
205 | ) : ( 206 | 207 | )} 208 | {/* Pagination - Only show if has results */} 209 | {!isLoading && filteredMemes.length > 0 && ( 210 |
211 | 218 | 219 | {renderPagination().map((page, index) => ( 220 | 221 | {page === '...' ? ( 222 | ... 223 | ) : ( 224 | 238 | )} 239 | 240 | ))} 241 | 242 | 249 |
250 | )} 251 | 252 | ) : ( 253 | <> 254 | 255 | 256 | )} 257 |
258 | 259 |
260 |
261 | ); 262 | }; 263 | 264 | export default Home; 265 | -------------------------------------------------------------------------------- /src/components/LoadingOverlay.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * Loading Overlay Component 5 | * Provides visual feedback during export operations with progress indication 6 | */ 7 | const LoadingOverlay = ({ 8 | isVisible = false, 9 | message = 'Processing...', 10 | progress = null, 11 | canCancel = false, 12 | onCancel = null, 13 | type = 'export' // 'export', 'upload', 'processing' 14 | }) => { 15 | if (!isVisible) { 16 | return null; 17 | } 18 | 19 | const getLoadingIcon = () => { 20 | switch (type) { 21 | case 'export': 22 | return 'fas fa-download'; 23 | case 'upload': 24 | return 'fas fa-upload'; 25 | case 'processing': 26 | return 'fas fa-cog'; 27 | default: 28 | return 'fas fa-spinner'; 29 | } 30 | }; 31 | 32 | const getLoadingMessage = () => { 33 | if (message) return message; 34 | 35 | switch (type) { 36 | case 'export': 37 | return 'Exporting your meme...'; 38 | case 'upload': 39 | return 'Uploading image...'; 40 | case 'processing': 41 | return 'Processing image...'; 42 | default: 43 | return 'Please wait...'; 44 | } 45 | }; 46 | 47 | const loadingCSS = ` 48 | .loading-overlay { 49 | position: fixed; 50 | top: 0; 51 | left: 0; 52 | right: 0; 53 | bottom: 0; 54 | background: rgba(0, 0, 0, 0.7); 55 | backdrop-filter: blur(4px); 56 | display: flex; 57 | align-items: center; 58 | justify-content: center; 59 | z-index: 9999; 60 | animation: fadeIn 0.3s ease-out; 61 | } 62 | 63 | .loading-content { 64 | background: white; 65 | border-radius: 20px; 66 | padding: 40px; 67 | text-align: center; 68 | box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); 69 | border: 1px solid #e2e8f0; 70 | max-width: 400px; 71 | width: 90%; 72 | animation: scaleIn 0.3s ease-out; 73 | } 74 | 75 | .loading-icon-container { 76 | position: relative; 77 | margin-bottom: 24px; 78 | display: inline-block; 79 | } 80 | 81 | .loading-icon { 82 | font-size: 48px; 83 | color: #667eea; 84 | animation: pulse 2s ease-in-out infinite; 85 | } 86 | 87 | .loading-spinner { 88 | position: absolute; 89 | top: -8px; 90 | left: -8px; 91 | right: -8px; 92 | bottom: -8px; 93 | border: 3px solid #e2e8f0; 94 | border-top: 3px solid #667eea; 95 | border-radius: 50%; 96 | animation: spin 1s linear infinite; 97 | } 98 | 99 | .loading-title { 100 | font-size: 20px; 101 | font-weight: 600; 102 | color: #2d3748; 103 | margin: 0 0 12px 0; 104 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 105 | } 106 | 107 | .loading-message { 108 | font-size: 16px; 109 | color: #4a5568; 110 | margin: 0 0 24px 0; 111 | line-height: 1.5; 112 | } 113 | 114 | .loading-progress { 115 | margin-bottom: 24px; 116 | } 117 | 118 | .loading-progress-bar { 119 | width: 100%; 120 | height: 8px; 121 | background: #e2e8f0; 122 | border-radius: 4px; 123 | overflow: hidden; 124 | position: relative; 125 | } 126 | 127 | .loading-progress-fill { 128 | height: 100%; 129 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 130 | border-radius: 4px; 131 | transition: width 0.3s ease; 132 | position: relative; 133 | } 134 | 135 | .loading-progress-fill::after { 136 | content: ''; 137 | position: absolute; 138 | top: 0; 139 | left: 0; 140 | right: 0; 141 | bottom: 0; 142 | background: linear-gradient( 143 | 90deg, 144 | transparent, 145 | rgba(255, 255, 255, 0.4), 146 | transparent 147 | ); 148 | animation: shimmer 2s ease-in-out infinite; 149 | } 150 | 151 | .loading-progress-text { 152 | font-size: 14px; 153 | color: #718096; 154 | margin-top: 8px; 155 | font-weight: 500; 156 | } 157 | 158 | .loading-progress-indeterminate .loading-progress-fill { 159 | width: 30% !important; 160 | animation: indeterminateProgress 2s ease-in-out infinite; 161 | } 162 | 163 | .loading-cancel { 164 | background: none; 165 | border: 2px solid #e2e8f0; 166 | color: #4a5568; 167 | padding: 10px 20px; 168 | border-radius: 10px; 169 | cursor: pointer; 170 | font-size: 14px; 171 | font-weight: 500; 172 | transition: all 0.3s ease; 173 | font-family: inherit; 174 | } 175 | 176 | .loading-cancel:hover { 177 | border-color: #cbd5e0; 178 | background: #f7fafc; 179 | transform: translateY(-1px); 180 | } 181 | 182 | .loading-cancel:active { 183 | transform: translateY(0); 184 | } 185 | 186 | .loading-steps { 187 | text-align: left; 188 | margin-bottom: 24px; 189 | } 190 | 191 | .loading-step { 192 | display: flex; 193 | align-items: center; 194 | gap: 12px; 195 | padding: 8px 0; 196 | font-size: 14px; 197 | color: #4a5568; 198 | } 199 | 200 | .loading-step-icon { 201 | width: 20px; 202 | height: 20px; 203 | border-radius: 50%; 204 | display: flex; 205 | align-items: center; 206 | justify-content: center; 207 | font-size: 10px; 208 | flex-shrink: 0; 209 | } 210 | 211 | .loading-step-completed .loading-step-icon { 212 | background: #48bb78; 213 | color: white; 214 | } 215 | 216 | .loading-step-active .loading-step-icon { 217 | background: #667eea; 218 | color: white; 219 | animation: pulse 2s ease-in-out infinite; 220 | } 221 | 222 | .loading-step-pending .loading-step-icon { 223 | background: #e2e8f0; 224 | color: #a0aec0; 225 | } 226 | 227 | .loading-step-completed { 228 | color: #2d3748; 229 | } 230 | 231 | .loading-step-active { 232 | color: #2d3748; 233 | font-weight: 500; 234 | } 235 | 236 | @keyframes fadeIn { 237 | from { 238 | opacity: 0; 239 | } 240 | to { 241 | opacity: 1; 242 | } 243 | } 244 | 245 | @keyframes scaleIn { 246 | from { 247 | transform: scale(0.9); 248 | opacity: 0; 249 | } 250 | to { 251 | transform: scale(1); 252 | opacity: 1; 253 | } 254 | } 255 | 256 | @keyframes spin { 257 | from { 258 | transform: rotate(0deg); 259 | } 260 | to { 261 | transform: rotate(360deg); 262 | } 263 | } 264 | 265 | @keyframes pulse { 266 | 0%, 100% { 267 | opacity: 1; 268 | transform: scale(1); 269 | } 270 | 50% { 271 | opacity: 0.7; 272 | transform: scale(1.05); 273 | } 274 | } 275 | 276 | @keyframes shimmer { 277 | 0% { 278 | transform: translateX(-100%); 279 | } 280 | 100% { 281 | transform: translateX(100%); 282 | } 283 | } 284 | 285 | @keyframes indeterminateProgress { 286 | 0% { 287 | left: -30%; 288 | } 289 | 100% { 290 | left: 100%; 291 | } 292 | } 293 | 294 | /* Responsive design */ 295 | @media (max-width: 768px) { 296 | .loading-content { 297 | padding: 30px 24px; 298 | border-radius: 16px; 299 | max-width: 350px; 300 | } 301 | 302 | .loading-icon { 303 | font-size: 40px; 304 | } 305 | 306 | .loading-title { 307 | font-size: 18px; 308 | } 309 | 310 | .loading-message { 311 | font-size: 15px; 312 | } 313 | } 314 | 315 | @media (max-width: 480px) { 316 | .loading-content { 317 | padding: 24px 20px; 318 | border-radius: 12px; 319 | width: 95%; 320 | } 321 | 322 | .loading-icon { 323 | font-size: 36px; 324 | } 325 | 326 | .loading-title { 327 | font-size: 16px; 328 | margin-bottom: 8px; 329 | } 330 | 331 | .loading-message { 332 | font-size: 14px; 333 | margin-bottom: 20px; 334 | } 335 | 336 | .loading-cancel { 337 | padding: 8px 16px; 338 | font-size: 13px; 339 | } 340 | } 341 | `; 342 | 343 | return ( 344 | <> 345 | 346 |
347 |
348 |
349 | 350 |
351 |
352 | 353 |

354 | {type === 'export' ? 'Exporting Meme' : 355 | type === 'upload' ? 'Uploading Image' : 356 | type === 'processing' ? 'Processing Image' : 'Loading'} 357 |

358 | 359 |

{getLoadingMessage()}

360 | 361 | {progress !== null && ( 362 |
363 |
364 |
= 0 ? `${Math.max(0, Math.min(100, progress))}%` : '30%' }} 367 | >
368 |
369 | {progress >= 0 && ( 370 |
371 | {Math.round(progress)}% complete 372 |
373 | )} 374 |
375 | )} 376 | 377 | {type === 'export' && ( 378 |
379 |
380 |
381 | 382 |
383 | Processing image 384 |
385 |
386 |
387 | 388 |
389 | Generating download file 390 |
391 |
392 |
3
393 | Starting download 394 |
395 |
396 | )} 397 | 398 | {canCancel && onCancel && ( 399 | 406 | )} 407 |
408 |
409 | 410 | ); 411 | }; 412 | 413 | export default LoadingOverlay; -------------------------------------------------------------------------------- /src/utils/test-export-panel-standalone.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Advanced Export Options - Test 7 | 156 | 157 | 158 |
159 |

🎯 Advanced Export Options - Live Demo

160 |

This demonstrates the advanced export functionality that should appear after generating a meme.

161 | 162 | 163 |
164 |
165 |
SAMPLE MEME
166 |
Generated with Advanced Export Options
167 |
168 |
169 | 170 | 171 |
172 |
📤 Advanced Export Options
173 | 174 | 175 |
176 | 177 |
178 | 179 | 180 | 181 |
182 |
183 | 184 | 185 |
186 | 187 |
188 | Quality: 189 | 80% 190 |
191 | 192 |
193 | 194 | 195 |
196 |
197 | Estimated File Size: 198 | ~245 KB 199 |
200 |
201 | Format: PNG 202 |
203 |
204 | 205 | 206 | 209 |
210 | 211 |
212 |

🔧 How It Works

213 |
    214 |
  • Format Selection: Choose PNG (lossless), JPEG (compressed), or WebP (modern)
  • 215 |
  • Quality Control: Adjust compression quality for JPEG/WebP formats
  • 216 |
  • Real-time Estimation: See file size estimates update as you change settings
  • 217 |
  • Smart Downloads: Intelligent filename generation with timestamps
  • 218 |
219 | 220 |

🚨 Current Issue

221 |

The export panel isn't showing in the main app because:

222 |
    223 |
  1. Missing GROQ_API_KEY: The meme generation API needs this environment variable
  2. 224 |
  3. No meme generated: Export options only appear after successfully creating a meme
  4. 225 |
226 | 227 |

✅ To Fix

228 |

Set the GROQ_API_KEY environment variable and restart the Python server, then generate a meme to see the export options.

229 |
230 |
231 | 232 | 295 | 296 | --------------------------------------------------------------------------------