Spaces:
Running
Running
| <html lang="en" class="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Enigma Vault - Secure Note & Password Pad</title> | |
| <!-- Tailwind CSS CDN --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Font Awesome CDN --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> | |
| <!-- Google Fonts (Poppins) --> | |
| <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=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| body { | |
| font-family: 'Poppins', sans-serif; | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| } | |
| ::-webkit-scrollbar { width: 8px; } | |
| ::-webkit-scrollbar-track { background: #1e293b; } | |
| ::-webkit-scrollbar-thumb { background: #475569; border-radius: 10px; } | |
| ::-webkit-scrollbar-thumb:hover { background: #64748b; } | |
| #note-body { | |
| height: calc(100vh - 10rem); /* Adjust height dynamically */ | |
| } | |
| .strength-bar { | |
| transition: all 0.3s ease-in-out; | |
| } | |
| /* Fix for password overflow */ | |
| #generated-password { | |
| overflow-wrap: break-word; | |
| word-break: break-all; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-slate-900 text-slate-300"> | |
| <div class="flex h-screen"> | |
| <!-- Sidebar --> | |
| <aside class="w-1/3 md:w-1/4 lg:w-1/5 bg-slate-800/70 p-4 flex flex-col border-r border-slate-700"> | |
| <header class="mb-4"> | |
| <h1 class="text-2xl font-bold text-white text-center">Enigma Vault</h1> | |
| <p class="text-xs text-slate-400 text-center mb-4">Your Private Notes & Passwords</p> | |
| <button id="new-note-btn" class="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg transition duration-300 flex items-center justify-center gap-2"> | |
| <i class="fas fa-plus"></i> New Note | |
| </button> | |
| </header> | |
| <div id="note-list" class="flex-grow overflow-y-auto space-y-2"> | |
| <!-- Notes will be dynamically inserted here --> | |
| </div> | |
| <footer class="mt-4 pt-4 border-t border-slate-700 space-y-2"> | |
| <button id="open-password-generator-btn" class="w-full bg-slate-700 hover:bg-slate-600 text-white font-bold py-2 px-4 rounded-lg transition duration-300 flex items-center justify-center gap-2"> | |
| <i class="fas fa-key"></i> Password Generator | |
| </button> | |
| <button id="download-all-btn" class="w-full bg-slate-700 hover:bg-slate-600 text-white font-bold py-2 px-4 rounded-lg transition duration-300 flex items-center justify-center gap-2"> | |
| <i class="fas fa-file-archive"></i> Download All Notes | |
| </button> | |
| <div id="privacy-notice" class="text-center text-xs text-slate-500 mt-2 p-2"> | |
| <i class="fas fa-exclamation-triangle"></i> <strong>Temporary Pad.</strong> All notes are deleted if you refresh or close the page. Nothing is saved permanently. | |
| </div> | |
| </footer> | |
| </aside> | |
| <!-- Main Content --> | |
| <main class="w-2/3 md:w-3/4 lg:w-4/5 p-4 sm:p-6 md:p-8"> | |
| <!-- Welcome Screen --> | |
| <div id="welcome-screen" class="flex flex-col items-center justify-center h-full text-center"> | |
| <i class="fas fa-book-open text-6xl text-slate-600 mb-4"></i> | |
| <h2 class="text-3xl font-bold text-white">Welcome to Enigma Vault</h2> | |
| <p class="text-slate-400 mt-2">Select a note from the list or create a new one to get started.</p> | |
| </div> | |
| <!-- Note Editor --> | |
| <div id="note-editor" class="hidden h-full flex flex-col"> | |
| <div class="flex justify-between items-center mb-4 gap-4"> | |
| <input type="text" id="note-title" class="w-full bg-transparent text-3xl font-bold text-white focus:outline-none" placeholder="Note Title"> | |
| <div class="flex items-center gap-2 flex-shrink-0"> | |
| <button id="download-note-btn" title="Download this note" class="text-green-500 hover:text-green-400 p-2 rounded-full"> | |
| <i class="fas fa-download fa-lg"></i> | |
| </button> | |
| <button id="delete-note-btn" title="Delete this note" class="text-red-500 hover:text-red-400 p-2 rounded-full"> | |
| <i class="fas fa-trash-alt fa-lg"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <textarea id="note-body" class="w-full flex-grow bg-slate-800/50 rounded-lg p-4 text-slate-200 focus:outline-none focus:ring-2 focus:ring-green-500" placeholder="Start writing your secure thoughts..."></textarea> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- Password Generator Modal --> | |
| <div id="password-generator-modal" class="hidden fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center p-4 z-50"> | |
| <div class="bg-slate-800 rounded-xl shadow-lg p-6 w-full max-w-md border-t-4 border-amber-500"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-2xl font-bold text-amber-400">Password Generator</h2> | |
| <button id="close-modal-btn" class="text-slate-400 hover:text-white text-3xl leading-none">×</button> | |
| </div> | |
| <!-- Generated Password Display --> | |
| <div class="bg-slate-900 p-4 rounded-lg mb-4 flex items-start min-h-[80px]"> | |
| <span id="generated-password" class="text-xl font-mono text-white flex-grow">Click Generate...</span> | |
| <button id="copy-password-btn" class="ml-4 text-slate-400 hover:text-amber-400 flex-shrink-0"><i class="fas fa-copy"></i></button> | |
| </div> | |
| <!-- Strength Meter --> | |
| <div class="w-full bg-slate-700 rounded-full h-2.5 mb-2"> | |
| <div id="strength-bar" class="bg-red-600 h-2.5 rounded-full strength-bar" style="width: 10%"></div> | |
| </div> | |
| <p id="strength-text" class="text-center text-sm text-slate-400 mb-6">Strength: Very Weak</p> | |
| <!-- Options --> | |
| <div class="space-y-4"> | |
| <div> | |
| <label for="length" class="flex justify-between font-semibold">Length: <span id="length-val">16</span></label> | |
| <input id="length" type="range" min="6" max="32" value="16" class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer"> | |
| </div> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <label class="flex items-center"><input type="checkbox" id="uppercase" class="w-4 h-4" checked><span class="ml-2">Uppercase (A-Z)</span></label> | |
| <label class="flex items-center"><input type="checkbox" id="lowercase" class="w-4 h-4" checked><span class="ml-2">Lowercase (a-z)</span></label> | |
| <label class="flex items-center"><input type="checkbox" id="numbers" class="w-4 h-4" checked><span class="ml-2">Numbers (0-9)</span></label> | |
| <label class="flex items-center"><input type="checkbox" id="symbols" class="w-4 h-4" checked><span class="ml-2">Symbols (!@#$)</span></label> | |
| </div> | |
| <button id="generate-btn" class="w-full bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 rounded-lg transition">Generate New Password</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // DOM Elements | |
| const noteListEl = document.getElementById('note-list'); | |
| const newNoteBtn = document.getElementById('new-note-btn'); | |
| const deleteNoteBtn = document.getElementById('delete-note-btn'); | |
| const downloadNoteBtn = document.getElementById('download-note-btn'); | |
| const downloadAllBtn = document.getElementById('download-all-btn'); | |
| const noteEditorEl = document.getElementById('note-editor'); | |
| const welcomeScreenEl = document.getElementById('welcome-screen'); | |
| const noteTitleEl = document.getElementById('note-title'); | |
| const noteBodyEl = document.getElementById('note-body'); | |
| // State | |
| let notes = []; | |
| let activeNoteId = null; | |
| // --- Core Note Functions --- | |
| const saveNotes = () => { | |
| updateButtonStates(); | |
| }; | |
| const renderNoteList = () => { | |
| noteListEl.innerHTML = ''; | |
| if (notes.length === 0) { | |
| noteListEl.innerHTML = `<p class="text-center text-slate-500 text-sm p-4">No notes yet. Create one!</p>`; | |
| return; | |
| } | |
| const sortedNotes = [...notes].sort((a,b) => b.id - a.id); | |
| sortedNotes.forEach(note => { | |
| const noteItem = document.createElement('div'); | |
| noteItem.dataset.id = note.id; | |
| noteItem.className = `p-3 rounded-lg cursor-pointer transition ${note.id === activeNoteId ? 'bg-green-600/30' : 'hover:bg-slate-700'}`; | |
| const title = document.createElement('h3'); | |
| title.textContent = note.title || 'Untitled Note'; | |
| title.className = 'font-bold text-white truncate'; | |
| noteItem.appendChild(title); | |
| const excerpt = document.createElement('p'); | |
| excerpt.textContent = note.body.substring(0, 40) + (note.body.length > 40 ? '...' : ''); | |
| excerpt.className = 'text-sm text-slate-400 truncate'; | |
| noteItem.appendChild(excerpt); | |
| noteItem.addEventListener('click', () => selectNote(note.id)); | |
| noteListEl.appendChild(noteItem); | |
| }); | |
| }; | |
| const selectNote = (id) => { | |
| activeNoteId = id; | |
| const note = notes.find(n => n.id === id); | |
| if (note) { | |
| welcomeScreenEl.classList.add('hidden'); | |
| noteEditorEl.classList.remove('hidden'); | |
| noteTitleEl.value = note.title; | |
| noteBodyEl.value = note.body; | |
| } | |
| renderNoteList(); | |
| }; | |
| const createNewNote = () => { | |
| const newNote = { | |
| id: Date.now(), | |
| title: 'New Note', | |
| body: '' | |
| }; | |
| notes.push(newNote); | |
| saveNotes(); | |
| selectNote(newNote.id); | |
| }; | |
| const deleteActiveNote = () => { | |
| if (!activeNoteId) return; | |
| if(confirm('Are you sure you want to delete this note? This action cannot be undone.')){ | |
| notes = notes.filter(note => note.id !== activeNoteId); | |
| activeNoteId = null; | |
| saveNotes(); | |
| noteEditorEl.classList.add('hidden'); | |
| welcomeScreenEl.classList.remove('hidden'); | |
| renderNoteList(); | |
| } | |
| }; | |
| const downloadActiveNote = () => { | |
| if (!activeNoteId) return; | |
| const note = notes.find(n => n.id === activeNoteId); | |
| if (!note) return; | |
| const content = `Title: ${note.title}\n\n---\n\n${note.body}`; | |
| const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| const filename = (note.title || 'Untitled Note').replace(/[^a-z0-9]/gi, '_').toLowerCase(); | |
| a.href = url; | |
| a.download = `${filename}.txt`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }; | |
| const downloadAllNotes = () => { | |
| if (notes.length === 0) { | |
| alert('There are no notes to download.'); | |
| return; | |
| } | |
| const zip = new JSZip(); | |
| notes.forEach(note => { | |
| const content = `Title: ${note.title}\n\n---\n\n${note.body}`; | |
| const filename = (note.title || 'Untitled Note').replace(/[^a-z0-9]/gi, '_').toLowerCase(); | |
| zip.file(`${filename}_${note.id}.txt`, content); | |
| }); | |
| zip.generateAsync({ type: "blob" }) | |
| .then(function(blob) { | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'enigma-vault-notes.zip'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }); | |
| }; | |
| const updateButtonStates = () => { | |
| if (notes.length === 0) { | |
| downloadAllBtn.disabled = true; | |
| downloadAllBtn.classList.add('opacity-50', 'cursor-not-allowed'); | |
| } else { | |
| downloadAllBtn.disabled = false; | |
| downloadAllBtn.classList.remove('opacity-50', 'cursor-not-allowed'); | |
| } | |
| }; | |
| // Event Listeners for Notes | |
| newNoteBtn.addEventListener('click', createNewNote); | |
| deleteNoteBtn.addEventListener('click', deleteActiveNote); | |
| downloadNoteBtn.addEventListener('click', downloadActiveNote); | |
| downloadAllBtn.addEventListener('click', downloadAllNotes); | |
| noteTitleEl.addEventListener('input', () => { | |
| const note = notes.find(n => n.id === activeNoteId); | |
| if(note) { | |
| note.title = noteTitleEl.value; | |
| saveNotes(); | |
| const noteInList = noteListEl.querySelector(`[data-id='${activeNoteId}'] h3`); | |
| if(noteInList) noteInList.textContent = note.title || 'Untitled Note'; | |
| } | |
| }); | |
| noteBodyEl.addEventListener('input', () => { | |
| const note = notes.find(n => n.id === activeNoteId); | |
| if(note) { | |
| note.body = noteBodyEl.value; | |
| saveNotes(); | |
| const noteInList = noteListEl.querySelector(`[data-id='${activeNoteId}'] p`); | |
| if(noteInList) noteInList.textContent = note.body.substring(0, 40) + (note.body.length > 40 ? '...' : ''); | |
| } | |
| }); | |
| // --- Password Generator Logic --- | |
| const openModalBtn = document.getElementById('open-password-generator-btn'); | |
| const closeModalBtn = document.getElementById('close-modal-btn'); | |
| const modal = document.getElementById('password-generator-modal'); | |
| const generateBtn = document.getElementById('generate-btn'); | |
| const copyBtn = document.getElementById('copy-password-btn'); | |
| const lengthSlider = document.getElementById('length'); | |
| const lengthVal = document.getElementById('length-val'); | |
| const uppercaseCheck = document.getElementById('uppercase'); | |
| const lowercaseCheck = document.getElementById('lowercase'); | |
| const numbersCheck = document.getElementById('numbers'); | |
| const symbolsCheck = document.getElementById('symbols'); | |
| const passwordEl = document.getElementById('generated-password'); | |
| const strengthBar = document.getElementById('strength-bar'); | |
| const strengthText = document.getElementById('strength-text'); | |
| openModalBtn.addEventListener('click', () => modal.classList.remove('hidden')); | |
| closeModalBtn.addEventListener('click', () => modal.classList.add('hidden')); | |
| lengthSlider.addEventListener('input', (e) => { | |
| lengthVal.textContent = e.target.value; | |
| }); | |
| copyBtn.addEventListener('click', () => { | |
| if (passwordEl.textContent.length > 0 && passwordEl.textContent !== 'Click Generate...') { | |
| navigator.clipboard.writeText(passwordEl.textContent); | |
| copyBtn.innerHTML = '<i class="fas fa-check text-green-500"></i>'; | |
| setTimeout(() => { | |
| copyBtn.innerHTML = '<i class="fas fa-copy"></i>'; | |
| }, 1500); | |
| } | |
| }); | |
| generateBtn.addEventListener('click', () => { | |
| const length = +lengthSlider.value; | |
| const hasUpper = uppercaseCheck.checked; | |
| const hasLower = lowercaseCheck.checked; | |
| const hasNumber = numbersCheck.checked; | |
| const hasSymbol = symbolsCheck.checked; | |
| passwordEl.textContent = generatePassword(hasUpper, hasLower, hasNumber, hasSymbol, length); | |
| updatePasswordStrength(); | |
| }); | |
| const generatePassword = (upper, lower, number, symbol, length) => { | |
| let generatedPassword = ''; | |
| const typesCount = upper + lower + number + symbol; | |
| const typesArr = [{upper}, {lower}, {number}, {symbol}].filter(item => Object.values(item)[0]); | |
| if(typesCount === 0) return 'Select at least one option'; | |
| for(let i=0; i<length; i++) { | |
| const funcName = Object.keys(typesArr[Math.floor(Math.random() * typesArr.length)])[0]; | |
| generatedPassword += randomFunc[funcName](); | |
| } | |
| return generatedPassword.slice(0, length); | |
| } | |
| const randomFunc = { | |
| lower: () => String.fromCharCode(Math.floor(Math.random() * 26) + 97), | |
| upper: () => String.fromCharCode(Math.floor(Math.random() * 26) + 65), | |
| number: () => String.fromCharCode(Math.floor(Math.random() * 10) + 48), | |
| symbol: () => '!@#$%^&*(){}[]=<>/,.'.charAt(Math.floor(Math.random() * 20)) | |
| }; | |
| const updatePasswordStrength = () => { | |
| const password = passwordEl.textContent; | |
| let score = 0; | |
| if(password.length === 0 || password === 'Select at least one option') { | |
| strengthText.textContent = `Strength: N/A`; | |
| strengthBar.style.width = '0%'; | |
| return; | |
| } | |
| if (password.length > 10) score++; | |
| if (password.length > 15) score++; | |
| if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score++; | |
| if (/\d/.test(password)) score++; | |
| if (/[^A-Za-z0-9]/.test(password)) score++; | |
| let strength = { text: 'Very Weak', color: 'bg-red-600', width: '10%' }; | |
| if (score >= 5) strength = { text: 'Very Strong', color: 'bg-green-500', width: '100%' }; | |
| else if (score === 4) strength = { text: 'Strong', color: 'bg-green-400', width: '80%' }; | |
| else if (score === 3) strength = { text: 'Medium', color: 'bg-yellow-500', width: '60%' }; | |
| else if (score >= 1) strength = { text: 'Weak', color: 'bg-orange-500', width: '35%' }; | |
| strengthText.textContent = `Strength: ${strength.text}`; | |
| strengthBar.style.width = strength.width; | |
| strengthBar.className = `h-2.5 rounded-full strength-bar ${strength.color}`; | |
| }; | |
| // --- Initial Load --- | |
| const init = () => { | |
| renderNoteList(); | |
| updateButtonStates(); | |
| lengthVal.textContent = lengthSlider.value; // Set initial slider value display | |
| }; | |
| init(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |