feat(landing): add captcha challenge cycling option

This commit is contained in:
itsrubberduck
2026-02-22 18:32:17 +01:00
parent ba1b1090b1
commit 1e3cff1770

View File

@@ -652,6 +652,15 @@
placeholder="Type the answer"
class="w-full rounded-2xl border border-white/10 bg-[#0b1020]/40 px-4 py-3 text-sm text-white placeholder-white/40 outline-none focus:border-cyan-400"
/>
<div class="flex justify-end">
<button
type="button"
class="rounded-xl border border-white/20 px-3 py-1.5 text-[11px] font-medium uppercase tracking-[0.08em] text-cyan-200 transition hover:border-cyan-300/60 hover:text-cyan-100"
@click="tryDifferentWaitlistCaptcha"
>
Try something different
</button>
</div>
<p v-if="waitlistCaptchaReminder" class="text-xs text-amber-200">{{ waitlistCaptchaReminder }}</p>
<p v-else-if="waitlistCaptcha.answer && !waitlistCaptchaValid" class="text-xs text-red-300">
Check the ATC question and try again.
@@ -1210,7 +1219,11 @@ const CAPTCHA_CHALLENGES: CaptchaChallenge[] = [
},
]
const DEFAULT_CAPTCHA = CAPTCHA_CHALLENGES[0]
const DEFAULT_CAPTCHA: CaptchaChallenge = CAPTCHA_CHALLENGES[0] ?? {
id: 'fallback-vfr',
prompt: 'What does the “V” in VFR stand for?',
answers: ['visual', 'visual flight rules'],
}
const createCaptchaState = () =>
reactive({
@@ -1237,11 +1250,11 @@ const isCaptchaAnswerValid = (state: CaptchaState) => {
return state.challenge.answers.some((answer) => normaliseCaptchaValue(answer) === response)
}
const pickRandomChallenge = (excludeId?: string) => {
const pickRandomChallenge = (excludeId?: string): CaptchaChallenge => {
const pool = excludeId ? CAPTCHA_CHALLENGES.filter((challenge) => challenge.id !== excludeId) : CAPTCHA_CHALLENGES
const available = pool.length ? pool : CAPTCHA_CHALLENGES
const index = Math.floor(Math.random() * available.length)
return available[index]
return available[index] ?? DEFAULT_CAPTCHA
}
const rotateCaptchaChallenge = (state: CaptchaState) => {
@@ -1250,6 +1263,14 @@ const rotateCaptchaChallenge = (state: CaptchaState) => {
state.answer = ''
}
const cycleCaptchaChallenge = (state: CaptchaState) => {
const currentIndex = CAPTCHA_CHALLENGES.findIndex((challenge) => challenge.id === state.challenge?.id)
const normalizedIndex = currentIndex >= 0 ? currentIndex : 0
const nextIndex = (normalizedIndex + 1) % CAPTCHA_CHALLENGES.length
state.challenge = CAPTCHA_CHALLENGES[nextIndex] ?? DEFAULT_CAPTCHA
state.answer = ''
}
const numberFormatter = computed(() => new Intl.NumberFormat('en-US'))
const formatNumber = (value: number | null | undefined) => numberFormatter.value.format(Math.max(0, Math.round(value ?? 0)))
@@ -1295,6 +1316,11 @@ const waitlistLastJoinedFormatted = computed(() => {
})
const waitlistCaptchaValid = computed(() => isCaptchaAnswerValid(waitlistCaptcha))
const tryDifferentWaitlistCaptcha = () => {
cycleCaptchaChallenge(waitlistCaptcha)
waitlistCaptchaReminder.value = ''
}
watch(
() => waitlistForm.email,
(value) => {