├── public └── sounds │ └── silence.mp3 ├── website ├── src │ ├── index.css │ ├── assets │ │ ├── music.mp3 │ │ └── react.svg │ ├── declarations.d.ts │ ├── components │ │ ├── Footer.tsx │ │ ├── FeatureCard.tsx │ │ ├── CodeBlock.tsx │ │ ├── Header.tsx │ │ ├── SoundSelector.tsx │ │ └── AdvancedSoundDemo.tsx │ ├── utils │ │ └── cn.ts │ ├── main.tsx │ ├── App.css │ ├── App.tsx │ ├── pages │ │ ├── HomePage.tsx │ │ ├── DocumentationPage.tsx │ │ └── SoundLibraryPage.tsx │ └── manifest.json ├── tailwind.config.js ├── tsconfig.node.json ├── .gitignore ├── vite.config.js ├── public │ └── favicon.svg ├── tsconfig.json ├── README.md ├── eslint.config.js ├── package.json └── index.html ├── .DS_Store ├── sounds ├── .DS_Store ├── game │ ├── hit.mp3 │ ├── .DS_Store │ ├── coin.mp3 │ ├── miss.mp3 │ ├── void.mp3 │ ├── portal_closing.mp3 │ └── portal_opening.mp3 ├── ui │ ├── .DS_Store │ ├── buzz.mp3 │ ├── copy.mp3 │ ├── send.mp3 │ ├── blocked.mp3 │ ├── submit.mp3 │ ├── buzz_deep.mp3 │ ├── buzz_long.mp3 │ ├── input_blur.mp3 │ ├── pop_close.mp3 │ ├── pop_open.mp3 │ ├── popup_open.mp3 │ ├── tab_close.mp3 │ ├── tab_open.mp3 │ ├── toggle_off.mp3 │ ├── toggle_on.mp3 │ ├── button_hard.mp3 │ ├── button_soft.mp3 │ ├── input_focus.mp3 │ ├── item_select.mp3 │ ├── panel_expand.mp3 │ ├── popup_close.mp3 │ ├── radio_select.mp3 │ ├── success_blip.mp3 │ ├── window_close.mp3 │ ├── window_open.mp3 │ ├── button_medium.mp3 │ ├── button_squishy.mp3 │ ├── item_deselect.mp3 │ ├── keystroke_hard.mp3 │ ├── keystroke_soft.mp3 │ ├── panel_collapse.mp3 │ ├── success_bling.mp3 │ ├── success_chime.mp3 │ ├── keystroke_medium.mp3 │ ├── button_hard_double.mp3 │ └── button_soft_double.mp3 ├── misc │ ├── .DS_Store │ └── silence.mp3 ├── ambient │ ├── .DS_Store │ ├── rain.mp3 │ ├── wind.mp3 │ ├── campfire.mp3 │ ├── heartbeat.mp3 │ └── water_stream.mp3 ├── arcade │ ├── .DS_Store │ ├── coin.mp3 │ ├── jump.mp3 │ ├── level_up.mp3 │ ├── power_up.mp3 │ ├── upgrade.mp3 │ ├── coin_bling.mp3 │ ├── level_down.mp3 │ └── power_down.mp3 ├── system │ ├── .DS_Store │ ├── lock.mp3 │ ├── trash.mp3 │ ├── boot_up.mp3 │ ├── boot_down.mp3 │ ├── screenshot.mp3 │ ├── device_connect.mp3 │ └── device_disconnect.mp3 └── notification │ ├── .DS_Store │ ├── error.mp3 │ ├── info.mp3 │ ├── popup.mp3 │ ├── message.mp3 │ ├── success.mp3 │ ├── warning.mp3 │ ├── completed.mp3 │ ├── reminder.mp3 │ └── notification.mp3 ├── src ├── utils.ts ├── index.ts ├── types.ts ├── components.tsx ├── hooks.ts ├── manifest.json └── runtime.ts ├── tsconfig.json ├── rollup.config.js ├── LICENSE ├── package.json ├── README.md ├── .gitignore ├── scripts ├── generate-manifest.js ├── generate-types.js └── upload-to-cdn.js └── bin └── react-sounds-cli.js /public/sounds/silence.mp3: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/.DS_Store -------------------------------------------------------------------------------- /sounds/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/.DS_Store -------------------------------------------------------------------------------- /sounds/game/hit.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/game/hit.mp3 -------------------------------------------------------------------------------- /sounds/ui/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/.DS_Store -------------------------------------------------------------------------------- /sounds/ui/buzz.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/buzz.mp3 -------------------------------------------------------------------------------- /sounds/ui/copy.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/copy.mp3 -------------------------------------------------------------------------------- /sounds/ui/send.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/send.mp3 -------------------------------------------------------------------------------- /sounds/game/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/game/.DS_Store -------------------------------------------------------------------------------- /sounds/game/coin.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/game/coin.mp3 -------------------------------------------------------------------------------- /sounds/game/miss.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/game/miss.mp3 -------------------------------------------------------------------------------- /sounds/game/void.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/game/void.mp3 -------------------------------------------------------------------------------- /sounds/misc/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/misc/.DS_Store -------------------------------------------------------------------------------- /sounds/ui/blocked.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/blocked.mp3 -------------------------------------------------------------------------------- /sounds/ui/submit.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/submit.mp3 -------------------------------------------------------------------------------- /sounds/ambient/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ambient/.DS_Store -------------------------------------------------------------------------------- /sounds/ambient/rain.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ambient/rain.mp3 -------------------------------------------------------------------------------- /sounds/ambient/wind.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ambient/wind.mp3 -------------------------------------------------------------------------------- /sounds/arcade/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/arcade/.DS_Store -------------------------------------------------------------------------------- /sounds/arcade/coin.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/arcade/coin.mp3 -------------------------------------------------------------------------------- /sounds/arcade/jump.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/arcade/jump.mp3 -------------------------------------------------------------------------------- /sounds/misc/silence.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/misc/silence.mp3 -------------------------------------------------------------------------------- /sounds/system/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/system/.DS_Store -------------------------------------------------------------------------------- /sounds/system/lock.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/system/lock.mp3 -------------------------------------------------------------------------------- /sounds/system/trash.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/system/trash.mp3 -------------------------------------------------------------------------------- /sounds/ui/buzz_deep.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/buzz_deep.mp3 -------------------------------------------------------------------------------- /sounds/ui/buzz_long.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/buzz_long.mp3 -------------------------------------------------------------------------------- /sounds/ui/input_blur.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/input_blur.mp3 -------------------------------------------------------------------------------- /sounds/ui/pop_close.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/pop_close.mp3 -------------------------------------------------------------------------------- /sounds/ui/pop_open.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/pop_open.mp3 -------------------------------------------------------------------------------- /sounds/ui/popup_open.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/popup_open.mp3 -------------------------------------------------------------------------------- /sounds/ui/tab_close.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/tab_close.mp3 -------------------------------------------------------------------------------- /sounds/ui/tab_open.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/tab_open.mp3 -------------------------------------------------------------------------------- /sounds/ui/toggle_off.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/toggle_off.mp3 -------------------------------------------------------------------------------- /sounds/ui/toggle_on.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/toggle_on.mp3 -------------------------------------------------------------------------------- /sounds/arcade/level_up.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/arcade/level_up.mp3 -------------------------------------------------------------------------------- /sounds/arcade/power_up.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/arcade/power_up.mp3 -------------------------------------------------------------------------------- /sounds/arcade/upgrade.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/arcade/upgrade.mp3 -------------------------------------------------------------------------------- /sounds/system/boot_up.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/system/boot_up.mp3 -------------------------------------------------------------------------------- /sounds/ui/button_hard.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/button_hard.mp3 -------------------------------------------------------------------------------- /sounds/ui/button_soft.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/button_soft.mp3 -------------------------------------------------------------------------------- /sounds/ui/input_focus.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/input_focus.mp3 -------------------------------------------------------------------------------- /sounds/ui/item_select.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/item_select.mp3 -------------------------------------------------------------------------------- /sounds/ui/panel_expand.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/panel_expand.mp3 -------------------------------------------------------------------------------- /sounds/ui/popup_close.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/popup_close.mp3 -------------------------------------------------------------------------------- /sounds/ui/radio_select.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/radio_select.mp3 -------------------------------------------------------------------------------- /sounds/ui/success_blip.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/success_blip.mp3 -------------------------------------------------------------------------------- /sounds/ui/window_close.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/window_close.mp3 -------------------------------------------------------------------------------- /sounds/ui/window_open.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/window_open.mp3 -------------------------------------------------------------------------------- /sounds/ambient/campfire.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ambient/campfire.mp3 -------------------------------------------------------------------------------- /sounds/ambient/heartbeat.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ambient/heartbeat.mp3 -------------------------------------------------------------------------------- /sounds/arcade/coin_bling.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/arcade/coin_bling.mp3 -------------------------------------------------------------------------------- /sounds/arcade/level_down.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/arcade/level_down.mp3 -------------------------------------------------------------------------------- /sounds/arcade/power_down.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/arcade/power_down.mp3 -------------------------------------------------------------------------------- /sounds/notification/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/notification/.DS_Store -------------------------------------------------------------------------------- /sounds/notification/error.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/notification/error.mp3 -------------------------------------------------------------------------------- /sounds/notification/info.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/notification/info.mp3 -------------------------------------------------------------------------------- /sounds/notification/popup.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/notification/popup.mp3 -------------------------------------------------------------------------------- /sounds/system/boot_down.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/system/boot_down.mp3 -------------------------------------------------------------------------------- /sounds/system/screenshot.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/system/screenshot.mp3 -------------------------------------------------------------------------------- /sounds/ui/button_medium.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/button_medium.mp3 -------------------------------------------------------------------------------- /sounds/ui/button_squishy.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/button_squishy.mp3 -------------------------------------------------------------------------------- /sounds/ui/item_deselect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/item_deselect.mp3 -------------------------------------------------------------------------------- /sounds/ui/keystroke_hard.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/keystroke_hard.mp3 -------------------------------------------------------------------------------- /sounds/ui/keystroke_soft.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/keystroke_soft.mp3 -------------------------------------------------------------------------------- /sounds/ui/panel_collapse.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/panel_collapse.mp3 -------------------------------------------------------------------------------- /sounds/ui/success_bling.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/success_bling.mp3 -------------------------------------------------------------------------------- /sounds/ui/success_chime.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/success_chime.mp3 -------------------------------------------------------------------------------- /website/src/assets/music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/website/src/assets/music.mp3 -------------------------------------------------------------------------------- /sounds/ambient/water_stream.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ambient/water_stream.mp3 -------------------------------------------------------------------------------- /sounds/game/portal_closing.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/game/portal_closing.mp3 -------------------------------------------------------------------------------- /sounds/game/portal_opening.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/game/portal_opening.mp3 -------------------------------------------------------------------------------- /sounds/notification/message.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/notification/message.mp3 -------------------------------------------------------------------------------- /sounds/notification/success.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/notification/success.mp3 -------------------------------------------------------------------------------- /sounds/notification/warning.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/notification/warning.mp3 -------------------------------------------------------------------------------- /sounds/ui/keystroke_medium.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/keystroke_medium.mp3 -------------------------------------------------------------------------------- /sounds/notification/completed.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/notification/completed.mp3 -------------------------------------------------------------------------------- /sounds/notification/reminder.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/notification/reminder.mp3 -------------------------------------------------------------------------------- /sounds/system/device_connect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/system/device_connect.mp3 -------------------------------------------------------------------------------- /sounds/ui/button_hard_double.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/button_hard_double.mp3 -------------------------------------------------------------------------------- /sounds/ui/button_soft_double.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/ui/button_soft_double.mp3 -------------------------------------------------------------------------------- /sounds/notification/notification.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/notification/notification.mp3 -------------------------------------------------------------------------------- /sounds/system/device_disconnect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/HEAD/sounds/system/device_disconnect.mp3 -------------------------------------------------------------------------------- /website/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /website/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /website/src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.mp3" { 2 | const src: string; 3 | export default src; 4 | } 5 | 6 | declare module "*.wav" { 7 | const src: string; 8 | export default src; 9 | } 10 | 11 | declare module "*.ogg" { 12 | const src: string; 13 | export default src; 14 | } 15 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /website/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Footer: React.FC = () => { 4 | return ( 5 | 10 | ); 11 | }; 12 | 13 | export default Footer; 14 | -------------------------------------------------------------------------------- /website/src/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | /** 5 | * Combines clsx and tailwind-merge for optimal class merging. 6 | * @param inputs - Class names or conditional class values. 7 | * @returns A single string with merged class names. 8 | */ 9 | export const cn = (...inputs: ClassValue[]): string => { 10 | return twMerge(clsx(inputs)); 11 | }; 12 | -------------------------------------------------------------------------------- /website/vite.config.js: -------------------------------------------------------------------------------- 1 | import tailwindcss from "@tailwindcss/vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import path from "path"; 4 | import { defineConfig } from "vite"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | react(), 10 | tailwindcss({ config: "./tailwind.config.js" }), // Explicitly specify config path 11 | ], 12 | resolve: { 13 | alias: { 14 | "@": path.resolve(__dirname, "./src"), 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /website/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import { BrowserRouter } from "react-router-dom"; 3 | import { SoundProvider } from "react-sounds"; 4 | import App from "./App"; 5 | import "./index.css"; 6 | 7 | const rootElement = document.getElementById("root"); 8 | if (!rootElement) throw new Error("Failed to find the root element"); 9 | 10 | ReactDOM.createRoot(rootElement).render( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns an RFC 4122–compliant UUID v4 3 | * Works in every modern browser and in legacy environments without crypto 4 | */ 5 | export function uuidv4() { 6 | let r = ""; 7 | for (let i = 0; i < 32; i++) r += ((Math.random() * 16) | 0).toString(16); 8 | 9 | return ( 10 | r.slice(0, 8) + 11 | "-" + 12 | r.slice(8, 12) + 13 | "-4" + 14 | r.slice(13, 16) + 15 | "-" + // force version 4 16 | ((parseInt(r[16], 16) & 0x3) | 0x8).toString(16) + // force variant 10 17 | r.slice(17, 20) + 18 | "-" + 19 | r.slice(20) 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /website/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /website/src/components/FeatureCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface FeatureCardProps { 4 | title: string; 5 | description: string; 6 | icon?: string; 7 | } 8 | 9 | const FeatureCard: React.FC = ({ title, description, icon }) => { 10 | return ( 11 |
12 | {icon &&
{icon}
} 13 |

{title}

14 |

{description}

15 |
16 | ); 17 | }; 18 | 19 | export default FeatureCard; 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Export core functionality 2 | export { 3 | fetchSoundBlob, 4 | getCDNUrl, 5 | isSoundEnabled, 6 | makeRemoteSound, 7 | playSound, 8 | preloadSounds, 9 | setCDNUrl, 10 | setSoundEnabled, 11 | } from "./runtime"; 12 | 13 | // Export hooks 14 | export { useSound, useSoundEnabled, useSoundOnChange } from "./hooks"; 15 | 16 | // Export components 17 | export { Sound, SoundButton, SoundProvider } from "./components"; 18 | 19 | // Export types 20 | export type { 21 | GameSoundName, 22 | LibrarySoundName, 23 | NotificationSoundName, 24 | SoundCategory, 25 | SoundHookReturn, 26 | SoundOptions, 27 | UiSoundName, 28 | } from "./types"; 29 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "moduleResolution": "bundler", 9 | "allowImportingTsExtensions": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noEmit": true, 13 | "jsx": "react-jsx", 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "baseUrl": ".", 19 | "paths": { 20 | "@/*": ["src/*"] 21 | } 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "declaration": true, 7 | "sourceMap": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitReturns": true, 13 | "noImplicitThis": true, 14 | "noImplicitAny": true, 15 | "strictNullChecks": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "esModuleInterop": true, 19 | "resolveJsonModule": true, 20 | "jsx": "react", 21 | "outDir": "dist", 22 | "rootDir": "src" 23 | }, 24 | "include": ["src"], 25 | "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] 26 | } 27 | -------------------------------------------------------------------------------- /website/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /website/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Routes } from "react-router-dom"; 3 | import Footer from "./components/Footer"; 4 | import Header from "./components/Header"; 5 | import "./index.css"; 6 | import DocumentationPage from "./pages/DocumentationPage"; 7 | import HomePage from "./pages/HomePage"; 8 | import SoundLibraryPage from "./pages/SoundLibraryPage"; 9 | 10 | const App: React.FC = () => { 11 | return ( 12 |
13 |
14 |
15 | 16 | } /> 17 | } /> 18 | } /> 19 | 20 |
21 |
22 |
23 | ); 24 | }; 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. 13 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from "@rollup/plugin-commonjs"; 2 | import json from "@rollup/plugin-json"; 3 | import resolve from "@rollup/plugin-node-resolve"; 4 | import typescript from "@rollup/plugin-typescript"; 5 | import { terser } from "rollup-plugin-terser"; 6 | 7 | const packageJson = require("./package.json"); 8 | 9 | export default { 10 | input: "src/index.ts", 11 | output: [ 12 | { 13 | file: packageJson.main, 14 | format: "cjs", 15 | sourcemap: true, 16 | exports: "named", 17 | }, 18 | { 19 | file: packageJson.module, 20 | format: "esm", 21 | sourcemap: true, 22 | exports: "named", 23 | }, 24 | ], 25 | plugins: [ 26 | resolve(), 27 | commonjs(), 28 | json(), 29 | typescript({ 30 | tsconfig: "./tsconfig.json", 31 | exclude: ["**/__tests__/**", "**/*.test.ts", "**/*.test.tsx"], 32 | }), 33 | terser(), 34 | ], 35 | external: ["react", "react-dom", "howler"], 36 | }; 37 | -------------------------------------------------------------------------------- /website/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | 6 | export default [ 7 | { ignores: ['dist'] }, 8 | { 9 | files: ['**/*.{js,jsx}'], 10 | languageOptions: { 11 | ecmaVersion: 2020, 12 | globals: globals.browser, 13 | parserOptions: { 14 | ecmaVersion: 'latest', 15 | ecmaFeatures: { jsx: true }, 16 | sourceType: 'module', 17 | }, 18 | }, 19 | plugins: { 20 | 'react-hooks': reactHooks, 21 | 'react-refresh': reactRefresh, 22 | }, 23 | rules: { 24 | ...js.configs.recommended.rules, 25 | ...reactHooks.configs.recommended.rules, 26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 | 'react-refresh/only-export-components': [ 28 | 'warn', 29 | { allowConstantExport: true }, 30 | ], 31 | }, 32 | }, 33 | ] 34 | -------------------------------------------------------------------------------- /website/src/components/CodeBlock.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/utils/cn"; 2 | import Prism from "prismjs"; 3 | import "prismjs/components/prism-bash"; 4 | import "prismjs/components/prism-javascript"; 5 | import "prismjs/components/prism-jsx"; 6 | import "prismjs/components/prism-tsx"; 7 | import "prismjs/components/prism-typescript"; 8 | import "prismjs/themes/prism-tomorrow.css"; 9 | import React, { useEffect } from "react"; 10 | 11 | interface CodeBlockProps { 12 | className?: string; 13 | children: React.ReactNode; 14 | language?: string; 15 | } 16 | 17 | const CodeBlock: React.FC = ({ className, children, language = "tsx" }) => { 18 | const langClass = `language-${language}`; 19 | 20 | useEffect(() => { 21 | Prism.highlightAll(); 22 | }, [children, language]); 23 | 24 | return ( 25 |
26 |       {children}
27 |     
28 | ); 29 | }; 30 | 31 | export default CodeBlock; 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Aedilic Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-sounds-website", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "rm -rf dist/ && npm run sync && vite build", 9 | "start": "serve -s dist", 10 | "sync": "cp ../src/manifest.json ./src/", 11 | "lint": "eslint .", 12 | "preview": "vite preview" 13 | }, 14 | "dependencies": { 15 | "clsx": "^2.1.1", 16 | "howler": "^2.2.4", 17 | "prismjs": "^1.30.0", 18 | "react": "^19.0.0", 19 | "react-dom": "^19.0.0", 20 | "react-router-dom": "^7.5.2", 21 | "react-sounds": "^1.0.25", 22 | "serve": "^14.2.1", 23 | "tailwind-merge": "^3.2.0" 24 | }, 25 | "devDependencies": { 26 | "@eslint/js": "^9.22.0", 27 | "@tailwindcss/vite": "^4.1.4", 28 | "@types/prismjs": "^1.26.5", 29 | "@types/react": "^19.0.10", 30 | "@types/react-dom": "^19.0.4", 31 | "@typescript-eslint/eslint-plugin": "^8.31.1", 32 | "@typescript-eslint/parser": "^8.31.1", 33 | "@typescript-eslint/utils": "^8.31.1", 34 | "@vitejs/plugin-react": "^4.3.4", 35 | "autoprefixer": "^10.4.21", 36 | "eslint": "^9.25.1", 37 | "eslint-plugin-react-hooks": "^5.2.0", 38 | "eslint-plugin-react-refresh": "^0.4.19", 39 | "globals": "^16.0.0", 40 | "picomatch": "^4.0.2", 41 | "postcss": "^8.5.3", 42 | "tailwindcss": "^4.1.4", 43 | "typescript": "^5.3.3", 44 | "vite": "^6.3.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | react-sounds - Sound Effects for React 25 | 26 | 35 | 36 | 37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-sounds", 3 | "version": "1.0.25", 4 | "description": "A library of ready-to-play sound effects for React applications.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.esm.js", 7 | "types": "dist/index.d.ts", 8 | "sideEffects": false, 9 | "scripts": { 10 | "build": "rm -rf dist/ && rollup -c && cp src/manifest.json dist/", 11 | "build:sounds": "npm run generate-manifest && npm run generate-types && npm run upload-to-cdn", 12 | "build:all": "npm run build:sounds && npm run build", 13 | "lint": "eslint src", 14 | "generate-manifest": "node scripts/generate-manifest.js", 15 | "upload-to-cdn": "node scripts/upload-to-cdn.js", 16 | "generate-types": "node scripts/generate-types.js" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "sounds", 21 | "audio", 22 | "sound-effects", 23 | "ui-sounds" 24 | ], 25 | "author": "Lukas Schneider ", 26 | "license": "MIT", 27 | "peerDependencies": { 28 | "howler": "^2.2.3", 29 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 30 | }, 31 | "devDependencies": { 32 | "@aws-sdk/client-s3": "^3.797.0", 33 | "@rollup/plugin-commonjs": "^22.0.0", 34 | "@rollup/plugin-json": "^6.1.0", 35 | "@rollup/plugin-node-resolve": "^13.3.0", 36 | "@rollup/plugin-typescript": "^8.3.2", 37 | "@types/howler": "^2.2.7", 38 | "@types/node": "^18.0.0", 39 | "@types/react": "^18.0.14", 40 | "aws-cli": "^0.0.2", 41 | "dotenv": "^16.5.0", 42 | "eslint": "^8.18.0", 43 | "rollup": "^2.75.6", 44 | "rollup-plugin-terser": "^7.0.2", 45 | "tslib": "^2.8.1", 46 | "typescript": "^4.7.4", 47 | "yargs": "^17.7.2" 48 | }, 49 | "files": [ 50 | "dist", 51 | "bin" 52 | ], 53 | "bin": { 54 | "react-sounds-cli": "./bin/react-sounds-cli.js" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "https://github.com/aediliclabs/react-sounds.git" 59 | }, 60 | "bugs": { 61 | "url": "https://github.com/aediliclabs/react-sounds/issues" 62 | }, 63 | "homepage": "https://reactsounds.com", 64 | "dependencies": { 65 | "howler": "^2.2.3" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-sounds 🔊 2 | 3 |

4 | npm version 5 | License: MIT 6 | PRs Welcome 7 |

8 | 9 |

10 | Hundreds of ready-to-play sound effects for your React applications
11 | Add delight to your UI with just a few lines of code 12 |

13 | 14 |

15 | Demo • 16 | Documentation • 17 | Sound Explorer 18 |

19 | 20 | ## ✨ Why react-sounds? 21 | 22 | - 🪶 **Lightweight**: Only loads JS wrappers, audio files stay on CDN until needed 23 | - 🔄 **Lazy Loading**: Sounds are fetched only when they're used 24 | - 📦 **Offline Support**: Download sounds for self-hosting with the included CLI 25 | - 🎯 **Simple API**: Intuitive hooks and components 26 | - 🔊 **Extensive Library**: Hundreds of categorized sounds (UI, notification, game) 27 | 28 | ## 🚀 Quick Start 29 | 30 | ```bash 31 | npm install react-sounds howler 32 | # or 33 | yarn add react-sounds howler 34 | ``` 35 | 36 | ```tsx 37 | import { useSound } from 'react-sounds'; 38 | 39 | function Button() { 40 | const { play } = useSound('ui/button_1'); 41 | 42 | return ( 43 | 46 | ); 47 | } 48 | ``` 49 | 50 | ## 📚 Documentation 51 | 52 | For complete documentation including advanced usage, visit [reactsounds.com/docs](https://www.reactsounds.com/docs) 53 | 54 | ## 🎮 Live Demo 55 | 56 | Try the interactive demo at [reactsounds.com](https://www.reactsounds.com) 57 | 58 | ## 🔍 Explore All Sounds 59 | 60 | Browse and play all available sounds at [reactsounds.com/sounds](https://www.reactsounds.com/sounds) 61 | 62 | ## 💻 Browser Support 63 | 64 | Works in all modern browsers that support the Web Audio API (Chrome, Firefox, Safari, Edge) 65 | 66 | ## 📄 License 67 | 68 | MIT © Aedilic Inc. 69 | 70 | --- 71 | 72 |

73 | Made with ♥ by Aedilic Inc 74 |

75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | 138 | # custom 139 | .cursor/ 140 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Categories of sounds available in the library 4 | */ 5 | export type SoundCategory = 'ambient' | 'arcade' | 'game' | 'misc' | 'notification' | 'system' | 'ui'; 6 | 7 | /** 8 | * AmbientSoundName sound names 9 | */ 10 | export type AmbientSoundName = 'campfire' | 'heartbeat' | 'rain' | 'water_stream' | 'wind'; 11 | 12 | /** 13 | * ArcadeSoundName sound names 14 | */ 15 | export type ArcadeSoundName = 'coin' | 'coin_bling' | 'jump' | 'level_down' | 'level_up' | 'power_down' | 'power_up' | 'upgrade'; 16 | 17 | /** 18 | * GameSoundName sound names 19 | */ 20 | export type GameSoundName = 'coin' | 'hit' | 'miss' | 'portal_closing' | 'portal_opening' | 'void'; 21 | 22 | /** 23 | * MiscSoundName sound names 24 | */ 25 | export type MiscSoundName = 'silence'; 26 | 27 | /** 28 | * NotificationSoundName sound names 29 | */ 30 | export type NotificationSoundName = 'completed' | 'error' | 'info' | 'message' | 'notification' | 'popup' | 'reminder' | 'success' | 'warning'; 31 | 32 | /** 33 | * SystemSoundName sound names 34 | */ 35 | export type SystemSoundName = 'boot_down' | 'boot_up' | 'device_connect' | 'device_disconnect' | 'lock' | 'screenshot' | 'trash'; 36 | 37 | /** 38 | * UiSoundName sound names 39 | */ 40 | export type UiSoundName = 'blocked' | 'button_hard' | 'button_hard_double' | 'button_medium' | 'button_soft' | 'button_soft_double' | 'button_squishy' | 'buzz' | 'buzz_deep' | 'buzz_long' | 'copy' | 'input_blur' | 'input_focus' | 'item_deselect' | 'item_select' | 'keystroke_hard' | 'keystroke_medium' | 'keystroke_soft' | 'panel_collapse' | 'panel_expand' | 'pop_close' | 'pop_open' | 'popup_close' | 'popup_open' | 'radio_select' | 'send' | 'submit' | 'success_bling' | 'success_blip' | 'success_chime' | 'tab_close' | 'tab_open' | 'toggle_off' | 'toggle_on' | 'window_close' | 'window_open'; 41 | 42 | /** 43 | * All available sound names 44 | */ 45 | export type LibrarySoundName = 46 | | `ambient/${AmbientSoundName}` 47 | | `arcade/${ArcadeSoundName}` 48 | | `game/${GameSoundName}` 49 | | `misc/${MiscSoundName}` 50 | | `notification/${NotificationSoundName}` 51 | | `system/${SystemSoundName}` 52 | | `ui/${UiSoundName}`; 53 | 54 | /** 55 | * Sound options for playback 56 | */ 57 | export interface SoundOptions { 58 | /** 59 | * Volume of the sound (0.0 to 1.0) 60 | */ 61 | volume?: number; 62 | 63 | /** 64 | * Playback rate (1.0 is normal speed) 65 | */ 66 | rate?: number; 67 | 68 | /** 69 | * Sound should loop 70 | */ 71 | loop?: boolean; 72 | } 73 | 74 | /** 75 | * Return type for useSound hook 76 | */ 77 | export interface SoundHookReturn { 78 | /** 79 | * Play the sound with optional options 80 | */ 81 | play: (options?: SoundOptions) => Promise; 82 | 83 | /** 84 | * Stop the sound 85 | */ 86 | stop: () => void; 87 | 88 | /** 89 | * Pause the sound 90 | */ 91 | pause: () => void; 92 | 93 | /** 94 | * Resume the sound 95 | */ 96 | resume: () => void; 97 | 98 | /** 99 | * Check if the sound is currently playing 100 | */ 101 | isPlaying: boolean; 102 | 103 | /** 104 | * Check if the sound is loaded 105 | */ 106 | isLoaded: boolean; 107 | } -------------------------------------------------------------------------------- /website/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { playSound, useSoundEnabled, useSoundOnChange } from "react-sounds"; 4 | 5 | const Header: React.FC = () => { 6 | const [soundIsEnabled, setIsEnabled] = useSoundEnabled(); 7 | 8 | const handleSoundToggle = async () => { 9 | if (soundIsEnabled) await playSound("ui/toggle_off"); 10 | setIsEnabled(!soundIsEnabled); 11 | }; 12 | 13 | useSoundOnChange("ui/toggle_on", soundIsEnabled, { initial: false }); 14 | 15 | return ( 16 |
17 | 66 |
67 | ); 68 | }; 69 | 70 | export default Header; 71 | -------------------------------------------------------------------------------- /scripts/generate-manifest.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script generates a manifest.json file by scanning the sounds directory 5 | * and collecting metadata about each sound file. 6 | */ 7 | 8 | const fs = require("fs"); 9 | const path = require("path"); 10 | const crypto = require("crypto"); 11 | const { execSync } = require("child_process"); 12 | 13 | // Configuration 14 | const SOUNDS_DIR = path.resolve(__dirname, "../sounds"); 15 | const OUTPUT_DIR = path.resolve(__dirname, "../src"); 16 | const MANIFEST_FILE = path.join(OUTPUT_DIR, "manifest.json"); 17 | 18 | // Create output directory if it doesn't exist 19 | if (!fs.existsSync(OUTPUT_DIR)) { 20 | fs.mkdirSync(OUTPUT_DIR, { recursive: true }); 21 | } 22 | 23 | // Generate a hash for a file 24 | function generateFileHash(filePath) { 25 | const fileBuffer = fs.readFileSync(filePath); 26 | const hashSum = crypto.createHash("md5"); 27 | hashSum.update(fileBuffer); 28 | return hashSum.digest("hex").substring(0, 7); 29 | } 30 | 31 | // Get all valid category directories 32 | function getCategories() { 33 | const items = fs.readdirSync(SOUNDS_DIR, { withFileTypes: true }); 34 | return items.filter((item) => item.isDirectory() && item.name !== ".DS_Store").map((item) => item.name); 35 | } 36 | 37 | // Main function 38 | async function generateManifest() { 39 | console.log("Generating manifest..."); 40 | 41 | const manifest = { 42 | version: "1.0.0", 43 | sounds: {}, 44 | }; 45 | 46 | // Get all categories from the sounds directory 47 | const categories = getCategories(); 48 | console.log(`Found categories: ${categories.join(", ")}`); 49 | 50 | // Iterate through all categories and sound files 51 | for (const category of categories) { 52 | const categoryPath = path.join(SOUNDS_DIR, category); 53 | 54 | const files = fs.readdirSync(categoryPath); 55 | 56 | for (const file of files) { 57 | // Only process MP3 files 58 | if (!file.endsWith(".mp3")) continue; 59 | 60 | const filePath = path.join(categoryPath, file); 61 | const soundName = file.replace(".mp3", ""); 62 | const soundId = `${category}/${soundName}`; 63 | 64 | // Generate hash for the file 65 | const hash = generateFileHash(filePath); 66 | 67 | // Get file metadata (duration, etc.) 68 | let duration = 0; 69 | 70 | try { 71 | // Try to get audio duration using ffprobe if available 72 | const result = execSync(`ffprobe -i "${filePath}" -show_entries format=duration -v quiet -of csv="p=0"`, { 73 | encoding: "utf8", 74 | }); 75 | duration = parseFloat(result.trim()); 76 | } catch (e) { 77 | console.warn(`Could not get duration for ${filePath}. Setting default duration.`); 78 | // Set a default duration based on file size 79 | const stats = fs.statSync(filePath); 80 | duration = Math.round((stats.size / 16000) * 10) / 10; // Rough estimate 81 | } 82 | 83 | // Add to manifest 84 | manifest.sounds[soundId] = { 85 | src: `${category}/${soundName}.${hash}.mp3`, 86 | duration: duration, 87 | }; 88 | 89 | console.log(`Added ${soundId} to manifest`); 90 | } 91 | } 92 | 93 | // Save manifest 94 | fs.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2)); 95 | console.log(`Manifest saved to ${MANIFEST_FILE}`); 96 | } 97 | 98 | // Run the script 99 | generateManifest().catch((err) => { 100 | console.error("Error generating manifest:", err); 101 | process.exit(1); 102 | }); 103 | -------------------------------------------------------------------------------- /scripts/generate-types.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script generates TypeScript type definitions for sound names 5 | * based on the manifest.json file. 6 | */ 7 | 8 | const fs = require("fs"); 9 | const path = require("path"); 10 | 11 | // Configuration 12 | const MANIFEST_FILE = path.resolve(__dirname, "../src/manifest.json"); 13 | const TYPES_FILE = path.resolve(__dirname, "../src/types.ts"); 14 | 15 | // Main function 16 | async function generateTypes() { 17 | console.log("Generating TypeScript types..."); 18 | 19 | // Load manifest 20 | if (!fs.existsSync(MANIFEST_FILE)) { 21 | console.error(`Manifest file not found: ${MANIFEST_FILE}`); 22 | process.exit(1); 23 | } 24 | 25 | const manifest = JSON.parse(fs.readFileSync(MANIFEST_FILE, "utf8")); 26 | 27 | // Group sounds by category 28 | const soundsByCategory = {}; 29 | 30 | for (const soundId in manifest.sounds) { 31 | const [category, name] = soundId.split("/"); 32 | 33 | if (!soundsByCategory[category]) { 34 | soundsByCategory[category] = []; 35 | } 36 | 37 | soundsByCategory[category].push(name); 38 | } 39 | 40 | // Generate TypeScript content 41 | let content = ` 42 | /** 43 | * Categories of sounds available in the library 44 | */ 45 | export type SoundCategory = ${Object.keys(soundsByCategory) 46 | .map((cat) => `'${cat}'`) 47 | .join(" | ")}; 48 | 49 | `; 50 | 51 | // Generate type definitions for each category 52 | for (const category in soundsByCategory) { 53 | const soundNames = soundsByCategory[category]; 54 | const categoryTypeName = `${category.charAt(0).toUpperCase()}${category.slice(1)}SoundName`; 55 | 56 | content += `/** 57 | * ${categoryTypeName} sound names 58 | */ 59 | export type ${categoryTypeName} = ${soundNames.map((name) => `'${name}'`).join(" | ")}; 60 | 61 | `; 62 | } 63 | 64 | // Generate SoundName type 65 | content += `/** 66 | * All available sound names 67 | */ 68 | export type LibrarySoundName = 69 | ${Object.entries(soundsByCategory) 70 | .map(([category, _]) => { 71 | const categoryTypeName = `${category.charAt(0).toUpperCase()}${category.slice(1)}SoundName`; 72 | return `| \`${category}/\${${categoryTypeName}}\``; 73 | }) 74 | .join("\n ")}; 75 | 76 | /** 77 | * Sound options for playback 78 | */ 79 | export interface SoundOptions { 80 | /** 81 | * Volume of the sound (0.0 to 1.0) 82 | */ 83 | volume?: number; 84 | 85 | /** 86 | * Playback rate (1.0 is normal speed) 87 | */ 88 | rate?: number; 89 | 90 | /** 91 | * Sound should loop 92 | */ 93 | loop?: boolean; 94 | } 95 | 96 | /** 97 | * Return type for useSound hook 98 | */ 99 | export interface SoundHookReturn { 100 | /** 101 | * Play the sound with optional options 102 | */ 103 | play: (options?: SoundOptions) => Promise; 104 | 105 | /** 106 | * Stop the sound 107 | */ 108 | stop: () => void; 109 | 110 | /** 111 | * Pause the sound 112 | */ 113 | pause: () => void; 114 | 115 | /** 116 | * Resume the sound 117 | */ 118 | resume: () => void; 119 | 120 | /** 121 | * Check if the sound is currently playing 122 | */ 123 | isPlaying: boolean; 124 | 125 | /** 126 | * Check if the sound is loaded 127 | */ 128 | isLoaded: boolean; 129 | }`; 130 | 131 | // Save the file 132 | fs.writeFileSync(TYPES_FILE, content); 133 | console.log(`TypeScript types saved to ${TYPES_FILE}`); 134 | } 135 | 136 | // Run the script 137 | generateTypes().catch((err) => { 138 | console.error("Error generating TypeScript types:", err); 139 | process.exit(1); 140 | }); 141 | -------------------------------------------------------------------------------- /website/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/src/components/SoundSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from "react"; 2 | import { LibrarySoundName } from "react-sounds"; 3 | import manifest from "../manifest.json"; 4 | 5 | interface SoundSelectorProps { 6 | onSelect: (sound: LibrarySoundName) => void; 7 | value?: LibrarySoundName; 8 | } 9 | 10 | const SoundSelector: React.FC = ({ onSelect, value }) => { 11 | const [searchTerm, setSearchTerm] = useState(value ?? ""); 12 | const [isOpen, setIsOpen] = useState(false); 13 | 14 | // Group sounds by category for better organization 15 | const soundGroups = useMemo(() => { 16 | const groups: Record = {}; 17 | 18 | // Extract sounds from manifest and group by category 19 | Object.keys(manifest.sounds).forEach((soundKey) => { 20 | const [category, name] = soundKey.split("/"); 21 | const categoryName = category.charAt(0).toUpperCase() + category.slice(1) + " Sounds"; 22 | 23 | if (!groups[categoryName]) { 24 | groups[categoryName] = []; 25 | } 26 | groups[categoryName].push(soundKey as LibrarySoundName); 27 | }); 28 | 29 | return groups; 30 | }, []); 31 | 32 | // Filter sounds based on search term 33 | const filteredSounds = useMemo(() => { 34 | if (!searchTerm) return soundGroups; 35 | 36 | const filtered: Record = {}; 37 | Object.entries(soundGroups).forEach(([category, sounds]) => { 38 | const matchingSounds = sounds.filter((sound) => sound.toLowerCase().includes(searchTerm.toLowerCase())); 39 | if (matchingSounds.length > 0) { 40 | filtered[category] = matchingSounds; 41 | } 42 | }); 43 | return filtered; 44 | }, [searchTerm, soundGroups]); 45 | 46 | const handleSelect = (sound: LibrarySoundName) => { 47 | onSelect(sound); 48 | setSearchTerm(sound); 49 | setIsOpen(false); 50 | }; 51 | 52 | // Close dropdown when clicking outside 53 | useEffect(() => { 54 | const handleClickOutside = (event: MouseEvent) => { 55 | const target = event.target as HTMLElement; 56 | if (!target.closest(".sound-selector")) { 57 | setIsOpen(false); 58 | } 59 | }; 60 | 61 | document.addEventListener("mousedown", handleClickOutside); 62 | return () => document.removeEventListener("mousedown", handleClickOutside); 63 | }, []); 64 | 65 | return ( 66 |
67 |
68 | { 72 | setSearchTerm(e.target.value); 73 | setIsOpen(true); 74 | }} 75 | onFocus={() => setIsOpen(true)} 76 | placeholder="Search for a sound..." 77 | className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition duration-150 ease-in-out" 78 | /> 79 |
80 | 86 | 87 | 88 |
89 |
90 | 91 | {isOpen && ( 92 |
93 | {Object.entries(filteredSounds).map(([category, sounds]) => ( 94 |
95 |
{category}
96 | {sounds.map((sound) => ( 97 | 104 | ))} 105 |
106 | ))} 107 | {Object.keys(filteredSounds).length === 0 && ( 108 |
No sounds found
109 | )} 110 |
111 | )} 112 |
113 | ); 114 | }; 115 | 116 | export default SoundSelector; 117 | -------------------------------------------------------------------------------- /bin/react-sounds-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const https = require("https"); 6 | 7 | // Load the manifest 8 | const manifestPath = path.join(__dirname, "../dist/manifest.json"); 9 | let manifest; 10 | 11 | try { 12 | manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); 13 | } catch (err) { 14 | console.error("Error loading manifest:", err.message); 15 | process.exit(1); 16 | } 17 | 18 | // Get the CDN base URL 19 | const cdnBaseUrl = process.env.REACT_SOUNDS_CDN || "https://reacticons.sfo3.cdn.digitaloceanspaces.com/v1"; 20 | 21 | // Parse command line arguments 22 | const args = process.argv.slice(2); 23 | let command = args[0]; 24 | let soundNames = []; 25 | let outputDir = "./public/sounds"; 26 | 27 | if (command === "pick") { 28 | soundNames = args.slice(1).filter((arg) => !arg.startsWith("--")); 29 | 30 | // Check for output directory option 31 | const outputOption = args.find((arg) => arg.startsWith("--output=")); 32 | if (outputOption) { 33 | outputDir = outputOption.split("=")[1]; 34 | } else { 35 | const outputIndex = args.indexOf("--output"); 36 | if (outputIndex !== -1 && args[outputIndex + 1]) { 37 | outputDir = args[outputIndex + 1]; 38 | } 39 | } 40 | 41 | if (soundNames.length === 0) { 42 | console.error("Please specify at least one sound to pick."); 43 | console.log("Usage: npx react-sounds-cli pick [--output=]"); 44 | process.exit(1); 45 | } 46 | 47 | // Create output directory 48 | try { 49 | fs.mkdirSync(outputDir, { recursive: true }); 50 | 51 | // Create category subdirectories 52 | const categories = ["ui", "notification", "game"]; 53 | for (const category of categories) { 54 | fs.mkdirSync(path.join(outputDir, category), { recursive: true }); 55 | } 56 | } catch (err) { 57 | console.error(`Error creating directory ${outputDir}:`, err.message); 58 | process.exit(1); 59 | } 60 | 61 | console.log(`\n📦 Downloading ${soundNames.length} sounds to ${outputDir}...`); 62 | 63 | // Download each sound 64 | let successCount = 0; 65 | let failCount = 0; 66 | 67 | const downloadPromises = soundNames.map((name) => { 68 | return new Promise((resolve) => { 69 | // Check if sound exists in manifest 70 | if (!manifest.sounds[name]) { 71 | console.error(`❌ Sound "${name}" not found in manifest.`); 72 | failCount++; 73 | resolve(); 74 | return; 75 | } 76 | 77 | const soundInfo = manifest.sounds[name]; 78 | const cdnPath = soundInfo.src; 79 | const targetPath = path.join(outputDir, name + ".mp3"); 80 | 81 | // Ensure target directory exists 82 | const targetDir = path.dirname(targetPath); 83 | fs.mkdirSync(targetDir, { recursive: true }); 84 | 85 | // Download the file 86 | const file = fs.createWriteStream(targetPath); 87 | const url = `${cdnBaseUrl}/${cdnPath}`; 88 | 89 | https 90 | .get(url, (response) => { 91 | if (response.statusCode !== 200) { 92 | console.error(`❌ Failed to download "${name}": HTTP ${response.statusCode}`); 93 | failCount++; 94 | resolve(); 95 | return; 96 | } 97 | 98 | response.pipe(file); 99 | 100 | file.on("finish", () => { 101 | file.close(); 102 | console.log(`✅ Downloaded: ${name}`); 103 | successCount++; 104 | resolve(); 105 | }); 106 | }) 107 | .on("error", (err) => { 108 | fs.unlink(targetPath, () => {}); // Clean up on error 109 | console.error(`❌ Failed to download "${name}":`, err.message); 110 | failCount++; 111 | resolve(); 112 | }); 113 | }); 114 | }); 115 | 116 | Promise.all(downloadPromises).then(() => { 117 | console.log("\n📊 Summary:"); 118 | console.log(`✅ ${successCount} sounds downloaded successfully`); 119 | 120 | if (failCount > 0) { 121 | console.log(`❌ ${failCount} sounds failed to download`); 122 | } 123 | 124 | console.log("\n🔧 Sounds are now available for offline use!"); 125 | console.log("To use them in your project, set the CDN base URL:"); 126 | console.log("\nimport { setCDNUrl } from 'react-sounds';"); 127 | console.log(`setCDNUrl('${path.relative(process.cwd(), outputDir)}');\n`); 128 | }); 129 | } else if (command === "list") { 130 | // List all available sounds 131 | console.log("\n🔊 Available sounds:\n"); 132 | 133 | const categories = {}; 134 | 135 | // Group sounds by category 136 | for (const name in manifest.sounds) { 137 | const category = name.split("/")[0]; 138 | if (!categories[category]) { 139 | categories[category] = []; 140 | } 141 | categories[category].push(name); 142 | } 143 | 144 | // Print grouped sounds 145 | for (const category in categories) { 146 | console.log(`📁 ${category}:`); 147 | for (const name of categories[category]) { 148 | console.log(` - ${name}`); 149 | } 150 | console.log(""); 151 | } 152 | } else { 153 | // Show help 154 | console.log("\n🔊 react-sounds-cli"); 155 | console.log("A CLI for managing sounds in the react-sounds library.\n"); 156 | console.log("Commands:"); 157 | console.log(" pick [--output=] Download sounds for offline use"); 158 | console.log(" list List all available sounds\n"); 159 | console.log("Examples:"); 160 | console.log(" npx react-sounds-cli pick ui/click ui/hover notification/success"); 161 | console.log(" npx react-sounds-cli pick ui/click --output=./public/sounds"); 162 | console.log(" npx react-sounds-cli list\n"); 163 | } 164 | -------------------------------------------------------------------------------- /scripts/upload-to-cdn.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script uploads sound files to DigitalOcean Spaces 5 | * using the manifest to determine filenames. 6 | * 7 | * Environment variables: 8 | * - DO_SPACES_KEY: DigitalOcean Spaces access key 9 | * - DO_SPACES_SECRET: DigitalOcean Spaces secret key 10 | * - DO_SPACES_ENDPOINT: DigitalOcean Spaces endpoint (e.g., nyc3.digitaloceanspaces.com) 11 | * - DO_SPACES_NAME: DigitalOcean Space name 12 | * - CDN_PATH: Optional path prefix (defaults to "v1") 13 | * 14 | * Command line arguments: 15 | * --delete: Delete existing files before uploading 16 | */ 17 | 18 | require("dotenv").config(); 19 | 20 | const fs = require("fs"); 21 | const path = require("path"); 22 | const yargs = require("yargs/yargs"); 23 | const { hideBin } = require("yargs/helpers"); 24 | const { S3Client, PutObjectCommand, ListObjectsV2Command, DeleteObjectCommand } = require("@aws-sdk/client-s3"); 25 | 26 | // Parse command line arguments 27 | const argv = yargs(hideBin(process.argv)) 28 | .option("delete", { 29 | alias: "d", 30 | type: "boolean", 31 | description: "Delete existing files before uploading", 32 | default: false, 33 | }) 34 | .help().argv; 35 | 36 | // Configuration 37 | const SOUNDS_DIR = path.resolve(__dirname, "../sounds"); 38 | const MANIFEST_FILE = path.resolve(__dirname, "../src/manifest.json"); 39 | const CDN_PATH = process.env.CDN_PATH || "v1"; 40 | 41 | // Check if manifest exists 42 | if (!fs.existsSync(MANIFEST_FILE)) { 43 | console.error(`Manifest file not found: ${MANIFEST_FILE}`); 44 | console.error(`Please run 'npm run generate-manifest' first.`); 45 | process.exit(1); 46 | } 47 | 48 | // Check if required environment variables are set 49 | const requiredEnvVars = ["DO_SPACES_KEY", "DO_SPACES_SECRET", "DO_SPACES_ENDPOINT", "DO_SPACES_NAME"]; 50 | const missingEnvVars = requiredEnvVars.filter((envVar) => !process.env[envVar]); 51 | 52 | if (missingEnvVars.length > 0) { 53 | console.error(`Missing required environment variables: ${missingEnvVars.join(", ")}`); 54 | process.exit(1); 55 | } 56 | 57 | // Create an S3 client for DigitalOcean Spaces 58 | const s3Client = new S3Client({ 59 | credentials: { 60 | accessKeyId: process.env.DO_SPACES_KEY, 61 | secretAccessKey: process.env.DO_SPACES_SECRET, 62 | }, 63 | endpoint: `https://${process.env.DO_SPACES_ENDPOINT}`, 64 | region: "us-east-1", // This is required but not used for DO Spaces 65 | forcePathStyle: false, 66 | }); 67 | 68 | // Main function 69 | async function uploadToSpaces() { 70 | const spaceName = process.env.DO_SPACES_NAME; 71 | 72 | console.log(`Uploading sound files to DigitalOcean Spaces (${spaceName})...`); 73 | 74 | // Clear existing files in the CDN directory if --delete flag is provided 75 | if (argv.delete) { 76 | console.log(`Clearing existing files in ${CDN_PATH}...`); 77 | await clearDirectory(spaceName, CDN_PATH); 78 | } else { 79 | console.log(`Skipping file deletion (use --delete flag to clear existing files)`); 80 | } 81 | 82 | // Load manifest 83 | const manifest = JSON.parse(fs.readFileSync(MANIFEST_FILE, "utf8")); 84 | 85 | // Count for statistics 86 | let uploadCount = 0; 87 | let errorCount = 0; 88 | 89 | // Process each sound in the manifest 90 | for (const [soundId, soundInfo] of Object.entries(manifest.sounds)) { 91 | const [category, name] = soundId.split("/"); 92 | const sourceFile = path.join(SOUNDS_DIR, category, `${name}.mp3`); 93 | const cdnFilename = soundInfo.src; 94 | 95 | // Check if source file exists 96 | if (!fs.existsSync(sourceFile)) { 97 | console.error(`Source file not found: ${sourceFile}`); 98 | errorCount++; 99 | continue; 100 | } 101 | 102 | // Upload to DigitalOcean Spaces 103 | try { 104 | const spaceKey = `${CDN_PATH}/${cdnFilename}`; 105 | const fileContent = fs.readFileSync(sourceFile); 106 | 107 | const uploadParams = { 108 | Bucket: spaceName, 109 | Key: spaceKey, 110 | Body: fileContent, 111 | ContentType: "audio/mpeg", 112 | ACL: "public-read", // Make the file publicly accessible 113 | }; 114 | 115 | await s3Client.send(new PutObjectCommand(uploadParams)); 116 | 117 | console.log(`✅ Uploaded: ${soundId} -> ${cdnFilename}`); 118 | uploadCount++; 119 | } catch (error) { 120 | console.error(`❌ Failed to upload ${soundId}: ${error.message}`); 121 | errorCount++; 122 | } 123 | } 124 | 125 | console.log("\n📊 Upload Summary:"); 126 | console.log(`✅ ${uploadCount} sounds uploaded successfully`); 127 | 128 | if (errorCount > 0) { 129 | console.log(`❌ ${errorCount} sounds failed to upload`); 130 | process.exit(1); 131 | } 132 | } 133 | 134 | // Function to clear all objects in a directory 135 | async function clearDirectory(bucketName, directoryPath) { 136 | try { 137 | // List all objects in the directory 138 | const listParams = { 139 | Bucket: bucketName, 140 | Prefix: directoryPath, 141 | }; 142 | 143 | const listCommand = new ListObjectsV2Command(listParams); 144 | const listedObjects = await s3Client.send(listCommand); 145 | 146 | if (!listedObjects.Contents || listedObjects.Contents.length === 0) { 147 | console.log(`No existing objects found in ${directoryPath}`); 148 | return; 149 | } 150 | 151 | console.log(`Found ${listedObjects.Contents.length} objects to delete...`); 152 | 153 | // Delete each object 154 | let deletedCount = 0; 155 | for (const object of listedObjects.Contents) { 156 | const deleteParams = { 157 | Bucket: bucketName, 158 | Key: object.Key, 159 | }; 160 | 161 | await s3Client.send(new DeleteObjectCommand(deleteParams)); 162 | deletedCount++; 163 | 164 | if (deletedCount % 10 === 0 || deletedCount === listedObjects.Contents.length) { 165 | console.log(`Deleted ${deletedCount}/${listedObjects.Contents.length} objects...`); 166 | } 167 | } 168 | 169 | console.log(`Successfully cleared ${deletedCount} objects from ${directoryPath}`); 170 | } catch (error) { 171 | console.error(`Error clearing directory ${directoryPath}:`, error); 172 | throw error; 173 | } 174 | } 175 | 176 | // Run the script 177 | uploadToSpaces().catch((err) => { 178 | console.error("Error uploading to DigitalOcean Spaces:", err); 179 | process.exit(1); 180 | }); 181 | -------------------------------------------------------------------------------- /src/components.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactNode, useCallback, useEffect, useState } from "react"; 2 | import { setSoundContext, useSound } from "./hooks"; 3 | import { 4 | isSoundEnabled, 5 | playSound, 6 | preloadSounds, 7 | setSoundEnabled, 8 | SoundName, 9 | subscribeSoundState, 10 | unlockAudioContext, 11 | } from "./runtime"; 12 | import { SoundOptions } from "./types"; 13 | 14 | /** 15 | * Props for the Sound component 16 | */ 17 | interface SoundProps { 18 | /** 19 | * The sound to play (will use local bundled sounds if available) 20 | */ 21 | name: SoundName; 22 | 23 | /** 24 | * When to play the sound 25 | */ 26 | trigger?: "mount" | "unmount" | "none"; 27 | 28 | /** 29 | * Sound playback options 30 | */ 31 | options?: SoundOptions; 32 | 33 | /** 34 | * Children components 35 | */ 36 | children?: ReactNode; 37 | 38 | /** 39 | * Event handler for when the sound is loaded 40 | */ 41 | onLoad?: () => void; 42 | 43 | /** 44 | * Event handler for when the sound is played 45 | */ 46 | onPlay?: () => void; 47 | 48 | /** 49 | * Event handler for when the sound is stopped 50 | */ 51 | onStop?: () => void; 52 | 53 | /** 54 | * Event handler for when the sound fails to play 55 | */ 56 | onError?: (error: Error) => void; 57 | } 58 | 59 | /** 60 | * A component for playing a sound. 61 | * Will use locally downloaded sounds if available before falling back to CDN. 62 | */ 63 | export function Sound({ 64 | name, 65 | trigger = "none", 66 | options, 67 | children, 68 | onLoad, 69 | onPlay, 70 | onStop, 71 | onError, 72 | }: SoundProps): React.ReactElement { 73 | const { play, stop, isLoaded, isPlaying } = useSound(name, options); 74 | 75 | useEffect(() => { 76 | if (isLoaded && onLoad) { 77 | onLoad(); 78 | } 79 | }, [isLoaded, onLoad]); 80 | 81 | useEffect(() => { 82 | if (isPlaying && onPlay) { 83 | onPlay(); 84 | } 85 | }, [isPlaying, onPlay]); 86 | 87 | useEffect(() => { 88 | if (trigger === "mount") { 89 | play(options).catch((error) => { 90 | if (onError) onError(error); 91 | }); 92 | } 93 | 94 | return () => { 95 | if (trigger === "unmount") playSound(name, options); 96 | 97 | stop(); 98 | if (onStop) onStop(); 99 | }; 100 | }, [trigger, name, play, stop, onStop, options, onError]); 101 | 102 | return <>{children}; 103 | } 104 | 105 | /** 106 | * Props for the SoundButton component 107 | */ 108 | interface SoundButtonProps extends React.ButtonHTMLAttributes { 109 | /** 110 | * The sound to play when clicked (will use local bundled sounds if available) 111 | */ 112 | sound: SoundName; 113 | 114 | /** 115 | * Sound playback options 116 | */ 117 | soundOptions?: SoundOptions; 118 | 119 | /** 120 | * Children components 121 | */ 122 | children?: ReactNode; 123 | 124 | /** 125 | * Event handler for when the sound fails to play 126 | */ 127 | onSoundError?: (error: Error) => void; 128 | } 129 | 130 | /** 131 | * A button that plays a sound when clicked. 132 | * Will use locally downloaded sounds if available before falling back to CDN. 133 | */ 134 | export function SoundButton({ 135 | sound, 136 | soundOptions, 137 | children, 138 | onClick, 139 | onSoundError, 140 | ...props 141 | }: SoundButtonProps): React.ReactElement { 142 | const { play } = useSound(sound, soundOptions); 143 | 144 | const handleClick = (e: React.MouseEvent) => { 145 | play().catch((error) => { 146 | if (onSoundError) onSoundError(error); 147 | }); 148 | 149 | if (onClick) onClick(e); 150 | }; 151 | 152 | return ( 153 | 156 | ); 157 | } 158 | 159 | /** 160 | * Sound context for managing sound state 161 | */ 162 | interface SoundContextType { 163 | enabled: boolean; 164 | setEnabled: (enabled: boolean) => void; 165 | } 166 | 167 | export const SoundContext = createContext(null); 168 | 169 | // Register the context with hooks 170 | setSoundContext(SoundContext); 171 | 172 | /** 173 | * Props for the SoundProvider component 174 | */ 175 | interface SoundProviderProps { 176 | /** 177 | * Sounds to preload (will use local bundled sounds if available) 178 | */ 179 | preload?: SoundName[]; 180 | 181 | /** 182 | * Initial sound enabled state (uses localStorage if not provided) 183 | */ 184 | initialEnabled?: boolean; 185 | 186 | /** 187 | * How often to clean up unused sounds (in ms), set to 0 to disable 188 | */ 189 | cleanupInterval?: number; 190 | 191 | /** 192 | * Children components 193 | */ 194 | children: ReactNode; 195 | } 196 | 197 | /** 198 | * A provider that manages sound state and preloads sounds. 199 | * Will use locally downloaded sounds if available before falling back to CDN. 200 | */ 201 | export function SoundProvider({ preload = [], initialEnabled, children }: SoundProviderProps): React.ReactElement { 202 | // Initialize sound enabled state from props or runtime 203 | const [enabled, setEnabledState] = useState(() => { 204 | if (initialEnabled !== undefined) return initialEnabled; 205 | return isSoundEnabled(); 206 | }); 207 | 208 | // Update global state when React state changes 209 | const setEnabled = useCallback((newEnabled: boolean) => { 210 | setSoundEnabled(newEnabled); 211 | }, []); 212 | 213 | // Sync with global state changes from outside React 214 | useEffect(() => { 215 | return subscribeSoundState((newEnabled) => { 216 | setEnabledState(newEnabled); 217 | }); 218 | }, []); 219 | 220 | // Ensure audio context is unlocked when component mounts 221 | useEffect(() => { 222 | unlockAudioContext(); 223 | }, []); 224 | 225 | // Preload sounds when the component mounts 226 | useEffect(() => { 227 | if (preload.length > 0) { 228 | // Start preloading immediately but don't block rendering 229 | const preloadPromise = preloadSounds(preload); 230 | 231 | // Log any preloading errors but don't break the app 232 | preloadPromise.catch((error) => { 233 | console.error("Error preloading sounds:", error); 234 | }); 235 | } 236 | }, [preload]); 237 | 238 | return {children}; 239 | } 240 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { Howl } from "howler"; 2 | import { useCallback, useContext, useEffect, useRef, useState } from "react"; 3 | import { claimSound, freeSound, isSoundEnabled, preloadSounds, SoundName, unlockAudioContext } from "./runtime"; 4 | import { SoundHookReturn, SoundOptions } from "./types"; 5 | 6 | interface SoundContextType { 7 | enabled: boolean; 8 | setEnabled: (enabled: boolean) => void; 9 | } 10 | 11 | // We'll get the actual context from components.tsx when using it 12 | type SoundContextValue = React.Context; 13 | 14 | // This will be set by SoundProvider from components.tsx 15 | let SoundContext: SoundContextValue; 16 | 17 | // Function to set the context from components.tsx 18 | export function setSoundContext(context: SoundContextValue): void { 19 | SoundContext = context; 20 | } 21 | 22 | /** 23 | * Hook for using a sound in a React component. 24 | * Will use local bundled sounds if available before falling back to remote. 25 | */ 26 | export function useSound(soundName: SoundName, defaultOptions: SoundOptions = {}): SoundHookReturn { 27 | const [isLoaded, setIsLoaded] = useState(false); 28 | const [isPlaying, setIsPlaying] = useState(false); 29 | const soundRef = useRef(null); 30 | const activeSoundsRef = useRef void }>>([]); 31 | 32 | // Get sound enabled state from context if available, fall back to global state 33 | let enabled = isSoundEnabled(); 34 | try { 35 | const soundContext = SoundContext ? useContext(SoundContext) : null; 36 | if (soundContext) enabled = soundContext.enabled; 37 | } catch (e) {} 38 | 39 | // Lazy loading approach - only load the sound when needed 40 | const ensureLoaded = useCallback(async (): Promise => { 41 | if (soundRef.current) return soundRef.current; 42 | 43 | try { 44 | const howl = await claimSound(soundName); 45 | 46 | // Set up event listeners 47 | howl.on("end", (id) => { 48 | const soundIndex = activeSoundsRef.current.findIndex((sound) => sound.id === id); 49 | if (soundIndex >= 0) { 50 | const sound = activeSoundsRef.current[soundIndex]; 51 | 52 | if (sound.resolver) sound.resolver(); // Resolve sound (eg. created in play()) 53 | if (!sound.loop) activeSoundsRef.current.splice(soundIndex, 1); 54 | } 55 | 56 | // Only update isPlaying state if no active sounds remain 57 | if (activeSoundsRef.current.length === 0) { 58 | soundRef.current = freeSound(soundName); 59 | setIsPlaying(false); 60 | } 61 | }); 62 | 63 | soundRef.current = howl; 64 | setIsLoaded(true); 65 | return howl; 66 | } catch (error) { 67 | console.error("Error loading sound:", error); 68 | throw error; 69 | } 70 | }, [soundName, setIsPlaying]); 71 | 72 | const play = useCallback( 73 | async (options: SoundOptions = defaultOptions) => { 74 | if (!enabled) return; 75 | 76 | try { 77 | // Ensure audio context is unlocked before playing 78 | await unlockAudioContext(); 79 | 80 | // Ensure the sound is loaded 81 | const howl = await ensureLoaded(); 82 | 83 | const loop = options.loop !== undefined ? options.loop : false; 84 | if (options.volume !== undefined) howl.volume(options.volume); 85 | if (options.rate !== undefined) howl.rate(options.rate); 86 | howl.loop(loop); 87 | 88 | const id = howl.play(); 89 | setIsPlaying(true); 90 | 91 | if (loop) { 92 | // For looped sounds, we just track them but don't resolve 93 | activeSoundsRef.current.push({ id, loop }); 94 | return; 95 | } 96 | 97 | // For non-looped sounds, return a promise that resolves when the sound ends 98 | return new Promise((resolve) => { 99 | activeSoundsRef.current.push({ id, loop, resolver: () => resolve() }); 100 | }); 101 | } catch (error) { 102 | console.error("Error playing sound:", error); 103 | throw error; 104 | } 105 | }, 106 | // isLoaded is a required dep for handling changed sound name 107 | [defaultOptions, enabled, ensureLoaded, isLoaded] 108 | ); 109 | 110 | const stop = useCallback(() => { 111 | if (!soundRef.current) return; 112 | 113 | // Resolve any pending promises 114 | activeSoundsRef.current.forEach((sound) => { 115 | if (sound.resolver) sound.resolver(); 116 | }); 117 | 118 | soundRef.current.stop(); 119 | soundRef.current = freeSound(soundName); 120 | activeSoundsRef.current = []; 121 | setIsPlaying(false); 122 | }, []); 123 | 124 | const pause = useCallback(() => { 125 | if (!soundRef.current) return; 126 | 127 | soundRef.current.pause(); 128 | setIsPlaying(false); 129 | }, []); 130 | 131 | const resume = useCallback(() => { 132 | if (!soundRef.current || !enabled || activeSoundsRef.current.length === 0) return; 133 | 134 | // Try to unlock audio context before resuming 135 | unlockAudioContext().then(() => { 136 | activeSoundsRef.current.forEach(({ id }) => soundRef.current?.play(id)); 137 | setIsPlaying(true); 138 | }); 139 | }, [enabled]); 140 | 141 | useEffect(() => { 142 | if (!enabled && isPlaying) pause(); // Pause all sounds when disabled 143 | }, [enabled, isPlaying, pause]); 144 | 145 | // Cleanup on unmount 146 | useEffect(() => { 147 | preloadSounds([soundName]).then(() => setIsLoaded(true)); 148 | 149 | return () => { 150 | setIsLoaded(false); 151 | setIsPlaying(false); 152 | 153 | activeSoundsRef.current.forEach((sound) => { 154 | if (sound.resolver) sound.resolver(); // Resolve any pending promises 155 | }); 156 | activeSoundsRef.current = []; 157 | 158 | if (soundRef.current) { 159 | soundRef.current.stop(); 160 | soundRef.current = freeSound(soundName); 161 | } 162 | }; 163 | }, [soundName]); 164 | 165 | return { play, stop, pause, resume, isPlaying, isLoaded }; 166 | } 167 | 168 | interface UseSoundOnChangeOptions extends SoundOptions { 169 | initial?: boolean; 170 | } 171 | 172 | /** 173 | * Hook for playing a sound when a value changes 174 | */ 175 | export function useSoundOnChange(soundName: SoundName, value: T, options?: UseSoundOnChangeOptions): void { 176 | const { play } = useSound(soundName); 177 | const initialRef = useRef(true); 178 | 179 | useEffect(() => { 180 | const skipThisInitialRun = initialRef.current && options?.initial === false; 181 | initialRef.current = false; 182 | if (skipThisInitialRun) return; 183 | 184 | play(options).catch((err) => console.error("Failed to play sound:", err)); 185 | }, [value]); 186 | } 187 | 188 | /** 189 | * Hook for accessing and controlling the sound enabled state 190 | */ 191 | export function useSoundEnabled(): [boolean, (enabled: boolean) => void] { 192 | const context = useContext(SoundContext); 193 | if (!context) throw new Error("useSoundEnabled must be used within a SoundProvider"); 194 | 195 | return [context.enabled, context.setEnabled]; 196 | } 197 | -------------------------------------------------------------------------------- /website/src/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { useSound, useSoundEnabled } from "react-sounds"; 4 | import AdvancedSoundDemo from "../components/AdvancedSoundDemo"; 5 | import CodeBlock from "../components/CodeBlock"; 6 | import FeatureCard from "../components/FeatureCard"; 7 | import { cn } from "../utils/cn"; 8 | 9 | // CSS for the heartbeat pulse animation 10 | const heartbeatPulseStyle = ` 11 | @keyframes heartbeatPulse { 12 | 0%, 100% { transform: scale(1.05); } 13 | 10% { transform: scale(1.1); } /* First beat */ 14 | 20% { transform: scale(1.05); } /* Reset after first beat */ 15 | 30% { transform: scale(1.08); } /* Second beat (slightly smaller) */ 16 | 40% { transform: scale(1.05); } /* Reset after second beat */ 17 | } 18 | 19 | .heartbeat-pulse { 20 | animation: heartbeatPulse 0.5s infinite; 21 | } 22 | `; 23 | 24 | interface Feature { 25 | title: string; 26 | description: string; 27 | icon: string; 28 | } 29 | 30 | const HomePage: React.FC = () => { 31 | const hoverSound = useSound("ambient/heartbeat", { loop: true }); 32 | const [soundIsEnabled] = useSoundEnabled(); 33 | const [hasInteracted, setHasInteracted] = useState(false); 34 | const [isPulsing, setIsPulsing] = useState(false); 35 | 36 | // Function to handle hovering over the button 37 | const handleHoverStart = () => { 38 | if (!hoverSound.isPlaying) { 39 | hoverSound.play().catch(() => { 40 | // If the sound fails to play due to locked AudioContext, don't do anything 41 | // It will not unexpectedly play later 42 | }); 43 | } 44 | setIsPulsing(true); 45 | }; 46 | 47 | const handleHoverEnd = () => { 48 | hoverSound.stop(); 49 | setIsPulsing(false); 50 | }; 51 | 52 | // Track user's first click to know they've interacted 53 | const handleFirstInteraction = () => { 54 | setHasInteracted(true); 55 | }; 56 | 57 | const features: Feature[] = [ 58 | { 59 | title: "🔊 Extensive Library", 60 | description: "Access curated sounds organized by category (UI, notification, game, etc.).", 61 | icon: "🔊", 62 | }, 63 | { 64 | title: "🪶 Lightweight", 65 | description: 66 | "Only JS wrappers included in the package. Audio files are hosted on a CDN to keep your bundle size small.", 67 | icon: "🪶", 68 | }, 69 | { 70 | title: "🔄 Lazy Loading", 71 | description: "Sounds are fetched efficiently only when they are needed, improving initial load performance.", 72 | icon: "🔄", 73 | }, 74 | { 75 | title: "📦 Offline Support", 76 | description: "Easily download and bundle sounds for self-hosting using the included CLI tool.", 77 | icon: "📦", 78 | }, 79 | { 80 | title: "🎯 Simple API", 81 | description: 82 | "Integrate sounds effortlessly with easy-to-use hooks (like useSound) and components (like SoundButton).", 83 | icon: "🎯", 84 | }, 85 | { 86 | title: "⚙️ Configurable", 87 | description: "Customize CDN URLs, preload sounds, enable/disable sounds globally, and control playback options.", 88 | icon: "⚙️", 89 | }, 90 | ]; 91 | 92 | return ( 93 |
94 | {/* Style tag for custom animations */} 95 |