bsc

Comprehensive codebase and cou...
Log | Files | Refs | README | LICENSE

ExternalLinkWarning.vue (5789B)


      1 <script setup lang="ts">
      2 import { ref, onMounted, onUnmounted } from 'vue'
      3 
      4 const isVisible = ref(false)
      5 const pendingUrl = ref('')
      6 const copied = ref(false)
      7 
      8 const safeDomains = [
      9   'amit.is-a.dev',
     10   'github.com',
     11   'raw.githubusercontent.com',
     12   'raw.usercontent.amit.is-a.dev'
     13 ]
     14 
     15 const reset = () => {
     16   isVisible.value = false
     17   pendingUrl.value = ''
     18   copied.value = false
     19 }
     20 
     21 const handleGlobalClick = (e: MouseEvent) => {
     22   const target = (e.target as HTMLElement).closest('a')
     23   if (!target || !target.href) return
     24   try {
     25     const url = new URL(target.href)
     26     if (url.protocol !== 'http:' && url.protocol !== 'https:') return
     27     const isExternal = url.hostname !== window.location.hostname
     28     const isSafe = safeDomains.some(domain => url.hostname.endsWith(domain))
     29     if (isExternal && !isSafe) {
     30       if (e.ctrlKey || e.metaKey || e.shiftKey || e.button !== 0) return
     31       e.preventDefault()
     32       pendingUrl.value = target.href
     33       isVisible.value = true
     34     }
     35   } catch {}
     36 }
     37 
     38 onMounted(() => document.addEventListener('click', handleGlobalClick))
     39 onUnmounted(() => document.removeEventListener('click', handleGlobalClick))
     40 
     41 const openLink = () => {
     42   window.open(pendingUrl.value, '_blank', 'noopener,noreferrer')
     43   reset()
     44 }
     45 
     46 const copyUrl = async () => {
     47   await navigator.clipboard.writeText(pendingUrl.value)
     48   copied.value = true
     49   setTimeout(() => { copied.value = false }, 1500)
     50 }
     51 </script>
     52 
     53 <template>
     54   <Teleport to="body">
     55     <Transition name="fade">
     56       <div v-if="isVisible" class="overlay" @click="reset"></div>
     57     </Transition>
     58 
     59     <Transition name="sheet">
     60       <div v-if="isVisible" class="sheet" role="dialog" aria-modal="true">
     61         <div class="handle"></div>
     62 
     63         <p class="warning">
     64           This link will take you outside the website. I am not responsible for the content of external sites.
     65         </p>
     66 
     67         <div class="url-box">{{ pendingUrl }}</div>
     68 
     69         <div class="actions">
     70           <button class="btn btn-copy" @click="copyUrl">
     71             <svg v-if="!copied" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
     72               <rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
     73               <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
     74             </svg>
     75             <svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
     76               <polyline points="20 6 9 17 4 12"/>
     77             </svg>
     78             {{ copied ? 'Copied' : 'Copy Link' }}
     79           </button>
     80 
     81           <button class="btn btn-open" @click="openLink">
     82             <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
     83               <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
     84               <polyline points="15 3 21 3 21 9"/>
     85               <line x1="10" y1="14" x2="21" y2="3"/>
     86             </svg>
     87             Open Link
     88           </button>
     89         </div>
     90       </div>
     91     </Transition>
     92   </Teleport>
     93 </template>
     94 
     95 <style scoped>
     96 .overlay {
     97   position: fixed;
     98   inset: 0;
     99   background: rgba(0, 0, 0, 0.35);
    100   backdrop-filter: blur(6px);
    101   -webkit-backdrop-filter: blur(6px);
    102   z-index: 99998;
    103 }
    104 
    105 .sheet {
    106   position: fixed;
    107   left: 0;
    108   right: 0;
    109   bottom: 0;
    110   z-index: 99999;
    111   width: 100%;
    112   max-width: 420px;
    113   margin: 0 auto;
    114   background: var(--vp-c-bg);
    115   border: 1px solid rgba(255, 255, 255, 0.18);
    116   border-radius: 16px 16px 0 0;
    117   padding: 10px 20px calc(20px + env(safe-area-inset-bottom, 0px));
    118   box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15);
    119 }
    120 
    121 .handle {
    122   width: 36px;
    123   height: 4px;
    124   border-radius: 2px;
    125   background: var(--vp-c-divider);
    126   margin: 0 auto 16px;
    127 }
    128 
    129 .warning {
    130   font-size: 0.875rem;
    131   line-height: 1.5;
    132   color: var(--vp-c-text-2);
    133   margin: 0 0 14px;
    134   text-align: center;
    135 }
    136 
    137 .url-box {
    138   font-size: 0.8rem;
    139   font-family: var(--vp-font-family-mono);
    140   color: var(--vp-c-text-1);
    141   background: var(--vp-c-bg-alt);
    142   border-radius: 8px;
    143   padding: 10px 12px;
    144   margin-bottom: 16px;
    145   word-break: break-all;
    146   line-height: 1.4;
    147 }
    148 
    149 .actions {
    150   display: flex;
    151   gap: 10px;
    152 }
    153 
    154 .btn {
    155   flex: 1;
    156   display: flex;
    157   align-items: center;
    158   justify-content: center;
    159   gap: 6px;
    160   padding: 11px 12px;
    161   border-radius: 8px;
    162   border: none;
    163   font-size: 0.875rem;
    164   font-weight: 500;
    165   cursor: pointer;
    166   transition: opacity 0.15s;
    167 }
    168 
    169 .btn:active {
    170   opacity: 0.75;
    171 }
    172 
    173 .btn-copy {
    174   background: var(--vp-c-bg-alt);
    175   color: var(--vp-c-text-1);
    176 }
    177 
    178 .btn-open {
    179   background: var(--vp-c-brand-1);
    180   color: #fff;
    181 }
    182 
    183 /* Larger screens: centered rounded card instead of a bottom sheet */
    184 @media (min-width: 640px) {
    185   .sheet {
    186     left: 50%;
    187     right: auto;
    188     bottom: auto;
    189     top: 50%;
    190     transform: translate(-50%, -50%);
    191     width: 100%;
    192     max-width: 400px;
    193     border-radius: 16px;
    194     border: 1px solid rgba(255, 255, 255, 0.18);
    195     padding: 24px;
    196     box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
    197   }
    198 
    199   .handle {
    200     display: none;
    201   }
    202 
    203   .warning {
    204     text-align: left;
    205   }
    206 
    207   .sheet-enter-active,
    208   .sheet-leave-active {
    209     transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
    210   }
    211   .sheet-enter-from,
    212   .sheet-leave-to {
    213     opacity: 0;
    214     transform: translate(-50%, -46%);
    215   }
    216 }
    217 
    218 /* Transitions */
    219 .fade-enter-active,
    220 .fade-leave-active {
    221   transition: opacity 0.2s ease;
    222 }
    223 .fade-enter-from,
    224 .fade-leave-to {
    225   opacity: 0;
    226 }
    227 
    228 .sheet-enter-active {
    229   transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
    230 }
    231 .sheet-leave-active {
    232   transition: transform 0.2s ease;
    233 }
    234 .sheet-enter-from,
    235 .sheet-leave-to {
    236   transform: translateY(100%);
    237 }
    238 </style>
© notamitgamer • Site Built: 2026-07-21 13:58:23 UTC • git-mirror commit: 1037f62 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror