diff --git a/joke-generator/README.md b/joke-generator/README.md index c105083..29259f4 100644 --- a/joke-generator/README.md +++ b/joke-generator/README.md @@ -1,8 +1,29 @@ -# šŸ˜„ Random Joke Generator +# šŸ˜„ Enhanced Random Joke Generator -A complete, production-ready joke generator that fetches random jokes from an external API (JokeAPI). Includes implementations in Python and JavaScript with a beautiful web UI. +A complete, production-ready joke generator with **Favorites**, **Dark Mode**, and **Copy to Clipboard** features! Includes implementations in Python and JavaScript with a beautiful responsive web UI. -## šŸ“‹ Features +## ✨ New Features Added + +### šŸŒ™ Dark Mode +- Toggle between light and dark themes +- Preference saved to browser (localStorage) +- Beautiful color transitions +- Fully responsive design + +### ⭐ Favorites System +- Save jokes you love +- View all favorite jokes in a modal +- Manage favorites (copy, delete) +- Favorites persist in local storage (web) or JSON file (Python) +- Visual indicator for favorited jokes + +### šŸ“‹ Copy to Clipboard +- One-click copy jokes to clipboard +- Smooth visual feedback +- Works with full two-part jokes (setup + punchline) +- Toast notification on successful copy + +## šŸ“‹ Features Overview ✨ **Multiple Implementations** - Python CLI with interactive menu @@ -21,10 +42,16 @@ A complete, production-ready joke generator that fetches random jokes from an ex - Graceful failure messages šŸŽØ **Beautiful UI** -- Gradient design -- Smooth animations +- Gradient design with smooth transitions +- Dark mode support - Mobile responsive - Punchline reveal functionality +- Action buttons for favorites and copy + +šŸ“± **Data Persistence** +- Browser localStorage for web UI +- JSON file storage for Python CLI +- Automatic preference saving šŸ“” **External API Integration** - Uses [JokeAPI](https://jokeapi.dev/) @@ -47,9 +74,11 @@ python joke_generator.py **Features:** - Interactive menu system +- Save favorite jokes to file +- View all favorites +- Clear favorites - Press Enter to reveal punchlines - Safe mode enabled by default -- Session management with connection pooling ### Web UI @@ -57,21 +86,25 @@ python joke_generator.py Simply open `joke_generator.html` in any modern web browser! **Features:** -- Click "Get a Joke" to fetch a random joke -- Select joke type from dropdown -- Press Enter key as shortcut -- Automatic punchline reveal button for two-part jokes +- Click "Get a Joke" to fetch +- Toggle dark mode (šŸŒ™) +- View favorites (⭐) +- Copy jokes (šŸ“‹) +- Add to favorites (šŸ¤) +- All preferences saved automatically ### JavaScript Module **Usage:** ```javascript const generator = new JokeGeneratorUI(); -generator.init(); // Initialize event listeners +generator.init(); // Initialize with all features -// Or fetch directly -const joke = await generator.fetchJoke("programming", true); -generator.displayJoke(joke); +// All features available through the UI: +// - Dark mode toggle +// - Favorites management +// - Copy to clipboard +// - Punchline reveal ``` ## šŸ“š API Reference @@ -109,47 +142,66 @@ GET https://v2.jokeapi.dev/joke/programming?safe-mode=true ## šŸ’» Code Examples -### Python - Simple Usage +### Python - Using Favorites ```python -from joke_generator import JokeGenerator +from joke_generator import EnhancedJokeGenerator -# Create instance -gen = JokeGenerator() +gen = EnhancedJokeGenerator() -# Fetch and display a joke +# Fetch a joke joke = gen.get_joke("programming", safe_mode=True) -if joke: - gen.display_joke(joke) +gen.display_joke(joke) + +# Save to favorites +gen.save_favorite(joke) + +# View all favorites +gen.view_favorites() + +# Remove a favorite +gen.remove_favorite(0) ``` -### Python - Custom Implementation +### JavaScript - Access Favorites -```python -from joke_generator import JokeGenerator +```javascript +const generator = new JokeGeneratorUI(); +generator.init(); + +// Add current joke to favorites +generator.toggleFavorite(); -gen = JokeGenerator(timeout=15) -joke_data = gen.get_joke("knock-knock") +// Open favorites modal +generator.openFavoritesModal(); -if joke_data["type"] == "twopart": - print(joke_data["setup"]) - input("Press Enter for the punchline...") - print(joke_data["delivery"]) +// Copy joke to clipboard +await generator.copyJokeToClipboard(); ``` -### JavaScript - Fetch Joke +### JavaScript - Dark Mode ```javascript -const ui = new JokeGeneratorUI(); +// Toggle dark mode +generator.toggleDarkMode(); -// Fetch a programming joke -const joke = await ui.fetchJoke("programming", true); - -// Display it -ui.displayJoke(joke); +// Check current mode +console.log(generator.darkMode); ``` -## šŸ”§ Customization +## šŸŽØ Customization + +### Change Colors + +**CSS Variables in HTML:** +```css +:root { + --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --bg-primary: #ffffff; + --text-primary: #333333; + --accent-color: #667eea; +} +``` ### Change API Endpoint @@ -175,44 +227,48 @@ joke = generator.get_joke("any", safe_mode=False) const joke = await generator.fetchJoke("any", false); ``` -### Adjust Timeout +## šŸ“‚ File Structure -**Python:** -```python -generator = JokeGenerator(timeout=20) ``` - -**JavaScript:** -```javascript -generator.timeout = 20000; // milliseconds +joke-generator/ +ā”œā”€ā”€ joke_generator.py # Python CLI with favorites +ā”œā”€ā”€ joke_generator.js # JavaScript module (enhanced) +ā”œā”€ā”€ joke_generator.html # Web UI with dark mode +└── README.md # Documentation ``` ## 🌟 Future Enhancements -- [ ] Save favorite jokes to local storage +- [ ] Export favorites as CSV/JSON - [ ] Share jokes on social media -- [ ] Rate jokes (like/dislike) -- [ ] Search by keyword +- [ ] Joke rating system (1-5 stars) +- [ ] Search favorites by keyword - [ ] Multiple language support -- [ ] Daily joke email notifications -- [ ] Dark mode toggle -- [ ] Accessibility improvements (WCAG 2.1) - -## šŸ“ File Structure - -``` -joke-generator/ -ā”œā”€ā”€ joke_generator.py # Python CLI implementation -ā”œā”€ā”€ joke_generator.js # JavaScript module -ā”œā”€ā”€ joke_generator.html # Web UI -└── README.md # Documentation -``` +- [ ] Voice/Text-to-Speech +- [ ] Joke categories in modal +- [ ] Shuffle favorites mode +- [ ] Service Worker for offline support +- [ ] Unit tests and coverage ## šŸ”— Resources - [JokeAPI Documentation](https://jokeapi.dev/) - [Python Requests Library](https://requests.readthedocs.io/) - [MDN Web Docs - Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) +- [MDN Web Docs - localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) + +## šŸ’¾ Data Storage + +### Web UI (Browser) +- Favorites stored in `localStorage` (browser-specific, persists across sessions) +- Dark mode preference also in `localStorage` +- No server connection required +- Private to your browser + +### Python CLI +- Favorites stored in `joke_favorites.json` in the application directory +- Human-readable JSON format +- Can be shared or backed up easily ## šŸ“„ License @@ -225,3 +281,6 @@ Feel free to fork, modify, and use this project as you wish. Suggestions for imp --- **Made with ā¤ļø for the developer community** + +*Last Updated: 2026-05-14* +*Version: 2.0 (Enhanced)* diff --git a/joke-generator/joke_generator.html b/joke-generator/joke_generator.html index ba5b296..fc714d7 100644 --- a/joke-generator/joke_generator.html +++ b/joke-generator/joke_generator.html @@ -11,24 +11,51 @@ box-sizing: border-box; } + :root { + --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --text-primary: #333333; + --text-secondary: #666666; + --border-color: #e0e0e0; + --accent-color: #667eea; + --shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + } + + body.dark-mode { + --bg-primary: #1a1a2e; + --bg-secondary: #16213e; + --text-primary: #eaeaea; + --text-secondary: #b0b0b0; + --border-color: #404060; + --shadow: 0 20px 60px rgba(0, 0, 0, 0.7); + } + body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: var(--primary-gradient); min-height: 100vh; display: flex; justify-content: center; align-items: center; padding: 20px; + color: var(--text-primary); + transition: background 0.3s ease; + } + + body.dark-mode { + background: linear-gradient(135deg, #0f3460 0%, #16213e 100%); } .container { - background: white; + background: var(--bg-primary); border-radius: 20px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + box-shadow: var(--shadow); padding: 40px; - max-width: 600px; + max-width: 700px; width: 100%; animation: slideIn 0.5s ease-out; + transition: background 0.3s ease, color 0.3s ease; } @keyframes slideIn { @@ -43,25 +70,62 @@ } .header { - text-align: center; + display: flex; + justify-content: space-between; + align-items: center; margin-bottom: 30px; } + .header-content { + flex: 1; + } + .header h1 { font-size: 2.5em; - color: #333; margin-bottom: 10px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: var(--primary-gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .header p { - color: #666; + color: var(--text-secondary); font-size: 1.1em; } + .header-controls { + display: flex; + gap: 10px; + align-items: center; + } + + .icon-btn { + width: 45px; + height: 45px; + border: 2px solid var(--border-color); + background: var(--bg-primary); + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5em; + transition: all 0.3s; + color: var(--text-primary); + } + + .icon-btn:hover { + border-color: var(--accent-color); + transform: scale(1.05); + } + + .icon-btn.active { + background: var(--accent-color); + border-color: var(--accent-color); + color: white; + } + .controls { display: flex; gap: 10px; @@ -73,21 +137,22 @@ flex: 1; min-width: 150px; padding: 12px 15px; - border: 2px solid #e0e0e0; + border: 2px solid var(--border-color); border-radius: 10px; font-size: 1em; cursor: pointer; transition: border-color 0.3s; - background-color: white; + background-color: var(--bg-primary); + color: var(--text-primary); } select:hover { - border-color: #667eea; + border-color: var(--accent-color); } select:focus { outline: none; - border-color: #667eea; + border-color: var(--accent-color); box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } @@ -95,7 +160,7 @@ flex: 1; min-width: 150px; padding: 12px 30px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: var(--primary-gradient); color: white; border: none; border-radius: 10px; @@ -121,12 +186,13 @@ .joke-container { display: none; - background: #f8f9fa; + background: var(--bg-secondary); border-radius: 15px; padding: 25px; margin-bottom: 20px; animation: fadeIn 0.3s ease-in; - border-left: 5px solid #667eea; + border-left: 5px solid var(--accent-color); + transition: background 0.3s ease; } @keyframes fadeIn { @@ -140,26 +206,176 @@ #joke-text { font-size: 1.3em; - color: #333; + color: var(--text-primary); line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; + margin-bottom: 15px; + } + + .joke-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; + } + + .action-btn { + flex: 1; + min-width: 100px; + padding: 10px 15px; + background: var(--accent-color); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: 600; + font-size: 0.9em; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + gap: 5px; + } + + .action-btn:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); + } + + .action-btn.favorited { + background: #ff6b6b; + } + + .copy-feedback { + position: fixed; + top: 20px; + right: 20px; + background: #51cf66; + color: white; + padding: 12px 20px; + border-radius: 8px; + animation: slideInRight 0.3s ease; + z-index: 1000; + } + + @keyframes slideInRight { + from { + opacity: 0; + transform: translateX(100px); + } + to { + opacity: 1; + transform: translateX(0); + } } #punchline-btn { - margin-top: 15px; width: 100%; } + .favorites-modal { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + z-index: 999; + justify-content: center; + align-items: center; + padding: 20px; + } + + .favorites-modal.active { + display: flex; + animation: fadeIn 0.3s ease-in; + } + + .favorites-content { + background: var(--bg-primary); + border-radius: 20px; + padding: 30px; + max-width: 500px; + width: 100%; + max-height: 80vh; + overflow-y: auto; + transition: background 0.3s ease; + } + + .favorites-content h2 { + margin-bottom: 20px; + color: var(--text-primary); + } + + .favorite-item { + background: var(--bg-secondary); + padding: 15px; + border-radius: 10px; + margin-bottom: 10px; + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 10px; + transition: background 0.3s ease; + } + + .favorite-item-text { + flex: 1; + color: var(--text-primary); + line-height: 1.4; + } + + .favorite-item-actions { + display: flex; + gap: 5px; + } + + .favorite-item-actions button { + padding: 5px 10px; + font-size: 0.8em; + min-width: auto; + flex: none; + } + + .empty-favorites { + text-align: center; + color: var(--text-secondary); + padding: 40px 20px; + } + + .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + } + + .close-btn { + background: none; + border: none; + font-size: 1.5em; + cursor: pointer; + color: var(--text-primary); + width: auto; + padding: 0; + min-width: auto; + } + + .close-btn:hover { + color: var(--accent-color); + } + .footer { text-align: center; margin-top: 20px; - color: #999; + color: var(--text-secondary); font-size: 0.9em; + transition: color 0.3s ease; } .footer a { - color: #667eea; + color: var(--accent-color); text-decoration: none; transition: color 0.2s; } @@ -173,6 +389,16 @@ padding: 25px; } + .header { + flex-direction: column; + align-items: flex-start; + } + + .header-controls { + align-self: flex-end; + margin-top: 10px; + } + .header h1 { font-size: 2em; } @@ -185,14 +411,24 @@ min-width: unset; width: 100%; } + + .favorites-content { + max-height: 70vh; + } }
-

šŸ˜„ Joke Generator

-

Get random jokes instantly!

+
+

šŸ˜„ Joke Generator

+

Get random jokes instantly!

+
+
+ + +
@@ -207,7 +443,11 @@

šŸ˜„ Joke Generator

- + +
+ + +
+ +
+
+ +
+
+
+ diff --git a/joke-generator/joke_generator.js b/joke-generator/joke_generator.js index 23a2ce8..ac28e53 100644 --- a/joke-generator/joke_generator.js +++ b/joke-generator/joke_generator.js @@ -1,5 +1,6 @@ /** - * Random Joke Generator - JavaScript Implementation + * Random Joke Generator - Enhanced JavaScript Implementation + * Features: Favorites, Dark Mode, Copy to Clipboard * Fetches jokes from JokeAPI and displays them with smooth animations * Includes XSS protection and comprehensive error handling */ @@ -11,28 +12,282 @@ class JokeGeneratorUI { this.jokeTypes = ["any", "general", "programming", "knock-knock"]; this.currentJoke = null; this.isLoading = false; + this.favorites = this.loadFavorites(); + this.darkMode = this.loadDarkMode(); } /** - * Initialize event listeners + * Initialize event listeners and apply saved preferences */ init() { + // Apply dark mode preference + if (this.darkMode) { + document.body.classList.add("dark-mode"); + this.updateThemeButton(); + } + + // Get DOM elements const getJokeBtn = document.getElementById("get-joke-btn"); const punchlineBtn = document.getElementById("punchline-btn"); const jokeTypeSelect = document.getElementById("joke-type"); + const themeToggle = document.getElementById("theme-toggle"); + const favoritesBtn = document.getElementById("favorites-btn"); + const closeFavoritesBtn = document.getElementById("close-favorites"); + const copyBtn = document.getElementById("copy-btn"); + const favoriteBtn = document.getElementById("favorite-btn"); + const favoritesModal = document.getElementById("favorites-modal"); + // Event listeners if (getJokeBtn) { getJokeBtn.addEventListener("click", () => this.handleGetJoke()); - if (jokeTypeSelect) { - jokeTypeSelect.addEventListener("keypress", (e) => { - if (e.key === "Enter") this.handleGetJoke(); - }); - } + jokeTypeSelect.addEventListener("keypress", (e) => { + if (e.key === "Enter") this.handleGetJoke(); + }); } if (punchlineBtn) { punchlineBtn.addEventListener("click", () => this.showPunchline()); } + + if (themeToggle) { + themeToggle.addEventListener("click", () => this.toggleDarkMode()); + } + + if (favoritesBtn) { + favoritesBtn.addEventListener("click", () => this.openFavoritesModal()); + } + + if (closeFavoritesBtn) { + closeFavoritesBtn.addEventListener("click", () => this.closeFavoritesModal()); + } + + if (copyBtn) { + copyBtn.addEventListener("click", () => this.copyJokeToClipboard()); + } + + if (favoriteBtn) { + favoriteBtn.addEventListener("click", () => this.toggleFavorite()); + } + + // Close modal when clicking outside + favoritesModal.addEventListener("click", (e) => { + if (e.target === favoritesModal) { + this.closeFavoritesModal(); + } + }); + } + + /** + * Toggle dark mode on/off + */ + toggleDarkMode() { + this.darkMode = !this.darkMode; + document.body.classList.toggle("dark-mode"); + this.saveDarkMode(); + this.updateThemeButton(); + } + + /** + * Update theme button appearance + */ + updateThemeButton() { + const themeToggle = document.getElementById("theme-toggle"); + if (this.darkMode) { + themeToggle.textContent = "ā˜€ļø"; + themeToggle.title = "Toggle light mode"; + themeToggle.classList.add("active"); + } else { + themeToggle.textContent = "šŸŒ™"; + themeToggle.title = "Toggle dark mode"; + themeToggle.classList.remove("active"); + } + } + + /** + * Save dark mode preference to localStorage + */ + saveDarkMode() { + localStorage.setItem("joke-dark-mode", JSON.stringify(this.darkMode)); + } + + /** + * Load dark mode preference from localStorage + */ + loadDarkMode() { + const saved = localStorage.getItem("joke-dark-mode"); + return saved ? JSON.parse(saved) : false; + } + + /** + * Add or remove joke from favorites + */ + toggleFavorite() { + if (!this.currentJoke) return; + + const jokeText = this.getJokeText(); + const favoriteBtn = document.getElementById("favorite-btn"); + const isFavorited = this.favorites.some((fav) => fav.text === jokeText); + + if (isFavorited) { + this.favorites = this.favorites.filter((fav) => fav.text !== jokeText); + favoriteBtn.classList.remove("favorited"); + favoriteBtn.textContent = "šŸ¤ Add to Favorites"; + } else { + this.favorites.push({ + text: jokeText, + type: this.currentJoke.type, + timestamp: new Date().toISOString(), + }); + favoriteBtn.classList.add("favorited"); + favoriteBtn.textContent = "ā¤ļø Remove from Favorites"; + } + + this.saveFavorites(); + } + + /** + * Check if current joke is favorited + */ + updateFavoriteButton() { + if (!this.currentJoke) return; + + const jokeText = this.getJokeText(); + const favoriteBtn = document.getElementById("favorite-btn"); + const isFavorited = this.favorites.some((fav) => fav.text === jokeText); + + if (isFavorited) { + favoriteBtn.classList.add("favorited"); + favoriteBtn.textContent = "ā¤ļø Remove from Favorites"; + } else { + favoriteBtn.classList.remove("favorited"); + favoriteBtn.textContent = "šŸ¤ Add to Favorites"; + } + } + + /** + * Get the full joke text + */ + getJokeText() { + if (this.currentJoke.type === "single") { + return this.currentJoke.joke; + } else { + return `${this.currentJoke.setup}\n\n${this.currentJoke.delivery}`; + } + } + + /** + * Copy joke to clipboard + */ + async copyJokeToClipboard() { + if (!this.currentJoke) return; + + const jokeText = this.getJokeText(); + const copyBtn = document.getElementById("copy-btn"); + + try { + await navigator.clipboard.writeText(jokeText); + + // Visual feedback + const originalText = copyBtn.textContent; + copyBtn.textContent = "āœ“ Copied!"; + copyBtn.style.background = "#51cf66"; + + setTimeout(() => { + copyBtn.textContent = originalText; + copyBtn.style.background = ""; + }, 2000); + } catch (err) { + console.error("Failed to copy:", err); + alert("Failed to copy joke to clipboard"); + } + } + + /** + * Open favorites modal + */ + openFavoritesModal() { + const favoritesModal = document.getElementById("favorites-modal"); + favoritesModal.classList.add("active"); + this.renderFavorites(); + } + + /** + * Close favorites modal + */ + closeFavoritesModal() { + const favoritesModal = document.getElementById("favorites-modal"); + favoritesModal.classList.remove("active"); + } + + /** + * Render favorites list + */ + renderFavorites() { + const favoritesList = document.getElementById("favorites-list"); + + if (this.favorites.length === 0) { + favoritesList.innerHTML = ` +
+

šŸ“­ No favorite jokes yet!

+

Add jokes to your favorites to see them here.

+
+ `; + return; + } + + favoritesList.innerHTML = this.favorites + .map( + (fav, index) => ` +
+
${this.escapeHtml(fav.text)}
+
+ + +
+
+ ` + ) + .join(""); + } + + /** + * Copy favorite joke to clipboard + */ + async copyFavorite(index) { + if (this.favorites[index]) { + try { + await navigator.clipboard.writeText(this.favorites[index].text); + alert("āœ“ Joke copied to clipboard!"); + } catch (err) { + console.error("Failed to copy:", err); + alert("Failed to copy joke"); + } + } + } + + /** + * Remove favorite joke + */ + removeFavorite(index) { + this.favorites.splice(index, 1); + this.saveFavorites(); + this.renderFavorites(); + this.updateFavoriteButton(); + } + + /** + * Save favorites to localStorage + */ + saveFavorites() { + localStorage.setItem("joke-favorites", JSON.stringify(this.favorites)); + } + + /** + * Load favorites from localStorage + */ + loadFavorites() { + const saved = localStorage.getItem("joke-favorites"); + return saved ? JSON.parse(saved) : []; } /** @@ -81,12 +336,15 @@ class JokeGeneratorUI { return data; } catch (error) { if (error.name === "AbortError") { + console.error("Request timeout"); this.showError("Request timed out. Please try again."); } else if (error instanceof TypeError) { + console.error("Network error:", error); this.showError( "Network error. Please check your internet connection." ); } else { + console.error("Error fetching joke:", error); this.showError("Failed to fetch joke. Please try again."); } return null; @@ -123,6 +381,7 @@ class JokeGeneratorUI { this.isLoading = false; this.updateButtonState(); + this.updateFavoriteButton(); } /** @@ -147,7 +406,7 @@ class JokeGeneratorUI { const punchlineBtn = document.getElementById("punchline-btn"); jokeContainer.style.display = "block"; - jokeText.textContent = `Error: ${message}`; + jokeText.textContent = `āŒ ${message}`; punchlineBtn.style.display = "none"; this.isLoading = false; @@ -161,12 +420,12 @@ class JokeGeneratorUI { const getJokeBtn = document.getElementById("get-joke-btn"); if (getJokeBtn) { getJokeBtn.disabled = this.isLoading; - getJokeBtn.textContent = this.isLoading ? "Loading..." : "Get a Joke!"; + getJokeBtn.textContent = this.isLoading ? "Loading... ā³" : "Get a Joke! šŸ˜„"; } } /** - * Handle the Get Joke button click + * Handle the "Get Joke" button click */ async handleGetJoke() { if (this.isLoading) return; @@ -188,5 +447,6 @@ class JokeGeneratorUI { // Initialize when DOM is ready document.addEventListener("DOMContentLoaded", () => { const generator = new JokeGeneratorUI(); + window.jokeGenerator = generator; // Make globally accessible for inline events generator.init(); }); diff --git a/joke-generator/joke_generator.py b/joke-generator/joke_generator.py index fe8a596..4c9d1a5 100644 --- a/joke-generator/joke_generator.py +++ b/joke-generator/joke_generator.py @@ -1,19 +1,21 @@ """ -Random Joke Generator using JokeAPI +Random Joke Generator using JokeAPI - Enhanced Edition +Features: Favorites system with persistence A simple yet powerful joke generator that fetches jokes from an external API. Supports multiple joke types and provides a clean, interactive CLI experience. """ import requests import json -from typing import Dict, Optional +import os +from typing import Dict, Optional, List import sys class JokeGenerator: """ A class to generate random jokes using the JokeAPI. - Provides methods to fetch and display jokes in various formats. + Provides methods to fetch, display, and save favorite jokes. """ BASE_URL = "https://v2.jokeapi.dev/joke" @@ -23,6 +25,7 @@ class JokeGenerator: "programming": "Programming", "knock-knock": "Knock-Knock" } + FAVORITES_FILE = "joke_favorites.json" def __init__(self, timeout: int = 10): """ @@ -33,6 +36,7 @@ def __init__(self, timeout: int = 10): """ self.timeout = timeout self.session = requests.Session() + self.favorites = self.load_favorites() def get_joke(self, joke_type: str = "any", safe_mode: bool = True) -> Optional[Dict]: """ @@ -101,48 +105,123 @@ def display_joke(self, joke_data: Dict) -> None: print("=" * 60 + "\n") - def interactive_menu(self) -> None: - """Display an interactive menu for selecting and fetching jokes.""" - while True: - print("\nšŸŽŖ RANDOM JOKE GENERATOR šŸŽŖ") - print("-" * 40) - print("Select a joke type:") - print() + def get_joke_text(self, joke_data: Dict) -> str: + """ + Get the full joke text as a string. - for idx, (key, display_name) in enumerate(self.JOKE_TYPES.items(), 1): - print(f" {idx}. {display_name}") + Args: + joke_data (Dict): Joke data from the API - print(f" {len(self.JOKE_TYPES) + 1}. Exit") - print() + Returns: + str: The complete joke text + """ + if joke_data["type"] == "single": + return joke_data["joke"] + else: + return f"{joke_data['setup']}\n{joke_data['delivery']}" + def add_favorite(self, joke_data: Dict) -> None: + """ + Add a joke to favorites. + + Args: + joke_data (Dict): Joke data to add + """ + joke_text = self.get_joke_text(joke_data) + + # Check if already in favorites + if any(fav["text"] == joke_text for fav in self.favorites): + print("āœ“ Already in favorites!") + return + + self.favorites.append({ + "text": joke_text, + "type": joke_data.get("type", "unknown") + }) + self.save_favorites() + print("⭐ Added to favorites!") + + def load_favorites(self) -> List[Dict]: + """ + Load favorites from JSON file. + + Returns: + List[Dict]: List of favorite jokes + """ + if os.path.exists(self.FAVORITES_FILE): try: - choice = input("Enter your choice (1-5): ").strip() + with open(self.FAVORITES_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except (json.JSONDecodeError, IOError): + return [] + return [] + + def save_favorites(self) -> None: + """Save favorites to JSON file.""" + try: + with open(self.FAVORITES_FILE, "w", encoding="utf-8") as f: + json.dump(self.favorites, f, indent=2, ensure_ascii=False) + except IOError as e: + print(f"Error saving favorites: {e}") + + def view_favorites(self) -> None: + """Display all favorite jokes.""" + if not self.favorites: + print("\n" + "=" * 60) + print("šŸ“­ No Favorites Yet!") + print("=" * 60) + print("\nStart adding jokes to your favorites to see them here.\n") + return - if choice == str(len(self.JOKE_TYPES) + 1): - print("\nšŸ‘‹ Thanks for laughing with us! Goodbye!\n") - break + print("\n" + "=" * 60) + print("⭐ MY FAVORITE JOKES ⭐") + print("=" * 60) - if choice not in ["1", "2", "3", "4"]: - print("Invalid choice. Please try again.") - continue + for idx, favorite in enumerate(self.favorites, 1): + print(f"\n{idx}. {favorite['text']}\n") + print("-" * 60) - joke_type_list = list(self.JOKE_TYPES.keys()) - selected_type = joke_type_list[int(choice) - 1] + def clear_favorites(self) -> None: + """Clear all favorite jokes with confirmation.""" + if not self.favorites: + print("No favorites to clear.") + return - print("\nšŸ“” Fetching your joke...") - joke_data = self.get_joke(selected_type, safe_mode=True) + confirm = input("āš ļø Are you sure? This will delete all favorites. (yes/no): ").strip().lower() + if confirm == "yes": + self.favorites = [] + self.save_favorites() + print("āœ“ All favorites cleared!") + else: + print("Cancelled.") - if joke_data: - self.display_joke(joke_data) + def interactive_menu(self) -> None: + """Display an interactive menu for selecting and fetching jokes.""" + while True: + print("\nšŸŽŖ RANDOM JOKE GENERATOR šŸŽŖ") + print("-" * 40) + print("Select an option:") + print() + print(" 1. Get a Joke") + print(" 2. View Favorites") + print(" 3. Clear Favorites") + print(" 4. Exit") + print() - another = ( - input("Would you like another joke? (y/n): ") - .strip() - .lower() - ) - if another != "y": + try: + choice = input("Enter your choice (1-4): ").strip() + + if choice == "1": + self.get_joke_menu() + elif choice == "2": + self.view_favorites() + elif choice == "3": + self.clear_favorites() + elif choice == "4": print("\nšŸ‘‹ Thanks for laughing with us! Goodbye!\n") break + else: + print("Invalid choice. Please try again.") except KeyboardInterrupt: print("\n\nšŸ‘‹ Goodbye!\n") @@ -150,6 +229,42 @@ def interactive_menu(self) -> None: except Exception as e: print(f"An error occurred: {e}") + def get_joke_menu(self) -> None: + """Display menu for getting a joke.""" + print("\nSelect a joke type:") + print() + + for idx, (key, display_name) in enumerate(self.JOKE_TYPES.items(), 1): + print(f" {idx}. {display_name}") + + print() + + try: + choice = input("Enter your choice (1-4): ").strip() + + if choice not in ["1", "2", "3", "4"]: + print("Invalid choice. Please try again.") + return + + joke_type_list = list(self.JOKE_TYPES.keys()) + selected_type = joke_type_list[int(choice) - 1] + + print("\nšŸ“” Fetching your joke...") + joke_data = self.get_joke(selected_type, safe_mode=True) + + if joke_data: + self.display_joke(joke_data) + + # Ask to save to favorites + save = input("Would you like to save this to favorites? (y/n): ").strip().lower() + if save == "y": + self.add_favorite(joke_data) + + except ValueError: + print("Invalid input. Please enter a number.") + except Exception as e: + print(f"An error occurred: {e}") + def main(): """Main entry point for the joke generator."""