feat: add license filtering for sound effects

This commit is contained in:
Maze Winther
2025-08-04 02:59:07 +02:00
parent 35ade4b25f
commit acc2089d0d
6 changed files with 84 additions and 15 deletions
@@ -12,6 +12,7 @@ const searchParamsSchema = z.object({
.enum(["downloads", "rating", "created", "score"]) .enum(["downloads", "rating", "created", "score"])
.default("downloads"), .default("downloads"),
min_rating: z.coerce.number().min(0).max(5).default(3), min_rating: z.coerce.number().min(0).max(5).default(3),
commercial_only: z.coerce.boolean().default(true),
}); });
const freesoundResultSchema = z.object({ const freesoundResultSchema = z.object({
@@ -124,6 +125,7 @@ export async function GET(request: NextRequest) {
page_size: pageSize, page_size: pageSize,
sort, sort,
min_rating, min_rating,
commercial_only,
} = validationResult.data; } = validationResult.data;
if (type === "songs") { if (type === "songs") {
@@ -160,6 +162,15 @@ export async function GET(request: NextRequest) {
if (type === "effects" || !type) { if (type === "effects" || !type) {
params.append("filter", "duration:[* TO 30.0]"); params.append("filter", "duration:[* TO 30.0]");
params.append("filter", `avg_rating:[${min_rating} TO *]`); params.append("filter", `avg_rating:[${min_rating} TO *]`);
// Filter by license if commercial_only is true
if (commercial_only) {
params.append(
"filter",
'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")'
);
}
params.append( params.append(
"filter", "filter",
"tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion" "tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion"
@@ -5,11 +5,23 @@ import { useState, useMemo, useRef, useEffect } from "react";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { PlayIcon, PauseIcon, HeartIcon, PlusIcon } from "lucide-react"; import {
PlayIcon,
PauseIcon,
HeartIcon,
PlusIcon,
ListFilter,
} from "lucide-react";
import { useSoundsStore } from "@/stores/sounds-store"; import { useSoundsStore } from "@/stores/sounds-store";
import { useSoundSearch } from "@/hooks/use-sound-search"; import { useSoundSearch } from "@/hooks/use-sound-search";
import type { SoundEffect, SavedSound } from "@/types/sounds"; import type { SoundEffect, SavedSound } from "@/types/sounds";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuCheckboxItem,
} from "@/components/ui/dropdown-menu";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -19,6 +31,7 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
export function SoundsView() { export function SoundsView() {
return ( return (
@@ -66,6 +79,8 @@ function SoundEffectsView() {
loadSavedSounds, loadSavedSounds,
isSoundSaved, isSoundSaved,
toggleSavedSound, toggleSavedSound,
showCommercialOnly,
toggleCommercialFilter,
} = useSoundsStore(); } = useSoundsStore();
const { const {
results: searchResults, results: searchResults,
@@ -73,7 +88,7 @@ function SoundEffectsView() {
loadMore, loadMore,
hasNextPage, hasNextPage,
isLoadingMore, isLoadingMore,
} = useSoundSearch(searchQuery); } = useSoundSearch(searchQuery, showCommercialOnly);
// Audio playback state // Audio playback state
const [playingId, setPlayingId] = useState<number | null>(null); const [playingId, setPlayingId] = useState<number | null>(null);
@@ -144,14 +159,41 @@ function SoundEffectsView() {
return ( return (
<div className="flex flex-col gap-5 mt-1 h-full"> <div className="flex flex-col gap-5 mt-1 h-full">
<Input <div className="flex items-center gap-3">
placeholder="Search sound effects" <Input
className="bg-panel-accent" placeholder="Search sound effects"
value={searchQuery} className="bg-panel-accent w-full"
onChange={(e) => setSearchQuery(e.target.value)} containerClassName="w-full"
showClearIcon value={searchQuery}
onClear={() => setSearchQuery("")} onChange={(e) => setSearchQuery(e.target.value)}
/> showClearIcon
onClear={() => setSearchQuery("")}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="text"
size="icon"
className={cn(showCommercialOnly && "text-primary")}
>
<ListFilter className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuCheckboxItem
checked={showCommercialOnly}
onCheckedChange={toggleCommercialFilter}
>
Show only commercially licensed
</DropdownMenuCheckboxItem>
<div className="px-2 py-1.5 text-xs text-muted-foreground">
{showCommercialOnly
? "Only showing sounds licensed for commercial use"
: "Showing all sounds regardless of license"}
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="relative h-full overflow-hidden"> <div className="relative h-full overflow-hidden">
<ScrollArea <ScrollArea
+7 -3
View File
@@ -126,20 +126,24 @@ const DropdownMenuCheckboxItem = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
dropdownMenuItemVariants({ variant }), dropdownMenuItemVariants({ variant }),
"pl-8 pr-2", "pl-2 pr-8",
className className
)} )}
checked={checked} checked={checked}
onSelect={(e) => {
e.preventDefault();
}}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> {children}
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
)); ));
DropdownMenuCheckboxItem.displayName = DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName; DropdownMenuPrimitive.CheckboxItem.displayName;
+3 -1
View File
@@ -9,6 +9,7 @@ interface InputProps extends React.ComponentProps<"input"> {
onShowPasswordChange?: (show: boolean) => void; onShowPasswordChange?: (show: boolean) => void;
showClearIcon?: boolean; showClearIcon?: boolean;
onClear?: () => void; onClear?: () => void;
containerClassName?: string;
} }
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
@@ -16,6 +17,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
{ {
className, className,
type, type,
containerClassName,
showPassword, showPassword,
onShowPasswordChange, onShowPasswordChange,
showClearIcon, showClearIcon,
@@ -45,7 +47,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
iconCount === 2 ? "pr-20" : iconCount === 1 ? "pr-10" : ""; iconCount === 2 ? "pr-20" : iconCount === 1 ? "pr-10" : "";
return ( return (
<div className={hasIcons ? "relative w-full" : ""}> <div className={cn(hasIcons ? "relative w-full" : "", containerClassName)}>
<input <input
type={inputType} type={inputType}
className={cn( className={cn(
+2 -1
View File
@@ -9,7 +9,7 @@ import { useSoundsStore } from "@/stores/sounds-store";
* - Proper error handling * - Proper error handling
*/ */
export function useSoundSearch(query: string) { export function useSoundSearch(query: string, commercialOnly: boolean) {
const { const {
searchResults, searchResults,
isSearching, isSearching,
@@ -49,6 +49,7 @@ export function useSoundSearch(query: string) {
searchParams.set("q", query); searchParams.set("q", query);
} }
searchParams.set("commercial_only", commercialOnly.toString());
const response = await fetch( const response = await fetch(
`/api/sounds/search?${searchParams.toString()}` `/api/sounds/search?${searchParams.toString()}`
); );
+9
View File
@@ -13,6 +13,10 @@ interface SoundsStore {
error: string | null; error: string | null;
hasLoaded: boolean; hasLoaded: boolean;
// Filter state
showCommercialOnly: boolean;
toggleCommercialFilter: () => void;
// Search state // Search state
searchQuery: string; searchQuery: string;
searchResults: SoundEffect[]; searchResults: SoundEffect[];
@@ -72,6 +76,11 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({
isLoading: false, isLoading: false,
error: null, error: null,
hasLoaded: false, hasLoaded: false,
showCommercialOnly: true,
toggleCommercialFilter: () => {
set((state) => ({ showCommercialOnly: !state.showCommercialOnly }));
},
// Search state // Search state
searchQuery: "", searchQuery: "",