summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apps/web/src/app/api/sounds/search/route.ts11
-rw-r--r--apps/web/src/components/editor/media-panel/views/sounds.tsx62
-rw-r--r--apps/web/src/components/ui/dropdown-menu.tsx10
-rw-r--r--apps/web/src/components/ui/input.tsx4
-rw-r--r--apps/web/src/hooks/use-sound-search.ts3
-rw-r--r--apps/web/src/stores/sounds-store.ts9
6 files changed, 84 insertions, 15 deletions
diff --git a/apps/web/src/app/api/sounds/search/route.ts b/apps/web/src/app/api/sounds/search/route.ts
index 15954140..c89bc76c 100644
--- a/apps/web/src/app/api/sounds/search/route.ts
+++ b/apps/web/src/app/api/sounds/search/route.ts
@@ -12,6 +12,7 @@ const searchParamsSchema = z.object({
.enum(["downloads", "rating", "created", "score"])
.default("downloads"),
min_rating: z.coerce.number().min(0).max(5).default(3),
+ commercial_only: z.coerce.boolean().default(true),
});
const freesoundResultSchema = z.object({
@@ -124,6 +125,7 @@ export async function GET(request: NextRequest) {
page_size: pageSize,
sort,
min_rating,
+ commercial_only,
} = validationResult.data;
if (type === "songs") {
@@ -160,6 +162,15 @@ export async function GET(request: NextRequest) {
if (type === "effects" || !type) {
params.append("filter", "duration:[* TO 30.0]");
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(
"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"
diff --git a/apps/web/src/components/editor/media-panel/views/sounds.tsx b/apps/web/src/components/editor/media-panel/views/sounds.tsx
index 5ffa51a9..ed0db8ab 100644
--- a/apps/web/src/components/editor/media-panel/views/sounds.tsx
+++ b/apps/web/src/components/editor/media-panel/views/sounds.tsx
@@ -5,12 +5,24 @@ import { useState, useMemo, useRef, useEffect } from "react";
import { Separator } from "@/components/ui/separator";
import { ScrollArea } from "@/components/ui/scroll-area";
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 { useSoundSearch } from "@/hooks/use-sound-search";
import type { SoundEffect, SavedSound } from "@/types/sounds";
import { Button } from "@/components/ui/button";
import {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuCheckboxItem,
+} from "@/components/ui/dropdown-menu";
+import {
Dialog,
DialogContent,
DialogDescription,
@@ -19,6 +31,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
+import { cn } from "@/lib/utils";
export function SoundsView() {
return (
@@ -66,6 +79,8 @@ function SoundEffectsView() {
loadSavedSounds,
isSoundSaved,
toggleSavedSound,
+ showCommercialOnly,
+ toggleCommercialFilter,
} = useSoundsStore();
const {
results: searchResults,
@@ -73,7 +88,7 @@ function SoundEffectsView() {
loadMore,
hasNextPage,
isLoadingMore,
- } = useSoundSearch(searchQuery);
+ } = useSoundSearch(searchQuery, showCommercialOnly);
// Audio playback state
const [playingId, setPlayingId] = useState<number | null>(null);
@@ -144,14 +159,41 @@ function SoundEffectsView() {
return (
<div className="flex flex-col gap-5 mt-1 h-full">
- <Input
- placeholder="Search sound effects"
- className="bg-panel-accent"
- value={searchQuery}
- onChange={(e) => setSearchQuery(e.target.value)}
- showClearIcon
- onClear={() => setSearchQuery("")}
- />
+ <div className="flex items-center gap-3">
+ <Input
+ placeholder="Search sound effects"
+ className="bg-panel-accent w-full"
+ containerClassName="w-full"
+ value={searchQuery}
+ 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">
<ScrollArea
diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx
index bdd86d58..823d48a3 100644
--- a/apps/web/src/components/ui/dropdown-menu.tsx
+++ b/apps/web/src/components/ui/dropdown-menu.tsx
@@ -126,20 +126,24 @@ const DropdownMenuCheckboxItem = React.forwardRef<
ref={ref}
className={cn(
dropdownMenuItemVariants({ variant }),
- "pl-8 pr-2",
+ "pl-2 pr-8",
className
)}
checked={checked}
+ onSelect={(e) => {
+ e.preventDefault();
+ }}
{...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>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
- {children}
</DropdownMenuPrimitive.CheckboxItem>
));
+
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx
index 4fb0f5b4..440191f2 100644
--- a/apps/web/src/components/ui/input.tsx
+++ b/apps/web/src/components/ui/input.tsx
@@ -9,6 +9,7 @@ interface InputProps extends React.ComponentProps<"input"> {
onShowPasswordChange?: (show: boolean) => void;
showClearIcon?: boolean;
onClear?: () => void;
+ containerClassName?: string;
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
@@ -16,6 +17,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
{
className,
type,
+ containerClassName,
showPassword,
onShowPasswordChange,
showClearIcon,
@@ -45,7 +47,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
iconCount === 2 ? "pr-20" : iconCount === 1 ? "pr-10" : "";
return (
- <div className={hasIcons ? "relative w-full" : ""}>
+ <div className={cn(hasIcons ? "relative w-full" : "", containerClassName)}>
<input
type={inputType}
className={cn(
diff --git a/apps/web/src/hooks/use-sound-search.ts b/apps/web/src/hooks/use-sound-search.ts
index e815a668..db9a7e8e 100644
--- a/apps/web/src/hooks/use-sound-search.ts
+++ b/apps/web/src/hooks/use-sound-search.ts
@@ -9,7 +9,7 @@ import { useSoundsStore } from "@/stores/sounds-store";
* - Proper error handling
*/
-export function useSoundSearch(query: string) {
+export function useSoundSearch(query: string, commercialOnly: boolean) {
const {
searchResults,
isSearching,
@@ -49,6 +49,7 @@ export function useSoundSearch(query: string) {
searchParams.set("q", query);
}
+ searchParams.set("commercial_only", commercialOnly.toString());
const response = await fetch(
`/api/sounds/search?${searchParams.toString()}`
);
diff --git a/apps/web/src/stores/sounds-store.ts b/apps/web/src/stores/sounds-store.ts
index 3771c07e..215253fb 100644
--- a/apps/web/src/stores/sounds-store.ts
+++ b/apps/web/src/stores/sounds-store.ts
@@ -13,6 +13,10 @@ interface SoundsStore {
error: string | null;
hasLoaded: boolean;
+ // Filter state
+ showCommercialOnly: boolean;
+ toggleCommercialFilter: () => void;
+
// Search state
searchQuery: string;
searchResults: SoundEffect[];
@@ -72,6 +76,11 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({
isLoading: false,
error: null,
hasLoaded: false,
+ showCommercialOnly: true,
+
+ toggleCommercialFilter: () => {
+ set((state) => ({ showCommercialOnly: !state.showCommercialOnly }));
+ },
// Search state
searchQuery: "",