Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AstraPay</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Erica+One&display=swap" rel="stylesheet"> | |
| <link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet"> | |
| <script src="https://cdn.quilljs.com/1.3.6/quill.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/turndown@7.1.3/dist/turndown.js"></script> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| min-height: 100vh; | |
| background: linear-gradient(180deg, | |
| #0f0514 0%, | |
| #1a0a1a 15%, | |
| #2d1a3d 35%, | |
| #4a2a5a 50%, | |
| #3d2a4a 60%, | |
| #2d1a3d 70%, | |
| #1a0a1a 80%, | |
| #0f0514 85%, | |
| #000000 100% | |
| ); | |
| background-attachment: fixed; | |
| font-family: Arial, sans-serif; | |
| } | |
| .main-content { | |
| margin-top: 0; | |
| height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .container { | |
| text-align: center; | |
| } | |
| .title { | |
| color: #ffffff; | |
| font-size: 4rem; | |
| font-weight: normal; | |
| font-family: 'Erica One', cursive; | |
| text-shadow: 2px 2px 4px rgba(255, 255, 255, 0.3); | |
| margin-bottom: 2rem; | |
| letter-spacing: 0.02em; | |
| } | |
| .button { | |
| background-color: #ffffff; | |
| color: #000000; | |
| border: none; | |
| padding: 1rem 2rem; | |
| font-size: 1.2rem; | |
| font-weight: bold; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| text-decoration: none; | |
| display: inline-block; | |
| } | |
| .button:hover { | |
| background-color: #f0f0f0; | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 8px rgba(255, 255, 255, 0.2); | |
| } | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| z-index: 50; | |
| left: 0; | |
| top: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.8); | |
| backdrop-filter: blur(4px); | |
| -webkit-backdrop-filter: blur(4px); | |
| animation: fadeIn 0.15s ease-out; | |
| } | |
| .modal.show { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| } | |
| to { | |
| opacity: 1; | |
| } | |
| } | |
| @keyframes slideIn { | |
| from { | |
| opacity: 0; | |
| transform: scale(0.96); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| } | |
| .modal-content { | |
| background-color: #1a1a1a; | |
| position: relative; | |
| padding: 0; | |
| border-radius: 12px; | |
| width: 90%; | |
| max-width: 500px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); | |
| animation: slideIn 0.2s ease-out; | |
| display: flex; | |
| flex-direction: column; | |
| max-height: 90vh; | |
| overflow: hidden; | |
| } | |
| .modal-header { | |
| color: #ffffff; | |
| padding: 1.5rem 1.5rem 0 1.5rem; | |
| margin-bottom: 1.5rem; | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| letter-spacing: -0.01em; | |
| line-height: 1.4; | |
| } | |
| .modal-close { | |
| position: absolute; | |
| right: 1rem; | |
| top: 1rem; | |
| background: transparent; | |
| border: none; | |
| color: rgba(255, 255, 255, 0.7); | |
| cursor: pointer; | |
| padding: 0.5rem; | |
| border-radius: 6px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s ease; | |
| width: 32px; | |
| height: 32px; | |
| } | |
| .modal-close:hover { | |
| background-color: rgba(255, 255, 255, 0.1); | |
| color: rgba(255, 255, 255, 1); | |
| } | |
| .modal-close:focus { | |
| outline: 2px solid rgba(255, 255, 255, 0.5); | |
| outline-offset: 2px; | |
| } | |
| .modal-close-icon { | |
| width: 16px; | |
| height: 16px; | |
| stroke: currentColor; | |
| stroke-width: 2; | |
| fill: none; | |
| } | |
| .modal-body { | |
| padding: 0 1.5rem 1.5rem 1.5rem; | |
| overflow-y: auto; | |
| } | |
| .form-group { | |
| margin-bottom: 1.25rem; | |
| } | |
| .form-label { | |
| display: block; | |
| color: rgba(255, 255, 255, 0.9); | |
| margin-bottom: 0.5rem; | |
| font-weight: 500; | |
| font-size: 0.875rem; | |
| } | |
| .form-input { | |
| width: 100%; | |
| padding: 0.625rem 0.875rem; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 6px; | |
| background-color: rgba(255, 255, 255, 0.05); | |
| color: #ffffff; | |
| font-size: 0.9375rem; | |
| transition: all 0.2s ease; | |
| box-sizing: border-box; | |
| } | |
| .form-input:focus { | |
| outline: none; | |
| border-color: rgba(255, 255, 255, 0.3); | |
| background-color: rgba(255, 255, 255, 0.08); | |
| } | |
| .form-input::placeholder { | |
| color: rgba(255, 255, 255, 0.4); | |
| } | |
| .form-input-email { | |
| width: 100%; | |
| padding: 0.625rem 0.875rem; | |
| border: 1px solid rgba(255, 255, 255, 0.15); | |
| border-radius: 8px; | |
| background-color: rgba(255, 255, 255, 0.06); | |
| color: #ffffff; | |
| font-size: 0.9375rem; | |
| transition: all 0.3s ease; | |
| box-sizing: border-box; | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; | |
| } | |
| .form-input-email:focus { | |
| outline: none; | |
| border-color: rgba(255, 255, 255, 0.4); | |
| background-color: rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1); | |
| } | |
| .form-input-email::placeholder { | |
| color: rgba(255, 255, 255, 0.5); | |
| font-style: italic; | |
| } | |
| .form-input-email:hover:not(:focus) { | |
| border-color: rgba(255, 255, 255, 0.25); | |
| background-color: rgba(255, 255, 255, 0.08); | |
| } | |
| .form-textarea { | |
| width: 100%; | |
| padding: 0.625rem 0.875rem; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 6px; | |
| background-color: rgba(255, 255, 255, 0.05); | |
| color: #ffffff; | |
| font-size: 0.9375rem; | |
| min-height: 100px; | |
| resize: vertical; | |
| transition: all 0.2s ease; | |
| box-sizing: border-box; | |
| font-family: inherit; | |
| } | |
| .form-textarea:focus { | |
| outline: none; | |
| border-color: rgba(255, 255, 255, 0.3); | |
| background-color: rgba(255, 255, 255, 0.08); | |
| } | |
| .form-textarea::placeholder { | |
| color: rgba(255, 255, 255, 0.4); | |
| } | |
| #description-editor-container { | |
| margin-top: 0.5rem; | |
| } | |
| #description-editor-container .ql-container { | |
| background-color: rgba(255, 255, 255, 0.05); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 0 0 6px 6px; | |
| color: #ffffff; | |
| font-family: inherit; | |
| font-size: 0.9375rem; | |
| } | |
| #description-editor-container .ql-toolbar { | |
| background-color: rgba(255, 255, 255, 0.05); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 6px 6px 0 0; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| #description-editor-container .ql-toolbar .ql-stroke { | |
| stroke: rgba(255, 255, 255, 0.7); | |
| } | |
| #description-editor-container .ql-toolbar .ql-fill { | |
| fill: rgba(255, 255, 255, 0.7); | |
| } | |
| #description-editor-container .ql-toolbar button:hover, | |
| #description-editor-container .ql-toolbar button.ql-active { | |
| background-color: rgba(255, 255, 255, 0.1); | |
| } | |
| #description-editor-container .ql-toolbar button:hover .ql-stroke, | |
| #description-editor-container .ql-toolbar button.ql-active .ql-stroke { | |
| stroke: #ffffff; | |
| } | |
| #description-editor-container .ql-toolbar button:hover .ql-fill, | |
| #description-editor-container .ql-toolbar button.ql-active .ql-fill { | |
| fill: #ffffff; | |
| } | |
| #description-editor-container .ql-editor { | |
| color: #ffffff; | |
| min-height: 150px; | |
| } | |
| #description-editor-container .ql-editor.ql-blank::before { | |
| color: rgba(255, 255, 255, 0.4); | |
| font-style: italic; | |
| } | |
| #description-editor-container .ql-snow .ql-picker { | |
| color: rgba(255, 255, 255, 0.7); | |
| } | |
| #description-editor-container .ql-snow .ql-picker-options { | |
| background-color: rgba(30, 30, 30, 0.95); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| #description-editor-container .ql-snow .ql-picker-item { | |
| color: rgba(255, 255, 255, 0.7); | |
| } | |
| #description-editor-container .ql-snow .ql-picker-item:hover { | |
| background-color: rgba(255, 255, 255, 0.1); | |
| color: #ffffff; | |
| } | |
| #description-editor-container .ql-snow .ql-stroke { | |
| stroke: rgba(255, 255, 255, 0.7); | |
| } | |
| #description-editor-container .ql-snow .ql-fill { | |
| fill: rgba(255, 255, 255, 0.7); | |
| } | |
| .word-counter { | |
| margin-top: 0.25rem; | |
| font-size: 0.75rem; | |
| color: rgba(255, 255, 255, 0.6); | |
| text-align: right; | |
| font-weight: 400; | |
| } | |
| .modal-buttons { | |
| display: flex; | |
| gap: 0.75rem; | |
| justify-content: flex-end; | |
| margin-top: 1.5rem; | |
| padding-top: 1.5rem; | |
| border-top: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .btn-secondary { | |
| background-color: transparent; | |
| color: rgba(255, 255, 255, 0.9); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| padding: 0.625rem 1.25rem; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-size: 0.9375rem; | |
| font-weight: 500; | |
| transition: all 0.2s ease; | |
| } | |
| .btn-secondary:hover { | |
| background-color: rgba(255, 255, 255, 0.1); | |
| border-color: rgba(255, 255, 255, 0.3); | |
| } | |
| .button { | |
| background-color: #ffffff; | |
| color: #000000; | |
| border: none; | |
| padding: 0.625rem 1.25rem; | |
| font-size: 0.9375rem; | |
| font-weight: 500; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| text-decoration: none; | |
| display: inline-block; | |
| } | |
| .button:hover { | |
| background-color: #f0f0f0; | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 12px rgba(255, 255, 255, 0.15); | |
| } | |
| .top-banner { | |
| background: rgba(100, 100, 255, 0.15); | |
| border: 1px solid rgba(100, 100, 255, 0.3); | |
| border-radius: 8px; | |
| padding: 0.75rem 1.5rem; | |
| margin-bottom: 2rem; | |
| color: #6464ff; | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| text-align: center; | |
| max-width: 500px; | |
| margin-left: auto; | |
| margin-right: auto; | |
| } | |
| .top-page-banner { | |
| background: rgba(100, 100, 255, 0.15); | |
| border-bottom: 1px solid rgba(100, 100, 255, 0.3); | |
| padding: 0.75rem 1.5rem; | |
| color: #6464ff; | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| text-align: center; | |
| width: 100%; | |
| } | |
| .banner { | |
| background: rgba(255, 193, 7, 0.15); | |
| border: 1px solid rgba(255, 193, 7, 0.3); | |
| border-radius: 8px; | |
| padding: 0.75rem 1.5rem; | |
| margin-bottom: 2rem; | |
| color: #ffc107; | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| text-align: center; | |
| max-width: 500px; | |
| margin-left: auto; | |
| margin-right: auto; | |
| } | |
| .toast-container { | |
| position: fixed; | |
| bottom: 1.5rem; | |
| right: 1.5rem; | |
| z-index: 100; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| pointer-events: none; | |
| } | |
| .toast { | |
| background-color: #1a1a1a; | |
| color: #ffffff; | |
| padding: 0.875rem 1rem; | |
| border-radius: 8px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); | |
| min-width: 300px; | |
| max-width: 400px; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| pointer-events: auto; | |
| animation: toastSlideIn 0.3s ease-out; | |
| font-size: 0.9375rem; | |
| } | |
| .toast.error { | |
| border-color: rgba(239, 68, 68, 0.3); | |
| background-color: rgba(239, 68, 68, 0.1); | |
| } | |
| .toast.success { | |
| border-color: rgba(34, 197, 94, 0.3); | |
| background-color: rgba(34, 197, 94, 0.1); | |
| } | |
| @keyframes toastSlideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateX(100%); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| } | |
| @keyframes toastSlideOut { | |
| from { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| to { | |
| opacity: 0; | |
| transform: translateX(100%); | |
| } | |
| } | |
| .toast.hiding { | |
| animation: toastSlideOut 0.2s ease-in forwards; | |
| } | |
| .toast-icon { | |
| flex-shrink: 0; | |
| width: 20px; | |
| height: 20px; | |
| } | |
| .toast-message { | |
| flex: 1; | |
| line-height: 1.5; | |
| } | |
| .toast-close { | |
| background: transparent; | |
| border: none; | |
| color: rgba(255, 255, 255, 0.6); | |
| cursor: pointer; | |
| padding: 0.25rem; | |
| border-radius: 4px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s ease; | |
| flex-shrink: 0; | |
| } | |
| .toast-close:hover { | |
| background-color: rgba(255, 255, 255, 0.1); | |
| color: rgba(255, 255, 255, 1); | |
| } | |
| .toast-close-icon { | |
| width: 16px; | |
| height: 16px; | |
| stroke: currentColor; | |
| stroke-width: 2; | |
| fill: none; | |
| } | |
| .bottom-notification { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| z-index: 1000; | |
| max-width: 300px; | |
| opacity: 1; | |
| transition: opacity 0.3s ease; | |
| } | |
| .bottom-notification.fade-out { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .notification-content { | |
| background: rgba(0, 0, 0, 0.8); | |
| color: #ffffff; | |
| padding: 12px 16px; | |
| border-radius: 8px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); | |
| font-size: 0.9rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 10px; | |
| } | |
| .close-btn { | |
| background: none; | |
| border: none; | |
| color: #ffffff; | |
| cursor: pointer; | |
| padding: 2px; | |
| border-radius: 4px; | |
| transition: background-color 0.2s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| } | |
| .close-btn:hover { | |
| background-color: rgba(255, 255, 255, 0.1); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="top-page-banner"> | |
| To send astras to AstraPay, please friend astrapay@astranova.org, your friend request will be accepted instantly. | |
| </div> | |
| <div class="main-content"> | |
| <div class="container"> | |
| <h1 class="title">AstraPay</h1> | |
| <div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;"> | |
| <button class="button" onclick="openModal()" style="display: flex; align-items: center; gap: 0.5rem;"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect> | |
| <line x1="1" y1="10" x2="23" y2="10"></line> | |
| </svg> | |
| Create Payment Link | |
| </button> | |
| <button class="button" onclick="openClaimModal()" style="background-color: rgba(255, 255, 255, 0.9); display: flex; align-items: center; gap: 0.5rem;"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="20 12 20 22 4 22 4 12"></polyline> | |
| <rect x="2" y="7" width="20" height="5"></rect> | |
| <line x1="12" y1="22" x2="12" y2="7"></line> | |
| <path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"></path> | |
| <path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"></path> | |
| </svg> | |
| Create Claim Link | |
| </button> | |
| <button class="button" onclick="openEnterIdModal()" style="background-color: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.3); color: #ffffff; display: flex; align-items: center; gap: 0.5rem;"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M21 21l-6-6m2-5a7 7 0 1 1-14 0 7 7 0 0 1 14 0z"></path> | |
| </svg> | |
| Enter ID | |
| </button> | |
| </div> | |
| </div> | |
| <div id="paymentModal" class="modal"> | |
| <div class="modal-content"> | |
| <button class="modal-close" onclick="closeModal()" aria-label="Close"> | |
| <svg class="modal-close-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
| <path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/> | |
| </svg> | |
| </button> | |
| <div class="modal-header">Create Payment Link</div> | |
| <div class="modal-body"> | |
| <div class="form-group"> | |
| <label class="form-label" for="astras">Number of Astras</label> | |
| <input type="number" id="astras" class="form-input" min="1" max="10000" placeholder="Enter number of astras" required oninput="validateAstrasAmount(this)"> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="description">Description</label> | |
| <textarea id="description" class="form-textarea" placeholder="Enter payment description" required></textarea> | |
| <div id="description-editor-container"></div> | |
| <div id="word-counter" class="word-counter">0 words</div> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="recipient-email">Your AstraNova Email</label> | |
| <input type="email" id="recipient-email" class="form-input-email" placeholder="Enter your AstraNova email" required> | |
| </div> | |
| <div class="modal-buttons"> | |
| <button class="btn-secondary" onclick="closeModal()" style="display: flex; align-items: center; gap: 0.5rem;"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <line x1="18" y1="6" x2="6" y2="18"></line> | |
| <line x1="6" y1="6" x2="18" y2="18"></line> | |
| </svg> | |
| Cancel | |
| </button> | |
| <button class="button" onclick="createPaymentLink()" style="display: flex; align-items: center; gap: 0.5rem;"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <line x1="12" y1="5" x2="12" y2="19"></line> | |
| <line x1="5" y1="12" x2="19" y2="12"></line> | |
| </svg> | |
| Create Link | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="claimModal" class="modal"> | |
| <div class="modal-content"> | |
| <button class="modal-close" onclick="closeClaimModal()" aria-label="Close"> | |
| <svg class="modal-close-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
| <path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/> | |
| </svg> | |
| </button> | |
| <div class="modal-header">Create Claim Link</div> | |
| <div class="modal-body"> | |
| <div class="form-group"> | |
| <label class="form-label" for="claim-astras">Number of Astras</label> | |
| <input type="number" id="claim-astras" class="form-input" min="1" max="10000" placeholder="Enter number of astras" required oninput="validateClaimAstrasAmount(this)"> | |
| <div style="font-size: 0.8rem; color: rgba(255, 255, 255, 0.6); margin-top: 0.5rem;"> | |
| You'll send: <span id="claim-send-amount">-</span> Astras (<span id="claim-fee-percent">1</span>%)<br> | |
| Claimable: <span id="claim-claim-amount">-</span> Astras<br> | |
| Fee: <span id="claim-fee-amount">-</span> Astras | |
| </div> | |
| </div> | |
| <div class="modal-buttons"> | |
| <button class="btn-secondary" onclick="closeClaimModal()" style="display: flex; align-items: center; gap: 0.5rem;"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <line x1="18" y1="6" x2="6" y2="18"></line> | |
| <line x1="6" y1="6" x2="18" y2="18"></line> | |
| </svg> | |
| Cancel | |
| </button> | |
| <button class="button" onclick="createClaimLink()" style="display: flex; align-items: center; gap: 0.5rem;"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="20 12 20 22 4 22 4 12"></polyline> | |
| <rect x="2" y="7" width="20" height="5"></rect> | |
| <line x1="12" y1="22" x2="12" y2="7"></line> | |
| <path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"></path> | |
| <path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"></path> | |
| </svg> | |
| Create Claim Link | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="enterIdModal" class="modal"> | |
| <div class="modal-content"> | |
| <button class="modal-close" onclick="closeEnterIdModal()" aria-label="Close"> | |
| <svg class="modal-close-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
| <path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/> | |
| </svg> | |
| </button> | |
| <div class="modal-header">Enter Link ID</div> | |
| <div class="modal-body"> | |
| <div class="form-group"> | |
| <label class="form-label" for="link-id">Enter Link ID</label> | |
| <input type="text" id="link-id" class="form-input" placeholder="e.g., 550e8400-e29b-41d4-a716-446655440000" maxlength="36" style="font-size: 1rem; text-align: center;" required onkeypress="if(event.key === 'Enter') goToLink()"> | |
| </div> | |
| <div class="modal-buttons"> | |
| <button class="btn-secondary" onclick="closeEnterIdModal()" style="display: flex; align-items: center; gap: 0.5rem;"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <line x1="18" y1="6" x2="6" y2="18"></line> | |
| <line x1="6" y1="6" x2="18" y2="18"></line> | |
| </svg> | |
| Cancel | |
| </button> | |
| <button class="button" onclick="goToLink()" style="display: flex; align-items: center; gap: 0.5rem;"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M5 12h14M12 5l7 7-7 7"></path> | |
| </svg> | |
| Go to Link | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="toastContainer" class="toast-container"></div> | |
| <script> | |
| let descriptionEditor = null; | |
| function initDescriptionEditor() { | |
| if (descriptionEditor) { | |
| const container = document.getElementById('description-editor-container'); | |
| container.innerHTML = ''; | |
| descriptionEditor = null; | |
| } | |
| const textarea = document.getElementById('description'); | |
| textarea.style.display = 'none'; | |
| const container = document.getElementById('description-editor-container'); | |
| container.innerHTML = '<div id="quill-editor"></div>'; | |
| descriptionEditor = new Quill('#quill-editor', { | |
| theme: 'snow', | |
| placeholder: 'Enter payment description', | |
| modules: { | |
| toolbar: [ | |
| [{ 'header': [1, 2, 3, false] }], | |
| ['bold', 'italic', 'underline'] | |
| ] | |
| } | |
| }); | |
| const quillContainer = document.querySelector('#description-editor-container .ql-container'); | |
| if (quillContainer) { | |
| quillContainer.style.backgroundColor = 'rgba(255, 255, 255, 0.05)'; | |
| quillContainer.style.borderColor = 'rgba(255, 255, 255, 0.1)'; | |
| } | |
| function updateWordCount() { | |
| if (!descriptionEditor) return; | |
| const text = descriptionEditor.getText(); | |
| const words = text.trim() === '' ? 0 : text.trim().split(/\s+/).length; | |
| const counter = document.getElementById('word-counter'); | |
| if (counter) { | |
| counter.textContent = words + ' word' + (words !== 1 ? 's' : ''); | |
| } | |
| } | |
| descriptionEditor.on('text-change', updateWordCount); | |
| updateWordCount(); | |
| } | |
| function getQuillMarkdown() { | |
| if (!descriptionEditor) return ''; | |
| try { | |
| const html = descriptionEditor.root.innerHTML; | |
| if (!html || html === '<p><br></p>' || html.trim() === '') return ''; | |
| const text = descriptionEditor.getText(); | |
| if (!text || text.trim() === '') return ''; | |
| if (typeof TurndownService === 'undefined') { | |
| return text.trim(); | |
| } | |
| const turndownService = new TurndownService({ | |
| headingStyle: 'atx', | |
| codeBlockStyle: 'fenced', | |
| emDelimiter: '*', | |
| strongDelimiter: '**', | |
| bulletListMarker: '-', | |
| linkStyle: 'inlined' | |
| }); | |
| turndownService.addRule('underline', { | |
| filter: ['u'], | |
| replacement: function(content) { | |
| return '__' + content + '__'; | |
| } | |
| }); | |
| turndownService.addRule('strikethrough', { | |
| filter: ['del', 's', 'strike'], | |
| replacement: function(content) { | |
| return '~~' + content + '~~'; | |
| } | |
| }); | |
| const markdown = turndownService.turndown(html); | |
| return markdown && typeof markdown === 'string' ? markdown.trim() : text.trim(); | |
| } catch (error) { | |
| console.error('Error converting to markdown:', error); | |
| if (descriptionEditor) { | |
| const text = descriptionEditor.getText(); | |
| return text ? text.trim() : ''; | |
| } | |
| return ''; | |
| } | |
| } | |
| function showToast(message, type = 'error') { | |
| const container = document.getElementById('toastContainer'); | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| const iconSvg = type === 'error' | |
| ? '<svg class="toast-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' | |
| : '<svg class="toast-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'; | |
| toast.innerHTML = ` | |
| ${iconSvg} | |
| <span class="toast-message">${message}</span> | |
| <button class="toast-close" onclick="this.parentElement.remove()" aria-label="Close"> | |
| <svg class="toast-close-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
| <path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/> | |
| </svg> | |
| </button> | |
| `; | |
| container.appendChild(toast); | |
| setTimeout(() => { | |
| toast.classList.add('hiding'); | |
| setTimeout(() => { | |
| if (toast.parentElement) { | |
| toast.remove(); | |
| } | |
| }, 200); | |
| }, 4000); | |
| } | |
| function validateAstrasAmount(input) { | |
| const value = parseInt(input.value); | |
| if (!isNaN(value) && value > 10000) { | |
| input.value = 10000; | |
| showToast('Maximum 10,000 Astras per payment link'); | |
| } | |
| } | |
| function openModal() { | |
| const modal = document.getElementById('paymentModal'); | |
| modal.style.display = 'flex'; | |
| setTimeout(() => { | |
| modal.classList.add('show'); | |
| }, 10); | |
| document.body.style.overflow = 'hidden'; | |
| setTimeout(() => { | |
| initDescriptionEditor(); | |
| }, 50); | |
| } | |
| function closeModal() { | |
| const modal = document.getElementById('paymentModal'); | |
| modal.classList.remove('show'); | |
| setTimeout(() => { | |
| modal.style.display = 'none'; | |
| }, 200); | |
| document.body.style.overflow = ''; | |
| document.getElementById('astras').value = ''; | |
| document.getElementById('recipient-email').value = ''; | |
| if (descriptionEditor) { | |
| const container = document.getElementById('description-editor-container'); | |
| container.innerHTML = ''; | |
| descriptionEditor = null; | |
| } | |
| const counter = document.getElementById('word-counter'); | |
| if (counter) { | |
| counter.textContent = '0 words'; | |
| } | |
| const textarea = document.getElementById('description'); | |
| textarea.value = ''; | |
| textarea.style.display = 'block'; | |
| } | |
| async function createPaymentLink() { | |
| const astras = document.getElementById('astras').value; | |
| let description = ''; | |
| if (descriptionEditor) { | |
| const markdown = getQuillMarkdown(); | |
| description = markdown && typeof markdown === 'string' ? markdown.trim() : ''; | |
| if (!description) { | |
| const text = descriptionEditor.getText(); | |
| description = text ? text.trim() : ''; | |
| } | |
| } else { | |
| description = document.getElementById('description').value.trim(); | |
| } | |
| const recipientEmail = document.getElementById('recipient-email').value.trim(); | |
| if (!astras) { | |
| showToast('Please enter the number of Astras'); | |
| document.getElementById('astras').focus(); | |
| return; | |
| } | |
| const astrasAmount = parseInt(astras); | |
| if (astrasAmount > 10000) { | |
| showToast('Maximum 10,000 Astras per payment link'); | |
| document.getElementById('astras').focus(); | |
| return; | |
| } | |
| if (!description) { | |
| showToast('Please enter a description'); | |
| if (descriptionEditor) { | |
| descriptionEditor.focus(); | |
| } else { | |
| document.getElementById('description').focus(); | |
| } | |
| return; | |
| } | |
| if (!recipientEmail) { | |
| showToast('Please enter your AstraNova email'); | |
| document.getElementById('recipient-email').focus(); | |
| return; | |
| } | |
| try { | |
| const response = await fetch('/create-payment-link', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| amount: astrasAmount, | |
| description: description.trim(), | |
| recipient_email: recipientEmail | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| showToast('Payment link created successfully!', 'success'); | |
| setTimeout(() => { | |
| top.location.href = data.payment_url; | |
| }, 500); | |
| } else { | |
| showToast('Error creating payment link: ' + data.error); | |
| } | |
| } catch (error) { | |
| showToast('Error creating payment link: ' + error.message); | |
| } | |
| } | |
| function openClaimModal() { | |
| const modal = document.getElementById('claimModal'); | |
| modal.style.display = 'flex'; | |
| setTimeout(() => { | |
| modal.classList.add('show'); | |
| }, 10); | |
| document.body.style.overflow = 'hidden'; | |
| } | |
| function closeClaimModal() { | |
| const modal = document.getElementById('claimModal'); | |
| modal.classList.remove('show'); | |
| setTimeout(() => { | |
| modal.style.display = 'none'; | |
| }, 200); | |
| document.body.style.overflow = ''; | |
| document.getElementById('claim-astras').value = ''; | |
| document.getElementById('claim-send-amount').textContent = '-'; | |
| document.getElementById('claim-claim-amount').textContent = '-'; | |
| document.getElementById('claim-fee-percent').textContent = '1'; | |
| document.getElementById('claim-fee-percent-negative').textContent = '-1'; | |
| document.getElementById('claim-fee-amount').textContent = '-'; | |
| } | |
| function validateClaimAstrasAmount(input) { | |
| const value = parseInt(input.value); | |
| if (!isNaN(value) && value > 0) { | |
| const fee = value > 100 ? 2 : 1; | |
| const sendAmount = value + fee; | |
| const claimAmount = value; | |
| document.getElementById('claim-send-amount').textContent = sendAmount; | |
| document.getElementById('claim-claim-amount').textContent = claimAmount; | |
| document.getElementById('claim-fee-percent').textContent = fee; | |
| document.getElementById('claim-fee-percent-negative').textContent = '-' + fee; | |
| document.getElementById('claim-fee-amount').textContent = fee; | |
| } else { | |
| document.getElementById('claim-send-amount').textContent = '-'; | |
| document.getElementById('claim-claim-amount').textContent = '-'; | |
| document.getElementById('claim-fee-percent').textContent = '1'; | |
| document.getElementById('claim-fee-percent-negative').textContent = '-1'; | |
| document.getElementById('claim-fee-amount').textContent = '-'; | |
| } | |
| if (!isNaN(value) && value > 10000) { | |
| input.value = 10000; | |
| showToast('Maximum 10,000 Astras per claim link'); | |
| } | |
| } | |
| async function createClaimLink() { | |
| const astras = document.getElementById('claim-astras').value; | |
| if (!astras) { | |
| showToast('Please enter the number of Astras'); | |
| document.getElementById('claim-astras').focus(); | |
| return; | |
| } | |
| const astrasAmount = parseInt(astras); | |
| if (astrasAmount > 10000) { | |
| showToast('Maximum 10,000 Astras per claim link'); | |
| document.getElementById('claim-astras').focus(); | |
| return; | |
| } | |
| try { | |
| const response = await fetch('/create-claim-link', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| amount: astrasAmount | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| showToast('Claim link created successfully!', 'success'); | |
| setTimeout(() => { | |
| top.location.href = data.claim_url; | |
| }, 500); | |
| } else { | |
| showToast('Error creating claim link: ' + data.error); | |
| } | |
| } catch (error) { | |
| showToast('Error creating claim link: ' + error.message); | |
| } | |
| } | |
| function openEnterIdModal() { | |
| const modal = document.getElementById('enterIdModal'); | |
| modal.style.display = 'flex'; | |
| setTimeout(() => { | |
| modal.classList.add('show'); | |
| }, 10); | |
| document.body.style.overflow = 'hidden'; | |
| document.getElementById('link-id').focus(); | |
| } | |
| function closeEnterIdModal() { | |
| const modal = document.getElementById('enterIdModal'); | |
| modal.classList.remove('show'); | |
| setTimeout(() => { | |
| modal.style.display = 'none'; | |
| }, 200); | |
| document.body.style.overflow = ''; | |
| document.getElementById('link-id').value = ''; | |
| } | |
| async function goToLink() { | |
| const linkId = document.getElementById('link-id').value.trim(); | |
| const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; | |
| if (!linkId || !uuidRegex.test(linkId)) { | |
| showToast('Please enter a valid Link ID'); | |
| document.getElementById('link-id').focus(); | |
| return; | |
| } | |
| try { | |
| const response = await fetch(`/check-link/${linkId}`); | |
| const data = await response.json(); | |
| if (response.ok && data.exists) { | |
| if (data.type === 'payment') { | |
| top.location.href = `/pay/${linkId}`; | |
| } else if (data.type === 'claim') { | |
| top.location.href = `/claim/${linkId}`; | |
| } | |
| } else { | |
| showToast('Link not found. Please check the code and try again.'); | |
| document.getElementById('link-id').focus(); | |
| } | |
| } catch (error) { | |
| showToast('Error checking link: ' + error.message); | |
| } | |
| } | |
| window.onclick = function(event) { | |
| const paymentModal = document.getElementById('paymentModal'); | |
| const claimModal = document.getElementById('claimModal'); | |
| const enterIdModal = document.getElementById('enterIdModal'); | |
| if (event.target === paymentModal) { | |
| closeModal(); | |
| } | |
| if (event.target === claimModal) { | |
| closeClaimModal(); | |
| } | |
| if (event.target === enterIdModal) { | |
| closeEnterIdModal(); | |
| } | |
| } | |
| document.addEventListener('keydown', function(event) { | |
| if (event.key === 'Escape') { | |
| const paymentModal = document.getElementById('paymentModal'); | |
| const claimModal = document.getElementById('claimModal'); | |
| const enterIdModal = document.getElementById('enterIdModal'); | |
| if (paymentModal.classList.contains('show')) { | |
| closeModal(); | |
| } | |
| if (claimModal.classList.contains('show')) { | |
| closeClaimModal(); | |
| } | |
| if (enterIdModal.classList.contains('show')) { | |
| closeEnterIdModal(); | |
| } | |
| } | |
| }); | |
| function closeNotification() { | |
| const notification = document.getElementById('slack-notification'); | |
| notification.classList.add('fade-out'); | |
| setTimeout(() => { | |
| notification.style.display = 'none'; | |
| }, 300); | |
| } | |
| </script> | |
| <div id="slack-notification" class="bottom-notification"> | |
| <div class="notification-content"> | |
| Contact Matthew on Slack if any issues occur. | |
| <button class="close-btn" onclick="closeNotification()"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <line x1="18" y1="6" x2="6" y2="18"></line> | |
| <line x1="6" y1="6" x2="18" y2="18"></line> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </body> | |
| </html> | |