diff --git a/app/Http/Controllers/FrontHomeController.php b/app/Http/Controllers/FrontHomeController.php index e23060f..b51597f 100644 --- a/app/Http/Controllers/FrontHomeController.php +++ b/app/Http/Controllers/FrontHomeController.php @@ -4,6 +4,7 @@ use App\Models\BackgroundMedia; use App\Models\MemeMedia; +use App\Services\MemeMediaService; use Artesaos\SEOTools\Facades\JsonLd; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Cache; @@ -11,6 +12,10 @@ class FrontHomeController extends Controller { + public function __construct( + private MemeMediaService $memeMediaService + ) {} + public function index() { if (App::environment('production') && env('COMING_SOON_ENABLED', true)) { @@ -28,12 +33,16 @@ public function index() // Get FAQ data $faqData = $this->getFaqData(); + // Get popular keywords for search suggestions + $popularKeywords = $this->memeMediaService->getPopularKeywords(10); + // Add FAQ JSON-LD structured data $this->addFaqJsonLd($faqData); return Inertia::render('home/home', [ 'stats' => $stats, 'faqData' => $faqData, + 'popularKeywords' => $popularKeywords, ]); } diff --git a/app/Http/Controllers/FrontMemeController.php b/app/Http/Controllers/FrontMemeController.php index 86a1480..7336a4e 100644 --- a/app/Http/Controllers/FrontMemeController.php +++ b/app/Http/Controllers/FrontMemeController.php @@ -2,7 +2,7 @@ namespace App\Http\Controllers; -use App\Models\MemeMedia; +use App\Services\MemeMediaService; use Artesaos\SEOTools\Facades\SEOMeta; use Artesaos\SEOTools\Facades\OpenGraph; use Artesaos\SEOTools\Facades\TwitterCard; @@ -15,6 +15,10 @@ class FrontMemeController extends Controller { + public function __construct( + private MemeMediaService $memeMediaService + ) {} + public function index(Request $request): Response { return $this->getMemes($request->input('search')); @@ -29,23 +33,7 @@ public function search(string $search): Response private function getMemes(?string $search = null): Response { - $query = MemeMedia::query() - ->where('is_enabled', true) - ->orderBy('id', 'desc'); - - // Search functionality - if ($search) { - $query->where(function ($q) use ($search) { - $q->where('name', 'ilike', "%{$search}%") - ->orWhere('description', 'ilike', "%{$search}%") - ->orWhereJsonContains('keywords', $search) - ->orWhereJsonContains('action_keywords', $search) - ->orWhereJsonContains('emotion_keywords', $search) - ->orWhereJsonContains('misc_keywords', $search); - }); - } - - $memes = $query->cursorPaginate(24); + $memes = $this->memeMediaService->searchMemes($search, 24); // Set up SEO meta tags $title = $search ? ucfirst($search) . " Memes in MEMEFA.ST" : 'Meme Library - Thousands of Video Meme Templates'; @@ -89,21 +77,10 @@ private function getMemes(?string $search = null): Response TwitterCard::setType('summary_large_image'); // Get available types for filter - $types = MemeMedia::where('is_enabled', true) - ->distinct() - ->pluck('type') - ->filter(); + $types = $this->memeMediaService->getAvailableTypes(); // Get popular keywords for filter - $popularKeywords = MemeMedia::where('is_enabled', true) - ->get() - ->pluck('keywords') - ->flatten() - ->countBy() - ->sort() - ->reverse() - ->take(20) - ->keys(); + $popularKeywords = $this->memeMediaService->getPopularKeywords(20); return Inertia::render('memes/index', [ 'memes' => $memes, @@ -118,39 +95,10 @@ private function getMemes(?string $search = null): Response public function show(string $slug): Response { - $meme = MemeMedia::where('slug', $slug) - ->where('is_enabled', true) - ->firstOrFail(); + $meme = $this->memeMediaService->findBySlug($slug); // Get related memes based on similar keywords - $relatedMemes = MemeMedia::where('is_enabled', true) - ->where('id', '!=', $meme->id) - ->where(function ($query) use ($meme) { - if ($meme->keywords) { - foreach ($meme->keywords as $keyword) { - $query->orWhereJsonContains('keywords', $keyword) - ->orWhereJsonContains('action_keywords', $keyword) - ->orWhereJsonContains('emotion_keywords', $keyword) - ->orWhereJsonContains('misc_keywords', $keyword); - } - } - }) - ->limit(6) - ->get(); - - // If we have less than 6 related memes, fill up with random ones - if ($relatedMemes->count() < 6) { - $excludeIds = $relatedMemes->pluck('id')->push($meme->id)->toArray(); - $needed = 6 - $relatedMemes->count(); - - $randomMemes = MemeMedia::where('is_enabled', true) - ->whereNotIn('id', $excludeIds) - ->inRandomOrder() - ->limit($needed) - ->get(); - - $relatedMemes = $relatedMemes->merge($randomMemes); - } + $relatedMemes = $this->memeMediaService->getRelatedMemes($meme, 6); // Set up SEO meta tags for individual meme page $title = "{$meme->name} - Make Video Memes with MEMEFA.ST"; @@ -184,21 +132,8 @@ public function show(string $slug): Response public function generateOG(string $ids): HttpResponse { - // Decode the hashids to get the meme media ID - $memeId = hashids_decode($ids); - - if (!$memeId) { - abort(404, 'Meme not found'); - } - - // Get the meme media - $meme = MemeMedia::where('id', $memeId) - ->where('is_enabled', true) - ->first(); - - if (!$meme) { - abort(404, 'Meme not found'); - } + // Get the meme media using the service + $meme = $this->memeMediaService->findByHashIds($ids); // Load the template image $templatePath = public_path('memefast-og-template.jpg'); diff --git a/app/Services/MemeMediaService.php b/app/Services/MemeMediaService.php new file mode 100644 index 0000000..ba3b0bf --- /dev/null +++ b/app/Services/MemeMediaService.php @@ -0,0 +1,162 @@ +buildSearchQuery($search); + + return $query->cursorPaginate($perPage); + } + + /** + * Get popular keywords for filtering + */ + public function getPopularKeywords(int $limit = 20): SupportCollection + { + $cacheKey = "popular_keywords_limit_{$limit}"; + + return Cache::remember($cacheKey, 60 * 60 * 24, function () use ($limit) { + return MemeMedia::where('is_enabled', true) + ->get() + ->pluck('keywords') + ->flatten() + ->countBy() + ->sort() + ->reverse() + ->take($limit) + ->keys(); + }); + } + + /** + * Get available meme types for filtering + */ + public function getAvailableTypes(): SupportCollection + { + return MemeMedia::where('is_enabled', true) + ->distinct() + ->pluck('type') + ->filter(); + } + + /** + * Find meme by slug + */ + public function findBySlug(string $slug): MemeMedia + { + return MemeMedia::where('slug', $slug) + ->where('is_enabled', true) + ->firstOrFail(); + } + + /** + * Find meme by hashids + */ + public function findByHashIds(string $ids): MemeMedia + { + $memeId = hashids_decode($ids); + + if (!$memeId) { + throw new ModelNotFoundException('Meme not found'); + } + + return MemeMedia::where('id', $memeId) + ->where('is_enabled', true) + ->firstOrFail(); + } + + /** + * Get related memes based on keywords + */ + public function getRelatedMemes(MemeMedia $meme, int $limit = 6): Collection + { + $relatedMemes = MemeMedia::where('is_enabled', true) + ->where('id', '!=', $meme->id) + ->where(function ($query) use ($meme) { + if ($meme->keywords) { + foreach ($meme->keywords as $keyword) { + $query->orWhereJsonContains('keywords', $keyword) + ->orWhereJsonContains('action_keywords', $keyword) + ->orWhereJsonContains('emotion_keywords', $keyword) + ->orWhereJsonContains('misc_keywords', $keyword); + } + } + }) + ->limit($limit) + ->get(); + + // If we have less than the desired limit, fill up with random ones + if ($relatedMemes->count() < $limit) { + $excludeIds = $relatedMemes->pluck('id')->push($meme->id)->toArray(); + $needed = $limit - $relatedMemes->count(); + + $randomMemes = $this->fillWithRandomMemes($relatedMemes, $limit, $excludeIds); + $relatedMemes = $relatedMemes->merge($randomMemes); + } + + return $relatedMemes; + } + + /** + * Fill collection with random memes up to target count + */ + public function fillWithRandomMemes(Collection $existing, int $targetCount, array $excludeIds): Collection + { + $needed = $targetCount - $existing->count(); + + if ($needed <= 0) { + return collect(); + } + + return MemeMedia::where('is_enabled', true) + ->whereNotIn('id', $excludeIds) + ->inRandomOrder() + ->limit($needed) + ->get(); + } + + /** + * Build search query with keyword matching + */ + private function buildSearchQuery(?string $search = null): Builder + { + $query = $this->getEnabledMemesQuery(); + + if ($search) { + $query->where(function ($q) use ($search) { + $q->where('name', 'ilike', "%{$search}%") + ->orWhere('description', 'ilike', "%{$search}%") + ->orWhereJsonContains('keywords', $search) + ->orWhereJsonContains('action_keywords', $search) + ->orWhereJsonContains('emotion_keywords', $search) + ->orWhereJsonContains('misc_keywords', $search); + }); + } + + return $query; + } + + /** + * Get base query for enabled memes + */ + private function getEnabledMemesQuery(): Builder + { + return MemeMedia::query() + ->where('is_enabled', true) + ->orderBy('id', 'desc'); + } +} \ No newline at end of file diff --git a/resources/js/components/ui/keyword-badge.tsx b/resources/js/components/ui/keyword-badge.tsx index 4a09d6f..3a997ef 100644 --- a/resources/js/components/ui/keyword-badge.tsx +++ b/resources/js/components/ui/keyword-badge.tsx @@ -21,4 +21,4 @@ export function KeywordBadge({ keyword, size = 'default', className = '' }: Keyw {keyword} ); -} \ No newline at end of file +} diff --git a/resources/js/modules/editor/editor.jsx b/resources/js/modules/editor/editor.jsx index b4271d5..35c603c 100644 --- a/resources/js/modules/editor/editor.jsx +++ b/resources/js/modules/editor/editor.jsx @@ -108,7 +108,7 @@ const useResponsiveDimensions = () => { }; const Editor = ({ setInitialMeme, setInitialBackground, setInitialText }) => { - const { init, setInitialMeme: setStoreMeme, setInitialBackground: setStoreBackground, setInitialText: setStoreText } = useMediaStore(); + const { init, setInitialMeme: setStoreMeme, setInitialBackground: setStoreBackground, setInitialText: setStoreText, clearInitialState } = useMediaStore(); const { getSetting } = useLocalSettingsStore(); const { setSelectedTextElement } = useVideoEditorStore(); const emitter = useMitt(); @@ -121,6 +121,9 @@ const Editor = ({ setInitialMeme, setInitialBackground, setInitialText }) => { const isBelowMinWidth = useViewportDetection(320); useEffect(() => { + // Clear any previous initial state to allow fresh initialization + clearInitialState(); + // Set initial values if props are provided if (setInitialMeme) { setInitialMeme(setStoreMeme); @@ -134,7 +137,12 @@ const Editor = ({ setInitialMeme, setInitialBackground, setInitialText }) => { // Initialize (will skip API call if initial values were set) init(); - }, [setInitialMeme, setInitialBackground, setInitialText, setStoreMeme, setStoreBackground, setStoreText, init]); + + // Cleanup: Clear initial state when component unmounts + return () => { + clearInitialState(); + }; + }, [setInitialMeme, setInitialBackground, setInitialText, setStoreMeme, setStoreBackground, setStoreText, init, clearInitialState]); // Listen for text element selection (but don't auto-open sidebar) useEffect(() => { diff --git a/resources/js/pages/home/home.tsx b/resources/js/pages/home/home.tsx index a30b81f..d664a44 100644 --- a/resources/js/pages/home/home.tsx +++ b/resources/js/pages/home/home.tsx @@ -6,7 +6,7 @@ import Footer from './partials/Footer.jsx'; import Hero from './partials/Hero.jsx'; import MemeLibrarySearch from './partials/MemeLibrarySearch.jsx'; -const Home = ({ faqData }) => { +const Home = ({ faqData, popularKeywords }) => { const [isClient, setIsClient] = useState(false); const [Editor, setEditor] = useState(null); @@ -34,7 +34,7 @@ const Home = ({ faqData }) => {
- +
diff --git a/resources/js/pages/home/partials/MemeLibrarySearch.jsx b/resources/js/pages/home/partials/MemeLibrarySearch.jsx index a301257..d2a1e8b 100644 --- a/resources/js/pages/home/partials/MemeLibrarySearch.jsx +++ b/resources/js/pages/home/partials/MemeLibrarySearch.jsx @@ -1,11 +1,12 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { KeywordBadge } from '@/components/ui/keyword-badge'; import { router } from '@inertiajs/react'; import { Search } from 'lucide-react'; import { useState } from 'react'; import { route } from 'ziggy-js'; -const MemeLibrarySearch = () => { +const MemeLibrarySearch = ({ popularKeywords = [] }) => { const [searchQuery, setSearchQuery] = useState(''); const [isSearching, setIsSearching] = useState(false); @@ -32,13 +33,21 @@ const MemeLibrarySearch = () => { } }; + const handleKeywordClick = (keyword) => { + setSearchQuery(keyword); + setIsSearching(true); + router.visit(route('memes.search', { search: keyword }), { + onFinish: () => setIsSearching(false), + }); + }; + return (
{/* Section heading */}
-

Explore Our Meme Library

+

Find the perfect meme

Search through our database of popular meme templates and find the perfect one for your video

@@ -65,10 +74,27 @@ const MemeLibrarySearch = () => {
+ {/* Popular Keywords */} + {popularKeywords.length > 0 && ( +
+
+ {popularKeywords.map((keyword, index) => ( + handleKeywordClick(keyword)} + disabled={isSearching} + /> + ))} +
+
+ )} + {/* Browse all link */}
diff --git a/resources/js/pages/memes/index.tsx b/resources/js/pages/memes/index.tsx index 90996ae..7a79bfc 100644 --- a/resources/js/pages/memes/index.tsx +++ b/resources/js/pages/memes/index.tsx @@ -89,7 +89,9 @@ export default function MemesIndex({ memes, popularKeywords, filters, dynamicDes - {filters.search ? `${filters.search.charAt(0).toUpperCase() + filters.search.slice(1)} Memes` : 'Meme Library'} + + {filters.search ? `${filters.search.charAt(0).toUpperCase() + filters.search.slice(1)} Memes` : 'Meme Library'} + @@ -100,10 +102,10 @@ export default function MemesIndex({ memes, popularKeywords, filters, dynamicDes {filters.search ? `${filters.search.charAt(0).toUpperCase() + filters.search.slice(1)} Memes` : 'Meme Library'}

- {filters.search - ? (dynamicDescription || `Discover ${filters.search} meme templates and create viral content for TikTok, Instagram Reels, and YouTube Shorts.`) - : 'Thousands of memes ready for TikTok, Reels, Threads and YouTube Shorts. No signup needed - click any meme to start creating!' - } + {filters.search + ? dynamicDescription || + `Discover ${filters.search} meme templates and create viral content for TikTok, Instagram Reels, and YouTube Shorts.` + : 'Thousands of memes ready for TikTok, Reels, Threads and YouTube Shorts. No signup needed - click any meme to start creating!'}

@@ -204,6 +206,32 @@ export default function MemesIndex({ memes, popularKeywords, filters, dynamicDes )} )} + + {/* Discord Request Section */} +
+
+
+

Can't find the meme you're looking for?

+

+ Request it in our Discord community and we'll add it to the library! +

+
+
+ + {import.meta.env.VITE_DISCORD_LINK && ( + + Request in Discord + + + )} +