Skip to content

Commit 0eb6146

Browse files
committed
feat: Implement asset management system with new hook, component, and Firebase functions.
1 parent e516718 commit 0eb6146

3 files changed

Lines changed: 327 additions & 82 deletions

File tree

components/admin/assets/asset-manager.tsx

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,60 @@ const formatBytes = (bytes: number, decimals = 2) => {
203203
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
204204
};
205205

206+
// Lazy loading thumbnail component
207+
import { getDownloadURL, ref } from "firebase/storage";
208+
import { storage } from "@/lib/firebase";
209+
210+
const AssetThumbnail = ({ asset }: { asset: Asset }) => {
211+
const [url, setUrl] = useState<string | null>(asset.url || null);
212+
const [loading, setLoading] = useState(!asset.url);
213+
const [error, setError] = useState(false);
214+
215+
useEffect(() => {
216+
if (url) return;
217+
218+
let active = true;
219+
const fetchUrl = async () => {
220+
try {
221+
const downloadUrl = await getDownloadURL(ref(storage, asset.path));
222+
if (active) {
223+
setUrl(downloadUrl);
224+
setLoading(false);
225+
}
226+
} catch (e) {
227+
if (active) {
228+
console.error("Failed to load thumbnail", e);
229+
setError(true);
230+
setLoading(false);
231+
}
232+
}
233+
};
234+
235+
fetchUrl();
236+
return () => {
237+
active = false;
238+
};
239+
}, [asset.path, url]);
240+
241+
if (loading)
242+
return <div className="animate-pulse bg-white/10 w-full h-full" />;
243+
if (error || !url)
244+
return (
245+
<div className="flex items-center justify-center w-full h-full bg-white/5">
246+
<ImageIcon className="h-8 w-8 text-white/20" />
247+
</div>
248+
);
249+
250+
return (
251+
<img
252+
src={url}
253+
alt={asset.name}
254+
className="object-cover w-full h-full transition-transform group-hover:scale-105"
255+
onError={() => setError(true)}
256+
/>
257+
);
258+
};
259+
206260
export function AssetManager({ rootPath = "" }: { rootPath?: string }) {
207261
// Root level management for Tabs
208262
const {
@@ -246,6 +300,7 @@ export function AssetManager({ rootPath = "" }: { rootPath?: string }) {
246300
getFileBlob,
247301
downloadFolder,
248302
downloadFile,
303+
syncIndex,
249304
} = useAssets(currentPath);
250305

251306
const [newFolderName, setNewFolderName] = useState("");
@@ -633,11 +688,7 @@ export function AssetManager({ rootPath = "" }: { rootPath?: string }) {
633688
</div>
634689
<div className="aspect-square bg-black/40 flex items-center justify-center relative overflow-hidden">
635690
{asset.mimeType?.startsWith("image/") ? (
636-
<img
637-
src={asset.url}
638-
alt={asset.name}
639-
className="object-cover w-full h-full transition-transform group-hover:scale-105"
640-
/>
691+
<AssetThumbnail asset={asset} />
641692
) : (
642693
(() => {
643694
const { icon: Icon, color } = getFileIcon(
@@ -689,8 +740,11 @@ export function AssetManager({ rootPath = "" }: { rootPath?: string }) {
689740
>
690741
{asset.name}
691742
</p>
692-
<p className="text-xs text-white/40 mt-1">
693-
{formatBytes(asset.size || 0)}
743+
<p
744+
className="text-xs text-white/40 mt-1 truncate"
745+
title={searchTerm ? asset.path : undefined}
746+
>
747+
{searchTerm ? asset.path : formatBytes(asset.size || 0)}
694748
</p>
695749
</div>
696750
<DropdownMenu>
@@ -978,15 +1032,26 @@ export function AssetManager({ rootPath = "" }: { rootPath?: string }) {
9781032

9791033
{/* Search and Actions Row */}
9801034
<div className="flex flex-col sm:flex-row gap-2">
981-
<div className="relative flex-1">
982-
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-white/50" />
983-
<Input
984-
type="search"
985-
placeholder="Search assets..."
986-
className="pl-8 bg-[#121212] border-white/20 focus-visible:ring-purple-500 w-full"
987-
value={searchTerm}
988-
onChange={(e) => setSearchTerm(e.target.value)}
989-
/>
1035+
<div className="flex gap-2 w-full sm:w-auto">
1036+
{/* Manual Re-Index Button Removed as per user request */}
1037+
1038+
<div className="flex-1 sm:flex-none relative">
1039+
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-white/40" />
1040+
<Input
1041+
className="pl-9 w-full sm:w-[250px] bg-white/5 border-white/10 text-white placeholder:text-white/40 focus:border-purple-500/50"
1042+
placeholder="Search assets..."
1043+
value={searchTerm}
1044+
onChange={(e) => setSearchTerm(e.target.value)}
1045+
/>
1046+
{searchTerm && (
1047+
<button
1048+
onClick={() => setSearchTerm("")}
1049+
className="absolute right-2.5 top-2.5 text-white/40 hover:text-white"
1050+
>
1051+
<X className="h-4 w-4" />
1052+
</button>
1053+
)}
1054+
</div>
9901055
</div>
9911056

9921057
<div className="flex gap-2">

0 commit comments

Comments
 (0)