diff --git a/app/Http/Controllers/FrontMediaController.php b/app/Http/Controllers/FrontMediaController.php
index 5f07797..8e9312c 100644
--- a/app/Http/Controllers/FrontMediaController.php
+++ b/app/Http/Controllers/FrontMediaController.php
@@ -55,4 +55,83 @@ public function background(Request $request)
],
]);
}
+
+ public function searchMemes(Request $request)
+ {
+ $query = $request->input('query', '');
+ $limit = $request->input('limit', 30);
+
+ if (empty($query)) {
+ // Return random memes if no search query
+ $memes = MemeMedia::where('type', 'video')->where('sub_type', 'overlay')->take($limit)->inRandomOrder()->get();
+ } else {
+ // PostgreSQL full-text search with ranking
+ $memes = MemeMedia::where('type', 'video')
+ ->where('sub_type', 'overlay')
+ ->where(function ($q) use ($query) {
+ // Search in name and description using ILIKE for partial matches
+ $q->where('name', 'ILIKE', "%{$query}%")
+ ->orWhere('description', 'ILIKE', "%{$query}%")
+ // Search in JSON arrays using PostgreSQL JSON operators
+ ->orWhereRaw("keywords::text ILIKE ?", ["%{$query}%"])
+ ->orWhereRaw("action_keywords::text ILIKE ?", ["%{$query}%"])
+ ->orWhereRaw("emotion_keywords::text ILIKE ?", ["%{$query}%"])
+ ->orWhereRaw("misc_keywords::text ILIKE ?", ["%{$query}%"]);
+ })
+ ->orderByRaw("
+ CASE
+ WHEN name ILIKE ? THEN 1
+ WHEN description ILIKE ? THEN 2
+ WHEN keywords::text ILIKE ? THEN 3
+ WHEN action_keywords::text ILIKE ? THEN 4
+ WHEN emotion_keywords::text ILIKE ? THEN 5
+ WHEN misc_keywords::text ILIKE ? THEN 6
+ ELSE 7
+ END, name ASC
+ ", ["%{$query}%", "%{$query}%", "%{$query}%", "%{$query}%", "%{$query}%", "%{$query}%"])
+ ->take($limit)
+ ->get();
+ }
+
+ return response()->json([
+ 'success' => [
+ 'data' => [
+ 'memes' => $memes,
+ 'query' => $query,
+ ],
+ ],
+ ]);
+ }
+
+ public function searchBackgrounds(Request $request)
+ {
+ $query = $request->input('query', '');
+ $limit = $request->input('limit', 30);
+
+ if (empty($query)) {
+ // Return random backgrounds if no search query
+ $backgrounds = BackgroundMedia::where('status', 'completed')->take($limit)->inRandomOrder()->get();
+ } else {
+ // Search in prompt field using ILIKE for partial matches
+ $backgrounds = BackgroundMedia::where('status', 'completed')
+ ->where('prompt', 'ILIKE', "%{$query}%")
+ ->orderByRaw("
+ CASE
+ WHEN prompt ILIKE ? THEN 1
+ ELSE 2
+ END, prompt ASC
+ ", ["%{$query}%"])
+ ->take($limit)
+ ->get();
+ }
+
+ return response()->json([
+ 'success' => [
+ 'data' => [
+ 'backgrounds' => $backgrounds,
+ 'query' => $query,
+ ],
+ ],
+ ]);
+ }
}
diff --git a/resources/css/app.css b/resources/css/app.css
index dbde794..5ec21c1 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -432,3 +432,17 @@ @keyframes shine {
.shiny-text.disabled {
animation: none;
}
+
+/* Shimmer animation for skeleton loading */
+@keyframes shimmer {
+ 0% {
+ background-position: -200% 0;
+ }
+ 100% {
+ background-position: 200% 0;
+ }
+}
+
+@utility animate-shimmer {
+ animation: shimmer 2s ease-in-out infinite;
+}
diff --git a/resources/js/modules/editor/partials/edit-sidebar.jsx b/resources/js/modules/editor/partials/edit-sidebar.jsx
index dcb1d61..e421d3d 100644
--- a/resources/js/modules/editor/partials/edit-sidebar.jsx
+++ b/resources/js/modules/editor/partials/edit-sidebar.jsx
@@ -1,10 +1,11 @@
import { Button } from '@/components/ui/button';
+import { GridSkeleton } from '@/components/ui/grid-skeleton';
+import { Input } from '@/components/ui/input';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
-import { Spinner } from '@/components/ui/spinner';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { cn } from '@/lib/utils';
import useMediaStore from '@/stores/MediaStore';
-import { Edit3 } from 'lucide-react';
+import { Edit3, Search, X } from 'lucide-react';
import { useEffect, useState } from 'react';
export default function EditSidebar({ isOpen, onClose }) {
@@ -17,6 +18,8 @@ export default function EditSidebar({ isOpen, onClose }) {
selectedBackground,
fetchMemes,
fetchBackgrounds,
+ searchMemes,
+ searchBackgrounds,
selectMeme,
selectBackground,
clearSelectedMeme,
@@ -25,24 +28,120 @@ export default function EditSidebar({ isOpen, onClose }) {
// Track the current active tab
const [activeTab, setActiveTab] = useState('memes');
+
+ // Track search queries
+ const [searchQueries, setSearchQueries] = useState({
+ memes: '',
+ backgrounds: ''
+ });
+
+ // Track if data has been loaded for each tab (to prevent infinite loading on empty results)
+ const [dataLoaded, setDataLoaded] = useState({
+ memes: false,
+ backgrounds: false
+ });
// Fetch data when sidebar opens for the current active tab
useEffect(() => {
if (isOpen) {
- if (activeTab === 'memes' && memes.length === 0 && !isFetchingMemes) {
- fetchMemes();
- } else if (activeTab === 'backgrounds' && backgrounds.length === 0 && !isFetchingBackgrounds) {
- fetchBackgrounds();
+ if (activeTab === 'memes' && !dataLoaded.memes && !isFetchingMemes) {
+ const query = searchQueries.memes;
+ if (query) {
+ searchMemes(query).finally(() => {
+ setDataLoaded(prev => ({ ...prev, memes: true }));
+ });
+ } else {
+ fetchMemes().finally(() => {
+ setDataLoaded(prev => ({ ...prev, memes: true }));
+ });
+ }
+ } else if (activeTab === 'backgrounds' && !dataLoaded.backgrounds && !isFetchingBackgrounds) {
+ const query = searchQueries.backgrounds;
+ if (query) {
+ searchBackgrounds(query).finally(() => {
+ setDataLoaded(prev => ({ ...prev, backgrounds: true }));
+ });
+ } else {
+ fetchBackgrounds().finally(() => {
+ setDataLoaded(prev => ({ ...prev, backgrounds: true }));
+ });
+ }
}
}
- }, [isOpen, activeTab, memes.length, backgrounds.length, isFetchingMemes, isFetchingBackgrounds]);
+ }, [isOpen, activeTab, dataLoaded.memes, dataLoaded.backgrounds, isFetchingMemes, isFetchingBackgrounds]);
const handleTabChange = (value) => {
setActiveTab(value);
- if (value === 'memes' && memes.length === 0 && !isFetchingMemes) {
- fetchMemes();
- } else if (value === 'backgrounds' && backgrounds.length === 0 && !isFetchingBackgrounds) {
- fetchBackgrounds();
+ if (value === 'memes' && !dataLoaded.memes && !isFetchingMemes) {
+ const query = searchQueries.memes;
+ if (query) {
+ searchMemes(query).finally(() => {
+ setDataLoaded(prev => ({ ...prev, memes: true }));
+ });
+ } else {
+ fetchMemes().finally(() => {
+ setDataLoaded(prev => ({ ...prev, memes: true }));
+ });
+ }
+ } else if (value === 'backgrounds' && !dataLoaded.backgrounds && !isFetchingBackgrounds) {
+ const query = searchQueries.backgrounds;
+ if (query) {
+ searchBackgrounds(query).finally(() => {
+ setDataLoaded(prev => ({ ...prev, backgrounds: true }));
+ });
+ } else {
+ fetchBackgrounds().finally(() => {
+ setDataLoaded(prev => ({ ...prev, backgrounds: true }));
+ });
+ }
+ }
+ };
+
+ // Handle search input changes
+ const handleSearchChange = (value) => {
+ setSearchQueries(prev => ({
+ ...prev,
+ [activeTab]: value
+ }));
+ };
+
+ // Handle search submission
+ const handleSearch = () => {
+ const query = searchQueries[activeTab];
+ if (activeTab === 'memes') {
+ searchMemes(query).finally(() => {
+ setDataLoaded(prev => ({ ...prev, memes: true }));
+ });
+ } else if (activeTab === 'backgrounds') {
+ searchBackgrounds(query).finally(() => {
+ setDataLoaded(prev => ({ ...prev, backgrounds: true }));
+ });
+ }
+ };
+
+ // Handle clearing search
+ const handleClearSearch = () => {
+ setSearchQueries(prev => ({
+ ...prev,
+ [activeTab]: ''
+ }));
+
+ // Reset data loaded state and fetch fresh data without search
+ if (activeTab === 'memes') {
+ fetchMemes().finally(() => {
+ setDataLoaded(prev => ({ ...prev, memes: true }));
+ });
+ } else if (activeTab === 'backgrounds') {
+ fetchBackgrounds().finally(() => {
+ setDataLoaded(prev => ({ ...prev, backgrounds: true }));
+ });
+ }
+ };
+
+ // Handle Enter key in search input
+ const handleSearchKeyDown = (e) => {
+ if (e.key === 'Enter') {
+ handleSearch();
}
};
@@ -114,9 +213,39 @@ export default function EditSidebar({ isOpen, onClose }) {