11<script setup>
22import { ref , reactive , onMounted } from ' vue' ;
3- import { getParamsFromURL } from ' ../utils/urlParams' ;
3+ import { getParamsFromURL , updateURLParams } from ' ../utils/urlParams' ;
44import { generatePassword } from ' ../utils/password' ;
55import OptionsPanel from ' ./OptionsPanel.vue' ;
66import KeyboardExcluder from ' ./KeyboardExcluder.vue' ;
@@ -20,6 +20,7 @@ const settings = reactive({
2020const generatedPassword = ref (' ' );
2121const generationError = ref (' ' ); // To display errors from generatePassword
2222const copyStatus = ref (' idle' ); // idle, copying, success, error
23+ const shareUrlStatus = ref (' idle' ); // idle, copying, success, error
2324
2425// --- Logic ---
2526function handleSettingsChanged (newSettings ) {
@@ -93,6 +94,82 @@ async function copyPassword() {
9394 }
9495}
9596
97+ // Function to get sharable URL with current settings
98+ function getShareableUrl () {
99+ // Build a new URL with the current settings without updating the browser URL
100+ const url = new URL (window .location .href );
101+
102+ // Clear existing parameters
103+ url .search = ' ' ;
104+
105+ // Add parameters for current settings
106+ const params = new URLSearchParams ();
107+
108+ if (settings .length !== 20 ) { // Default is 20
109+ params .set (' len' , settings .length .toString ());
110+ }
111+
112+ if (settings .excludeLowercase ) {
113+ params .set (' exLower' , ' ' );
114+ }
115+
116+ if (settings .excludeNumbers ) {
117+ params .set (' exNum' , ' ' );
118+ }
119+
120+ if (settings .excludeUppercase ) {
121+ params .set (' exUpper' , ' ' );
122+ }
123+
124+ if (settings .excludeSymbols ) {
125+ params .set (' exSym' , ' ' );
126+ }
127+
128+ if (settings .ruleNoLeadingSpecial ) {
129+ params .set (' ruleNoLead' , ' ' );
130+ }
131+
132+ if (settings .excludedChars ) {
133+ params .set (' exc' , encodeURIComponent (settings .excludedChars ));
134+ }
135+
136+ // Set the search portion of the URL
137+ url .search = params .toString ();
138+
139+ return url .href ;
140+ }
141+
142+ // Function to copy shareable URL to clipboard
143+ async function copyShareableUrl () {
144+ const shareableUrl = getShareableUrl ();
145+
146+ if (! navigator .clipboard ) {
147+ shareUrlStatus .value = ' error' ;
148+ return ;
149+ }
150+
151+ shareUrlStatus .value = ' copying' ;
152+ try {
153+ await navigator .clipboard .writeText (shareableUrl);
154+ shareUrlStatus .value = ' success' ;
155+
156+ // Reset status after a short delay
157+ setTimeout (() => {
158+ if (shareUrlStatus .value === ' success' ) {
159+ shareUrlStatus .value = ' idle' ;
160+ }
161+ }, 1500 );
162+ } catch (err) {
163+ console .error (' Failed to copy URL: ' , err);
164+ shareUrlStatus .value = ' error' ;
165+ setTimeout (() => {
166+ if (shareUrlStatus .value === ' error' ) {
167+ shareUrlStatus .value = ' idle' ;
168+ }
169+ }, 2000 );
170+ }
171+ }
172+
96173// --- Lifecycle ---
97174onMounted (() => {
98175 // First, load settings from URL if present
@@ -125,10 +202,14 @@ onMounted(() => {
125202
126203 <!-- Password Display Area -->
127204 <div class =" mb-8 relative" >
128- <div v-if =" generatedPassword"
129- class =" p-5 pr-28 bg-gradient-to-r from-slate-100 to-white rounded-xl font-mono text-base sm:text-lg break-all text-center shadow-inner text-slate-800 border border-slate-200"
205+ <div v-if =" generatedPassword" @click = " copyPassword "
206+ class =" p-5 pr-28 bg-gradient-to-r from-slate-100 to-white rounded-xl font-mono text-base sm:text-lg break-all text-center shadow-inner text-slate-800 border border-slate-200 cursor-pointer hover:bg-slate-50 transition-colors duration-200 "
130207 aria-live =" polite" >
131208 {{ generatedPassword }}
209+ <div v-if =" copyStatus === 'success'"
210+ class =" absolute inset-0 flex items-center justify-center bg-white/80 rounded-xl" >
211+ <span class =" text-green-600 font-medium text-base sm:text-lg" >Copied to clipboard!</span >
212+ </div >
132213 </div >
133214
134215 <!-- Error Display -->
@@ -157,32 +238,35 @@ onMounted(() => {
157238 </svg >
158239 </button >
159240
160- <!-- Copy Button -->
161- <button @click = " copyPassword " :disabled = " copyStatus === 'copying' || copyStatus === 'success' "
162- class =" p-2.5 rounded-full transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-purple -500 group"
241+ <!-- Share URL Button -->
242+ <button @click.stop = " copyShareableUrl "
243+ class =" p-2.5 rounded-full transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-indigo -500 group"
163244 :class =" {
164- 'text-slate-500 hover:bg-slate-100 hover:text-purple -600': copyStatus === 'idle',
165- 'text-green-600 bg-green-50 scale-110': copyStatus === 'success',
166- 'text-red-600 bg-red-50': copyStatus === 'error',
167- 'text-slate-400 cursor-default': copyStatus === 'copying'
168- }" aria-label =" Copy password to clipboard " >
245+ 'text-slate-500 hover:bg-slate-100 hover:text-indigo -600': shareUrlStatus === 'idle',
246+ 'text-green-600 bg-green-50 scale-110': shareUrlStatus === 'success',
247+ 'text-red-600 bg-red-50': shareUrlStatus === 'error',
248+ 'text-slate-400 cursor-default': shareUrlStatus === 'copying'
249+ }" aria-label =" Copy shareable URL " >
169250 <!-- Tooltip -->
170251 <span
171252 class =" absolute bottom-full right-0 mb-2 hidden group-hover:block group-focus:block bg-slate-800 text-white text-xs rounded-lg py-1.5 px-3 z-10 whitespace-nowrap"
172- v-if =" copyStatus === 'idle' || copyStatus === 'error'" >
173- {{ copyStatus === 'error' ? 'Failed to copy' : 'Copy to clipboard ' }}
253+ v-if =" shareUrlStatus === 'idle' || shareUrlStatus === 'error'" >
254+ {{ shareUrlStatus === 'error' ? 'Failed to copy URL ' : 'Copy shareable URL ' }}
174255 </span >
175- <span v-if =" copyStatus === 'success'" aria-live =" polite" class =" text-sm font-medium" >Copied!</span >
256+ <span v-if =" shareUrlStatus === 'success'" aria-live =" polite" class =" text-sm font-medium" >URL Copied!</span >
176257 <svg v-else xmlns =" http://www.w3.org/2000/svg" class =" h-5 w-5 sm:h-6 sm:w-6" fill =" none" viewBox =" 0 0 24 24"
177258 stroke =" currentColor" stroke-width =" 1.5" >
178259 <path stroke-linecap =" round" stroke-linejoin =" round"
179- d =" M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z " />
260+ d =" M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z " />
180261 </svg >
181262 </button >
182263 </div >
183264 <p v-if =" copyStatus === 'error'" class =" text-xs text-red-600 mt-1 text-right" aria-live =" assertive" >
184265 Failed to copy!
185266 </p >
267+ <p v-if =" shareUrlStatus === 'error'" class =" text-xs text-red-600 mt-1 text-right" aria-live =" assertive" >
268+ Failed to copy URL!
269+ </p >
186270 </div >
187271
188272 <!-- Options Panel Integration -->
@@ -202,11 +286,6 @@ onMounted(() => {
202286 <NetworkMonitor />
203287 </div >
204288
205- <!-- Footer/Made with -->
206- <div class =" mt-10 sm:mt-12 text-center text-slate-500 text-sm" >
207- <p class =" font-medium" >Built with Vue.js & Tailwind CSS</p >
208- </div >
209-
210289 </div >
211290 </div >
212291</template >
0 commit comments