mirror of
https://github.com/4ev-link/4ev.link.git
synced 2026-01-13 16:18:05 +00:00
Refactor: Consolidate state to fix sidebar link display
This commit is contained in:
38
dash.html
38
dash.html
@@ -13,7 +13,7 @@
|
||||
<body class="bg-slate-50 text-slate-800 font-sans">
|
||||
<script>if (!localStorage.getItem('username')) window.location.href = '/';</script>
|
||||
|
||||
<div x-data="dashboard()" x-init="init()" @link-modified.window="fetchLinks(); view='create'" class="min-h-screen flex flex-col">
|
||||
<div x-data="dashboard()" x-init="fetchLinks()" @link-modified.window="fetchLinks(); view='create'" class="min-h-screen flex flex-col">
|
||||
<header class="bg-white/80 backdrop-blur-sm border-b border-slate-200 sticky top-0 z-10">
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
@@ -33,8 +33,8 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div x-show="sidebarOpen" @click.away="sidebarOpen = false" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-200" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="fixed inset-0 bg-black/30 z-20" style="display: none;"></div>
|
||||
<aside x-show="sidebarOpen" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0" x-transition:leave="transition ease-in duration-200" x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full" class="fixed top-0 right-0 h-full w-full max-w-sm bg-white border-l border-slate-200 shadow-lg z-30 flex flex-col" style="display: none;">
|
||||
<div x-show="sidebarOpen" @click.away="sidebarOpen = false" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="fixed inset-0 bg-black/30 z-20" style="display: none;"></div>
|
||||
<aside x-show="sidebarOpen" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full" class="fixed top-0 right-0 h-full w-full max-w-sm bg-white border-l border-slate-200 shadow-lg z-30 flex flex-col" style="display: none;">
|
||||
<div class="p-4 border-b border-slate-200 flex justify-between items-center">
|
||||
<h2 class="text-lg font-semibold">Your Links</h2>
|
||||
<button @click="sidebarOpen = false" class="text-slate-500 hover:text-slate-900"><i data-lucide="x"></i></button>
|
||||
@@ -62,12 +62,11 @@
|
||||
<div class="max-w-5xl mx-auto p-4 sm:p-6 lg:p-8">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<template x-if="view === 'create'">
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<template x-if="result.url">
|
||||
<div class="bg-green-100 border border-green-300 text-green-800 p-3.5 rounded-md mb-6" role="alert">
|
||||
<div x-data="{copied:false}" class="bg-green-100 border border-green-300 text-green-800 p-3.5 rounded-md mb-6" role="alert">
|
||||
<p class="font-semibold">Success! Your link is ready:</p>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<a :href="result.url" x-text="result.url" target="_blank" class="font-mono text-slate-700 hover:underline"></a>
|
||||
@@ -78,17 +77,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form @submit.prevent="createLink" class="space-y-4">
|
||||
<div>
|
||||
<label for="longUrl" class="block text-sm font-medium text-slate-700 mb-1">Destination URL</label>
|
||||
<input x-model="destination_url" type="text" id="longUrl" placeholder="example.com/very-long-url-to-shorten" 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">
|
||||
<input x-model="form.destination_url" type="text" id="longUrl" placeholder="example.com/very-long-url-to-shorten" 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>
|
||||
<label for="customSlug" class="block text-sm font-medium text-slate-700 mb-1">Custom slug (optional)</label>
|
||||
<div class="flex items-center">
|
||||
<span class="p-3 bg-slate-100 border border-r-0 border-slate-300 rounded-l-md text-slate-500">4ev.link/</span>
|
||||
<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">
|
||||
<input x-model="form.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>
|
||||
@@ -102,28 +100,28 @@
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="view === 'edit'">
|
||||
<div x-data="editForm(editingLink)" 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">
|
||||
<div class="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold mb-1">Edit link</h1>
|
||||
<p class="text-slate-500 font-mono" x-text="`4ev.link/${slug}`"></p>
|
||||
<p class="text-slate-500 font-mono" x-text="`4ev.link/${form.slug}`"></p>
|
||||
</div>
|
||||
<button @click="$dispatch('link-modified')" class="text-sm font-semibold text-slate-500 hover:text-slate-900">← Back to create</button>
|
||||
<button @click="view = 'create'" class="text-sm font-semibold text-slate-500 hover:text-slate-900">← Back</button>
|
||||
</div>
|
||||
<template x-if="result">
|
||||
<template x-if="result.message">
|
||||
<div class="bg-green-100 border border-green-300 text-green-800 p-3.5 rounded-md mb-6" role="alert">
|
||||
<p class="font-semibold" x-text="result"></p>
|
||||
<p class="font-semibold" x-text="result.message"></p>
|
||||
</div>
|
||||
</template>
|
||||
<form @submit.prevent="update" class="space-y-4">
|
||||
<form @submit.prevent="updateLink" class="space-y-4">
|
||||
<div>
|
||||
<label for="editDestinationUrl" class="block text-sm font-medium text-slate-700 mb-1">Destination URL</label>
|
||||
<input x-model="destination_url" type="text" id="editDestinationUrl" 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">
|
||||
<input x-model="form.destination_url" type="text" id="editDestinationUrl" 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>
|
||||
<p x-text="error" x-show="error" class="text-rose-500 text-sm h-5 -mt-2 text-center"></p>
|
||||
<div class="flex gap-4">
|
||||
<button type="button" @click="destroy" :disabled="loading" class="w-full py-3 font-semibold rounded-lg text-rose-600 bg-rose-100 hover:bg-rose-200 transition-colors flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<button type="button" @click="deleteLink" :disabled="loading" class="w-full py-3 font-semibold rounded-lg text-rose-600 bg-rose-100 hover:bg-rose-200 transition-colors flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-show="!loading">Delete</span>
|
||||
<i x-show="loading" data-lucide="loader-2" class="animate-spin w-6 h-6"></i>
|
||||
</button>
|
||||
@@ -141,10 +139,8 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function dashboard(){return{sidebarOpen:!1,view:"create",links:[],linksLoading:!0,linksError:"",editingLink:null,user:localStorage.getItem("username"),init(){this.fetchLinks()},async fetchLinks(){this.linksLoading=!0,this.linksError="";try{const t=localStorage.getItem("username"),s=localStorage.getItem("pass_hash");if(!t||!s)throw new Error("Auth error.");const i=await fetch("/api/links/list",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:t,pass_hash:s})});if(!i.ok)throw new Error(await i.text()||"Failed to fetch links.");this.links=await i.json()}catch(t){this.linksError=t.message}finally{this.linksLoading=!1,this.$nextTick(()=>lucide.createIcons())}},edit(t){this.editingLink=t,this.view="edit",this.sidebarOpen=!1}}}
|
||||
function linkForm(){return{destination_url:"",slug:"",loading:!1,error:"",result:{},copied:!1,async createLink(){this.loading=!0,this.error="",this.result={};const t=grecaptcha.getResponse();if(!t)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("Auth error. Please log in again.");const a=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":t})});if(!a.ok)throw new Error(await a.text()||"Failed to create link.");const i=await a.json(),o=window.location.host;this.result={...i,url:`https://${o}/${i.slug}`},this.destination_url="",this.slug="",this.$dispatch("link-modified"),this.$nextTick(()=>lucide.createIcons())}catch(e){this.error=e.message}finally{this.loading=!1,grecaptcha.reset()}}}}
|
||||
function editForm(i){return{slug:i.slug,destination_url:i.destination,loading:!1,error:"",result:"",async update(){if(this.destination_url===i.destination)return this.error="Destination is unchanged.";this.loading=!0,this.error="",this.result="";const t=grecaptcha.getResponse();if(!t)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("Auth error.");const a=await fetch("/api/links/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({slug:this.slug,destination_url:this.destination_url,username:e,pass_hash:s,"g-recaptcha-response":t})});if(!a.ok)throw new Error(await a.text()||"Failed to update link.");this.result="Successfully updated link.",setTimeout(()=>this.$dispatch("link-modified"),1500)}catch(e){this.error=e.message}finally{this.loading=!1,grecaptcha.reset()}},async destroy(){if(!confirm(`Are you sure you want to delete 4ev.link/${this.slug}? This is permanent.`))return;this.loading=!0,this.error="",this.result="";const t=grecaptcha.getResponse();if(!t)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("Auth error.");const a=await fetch("/api/links/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({slug:this.slug,username:e,pass_hash:s,"g-recaptcha-response":t})});if(!a.ok)throw new Error(await a.text()||"Failed to delete link.");this.result="Successfully deleted link.",setTimeout(()=>this.$dispatch("link-modified"),1500)}catch(e){this.error=e.message}finally{this.loading=!1,grecaptcha.reset()}}}}
|
||||
lucide.createIcons();
|
||||
function dashboard(){return{sidebarOpen:!1,view:"create",links:[],linksLoading:!0,linksError:"",user:localStorage.getItem("username"),loading:!1,error:"",result:{},form:{destination_url:"",slug:"",original_destination:""},async fetchLinks(){this.linksLoading=!0,this.linksError="";try{const t=localStorage.getItem("username"),s=localStorage.getItem("pass_hash");if(!t||!s)throw new Error("Auth error.");const i=await fetch("/api/links/list",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:t,pass_hash:s})});if(!i.ok)throw new Error(await i.text()||"Failed to fetch links.");this.links=await i.json()}catch(t){this.linksError=t.message}finally{this.linksLoading=!1,this.$nextTick(()=>lucide.createIcons())}},edit(t){this.view="edit",this.form.slug=t.slug,this.form.destination_url=t.destination,this.form.original_destination=t.destination,this.sidebarOpen=!1,this.error="",this.result={}},async createLink(){this.loading=!0,this.error="",this.result={};const t=grecaptcha.getResponse();if(!t)return this.error="Complete CAPTCHA",this.loading=!1;try{const e=localStorage.getItem("username"),s=localStorage.getItem("pass_hash");if(!e||!s)throw new Error("Auth error.");const a=await fetch("/api/links/create",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({destination_url:this.form.destination_url,slug:this.form.slug||null,username:e,pass_hash:s,"g-recaptcha-response":t})});if(!a.ok)throw new Error(await a.text()||"Failed to create.");const i=await a.json(),o=window.location.host;this.result={...i,url:`https://${o}/${i.slug}`},this.form.destination_url="",this.form.slug="",this.fetchLinks()}catch(e){this.error=e.message}finally{this.loading=!1,grecaptcha.reset()}},async updateLink(){if(this.form.destination_url===this.form.original_destination)return this.error="Destination unchanged.";this.loading=!0,this.error="",this.result={};const t=grecaptcha.getResponse();if(!t)return this.error="Complete CAPTCHA",this.loading=!1;try{const e=localStorage.getItem("username"),s=localStorage.getItem("pass_hash");if(!e||!s)throw new Error("Auth error.");const a=await fetch("/api/links/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({slug:this.form.slug,destination_url:this.form.destination_url,username:e,pass_hash:s,"g-recaptcha-response":t})});if(!a.ok)throw new Error(await a.text()||"Failed to update.");this.result={message:"Update successful."},setTimeout(()=>{this.view="create",this.fetchLinks()},1500)}catch(e){this.error=e.message}finally{this.loading=!1,grecaptcha.reset()}},async deleteLink(){if(!confirm(`Delete 4ev.link/${this.form.slug}? This is permanent.`))return;this.loading=!0,this.error="",this.result={};const t=grecaptcha.getResponse();if(!t)return this.error="Complete CAPTCHA",this.loading=!1;try{const e=localStorage.getItem("username"),s=localStorage.getItem("pass_hash");if(!e||!s)throw new Error("Auth error.");const a=await fetch("/api/links/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({slug:this.form.slug,username:e,pass_hash:s,"g-recaptcha-response":t})});if(!a.ok)throw new Error(await a.text()||"Failed to delete.");this.result={message:"Delete successful."},setTimeout(()=>{this.view="create",this.fetchLinks()},1500)}catch(e){this.error=e.message}finally{this.loading=!1,grecaptcha.reset()}}}}
|
||||
lucide.createIcons();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user