From 3e89d2998588f8e35c524e523ba2ee853f0f6475 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 20 Apr 2026 11:31:17 +0200 Subject: refactor: restructure files to their domains, new preview overlay system, and dep graph --- apps/web/src/sounds/components/assets-view.tsx | 563 +++++++++++++++++++++++++ 1 file changed, 563 insertions(+) create mode 100644 apps/web/src/sounds/components/assets-view.tsx (limited to 'apps/web/src/sounds/components') diff --git a/apps/web/src/sounds/components/assets-view.tsx b/apps/web/src/sounds/components/assets-view.tsx new file mode 100644 index 00000000..fc7528c8 --- /dev/null +++ b/apps/web/src/sounds/components/assets-view.tsx @@ -0,0 +1,563 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { useInfiniteScroll } from "@/hooks/use-infinite-scroll"; +import { useSoundSearch } from "@/sounds/use-sound-search"; +import { useSoundsStore } from "@/sounds/sounds-store"; +import type { SavedSound, SoundEffect } from "@/sounds/types"; +import { cn } from "@/utils/ui"; +import { + FavouriteIcon, + FilterMailIcon, + PauseIcon, + PlayIcon, + PlusSignIcon, +} from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; + +export function SoundsView() { + return ( +
+ +
+ + Sound effects + Saved + +
+ + + + + + + +
+
+ ); +} + +function SoundEffectsView() { + const { + topSoundEffects, + isLoading, + searchQuery, + setSearchQuery, + scrollPosition, + setScrollPosition, + loadSavedSounds, + showCommercialOnly, + toggleCommercialFilter, + hasLoaded, + setTopSoundEffects, + setLoading, + setError, + setHasLoaded, + setCurrentPage, + setHasNextPage, + setTotalCount, + } = useSoundsStore(); + const { + results: searchResults, + isLoading: isSearching, + loadMore, + hasNextPage, + isLoadingMore, + } = useSoundSearch({ + query: searchQuery, + commercialOnly: showCommercialOnly, + }); + + const [playingId, setPlayingId] = useState(null); + const [audioElement, setAudioElement] = useState( + null, + ); + + const { scrollAreaRef, handleScroll } = useInfiniteScroll({ + onLoadMore: loadMore, + hasMore: hasNextPage, + isLoading: isLoadingMore || isSearching, + }); + + useEffect(() => { + loadSavedSounds(); + }, [loadSavedSounds]); + + useEffect(() => { + if (hasLoaded) { + return; + } + + let shouldIgnore = false; + + const fetchTopSounds = async () => { + try { + if (!shouldIgnore) { + setLoading({ loading: true }); + setError({ error: null }); + } + + const response = await fetch( + "/api/sounds/search?page_size=50&sort=downloads", + ); + + if (!shouldIgnore) { + if (!response.ok) { + throw new Error(`Failed to fetch: ${response.status}`); + } + + const data = await response.json(); + setTopSoundEffects({ sounds: data.results }); + setHasLoaded({ loaded: true }); + + setCurrentPage({ page: 1 }); + setHasNextPage({ hasNext: !!data.next }); + setTotalCount({ count: data.count }); + } + } catch (error) { + if (!shouldIgnore) { + console.error("Failed to fetch top sounds:", error); + setError({ + error: + error instanceof Error ? error.message : "Failed to load sounds", + }); + } + } finally { + if (!shouldIgnore) { + setLoading({ loading: false }); + } + } + }; + + const timeoutId = setTimeout(fetchTopSounds, 100, {}); + + return () => { + shouldIgnore = true; + clearTimeout(timeoutId); + }; + }, [ + hasLoaded, + setTopSoundEffects, + setLoading, + setError, + setHasLoaded, + setCurrentPage, + setHasNextPage, + setTotalCount, + ]); + + useEffect(() => { + if (!scrollAreaRef.current || scrollPosition <= 0) { + return; + } + + const restoreScrollPosition = () => { + scrollAreaRef.current?.scrollTo({ top: scrollPosition }); + }; + + const timeoutId = setTimeout(restoreScrollPosition, 100, {}); + + return () => clearTimeout(timeoutId); + }, [scrollPosition, scrollAreaRef]); + + const handleScrollWithPosition = ({ + currentTarget, + }: React.UIEvent) => { + const { scrollTop } = currentTarget; + setScrollPosition({ position: scrollTop }); + handleScroll({ currentTarget } as React.UIEvent); + }; + + const displayedSounds = searchQuery ? searchResults : topSoundEffects; + + const playSound = ({ sound }: { sound: SoundEffect }) => { + if (playingId === sound.id) { + audioElement?.pause(); + setPlayingId(null); + return; + } + + audioElement?.pause(); + + if (sound.previewUrl) { + const audio = new Audio(sound.previewUrl); + audio.addEventListener("ended", () => { + setPlayingId(null); + }); + audio.addEventListener("error", () => { + setPlayingId(null); + }); + audio.play().catch((error) => { + console.error("Failed to play sound preview:", error); + setPlayingId(null); + }); + + setAudioElement(audio); + setPlayingId(sound.id); + } + }; + + return ( +
+
+ + setSearchQuery({ query: currentTarget.value }) + } + showClearIcon + onClear={() => setSearchQuery({ query: "" })} + /> + + + + + + toggleCommercialFilter()} + > + Show only commercially licensed + +
+ {showCommercialOnly + ? "Only showing sounds licensed for commercial use" + : "Showing all sounds regardless of license"} +
+
+
+
+ +
+ +
+ {isLoading && !searchQuery && ( +
+ Loading sounds... +
+ )} + {isSearching && searchQuery && ( +
Searching...
+ )} + {displayedSounds.map((sound) => ( + + ))} + {!isLoading && !isSearching && displayedSounds.length === 0 && ( +
+ {searchQuery ? "No sounds found" : "No sounds available"} +
+ )} + {isLoadingMore && ( +
+ Loading more sounds... +
+ )} +
+
+
+
+ ); +} + +function SavedSoundsView() { + const { + savedSounds, + isLoadingSavedSounds, + savedSoundsError, + loadSavedSounds, + clearSavedSounds, + } = useSoundsStore(); + + const [playingId, setPlayingId] = useState(null); + const [audioElement, setAudioElement] = useState( + null, + ); + + const [showClearDialog, setShowClearDialog] = useState(false); + + useEffect(() => { + loadSavedSounds(); + }, [loadSavedSounds]); + + const playSound = ({ sound }: { sound: SoundEffect }) => { + if (playingId === sound.id) { + audioElement?.pause(); + setPlayingId(null); + return; + } + + audioElement?.pause(); + + if (sound.previewUrl) { + const audio = new Audio(sound.previewUrl); + audio.addEventListener("ended", () => { + setPlayingId(null); + }); + audio.addEventListener("error", () => { + setPlayingId(null); + }); + audio.play().catch((error) => { + console.error("Failed to play sound preview:", error); + setPlayingId(null); + }); + + setAudioElement(audio); + setPlayingId(sound.id); + } + }; + + const convertToSoundEffect = ({ + savedSound, + }: { + savedSound: SavedSound; + }): SoundEffect => ({ + id: savedSound.id, + name: savedSound.name, + description: "", + url: "", + previewUrl: savedSound.previewUrl, + downloadUrl: savedSound.downloadUrl, + duration: savedSound.duration, + filesize: 0, + type: "audio", + channels: 0, + bitrate: 0, + bitdepth: 0, + samplerate: 0, + username: savedSound.username, + tags: savedSound.tags, + license: savedSound.license, + created: savedSound.savedAt, + downloads: 0, + rating: 0, + ratingCount: 0, + }); + + if (isLoadingSavedSounds) { + return ( +
+
+ Loading saved sounds... +
+
+ ); + } + + if (savedSoundsError) { + return ( +
+
+ Error: {savedSoundsError} +
+
+ ); + } + + if (savedSounds.length === 0) { + return ( +
+ +
+

No saved sounds

+

+ Click the heart icon on any sound to save it here +

+
+
+ ); + } + + return ( +
+
+

+ {savedSounds.length} saved{" "} + {savedSounds.length === 1 ? "sound" : "sounds"} +

+ + + + + + + Clear all saved sounds? + + This will permanently remove all {savedSounds.length} saved + sounds from your collection. This action cannot be undone. + + + + + + + + +
+ +
+ +
+ {savedSounds.map((sound) => ( + + ))} +
+
+
+
+ ); +} + +interface AudioItemProps { + sound: SoundEffect; + isPlaying: boolean; + onPlay: ({ sound }: { sound: SoundEffect }) => void; +} + +function AudioItem({ sound, isPlaying, onPlay }: AudioItemProps) { + const { addSoundToTimeline, isSoundSaved, toggleSavedSound } = + useSoundsStore(); + const isSaved = isSoundSaved({ soundId: sound.id }); + + const handleClick = () => { + onPlay({ sound }); + }; + + const handleSaveClick = ({ + stopPropagation, + }: React.MouseEvent) => { + stopPropagation(); + toggleSavedSound({ soundEffect: sound }); + }; + + const handleAddToTimeline = async ({ + stopPropagation, + }: React.MouseEvent) => { + stopPropagation(); + await addSoundToTimeline({ sound }); + }; + + return ( +
+ + +
+ + +
+
+ ); +} -- cgit v1.3.1