This commit is contained in:
ct
2025-07-15 02:28:22 +08:00
parent 6838c542f5
commit d245f5f77c
5 changed files with 289 additions and 25 deletions

View File

@@ -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;
}

View File

@@ -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 }) {
<TabsTrigger value="backgrounds">Background</TabsTrigger>
</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="">
{isFetchingBackgrounds && <Spinner className="h-4 w-4"></Spinner>}
{!isFetchingBackgrounds && backgrounds.length === 0 && <div className="w-full text-center">No backgrounds available.</div>}
{isFetchingBackgrounds && <GridSkeleton itemCount={6} />}
{!isFetchingBackgrounds && backgrounds.length === 0 && <div className="w-full text-center p-8 text-gray-500">No backgrounds found.</div>}
{!isFetchingBackgrounds && backgrounds.length > 0 && (
<>
<div className="grid grid-cols-2 gap-2">
@@ -139,18 +268,13 @@ export default function EditSidebar({ isOpen, onClose }) {
</Button>
))}
</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 value="memes" className="">
{isFetchingMemes && <Spinner className="h-4 w-4"></Spinner>}
{!isFetchingMemes && memes.length === 0 && <div className="w-full text-center">No memes available.</div>}
{isFetchingMemes && <GridSkeleton itemCount={6} />}
{!isFetchingMemes && memes.length === 0 && <div className="w-full text-center p-8 text-gray-500">No memes found.</div>}
{!isFetchingMemes && memes.length > 0 && (
<>
<div className="grid grid-cols-2 gap-2 p-2">
@@ -167,11 +291,6 @@ export default function EditSidebar({ isOpen, onClose }) {
</Button>
))}
</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>

View File

@@ -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
restoreMemeStateToDefault: () => {
console.log('restoreMemeStateToDefault');