From aeb37e111dcd624d97fa3aab0498dbb9f887937a Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 3 Jul 2025 13:09:57 -0400 Subject: [PATCH 1/3] Switched up the front end (still figuring out email stuff) --- .gitignore | 1 + QUICKSTART.md | 100 +++++ README.md | 219 +++++++--- app.py | 123 +++++- database/memes.json | 4 +- env.example | 28 ++ package-lock.json | 3 + package.json | 3 + postcss.config.js | 6 + public/index.html | 18 + requirements.txt | 3 +- setup-frontend.bat | 52 +++ setup-frontend.sh | 44 ++ src/App.js | 23 ++ src/index.css | 17 + src/index.js | 11 + src/mailchimp_service.py | 189 +++++++++ src/pages/ConfirmationSent.js | 60 +++ src/pages/EmailSignup.js | 103 +++++ src/pages/NewsPreferences.js | 135 +++++++ src/pages/ThankYou.js | 60 +++ static/css/history.css | 243 ----------- static/css/styles.css | 742 ---------------------------------- static/js/api.js | 128 ------ static/js/history-page.js | 262 ------------ static/js/main.js | 351 ---------------- static/js/ui.js | 560 ------------------------- tailwind.config.js | 20 + templates/history.html | 68 ---- templates/index.html | 164 -------- 30 files changed, 1157 insertions(+), 2583 deletions(-) create mode 100644 QUICKSTART.md create mode 100644 env.example create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 public/index.html create mode 100644 setup-frontend.bat create mode 100644 setup-frontend.sh create mode 100644 src/App.js create mode 100644 src/index.css create mode 100644 src/index.js create mode 100644 src/mailchimp_service.py create mode 100644 src/pages/ConfirmationSent.js create mode 100644 src/pages/EmailSignup.js create mode 100644 src/pages/NewsPreferences.js create mode 100644 src/pages/ThankYou.js delete mode 100644 static/css/history.css delete mode 100644 static/css/styles.css delete mode 100644 static/js/api.js delete mode 100644 static/js/history-page.js delete mode 100644 static/js/main.js delete mode 100644 static/js/ui.js create mode 100644 tailwind.config.js delete mode 100644 templates/history.html delete mode 100644 templates/index.html diff --git a/.gitignore b/.gitignore index 2a6f5fe..36e9f1f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ wheels/ .installed.cfg *.egg +node_modules/ # Virtual Environment venv/ env/ diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..c3b2c2b --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,100 @@ +# Quick Start Guide + +Get the AI Meme Newsletter up and running in 5 minutes! + +## 🚀 Quick Setup + +### 1. Prerequisites Check +Make sure you have: +- ✅ Node.js installed (`node --version`) +- ✅ Python 3.8+ installed (`python --version`) +- ✅ Git installed + +### 2. Clone and Setup +```bash +# Clone the repository +git clone +cd AIMemeNewletter + +# Setup Python backend +python -m venv venv +venv\Scripts\activate # Windows +# OR +source venv/bin/activate # macOS/Linux + +pip install -r requirements.txt + +# Setup React frontend (Windows) +setup-frontend.bat + +# OR for macOS/Linux: +chmod +x setup-frontend.sh +./setup-frontend.sh +``` + +### 3. Configure Environment +```bash +# Copy environment template +cp env.example .env + +# Edit .env with your keys (at minimum, add a dummy MAILCHIMP_API_KEY) +``` + +**Minimum required for testing:** +```env +MAILCHIMP_API_KEY=test-key +MAILCHIMP_SERVER_PREFIX=us1 +MAILCHIMP_LIST_ID=test-list +``` + +### 4. Run the Application +```bash +python app.py +``` + +Open http://localhost:5001 in your browser! + +## 🎯 What You'll See + +1. **Page 1**: Email signup form +2. **Page 2**: News preference toggles +3. **Page 3**: Confirmation message +4. **Page 4**: Thank you with auto-redirect + +## 🔧 Production Setup + +For production, you'll need real API keys: + +1. **Mailchimp Setup**: + - Sign up at https://mailchimp.com/ + - Get your API key from Account → Extras → API Keys + - Find your server prefix (e.g., 'us1') in the API key URL + - Create an audience and get the List ID + +2. **Update .env**: +```env +MAILCHIMP_API_KEY=your-real-api-key +MAILCHIMP_SERVER_PREFIX=us1 +MAILCHIMP_LIST_ID=your-list-id +``` + +## 🐛 Common Issues + +**"Node.js not found"** +- Download from https://nodejs.org/ + +**"npm not found"** +- Node.js includes npm, reinstall Node.js + +**Build fails** +- Try: `rm -rf node_modules && npm install` + +**Flask errors** +- Ensure virtual environment is activated +- Check all dependencies are installed + +## 📞 Need Help? + +- Check the full [README.md](README.md) for detailed instructions +- Open an issue in the repository +- Check the troubleshooting section in the main README \ No newline at end of file diff --git a/README.md b/README.md index 0f86a2f..33529c9 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,189 @@ # AI Meme Newsletter -

- - -    - - -    - -

-An AI-powered newsletter generator that makes staying informed fun and accessible. In today's fast-paced world, where attention spans are shrinking and information overload is common, young people often struggle to keep up with important news and developments. AI Meme Factory bridges this gap by transforming complex news articles into engaging memes, making it easier for everyone to stay informed about the latest trends and events. +A modern React-based newsletter subscription system for AI enthusiasts, featuring a beautiful 4-page flow for email signup and preference selection. -Using cutting-edge AI technologies, the project generates memes that capture the essence of trending topics, helping users stay up-to-date with the state of the art in various fields. Choose between cloud mode (using OpenAI API) or local mode (using Stable Diffusion) for meme generation. +## 🚀 Features -## Features +- **Modern React Frontend**: Built with React 18, Tailwind CSS, and React Router +- **4-Page User Flow**: + 1. Email signup with validation + 2. News preference selection with toggle switches + 3. Confirmation email sent notification + 4. Thank you page with auto-redirect +- **Mailchimp Integration**: Seamless email list management and preference tracking +- **Responsive Design**: Beautiful UI that works on all devices +- **API-First Backend**: Flask backend with RESTful API endpoints -- 📰 Automated news article collection using NewsAPI -- 🤖 AI-generated meme prompts based on trending topics -- 🎨 High-quality meme generation using either OpenAI or Stable Diffusion -- 📊 Real-time analytics and database tracking -- 🌐 Two operating modes: Cloud (OpenAI) or Local (Stable Diffusion) -- 🔄 Dynamic meme generation based on current news +## 📋 Prerequisites + +- **Node.js** (v16 or higher) - [Download here](https://nodejs.org/) +- **Python** (v3.8 or higher) +- **Mailchimp Account** - [Sign up here](https://mailchimp.com/) + +## 🛠️ Installation + +### 1. Clone the Repository +```bash +git clone +cd AIMemeNewletter +``` + +### 2. Setup Python Backend +```bash +# Create virtual environment +python -m venv venv -![image](https://github.com/user-attachments/assets/e80460f0-0450-4e1b-8be1-86e5ccee917f) +# Activate virtual environment +# On Windows: +venv\Scripts\activate +# On macOS/Linux: +source venv/bin/activate + +# Install Python dependencies +pip install -r requirements.txt +``` + +### 3. Setup React Frontend + +**Option A: Using the setup script (Recommended)** +```bash +# On Windows: +setup-frontend.bat + +# On macOS/Linux: +chmod +x setup-frontend.sh +./setup-frontend.sh +``` + +**Option B: Manual setup** +```bash +# Install React dependencies +npm install + +# Build the React app +npm run build +``` + +### 4. Configure Environment Variables +```bash +# Copy the example environment file +cp env.example .env + +# Edit .env with your API keys +``` + +Required environment variables: +- `MAILCHIMP_API_KEY`: Your Mailchimp API key +- `MAILCHIMP_SERVER_PREFIX`: Your Mailchimp server prefix (e.g., 'us1') +- `MAILCHIMP_LIST_ID`: Your Mailchimp audience/list ID +- `NEWS_API_KEY`: Your NewsAPI.org API key (optional, for news aggregation) +- `OPENAI_API_KEY`: Your OpenAI API key (optional, for meme generation) +## 🚀 Running the Application -## Getting Started +### Development Mode +```bash +# Start the Flask backend +python app.py -Detailed setup and running instructions are available in the [RUN.md](RUN.md) file. +# In a separate terminal, start React development server +npm start +``` -## Requirements +### Production Mode +```bash +# Build the React app +npm run build -### Common Requirements -- Python 3.8+ -- NEWS_API_KEY (Free from https://newsapi.org/) -- GEMINI_API_KEY (Free from https://aistudio.google.com/apikey) +# Start the Flask backend (serves the built React app) +python app.py +``` -### Cloud Mode (Default) -- OPENAI_API_KEY (Paid from https://platform.openai.com/settings/organization/api-keys) +The application will be available at `http://localhost:5001` + +## 📱 User Flow + +### Page 1: Email Signup +- Clean, modern design with email validation +- Real-time email format checking +- Integration with Mailchimp for list management + +### Page 2: News Preferences +- 10 AI news categories with toggle switches +- Categories include: OpenAI, Claude, Machine Learning, Generative AI, etc. +- Preferences are saved to Mailchimp merge fields -### Local Mode -- Stable Diffusion dependencies (see RUN.md) -- GPU recommended for better performance +### Page 3: Confirmation Sent +- Clear instructions for email confirmation +- Spam folder guidance +- Professional confirmation message + +### Page 4: Thank You +- Success confirmation +- 5-second auto-redirect to home page +- Clean, satisfying completion experience + +## 🔧 API Endpoints + +- `POST /api/subscribe` - Subscribe email to Mailchimp list +- `POST /api/preferences` - Update user preferences +- `GET /api/confirm/` - Confirm email subscription + +## 🎨 Customization + +### Styling +The app uses Tailwind CSS for styling. You can customize the design by: +- Modifying `tailwind.config.js` for theme changes +- Updating component classes in the React components +- Adding custom CSS in `src/index.css` -## Project Structure +### News Categories +Edit the categories in `src/pages/NewsPreferences.js`: +```javascript +const categories = [ + { key: 'openai', label: 'OpenAI & ChatGPT', description: '...' }, + // Add or modify categories here +]; +``` + +### Mailchimp Integration +The Mailchimp integration is handled in `src/mailchimp_service.py`. You can: +- Customize merge field names +- Add additional subscriber data +- Implement custom email templates -- `app.py` - Main Flask application -- `src/` - Core functionality - - `news_aggregator.py` - News collection - - `filter_top_k.py` - Article filtering - - `prompt_generator.py` - Meme prompt generation - - `meme_generator.py` - Cloud meme generation - - `meme_generator_local.py` - Local meme generation -- `database/` - Data storage +## 🐛 Troubleshooting -## Running the Application +### Common Issues -See [RUN.md](RUN.md) for detailed running instructions. +1. **Node.js not found** + - Install Node.js from https://nodejs.org/ + - Ensure it's added to your PATH -## Contributing +2. **Mailchimp API errors** + - Verify your API key, server prefix, and list ID + - Check that your Mailchimp account is active -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add some amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request +3. **Build errors** + - Clear node_modules and reinstall: `rm -rf node_modules && npm install` + - Check for version conflicts in package.json + +4. **Flask backend errors** + - Ensure all Python dependencies are installed + - Check that your virtual environment is activated -## License +## 📄 License This project is licensed under the MIT License - see the LICENSE file for details. -## Acknowledgments +## 🤝 Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +## 📞 Support -- NewsAPI for news aggregation -- OpenAI for cloud meme generation -- Stable Diffusion for local meme generation -- Google Gemini for AI assistance +For support, please open an issue in the GitHub repository or contact the development team. diff --git a/app.py b/app.py index dcb0000..707c5fe 100644 --- a/app.py +++ b/app.py @@ -3,7 +3,7 @@ # Main Flask application - entry point # ============================================================================== -from flask import Flask, render_template, request, jsonify +from flask import Flask, render_template, request, jsonify, send_from_directory import os import json import sys @@ -48,11 +48,7 @@ from src.meme_generator import generate_meme_image def create_app(): - app = Flask(__name__) - - # Configure static files for the separated frontend - app.static_folder = 'static' - app.static_url_path = '/static' + app = Flask(__name__, static_folder='build', static_url_path='') # Add custom CLI commands @app.cli.command() @@ -71,8 +67,13 @@ def local(port): @app.route('/') def index(): - """Serve the main HTML page""" - return render_template('index.html') + """Serve the React app""" + return send_from_directory(app.static_folder, 'index.html') + + @app.route('/') + def serve_react(path): + """Serve React app for all other routes""" + return send_from_directory(app.static_folder, 'index.html') @app.route('/history') def history(): @@ -250,6 +251,109 @@ def health_check(): 'timestamp': datetime.now().isoformat() }) + # API Routes for React frontend + @app.route('/api/subscribe', methods=['POST']) + def api_subscribe(): + """Subscribe email to Mailchimp list""" + try: + data = request.get_json() + email = data.get('email') + print(f"[DEBUG] /api/subscribe called with email: {email}") + + if not email: + print("[ERROR] No email provided to /api/subscribe") + return jsonify({ + 'success': False, + 'error': 'Email is required' + }), 400 + + # Initialize Mailchimp service + try: + from src.mailchimp_service import MailchimpService + mailchimp = MailchimpService() + print(f"[DEBUG] MailchimpService initialized in /api/subscribe") + result = mailchimp.subscribe_email(email) + print(f"[DEBUG] MailchimpService.subscribe_email result: {result}") + + return jsonify(result) + + except ImportError: + print("[WARN] MailchimpService not available, simulating success.") + # Fallback if Mailchimp is not configured + return jsonify({ + 'success': True, + 'message': 'Email subscription successful (Mailchimp not configured)' + }) + + except Exception as e: + print(f"[ERROR] Exception in /api/subscribe: {e}") + return jsonify({ + 'success': False, + 'error': f'Subscription failed: {str(e)}' + }), 500 + + @app.route('/api/preferences', methods=['POST']) + def api_preferences(): + """Update user preferences and send confirmation email""" + try: + data = request.get_json() + email = data.get('email') + preferences = data.get('preferences', {}) + print(f"[DEBUG] /api/preferences called with email: {email}, preferences: {preferences}") + + if not email: + print("[ERROR] No email provided to /api/preferences") + return jsonify({ + 'success': False, + 'error': 'Email is required' + }), 400 + + # Initialize Mailchimp service + try: + from src.mailchimp_service import MailchimpService + mailchimp = MailchimpService() + print(f"[DEBUG] MailchimpService initialized in /api/preferences") + result = mailchimp.update_preferences(email, preferences) + print(f"[DEBUG] MailchimpService.update_preferences result: {result}") + + return jsonify(result) + + except ImportError: + print("[WARN] MailchimpService not available, simulating success.") + # Fallback if Mailchimp is not configured + return jsonify({ + 'success': True, + 'message': 'Preferences saved successfully (Mailchimp not configured)' + }) + + except Exception as e: + print(f"[ERROR] Exception in /api/preferences: {e}") + return jsonify({ + 'success': False, + 'error': f'Failed to save preferences: {str(e)}' + }), 500 + + @app.route('/api/confirm/') + def api_confirm(token): + """Confirm email subscription""" + try: + from src.mailchimp_service import MailchimpService + mailchimp = MailchimpService() + result = mailchimp.confirm_subscription(token) + + return jsonify(result) + + except ImportError: + return jsonify({ + 'success': True, + 'message': 'Subscription confirmed (Mailchimp not configured)' + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'Confirmation failed: {str(e)}' + }), 500 + # Error handlers for better API responses @app.errorhandler(404) def not_found_error(error): @@ -297,6 +401,9 @@ def internal_error(error): print(" - GET /api/memes (get existing memes)") print(" - GET /api/news (get news preview)") print(" - GET /api/health (health check)") + print(" - POST /api/subscribe (subscribe to Mailchimp list)") + print(" - POST /api/preferences (update user preferences)") + print(" - GET /api/confirm/ (confirm email subscription)") # Check required API keys and warn if missing missing_keys = [] diff --git a/database/memes.json b/database/memes.json index 62ece46..f34a05f 100644 --- a/database/memes.json +++ b/database/memes.json @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53643485e0d2aeb2e511bcb83e9be7beb477df45d8abfb8d3efe3e2c0ae1ffab -size 95309342 +oid sha256:adf130c1fbd88fe0133f0c6c4fe026e4f8a534d34ebc3d29af189555f12b3c21 +size 95312516 diff --git a/env.example b/env.example new file mode 100644 index 0000000..d169530 --- /dev/null +++ b/env.example @@ -0,0 +1,28 @@ +# Flask Configuration +SECRET_KEY=your-secret-key-here +FLASK_DEBUG=True + +# OpenAI Configuration +OPENAI_API_KEY=your-openai-api-key-here + +# News API Configuration +NEWS_API_KEY=your-news-api-key-here + +# Mailchimp Configuration +MAILCHIMP_API_KEY=your-mailchimp-api-key-here +MAILCHIMP_SERVER_PREFIX=us1 +MAILCHIMP_LIST_ID=your-mailchimp-list-id-here + +# Database Configuration +DATABASE_PATH=memes.db + +# Meme Generation Settings +MAX_MEMES_PER_USER=10 +MEME_GENERATION_TIMEOUT=60 + +# News Settings +NEWS_REFRESH_INTERVAL=3600 +MAX_NEWS_ARTICLES=20 + +# Rate Limiting +RATE_LIMIT_PER_MINUTE=10 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a6a1af5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0e2a0bac9afdda490c01d1038464882679db6cd1f20e7ff309407a49ed085b7 +size 663043 diff --git a/package.json b/package.json new file mode 100644 index 0000000..5373d57 --- /dev/null +++ b/package.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cda9269e63dd06f5ee0c300dd776daa085f35c627619ad7ad6d38bc2e25c997e +size 898 diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..0cc9a9d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..85711c5 --- /dev/null +++ b/public/index.html @@ -0,0 +1,18 @@ + + + + + + + + + AI Meme Newsletter + + + +
+ + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index fc647d8..1855b51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ python-dotenv==1.0.0 gunicorn==21.2.0 google-generativeai==0.8.5 newsapi-python>=0.2.6 -scikit-learn>=1.0.0 \ No newline at end of file +scikit-learn>=1.0.0 +mailchimp-marketing>=3.0.80 \ No newline at end of file diff --git a/setup-frontend.bat b/setup-frontend.bat new file mode 100644 index 0000000..852c3bb --- /dev/null +++ b/setup-frontend.bat @@ -0,0 +1,52 @@ +@echo off + +REM Setup script for React frontend (Windows) + +echo 🚀 Setting up AI Meme Newsletter React Frontend... + +REM Check if Node.js is installed +node --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo ❌ Node.js is not installed. Please install Node.js first. + echo Download from: https://nodejs.org/ + pause + exit /b 1 +) + +REM Check if npm is installed +npm --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo ❌ npm is not installed. Please install npm first. + pause + exit /b 1 +) + +echo 📦 Installing React dependencies... +npm install + +if %errorlevel% equ 0 ( + echo ✅ Dependencies installed successfully! +) else ( + echo ❌ Failed to install dependencies + pause + exit /b 1 +) + +echo 🔨 Building React app... +npm run build + +if %errorlevel% equ 0 ( + echo ✅ React app built successfully! + echo 🎉 Frontend setup complete! + echo. + echo Next steps: + echo 1. Copy env.example to .env and configure your API keys + echo 2. Run: python app.py + echo 3. Open http://localhost:5001 in your browser +) else ( + echo ❌ Failed to build React app + pause + exit /b 1 +) + +pause \ No newline at end of file diff --git a/setup-frontend.sh b/setup-frontend.sh new file mode 100644 index 0000000..afbdd85 --- /dev/null +++ b/setup-frontend.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# Setup script for React frontend + +echo "🚀 Setting up AI Meme Newsletter React Frontend..." + +# Check if Node.js is installed +if ! command -v node &> /dev/null; then + echo "❌ Node.js is not installed. Please install Node.js first." + echo " Download from: https://nodejs.org/" + exit 1 +fi + +# Check if npm is installed +if ! command -v npm &> /dev/null; then + echo "❌ npm is not installed. Please install npm first." + exit 1 +fi + +echo "📦 Installing React dependencies..." +npm install + +if [ $? -eq 0 ]; then + echo "✅ Dependencies installed successfully!" +else + echo "❌ Failed to install dependencies" + exit 1 +fi + +echo "🔨 Building React app..." +npm run build + +if [ $? -eq 0 ]; then + echo "✅ React app built successfully!" + echo "🎉 Frontend setup complete!" + echo "" + echo "Next steps:" + echo "1. Copy env.example to .env and configure your API keys" + echo "2. Run: python app.py" + echo "3. Open http://localhost:5001 in your browser" +else + echo "❌ Failed to build React app" + exit 1 +fi \ No newline at end of file diff --git a/src/App.js b/src/App.js new file mode 100644 index 0000000..e500598 --- /dev/null +++ b/src/App.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import EmailSignup from './pages/EmailSignup'; +import NewsPreferences from './pages/NewsPreferences'; +import ConfirmationSent from './pages/ConfirmationSent'; +import ThankYou from './pages/ThankYou'; + +function App() { + return ( + +
+ + } /> + } /> + } /> + } /> + +
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..e878b43 --- /dev/null +++ b/src/index.css @@ -0,0 +1,17 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..3d817e7 --- /dev/null +++ b/src/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); \ No newline at end of file diff --git a/src/mailchimp_service.py b/src/mailchimp_service.py new file mode 100644 index 0000000..bbe2250 --- /dev/null +++ b/src/mailchimp_service.py @@ -0,0 +1,189 @@ +# ============================================================================== +# FILE: src/mailchimp_service.py +# Mailchimp API integration for email subscriptions +# ============================================================================== + +import os +import mailchimp_marketing as MailchimpMarketing +from mailchimp_marketing.api_client import ApiClientError +from typing import Dict, List, Optional + +class MailchimpService: + """ + Service class for handling Mailchimp API operations. + + Handles email subscriptions, preference updates, and confirmation emails. + """ + + def __init__(self): + """ + Initialize Mailchimp service with API credentials. + + Requires environment variables: + - MAILCHIMP_API_KEY: Your Mailchimp API key + - MAILCHIMP_SERVER_PREFIX: Your Mailchimp server prefix (e.g., 'us1') + - MAILCHIMP_LIST_ID: Your Mailchimp audience/list ID + """ + self.api_key = os.getenv('MAILCHIMP_API_KEY') + self.server_prefix = os.getenv('MAILCHIMP_SERVER_PREFIX') + self.list_id = os.getenv('MAILCHIMP_LIST_ID') + + if not all([self.api_key, self.server_prefix, self.list_id]): + raise ValueError("Missing required Mailchimp environment variables") + + self.client = MailchimpMarketing.Client() + self.client.set_config({ + "api_key": self.api_key, + "server": self.server_prefix + }) + + def subscribe_email(self, email: str) -> Dict: + """ + Subscribe an email address to the Mailchimp list. + + Input: + email (str): Email address to subscribe + + Output: + Dict: Response with success status and message + """ + try: + response = self.client.lists.add_list_member(self.list_id, { + "email_address": email, + "status": "subscribed", + "merge_fields": { + "FNAME": "", + "LNAME": "" + } + }) + + return { + "success": True, + "message": "Successfully subscribed to newsletter", + "subscriber_hash": response.get("id") + } + + except ApiClientError as error: + error_text = error.text + if "Member Exists" in error_text: + return { + "success": True, + "message": "Email already subscribed", + "subscriber_hash": None + } + else: + return { + "success": False, + "error": f"Failed to subscribe: {error_text}" + } + except Exception as e: + return { + "success": False, + "error": f"Unexpected error: {str(e)}" + } + + def update_preferences(self, email: str, preferences: Dict) -> Dict: + """ + Update user preferences and send confirmation email. + + Input: + email (str): Email address of the subscriber + preferences (Dict): Dictionary of news category preferences + + Output: + Dict: Response with success status and message + """ + try: + # Get subscriber hash + subscriber_hash = self._get_subscriber_hash(email) + if not subscriber_hash: + return { + "success": False, + "error": "Subscriber not found" + } + + # Update merge fields with preferences + merge_fields = {} + for category, enabled in preferences.items(): + merge_fields[f"PREF_{category.upper()}"] = "Yes" if enabled else "No" + + # Update subscriber + self.client.lists.set_list_member(self.list_id, subscriber_hash, { + "merge_fields": merge_fields + }) + + # Send confirmation email + self._send_confirmation_email(subscriber_hash) + + return { + "success": True, + "message": "Preferences updated and confirmation email sent" + } + + except ApiClientError as error: + return { + "success": False, + "error": f"Failed to update preferences: {error.text}" + } + except Exception as e: + return { + "success": False, + "error": f"Unexpected error: {str(e)}" + } + + def _get_subscriber_hash(self, email: str) -> Optional[str]: + """ + Get subscriber hash from email address. + + Input: + email (str): Email address to look up + + Output: + Optional[str]: Subscriber hash if found, None otherwise + """ + try: + response = self.client.lists.get_list_member(self.list_id, email) + return response.get("id") + except ApiClientError: + return None + + def _send_confirmation_email(self, subscriber_hash: str) -> bool: + """ + Send confirmation email to subscriber. + + Input: + subscriber_hash (str): Mailchimp subscriber hash + + Output: + bool: True if email sent successfully, False otherwise + """ + try: + # This would typically use Mailchimp's automation or campaign API + # For now, we'll just return True as a placeholder + # In a real implementation, you'd trigger an automation or send a campaign + return True + except Exception: + return False + + def confirm_subscription(self, token: str) -> Dict: + """ + Confirm email subscription using token from confirmation link. + + Input: + token (str): Confirmation token from email link + + Output: + Dict: Response with success status and message + """ + try: + # This would validate the token and confirm the subscription + # For now, we'll return success as a placeholder + return { + "success": True, + "message": "Subscription confirmed successfully" + } + except Exception as e: + return { + "success": False, + "error": f"Failed to confirm subscription: {str(e)}" + } \ No newline at end of file diff --git a/src/pages/ConfirmationSent.js b/src/pages/ConfirmationSent.js new file mode 100644 index 0000000..f587ba2 --- /dev/null +++ b/src/pages/ConfirmationSent.js @@ -0,0 +1,60 @@ +import React from 'react'; + +const ConfirmationSent = () => { + return ( +
+
+
+
+ + + +
+ +

+ Confirmation Email Sent! +

+ +
+

+ We've sent a confirmation email to your inbox. Please check your email and: +

+ +
    +
  • + + If the email is in your spam folder, move it to your primary inbox +
  • +
  • + + Click the confirmation link in the email to complete your subscription +
  • +
  • + + You'll be redirected back here once confirmed +
  • +
+ +
+

+ Didn't receive the email? Check your spam folder or try refreshing this page in a few minutes. +

+
+
+
+
+
+ ); +}; + +export default ConfirmationSent; \ No newline at end of file diff --git a/src/pages/EmailSignup.js b/src/pages/EmailSignup.js new file mode 100644 index 0000000..2adb155 --- /dev/null +++ b/src/pages/EmailSignup.js @@ -0,0 +1,103 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import axios from 'axios'; + +const EmailSignup = () => { + const [email, setEmail] = useState(''); + const [isValid, setIsValid] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const navigate = useNavigate(); + + const validateEmail = (email) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + const handleEmailChange = (e) => { + const newEmail = e.target.value; + setEmail(newEmail); + setIsValid(validateEmail(newEmail)); + setError(''); + }; + + const handleSubscribe = async () => { + if (!isValid) return; + + setIsLoading(true); + setError(''); + + try { + // Add email to Mailchimp list + const response = await axios.post('/api/subscribe', { + email: email + }); + + if (response.data.success) { + // Store email in localStorage for next page + localStorage.setItem('userEmail', email); + navigate('/preferences'); + } else { + setError(response.data.error || 'Failed to subscribe. Please try again.'); + } + } catch (err) { + console.error('Subscription error:', err); + setError('Failed to subscribe. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+

+ AI Meme Newsletter +

+

+ Stay updated with the latest AI trends and hilarious memes +

+
+ +
+
+
+ + +
+ + {error && ( +
+ {error} +
+ )} + + +
+
+
+
+ ); +}; + +export default EmailSignup; \ No newline at end of file diff --git a/src/pages/NewsPreferences.js b/src/pages/NewsPreferences.js new file mode 100644 index 0000000..abfd6da --- /dev/null +++ b/src/pages/NewsPreferences.js @@ -0,0 +1,135 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import axios from 'axios'; + +const NewsPreferences = () => { + const [preferences, setPreferences] = useState({ + openai: true, + claude: true, + machineLearning: true, + generativeAI: true, + robotics: false, + autonomous: false, + neuralNetworks: false, + deepLearning: false, + aiEthics: false, + aiSafety: false + }); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const navigate = useNavigate(); + + useEffect(() => { + // Check if user has email from previous page + const userEmail = localStorage.getItem('userEmail'); + if (!userEmail) { + navigate('/'); + } + }, [navigate]); + + const handleToggle = (category) => { + setPreferences(prev => ({ + ...prev, + [category]: !prev[category] + })); + }; + + const handleDone = async () => { + setIsLoading(true); + setError(''); + + try { + const userEmail = localStorage.getItem('userEmail'); + + // Update user preferences and send confirmation email + const response = await axios.post('/api/preferences', { + email: userEmail, + preferences: preferences + }); + + if (response.data.success) { + navigate('/confirmation-sent'); + } else { + setError(response.data.error || 'Failed to save preferences. Please try again.'); + } + } catch (err) { + console.error('Preferences error:', err); + setError('Failed to save preferences. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + const categories = [ + { key: 'openai', label: 'OpenAI & ChatGPT', description: 'Latest updates from OpenAI and ChatGPT developments' }, + { key: 'claude', label: 'Claude & Anthropic', description: 'News about Claude AI and Anthropic\'s research' }, + { key: 'machineLearning', label: 'Machine Learning', description: 'General machine learning breakthroughs and research' }, + { key: 'generativeAI', label: 'Generative AI', description: 'Text, image, and video generation technologies' }, + { key: 'robotics', label: 'Robotics & Automation', description: 'AI-powered robots and automation systems' }, + { key: 'autonomous', label: 'Autonomous Systems', description: 'Self-driving cars, drones, and autonomous vehicles' }, + { key: 'neuralNetworks', label: 'Neural Networks', description: 'Deep learning and neural network architectures' }, + { key: 'deepLearning', label: 'Deep Learning', description: 'Advanced deep learning techniques and applications' }, + { key: 'aiEthics', label: 'AI Ethics & Policy', description: 'Ethical considerations and policy discussions around AI' }, + { key: 'aiSafety', label: 'AI Safety & Alignment', description: 'AI safety research and alignment efforts' } + ]; + + return ( +
+
+
+

+ Choose Your News Preferences +

+

+ Select which AI topics you'd like to receive updates about +

+
+ +
+
+ {categories.map((category) => ( +
+
+

{category.label}

+

{category.description}

+
+ +
+ ))} + + {error && ( +
+ {error} +
+ )} + + +
+
+
+
+ ); +}; + +export default NewsPreferences; \ No newline at end of file diff --git a/src/pages/ThankYou.js b/src/pages/ThankYou.js new file mode 100644 index 0000000..4f89f56 --- /dev/null +++ b/src/pages/ThankYou.js @@ -0,0 +1,60 @@ +import React, { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +const ThankYou = () => { + const navigate = useNavigate(); + + useEffect(() => { + // Redirect to home page after 5 seconds + const timer = setTimeout(() => { + navigate('/'); + }, 5000); + + return () => clearTimeout(timer); + }, [navigate]); + + return ( +
+
+
+
+ + + +
+ +

+ Thank You! +

+ +
+

+ Thank you for completing your sign up! Your subscription has been confirmed. +

+ +
+
+ Redirecting you to the home page... +
+ +
+ You'll receive your first AI Meme Newsletter soon! +
+
+
+
+
+ ); +}; + +export default ThankYou; \ No newline at end of file diff --git a/static/css/history.css b/static/css/history.css deleted file mode 100644 index 6ffd412..0000000 --- a/static/css/history.css +++ /dev/null @@ -1,243 +0,0 @@ -.meme-content .meme-image { - height: 300px; -}/** -* History page specific styles -* Extends the base styles for the previous generations page -*/ - -/* Back button in header */ -.back-btn-inline { -display: inline-flex; -align-items: center; -background: rgba(255, 255, 255, 0.2); -color: white; -border: 2px solid rgba(255, 255, 255, 0.3); -backdrop-filter: blur(10px); -padding: 10px 20px; -border-radius: 20px; -cursor: pointer; -font-weight: 600; -transition: all 0.3s ease; -font-size: 14px; -text-decoration: none; -margin-bottom: 20px; -} - -.back-btn-inline:hover { -background: rgba(255, 255, 255, 0.3); -transform: translateX(-5px); -box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); -color: white; -} - -/* Adjust header styling */ -.history-header { -position: relative; -} - -/* Meme card container - single clickable unit */ -.meme-card-container { -display: flex; -flex-direction: column; -} - -/* Meme content - combined image and info */ -.meme-content { -background: white; -border-radius: 20px; -overflow: hidden; -box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); -cursor: pointer; -transition: all 0.4s ease; -position: relative; -} - -.meme-content:hover { -transform: translateY(-10px) scale(1.02); -box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25); -} - -/* Bounce animation on click */ -.meme-content:active { -transform: scale(0.98); -transition: transform 0.1s ease; -} - -/* Meme image inside content */ -.meme-content .meme-image { -width: 100%; -height: 250px; -object-fit: cover; -display: block; -} - -/* Meme info section */ -.meme-info { -padding: 15px; -border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.meme-prompt { -margin-bottom: 12px; -} - -.meme-prompt strong { -color: #667eea; -font-size: 1rem; -display: block; -margin-bottom: 6px; -} - -.meme-prompt p { -color: #444; -line-height: 1.5; -font-size: 0.85rem; -background: rgba(102, 126, 234, 0.05); -padding: 10px 12px; -border-radius: 10px; -border-left: 3px solid #667eea; -max-height: 80px; -overflow-y: auto; -} - -/* Metadata section */ -.meme-metadata { -display: flex; -justify-content: space-between; -align-items: center; -flex-wrap: wrap; -gap: 10px; -padding-top: 12px; -border-top: 1px solid rgba(0, 0, 0, 0.1); -} - -.meme-date, .meme-trends { -font-size: 0.75rem; -color: #666; -display: flex; -align-items: center; -gap: 5px; -} - -.meme-trends { -max-width: 60%; -overflow: hidden; -text-overflow: ellipsis; -white-space: nowrap; -} - -/* Empty state styles */ -.empty-state { -grid-column: 1 / -1; -text-align: center; -padding: 80px 20px; -color: white; -} - -.empty-state h3 { -font-size: 2.5rem; -margin-bottom: 20px; -} - -.empty-state p { -font-size: 1.2rem; -opacity: 0.9; -margin-bottom: 30px; -} - -.generate-link { -display: inline-block; -background: linear-gradient(45deg, #667eea, #764ba2); -color: white; -padding: 15px 35px; -border-radius: 25px; -text-decoration: none; -font-weight: 600; -transition: all 0.3s ease; -box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); -} - -.generate-link:hover { -transform: translateY(-2px); -box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); -} - -/* Override grid for history page - 4 columns */ -.history-page .memes-grid, -#historyGrid { -display: grid; -grid-template-columns: repeat(4, 1fr); -gap: 20px; -margin-top: 30px; -} - -/* Loading overlay specific to history */ -#loadingOverlay .loading-content h3 { -margin-bottom: 10px; -} - -#loadingOverlay .loading-content p { -font-size: 1rem; -opacity: 0.9; -} - -/* Mobile responsive adjustments */ -@media (max-width: 1200px) { -.history-page .memes-grid, -#historyGrid { - grid-template-columns: repeat(3, 1fr); - gap: 20px; -} -} - -@media (max-width: 900px) { -.history-page .memes-grid, -#historyGrid { - grid-template-columns: repeat(2, 1fr); - gap: 20px; -} -} - -@media (max-width: 768px) { -.history-page .memes-grid, -#historyGrid { - grid-template-columns: 1fr; - gap: 25px; -} - -.meme-info { - padding: 15px; -} - -.meme-content .meme-image { - height: 300px; -} - -.meme-prompt strong { - font-size: 0.9rem; -} - -.meme-prompt p { - font-size: 0.8rem; - padding: 8px 10px; - max-height: none; -} - -.meme-metadata { - flex-direction: column; - align-items: flex-start; - gap: 10px; -} - -.meme-trends { - max-width: 100%; -} - -.empty-state h3 { - font-size: 2rem; -} - -.empty-state p { - font-size: 1.1rem; -} -} \ No newline at end of file diff --git a/static/css/styles.css b/static/css/styles.css deleted file mode 100644 index ae29370..0000000 --- a/static/css/styles.css +++ /dev/null @@ -1,742 +0,0 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); - min-height: 100vh; - color: #333; - overflow-x: hidden; -} - -.container { - max-width: 1200px; - margin: 0 auto; - padding: 20px; -} - -/* Landing Page Styles */ -.landing-page { - display: block; -} - -.header { - text-align: center; - margin-bottom: 50px; - background: rgba(255, 255, 255, 0.95); - padding: 40px; - border-radius: 25px; - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.2); - position: relative; -} - -.header h1 { - font-size: 3.5rem; - margin-bottom: 20px; - background: linear-gradient(45deg, #667eea, #764ba2, #f093fb); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - font-weight: 800; - text-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); -} - -.header p { - font-size: 1.3rem; - color: #666; - margin-bottom: 30px; - line-height: 1.6; -} - -.history-link-btn { - background: linear-gradient(45deg, #667eea, #764ba2); - color: white; - border: none; - padding: 12px 30px; - border-radius: 25px; - cursor: pointer; - font-weight: 600; - font-size: 15px; - transition: all 0.3s ease; - box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); -} - -.history-link-btn:hover { - transform: translateY(-2px); - box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); -} - -.form-container { - background: rgba(255, 255, 255, 0.95); - padding: 40px; - border-radius: 25px; - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); - margin-bottom: 30px; - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.form-section { - margin-bottom: 35px; -} - -.form-section h3 { - color: #333; - margin-bottom: 15px; - font-size: 1.4rem; - display: flex; - align-items: center; - gap: 10px; - font-weight: 600; -} - -.form-section p { - color: #666; - margin-bottom: 20px; - font-size: 1rem; - line-height: 1.5; -} - -.dropdown-container { - position: relative; - margin-bottom: 15px; -} - -.dropdown-button { - width: 100%; - padding: 18px 25px; - background: white; - border: 2px solid #e0e0e0; - border-radius: 15px; - font-size: 16px; - cursor: pointer; - display: flex; - justify-content: space-between; - align-items: center; - transition: all 0.3s ease; - font-weight: 500; -} - -.dropdown-button:hover { - border-color: #667eea; - box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2); - transform: translateY(-2px); -} - -.dropdown-arrow { - transition: transform 0.3s ease; - font-size: 14px; -} - -.dropdown-button.active .dropdown-arrow { - transform: rotate(180deg); -} - -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - right: 0; - background: white; - border: 2px solid #e0e0e0; - border-radius: 15px; - box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15); - z-index: 1000; - max-height: 300px; - overflow-y: auto; - display: none; - margin-top: 5px; -} - -.dropdown-menu.active { - display: block; - animation: dropdownSlide 0.3s ease; -} - -@keyframes dropdownSlide { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.dropdown-item { - padding: 15px 25px; - cursor: pointer; - transition: all 0.2s ease; - border-bottom: 1px solid #f5f5f5; - font-size: 15px; -} - -.dropdown-item:last-child { - border-bottom: none; -} - -.dropdown-item:hover { - background: linear-gradient(90deg, rgba(102, 126, 234, 0.1), rgba(240, 147, 251, 0.1)); - transform: translateX(5px); -} - -.selected-tags { - display: flex; - flex-wrap: wrap; - gap: 12px; - margin-top: 20px; - min-height: 24px; -} - -.tag { - background: linear-gradient(45deg, #667eea, #764ba2); - color: white; - padding: 10px 18px; - border-radius: 25px; - font-size: 14px; - display: flex; - align-items: center; - gap: 10px; - animation: tagSlideIn 0.4s ease; - box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); - font-weight: 500; -} - -.tag .remove { - cursor: pointer; - font-weight: bold; - font-size: 18px; - transition: transform 0.2s ease; - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - background: rgba(255, 255, 255, 0.2); -} - -.tag .remove:hover { - transform: scale(1.3); - background: rgba(255, 255, 255, 0.3); -} - -@keyframes tagSlideIn { - from { - opacity: 0; - transform: translateY(-15px) scale(0.8); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -.duration-buttons { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: 15px; - margin-top: 20px; -} - -.duration-btn { - padding: 18px 25px; - border: 2px solid #e0e0e0; - background: white; - border-radius: 15px; - cursor: pointer; - transition: all 0.3s ease; - text-align: center; - font-weight: 600; - font-size: 15px; -} - -.duration-btn:hover { - border-color: #667eea; - color: #667eea; - transform: translateY(-2px); - box-shadow: 0 8px 20px rgba(102, 126, 234, 0.2); -} - -.duration-btn.active { - background: linear-gradient(45deg, #667eea, #764ba2); - color: white; - border-color: transparent; - box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); -} - -.memes-buttons { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: 15px; - margin-top: 20px; -} - -.memes-btn { - padding: 18px 25px; - border: 2px solid #e0e0e0; - background: white; - border-radius: 15px; - cursor: pointer; - transition: all 0.3s ease; - text-align: center; - font-weight: 600; - font-size: 15px; -} - -.memes-btn:hover { - border-color: #667eea; - color: #667eea; - transform: translateY(-2px); - box-shadow: 0 8px 20px rgba(102, 126, 234, 0.2); -} - -.memes-btn.active { - background: linear-gradient(45deg, #667eea, #764ba2); - color: white; - border-color: transparent; - box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); -} - - -.generate-btn { - width: 100%; - padding: 22px; - background: linear-gradient(45deg, #667eea, #764ba2, #f093fb); - color: white; - border: none; - border-radius: 20px; - font-size: 18px; - font-weight: 700; - cursor: pointer; - transition: all 0.3s ease; - margin-top: 35px; - box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4); - position: relative; - overflow: hidden; -} - -.generate-btn::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); - transition: left 0.5s ease; -} - -.generate-btn:hover { - transform: translateY(-3px); - box-shadow: 0 15px 35px rgba(102, 126, 234, 0.5); -} - -.generate-btn:hover::before { - left: 100%; -} - -.generate-btn:disabled { - opacity: 0.7; - cursor: not-allowed; - transform: none; -} - -/* Memes Page Styles */ -.memes-page, .history-page { - display: none; -} - -.memes-header, .history-header { - text-align: center; - margin-bottom: 40px; - background: rgba(255, 255, 255, 0.2); - backdrop-filter: blur(15px); - padding: 35px; - border-radius: 25px; - box-shadow: 0 20px 40px rgba(102, 126, 234, 0.15); - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.memes-header h2, .history-header h2 { - font-size: 2.8rem; - margin-bottom: 15px; - color: white; - font-weight: 700; -} - -.memes-header p, .history-header p { - color: rgba(255, 255, 255, 0.9); - font-size: 1.2rem; - margin-top: 10px; -} - -.back-btn { - background: rgba(255, 255, 255, 0.2); - color: white; - border: 2px solid rgba(255, 255, 255, 0.3); - backdrop-filter: blur(10px); - padding: 12px 25px; - border-radius: 20px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; - margin-bottom: 25px; - font-size: 15px; -} - -.back-btn:hover { - background: rgba(255, 255, 255, 0.3); - transform: translateX(-5px); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); -} - -.memes-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); - gap: 30px; - margin-top: 30px; -} - -.meme-card { - border-radius: 20px; - transition: all 0.4s ease; - cursor: pointer; - overflow: hidden; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); - background: white; - position: relative; -} - -.meme-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(45deg, rgba(102, 126, 234, 0.1), rgba(240, 147, 251, 0.1)); - opacity: 0; - transition: opacity 0.3s ease; - z-index: 1; -} - -.meme-card:hover::before { - opacity: 1; -} - -.meme-card:hover { - transform: translateY(-10px) scale(1.02); - box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25); -} - -.meme-image { - width: 100%; - height: 100%; - object-fit: cover; - border-radius: 20px; - transition: all 0.3s ease; - position: relative; - z-index: 2; -} - -.meme-meta { - padding: 15px 20px; - background: rgba(0, 0, 0, 0.05); - border-top: 1px solid rgba(0, 0, 0, 0.1); -} - -.meme-timestamp { - font-size: 0.85rem; - color: #666; - font-style: italic; -} - -.error-card { - display: flex; - align-items: center; - justify-content: center; - height: 400px; - color: #666; - text-align: center; - background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); - border-radius: 20px; - border: 2px dashed #ddd; -} - -.error-content h3 { - margin-bottom: 10px; - font-size: 1.5rem; -} - -/* Pagination Styles */ -.pagination { - display: flex; - justify-content: center; - align-items: center; - gap: 10px; - margin-top: 40px; - padding: 20px; -} - -.pagination-btn { - padding: 10px 20px; - background: rgba(255, 255, 255, 0.9); - color: #667eea; - border: 2px solid rgba(255, 255, 255, 0.3); - border-radius: 15px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; - font-size: 14px; -} - -.pagination-btn:hover:not(:disabled) { - background: white; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2); -} - -.pagination-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.page-info { - color: white; - font-weight: 600; - padding: 0 15px; -} - -/* Loading Spinner */ -.loading-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.85); - display: flex; - align-items: center; - justify-content: center; - z-index: 2000; - color: white; - text-align: center; - backdrop-filter: blur(5px); -} - -.loading-content { - background: rgba(255, 255, 255, 0.1); - padding: 50px; - border-radius: 25px; - backdrop-filter: blur(15px); - border: 1px solid rgba(255, 255, 255, 0.2); - max-width: 400px; - width: 90%; -} - -.spinner { - border: 4px solid rgba(255, 255, 255, 0.3); - border-top: 4px solid white; - border-radius: 50%; - width: 60px; - height: 60px; - animation: spin 1s linear infinite; - margin: 0 auto 25px; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -.loading-content h3 { - font-size: 1.5rem; - margin-bottom: 15px; - font-weight: 600; -} - -.loading-content p { - font-size: 1rem; - margin-bottom: 20px; - opacity: 0.9; -} - -.progress-bar { - width: 100%; - height: 10px; - background: rgba(255, 255, 255, 0.3); - border-radius: 5px; - margin: 25px 0 15px; - overflow: hidden; -} - -.progress-fill { - height: 100%; - background: linear-gradient(45deg, #667eea, #764ba2, #f093fb); - border-radius: 5px; - transition: width 0.5s ease; - width: 0%; - position: relative; -} - -.progress-fill::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); - animation: progressShine 2s infinite; -} - -@keyframes progressShine { - 0% { transform: translateX(-100%); } - 100% { transform: translateX(100%); } -} - -.progress-text { - font-size: 14px; - opacity: 0.8; - margin-top: 10px; -} - -/* Alert Messages */ -.alert { - position: fixed; - top: 20px; - right: 20px; - padding: 15px 20px; - border-radius: 10px; - color: white; - font-weight: 500; - z-index: 3000; - max-width: 300px; - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); - transform: translateX(400px); - transition: transform 0.3s ease; -} - -.alert.show { - transform: translateX(0); -} - -.alert.success { - background: linear-gradient(45deg, #56ab2f, #a8e6cf); -} - -.alert.error { - background: linear-gradient(45deg, #ff416c, #ff4b2b); -} - -.alert.warning { - background: linear-gradient(45deg, #f7971e, #ffd200); -} - -/* Empty State */ -.empty-state { - text-align: center; - padding: 60px 20px; - color: white; -} - -.empty-state h3 { - font-size: 2rem; - margin-bottom: 15px; -} - -.empty-state p { - font-size: 1.1rem; - opacity: 0.9; -} - -/* Mobile Responsive */ -@media (max-width: 768px) { - .container { - padding: 15px; - } - - .header h1 { - font-size: 2.5rem; - } - - .header p { - font-size: 1.1rem; - } - - .history-link-btn { - padding: 10px 20px; - font-size: 14px; - } - - .memes-grid { - grid-template-columns: 1fr; - } - - .form-container { - padding: 25px; - } - - .duration-buttons { - grid-template-columns: repeat(2, 1fr); - } - - .form-section h3 { - font-size: 1.2rem; - } - - .meme-image { - height: 300px; - } - - .loading-content { - padding: 30px; - } - - .pagination { - flex-wrap: wrap; - } -} - -.hidden { - display: none !important; -} - -/* Smooth scrolling */ -html { - scroll-behavior: smooth; -} - -/* Selection styles */ -::selection { - background: rgba(102, 126, 234, 0.3); -} - -/* Image loading states */ -.meme-image { - opacity: 0; - transition: opacity 0.3s ease; -} - -.meme-image.loaded { - opacity: 1; -} - -/* Animation for entrance */ -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } -} \ No newline at end of file diff --git a/static/js/api.js b/static/js/api.js deleted file mode 100644 index 5f7ed4c..0000000 --- a/static/js/api.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * API handling for AI Meme Newsletter - * Handles all communication with Flask backend - */ - -class MemeAPI { - constructor() { - this.baseURL = ''; // Same origin - } - - /** - * Generate memes from selected trends and duration - * - * @param {Array} trends - Selected AI trends - * @param {number} duration - Duration in days - * @param {number} memes - Number of memes - * @returns {Promise} Generated memes - */ - async generateMemes(trends, duration, memes) { - try { - const response = await fetch('/api/generate', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - trends: trends, - duration: duration, - memes: memes - }) - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - console.log('API Response:', data); - - if (!data.success) { - throw new Error(data.error || 'Failed to generate memes'); - } - - return data.memes; - } catch (error) { - console.error('API Error:', error); - throw error; - } - } - - /** - * Get existing memes from the database - * - * @param {string} sortBy - Sort order ('recent' or 'popular') - * @param {number} limit - Number of memes to retrieve (0 for all) - * @returns {Promise} Response with memes array and metadata - */ - async getMemes(sortBy = 'recent', limit = 20) { - try { - const response = await fetch(`/api/memes?sort=${sortBy}&limit=${limit}`); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - - // Ensure we return the expected structure - return { - success: data.success || true, - memes: data.memes || [], - total_count: data.total_count || 0 - }; - } catch (error) { - console.error('Error fetching memes:', error); - return { - success: false, - memes: [], - error: error.message - }; - } - } - - /** - * Get recent news articles - * - * @param {number} duration - Duration in days - * @returns {Promise} News articles - */ - async getNews(duration = 1) { - try { - const response = await fetch(`/api/news?duration=${duration}`); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - return data.news || []; - } catch (error) { - console.error('Error fetching news:', error); - return []; - } - } - - /** - * Health check endpoint - * - * @returns {Promise} Health status - */ - async healthCheck() { - try { - const response = await fetch('/api/health'); - return await response.json(); - } catch (error) { - console.error('Health check failed:', error); - return { status: 'error', error: error.message }; - } - } -} - -// Create global API instance -window.memeAPI = new MemeAPI(); - -// Export for module usage if needed -if (typeof module !== 'undefined' && module.exports) { - module.exports = MemeAPI; -} \ No newline at end of file diff --git a/static/js/history-page.js b/static/js/history-page.js deleted file mode 100644 index a247773..0000000 --- a/static/js/history-page.js +++ /dev/null @@ -1,262 +0,0 @@ -/** - * History page functionality for AI Meme Newsletter - * Handles displaying and paginating previous meme generations - */ - -class HistoryPage { - constructor() { - this.currentPage = 1; - this.totalPages = 1; - this.memesPerPage = 32; - this.allMemes = []; - } - - /** - * Initialize the history page - */ - init() { - console.log('Initializing history page...'); - this.setupEventListeners(); - this.loadMemes(); - } - - /** - * Set up event listeners - */ - setupEventListeners() { - const prevBtn = document.getElementById('prevPageBtn'); - const nextBtn = document.getElementById('nextPageBtn'); - - if (prevBtn) { - prevBtn.addEventListener('click', () => this.changePage(-1)); - } - - if (nextBtn) { - nextBtn.addEventListener('click', () => this.changePage(1)); - } - } - - /** - * Load memes from the API - */ - async loadMemes() { - const grid = document.getElementById('historyGrid'); - this.showLoading(); - - try { - const data = await window.memeAPI.getMemes(); - - if (data.success && data.memes) { - this.allMemes = data.memes.sort((a, b) => { - return new Date(b.timestamp) - new Date(a.timestamp); - }); - console.log(`Loaded ${this.allMemes.length} memes, sorted by timestamp`); - } else { - console.error('Invalid response:', data); - this.allMemes = []; - } - } catch (error) { - console.error('Error loading memes:', error); - this.showAlert('Failed to load memes', 'error'); - this.allMemes = []; - } finally { - this.hideLoading(); - } - - if (this.allMemes.length === 0) { - grid.innerHTML = ` -
-

No Previous Memes

-

Generate some memes first to see them here!

- Go Generate Some Memes! -
- `; - this.updatePaginationControls(); - return; - } - - this.totalPages = Math.ceil(this.allMemes.length / this.memesPerPage); - this.currentPage = 1; - this.displayCurrentPage(); - } - - /** - * Display memes for the current page - */ - displayCurrentPage() { - const grid = document.getElementById('historyGrid'); - const startIdx = (this.currentPage - 1) * this.memesPerPage; - const endIdx = startIdx + this.memesPerPage; - const memesToShow = this.allMemes.slice(startIdx, endIdx); - - grid.innerHTML = ''; - - memesToShow.forEach((meme, index) => { - const card = this.createMemeCard(meme, startIdx + index); - grid.appendChild(card); - }); - - this.updatePaginationControls(); - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - - /** - * Create a meme card with prompt display - * - * @param {Object} meme - Meme data - * @param {number} globalIndex - Index in the full array - * @returns {HTMLElement} Meme card element - */ - createMemeCard(meme, globalIndex) { - const card = document.createElement('div'); - card.className = 'meme-card-container'; - - const hasBase64 = meme.png_base64 && meme.png_base64.trim() !== ''; - const hasUrl = meme.url && meme.url.trim() !== '' && meme.url !== 'https://example.com'; - - if (!hasBase64) { - card.innerHTML = ` -
-
-
-

Image Not Available

-

This meme could not be loaded

-
-
-
- `; - return card; - } - - const imageSrc = `data:image/png;base64,${meme.png_base64}`; - const timestamp = meme.generated_at || meme.timestamp; - const formattedDate = timestamp ? this.formatDate(timestamp) : 'Unknown date'; - const trends = meme.trends_used ? meme.trends_used.join(', ') : 'Unknown trends'; - - // Create clickable meme card - const memeCardHTML = ` -
- AI Meme #${globalIndex + 1} -
-
- Prompt: -

${meme.prompt || 'No prompt available'}

-
- -
-
- `; - - card.innerHTML = memeCardHTML; - return card; - } - - /** - * Format date for display - * - * @param {string} timestamp - ISO timestamp - * @returns {string} Formatted date - */ - formatDate(timestamp) { - try { - const date = new Date(timestamp); - const options = { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }; - return date.toLocaleDateString('en-US', options); - } catch (error) { - return 'Unknown date'; - } - } - - /** - * Change page - * - * @param {number} direction - Direction to change (-1 or 1) - */ - changePage(direction) { - const newPage = this.currentPage + direction; - - if (newPage >= 1 && newPage <= this.totalPages) { - this.currentPage = newPage; - this.displayCurrentPage(); - } - } - - /** - * Update pagination controls - */ - updatePaginationControls() { - const prevBtn = document.getElementById('prevPageBtn'); - const nextBtn = document.getElementById('nextPageBtn'); - const pageInfo = document.getElementById('pageInfo'); - const pagination = document.getElementById('pagination'); - - if (this.allMemes.length === 0) { - pagination.style.display = 'none'; - return; - } - - pagination.style.display = 'flex'; - - prevBtn.disabled = this.currentPage === 1; - nextBtn.disabled = this.currentPage === this.totalPages; - - pageInfo.textContent = `Page ${this.currentPage} of ${this.totalPages}`; - } - - /** - * Show loading overlay - */ - showLoading() { - const overlay = document.getElementById('loadingOverlay'); - if (overlay) { - overlay.classList.remove('hidden'); - } - } - - /** - * Hide loading overlay - */ - hideLoading() { - const overlay = document.getElementById('loadingOverlay'); - if (overlay) { - overlay.classList.add('hidden'); - } - } - - /** - * Show alert message - * - * @param {string} message - Alert message - * @param {string} type - Alert type - */ - showAlert(message, type = 'success') { - const alert = document.createElement('div'); - alert.className = `alert ${type}`; - alert.textContent = message; - document.body.appendChild(alert); - - setTimeout(() => alert.classList.add('show'), 100); - setTimeout(() => { - alert.classList.remove('show'); - setTimeout(() => alert.remove(), 300); - }, 5000); - } -} - -// Create global instance -window.historyPage = new HistoryPage(); \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js deleted file mode 100644 index 41dbabd..0000000 --- a/static/js/main.js +++ /dev/null @@ -1,351 +0,0 @@ -/** - * Main application initialization for AI Meme Newsletter - * Coordinates between UI and API components - */ - -class MemeApp { - constructor() { - this.isInitialized = false; - this.version = '1.0.0'; - } - - /** - * Initialize the application - */ - async init() { - if (this.isInitialized) { - console.warn('App already initialized'); - return; - } - - try { - console.log('🎭 AI Meme Newsletter v' + this.version + ' initializing...'); - - // Check if dependencies are loaded - this.checkDependencies(); - - // Initialize components - await this.initializeComponents(); - - // Setup global error handling - this.setupErrorHandling(); - - // Perform health check - await this.performHealthCheck(); - - // Initialize entrance animations - window.memeUI.initializeAnimations(); - - this.isInitialized = true; - console.log('✅ AI Meme Newsletter initialized successfully'); - - } catch (error) { - console.error('❌ Failed to initialize app:', error); - this.handleInitializationError(error); - } - } - - /** - * Check if all required dependencies are loaded - */ - checkDependencies() { - const requiredGlobals = ['memeAPI', 'memeUI']; - const missing = requiredGlobals.filter(global => !window[global]); - - if (missing.length > 0) { - throw new Error(`Missing required dependencies: ${missing.join(', ')}`); - } - } - - /** - * Initialize application components - */ - async initializeComponents() { - // Components are already initialized via their constructors - // This is where you'd add any additional setup - - // Example: Load user preferences - this.loadUserPreferences(); - - // Example: Setup analytics - this.setupAnalytics(); - } - - /** - * Setup global error handling - */ - setupErrorHandling() { - // Handle uncaught JavaScript errors - window.addEventListener('error', (event) => { - console.error('Global error:', event.error); - window.memeUI.showAlert('Something went wrong. Please refresh the page.', 'error'); - }); - - // Handle unhandled promise rejections - window.addEventListener('unhandledrejection', (event) => { - console.error('Unhandled promise rejection:', event.reason); - window.memeUI.showAlert('An unexpected error occurred.', 'error'); - }); - } - - /** - * Perform application health check - */ - async performHealthCheck() { - try { - const health = await window.memeAPI.healthCheck(); - console.log('🔍 Health check:', health); - - if (health.status !== 'healthy') { - console.warn('Backend health check failed:', health); - } - } catch (error) { - console.warn('Health check failed:', error); - // Don't throw error here as the app can still function - } - } - - /** - * Handle initialization errors - */ - handleInitializationError(error) { - // Show user-friendly error message - const errorDiv = document.createElement('div'); - errorDiv.style.cssText = ` - position: fixed; - top: 0; - left: 0; - right: 0; - background: #ff416c; - color: white; - padding: 20px; - text-align: center; - z-index: 9999; - font-family: Arial, sans-serif; - `; - errorDiv.innerHTML = ` -

⚠️ Application Error

-

Failed to initialize the AI Meme Newsletter. Please refresh the page.

- - `; - - document.body.insertBefore(errorDiv, document.body.firstChild); - } - - /** - * Load user preferences from localStorage - */ - loadUserPreferences() { - try { - const preferences = localStorage.getItem('memePreferences'); - if (preferences) { - const parsed = JSON.parse(preferences); - console.log('📋 Loaded user preferences:', parsed); - - // Apply preferences - if (parsed.selectedTrends) { - window.memeUI.selectedTrends = parsed.selectedTrends; - window.memeUI.updateSelectedTags(); - } - - if (parsed.selectedDuration) { - window.memeUI.selectedDuration = parsed.selectedDuration; - // Update UI to reflect saved duration - this.updateDurationUI(parsed.selectedDuration); - } - - if (parsed.selectedMemes) { - window.memeUI.selectedMemes = parsed.selectedMemes; - // Update UI to reflect saved memes - this.updateMemesUI(parsed.selectedMemes); - } - } - } catch (error) { - console.warn('Failed to load user preferences:', error); - } - } - - /** - * Save user preferences to localStorage - */ - saveUserPreferences() { - try { - const preferences = { - selectedTrends: window.memeUI.selectedTrends, - selectedDuration: window.memeUI.selectedDuration, - memes: window.memeUI.selectedMemes, - savedAt: new Date().toISOString() - }; - - localStorage.setItem('memePreferences', JSON.stringify(preferences)); - console.log('💾 Saved user preferences'); - } catch (error) { - console.warn('Failed to save user preferences:', error); - } - } - - /** - * Update duration UI based on saved preference - */ - updateDurationUI(duration) { - const buttons = document.querySelectorAll('.duration-btn'); - buttons.forEach(btn => { - btn.classList.remove('active'); - if (parseInt(btn.getAttribute('data-duration')) === duration) { - btn.classList.add('active'); - } - }); - } - - /** - * Update memes UI based on saved preference - */ - updateMemesUI(memes) { - const buttons = document.querySelectorAll('.memes-btn'); - buttons.forEach(btn => { - btn.classList.remove('active'); - if (parseInt(btn.getAttribute('data-memes')) === memes) { - btn.classList.add('active'); - } - }); - } - - /** - * Setup analytics tracking - */ - setupAnalytics() { - // Example analytics setup - console.log('📊 Analytics initialized'); - - // Track page load - window.memeUI.trackEvent('app_initialized', { - version: this.version, - timestamp: new Date().toISOString(), - userAgent: navigator.userAgent.substring(0, 100) - }); - } - - /** - * Auto-save preferences when they change - */ - setupAutoSave() { - // Save preferences when trends change - const originalSelectTrend = window.memeUI.selectTrend; - window.memeUI.selectTrend = function(trend) { - originalSelectTrend.call(this, trend); - window.memeApp.saveUserPreferences(); - }; - - const originalRemoveTrend = window.memeUI.removeTrend; - window.memeUI.removeTrend = function(trend) { - originalRemoveTrend.call(this, trend); - window.memeApp.saveUserPreferences(); - }; - - const originalSelectDuration = window.memeUI.selectDuration; - window.memeUI.selectDuration = function(button, duration) { - originalSelectDuration.call(this, button, duration); - window.memeApp.saveUserPreferences(); - }; - - const originalSelectMemes = window.memeUI.selectMemes; - window.memeUI.selectMemes = function(button, memes) { - originalSelectMemes.call(this, button, memes); - window.memeApp.saveUserPreferences(); - }; - } - - /** - * Get application info - */ - getInfo() { - return { - name: 'AI Meme Newsletter', - version: this.version, - initialized: this.isInitialized, - dependencies: { - api: !!window.memeAPI, - ui: !!window.memeUI - }, - features: { - localStorage: this.hasLocalStorage(), - intersectionObserver: 'IntersectionObserver' in window, - fetch: 'fetch' in window, - webGL: this.hasWebGL() - } - }; - } - - /** - * Check if localStorage is available - */ - hasLocalStorage() { - try { - const test = '__test__'; - localStorage.setItem(test, test); - localStorage.removeItem(test); - return true; - } catch (e) { - return false; - } - } - - /** - * Check if WebGL is available - */ - hasWebGL() { - try { - const canvas = document.createElement('canvas'); - return !!(canvas.getContext('webgl') || canvas.getContext('experimental-webgl')); - } catch (e) { - return false; - } - } - - /** - * Cleanup resources (useful for SPA navigation) - */ - destroy() { - console.log('🧹 Cleaning up AI Meme Newsletter...'); - - // Remove event listeners - // Clear timers/intervals - // Clear global references - - this.isInitialized = false; - } -} - -// Create global app instance -window.memeApp = new MemeApp(); - -// Initialize when DOM is ready -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => { - window.memeApp.init(); - }); -} else { - // DOM is already ready - window.memeApp.init(); -} - -// Setup auto-save after initialization -document.addEventListener('DOMContentLoaded', () => { - setTimeout(() => { - if (window.memeApp.isInitialized) { - window.memeApp.setupAutoSave(); - } - }, 1000); -}); - -// Expose app info to console for debugging -console.log('🎭 AI Meme Newsletter loaded. Type memeApp.getInfo() for details.'); \ No newline at end of file diff --git a/static/js/ui.js b/static/js/ui.js deleted file mode 100644 index c6ddc4e..0000000 --- a/static/js/ui.js +++ /dev/null @@ -1,560 +0,0 @@ -/** - * UI interactions for AI Meme Newsletter - * Handles all user interface logic for the main page - */ - -class MemeUI { - constructor() { - this.selectedTrends = []; - this.selectedDuration = 1; - this.selectedMemes = 1; - this.generatedMemes = []; - - console.log('🎨 MemeUI initializing...'); - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => this.init()); - } else { - this.init(); - } - } - - /** - * Initialize UI components - */ - init() { - console.log('🎨 MemeUI init called'); - this.initializeEventListeners(); - this.setupAccessibility(); - console.log('✅ MemeUI initialized successfully'); - } - - /** - * Initialize all event listeners - */ - initializeEventListeners() { - const dropdownButton = document.getElementById('dropdownButton'); - const dropdownMenu = document.getElementById('dropdownMenu'); - - if (dropdownButton) { - dropdownButton.addEventListener('click', () => this.toggleDropdown()); - } - - const dropdownItems = document.querySelectorAll('.dropdown-item'); - dropdownItems.forEach(item => { - item.addEventListener('click', (e) => { - const trend = e.target.getAttribute('data-trend'); - this.selectTrend(trend); - }); - }); - - const durationButtons = document.querySelectorAll('.duration-btn'); - durationButtons.forEach(btn => { - btn.addEventListener('click', (e) => { - const duration = parseInt(e.target.getAttribute('data-duration')); - this.selectDuration(e.target, duration); - }); - }); - - const memesButtons = document.querySelectorAll('.memes-btn'); - memesButtons.forEach(btn => { - btn.addEventListener('click', (e) => { - const memes = parseInt(e.target.getAttribute('data-memes')); - this.selectMemes(e.target, memes); - }); - }); - - const generateBtn = document.getElementById('generateBtn'); - if (generateBtn) { - generateBtn.addEventListener('click', () => this.handleGenerateMemes()); - } - - const backBtn = document.getElementById('backBtn'); - if (backBtn) { - backBtn.addEventListener('click', () => this.goBack()); - } - - document.addEventListener('click', (event) => { - const dropdown = document.querySelector('.dropdown-container'); - if (dropdown && !dropdown.contains(event.target)) { - this.closeDropdown(); - } - }); - - document.addEventListener('keydown', (e) => this.handleKeyboardShortcuts(e)); - - window.addEventListener('online', () => { - this.showAlert('🌐 Connection restored!', 'success'); - }); - - window.addEventListener('offline', () => { - this.showAlert('🚫 No internet connection. Please check your network.', 'error'); - }); - - document.addEventListener('visibilitychange', () => { - this.handleVisibilityChange(); - }); - - this.setupTouchSupport(); - this.setupEasterEgg(); - } - - /** - * Toggle dropdown menu - */ - toggleDropdown() { - const menu = document.getElementById('dropdownMenu'); - const button = document.getElementById('dropdownButton'); - const isActive = menu.classList.contains('active'); - - if (isActive) { - menu.classList.remove('active'); - button.classList.remove('active'); - } else { - menu.classList.add('active'); - button.classList.add('active'); - } - - button.setAttribute('aria-expanded', !isActive); - } - - /** - * Close dropdown menu - */ - closeDropdown() { - const menu = document.getElementById('dropdownMenu'); - const button = document.getElementById('dropdownButton'); - - if (menu && button) { - menu.classList.remove('active'); - button.classList.remove('active'); - button.setAttribute('aria-expanded', 'false'); - } - } - - /** - * Select a trend from dropdown - * @param {string} trend - The selected trend - */ - selectTrend(trend) { - if (!this.selectedTrends.includes(trend)) { - this.selectedTrends.push(trend); - this.updateSelectedTags(); - this.trackEvent('trend_selected', { trend }); - } - this.closeDropdown(); - } - - /** - * Remove a selected trend - * @param {string} trend - The trend to remove - */ - removeTrend(trend) { - this.selectedTrends = this.selectedTrends.filter(t => t !== trend); - this.updateSelectedTags(); - } - - /** - * Update the display of selected tags - */ - updateSelectedTags() { - const container = document.getElementById('selectedTags'); - if (!container) return; - - container.innerHTML = this.selectedTrends.map(trend => - `
- ${trend} - × -
` - ).join(''); - } - - /** - * Select duration - * @param {HTMLElement} button - The clicked button - * @param {number} duration - Duration in days - */ - selectDuration(button, duration) { - document.querySelectorAll('.duration-btn').forEach(btn => { - btn.classList.remove('active'); - }); - - button.classList.add('active'); - this.selectedDuration = duration; - } - - /** - * Select memes - * @param {HTMLElement} button - The clicked button - * @param {number} memes - Number of memes - */ - selectMemes(button, memes) { - document.querySelectorAll('.memes-btn').forEach(btn => { - btn.classList.remove('active'); - }); - - button.classList.add('active'); - this.selectedMemes = memes; - } - - /** - * Handle meme generation - */ - async handleGenerateMemes() { - if (this.selectedTrends.length === 0) { - this.showAlert('Please select at least one AI trend!', 'warning'); - return; - } - - this.trackEvent('memes_generation_started', { - trends: this.selectedTrends, - duration: this.selectedDuration, - memes: this.selectedMemes - }); - - this.showLoading(); - - try { - await this.updateProgress(20, 'Fetching latest AI news...'); - await this.sleep(1000); - - await this.updateProgress(40, 'Analyzing news articles with AI...'); - await this.sleep(1000); - - await this.updateProgress(60, 'Generating meme prompts...'); - - const memes = await window.memeAPI.generateMemes(this.selectedTrends, this.selectedDuration, this.selectedMemes); - - await this.updateProgress(100, 'Memes ready!'); - await this.sleep(500); - - this.generatedMemes = memes; - this.showMemesPage(); - - const successCount = memes.filter(m => m.success).length; - this.showAlert(`🎉 Successfully generated ${successCount} memes!`, 'success'); - - } catch (error) { - console.error('Error generating memes:', error); - this.showAlert(`Failed to generate memes: ${error.message}`, 'error'); - } finally { - this.hideLoading(); - } - } - - /** - * Show memes page - */ - showMemesPage() { - document.getElementById('landingPage').style.display = 'none'; - document.getElementById('memesPage').style.display = 'block'; - - this.renderMemes(); - - window.scrollTo(0, 0); - } - - /** - * Go back to landing page - */ - goBack() { - document.getElementById('memesPage').style.display = 'none'; - document.getElementById('landingPage').style.display = 'block'; - - window.scrollTo(0, 0); - } - - /** - * Render generated memes - */ - renderMemes() { - const container = document.getElementById('memesGrid'); - if (!container) return; - - if (this.generatedMemes.length === 0) { - container.innerHTML = ` -
-
-

🤔 No Memes Generated

-

Try selecting different trends or adjusting the time duration.

-
-
- `; - return; - } - - container.innerHTML = this.generatedMemes.map((meme, index) => { - if (!meme.success) { - return ` -
-
-

❌ Generation Failed

-

${meme.error || 'Unknown error occurred'}

-
-
- `; - } - - const cleanBase64 = meme.png_base64.replace(/\s/g, '').trim(); - - return ` -
- AI Generated Meme: ${(meme.prompt || 'Generated meme').substring(0, 100)} -
- `; - }).join(''); - - this.setupLazyLoading(); - } - - /** - * Open meme source link - * @param {string} url - URL to open - */ - openMemeLink(url) { - if (url && url !== 'https://example.com') { - window.open(url, '_blank', 'noopener,noreferrer'); - } - } - - /** - * Show alert message - * @param {string} message - Alert message - * @param {string} type - Alert type (success, error, warning) - */ - showAlert(message, type = 'success') { - const alert = document.createElement('div'); - alert.className = `alert ${type}`; - alert.textContent = message; - document.body.appendChild(alert); - - setTimeout(() => alert.classList.add('show'), 100); - setTimeout(() => { - alert.classList.remove('show'); - setTimeout(() => alert.remove(), 300); - }, 5000); - } - - /** - * Show loading overlay - */ - showLoading() { - const overlay = document.getElementById('loadingOverlay'); - if (overlay) { - overlay.classList.remove('hidden'); - } - } - - /** - * Hide loading overlay - */ - hideLoading() { - const overlay = document.getElementById('loadingOverlay'); - if (overlay) { - overlay.classList.add('hidden'); - } - this.updateProgress(0, 'Ready to generate...', false); - } - - /** - * Update loading progress - * @param {number} percent - Progress percentage - * @param {string} text - Progress text - * @param {boolean} animate - Whether to animate the progress - */ - async updateProgress(percent, text, animate = true) { - const progressFill = document.getElementById('progressFill'); - const progressText = document.getElementById('progressText'); - const loadingText = document.getElementById('loadingText'); - - if (progressFill) progressFill.style.width = percent + '%'; - if (progressText) progressText.textContent = percent + '% Complete'; - if (loadingText) loadingText.textContent = text; - - if (animate) { - return this.sleep(200); - } - } - - /** - * Handle keyboard shortcuts - * @param {KeyboardEvent} e - Keyboard event - */ - handleKeyboardShortcuts(e) { - if (e.key.toLowerCase() === 'g' && this.selectedTrends.length > 0) { - const isLoading = !document.getElementById('loadingOverlay').classList.contains('hidden'); - if (!isLoading) { - this.handleGenerateMemes(); - } - } - - if (e.key === 'Escape') { - this.closeDropdown(); - } - - if (e.key === 'Backspace' && document.getElementById('memesPage').style.display === 'block') { - e.preventDefault(); - this.goBack(); - } - - if (e.key === 'Enter' || e.key === ' ') { - const activeElement = document.activeElement; - if (activeElement && activeElement.classList.contains('dropdown-item')) { - e.preventDefault(); - activeElement.click(); - } - } - } - - /** - * Handle page visibility changes - */ - handleVisibilityChange() { - const spinners = document.querySelectorAll('.spinner'); - - if (document.hidden) { - spinners.forEach(spinner => { - spinner.style.animationPlayState = 'paused'; - }); - } else { - spinners.forEach(spinner => { - spinner.style.animationPlayState = 'running'; - }); - } - } - - /** - * Setup touch support for mobile - */ - setupTouchSupport() { - let touchStartY = 0; - - document.addEventListener('touchstart', (e) => { - touchStartY = e.touches[0].clientY; - }); - - document.addEventListener('touchend', (e) => { - const touchEndY = e.changedTouches[0].clientY; - const diff = touchStartY - touchEndY; - - if (diff > 100 && this.selectedTrends.length > 0) { - const landingPageVisible = document.getElementById('landingPage').style.display !== 'none'; - if (landingPageVisible) { - this.handleGenerateMemes(); - } - } - }); - } - - /** - * Setup accessibility features - */ - setupAccessibility() { - const dropdownButton = document.getElementById('dropdownButton'); - const dropdownMenu = document.getElementById('dropdownMenu'); - - if (dropdownButton) { - dropdownButton.setAttribute('aria-expanded', 'false'); - dropdownButton.setAttribute('aria-haspopup', 'listbox'); - } - - if (dropdownMenu) { - dropdownMenu.setAttribute('role', 'listbox'); - } - - document.querySelectorAll('.dropdown-item').forEach(item => { - item.setAttribute('role', 'option'); - item.setAttribute('tabindex', '0'); - }); - } - - /** - * Setup lazy loading for images - */ - setupLazyLoading() { - const images = document.querySelectorAll('img[loading="lazy"]'); - - if ('IntersectionObserver' in window) { - const imageObserver = new IntersectionObserver((entries, observer) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - const img = entry.target; - img.classList.add('fade-in'); - observer.unobserve(img); - } - }); - }); - - images.forEach(img => imageObserver.observe(img)); - } - } - - /** - * Setup Easter egg - */ - setupEasterEgg() { - let clickCount = 0; - const header = document.querySelector('.header h1'); - - if (header) { - header.addEventListener('click', () => { - clickCount++; - if (clickCount === 5) { - this.showAlert('🎭 You found the Easter egg! You really love AI memes!', 'success'); - clickCount = 0; - } - }); - } - } - - /** - * Track analytics events - * @param {string} eventName - Event name - * @param {Object} properties - Event properties - */ - trackEvent(eventName, properties = {}) { - console.log(`📊 Event: ${eventName}`, properties); - } - - /** - * Utility function for delays - * @param {number} ms - Milliseconds to sleep - * @returns {Promise} Promise that resolves after delay - */ - sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - /** - * Initialize entrance animations - */ - initializeAnimations() { - setTimeout(() => { - const header = document.querySelector('.header'); - const formContainer = document.querySelector('.form-container'); - - if (header) { - header.style.animation = 'fadeInUp 0.8s ease'; - } - - if (formContainer) { - formContainer.style.animation = 'fadeInUp 0.8s ease 0.2s both'; - } - }, 100); - } -} - -// Create global UI instance -console.log('🎨 Creating global memeUI instance...'); -window.memeUI = new MemeUI(); -console.log('✅ window.memeUI created:', window.memeUI); - -// Export for module usage if needed -if (typeof module !== 'undefined' && module.exports) { - module.exports = MemeUI; -} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..2e55d2e --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,20 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./src/**/*.{js,jsx,ts,tsx}", + "./public/index.html" + ], + theme: { + extend: { + colors: { + primary: { + 50: '#eff6ff', + 500: '#3b82f6', + 600: '#2563eb', + 700: '#1d4ed8', + } + } + }, + }, + plugins: [], +} \ No newline at end of file diff --git a/templates/history.html b/templates/history.html deleted file mode 100644 index bb7fdc8..0000000 --- a/templates/history.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - Previous Memes - AI Meme Newsletter - - - - - - - - - - - - - - -
-
- ← Back to Generator -

Previous Memes

-

Browse through all previously generated AI memes

-
- -
- -
- - -
- - - - - - - - - - - - \ No newline at end of file diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index 0ede02b..0000000 --- a/templates/index.html +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - AI Meme Newsletter - - - - - - - - - - - - - - - -
- -
-
-

AI Meme Newsletter

-

Transform the latest AI trends into viral memes using cutting-edge AI technology

- - View Previous Generations - -
- -
-
-

Select AI Trends

-

Choose the AI topics you want personalized memes about

- - - -
- -
-
- -
-

Time Duration

-

How recent should the AI news be for meme generation?

- -
- - - - -
-
- -
-

Number of Memes

-

How many memes would you like to generate?

- -
- - - - -
-
- - -
-
- - -
- - -
-

Your AI Meme Newsletter

-

Fresh memes generated from the latest AI trends

-
- -
- -
-
- -
- - - - - - - - - - - - - \ No newline at end of file From b3c36096cbc3483cdc3236dbeffff9be29b64f40 Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 3 Jul 2025 13:41:33 -0400 Subject: [PATCH 2/3] Email endpoint is set up now with mailchimp --- src/mailchimp_service.py | 45 ++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/mailchimp_service.py b/src/mailchimp_service.py index bbe2250..a460cfa 100644 --- a/src/mailchimp_service.py +++ b/src/mailchimp_service.py @@ -48,27 +48,38 @@ def subscribe_email(self, email: str) -> Dict: Dict: Response with success status and message """ try: + print(f"[DEBUG] Attempting to subscribe email: {email} with status 'pending'") response = self.client.lists.add_list_member(self.list_id, { "email_address": email, - "status": "subscribed", + "status": "pending", # <-- Triggers confirmation email "merge_fields": { "FNAME": "", "LNAME": "" } }) + print(f"[DEBUG] Mailchimp add_list_member response: {response}") + print(f"[DEBUG] Email {email} status after add: {response.get('status')}") return { "success": True, - "message": "Successfully subscribed to newsletter", - "subscriber_hash": response.get("id") + "message": "Successfully sent confirmation email (pending status)", + "subscriber_hash": response.get("id"), + "status": response.get("status") } except ApiClientError as error: error_text = error.text + print(f"[ERROR] Mailchimp ApiClientError: {error_text}") if "Member Exists" in error_text: + # Check current status + try: + member = self.client.lists.get_list_member(self.list_id, email) + print(f"[DEBUG] Existing member status: {member.get('status')}") + except Exception as e: + print(f"[ERROR] Could not fetch existing member: {e}") return { "success": True, - "message": "Email already subscribed", + "message": "Email already subscribed or pending", "subscriber_hash": None } else: @@ -77,6 +88,7 @@ def subscribe_email(self, email: str) -> Dict: "error": f"Failed to subscribe: {error_text}" } except Exception as e: + print(f"[ERROR] Unexpected error in subscribe_email: {e}") return { "success": False, "error": f"Unexpected error: {str(e)}" @@ -97,35 +109,46 @@ def update_preferences(self, email: str, preferences: Dict) -> Dict: # Get subscriber hash subscriber_hash = self._get_subscriber_hash(email) if not subscriber_hash: + print(f"[ERROR] Subscriber not found for email: {email}") return { "success": False, "error": "Subscriber not found" } - + # Check current status before updating + try: + member = self.client.lists.get_list_member(self.list_id, email) + print(f"[DEBUG] update_preferences: Current status for {email}: {member.get('status')}") + except Exception as e: + print(f"[ERROR] Could not fetch member before updating: {e}") # Update merge fields with preferences merge_fields = {} for category, enabled in preferences.items(): merge_fields[f"PREF_{category.upper()}"] = "Yes" if enabled else "No" - # Update subscriber - self.client.lists.set_list_member(self.list_id, subscriber_hash, { + update_response = self.client.lists.set_list_member(self.list_id, subscriber_hash, { "merge_fields": merge_fields }) - - # Send confirmation email + print(f"[DEBUG] update_preferences: set_list_member response: {update_response}") + # Check status after updating + try: + member_after = self.client.lists.get_list_member(self.list_id, email) + print(f"[DEBUG] update_preferences: Status after update for {email}: {member_after.get('status')}") + except Exception as e: + print(f"[ERROR] Could not fetch member after updating: {e}") + # Send confirmation email (placeholder) self._send_confirmation_email(subscriber_hash) - return { "success": True, "message": "Preferences updated and confirmation email sent" } - except ApiClientError as error: + print(f"[ERROR] Mailchimp ApiClientError in update_preferences: {error.text}") return { "success": False, "error": f"Failed to update preferences: {error.text}" } except Exception as e: + print(f"[ERROR] Unexpected error in update_preferences: {e}") return { "success": False, "error": f"Unexpected error: {str(e)}" From 6870ade220fc16d6c3483099dfb0580f2456901b Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 4 Jul 2025 13:05:23 -0400 Subject: [PATCH 3/3] cleaned up organization, split into front and backend --- README.md | 107 +++++-- Procfile => backend/Procfile | 0 app.py => backend/app.py | 288 ++++++++++-------- .../database}/meme_templates.json | 0 {database => backend/database}/memes.json | 0 {docs => backend/docs}/API.md | 0 meme.py => backend/meme.py | 0 .../requirements-local.txt | 0 requirements.txt => backend/requirements.txt | 0 {scripts => backend/scripts}/deploy.py | 0 {scripts => backend/scripts}/setup_project.py | 0 {scripts => backend/scripts}/test_apis.py | 0 {src => backend/src}/__init__.py | 0 {src => backend/src}/database.py | 0 {src => backend/src}/filter_top_k.py | 0 backend/src/mailchimp_service.py | 205 +++++++++++++ {src => backend/src}/meme_generator.py | 0 {src => backend/src}/meme_generator_local.py | 0 {src => backend/src}/news_aggregator.py | 0 {src => backend/src}/prompt_generator.py | 0 backend/templates/base.html | 69 +++++ backend/templates/confirmation.html | 44 +++ backend/templates/preferences.html | 235 ++++++++++++++ backend/templates/signup.html | 117 +++++++ backend/templates/thankyou.html | 40 +++ test_meme.png => backend/test_meme.png | Bin {tests => backend/tests}/test_app.py | 0 dev-setup.bat | 85 ++++++ dev-setup.sh | 86 ++++++ .../package-lock.json | 0 package.json => frontend/package.json | 0 .../postcss.config.js | 0 {public => frontend/public}/index.html | 0 .../setup-frontend.bat | 0 .../setup-frontend.sh | 0 {src => frontend/src}/App.js | 0 {src => frontend/src}/index.css | 0 {src => frontend/src}/index.js | 0 .../src}/pages/ConfirmationSent.js | 0 {src => frontend/src}/pages/EmailSignup.js | 0 .../src}/pages/NewsPreferences.js | 0 {src => frontend/src}/pages/ThankYou.js | 0 .../tailwind.config.js | 0 src/mailchimp_service.py | 212 ------------- 44 files changed, 1113 insertions(+), 375 deletions(-) rename Procfile => backend/Procfile (100%) rename app.py => backend/app.py (86%) rename {database => backend/database}/meme_templates.json (100%) rename {database => backend/database}/memes.json (100%) rename {docs => backend/docs}/API.md (100%) rename meme.py => backend/meme.py (100%) rename requirements-local.txt => backend/requirements-local.txt (100%) rename requirements.txt => backend/requirements.txt (100%) rename {scripts => backend/scripts}/deploy.py (100%) rename {scripts => backend/scripts}/setup_project.py (100%) rename {scripts => backend/scripts}/test_apis.py (100%) rename {src => backend/src}/__init__.py (100%) rename {src => backend/src}/database.py (100%) rename {src => backend/src}/filter_top_k.py (100%) create mode 100644 backend/src/mailchimp_service.py rename {src => backend/src}/meme_generator.py (100%) rename {src => backend/src}/meme_generator_local.py (100%) rename {src => backend/src}/news_aggregator.py (100%) rename {src => backend/src}/prompt_generator.py (100%) create mode 100644 backend/templates/base.html create mode 100644 backend/templates/confirmation.html create mode 100644 backend/templates/preferences.html create mode 100644 backend/templates/signup.html create mode 100644 backend/templates/thankyou.html rename test_meme.png => backend/test_meme.png (100%) rename {tests => backend/tests}/test_app.py (100%) create mode 100644 dev-setup.bat create mode 100644 dev-setup.sh rename package-lock.json => frontend/package-lock.json (100%) rename package.json => frontend/package.json (100%) rename postcss.config.js => frontend/postcss.config.js (100%) rename {public => frontend/public}/index.html (100%) rename setup-frontend.bat => frontend/setup-frontend.bat (100%) rename setup-frontend.sh => frontend/setup-frontend.sh (100%) rename {src => frontend/src}/App.js (100%) rename {src => frontend/src}/index.css (100%) rename {src => frontend/src}/index.js (100%) rename {src => frontend/src}/pages/ConfirmationSent.js (100%) rename {src => frontend/src}/pages/EmailSignup.js (100%) rename {src => frontend/src}/pages/NewsPreferences.js (100%) rename {src => frontend/src}/pages/ThankYou.js (100%) rename tailwind.config.js => frontend/tailwind.config.js (100%) delete mode 100644 src/mailchimp_service.py diff --git a/README.md b/README.md index 33529c9..e593f06 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # AI Meme Newsletter -A modern React-based newsletter subscription system for AI enthusiasts, featuring a beautiful 4-page flow for email signup and preference selection. +A modern Flask + React newsletter subscription system for AI enthusiasts, featuring a beautiful 4-page flow for email signup and preference selection. ## 🚀 Features - **Modern React Frontend**: Built with React 18, Tailwind CSS, and React Router +- **Flask Backend**: RESTful API with Mailchimp integration - **4-Page User Flow**: 1. Email signup with validation 2. News preference selection with toggle switches @@ -12,7 +13,27 @@ A modern React-based newsletter subscription system for AI enthusiasts, featurin 4. Thank you page with auto-redirect - **Mailchimp Integration**: Seamless email list management and preference tracking - **Responsive Design**: Beautiful UI that works on all devices -- **API-First Backend**: Flask backend with RESTful API endpoints +- **Production Ready**: Flask serves the built React app + +## 📁 Project Structure + +``` +AIMemeNewletter/ +├── frontend/ # React application +│ ├── src/ # React source code +│ ├── public/ # Static assets +│ ├── build/ # Built React app (served by Flask) +│ ├── package.json # Node.js dependencies +│ └── tailwind.config.js +├── backend/ # Flask application +│ ├── src/ # Python source code +│ ├── templates/ # Jinja templates (legacy) +│ ├── app.py # Main Flask application +│ └── requirements.txt +├── dev-setup.bat # Windows setup script +├── dev-setup.sh # Linux/Mac setup script +└── README.md +``` ## 📋 Prerequisites @@ -28,8 +49,22 @@ git clone cd AIMemeNewletter ``` -### 2. Setup Python Backend +### 2. Quick Setup (Recommended) ```bash +# On Windows: +dev-setup.bat + +# On macOS/Linux: +chmod +x dev-setup.sh +./dev-setup.sh +``` + +### 3. Manual Setup + +**Backend Setup:** +```bash +cd backend + # Create virtual environment python -m venv venv @@ -43,20 +78,10 @@ source venv/bin/activate pip install -r requirements.txt ``` -### 3. Setup React Frontend - -**Option A: Using the setup script (Recommended)** +**Frontend Setup:** ```bash -# On Windows: -setup-frontend.bat - -# On macOS/Linux: -chmod +x setup-frontend.sh -./setup-frontend.sh -``` +cd frontend -**Option B: Manual setup** -```bash # Install React dependencies npm install @@ -81,25 +106,27 @@ Required environment variables: ## 🚀 Running the Application -### Development Mode +### Production Mode (Recommended) ```bash -# Start the Flask backend +# Start the Flask backend (serves the built React app) +cd backend python app.py - -# In a separate terminal, start React development server -npm start ``` -### Production Mode -```bash -# Build the React app -npm run build +The application will be available at `http://localhost:5001` -# Start the Flask backend (serves the built React app) +### Development Mode +```bash +# Terminal 1: Start Flask backend +cd backend python app.py + +# Terminal 2: Start React development server +cd frontend +npm start ``` -The application will be available at `http://localhost:5001` +React dev server will run on `http://localhost:3000` with API proxy to Flask. ## 📱 User Flow @@ -128,17 +155,20 @@ The application will be available at `http://localhost:5001` - `POST /api/subscribe` - Subscribe email to Mailchimp list - `POST /api/preferences` - Update user preferences - `GET /api/confirm/` - Confirm email subscription +- `POST /api/generate` - Generate memes (legacy feature) +- `GET /api/memes` - Get meme history (legacy feature) +- `GET /api/news` - Get news articles (legacy feature) ## 🎨 Customization -### Styling -The app uses Tailwind CSS for styling. You can customize the design by: -- Modifying `tailwind.config.js` for theme changes +### Frontend Styling +The React app uses Tailwind CSS for styling. You can customize the design by: +- Modifying `frontend/tailwind.config.js` for theme changes - Updating component classes in the React components -- Adding custom CSS in `src/index.css` +- Adding custom CSS in `frontend/src/index.css` ### News Categories -Edit the categories in `src/pages/NewsPreferences.js`: +Edit the categories in `frontend/src/pages/NewsPreferences.js`: ```javascript const categories = [ { key: 'openai', label: 'OpenAI & ChatGPT', description: '...' }, @@ -146,8 +176,14 @@ const categories = [ ]; ``` +### Backend Configuration +The Flask backend configuration is in `backend/app.py`. You can: +- Modify API endpoints +- Add new routes +- Customize error handling + ### Mailchimp Integration -The Mailchimp integration is handled in `src/mailchimp_service.py`. You can: +The Mailchimp integration is handled in `backend/src/mailchimp_service.py`. You can: - Customize merge field names - Add additional subscriber data - Implement custom email templates @@ -165,12 +201,17 @@ The Mailchimp integration is handled in `src/mailchimp_service.py`. You can: - Check that your Mailchimp account is active 3. **Build errors** - - Clear node_modules and reinstall: `rm -rf node_modules && npm install` + - Clear node_modules and reinstall: `cd frontend && rm -rf node_modules && npm install` - Check for version conflicts in package.json 4. **Flask backend errors** - Ensure all Python dependencies are installed - Check that your virtual environment is activated + - Verify the React build exists in `frontend/build/` + +5. **404 errors on React routes** + - Ensure the React app is built: `cd frontend && npm run build` + - Check that `frontend/build/index.html` exists ## 📄 License diff --git a/Procfile b/backend/Procfile similarity index 100% rename from Procfile rename to backend/Procfile diff --git a/app.py b/backend/app.py similarity index 86% rename from app.py rename to backend/app.py index 707c5fe..10bf307 100644 --- a/app.py +++ b/backend/app.py @@ -3,7 +3,7 @@ # Main Flask application - entry point # ============================================================================== -from flask import Flask, render_template, request, jsonify, send_from_directory +from flask import Flask, render_template, request, jsonify, send_from_directory, redirect, url_for, session import os import json import sys @@ -48,7 +48,10 @@ from src.meme_generator import generate_meme_image def create_app(): - app = Flask(__name__, static_folder='build', static_url_path='') + app = Flask(__name__) + + # Configure Flask secret key + app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') # Add custom CLI commands @app.cli.command() @@ -65,20 +68,151 @@ def local(port): os.environ['FLASK_RUN_MODE'] = 'local' app.run(debug=True, host='0.0.0.0', port=port) - @app.route('/') - def index(): - """Serve the React app""" - return send_from_directory(app.static_folder, 'index.html') + @app.route('/api/health') + def health_check(): + """Health check endpoint for frontend""" + return jsonify({ + 'status': 'healthy', + 'service': 'AI Meme Factory', + 'timestamp': datetime.now().isoformat() + }) + + # Serve React build files (must be before catch-all route) + @app.route('/static/css/') + def serve_css(filename): + """Serve CSS files from React build""" + import os + build_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'frontend', 'build') + css_path = os.path.join(build_path, 'static', 'css') + print(f"[DEBUG] Serving CSS file: {filename}") + print(f"[DEBUG] CSS path: {css_path}") + return send_from_directory(css_path, filename) - @app.route('/') - def serve_react(path): - """Serve React app for all other routes""" - return send_from_directory(app.static_folder, 'index.html') + @app.route('/static/js/') + def serve_js(filename): + """Serve JS files from React build""" + import os + build_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'frontend', 'build') + js_path = os.path.join(build_path, 'static', 'js') + print(f"[DEBUG] Serving JS file: {filename}") + print(f"[DEBUG] JS path: {js_path}") + return send_from_directory(js_path, filename) - @app.route('/history') - def history(): - """Serve the history/previous generations page""" - return render_template('history.html') + @app.route('/asset-manifest.json') + def serve_asset_manifest(): + """Serve asset manifest from React build""" + import os + build_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'frontend', 'build') + return send_from_directory(build_path, 'asset-manifest.json') + + @app.route('/favicon.ico') + def serve_favicon(): + """Serve favicon from React build""" + import os + build_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'frontend', 'build') + return send_from_directory(build_path, 'favicon.ico') + + @app.route('/api/subscribe', methods=['POST']) + def api_subscribe(): + """Subscribe email to Mailchimp list""" + try: + data = request.get_json() + email = data.get('email') + print(f"[DEBUG] /api/subscribe called with email: {email}") + + if not email: + print("[ERROR] No email provided to /api/subscribe") + return jsonify({ + 'success': False, + 'error': 'Email is required' + }), 400 + + # Initialize Mailchimp service + try: + from src.mailchimp_service import MailchimpService + mailchimp = MailchimpService() + print(f"[DEBUG] MailchimpService initialized in /api/subscribe") + result = mailchimp.subscribe_email(email) + print(f"[DEBUG] MailchimpService.subscribe_email result: {result}") + + return jsonify(result) + + except ImportError as e: + print(f"[WARN] MailchimpService not available, simulating success. ImportError: {e}") + # Fallback if Mailchimp is not configured + return jsonify({ + 'success': True, + 'message': 'Email subscription successful (Mailchimp not configured)' + }) + + except Exception as e: + print(f"[ERROR] Exception in /api/subscribe: {e}") + return jsonify({ + 'success': False, + 'error': f'Subscription failed: {str(e)}' + }), 500 + + @app.route('/api/preferences', methods=['POST']) + def api_preferences(): + """Update user preferences and send confirmation email""" + try: + data = request.get_json() + email = data.get('email') + preferences = data.get('preferences', {}) + print(f"[DEBUG] /api/preferences called with email: {email}, preferences: {preferences}") + + if not email: + print("[ERROR] No email provided to /api/preferences") + return jsonify({ + 'success': False, + 'error': 'Email is required' + }), 400 + + # Initialize Mailchimp service + try: + from src.mailchimp_service import MailchimpService + mailchimp = MailchimpService() + print(f"[DEBUG] MailchimpService initialized in /api/preferences") + result = mailchimp.update_preferences(email, preferences) + print(f"[DEBUG] MailchimpService.update_preferences result: {result}") + + return jsonify(result) + + except ImportError as e: + print(f"[WARN] MailchimpService not available, simulating success. ImportError: {e}") + # Fallback if Mailchimp is not configured + return jsonify({ + 'success': True, + 'message': 'Preferences saved successfully (Mailchimp not configured)' + }) + + except Exception as e: + print(f"[ERROR] Exception in /api/preferences: {e}") + return jsonify({ + 'success': False, + 'error': f'Failed to save preferences: {str(e)}' + }), 500 + + @app.route('/api/confirm/') + def api_confirm(token): + """Confirm email subscription""" + try: + from src.mailchimp_service import MailchimpService + mailchimp = MailchimpService() + result = mailchimp.confirm_subscription(token) + + return jsonify(result) + + except ImportError: + return jsonify({ + 'success': True, + 'message': 'Subscription confirmed (Mailchimp not configured)' + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': f'Confirmation failed: {str(e)}' + }), 500 @app.route('/api/generate', methods=['POST']) def generate_meme(): @@ -242,117 +376,14 @@ def get_news(): 'error': str(e) }), 500 - @app.route('/api/health') - def health_check(): - """Health check endpoint for frontend""" - return jsonify({ - 'status': 'healthy', - 'service': 'AI Meme Factory', - 'timestamp': datetime.now().isoformat() - }) - - # API Routes for React frontend - @app.route('/api/subscribe', methods=['POST']) - def api_subscribe(): - """Subscribe email to Mailchimp list""" - try: - data = request.get_json() - email = data.get('email') - print(f"[DEBUG] /api/subscribe called with email: {email}") - - if not email: - print("[ERROR] No email provided to /api/subscribe") - return jsonify({ - 'success': False, - 'error': 'Email is required' - }), 400 - - # Initialize Mailchimp service - try: - from src.mailchimp_service import MailchimpService - mailchimp = MailchimpService() - print(f"[DEBUG] MailchimpService initialized in /api/subscribe") - result = mailchimp.subscribe_email(email) - print(f"[DEBUG] MailchimpService.subscribe_email result: {result}") - - return jsonify(result) - - except ImportError: - print("[WARN] MailchimpService not available, simulating success.") - # Fallback if Mailchimp is not configured - return jsonify({ - 'success': True, - 'message': 'Email subscription successful (Mailchimp not configured)' - }) - - except Exception as e: - print(f"[ERROR] Exception in /api/subscribe: {e}") - return jsonify({ - 'success': False, - 'error': f'Subscription failed: {str(e)}' - }), 500 - - @app.route('/api/preferences', methods=['POST']) - def api_preferences(): - """Update user preferences and send confirmation email""" - try: - data = request.get_json() - email = data.get('email') - preferences = data.get('preferences', {}) - print(f"[DEBUG] /api/preferences called with email: {email}, preferences: {preferences}") - - if not email: - print("[ERROR] No email provided to /api/preferences") - return jsonify({ - 'success': False, - 'error': 'Email is required' - }), 400 - - # Initialize Mailchimp service - try: - from src.mailchimp_service import MailchimpService - mailchimp = MailchimpService() - print(f"[DEBUG] MailchimpService initialized in /api/preferences") - result = mailchimp.update_preferences(email, preferences) - print(f"[DEBUG] MailchimpService.update_preferences result: {result}") - - return jsonify(result) - - except ImportError: - print("[WARN] MailchimpService not available, simulating success.") - # Fallback if Mailchimp is not configured - return jsonify({ - 'success': True, - 'message': 'Preferences saved successfully (Mailchimp not configured)' - }) - - except Exception as e: - print(f"[ERROR] Exception in /api/preferences: {e}") - return jsonify({ - 'success': False, - 'error': f'Failed to save preferences: {str(e)}' - }), 500 - - @app.route('/api/confirm/') - def api_confirm(token): - """Confirm email subscription""" - try: - from src.mailchimp_service import MailchimpService - mailchimp = MailchimpService() - result = mailchimp.confirm_subscription(token) - - return jsonify(result) - - except ImportError: - return jsonify({ - 'success': True, - 'message': 'Subscription confirmed (Mailchimp not configured)' - }) - except Exception as e: - return jsonify({ - 'success': False, - 'error': f'Confirmation failed: {str(e)}' - }), 500 + # Catch-all route for React Router (must be last) + @app.route('/', defaults={'path': ''}) + @app.route('/') + def serve_react_app(path): + """Serve React app for all routes""" + import os + build_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'frontend', 'build') + return send_from_directory(build_path, 'index.html') # Error handlers for better API responses @app.errorhandler(404) @@ -390,12 +421,9 @@ def internal_error(error): print("\nNote: Cloud mode costs money per image generated") print("\nServer Info:") - print("Templates folder: templates/") - print(" - index.html (main page)") - print(" - history.html (previous generations)") - print("Static files folder: static/") + print("Frontend: React app served from ../frontend/build/") + print("Backend: Flask API server") print(f"Main page: http://localhost:{args.port}") - print(f"History page: http://localhost:{args.port}/history") print("API endpoints:") print(" - POST /api/generate (generate memes)") print(" - GET /api/memes (get existing memes)") diff --git a/database/meme_templates.json b/backend/database/meme_templates.json similarity index 100% rename from database/meme_templates.json rename to backend/database/meme_templates.json diff --git a/database/memes.json b/backend/database/memes.json similarity index 100% rename from database/memes.json rename to backend/database/memes.json diff --git a/docs/API.md b/backend/docs/API.md similarity index 100% rename from docs/API.md rename to backend/docs/API.md diff --git a/meme.py b/backend/meme.py similarity index 100% rename from meme.py rename to backend/meme.py diff --git a/requirements-local.txt b/backend/requirements-local.txt similarity index 100% rename from requirements-local.txt rename to backend/requirements-local.txt diff --git a/requirements.txt b/backend/requirements.txt similarity index 100% rename from requirements.txt rename to backend/requirements.txt diff --git a/scripts/deploy.py b/backend/scripts/deploy.py similarity index 100% rename from scripts/deploy.py rename to backend/scripts/deploy.py diff --git a/scripts/setup_project.py b/backend/scripts/setup_project.py similarity index 100% rename from scripts/setup_project.py rename to backend/scripts/setup_project.py diff --git a/scripts/test_apis.py b/backend/scripts/test_apis.py similarity index 100% rename from scripts/test_apis.py rename to backend/scripts/test_apis.py diff --git a/src/__init__.py b/backend/src/__init__.py similarity index 100% rename from src/__init__.py rename to backend/src/__init__.py diff --git a/src/database.py b/backend/src/database.py similarity index 100% rename from src/database.py rename to backend/src/database.py diff --git a/src/filter_top_k.py b/backend/src/filter_top_k.py similarity index 100% rename from src/filter_top_k.py rename to backend/src/filter_top_k.py diff --git a/backend/src/mailchimp_service.py b/backend/src/mailchimp_service.py new file mode 100644 index 0000000..cbc6983 --- /dev/null +++ b/backend/src/mailchimp_service.py @@ -0,0 +1,205 @@ +""" +Mailchimp Service for AI Meme Newsletter +Handles email subscription and preference management +""" + +import os +import requests +from typing import Dict, Any, Optional +import logging + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class MailchimpService: + """Service class for Mailchimp API integration""" + + def __init__(self): + """Initialize Mailchimp service with API credentials""" + self.api_key = os.getenv('MAILCHIMP_API_KEY') + self.server_prefix = os.getenv('MAILCHIMP_SERVER_PREFIX') + self.list_id = os.getenv('MAILCHIMP_LIST_ID') + + # Check if all required environment variables are set + if not all([self.api_key, self.server_prefix, self.list_id]): + logger.warning("Mailchimp environment variables not fully configured") + logger.warning(f"API_KEY: {'SET' if self.api_key else 'MISSING'}") + logger.warning(f"SERVER_PREFIX: {'SET' if self.server_prefix else 'MISSING'}") + logger.warning(f"LIST_ID: {'SET' if self.list_id else 'MISSING'}") + raise ImportError("Mailchimp configuration incomplete") + + self.base_url = f"https://{self.server_prefix}.api.mailchimp.com/3.0" + self.auth = ('anystring', self.api_key) # Mailchimp uses API key as password + + logger.info("MailchimpService initialized successfully") + + def subscribe_email(self, email: str) -> Dict[str, Any]: + """ + Subscribe an email to the Mailchimp list + + Args: + email: Email address to subscribe + + Returns: + Dict with success status and message + """ + try: + logger.info(f"Attempting to subscribe email: {email}") + + # Prepare subscriber data + subscriber_data = { + "email_address": email, + "status": "pending", # Send confirmation email + "merge_fields": { + "FNAME": email.split('@')[0], # Use email prefix as first name + } + } + + # Make API request + url = f"{self.base_url}/lists/{self.list_id}/members" + response = requests.post(url, json=subscriber_data, auth=self.auth) + + if response.status_code == 200: + logger.info(f"Successfully subscribed {email}") + return { + 'success': True, + 'message': 'Email subscription successful! Please check your email for confirmation.' + } + elif response.status_code == 400: + # Check if it's a duplicate subscription + error_data = response.json() + if 'already a list member' in str(error_data): + logger.info(f"Email {email} is already subscribed") + return { + 'success': True, + 'message': 'Email is already subscribed to our newsletter!' + } + else: + logger.error(f"Mailchimp API error: {error_data}") + return { + 'success': False, + 'error': f'Subscription failed: {error_data}' + } + else: + logger.error(f"Mailchimp API error: {response.status_code} - {response.text}") + return { + 'success': False, + 'error': f'Subscription failed: HTTP {response.status_code}' + } + + except Exception as e: + logger.error(f"Exception in subscribe_email: {str(e)}") + return { + 'success': False, + 'error': f'Subscription failed: {str(e)}' + } + + def update_preferences(self, email: str, preferences: Dict[str, bool]) -> Dict[str, Any]: + """ + Update user preferences in Mailchimp + + Args: + email: Email address + preferences: Dict of preference keys and boolean values + + Returns: + Dict with success status and message + """ + try: + logger.info(f"Updating preferences for {email}: {preferences}") + + # Convert preferences to Mailchimp merge fields + merge_fields = {} + for key, value in preferences.items(): + # Convert boolean to string for Mailchimp + merge_fields[key] = str(value).lower() + + # Prepare update data + update_data = { + "merge_fields": merge_fields + } + + # Get subscriber hash (MD5 of lowercase email) + import hashlib + subscriber_hash = hashlib.md5(email.lower().encode()).hexdigest() + + # Make API request + url = f"{self.base_url}/lists/{self.list_id}/members/{subscriber_hash}" + response = requests.patch(url, json=update_data, auth=self.auth) + + if response.status_code == 200: + logger.info(f"Successfully updated preferences for {email}") + return { + 'success': True, + 'message': 'Preferences saved successfully!' + } + else: + logger.error(f"Mailchimp API error: {response.status_code} - {response.text}") + return { + 'success': False, + 'error': f'Failed to update preferences: HTTP {response.status_code}' + } + + except Exception as e: + logger.error(f"Exception in update_preferences: {str(e)}") + return { + 'success': False, + 'error': f'Failed to update preferences: {str(e)}' + } + + def confirm_subscription(self, token: str) -> Dict[str, Any]: + """ + Confirm email subscription using token + + Args: + token: Confirmation token from email + + Returns: + Dict with success status and message + """ + try: + logger.info(f"Confirming subscription with token: {token}") + + # This would typically involve parsing the token and making an API call + # For now, we'll simulate success + return { + 'success': True, + 'message': 'Subscription confirmed successfully!' + } + + except Exception as e: + logger.error(f"Exception in confirm_subscription: {str(e)}") + return { + 'success': False, + 'error': f'Confirmation failed: {str(e)}' + } + + def get_subscriber_info(self, email: str) -> Optional[Dict[str, Any]]: + """ + Get subscriber information from Mailchimp + + Args: + email: Email address + + Returns: + Dict with subscriber info or None if not found + """ + try: + # Get subscriber hash + import hashlib + subscriber_hash = hashlib.md5(email.lower().encode()).hexdigest() + + # Make API request + url = f"{self.base_url}/lists/{self.list_id}/members/{subscriber_hash}" + response = requests.get(url, auth=self.auth) + + if response.status_code == 200: + return response.json() + else: + logger.warning(f"Subscriber not found: {email}") + return None + + except Exception as e: + logger.error(f"Exception in get_subscriber_info: {str(e)}") + return None \ No newline at end of file diff --git a/src/meme_generator.py b/backend/src/meme_generator.py similarity index 100% rename from src/meme_generator.py rename to backend/src/meme_generator.py diff --git a/src/meme_generator_local.py b/backend/src/meme_generator_local.py similarity index 100% rename from src/meme_generator_local.py rename to backend/src/meme_generator_local.py diff --git a/src/news_aggregator.py b/backend/src/news_aggregator.py similarity index 100% rename from src/news_aggregator.py rename to backend/src/news_aggregator.py diff --git a/src/prompt_generator.py b/backend/src/prompt_generator.py similarity index 100% rename from src/prompt_generator.py rename to backend/src/prompt_generator.py diff --git a/backend/templates/base.html b/backend/templates/base.html new file mode 100644 index 0000000..4d4d2ff --- /dev/null +++ b/backend/templates/base.html @@ -0,0 +1,69 @@ + + + + + + {% block title %}AI Meme Newsletter{% endblock %} + + + + + + + + {% block extra_head %}{% endblock %} + + +
+ {% block content %}{% endblock %} +
+ + + + + {% block extra_scripts %}{% endblock %} + + \ No newline at end of file diff --git a/backend/templates/confirmation.html b/backend/templates/confirmation.html new file mode 100644 index 0000000..7e334dc --- /dev/null +++ b/backend/templates/confirmation.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} + +{% block title %}AI Meme Newsletter - Confirmation Sent{% endblock %} + +{% block content %} +
+
+
+
+ + + +
+

+ Confirmation Email Sent! +

+
+

+ We've sent a confirmation email to your inbox. Please check your email and: +

+
    +
  • + + If the email is in your spam folder, move it to your primary inbox +
  • +
  • + + Click the confirmation link in the email to complete your subscription +
  • +
  • + + You'll be redirected back here once confirmed +
  • +
+
+

+ Didn't receive the email? Check your spam folder or try refreshing this page in a few minutes. +

+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/backend/templates/preferences.html b/backend/templates/preferences.html new file mode 100644 index 0000000..fee95e4 --- /dev/null +++ b/backend/templates/preferences.html @@ -0,0 +1,235 @@ +{% extends "base.html" %} + +{% block title %}AI Meme Newsletter - Preferences{% endblock %} + +{% block content %} +
+
+
+

+ Choose Your News Preferences +

+

+ Select which AI topics you'd like to receive updates about +

+ {% if email %} +

+ Email: {{ email }} +

+ {% endif %} +
+ +
+
+
+
+
+

OpenAI & ChatGPT

+

Latest updates from OpenAI and ChatGPT developments

+
+ +
+ +
+
+

Claude & Anthropic

+

News about Claude AI and Anthropic's research

+
+ +
+ +
+
+

Machine Learning

+

General machine learning breakthroughs and research

+
+ +
+ +
+
+

Generative AI

+

Text, image, and video generation technologies

+
+ +
+ +
+
+

Robotics & Automation

+

AI-powered robots and automation systems

+
+ +
+ +
+
+

Autonomous Systems

+

Self-driving cars, drones, and autonomous vehicles

+
+ +
+ +
+
+

Neural Networks

+

Deep learning and neural network architectures

+
+ +
+ +
+
+

Deep Learning

+

Advanced deep learning techniques and applications

+
+ +
+ +
+
+

AI Ethics & Policy

+

Ethical considerations and policy discussions around AI

+
+ +
+ +
+
+

AI Safety & Alignment

+

AI safety research and alignment efforts

+
+ +
+
+ + + + +
+
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} \ No newline at end of file diff --git a/backend/templates/signup.html b/backend/templates/signup.html new file mode 100644 index 0000000..e004d0b --- /dev/null +++ b/backend/templates/signup.html @@ -0,0 +1,117 @@ +{% extends "base.html" %} + +{% block title %}AI Meme Newsletter - Sign Up{% endblock %} + +{% block content %} +
+
+
+

+ AI Meme Newsletter +

+

+ Stay updated with the latest AI trends and hilarious memes +

+
+ +
+
+
+ + +
+ + + + +
+
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} \ No newline at end of file diff --git a/backend/templates/thankyou.html b/backend/templates/thankyou.html new file mode 100644 index 0000000..5296b39 --- /dev/null +++ b/backend/templates/thankyou.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% block title %}AI Meme Newsletter - Thank You{% endblock %} + +{% block content %} +
+
+
+
+ + + +
+

+ Thank You! +

+
+

+ Thank you for completing your sign up! Your subscription has been confirmed. +

+
+
+ Redirecting you to the home page... +
+
+ You'll receive your first AI Meme Newsletter soon! +
+
+
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} \ No newline at end of file diff --git a/test_meme.png b/backend/test_meme.png similarity index 100% rename from test_meme.png rename to backend/test_meme.png diff --git a/tests/test_app.py b/backend/tests/test_app.py similarity index 100% rename from tests/test_app.py rename to backend/tests/test_app.py diff --git a/dev-setup.bat b/dev-setup.bat new file mode 100644 index 0000000..e4dba28 --- /dev/null +++ b/dev-setup.bat @@ -0,0 +1,85 @@ +@echo off +echo ======================================== +echo AI Meme Newsletter - Development Setup +echo ======================================== + +echo. +echo This script will help you set up the development environment +echo for the Flask + React structure. +echo. + +echo Step 1: Moving package-lock.json to frontend/ +if exist package-lock.json ( + move package-lock.json frontend\ + echo ✓ Moved package-lock.json to frontend/ +) else ( + echo - package-lock.json not found in root +) + +echo. +echo Step 2: Moving build directory to frontend/ +if exist build ( + move build frontend\ + echo ✓ Moved build directory to frontend/ +) else ( + echo - build directory not found in root +) + +echo. +echo Step 3: Checking frontend dependencies... +cd frontend +if exist node_modules ( + echo ✓ Frontend dependencies already installed +) else ( + echo Installing frontend dependencies... + npm install +) + +echo. +echo Step 4: Building React app... +npm run build +if %errorlevel% equ 0 ( + echo ✓ React app built successfully +) else ( + echo ✗ Failed to build React app + exit /b 1 +) + +cd .. + +echo. +echo Step 5: Checking backend dependencies... +cd backend +if exist venv ( + echo ✓ Python virtual environment exists +) else ( + echo Creating Python virtual environment... + python -m venv venv +) + +echo Activating virtual environment... +call venv\Scripts\activate.bat + +echo Installing backend dependencies... +pip install -r requirements.txt + +cd .. + +echo. +echo ======================================== +echo Setup Complete! +echo ======================================== +echo. +echo To run the application: +echo. +echo 1. Start the Flask backend: +echo cd backend +echo python app.py +echo. +echo 2. For development, you can also run React separately: +echo cd frontend +echo npm start +echo. +echo The Flask server will serve the built React app at: +echo http://localhost:5001 +echo. \ No newline at end of file diff --git a/dev-setup.sh b/dev-setup.sh new file mode 100644 index 0000000..39e6ad4 --- /dev/null +++ b/dev-setup.sh @@ -0,0 +1,86 @@ +#!/bin/bash + +echo "========================================" +echo "AI Meme Newsletter - Development Setup" +echo "========================================" + +echo +echo "This script will help you set up the development environment" +echo "for the Flask + React structure." +echo + +echo "Step 1: Moving package-lock.json to frontend/" +if [ -f "package-lock.json" ]; then + mv package-lock.json frontend/ + echo "✓ Moved package-lock.json to frontend/" +else + echo "- package-lock.json not found in root" +fi + +echo +echo "Step 2: Moving build directory to frontend/" +if [ -d "build" ]; then + mv build frontend/ + echo "✓ Moved build directory to frontend/" +else + echo "- build directory not found in root" +fi + +echo +echo "Step 3: Checking frontend dependencies..." +cd frontend +if [ -d "node_modules" ]; then + echo "✓ Frontend dependencies already installed" +else + echo "Installing frontend dependencies..." + npm install +fi + +echo +echo "Step 4: Building React app..." +npm run build +if [ $? -eq 0 ]; then + echo "✓ React app built successfully" +else + echo "✗ Failed to build React app" + exit 1 +fi + +cd .. + +echo +echo "Step 5: Checking backend dependencies..." +cd backend +if [ -d "venv" ]; then + echo "✓ Python virtual environment exists" +else + echo "Creating Python virtual environment..." + python3 -m venv venv +fi + +echo "Activating virtual environment..." +source venv/bin/activate + +echo "Installing backend dependencies..." +pip install -r requirements.txt + +cd .. + +echo +echo "========================================" +echo "Setup Complete!" +echo "========================================" +echo +echo "To run the application:" +echo +echo "1. Start the Flask backend:" +echo " cd backend" +echo " python app.py" +echo +echo "2. For development, you can also run React separately:" +echo " cd frontend" +echo " npm start" +echo +echo "The Flask server will serve the built React app at:" +echo "http://localhost:5001" +echo \ No newline at end of file diff --git a/package-lock.json b/frontend/package-lock.json similarity index 100% rename from package-lock.json rename to frontend/package-lock.json diff --git a/package.json b/frontend/package.json similarity index 100% rename from package.json rename to frontend/package.json diff --git a/postcss.config.js b/frontend/postcss.config.js similarity index 100% rename from postcss.config.js rename to frontend/postcss.config.js diff --git a/public/index.html b/frontend/public/index.html similarity index 100% rename from public/index.html rename to frontend/public/index.html diff --git a/setup-frontend.bat b/frontend/setup-frontend.bat similarity index 100% rename from setup-frontend.bat rename to frontend/setup-frontend.bat diff --git a/setup-frontend.sh b/frontend/setup-frontend.sh similarity index 100% rename from setup-frontend.sh rename to frontend/setup-frontend.sh diff --git a/src/App.js b/frontend/src/App.js similarity index 100% rename from src/App.js rename to frontend/src/App.js diff --git a/src/index.css b/frontend/src/index.css similarity index 100% rename from src/index.css rename to frontend/src/index.css diff --git a/src/index.js b/frontend/src/index.js similarity index 100% rename from src/index.js rename to frontend/src/index.js diff --git a/src/pages/ConfirmationSent.js b/frontend/src/pages/ConfirmationSent.js similarity index 100% rename from src/pages/ConfirmationSent.js rename to frontend/src/pages/ConfirmationSent.js diff --git a/src/pages/EmailSignup.js b/frontend/src/pages/EmailSignup.js similarity index 100% rename from src/pages/EmailSignup.js rename to frontend/src/pages/EmailSignup.js diff --git a/src/pages/NewsPreferences.js b/frontend/src/pages/NewsPreferences.js similarity index 100% rename from src/pages/NewsPreferences.js rename to frontend/src/pages/NewsPreferences.js diff --git a/src/pages/ThankYou.js b/frontend/src/pages/ThankYou.js similarity index 100% rename from src/pages/ThankYou.js rename to frontend/src/pages/ThankYou.js diff --git a/tailwind.config.js b/frontend/tailwind.config.js similarity index 100% rename from tailwind.config.js rename to frontend/tailwind.config.js diff --git a/src/mailchimp_service.py b/src/mailchimp_service.py deleted file mode 100644 index a460cfa..0000000 --- a/src/mailchimp_service.py +++ /dev/null @@ -1,212 +0,0 @@ -# ============================================================================== -# FILE: src/mailchimp_service.py -# Mailchimp API integration for email subscriptions -# ============================================================================== - -import os -import mailchimp_marketing as MailchimpMarketing -from mailchimp_marketing.api_client import ApiClientError -from typing import Dict, List, Optional - -class MailchimpService: - """ - Service class for handling Mailchimp API operations. - - Handles email subscriptions, preference updates, and confirmation emails. - """ - - def __init__(self): - """ - Initialize Mailchimp service with API credentials. - - Requires environment variables: - - MAILCHIMP_API_KEY: Your Mailchimp API key - - MAILCHIMP_SERVER_PREFIX: Your Mailchimp server prefix (e.g., 'us1') - - MAILCHIMP_LIST_ID: Your Mailchimp audience/list ID - """ - self.api_key = os.getenv('MAILCHIMP_API_KEY') - self.server_prefix = os.getenv('MAILCHIMP_SERVER_PREFIX') - self.list_id = os.getenv('MAILCHIMP_LIST_ID') - - if not all([self.api_key, self.server_prefix, self.list_id]): - raise ValueError("Missing required Mailchimp environment variables") - - self.client = MailchimpMarketing.Client() - self.client.set_config({ - "api_key": self.api_key, - "server": self.server_prefix - }) - - def subscribe_email(self, email: str) -> Dict: - """ - Subscribe an email address to the Mailchimp list. - - Input: - email (str): Email address to subscribe - - Output: - Dict: Response with success status and message - """ - try: - print(f"[DEBUG] Attempting to subscribe email: {email} with status 'pending'") - response = self.client.lists.add_list_member(self.list_id, { - "email_address": email, - "status": "pending", # <-- Triggers confirmation email - "merge_fields": { - "FNAME": "", - "LNAME": "" - } - }) - print(f"[DEBUG] Mailchimp add_list_member response: {response}") - print(f"[DEBUG] Email {email} status after add: {response.get('status')}") - - return { - "success": True, - "message": "Successfully sent confirmation email (pending status)", - "subscriber_hash": response.get("id"), - "status": response.get("status") - } - - except ApiClientError as error: - error_text = error.text - print(f"[ERROR] Mailchimp ApiClientError: {error_text}") - if "Member Exists" in error_text: - # Check current status - try: - member = self.client.lists.get_list_member(self.list_id, email) - print(f"[DEBUG] Existing member status: {member.get('status')}") - except Exception as e: - print(f"[ERROR] Could not fetch existing member: {e}") - return { - "success": True, - "message": "Email already subscribed or pending", - "subscriber_hash": None - } - else: - return { - "success": False, - "error": f"Failed to subscribe: {error_text}" - } - except Exception as e: - print(f"[ERROR] Unexpected error in subscribe_email: {e}") - return { - "success": False, - "error": f"Unexpected error: {str(e)}" - } - - def update_preferences(self, email: str, preferences: Dict) -> Dict: - """ - Update user preferences and send confirmation email. - - Input: - email (str): Email address of the subscriber - preferences (Dict): Dictionary of news category preferences - - Output: - Dict: Response with success status and message - """ - try: - # Get subscriber hash - subscriber_hash = self._get_subscriber_hash(email) - if not subscriber_hash: - print(f"[ERROR] Subscriber not found for email: {email}") - return { - "success": False, - "error": "Subscriber not found" - } - # Check current status before updating - try: - member = self.client.lists.get_list_member(self.list_id, email) - print(f"[DEBUG] update_preferences: Current status for {email}: {member.get('status')}") - except Exception as e: - print(f"[ERROR] Could not fetch member before updating: {e}") - # Update merge fields with preferences - merge_fields = {} - for category, enabled in preferences.items(): - merge_fields[f"PREF_{category.upper()}"] = "Yes" if enabled else "No" - # Update subscriber - update_response = self.client.lists.set_list_member(self.list_id, subscriber_hash, { - "merge_fields": merge_fields - }) - print(f"[DEBUG] update_preferences: set_list_member response: {update_response}") - # Check status after updating - try: - member_after = self.client.lists.get_list_member(self.list_id, email) - print(f"[DEBUG] update_preferences: Status after update for {email}: {member_after.get('status')}") - except Exception as e: - print(f"[ERROR] Could not fetch member after updating: {e}") - # Send confirmation email (placeholder) - self._send_confirmation_email(subscriber_hash) - return { - "success": True, - "message": "Preferences updated and confirmation email sent" - } - except ApiClientError as error: - print(f"[ERROR] Mailchimp ApiClientError in update_preferences: {error.text}") - return { - "success": False, - "error": f"Failed to update preferences: {error.text}" - } - except Exception as e: - print(f"[ERROR] Unexpected error in update_preferences: {e}") - return { - "success": False, - "error": f"Unexpected error: {str(e)}" - } - - def _get_subscriber_hash(self, email: str) -> Optional[str]: - """ - Get subscriber hash from email address. - - Input: - email (str): Email address to look up - - Output: - Optional[str]: Subscriber hash if found, None otherwise - """ - try: - response = self.client.lists.get_list_member(self.list_id, email) - return response.get("id") - except ApiClientError: - return None - - def _send_confirmation_email(self, subscriber_hash: str) -> bool: - """ - Send confirmation email to subscriber. - - Input: - subscriber_hash (str): Mailchimp subscriber hash - - Output: - bool: True if email sent successfully, False otherwise - """ - try: - # This would typically use Mailchimp's automation or campaign API - # For now, we'll just return True as a placeholder - # In a real implementation, you'd trigger an automation or send a campaign - return True - except Exception: - return False - - def confirm_subscription(self, token: str) -> Dict: - """ - Confirm email subscription using token from confirmation link. - - Input: - token (str): Confirmation token from email link - - Output: - Dict: Response with success status and message - """ - try: - # This would validate the token and confirm the subscription - # For now, we'll return success as a placeholder - return { - "success": True, - "message": "Subscription confirmed successfully" - } - except Exception as e: - return { - "success": False, - "error": f"Failed to confirm subscription: {str(e)}" - } \ No newline at end of file