Fix: Explicitly render reCAPTCHA on form display

This commit is contained in:
2025-10-10 11:29:19 -07:00
parent e06fb29efb
commit 7df65b26eb

View File

@@ -8,7 +8,7 @@
<script src="https://cdn.tailwindcss.com"></script>
<script src="//unpkg.com/alpinejs" defer></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<script src="https://www.google.com/recaptcha/api.js?render=explicit" async defer></script>
</head>
<body class="bg-slate-50 text-slate-800 font-sans">
<script>if (!localStorage.getItem('username')) window.location.href = '/';</script>
@@ -37,9 +37,9 @@
<main class="flex-grow">
<div class="max-w-5xl mx-auto p-4 sm:p-6 lg:p-8">
<div class="max-w-2xl mx-auto">
<div x-data="linkForm()" class="bg-white p-8 rounded-xl shadow-sm border border-slate-200">
<div class="bg-white p-8 rounded-xl shadow-sm border border-slate-200">
<template x-if="!editingSlug">
<div>
<div x-data="linkForm()" x-init="renderCaptcha()">
<h1 class="text-2xl font-bold mb-1">Create a new link</h1>
<p class="text-slate-500 mb-6">Shorten a long URL into a memorable link.</p>
@@ -68,7 +68,7 @@
<input x-model="slug" type="text" id="customSlug" placeholder="my-custom-link" class="w-full p-3 bg-slate-50 border border-slate-300 rounded-r-md focus:outline-none focus:ring-2 focus:ring-slate-400 focus:border-slate-400 transition">
</div>
</div>
<div class="g-recaptcha my-4 flex justify-center" data-sitekey="6LeXhdYrAAAAALW6DdgxNeHU0kwBncdicLnVYvXT"></div>
<div class="recaptcha-container my-4 flex justify-center"></div>
<p x-text="error" x-show="error" class="text-rose-500 text-sm h-5 -mt-2 text-center"></p>
<button type="submit" :disabled="loading" class="w-full py-3 font-semibold rounded-lg text-white bg-slate-800 hover:bg-slate-900 transition-colors flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!loading">Create Short Link</span>
@@ -92,7 +92,7 @@
<label for="editUrl" class="block text-sm font-medium text-slate-700 mb-1">Destination URL</label>
<input x-model="destination_url" type="text" id="editUrl" placeholder="example.com/new-destination" required class="w-full p-3 bg-slate-50 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-slate-400 focus:border-slate-400 transition">
</div>
<div class="g-recaptcha my-4 flex justify-center" data-sitekey="6LeXhdYrAAAAALW6DdgxNeHU0kwBncdicLnVYvXT"></div>
<div class="recaptcha-container my-4 flex justify-center"></div>
<p x-text="error" x-show="error" class="text-rose-500 text-sm h-5 -mt-2 text-center"></p>
<div class="flex gap-3">
<button type="submit" :disabled="loading" class="flex-1 py-3 font-semibold rounded-lg text-white bg-slate-800 hover:bg-slate-900 transition-colors flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed">
@@ -180,8 +180,8 @@
<script>
function dashboard(){return{user:localStorage.getItem("username"),sidebarOpen:!1,editingSlug:null,links:[],loadingLinks:!0,errorLinks:"",async fetchLinks(){this.loadingLinks=!0,this.errorLinks="";try{const e=localStorage.getItem("username"),t=localStorage.getItem("pass_hash");if(!e||!t)throw new Error("Authentication error.");const s=await fetch("/api/links/list",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,pass_hash:t})});if(!s.ok)throw new Error(await s.text()||"Failed to fetch links.");this.links=(await s.json()).reverse()}catch(e){this.errorLinks=e.message}finally{this.loadingLinks=!1,this.$nextTick(()=>lucide.createIcons())}}}}
function linkForm(){return{destination_url:"",slug:"",loading:!1,error:"",result:{},copied:!1,async createLink(){this.loading=!0,this.error="",this.result={};const o=grecaptcha.getResponse();if(!o)return this.error="Please complete the CAPTCHA.",this.loading=!1,void 0;try{const e=localStorage.getItem("username"),s=localStorage.getItem("pass_hash");if(!e||!s)throw new Error("Authentication error. Please log in again.");const t=await fetch("/api/links/create",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({destination_url:this.destination_url,slug:this.slug||null,username:e,pass_hash:s,"g-recaptcha-response":o})});if(!t.ok)throw new Error(await t.text()||"Failed to create link.");const i=await t.json(),r=window.location.host;this.result={...i,url:`https://${r}/${i.slug}`},this.destination_url="",this.slug="",this.$dispatch("link-created"),this.$nextTick(()=>lucide.createIcons())}catch(e){this.error=e.message}finally{this.loading=!1,grecaptcha.reset()}}}}
function editForm(){return{destination_url:"",loading:!1,error:"",async init(){await this.loadCurrentUrl()},async loadCurrentUrl(){try{const e=await fetch(`/api/links/get?slug=${this.editingSlug}`);if(!e.ok)throw new Error("Failed to load link");const s=await e.json();this.destination_url=s.destination_url.startsWith("http")?s.destination_url:`https://${s.destination_url}`}catch(e){this.error=e.message}},async updateLink(){this.loading=!0,this.error="";const o=grecaptcha.getResponse();if(!o)return this.error="Please complete the CAPTCHA.",this.loading=!1,void 0;try{const e=localStorage.getItem("username"),s=localStorage.getItem("pass_hash");if(!e||!s)throw new Error("Authentication error.");const t=await fetch("/api/links/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({slug:this.editingSlug,destination_url:this.destination_url,username:e,pass_hash:s,"g-recaptcha-response":o})});if(!t.ok)throw new Error(await t.text()||"Failed to update link.");this.editingSlug=null,this.$dispatch("link-created")}catch(e){this.error=e.message}finally{this.loading=!1,grecaptcha.reset()}},async deleteLink(){if(!confirm("Are you sure you want to delete this link?"))return;this.loading=!0,this.error="";const o=grecaptcha.getResponse();if(!o)return this.error="Please complete the CAPTCHA.",this.loading=!1,void 0;try{const e=localStorage.getItem("username"),s=localStorage.getItem("pass_hash");if(!e||!s)throw new Error("Authentication error.");const t=await fetch("/api/links/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({slug:this.editingSlug,username:e,pass_hash:s,"g-recaptcha-response":o})});if(!t.ok)throw new Error(await t.text()||"Failed to delete link.");this.editingSlug=null,this.$dispatch("link-created")}catch(e){this.error=e.message}finally{this.loading=!1,grecaptcha.reset()}}}}
function linkForm(){return{destination_url:"",slug:"",loading:!1,error:"",result:{},copied:!1,widgetId:null,renderCaptcha(){if(!window.grecaptcha?.render)return setTimeout(()=>this.renderCaptcha(),100);this.$nextTick(()=>{const e=this.$el.querySelector(".recaptcha-container");e&&(e.innerHTML="",this.widgetId=grecaptcha.render(e,{sitekey:"6LeXhdYrAAAAALW6DdgxNeHU0kwBncdicLnVYvXT"}))})},async createLink(){this.loading=!0,this.error="",this.result={};const o=grecaptcha.getResponse(this.widgetId);if(!o)return this.error="Please complete the CAPTCHA.",this.loading=!1,void 0;try{const e=localStorage.getItem("username"),s=localStorage.getItem("pass_hash");if(!e||!s)throw new Error("Authentication error.");const t=await fetch("/api/links/create",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({destination_url:this.destination_url,slug:this.slug||null,username:e,pass_hash:s,"g-recaptcha-response":o})});if(!t.ok)throw new Error(await t.text()||"Failed to create link.");const i=await t.json(),r=window.location.host;this.result={...i,url:`https://${r}/${i.slug}`},this.destination_url="",this.slug="",this.$dispatch("link-created"),this.$nextTick(()=>lucide.createIcons())}catch(e){this.error=e.message}finally{this.loading=!1,grecaptcha.reset(this.widgetId)}}}}
function editForm(){return{destination_url:"",loading:!1,error:"",widgetId:null,async init(){await this.loadCurrentUrl(),this.renderCaptcha()},renderCaptcha(){if(!window.grecaptcha?.render)return setTimeout(()=>this.renderCaptcha(),100);this.$nextTick(()=>{const e=this.$el.querySelector(".recaptcha-container");e&&(e.innerHTML="",this.widgetId=grecaptcha.render(e,{sitekey:"6LeXhdYrAAAAALW6DdgxNeHU0kwBncdicLnVYvXT"}))})},async loadCurrentUrl(){try{const e=await fetch(`/api/links/get?slug=${this.editingSlug}`);if(!e.ok)throw new Error("Failed to load link");const s=await e.json();this.destination_url=s.destination_url.startsWith("http")?s.destination_url:`https://${s.destination_url}`}catch(e){this.error=e.message}},async updateLink(){this.loading=!0,this.error="";const o=grecaptcha.getResponse(this.widgetId);if(!o)return this.error="Please complete the CAPTCHA.",this.loading=!1,void 0;try{const e=localStorage.getItem("username"),s=localStorage.getItem("pass_hash");if(!e||!s)throw new Error("Authentication error.");const t=await fetch("/api/links/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({slug:this.editingSlug,destination_url:this.destination_url,username:e,pass_hash:s,"g-recaptcha-response":o})});if(!t.ok)throw new Error(await t.text()||"Failed to update link.");this.editingSlug=null,this.$dispatch("link-created")}catch(e){this.error=e.message}finally{this.loading=!1,grecaptcha.reset(this.widgetId)}},async deleteLink(){if(!confirm("Are you sure you want to delete this link?"))return;this.loading=!0,this.error="";const o=grecaptcha.getResponse(this.widgetId);if(!o)return this.error="Please complete the CAPTCHA.",this.loading=!1,void 0;try{const e=localStorage.getItem("username"),s=localStorage.getItem("pass_hash");if(!e||!s)throw new Error("Authentication error.");const t=await fetch("/api/links/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({slug:this.editingSlug,username:e,pass_hash:s,"g-recaptcha-response":o})});if(!t.ok)throw new Error(await t.text()||"Failed to delete link.");this.editingSlug=null,this.$dispatch("link-created")}catch(e){this.error=e.message}finally{this.loading=!1,grecaptcha.reset(this.widgetId)}}}}
lucide.createIcons();
</script>
</body>