diff --git a/app/Console/Commands/PopulateMemeMediaSlugs.php b/app/Console/Commands/PopulateMemeMediaSlugs.php
new file mode 100644
index 0000000..9bd9d54
--- /dev/null
+++ b/app/Console/Commands/PopulateMemeMediaSlugs.php
@@ -0,0 +1,51 @@
+info('Starting to populate MemeMedia slugs...');
+
+ $memes = MemeMedia::whereNull('slug')->get();
+
+ if ($memes->isEmpty()) {
+ $this->info('No memes found without slugs.');
+
+ return;
+ }
+
+ $this->info("Found {$memes->count()} memes without slugs.");
+
+ $bar = $this->output->createProgressBar($memes->count());
+ $bar->start();
+
+ foreach ($memes as $meme) {
+ $baseSlug = Str::slug($meme->name);
+ $slug = $baseSlug;
+ $counter = 1;
+
+ // Ensure slug is unique
+ while (MemeMedia::where('slug', $slug)->exists()) {
+ $slug = $baseSlug.'-'.$counter;
+ $counter++;
+ }
+
+ $meme->update(['slug' => $slug]);
+ $bar->advance();
+ }
+
+ $bar->finish();
+ $this->newLine();
+ $this->info('Successfully populated all MemeMedia slugs!');
+ }
+}
diff --git a/app/Http/Controllers/FrontMemeController.php b/app/Http/Controllers/FrontMemeController.php
new file mode 100644
index 0000000..e08210d
--- /dev/null
+++ b/app/Http/Controllers/FrontMemeController.php
@@ -0,0 +1,98 @@
+getMemes($request->input('search'));
+ }
+
+ public function search(string $search): Response
+ {
+ // Convert + back to spaces for search
+ $searchTerm = str_replace('+', ' ', $search);
+ return $this->getMemes($searchTerm);
+ }
+
+ private function getMemes(?string $search = null): Response
+ {
+ $query = MemeMedia::query()
+ ->where('is_enabled', true)
+ ->orderBy('created_at', '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->paginate(12);
+
+ // Get available types for filter
+ $types = MemeMedia::where('is_enabled', true)
+ ->distinct()
+ ->pluck('type')
+ ->filter();
+
+ // Get popular keywords for filter
+ $popularKeywords = MemeMedia::where('is_enabled', true)
+ ->get()
+ ->pluck('keywords')
+ ->flatten()
+ ->countBy()
+ ->sort()
+ ->reverse()
+ ->take(20)
+ ->keys();
+
+ return Inertia::render('memes/index', [
+ 'memes' => $memes,
+ 'types' => $types,
+ 'popularKeywords' => $popularKeywords,
+ 'filters' => [
+ 'search' => $search,
+ ],
+ ]);
+ }
+
+ public function show(string $slug): Response
+ {
+ $meme = MemeMedia::where('slug', $slug)
+ ->where('is_enabled', true)
+ ->firstOrFail();
+
+ // 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();
+
+ return Inertia::render('memes/show', [
+ 'meme' => $meme,
+ 'relatedMemes' => $relatedMemes,
+ ]);
+ }
+}
diff --git a/app/Models/MemeMedia.php b/app/Models/MemeMedia.php
index 66f599e..f1f92a4 100644
--- a/app/Models/MemeMedia.php
+++ b/app/Models/MemeMedia.php
@@ -53,6 +53,7 @@ class MemeMedia extends Model
'sub_type',
'original_id',
'name',
+ 'slug',
'description',
'keywords',
'group',
diff --git a/database/migrations/2025_07_16_084731_add_slug_to_meme_medias_table.php b/database/migrations/2025_07_16_084731_add_slug_to_meme_medias_table.php
new file mode 100644
index 0000000..7f2ed61
--- /dev/null
+++ b/database/migrations/2025_07_16_084731_add_slug_to_meme_medias_table.php
@@ -0,0 +1,30 @@
+string('slug')->nullable()->after('name');
+ $table->index('slug');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('meme_medias', function (Blueprint $table) {
+ $table->dropIndex(['slug']);
+ $table->dropColumn('slug');
+ });
+ }
+};
diff --git a/resources/js/components/ui/keyword-badge.tsx b/resources/js/components/ui/keyword-badge.tsx
new file mode 100644
index 0000000..4a09d6f
--- /dev/null
+++ b/resources/js/components/ui/keyword-badge.tsx
@@ -0,0 +1,24 @@
+import { Link } from '@inertiajs/react';
+import { route } from 'ziggy-js';
+
+interface KeywordBadgeProps {
+ keyword: string;
+ size?: 'default' | 'lg';
+ className?: string;
+}
+
+const sizeClasses = {
+ default: 'px-2.5 py-0.5 text-xs',
+ lg: 'px-3 py-1 text-sm',
+};
+
+export function KeywordBadge({ keyword, size = 'default', className = '' }: KeywordBadgeProps) {
+ return (
+
+ {keyword}
+
+ );
+}
\ No newline at end of file
diff --git a/resources/js/modules/editor/partials/editor-header.jsx b/resources/js/modules/editor/partials/editor-header.jsx
index e742b82..bd483bf 100644
--- a/resources/js/modules/editor/partials/editor-header.jsx
+++ b/resources/js/modules/editor/partials/editor-header.jsx
@@ -1,4 +1,4 @@
-import { cn } from '@/lib/utils';
+import BrandLogo from '@/pages/home/partials/BrandLogo';
import { useMitt } from '@/plugins/MittContext';
import useLocalSettingsStore from '@/stores/localSettingsStore';
@@ -12,14 +12,7 @@ const EditorHeader = ({ className = '', onNavClick = () => {}, isNavActive = fal
};
return (
-
-

-
-
- MEME
- FAST
-
-
+
//
//