Fix: Persist analytics toggle state and display analytics data

This commit is contained in:
2025-10-11 09:10:55 -07:00
parent bac06a3aab
commit e850e6b252

View File

@@ -36,7 +36,7 @@
<div class="flex-grow flex relative">
<main class="flex-grow">
<div class="max-w-5xl mx-auto p-4 sm:p-6 lg:p-8">
<div class="max-w-5xl mx-auto p-4 sm:p-6 lg:px-8">
<div class="max-w-2xl mx-auto">
<div class="bg-white p-8 rounded-xl shadow-sm border border-slate-200">
<template x-if="!editingSlug">
@@ -95,7 +95,7 @@
</div>
<div class="flex items-center justify-between py-2">
<span class="text-sm font-medium text-slate-700">Enable Analytics</span>
<button type="button" @click="analyticsEnabled = !analyticsEnabled" :class="analyticsEnabled ? 'bg-slate-800' : 'bg-slate-200'" class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2" role="switch" :aria-checked="analyticsEnabled">
<button type="button" @click="analyticsEnabled = !analyticsEnabled; fetchAnalytics()" :class="analyticsEnabled ? 'bg-slate-800' : 'bg-slate-200'" class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2" role="switch" :aria-checked="analyticsEnabled">
<span :class="analyticsEnabled ? 'translate-x-5' : 'translate-x-0'" class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
</button>
</div>
@@ -111,6 +111,29 @@
</button>
</div>
</form>
<template x-if="analyticsEnabled && analytics.length > 0">
<div class="mt-8 pt-6 border-t border-slate-200">
<h3 class="text-lg font-semibold mb-4">Analytics</h3>
<div class="bg-slate-50 rounded-lg p-4 border border-slate-200">
<div class="space-y-3">
<template x-for="item in analytics" :key="item.referrer">
<div class="flex items-center justify-between py-2 border-b border-slate-200 last:border-0">
<span class="text-sm font-medium text-slate-700" x-text="item.referrer"></span>
<span class="text-sm font-semibold text-slate-900" x-text="item.count + ' clicks'"></span>
</div>
</template>
</div>
</div>
</div>
</template>
<template x-if="analyticsEnabled && analytics.length === 0 && !loadingAnalytics">
<div class="mt-8 pt-6 border-t border-slate-200">
<h3 class="text-lg font-semibold mb-4">Analytics</h3>
<p class="text-slate-500 text-center py-4">No analytics data yet. Share your link to start collecting data!</p>
</div>
</template>
</div>
</template>
</div>
@@ -193,7 +216,7 @@
<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,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:"",analyticsEnabled:!1,loading:!1,error:"",widgetId:null,async init(){await this.loadCurrentUrl(),this.renderTurnstile()},renderTurnstile(){if(!window.turnstile?.render)return setTimeout(()=>this.renderTurnstile(),100);this.$nextTick(()=>{const e=this.$el.querySelector(".turnstile-container");e&&(e.innerHTML="",this.widgetId=turnstile.render(e,{sitekey:"0x4AAAAAAB54R0OUQDyuiUS5"}))})},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}`,this.analyticsEnabled=s.analytics_enabled}catch(e){this.error=e.message}},async updateLink(){this.loading=!0,this.error="";const o=turnstile.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,analytics_enabled:this.analyticsEnabled,username:e,pass_hash:s,"cf-turnstile-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,turnstile.reset(this.widgetId)}},async deleteLink(){if(!confirm("Are you sure you want to delete this link?"))return;this.loading=!0,this.error="";const o=turnstile.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,"cf-turnstile-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,turnstile.reset(this.widgetId)}}}}
function editForm(){return{destination_url:"",analyticsEnabled:!1,analytics:[],loadingAnalytics:!1,loading:!1,error:"",widgetId:null,async init(){await this.loadCurrentUrl(),this.renderTurnstile(),this.analyticsEnabled&&await this.fetchAnalytics()},renderTurnstile(){if(!window.turnstile?.render)return setTimeout(()=>this.renderTurnstile(),100);this.$nextTick(()=>{const e=this.$el.querySelector(".turnstile-container");e&&(e.innerHTML="",this.widgetId=turnstile.render(e,{sitekey:"0x4AAAAAAB54R0OUQDyuiUS5"}))})},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}`,this.analyticsEnabled=s.analytics_enabled||!1}catch(e){this.error=e.message}},async fetchAnalytics(){if(!this.analyticsEnabled)return this.analytics=[],void 0;this.loadingAnalytics=!0;try{const e=await fetch(`/api/analytics/get?slug=${this.editingSlug}`);if(!e.ok)throw new Error("Failed to load analytics");this.analytics=await e.json()}catch(e){console.error(e)}finally{this.loadingAnalytics=!1,this.$nextTick(()=>lucide.createIcons())}},async updateLink(){this.loading=!0,this.error="";const o=turnstile.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,analytics_enabled:this.analyticsEnabled,username:e,pass_hash:s,"cf-turnstile-response":o})});if(!t.ok)throw new Error(await t.text()||"Failed to update link.");await this.fetchAnalytics(),this.editingSlug=null,this.$dispatch("link-created")}catch(e){this.error=e.message}finally{this.loading=!1,turnstile.reset(this.widgetId)}},async deleteLink(){if(!confirm("Are you sure you want to delete this link?"))return;this.loading=!0,this.error="";const o=turnstile.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,"cf-turnstile-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,turnstile.reset(this.widgetId)}}}}
lucide.createIcons();
</script>
</body>