├── .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 |
51 | ×
52 |
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 |
53 |
54 | {imageURL && (
55 |
56 |
Generated Meme:
57 |
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 | 
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 |
{
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 |
84 | ↑
85 |
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 | Generate Meme
22 |
23 |
24 |
25 |
Original Image:
26 |
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 |
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 |
181 | Export Format
182 |
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 |
220 | Quality
221 |
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 |
254 |
255 |
256 |
{getButtonText()}
257 | {exportStatus === 'idle' && format !== 'png' && (
258 |
Quality: {quality}%
259 | )}
260 |
261 |
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 |
262 | Try Again
263 |
264 |
265 |
269 | Reload Page
270 |
271 |
272 |
273 | {process.env.NODE_ENV === 'development' && (
274 | <>
275 |
this.setState({ showDetails: !this.state.showDetails })}
278 | >
279 | {this.state.showDetails ? 'Hide' : 'Show'} Error Details
280 |
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 | setSelectedCategory(category.id)}
143 | className={`flex items-center gap-1 px-3 py-1 rounded-full text-sm transition-all ${
144 | selectedCategory === category.id
145 | ? 'bg-pink-600 text-white'
146 | : 'bg-gray-700 text-gray-300 hover:bg-gray-600'
147 | }`}
148 | >
149 | {category.icon} {category.name}
150 |
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 |
window.location.reload()}
168 | className="text-red-400 hover:text-red-300 text-xs underline mt-1"
169 | >
170 | Try Again
171 |
172 |
173 |
174 |
175 | )}
176 |
177 | {/* Loading State */}
178 | {isLoading ? (
179 |
180 | {[...Array(18)].map((_, i) => (
181 |
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 |
{
197 | setSearchQuery('');
198 | setSelectedCategory('all');
199 | }}
200 | className="bg-pink-600 hover:bg-pink-700 text-white px-4 py-2 rounded-lg transition-colors"
201 | >
202 | Clear Filters
203 |
204 |
205 | ) : (
206 |
207 | )}
208 | {/* Pagination - Only show if has results */}
209 | {!isLoading && filteredMemes.length > 0 && (
210 |
211 |
216 | Previous
217 |
218 |
219 | {renderPagination().map((page, index) => (
220 |
221 | {page === '...' ? (
222 | ...
223 | ) : (
224 | paginate(page)}
231 | style={currentPage === page ? {
232 | boxShadow: '0 4px 20px rgba(37, 99, 235, 0.6), 0 0 0 2px rgba(59, 130, 246, 0.4)',
233 | background: 'linear-gradient(135deg, #3b82f6, #2563eb, #1d4ed8)'
234 | } : {}}
235 | >
236 | {page}
237 |
238 | )}
239 |
240 | ))}
241 |
242 |
247 | Next
248 |
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 |
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 |
404 | Cancel
405 |
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 |
183 |
184 |
185 |
186 |
Quality Settings
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 |
207 | 📥 Export Meme
208 |
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 | Missing GROQ_API_KEY: The meme generation API needs this environment variable
224 | No meme generated: Export options only appear after successfully creating a meme
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 |
--------------------------------------------------------------------------------