Skip to content

Commit 0aab719

Browse files
committed
feat: tilt settings + 30s settings refresh
1 parent 2b154a8 commit 0aab719

File tree

4 files changed

+105
-20
lines changed

4 files changed

+105
-20
lines changed

client/src/composables/useSpamProtection.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1-
import { ref, computed, onMounted, onUnmounted } from 'vue'
1+
import { ref, computed, onMounted, onUnmounted, unref } from 'vue'
22

3-
const SPAM_THRESHOLD = 15 // clicks
43
const SPAM_WINDOW = 10000 // 10 seconds in ms
5-
const COOLDOWN_DURATION = 10000 // 5 seconds in ms
64

7-
export function useSpamProtection (roomId) {
5+
export function useSpamProtection (roomId, settings = {}) {
86
const clicks = ref([])
97
const cooldownUntil = ref(0)
108
const buttonStates = ref({}) // emoji -> { lastClick, state, clickCount, pendingRequests }
119
const forceUpdate = ref(0) // Used to force reactivity updates
1210

11+
// Get settings from room config or use defaults (supports reactive settings)
12+
const SPAM_THRESHOLD = computed(() => {
13+
const s = unref(settings)
14+
return s.maxReactions || 15
15+
})
16+
const COOLDOWN_DURATION = computed(() => {
17+
const s = unref(settings)
18+
return (s.cooldownSeconds || 10) * 1000 // Convert to ms
19+
})
20+
1321
const storageKey = `spam_protection_${roomId}`
1422

1523
// Timer to update cooldown display every 100ms
@@ -104,7 +112,7 @@ export function useSpamProtection (roomId) {
104112
const cooldownProgress = computed(() => {
105113
const remaining = cooldownRemaining.value
106114
if (remaining === 0) return 0
107-
const total = COOLDOWN_DURATION
115+
const total = COOLDOWN_DURATION.value
108116
return Math.round((remaining / total) * 100)
109117
})
110118

@@ -119,8 +127,8 @@ export function useSpamProtection (roomId) {
119127
clicks.value.push(now)
120128

121129
// Check for spam
122-
if (clicks.value.length >= SPAM_THRESHOLD) {
123-
cooldownUntil.value = now + COOLDOWN_DURATION
130+
if (clicks.value.length >= SPAM_THRESHOLD.value) {
131+
cooldownUntil.value = now + COOLDOWN_DURATION.value
124132
clicks.value = [] // Clear clicks after triggering cooldown
125133
startUpdateTimer() // Start the countdown timer
126134
}

client/src/views/Dashboard.vue

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,46 @@
115115
</div>
116116
</div>
117117

118+
<!-- Tilt Limit Settings -->
119+
<div class="space-y-3 pt-3 border-t border-gray-200">
120+
<div>
121+
<label class="block text-sm font-medium text-gray-700 mb-1">
122+
Spam Protection
123+
</label>
124+
<p class="text-xs text-gray-500 mb-3">
125+
Prevent spam by limiting how fast users can send reactions
126+
</p>
127+
</div>
128+
<div class="grid grid-cols-2 gap-3">
129+
<div>
130+
<label class="block text-xs font-medium text-gray-600 mb-1">
131+
Max Reactions
132+
</label>
133+
<input
134+
v-model.number="roomForm.tiltMaxReactions"
135+
type="number"
136+
min="1"
137+
max="100"
138+
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
139+
>
140+
<p class="text-xs text-gray-500 mt-1">Reactions before cooldown</p>
141+
</div>
142+
<div>
143+
<label class="block text-xs font-medium text-gray-600 mb-1">
144+
Cooldown (seconds)
145+
</label>
146+
<input
147+
v-model.number="roomForm.tiltCooldownSeconds"
148+
type="number"
149+
min="1"
150+
max="60"
151+
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
152+
>
153+
<p class="text-xs text-gray-500 mt-1">Wait time after limit</p>
154+
</div>
155+
</div>
156+
</div>
157+
118158
<!-- Submit Button -->
119159
<button
120160
type="submit"
@@ -286,6 +326,8 @@ const roomForm = reactive({
286326
emojis: ['❤️', '🔥', '👏'],
287327
backgroundInput: '',
288328
backgroundOutput: '',
329+
tiltMaxReactions: 15,
330+
tiltCooldownSeconds: 10,
289331
})
290332
291333
// Copy status tracking
@@ -339,6 +381,8 @@ async function validateAndLoadRoom () {
339381
roomForm.emojis = roomData.settings?.emojis?.map((e) => e.emoji) || ['❤️', '🔥', '👏']
340382
roomForm.backgroundInput = roomData.settings?.backgroundInput || ''
341383
roomForm.backgroundOutput = roomData.settings?.backgroundOutput || ''
384+
roomForm.tiltMaxReactions = roomData.settings?.tiltLimit?.maxReactions || 15
385+
roomForm.tiltCooldownSeconds = roomData.settings?.tiltLimit?.cooldownSeconds || 10
342386
} catch (err) {
343387
error.value = 'Unable to load room data. Please check your connection and try again.'
344388
console.error('Dashboard load error:', err)
@@ -359,6 +403,10 @@ async function updateRoom () {
359403
emojis: roomForm.emojis.filter((e) => e.trim()).map((emoji) => ({ emoji })),
360404
backgroundInput: roomForm.backgroundInput || undefined,
361405
backgroundOutput: roomForm.backgroundOutput || undefined,
406+
tiltLimit: {
407+
maxReactions: roomForm.tiltMaxReactions,
408+
cooldownSeconds: roomForm.tiltCooldownSeconds,
409+
},
362410
},
363411
}
364412

client/src/views/Input.vue

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,19 @@ const props = defineProps({
138138
})
139139
140140
const { fetchRoom, submitReaction: apiSubmitReaction } = useRoomApi()
141+
142+
const loading = ref(true)
143+
const error = ref(false)
144+
const roomData = ref(null)
145+
const emojis = computed(() => roomData.value?.settings?.emojis || [])
146+
let refreshInterval = null
147+
148+
// Get tilt limit settings from room or use defaults
149+
const tiltSettings = computed(() => ({
150+
maxReactions: roomData.value?.settings?.tiltLimit?.maxReactions || 15,
151+
cooldownSeconds: roomData.value?.settings?.tiltLimit?.cooldownSeconds || 10,
152+
}))
153+
141154
const {
142155
canSubmit,
143156
cooldownRemaining,
@@ -147,12 +160,7 @@ const {
147160
setButtonLoading,
148161
setButtonSuccess,
149162
clearButtonState,
150-
} = useSpamProtection(props.roomId)
151-
152-
const loading = ref(true)
153-
const error = ref(false)
154-
const roomData = ref(null)
155-
const emojis = computed(() => roomData.value?.settings?.emojis || [])
163+
} = useSpamProtection(props.roomId, tiltSettings)
156164
157165
// Computed property for background style
158166
const backgroundStyle = computed(() => {
@@ -182,23 +190,40 @@ const backgroundStyle = computed(() => {
182190
}
183191
})
184192
185-
onMounted(async () => {
186-
// Add classes to prevent zoom and scrolling on mobile
187-
document.body.classList.add('h-svh', 'overflow-hidden', 'touch-manipulation')
188-
189-
// Fetch room data
193+
// Fetch room data (initial load and periodic refresh)
194+
async function loadRoomData () {
190195
try {
191196
roomData.value = await fetchRoom(props.roomId)
192197
loading.value = false
193198
} catch (err) {
194199
console.error('Failed to fetch room:', err)
195-
error.value = true
196-
loading.value = false
200+
// Only show error on initial load
201+
if (!roomData.value) {
202+
error.value = true
203+
loading.value = false
204+
}
197205
}
206+
}
207+
208+
onMounted(async () => {
209+
// Add classes to prevent zoom and scrolling on mobile
210+
document.body.classList.add('h-svh', 'overflow-hidden', 'touch-manipulation')
211+
212+
// Initial fetch
213+
await loadRoomData()
214+
215+
// Set up periodic refresh every 30 seconds
216+
refreshInterval = setInterval(loadRoomData, 30000)
198217
})
199218
200219
onBeforeUnmount(() => {
201220
document.body.classList.remove('h-svh', 'overflow-hidden', 'touch-manipulation')
221+
222+
// Clear refresh interval
223+
if (refreshInterval) {
224+
clearInterval(refreshInterval)
225+
refreshInterval = null
226+
}
202227
})
203228
204229
async function submitReaction (emoji) {

functions/src/types/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export interface Room {
1111
}>
1212
backgroundInput?: string // Optional full URL or solid color for input view
1313
backgroundOutput?: string // Optional full URL or solid color for output view
14+
tiltLimit?: {
15+
maxReactions: number // Number of reactions before cooldown (default: 15)
16+
cooldownSeconds: number // Cooldown duration in seconds (default: 10)
17+
}
1418
}
1519
createdAt: Date
1620
updatedAt: Date

0 commit comments

Comments
 (0)