Skip to content

Commit 5b24e43

Browse files
ln-dev7claude
andcommitted
feat(sounds): add soundcn UI sound effects on key interactions
Wire soundcn (Web Audio API, zero-dep, CC0) on header items and home CTAs via a unified useUiSounds hook — click on nav links, pop on sheet/menu triggers, switch on theme toggle, error on logout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent dd705e3 commit 5b24e43

17 files changed

Lines changed: 430 additions & 9 deletions

app/page.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
Sparkles,
3232
Users,
3333
} from "lucide-react"
34+
import { useUiSounds } from "@/hooks/use-ui-sounds"
3435
import { useTheme } from "@/components/theme-provider"
3536

3637
import { siteConfig } from "@/config/site"
@@ -47,9 +48,15 @@ import { TiltedPreview } from "@/components/home/tilted-preview"
4748
export default function IndexPage() {
4849
const { login, isPending } = useGitHubLogin()
4950
const { resolvedTheme } = useTheme()
51+
const { playClick, playPop } = useUiSounds()
5052

5153
const { user } = useAuthContext()
5254

55+
const handleLogin = () => {
56+
playClick()
57+
login()
58+
}
59+
5360
// Contributors
5461
const { data: contributors, isLoading: isLoadingContributors } = useQuery(
5562
"contributors",
@@ -90,6 +97,7 @@ export default function IndexPage() {
9097
{/* Announcement pill */}
9198
<Link
9299
href="/image"
100+
onClick={() => playPop()}
93101
className="group relative z-10 inline-flex w-fit items-center gap-2 rounded-full border border-zinc-200 bg-white/80 px-3 py-1 text-sm font-medium shadow-sm backdrop-blur-sm transition-all hover:border-zinc-300 hover:bg-white hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900/80 dark:hover:border-zinc-700 dark:hover:bg-zinc-900"
94102
>
95103
<span className="relative flex h-2 w-2 shrink-0">
@@ -141,6 +149,7 @@ export default function IndexPage() {
141149
<div className="relative z-10 flex flex-col gap-3 sm:flex-row sm:gap-4">
142150
<Link
143151
href={siteConfig.links.explore}
152+
onClick={() => playClick()}
144153
className={cn(
145154
buttonVariants({ size: "lg" }),
146155
"group relative gap-2 overflow-hidden shadow-[0_10px_30px_-12px_rgba(99,102,241,0.5)] transition-shadow hover:shadow-[0_16px_40px_-12px_rgba(99,102,241,0.7)] dark:shadow-[0_10px_30px_-12px_rgba(0,0,0,0.6)]"
@@ -155,6 +164,7 @@ export default function IndexPage() {
155164
{user ? (
156165
<Link
157166
href="/dashboard"
167+
onClick={() => playClick()}
158168
className={cn(
159169
buttonVariants({ size: "lg", variant: "outline" }),
160170
"gap-2 backdrop-blur-sm"
@@ -170,7 +180,7 @@ export default function IndexPage() {
170180
"gap-2 backdrop-blur-sm"
171181
)}
172182
disabled={isPending}
173-
onClick={login}
183+
onClick={handleLogin}
174184
>
175185
{isPending ? (
176186
<Loader2 className="h-4 w-4 animate-spin" />

components/avatar-user.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { signOut } from "firebase/auth"
99
import { Verified } from "lucide-react"
1010

1111
import { siteConfig } from "@/config/site"
12+
import { useUiSounds } from "@/hooks/use-ui-sounds"
1213
import { Icons } from "@/components/icons"
1314
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
1415
import { Button, buttonVariants } from "@/components/ui/button"
@@ -43,12 +44,13 @@ export const useGitHubLogout = () => {
4344
export function AvatarUser() {
4445
const { logout } = useGitHubLogout()
4546
const { user, userPseudo } = useAuthContext()
47+
const { playClick, playPop, playError } = useUiSounds()
4648

4749
const { data, isLoading } = useDocument(userPseudo, "users")
4850
return (
4951
<Sheet>
5052
<SheetTrigger asChild>
51-
<div className="relative">
53+
<div className="relative" onClick={() => playPop()}>
5254
{data && data.exists && data.data.premium && (
5355
<div className="absolute -right-1 -bottom-1 z-10 flex items-center justify-center rounded-full border-2 border-yellow-500 bg-white dark:bg-zinc-800">
5456
<Verified className="h-5 w-5 shrink-0 p-1 text-yellow-500" />
@@ -109,13 +111,15 @@ export function AvatarUser() {
109111
<Link
110112
defaultChecked
111113
href="/dashboard"
114+
onClick={() => playClick()}
112115
className={buttonVariants({ size: "lg", variant: "outline" })}
113116
>
114117
Dashboard
115118
</Link>
116119
<Button
117120
className={buttonVariants({ size: "lg", variant: "destructive" })}
118121
onClick={() => {
122+
playError()
119123
logout()
120124
}}
121125
>
@@ -132,6 +136,7 @@ export function AvatarUser() {
132136
href={siteConfig.links.github}
133137
target="_blank"
134138
rel="noreferrer"
139+
onClick={() => playClick()}
135140
>
136141
<div
137142
className={buttonVariants({
@@ -148,6 +153,7 @@ export function AvatarUser() {
148153
href={siteConfig.links.twitter}
149154
target="_blank"
150155
rel="noreferrer"
156+
onClick={() => playClick()}
151157
>
152158
<div
153159
className={buttonVariants({

components/main-nav.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
"use client"
2+
13
import Link from "next/link"
24
import { ExternalLink } from "lucide-react"
35

46
import { NavItem } from "@/types/nav"
57
import { siteConfig } from "@/config/site"
68
import { cn } from "@/lib/utils"
9+
import { useUiSounds } from "@/hooks/use-ui-sounds"
710
import { Icons } from "@/components/icons"
811
import { Button } from "@/components/ui/button"
912
import {
@@ -20,10 +23,12 @@ interface MainNavProps {
2023
}
2124

2225
export function MainNav({ items }: MainNavProps) {
26+
const { playClick, playPop } = useUiSounds()
2327
return (
2428
<div className="flex gap-6 min-[1250px]:gap-10">
2529
<Link
2630
href="/"
31+
onClick={() => playClick()}
2732
className="hidden items-center space-x-2 min-[1250px]:flex"
2833
>
2934
<Icons.logo className="h-6 w-6" />
@@ -40,6 +45,9 @@ export function MainNav({ items }: MainNavProps) {
4045
<Link
4146
key={index}
4247
href={item.disabled ? "#" : item.href}
48+
onClick={() => {
49+
if (!item.disabled) playClick()
50+
}}
4351
className={cn(
4452
"group relative inline-flex h-9 w-max items-center justify-center rounded-md bg-transparent px-4 py-2 text-sm font-semibold transition-colors hover:bg-zinc-100 focus:bg-zinc-100 focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-zinc-50 data-[state=open]:bg-zinc-50 dark:text-zinc-100 dark:hover:bg-zinc-800 dark:hover:text-zinc-100 dark:focus:bg-zinc-800 dark:data-[active]:bg-zinc-800 dark:data-[state=open]:bg-zinc-800",
4553
item.disabled && "cursor-not-allowed"
@@ -67,6 +75,7 @@ export function MainNav({ items }: MainNavProps) {
6775
<DropdownMenuTrigger asChild>
6876
<Button
6977
variant="ghost"
78+
onClick={() => playPop()}
7079
className="-ml-4 text-base hover:bg-transparent focus:ring-0 min-[1250px]:hidden"
7180
>
7281
<Icons.logo className="mr-2 h-4 w-4" />{" "}
@@ -87,7 +96,13 @@ export function MainNav({ items }: MainNavProps) {
8796
{items?.map(
8897
(item, index) =>
8998
item.href && (
90-
<DropdownMenuItem key={index} asChild>
99+
<DropdownMenuItem
100+
key={index}
101+
asChild
102+
onClick={() => {
103+
if (!item.disabled) playClick()
104+
}}
105+
>
91106
<Link href={item.disabled ? "#" : item.href}>
92107
{item.title}
93108
{item.external && <ExternalLink className="ml-2 h-4 w-4" />}

components/site-header.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useParams, usePathname } from "next/navigation"
55
import { useAuthContext } from "@/context/AuthContext"
66

77
import { siteConfig } from "@/config/site"
8+
import { useUiSounds } from "@/hooks/use-ui-sounds"
89
import { Icons } from "@/components/icons"
910
import { MainNav } from "@/components/main-nav"
1011
import { ThemeSwitcher } from "@/components/theme-switcher"
@@ -19,6 +20,7 @@ export function SiteHeader() {
1920
const { user } = useAuthContext()
2021
const pathName = usePathname()
2122
const params = useParams()
23+
const { playClick } = useUiSounds()
2224

2325
const searchBar =
2426
params["link"] !== undefined || (pathName === "/links" && user) ? (
@@ -46,6 +48,7 @@ export function SiteHeader() {
4648
href={siteConfig.links.github}
4749
target="_blank"
4850
rel="noreferrer"
51+
onClick={() => playClick()}
4952
className={buttonVariants({
5053
size: "sm",
5154
variant: "ghost",
@@ -60,6 +63,7 @@ export function SiteHeader() {
6063
href={siteConfig.links.twitter}
6164
target="_blank"
6265
rel="noreferrer"
66+
onClick={() => playClick()}
6367
className={buttonVariants({
6468
size: "sm",
6569
variant: "ghost",

components/theme-switcher.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { Paintbrush, Check, Search } from "lucide-react"
44
import { useState } from "react"
55

6+
import { useUiSounds } from "@/hooks/use-ui-sounds"
67
import { useTheme } from "@/components/theme-provider"
78
import { Button } from "@/components/ui/button"
89
import { Input } from "@/components/ui/input"
@@ -347,6 +348,7 @@ const DARK_PREVIEWS: Record<
347348
export function ThemeSwitcher() {
348349
const { colorTheme, setColorTheme } = useColorThemeStore()
349350
const { resolvedTheme } = useTheme()
351+
const { playClick, playPop } = useUiSounds()
350352

351353
const isDark = resolvedTheme === "dark"
352354
const [search, setSearch] = useState("")
@@ -369,7 +371,12 @@ export function ThemeSwitcher() {
369371
return (
370372
<Sheet>
371373
<SheetTrigger asChild>
372-
<Button variant="ghost" size="icon" className="size-8">
374+
<Button
375+
variant="ghost"
376+
size="icon"
377+
className="size-8"
378+
onClick={() => playPop()}
379+
>
373380
<Paintbrush className="size-4" />
374381
</Button>
375382
</SheetTrigger>
@@ -429,7 +436,10 @@ export function ThemeSwitcher() {
429436
return (
430437
<button
431438
key={theme.id}
432-
onClick={() => setColorTheme(theme.id)}
439+
onClick={() => {
440+
playClick()
441+
setColorTheme(theme.id)
442+
}}
433443
className={cn(
434444
"relative flex flex-col gap-2 rounded-xl border-2 p-3 text-left transition-all hover:border-primary/50",
435445
isSelected

components/theme-toggle.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
"use client"
2+
13
import { useTheme } from "@/components/theme-provider"
24

5+
import { useUiSounds } from "@/hooks/use-ui-sounds"
36
import { Icons } from "@/components/icons"
47
import { Button } from "@/components/ui/button"
58
import {
@@ -11,26 +14,42 @@ import {
1114

1215
export function ThemeToggle() {
1316
const { setTheme } = useTheme()
17+
const { playSwitchOn, playSwitchOff, playClick } = useUiSounds()
1418

1519
return (
1620
<DropdownMenu>
1721
<DropdownMenuTrigger asChild>
18-
<Button variant="ghost" size="sm">
22+
<Button variant="ghost" size="sm" onClick={() => playClick()}>
1923
<Icons.sun className="scale-100 rotate-0 transition-all hover:text-zinc-900 dark:scale-0 dark:-rotate-90 dark:text-zinc-400 dark:hover:text-zinc-100" />
2024
<Icons.moon className="absolute scale-0 rotate-90 transition-all hover:text-zinc-900 dark:scale-100 dark:rotate-0 dark:text-zinc-400 dark:hover:text-zinc-100" />
2125
<span className="sr-only">Toggle theme</span>
2226
</Button>
2327
</DropdownMenuTrigger>
2428
<DropdownMenuContent align="end" forceMount>
25-
<DropdownMenuItem onClick={() => setTheme("light")}>
29+
<DropdownMenuItem
30+
onClick={() => {
31+
playSwitchOff()
32+
setTheme("light")
33+
}}
34+
>
2635
<Icons.sun className="mr-2 h-4 w-4" />
2736
<span>Light</span>
2837
</DropdownMenuItem>
29-
<DropdownMenuItem onClick={() => setTheme("dark")}>
38+
<DropdownMenuItem
39+
onClick={() => {
40+
playSwitchOn()
41+
setTheme("dark")
42+
}}
43+
>
3044
<Icons.moon className="mr-2 h-4 w-4" />
3145
<span>Dark</span>
3246
</DropdownMenuItem>
33-
<DropdownMenuItem onClick={() => setTheme("system")}>
47+
<DropdownMenuItem
48+
onClick={() => {
49+
playClick()
50+
setTheme("system")
51+
}}
52+
>
3453
<Icons.laptop className="mr-2 h-4 w-4" />
3554
<span>System</span>
3655
</DropdownMenuItem>

0 commit comments

Comments
 (0)