Skip to content

Commit f65ec11

Browse files
committed
Add doorbell channel using Supabase
1 parent 77661f6 commit f65ec11

6 files changed

Lines changed: 136 additions & 40 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ jobs:
2424
# Build job
2525
build:
2626
runs-on: ubuntu-latest
27+
env:
28+
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
29+
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
2730
steps:
2831
- name: Checkout
2932
uses: actions/checkout@v4

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ bun run dev
3535

3636
The site will be running at http://localhost:3000.
3737

38+
### Environment Variables
39+
40+
Realtime doorbell interactions rely on Supabase Realtime. Create a Supabase project (free tier is fine) and add the following to `.env.local`:
41+
42+
```sh
43+
NEXT_PUBLIC_SUPABASE_URL=<your-supabase-project-url>
44+
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your-public-anon-key>
45+
```
46+
47+
These keys are safe to expose to the browser but should be scoped to the realtime channel only via Supabase RLS/policies.
48+
3849
**Note: This project is being refactored to use styled-components exclusively. Please do not add new Tailwind classes. See [styling guidelines](./docs/conventions/styling-guidelines.md) for details.**
3950

4051
## Contributing

app/doorbell/page.tsx

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,79 @@
11
"use client"
2-
import { useState, useEffect } from "react"
2+
import { useState, useEffect, useRef, useCallback } from "react"
3+
import type { RealtimeChannel } from "@supabase/supabase-js"
34
import styled from "styled-components"
45
import { motion } from "framer-motion"
56
import { PotionBackground } from "../components/PotionBackground"
67
import { ErrorBoundary } from "../components/ErrorBoundary"
78
import { Button } from "../components/Button"
9+
import { supabaseClient } from "@/lib/supabaseClient"
810

911
// Components //
1012

1113
export default function Doorbell() {
1214
const [isRinging, setIsRinging] = useState(false)
15+
const channelRef = useRef<RealtimeChannel | null>(null)
16+
const lastRingIdRef = useRef<string | null>(null)
1317

14-
const handleDoorbellClick = () => {
18+
const triggerLocalRing = useCallback(() => {
1519
playDoorbellSound()
1620
setIsRinging(true)
21+
}, [])
22+
23+
const broadcastRing = useCallback(async (ringId: string) => {
24+
if (!channelRef.current) {
25+
return
26+
}
27+
28+
try {
29+
const status = await channelRef.current.send({
30+
type: "broadcast",
31+
event: "ring",
32+
payload: { ringId }
33+
})
34+
35+
if (status !== "ok") {
36+
console.error("Failed to broadcast doorbell ring:", status)
37+
}
38+
} catch (error) {
39+
console.error("Failed to broadcast doorbell ring:", error)
40+
}
41+
}, [])
42+
43+
const handleDoorbellClick = () => {
44+
const ringId = createRingIdentifier()
45+
lastRingIdRef.current = ringId
46+
triggerLocalRing()
47+
void broadcastRing(ringId)
1748
}
1849

50+
useEffect(() => {
51+
const client = supabaseClient
52+
if (!client) {
53+
return
54+
}
55+
56+
const channel = client
57+
.channel("doorbell")
58+
.on("broadcast", { event: "ring" }, ({ payload }) => {
59+
const ringPayload = payload as RingPayload | undefined
60+
if (ringPayload?.ringId && ringPayload.ringId === lastRingIdRef.current) {
61+
return
62+
}
63+
64+
triggerLocalRing()
65+
})
66+
.subscribe()
67+
68+
channelRef.current = channel
69+
70+
return () => {
71+
void channel.unsubscribe()
72+
client.removeChannel(channel)
73+
channelRef.current = null
74+
}
75+
}, [triggerLocalRing])
76+
1977
useEffect(() => {
2078
if (isRinging) {
2179
const timer = setTimeout(() => {
@@ -152,6 +210,18 @@ const AnimatedButtonContent = styled(motion.span)`
152210

153211
// Constants //
154212

213+
type RingPayload = {
214+
ringId?: string
215+
}
216+
217+
const createRingIdentifier = () => {
218+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
219+
return crypto.randomUUID()
220+
}
221+
222+
return `${Date.now()}-${Math.random().toString(16).slice(2)}`
223+
}
224+
155225
const playDoorbellSound = () => {
156226
try {
157227
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()

bun.lock

Lines changed: 27 additions & 38 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)