Update
This commit is contained in:
@@ -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,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -432,3 +432,17 @@ @keyframes shine {
|
|||||||
.shiny-text.disabled {
|
.shiny-text.disabled {
|
||||||
animation: none;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
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 { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import useMediaStore from '@/stores/MediaStore';
|
import useMediaStore from '@/stores/MediaStore';
|
||||||
import { Edit3 } from 'lucide-react';
|
import { Edit3, Search, X } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function EditSidebar({ isOpen, onClose }) {
|
export default function EditSidebar({ isOpen, onClose }) {
|
||||||
@@ -17,6 +18,8 @@ export default function EditSidebar({ isOpen, onClose }) {
|
|||||||
selectedBackground,
|
selectedBackground,
|
||||||
fetchMemes,
|
fetchMemes,
|
||||||
fetchBackgrounds,
|
fetchBackgrounds,
|
||||||
|
searchMemes,
|
||||||
|
searchBackgrounds,
|
||||||
selectMeme,
|
selectMeme,
|
||||||
selectBackground,
|
selectBackground,
|
||||||
clearSelectedMeme,
|
clearSelectedMeme,
|
||||||
@@ -26,23 +29,119 @@ export default function EditSidebar({ isOpen, onClose }) {
|
|||||||
// Track the current active tab
|
// Track the current active tab
|
||||||
const [activeTab, setActiveTab] = useState('memes');
|
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
|
// Fetch data when sidebar opens for the current active tab
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
if (activeTab === 'memes' && memes.length === 0 && !isFetchingMemes) {
|
if (activeTab === 'memes' && !dataLoaded.memes && !isFetchingMemes) {
|
||||||
fetchMemes();
|
const query = searchQueries.memes;
|
||||||
} else if (activeTab === 'backgrounds' && backgrounds.length === 0 && !isFetchingBackgrounds) {
|
if (query) {
|
||||||
fetchBackgrounds();
|
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) => {
|
const handleTabChange = (value) => {
|
||||||
setActiveTab(value);
|
setActiveTab(value);
|
||||||
if (value === 'memes' && memes.length === 0 && !isFetchingMemes) {
|
if (value === 'memes' && !dataLoaded.memes && !isFetchingMemes) {
|
||||||
fetchMemes();
|
const query = searchQueries.memes;
|
||||||
} else if (value === 'backgrounds' && backgrounds.length === 0 && !isFetchingBackgrounds) {
|
if (query) {
|
||||||
fetchBackgrounds();
|
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 }) {
|
|||||||
<TabsTrigger value="backgrounds">Background</TabsTrigger>
|
<TabsTrigger value="backgrounds">Background</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
|
{/* Search Bar */}
|
||||||
|
<div className="relative flex items-center gap-2 px-2 pt-3">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||||
|
<Input
|
||||||
|
placeholder={`Search ${activeTab === 'memes' ? 'memes' : 'backgrounds'}...`}
|
||||||
|
value={searchQueries[activeTab]}
|
||||||
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
|
onKeyDown={handleSearchKeyDown}
|
||||||
|
className="pl-10 pr-10"
|
||||||
|
/>
|
||||||
|
{searchQueries[activeTab] && (
|
||||||
|
<button
|
||||||
|
onClick={handleClearSearch}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleSearch}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="px-3"
|
||||||
|
>
|
||||||
|
<Search className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<TabsContent value="backgrounds" className="">
|
<TabsContent value="backgrounds" className="">
|
||||||
{isFetchingBackgrounds && <Spinner className="h-4 w-4"></Spinner>}
|
{isFetchingBackgrounds && <GridSkeleton itemCount={6} />}
|
||||||
{!isFetchingBackgrounds && backgrounds.length === 0 && <div className="w-full text-center">No backgrounds available.</div>}
|
{!isFetchingBackgrounds && backgrounds.length === 0 && <div className="w-full text-center p-8 text-gray-500">No backgrounds found.</div>}
|
||||||
{!isFetchingBackgrounds && backgrounds.length > 0 && (
|
{!isFetchingBackgrounds && backgrounds.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
@@ -139,18 +268,13 @@ export default function EditSidebar({ isOpen, onClose }) {
|
|||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="fixed bottom-5 mx-auto flex w-75 justify-center gap-2">
|
|
||||||
<Button className="rounded-full px-5" onClick={fetchBackgrounds}>
|
|
||||||
Refresh List{' '}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="memes" className="">
|
<TabsContent value="memes" className="">
|
||||||
{isFetchingMemes && <Spinner className="h-4 w-4"></Spinner>}
|
{isFetchingMemes && <GridSkeleton itemCount={6} />}
|
||||||
{!isFetchingMemes && memes.length === 0 && <div className="w-full text-center">No memes available.</div>}
|
{!isFetchingMemes && memes.length === 0 && <div className="w-full text-center p-8 text-gray-500">No memes found.</div>}
|
||||||
{!isFetchingMemes && memes.length > 0 && (
|
{!isFetchingMemes && memes.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-2 gap-2 p-2">
|
<div className="grid grid-cols-2 gap-2 p-2">
|
||||||
@@ -167,11 +291,6 @@ export default function EditSidebar({ isOpen, onClose }) {
|
|||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="fixed bottom-5 mx-auto flex w-75 justify-center gap-2">
|
|
||||||
<Button className="rounded-full px-5 shadow-lg" onClick={fetchMemes}>
|
|
||||||
Refresh List{' '}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -206,6 +206,56 @@ const useMediaStore = create(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Search memes
|
||||||
|
searchMemes: async (query = '') => {
|
||||||
|
set({ isFetchingMemes: true });
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.post(route('api.app.search.memes'), { query });
|
||||||
|
|
||||||
|
if (response?.data?.success?.data?.memes) {
|
||||||
|
set({
|
||||||
|
memes: response.data.success.data.memes,
|
||||||
|
isFetchingMemes: false,
|
||||||
|
});
|
||||||
|
return response.data.success.data.memes;
|
||||||
|
} else {
|
||||||
|
throw 'Invalid API response';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error searching memes:', error);
|
||||||
|
set({ isFetchingMemes: false });
|
||||||
|
if (error?.response?.data?.error?.message?.length > 0) {
|
||||||
|
toast.error(error.response.data.error.message);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Search backgrounds
|
||||||
|
searchBackgrounds: async (query = '') => {
|
||||||
|
set({ isFetchingBackgrounds: true });
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.post(route('api.app.search.background'), { query });
|
||||||
|
|
||||||
|
if (response?.data?.success?.data?.backgrounds) {
|
||||||
|
set({
|
||||||
|
backgrounds: response.data.success.data.backgrounds,
|
||||||
|
isFetchingBackgrounds: false,
|
||||||
|
});
|
||||||
|
return response.data.success.data.backgrounds;
|
||||||
|
} else {
|
||||||
|
throw 'Invalid API response';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error searching backgrounds:', error);
|
||||||
|
set({ isFetchingBackgrounds: false });
|
||||||
|
if (error?.response?.data?.error?.message?.length > 0) {
|
||||||
|
toast.error(error.response.data.error.message);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Reset store to default state
|
// Reset store to default state
|
||||||
restoreMemeStateToDefault: () => {
|
restoreMemeStateToDefault: () => {
|
||||||
console.log('restoreMemeStateToDefault');
|
console.log('restoreMemeStateToDefault');
|
||||||
|
|||||||
@@ -49,6 +49,8 @@
|
|||||||
Route::post('init', [FrontMediaController::class, 'init'])->name('api.app.init');
|
Route::post('init', [FrontMediaController::class, 'init'])->name('api.app.init');
|
||||||
|
|
||||||
Route::post('memes', [FrontMediaController::class, 'memes'])->name('api.app.memes');
|
Route::post('memes', [FrontMediaController::class, 'memes'])->name('api.app.memes');
|
||||||
|
Route::post('search/memes', [FrontMediaController::class, 'searchMemes'])->name('api.app.search.memes');
|
||||||
|
|
||||||
Route::post('background', [FrontMediaController::class, 'background'])->name('api.app.background');
|
Route::post('background', [FrontMediaController::class, 'background'])->name('api.app.background');
|
||||||
|
Route::post('search/background', [FrontMediaController::class, 'searchBackgrounds'])->name('api.app.search.background');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user