libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

index.html (15629B)


      1 <!DOCTYPE html>
      2 <html lang="en">
      3 <head>
      4     <meta charset="UTF-8">
      5     <meta name="viewport" content="width=device-width, initial-scale=1.0">
      6     <title>LibEuFin EbiSync - File Submission Portal</title>
      7     <link rel="preconnect" href="https://fonts.googleapis.com">
      8     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
      9     <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;600;700&family=Montserrat:wght@300;400;500;600&display=swap" rel="stylesheet">
     10     <style>
     11         :root {
     12             --cream: #FAF8F3;
     13             --charcoal: #2B2B2B;
     14             --rust: #C1503D;
     15             --sage: #8B9A7E;
     16             --gold: #D4AF37;
     17             --shadow: rgba(43, 43, 43, 0.08);
     18         }
     19 
     20         * {
     21             margin: 0;
     22             padding: 0;
     23             box-sizing: border-box;
     24         }
     25 
     26         body {
     27             font-family: 'Montserrat', sans-serif;
     28             background: var(--cream);
     29             color: var(--charcoal);
     30             line-height: 1.7;
     31             overflow-x: hidden;
     32         }
     33 
     34         /* Animated background texture */
     35         body::before {
     36             content: '';
     37             position: fixed;
     38             top: 0;
     39             left: 0;
     40             width: 100%;
     41             height: 100%;
     42             background-image: 
     43                 repeating-linear-gradient(90deg, transparent, transparent 2px, rgba(43, 43, 43, 0.015) 2px, rgba(43, 43, 43, 0.015) 4px),
     44                 repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(43, 43, 43, 0.015) 2px, rgba(43, 43, 43, 0.015) 4px);
     45             pointer-events: none;
     46             z-index: 0;
     47         }
     48 
     49         .container {
     50             max-width: 1200px;
     51             margin: 0 auto;
     52             padding: 0 2rem;
     53             position: relative;
     54             z-index: 1;
     55         }
     56 
     57         header {
     58             padding: 2rem 0 2rem;
     59         }
     60 
     61         h1 {
     62             font-family: 'Cormorant Garamond', serif;
     63             font-size: 4.5rem;
     64             font-weight: 300;
     65             letter-spacing: -0.02em;
     66             line-height: 1.1;
     67             margin-bottom: 1rem;
     68         }
     69 
     70         .subtitle {
     71             font-size: 1rem;
     72             letter-spacing: 0.15em;
     73             text-transform: uppercase;
     74             color: var(--rust);
     75             font-weight: 500;
     76         }
     77 
     78         .version-text {
     79             margin-left: auto;
     80             font-size: 0.75rem;
     81             color: rgba(43, 43, 43, 0.5);
     82             font-family: 'Courier New', monospace;
     83         }
     84 
     85         main {
     86             display: grid;
     87             grid-template-columns: 1fr 1fr;
     88             gap: 3rem;
     89             margin-bottom: 6rem;
     90         }
     91 
     92         section {
     93             background: white;
     94             padding: 1rem 2rem;
     95             border: 1px solid rgba(43, 43, 43, 0.1);
     96             position: relative;
     97             transition: all 0.4s ease;
     98         }
     99 
    100         section::before {
    101             content: '';
    102             position: absolute;
    103             top: 0;
    104             left: 0;
    105             width: 4px;
    106             height: 100%;
    107             background: var(--rust);
    108             transform: scaleY(0);
    109             transition: transform 0.4s ease;
    110         }
    111 
    112         section:hover::before {
    113             transform: scaleY(1);
    114         }
    115 
    116         h2 {
    117             font-family: 'Cormorant Garamond', serif;
    118             font-size: 2.5rem;
    119             font-weight: 400;
    120             margin-bottom: 0.5rem;
    121             letter-spacing: -0.01em;
    122         }
    123 
    124         .order-card {
    125             padding: 1.5rem;
    126             margin-bottom: 1rem;
    127             border: 1px solid rgba(43, 43, 43, 0.1);
    128             cursor: pointer;
    129             transition: all 0.3s ease;
    130             position: relative;
    131             background: var(--cream);
    132         }
    133 
    134         .order-card:hover {
    135             transform: translateX(8px);
    136             border-color: var(--rust);
    137             box-shadow: -8px 0 0 var(--rust);
    138         }
    139 
    140         .order-card.selected {
    141             background: var(--charcoal);
    142             color: var(--cream);
    143             border-color: var(--charcoal);
    144         }
    145 
    146         .order-card.selected .order-id {
    147             color: var(--gold);
    148         }
    149 
    150         .order-id {
    151             font-family: 'Courier New', monospace;
    152             color: var(--rust);
    153             margin-bottom: 0.5rem;
    154             font-weight: bold;
    155         }
    156 
    157         .order-description {
    158             font-size: 0.875rem;
    159             line-height: 1.6;
    160         }
    161 
    162         .file-upload-zone {
    163             padding: 3rem;
    164             border: 2px dashed rgba(43, 43, 43, 0.2);
    165             text-align: center;
    166             cursor: pointer;
    167             position: relative;
    168         }
    169 
    170         .file-upload-zone:hover {
    171             border-color: var(--rust);
    172             background: rgba(193, 80, 61, 0.03);
    173         }
    174 
    175         .file-upload-zone.drag-over {
    176             border-color: var(--sage);
    177             background: rgba(139, 154, 126, 0.1);
    178             transform: scale(1.02);
    179         }
    180 
    181         .upload-icon {
    182             font-size: 3rem;
    183             margin-bottom: 1rem;
    184             opacity: 0.3;
    185         }
    186 
    187         .upload-text {
    188             font-size: 1rem;
    189             margin-bottom: 0.5rem;
    190         }
    191 
    192         .upload-hint {
    193             font-size: 0.875rem;
    194             opacity: 0.6;
    195         }
    196 
    197         input[type="file"] {
    198             display: none;
    199         }
    200 
    201         .selected-file {
    202             margin-top: 1.5rem;
    203             padding: 1.5rem;
    204             background: var(--cream);
    205             border-left: 4px solid var(--sage);
    206         }
    207 
    208         .selected-file-name {
    209             font-weight: 600;
    210             margin-bottom: 0.5rem;
    211         }
    212 
    213         .selected-file-size {
    214             font-size: 0.875rem;
    215             opacity: 0.7;
    216         }
    217 
    218         .submit-button {
    219             width: 100%;
    220             margin-top: 2rem;
    221             padding: 1.25rem 2rem;
    222             background: var(--charcoal);
    223             color: var(--cream);
    224             border: none;
    225             font-size: 1rem;
    226             font-weight: 600;
    227             letter-spacing: 0.1em;
    228             text-transform: uppercase;
    229             cursor: pointer;
    230             transition: all 0.3s ease;
    231             position: relative;
    232             overflow: hidden;
    233         }
    234 
    235         .submit-button::before {
    236             content: '';
    237             position: absolute;
    238             top: 50%;
    239             left: 50%;
    240             width: 0;
    241             height: 0;
    242             background: var(--rust);
    243             border-radius: 50%;
    244             transform: translate(-50%, -50%);
    245             transition: width 0.6s ease, height 0.6s ease;
    246         }
    247 
    248         .submit-button:hover::before {
    249             width: 300%;
    250             height: 300%;
    251         }
    252 
    253         .submit-button:hover {
    254             color: white;
    255         }
    256 
    257         .submit-button span {
    258             position: relative;
    259             z-index: 1;
    260         }
    261 
    262         .submit-button:disabled {
    263             opacity: 0.4;
    264             cursor: not-allowed;
    265         }
    266 
    267         .message {
    268             margin-top: 2rem;
    269             padding: 1.5rem;
    270             border-left: 4px solid var(--sage);
    271             background: rgba(139, 154, 126, 0.1);
    272             animation: fadeIn 0.5s ease forwards;
    273         }
    274 
    275         .message.error {
    276             border-left-color: var(--rust);
    277             background: rgba(193, 80, 61, 0.1);
    278         }
    279 
    280         .message.success {
    281             border-left-color: var(--sage);
    282             background: rgba(139, 154, 126, 0.1);
    283         }
    284 
    285         .loading {
    286             display: inline-block;
    287             width: 20px;
    288             height: 20px;
    289             border: 2px solid rgba(43, 43, 43, 0.1);
    290             border-radius: 50%;
    291             border-top-color: var(--charcoal);
    292             animation: spin 1s linear infinite;
    293         }
    294 
    295         @keyframes spin {
    296             to {
    297                 transform: rotate(360deg);
    298             }
    299         }
    300 
    301         @media (max-width: 768px) {
    302             main {
    303                 grid-template-columns: 1fr;
    304             }
    305 
    306             h1 {
    307                 font-size: 3rem;
    308             }
    309         }
    310     </style>
    311 </head>
    312 <body>
    313     <div class="container">
    314         <header>
    315             <div class="subtitle">LibEuFin EbiSync</div>
    316             <h1>File Submission Portal</h1>
    317             <div class="version-text" id="versionText">Initializing...</div>
    318         </header>
    319 
    320         <main>
    321             <section>
    322                 <h2>Choose Order</h2>
    323                 <div class="orders-list" id="ordersList">
    324                     <div style="text-align: center; padding: 2rem; opacity: 0.5;">
    325                         <div class="loading"></div>
    326                         <p style="margin-top: 1rem;">Loading orders...</p>
    327                     </div>
    328                 </div>
    329             </section>
    330 
    331             <section>
    332                 <h2>Submit File</h2>
    333                 <div class="file-upload-zone" id="uploadZone">
    334                     <div class="upload-icon">📄</div>
    335                     <div class="upload-text">Drop XML file here or click to browse</div>
    336                     <div class="upload-hint">Accepts .xml files only</div>
    337                     <input type="file" id="fileInput" accept=".xml,application/xml,text/xml">
    338                 </div>
    339                 <div id="selectedFileInfo"></div>
    340                 <button class="submit-button" id="submitButton" disabled>
    341                     <span>Submit Document</span>
    342                 </button>
    343                 <div id="messageArea"></div>
    344             </section>
    345         </main>
    346     </div>
    347 
    348     <script>
    349         let selectedOrder = null;
    350         let selectedFile = null;
    351 
    352         // Initialize
    353         function init() {
    354             loadConfig()
    355             loadOrders();
    356             setupEventListeners();
    357         }
    358 
    359         async function loadConfig() {
    360             try {
    361                 const response = await fetch('/config');
    362                 const data = await response.json();
    363                 document.getElementById('versionText').textContent = `${data.spa_version} (${data.version})`;
    364             } catch (error) {
    365                 document.getElementById('versionText').textContent = 'Error';
    366                 showMessage('Unable to connect to backend server', 'error');
    367             }
    368         }
    369 
    370         async function loadOrders() {
    371             try {
    372                 const response = await fetch('/submit');
    373                 const data = await response.json();
    374                 
    375                 const ordersList = document.getElementById('ordersList');
    376                 ordersList.innerHTML = '';
    377 
    378                 data.orders.forEach(order => {
    379                     const orderCard = document.createElement('div');
    380                     orderCard.className = 'order-card';
    381                     orderCard.innerHTML = `
    382                         <div class="order-id">${order.id}</div>
    383                         <div class="order-description">${order.description}</div>
    384                     `;
    385                     orderCard.onclick = () => selectOrder(order, orderCard);
    386                     ordersList.appendChild(orderCard);
    387                 });
    388             } catch (error) {
    389                 document.getElementById('ordersList').innerHTML = 
    390                     '<p style="text-align: center; opacity: 0.5;">Failed to load orders</p>';
    391             }
    392         }
    393 
    394         function selectOrder(order, element) {
    395             document.querySelectorAll('.order-card').forEach(card => {
    396                 card.classList.remove('selected');
    397             });
    398             element.classList.add('selected');
    399             selectedOrder = order;
    400             updateSubmitButton();
    401         }
    402 
    403         function setupEventListeners() {
    404             const uploadZone = document.getElementById('uploadZone');
    405             const fileInput = document.getElementById('fileInput');
    406 
    407             uploadZone.addEventListener('click', () => fileInput.click());
    408 
    409             uploadZone.addEventListener('dragover', (e) => {
    410                 e.preventDefault();
    411                 uploadZone.classList.add('drag-over');
    412             });
    413 
    414             uploadZone.addEventListener('dragleave', () => {
    415                 uploadZone.classList.remove('drag-over');
    416             });
    417 
    418             uploadZone.addEventListener('drop', (e) => {
    419                 e.preventDefault();
    420                 uploadZone.classList.remove('drag-over');
    421                 const files = e.dataTransfer.files;
    422                 if (files.length > 0) {
    423                     handleFileSelect(files[0]);
    424                 }
    425             });
    426 
    427             fileInput.addEventListener('change', (e) => {
    428                 if (e.target.files.length > 0) {
    429                     handleFileSelect(e.target.files[0]);
    430                 }
    431             });
    432 
    433             document.getElementById('submitButton').addEventListener('click', submitFile);
    434         }
    435 
    436         function handleFileSelect(file) {
    437             if (!file.name.endsWith('.xml')) {
    438                 showMessage('Please select an XML file', 'error');
    439                 return;
    440             }
    441 
    442             selectedFile = file;
    443             const infoDiv = document.getElementById('selectedFileInfo');
    444             infoDiv.innerHTML = `
    445                 <div class="selected-file">
    446                     <div class="selected-file-name">📎 ${file.name}</div>
    447                     <div class="selected-file-size">${(file.size / 1024).toFixed(2)} KB</div>
    448                 </div>
    449             `;
    450             updateSubmitButton();
    451         }
    452 
    453         function updateSubmitButton() {
    454             const button = document.getElementById('submitButton');
    455             button.disabled = !(selectedOrder && selectedFile);
    456         }
    457 
    458         async function submitFile() {
    459             const button = document.getElementById('submitButton');
    460             button.disabled = true;
    461             button.innerHTML = '<span><div class="loading" style="display: inline-block; vertical-align: middle; margin-right: 10px;"></div>Submitting...</span>';
    462 
    463             const formData = new FormData();
    464             formData.append('order', selectedOrder.id);
    465             formData.append('file', selectedFile);
    466 
    467             clearMessage()
    468 
    469             try {
    470                 const response = await fetch('/submit', {
    471                     method: 'POST',
    472                     body: formData
    473                 });
    474                 const data = await response.json();
    475                 if (response.ok) {
    476                     showMessage(`Successfully submitted ${selectedFile.name} with order ${selectedOrder.id} as ${data.order}`, 'success');
    477                     // Reset form
    478                     selectedFile = null;
    479                     document.getElementById('selectedFileInfo').innerHTML = '';
    480                     document.getElementById('fileInput').value = '';
    481                 } else {
    482                     showMessage(`${data.code} - ${data.hint ?? 'Submission failed'}`, 'error');
    483                 }
    484             } catch (error) {
    485                 showMessage('Network error: ' + error.message, 'error');
    486             } finally {
    487                 button.disabled = false;
    488                 button.innerHTML = '<span>Submit Document</span>';
    489                 updateSubmitButton();
    490             }
    491         }
    492 
    493         function showMessage(text, type = 'info') {
    494             const messageArea = document.getElementById('messageArea');
    495             messageArea.innerHTML = `
    496                 <div class="message ${type}">
    497                     ${text}
    498                 </div>
    499             `;
    500         }
    501 
    502         function clearMessage() {
    503             const messageArea = document.getElementById('messageArea');
    504             messageArea.innerHTML = '';
    505         }
    506 
    507         // Start application
    508         init();
    509     </script>
    510 </body>
    511 </html>