diff --git a/package-lock.json b/package-lock.json index 65cf981c..674ffe66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -162,6 +162,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.13", @@ -553,6 +554,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -599,6 +601,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1636,8 +1639,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.3", @@ -1716,6 +1718,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz", "integrity": "sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==", "dev": true, + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1727,6 +1730,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.14.tgz", "integrity": "sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==", "dev": true, + "peer": true, "dependencies": { "@types/react": "*" } @@ -1838,6 +1842,7 @@ "integrity": "sha512-HURRrgGVzz2GQ2Imurp55FA+majHXgCXMzcwtojUZeRsAXyHNgEvxGRJf4QQY4kJeVakiugusGYeUqBgZ/xylg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "4.0.3", "fflate": "^0.8.2", @@ -1873,6 +1878,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1930,7 +1936,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -2217,6 +2222,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001541", "electron-to-chromium": "^1.4.535", @@ -2536,8 +2542,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.4.563", @@ -2731,6 +2736,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3960,6 +3966,7 @@ "integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@asamuzakjp/dom-selector": "^6.7.2", "cssstyle": "^5.3.1", @@ -4135,7 +4142,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -4572,6 +4578,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4716,7 +4723,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -4731,8 +4737,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/prop-types": { "version": "15.8.1", @@ -4779,6 +4784,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -4790,6 +4796,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -5510,6 +5517,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5756,6 +5764,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -5812,6 +5821,7 @@ "integrity": "sha512-IUSop8jgaT7w0g1yOM/35qVtKjr/8Va4PrjzH1OUb0YH4c3OXB2lCZDkMAB6glA8T5w8S164oJGsbcmAecr4sA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.3", "@vitest/mocker": "4.0.3", @@ -6345,24 +6355,13 @@ } } }, - "node_modules/vitest/node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/vitest/node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6418,6 +6417,7 @@ "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/src/App.jsx b/src/App.jsx index 3fd27dc8..3b90b282 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,22 +1,28 @@ /* eslint-disable no-unused-vars */ // App.jsx -import React from "react"; +import React, { useState } from "react"; import { Route, Routes } from "react-router-dom"; import { ThemeProvider } from "./ThemeContext"; import Home from "./components/Home"; import "./style.css"; import About from "./components/About"; import History from "./components/History"; +import MemeHistory from "./components/MemeHistory"; +import Favorites from "./components/Favorites"; import Dynamicmeme from "./components/Dynamicmeme"; import NewMeme from "./components/NewMeme"; const App = () => { + const [meme, setMeme] = useState(null); + return ( - } /> + } /> } /> } /> + } /> + } /> } /> } /> diff --git a/src/Meme.jsx b/src/Meme.jsx index 69a94e9d..aa73489f 100644 --- a/src/Meme.jsx +++ b/src/Meme.jsx @@ -1,5 +1,5 @@ /* eslint-disable react/prop-types */ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useToast } from "./contexts/ToastContext"; import { shareToTwitter, @@ -22,6 +22,32 @@ const Meme = ({ meme, setMeme }) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); const [showError, setShowError] = useState(false); + + // Keyboard shortcuts + useEffect(() => { + const handleKeyPress = (e) => { + if (e.ctrlKey || e.metaKey) { + switch (e.key) { + case 's': + e.preventDefault(); + downloadMeme(meme.url, "meme"); + break; + case 'Enter': + e.preventDefault(); + if (!isLoading) { + document.querySelector('form').requestSubmit(); + } + break; + } + } + if (e.key === 'Escape') { + setMeme(null); + } + }; + + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); + }, [meme.url, isLoading, setMeme]); const saveMemeToHistory = (memeData) => { const savedMemes = JSON.parse(localStorage.getItem('memeHistory') || '[]'); @@ -29,10 +55,18 @@ const Meme = ({ meme, setMeme }) => { id: Date.now(), url: memeData.url, template_name: meme.name || 'Unknown Template', + template_id: meme.id, texts: form.boxes.map(box => box.text || ''), - created_at: new Date().toISOString() + created_at: new Date().toISOString(), + thumbnail: meme.url }; + + // Keep only last 50 memes to avoid storage issues savedMemes.unshift(newMeme); + if (savedMemes.length > 50) { + savedMemes.splice(50); + } + localStorage.setItem('memeHistory', JSON.stringify(savedMemes)); }; @@ -98,31 +132,82 @@ const Meme = ({ meme, setMeme }) => {
{/* Left Section - Image */} -
- meme +
+
+ meme + {/* Meme Info Overlay */} +
+ {meme.width}x{meme.height}px +
+
+ + {/* Meme Stats */} +
+

{meme.name}

+
+ 📝 {meme.box_count} text boxes + 📈 {meme.width}x{meme.height} +
+
{/* Right Section - Caption Inputs */}
-

Add Your Captions

-
+
+

Add Your Captions

+
+

📝 Fill in the text boxes below

+

⌨️ Shortcuts: Ctrl+S (Save), Ctrl+Enter (Generate), Esc (Back)

+
+
+
{[...Array(meme.box_count)].map((_, index) => ( - { - const newBox = form.boxes; - newBox[index] = { text: e.target.value }; - setForm({ ...form, boxes: newBox }); - }} - /> +
+ { + const newBox = form.boxes; + newBox[index] = { text: e.target.value }; + setForm({ ...form, boxes: newBox }); + }} + /> + {/* Quick Text Options */} +
+ + +
+
))}
@@ -158,11 +243,11 @@ const Meme = ({ meme, setMeme }) => { ) : showError ? ( <> - ❌ Error + ❌ Failed - Try Again ) : showSuccessNote ? ( <> - ✅ Success! + ✅ Meme Created! ) : ( <> @@ -174,10 +259,52 @@ const Meme = ({ meme, setMeme }) => { type="button" className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg transition-colors font-medium" onClick={() => downloadMeme(meme.url, "meme")} + title="Save meme (Ctrl+S)" > 💾 Save + {/* Quick Actions */} + + + + {/* Success Note */} {showSuccessNote && (
@@ -187,8 +314,13 @@ const Meme = ({ meme, setMeme }) => { {/* Error Message */} {showError && ( -
- ❌ {error} +
+ ⚠️ +
+

Generation Failed

+

{error}

+

Check your connection and try again

+
)}
@@ -235,6 +367,36 @@ const Meme = ({ meme, setMeme }) => { > 📋 Copy Link + + {/* Additional Share Options */} + + +
)} diff --git a/src/Temp.jsx b/src/Temp.jsx index 6b82b1fe..7d5bbeeb 100644 --- a/src/Temp.jsx +++ b/src/Temp.jsx @@ -2,11 +2,13 @@ import React, { useRef, useLayoutEffect } from "react"; import gsap from "gsap"; import memesMeta from "./memesMeta"; +import { useFavorites } from "./hooks/useFavorites"; const Temp = ({ temp, setMeme }) => { const row1 = useRef(null); const row2 = useRef(null); const row3 = useRef(null); + const { toggleFavorite, isFavorite } = useFavorites(); useLayoutEffect(() => { const ctx1 = gsap.context(() => { @@ -45,21 +47,33 @@ const Temp = ({ temp, setMeme }) => { const renderTemplate = (temps) => (
setMeme(temps)} - // 1. Add aria-label to the clickable container aria-label={`Select meme template: ${temps.name}`} - role="button" // Indicates it is an interactive element + role="button" >
+ {/* Heart button */} + + {/* Caption overlay */}
{memesMeta[temps.name]?.captions?.join(", ")} diff --git a/src/components/Favorites.jsx b/src/components/Favorites.jsx new file mode 100644 index 00000000..2f84bd01 --- /dev/null +++ b/src/components/Favorites.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { useFavorites } from '../hooks/useFavorites'; + +const Favorites = ({ setMeme }) => { + const { favorites, toggleFavorite } = useFavorites(); + + if (favorites.length === 0) { + return ( +
+
+

Your Favorite Templates

+ +
+
❤️
+

No Favorites Yet

+

+ Heart your favorite meme templates to see them here +

+ + Browse Templates + +
+
+
+ ); + } + + return ( +
+
+

Your Favorite Templates

+ +

+ {favorites.length} favorite template{favorites.length !== 1 ? 's' : ''} +

+ +
+ {favorites.map((template) => ( +
setMeme(template)} + > +
+ {template.name} + +
+ + {/* Heart button */} + + + {/* Play icon on hover */} +
+
+ 🎭 +
+
+
+ +
+

{template.name}

+
+ 📝 {template.box_count} texts +
+
+
+ ))} +
+
+
+ ); +}; + +export default Favorites; \ No newline at end of file diff --git a/src/components/Home.jsx b/src/components/Home.jsx index c4aa2fe5..6cf2444d 100644 --- a/src/components/Home.jsx +++ b/src/components/Home.jsx @@ -12,23 +12,52 @@ const Home = () => { const [meme, setMeme] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [currentPage, setCurrentPage] = useState(1); + const [selectedCategory, setSelectedCategory] = useState('all'); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); const [itemsPerPage] = useState(18); // Fixed 18 items per page useEffect(() => { + setIsLoading(true); fetch("https://api.imgflip.com/get_memes") - .then((res) => res.json()) + .then((res) => { + if (!res.ok) throw new Error('Failed to load memes'); + return res.json(); + }) .then((data) => { setTemp(data.data.memes); + setIsLoading(false); + }) + .catch((err) => { + setError(err.message); + setIsLoading(false); }); }, []); - // Function to filter memes based on the search query - const filteredMemes = temp.filter((meme) => - meme.name.toLowerCase().includes(searchQuery.toLowerCase()) - ); + // Enhanced filtering with categories + const filteredMemes = temp.filter((meme) => { + const matchesSearch = meme.name.toLowerCase().includes(searchQuery.toLowerCase()); + + if (selectedCategory === 'all') return matchesSearch; + + const categoryKeywords = { + reaction: ['surprised', 'pikachu', 'yelling', 'woman', 'cat'], + funny: ['spongebob', 'mocking', 'fine', 'dog'], + choice: ['drake', 'buttons', 'two buttons', 'exit'], + success: ['cheers', 'dicaprio', 'handshake'], + programming: ['debugging', 'brain', 'expanding'] + }; + + const keywords = categoryKeywords[selectedCategory] || []; + const matchesCategory = keywords.some(keyword => + meme.name.toLowerCase().includes(keyword) + ); + + return matchesSearch && matchesCategory; + }); // Calculate the index of the last and first item on the current page const indexOfLastItem = currentPage * itemsPerPage; @@ -94,11 +123,91 @@ const Home = () => { setSearchQuery={setSearchQuery} /> -
+
{meme === null ? ( <> - -
+ {/* Categories */} +
+
+ {[ + { id: 'all', name: 'All', icon: '🎭' }, + { id: 'reaction', name: 'Reaction', icon: '😱' }, + { id: 'funny', name: 'Funny', icon: '😂' }, + { id: 'choice', name: 'Choice', icon: '🤔' }, + { id: 'success', name: 'Success', icon: '🎉' }, + { id: 'programming', name: 'Code', icon: '💻' } + ].map(category => ( + + ))} +
+

+ {filteredMemes.length} memes found + {selectedCategory !== 'all' && ` in ${selectedCategory}`} +

+
+ + {/* Error Message */} + {error && ( +
+
+ ⚠️ +
+

{error}

+ +
+
+
+ )} + + {/* Loading State */} + {isLoading ? ( +
+ {[...Array(18)].map((_, i) => ( +
+
+
+
+
+ ))} +
+ ) : filteredMemes.length === 0 ? ( +
+
😅
+

No memes found

+

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

+ +
+ ) : ( + + )} + {/* Pagination - Only show if has results */} + {!isLoading && filteredMemes.length > 0 && ( +
-
+
+ )} ) : ( <> diff --git a/src/components/MemeHistory.jsx b/src/components/MemeHistory.jsx new file mode 100644 index 00000000..93efa172 --- /dev/null +++ b/src/components/MemeHistory.jsx @@ -0,0 +1,139 @@ +import React, { useState, useEffect } from 'react'; +import { downloadMeme } from '../utils/socialShare'; + +const MemeHistory = () => { + const [history, setHistory] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const saved = JSON.parse(localStorage.getItem('memeHistory') || '[]'); + setHistory(saved); + setLoading(false); + }, []); + + const clearHistory = () => { + if (window.confirm('Are you sure you want to clear all history?')) { + localStorage.removeItem('memeHistory'); + setHistory([]); + } + }; + + const deleteMeme = (id) => { + const updated = history.filter(meme => meme.id !== id); + localStorage.setItem('memeHistory', JSON.stringify(updated)); + setHistory(updated); + }; + + if (loading) { + return ( +
+
+
+
+ {[...Array(8)].map((_, i) => ( +
+ ))} +
+
+
+ ); + } + + return ( +
+
+
+

Your Meme History

+ {history.length > 0 && ( + + )} +
+ + {history.length === 0 ? ( +
+
📝
+

No Memes Yet

+

+ Your generated memes will appear here +

+ + Create Your First Meme + +
+ ) : ( + <> +

+ {history.length} meme{history.length !== 1 ? 's' : ''} in your history +

+ +
+ {history.map((meme) => ( +
+
+ Generated meme +
+ + {/* Action buttons */} +
+ + +
+
+ +
+

+ {meme.template_name} +

+

+ {new Date(meme.created_at).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + })} +

+ + {/* Show meme texts */} + {meme.texts && meme.texts.length > 0 && ( +
+ {meme.texts.slice(0, 2).map((text, i) => ( +

"{text}"

+ ))} +
+ )} +
+
+ ))} +
+ + )} +
+
+ ); +}; + +export default MemeHistory; \ No newline at end of file diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index a6892b91..4977fb61 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -115,9 +115,10 @@ const Navbar = ({ setMeme, searchQuery, setSearchQuery }) => {
{[ { path: "/", label: "Home" }, + { path: "/favorites", label: "Favorites" }, + { path: "/meme-history", label: "My Memes" }, { path: "/dynamic", label: "Dynamic" }, - { path: "/about", label: "About" }, - { path: "/history", label: "History" } + { path: "/about", label: "About" } ].map(({ path, label }) => ( {
- + {searchQuery ? ( + + ) : ( + + )}
)} {
+ {/* Mobile Search Bar */} + {isMobileSearchOpen && isHomePage && ( +
+
+ + {searchQuery ? ( + + ) : ( + + )} +
+
+ )} + {/* Mobile Dropdown Menu */} {isMobileMenuOpen && (
{ > {[ { path: "/", label: "Home" }, + { path: "/favorites", label: "Favorites" }, + { path: "/meme-history", label: "My Memes" }, { path: "/dynamic", label: "Dynamic" }, { path: "/about", label: "About" }, - { path: "/history", label: "History" }, ].map(({ path, label }) => ( { + const [favorites, setFavorites] = useState([]); + + useEffect(() => { + const saved = JSON.parse(localStorage.getItem('favoriteMemes') || '[]'); + setFavorites(saved); + }, []); + + const toggleFavorite = (meme) => { + const saved = JSON.parse(localStorage.getItem('favoriteMemes') || '[]'); + const exists = saved.find(fav => fav.id === meme.id); + + let updated; + if (exists) { + updated = saved.filter(fav => fav.id !== meme.id); + } else { + updated = [...saved, { + id: meme.id, + name: meme.name, + url: meme.url, + box_count: meme.box_count, + width: meme.width, + height: meme.height, + added_at: new Date().toISOString() + }]; + } + + localStorage.setItem('favoriteMemes', JSON.stringify(updated)); + setFavorites(updated); + return !exists; + }; + + const isFavorite = (memeId) => { + return favorites.some(fav => fav.id === memeId); + }; + + return { favorites, toggleFavorite, isFavorite }; +}; \ No newline at end of file