Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Script Manager</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .rich-text-toolbar { | |
| display: flex; | |
| gap: 4px; | |
| padding: 8px; | |
| background: #f3f4f6; | |
| border-radius: 4px; | |
| margin-bottom: 8px; | |
| } | |
| .rich-text-button { | |
| padding: 4px 8px; | |
| background: white; | |
| border: 1px solid #d1d5db; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| .rich-text-button:hover { | |
| background: #e5e7eb; | |
| } | |
| .label-item { | |
| transition: all 0.2s ease; | |
| user-select: none; | |
| position: relative; | |
| } | |
| .label-item:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| } | |
| .script-card { | |
| transition: all 0.2s ease; | |
| } | |
| .script-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | |
| } | |
| .copy-notification { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| background: #4CAF50; | |
| color: white; | |
| padding: 12px 24px; | |
| border-radius: 4px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| z-index: 1000; | |
| opacity: 0; | |
| transition: opacity 0.3s ease; | |
| } | |
| .copy-notification.show { | |
| opacity: 1; | |
| } | |
| .highlight { | |
| background-color: #ffeb3b; | |
| padding: 0 2px; | |
| border-radius: 2px; | |
| } | |
| .category-tag { | |
| display: inline-flex; | |
| align-items: center; | |
| background-color: #e0f2fe; | |
| color: #0369a1; | |
| padding: 2px 8px; | |
| border-radius: 9999px; | |
| font-size: 0.75rem; | |
| margin-right: 4px; | |
| margin-bottom: 4px; | |
| } | |
| .category-tag button { | |
| margin-left: 4px; | |
| color: #0369a1; | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| font-size: 0.6rem; | |
| } | |
| .category-selector { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 4px; | |
| margin-top: 8px; | |
| } | |
| .category-option { | |
| display: flex; | |
| align-items: center; | |
| padding: 2px 8px; | |
| background-color: #f3f4f6; | |
| border-radius: 9999px; | |
| cursor: pointer; | |
| font-size: 0.75rem; | |
| } | |
| .category-option.selected { | |
| background-color: #3b82f6; | |
| color: white; | |
| } | |
| .category-option input { | |
| margin-right: 4px; | |
| } | |
| .move-btn { | |
| opacity: 0.5; | |
| transition: opacity 0.2s ease; | |
| } | |
| .label-item:hover .move-btn { | |
| opacity: 1; | |
| } | |
| .move-btn:hover { | |
| color: #3b82f6; | |
| } | |
| .toggle-dates-btn { | |
| transition: all 0.2s ease; | |
| } | |
| .toggle-dates-btn.active { | |
| color: #3b82f6; | |
| transform: scale(1.1); | |
| } | |
| /* Style for formatted text preview */ | |
| .formatted-text { | |
| white-space: pre-line; | |
| } | |
| .formatted-text b, .formatted-text strong { | |
| font-weight: bold; | |
| } | |
| .formatted-text i, .formatted-text em { | |
| font-style: italic; | |
| } | |
| .formatted-text u { | |
| text-decoration: underline; | |
| } | |
| /* Color picker styles */ | |
| .color-picker { | |
| position: relative; | |
| display: inline-block; | |
| } | |
| .color-preview { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 4px; | |
| border: 1px solid #d1d5db; | |
| cursor: pointer; | |
| } | |
| .color-palette { | |
| position: absolute; | |
| top: 100%; | |
| left: 0; | |
| z-index: 10; | |
| display: none; | |
| background: white; | |
| padding: 8px; | |
| border-radius: 4px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| width: 180px; | |
| } | |
| .color-palette.show { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .color-options-grid { | |
| display: grid; | |
| grid-template-columns: repeat(6, 1fr); | |
| gap: 4px; | |
| } | |
| .color-option { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| border: 1px solid #d1d5db; | |
| } | |
| .color-option:hover { | |
| transform: scale(1.1); | |
| } | |
| .custom-color-section { | |
| border-top: 1px solid #e5e7eb; | |
| padding-top: 8px; | |
| } | |
| .custom-color-input { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| .custom-color-input input { | |
| flex: 1; | |
| padding: 4px; | |
| border: 1px solid #d1d5db; | |
| border-radius: 4px; | |
| } | |
| .custom-color-input button { | |
| padding: 4px 8px; | |
| background: #3b82f6; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| .custom-color-indicator { | |
| display: inline-block; | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| margin-right: 4px; | |
| } | |
| /* Category filter buttons */ | |
| .category-filter-buttons { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin-bottom: 16px; | |
| } | |
| .category-filter-btn { | |
| padding: 4px 12px; | |
| border-radius: 9999px; | |
| background-color: #e5e7eb; | |
| color: #4b5563; | |
| font-size: 0.875rem; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| border: 1px solid #d1d5db; | |
| } | |
| .category-filter-btn:hover { | |
| background-color: #d1d5db; | |
| } | |
| .category-filter-btn.active { | |
| background-color: #3b82f6; | |
| color: white; | |
| border-color: #3b82f6; | |
| } | |
| .category-filter-btn.all-categories { | |
| background-color: #10b981; | |
| color: white; | |
| border-color: #10b981; | |
| } | |
| /* Import/Export buttons */ | |
| .data-actions { | |
| display: flex; | |
| gap: 8px; | |
| margin-top: 8px; | |
| } | |
| .data-actions button { | |
| padding: 4px 8px; | |
| font-size: 0.75rem; | |
| border-radius: 4px; | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| .import-export-container { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 16px; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <header class="mb-8"> | |
| <h1 class="text-3xl font-bold text-gray-800">Script Manager</h1> | |
| <p class="text-gray-600">Save and organize your frequently used scripts</p> | |
| </header> | |
| <div class="grid grid-cols-1 lg:grid-cols-4 gap-6"> | |
| <!-- Labels Section --> | |
| <div class="lg:col-span-1 bg-white p-6 rounded-lg shadow"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-semibold text-gray-800">Labels</h2> | |
| <div class="flex gap-2"> | |
| <button id="sortLabelsBtn" class="bg-gray-500 hover:bg-gray-600 text-white px-3 py-1 rounded" title="Sort A-Z"> | |
| <i class="fas fa-sort-alpha-down"></i> | |
| </button> | |
| <button id="addLabelBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded"> | |
| <i class="fas fa-plus"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div id="addLabelForm" class="hidden mb-4"> | |
| <div class="flex gap-2"> | |
| <input id="newLabelInput" type="text" placeholder="New label" class="flex-1 px-3 py-2 border rounded"> | |
| <button id="saveLabelBtn" class="bg-green-500 hover:bg-green-600 text-white px-3 py-2 rounded"> | |
| <i class="fas fa-check"></i> | |
| </button> | |
| <button id="cancelLabelBtn" class="bg-red-500 hover:bg-red-600 text-white px-3 py-2 rounded"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div id="labelsContainer" class="space-y-2"> | |
| <!-- Labels will be rendered here --> | |
| </div> | |
| <!-- Import/Export buttons for labels --> | |
| <div class="data-actions mt-4"> | |
| <button id="exportLabelsBtn" class="bg-purple-500 hover:bg-purple-600 text-white"> | |
| <i class="fas fa-file-export mr-1"></i> Export Labels | |
| </button> | |
| <button id="importLabelsBtn" class="bg-indigo-500 hover:bg-indigo-600 text-white"> | |
| <i class="fas fa-file-import mr-1"></i> Import Labels | |
| </button> | |
| <input type="file" id="labelsFileInput" class="hidden" accept=".json"> | |
| </div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="lg:col-span-3 space-y-6"> | |
| <!-- Pinned Scripts --> | |
| <div class="bg-white p-6 rounded-lg shadow"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-semibold text-blue-500">Pinned Scripts</h2> | |
| <button id="togglePinnedDatesBtn" class="toggle-dates-btn text-gray-500 hover:text-blue-500" title="Toggle dates"> | |
| <i class="fa-solid fa-eye-slash"></i> | |
| </button> | |
| </div> | |
| <div id="pinnedScriptsContainer" class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <!-- Pinned scripts will be rendered here --> | |
| </div> | |
| </div> | |
| <!-- Time Zone Converter --> | |
| <div class="bg-white p-6 rounded-lg shadow"> | |
| <h2 class="text-xl font-semibold text-blue-500 mb-4">Appointment Time Converter</h2> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Client Name</label> | |
| <input type="text" id="clientName" class="w-full px-3 py-2 border rounded" placeholder="First Last"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Client Email</label> | |
| <input type="email" id="clientEmail" class="w-full px-3 py-2 border rounded" placeholder="email@example.com"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Date</label> | |
| <input type="date" id="appointmentDate" class="w-full px-3 py-2 border rounded"> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Time (EST)</label> | |
| <select id="appointmentTime" class="w-full px-3 py-2 border rounded"></select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Convert to Timezone</label> | |
| <select id="timezoneSelect" class="w-full px-3 py-2 border rounded"> | |
| <option value="EST">Eastern (EST)</option> | |
| <option value="CST">Central (CST)</option> | |
| <option value="MST">Mountain (MST)</option> | |
| <option value="PST">Pacific (PST)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="flex gap-2"> | |
| <button id="generateScriptBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"> | |
| Generate Script | |
| </button> | |
| <button id="downloadCalendarBtn" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded hidden"> | |
| <i class="fas fa-calendar-plus mr-2"></i>Add to Calendar | |
| </button> | |
| </div> | |
| <div id="generatedScriptContainer" class="mt-4 hidden"> | |
| <div class="flex justify-between items-center mb-2"> | |
| <h3 class="font-medium">Generated Script</h3> | |
| <button id="copyGeneratedScriptBtn" class="text-blue-500 hover:text-blue-700"> | |
| <i class="fas fa-copy mr-1"></i> Copy | |
| </button> | |
| </div> | |
| <div id="generatedScript" class="bg-gray-100 p-4 rounded whitespace-pre-line"></div> | |
| </div> | |
| </div> | |
| <!-- All Scripts --> | |
| <div class="bg-white p-6 rounded-lg shadow"> | |
| <div class="import-export-container"> | |
| <div class="flex items-center gap-4"> | |
| <h2 class="text-xl font-semibold text-gray-800">All Scripts</h2> | |
| <button id="toggleDatesBtn" class="toggle-dates-btn text-gray-500 hover:text-blue-500" title="Toggle dates"> | |
| <i class="fa-solid fa-eye-slash"></i> | |
| </button> | |
| </div> | |
| <div class="flex gap-2"> | |
| <div class="relative"> | |
| <input type="text" id="searchInput" placeholder="Search scripts..." | |
| class="pl-8 pr-4 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> | |
| </div> | |
| <button id="addScriptBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded flex items-center gap-2"> | |
| <i class="fas fa-plus"></i> Add Script | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Category filter buttons --> | |
| <div class="category-filter-buttons"> | |
| <button class="category-filter-btn all-categories" data-category="all">All Categories</button> | |
| <!-- Category buttons will be rendered here --> | |
| </div> | |
| <!-- Import/Export buttons for scripts --> | |
| <div class="data-actions mb-4"> | |
| <button id="exportScriptsBtn" class="bg-purple-500 hover:bg-purple-600 text-white"> | |
| <i class="fas fa-file-export mr-1"></i> Export Scripts | |
| </button> | |
| <button id="importScriptsBtn" class="bg-indigo-500 hover:bg-indigo-600 text-white"> | |
| <i class="fas fa-file-import mr-1"></i> Import Scripts | |
| </button> | |
| <button id="exportAllDataBtn" class="bg-blue-500 hover:bg-blue-600 text-white"> | |
| <i class="fas fa-download mr-1"></i> Export All Data | |
| </button> | |
| <button id="importAllDataBtn" class="bg-green-500 hover:bg-green-600 text-white"> | |
| <i class="fas fa-upload mr-1"></i> Import All Data | |
| </button> | |
| <input type="file" id="scriptsFileInput" class="hidden" accept=".json"> | |
| <input type="file" id="allDataFileInput" class="hidden" accept=".json"> | |
| </div> | |
| <div id="scriptsContainer" class="space-y-4"> | |
| <!-- Scripts will be rendered here --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Script Modal --> | |
| <div id="scriptModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden"> | |
| <div class="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto"> | |
| <div class="flex justify-between items-center p-4 border-b"> | |
| <h3 class="text-lg font-semibold" id="modalTitle">Add New Script</h3> | |
| <button id="closeModalBtn" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <form id="scriptForm" class="p-4"> | |
| <input type="hidden" id="scriptId"> | |
| <div class="mb-4"> | |
| <label for="scriptTitle" class="block text-sm font-medium text-gray-700 mb-1">Title</label> | |
| <input type="text" id="scriptTitle", class="w-full px-3 py-2 border rounded" required> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Categories</label> | |
| <div class="category-selector" id="categorySelector"> | |
| <!-- Category options will be rendered here --> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="flex items-center"> | |
| <input type="checkbox" id="scriptPinned" class="rounded text-blue-500"> | |
| <span class="ml-2 text-sm text-gray-700">Pin this script</span> | |
| </label> | |
| </div> | |
| <div class="mb-4"> | |
| <label for="scriptContent" class="block text-sm font-medium text-gray-700 mb-1">Content</label> | |
| <div class="rich-text-toolbar"> | |
| <button type="button" id="boldBtn" class="rich-text-button"><i class="fas fa-bold"></i></button> | |
| <button type="button" id="italicBtn" class="rich-text-button"><i class="fas fa-italic"></i></button> | |
| <button type="button" id="underlineBtn" class="rich-text-button"><i class="fas fa-underline"></i></button> | |
| <div class="color-picker"> | |
| <div class="rich-text-button" id="colorPickerBtn"> | |
| <i class="fas fa-palette"></i> | |
| </div> | |
| <div class="color-palette" id="colorPalette"> | |
| <div class="color-options-grid"> | |
| <div class="color-option" style="background-color: #000000;" data-color="#000000"></div> | |
| <div class="color-option" style="background-color: #ffffff; border: 1px solid #000;" data-color="#ffffff"></div> | |
| <div class="color-option" style="background-color: #ff0000;" data-color="#ff0000"></div> | |
| <div class="color-option" style="background-color: #00ff00;" data-color="#00ff00"></div> | |
| <div class="color-option" style="background-color: #0000ff;" data-color="#0000ff"></div> | |
| <div class="color-option" style="background-color: #ffff00;" data-color="#ffff00"></div> | |
| <div class="color-option" style="background-color: #ff00ff;" data-color="#ff00ff"></div> | |
| <div class="color-option" style="background-color: #00ffff;" data-color="#00ffff"></div> | |
| <div class="color-option" style="background-color: #800000;" data-color="#800000"></div> | |
| <div class="color-option" style="background-color: #008000;" data-color="#008000"></div> | |
| <div class="color-option" style="background-color: #000080;" data-color="#000080"></div> | |
| <div class="color-option" style="background-color: #808000;" data-color="#808000"></div> | |
| <div class="color-option" style="background-color: #800080;" data-color="#800080"></div> | |
| <div class="color-option" style="background-color: #008080;" data-color="#008080"></div> | |
| <div class="color-option" style="background-color: #c0c0c0;" data-color="#c0c0c0"></div> | |
| <div class="color-option" style="background-color: #808080;" data-color="#808080"></div> | |
| <div class="color-option" style="background-color: #ffa500;" data-color="#ffa500"></div> | |
| <div class="color-option" style="background-color: #ff6347;" data-color="#ff6347"></div> | |
| <div class="color-option" style="background-color: #ff69b4;" data-color="#ff69b4"></div> | |
| <div class="color-option" style="background-color: #9370db;" data-color="#9370db"></div> | |
| <div class="color-option" style="background-color: #7fffd4;" data-color="#7fffd4"></div> | |
| <div class="color-option" style="background-color: #ffd700;" data-color="#ffd700"></div> | |
| <div class="color-option" style="background-color: #cd5c5c;" data-color="#cd5c5c"></div> | |
| <div class="color-option" style="background-color: #4b0082;" data-color="#4b0082"></div> | |
| <div class="color-option" style="background-color: #de4319;" data-color="#de4319"></div> | |
| </div> | |
| <div class="custom-color-section"> | |
| <div class="custom-color-input"> | |
| <input type="text", id="customColorInput" placeholder="#hex or color name" class="flex-1"> | |
| <button type="button" id="applyCustomColor">Apply</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <button type="button" id="placeholderBtn" class="rich-text-button">[Variable]</button> | |
| </div> | |
| <textarea id="scriptContent" rows="8" class="w-full px-3 py-2 border rounded" required></textarea> | |
| <div class="mt-2 text-sm text-gray-500">Preview:</div> | |
| <div id="contentPreview" class="formatted-text p-2 border rounded bg-gray-50 mt-1"></div> | |
| </div> | |
| <div class="flex justify-end gap-2 pt-4 border-t"> | |
| <button type="button" id="cancelScriptBtn" class="px-4 py-2 border rounded hover:bg-gray-100">Cancel</button> | |
| <button type="submit" id="saveScriptBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">Save</button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- Copy Notification --> | |
| <div id="copyNotification" class="copy-notification"> | |
| <i class="fas fa-check-circle mr-2"></i> Script copied to clipboard! | |
| </div> | |
| <script> | |
| // Initialize data from localStorage or with default values | |
| let scripts = JSON.parse(localStorage.getItem('scripts')) || [ | |
| { | |
| id: 1, | |
| title: "Welcome Message", | |
| content: "Hello [Name], thank you for reaching out! How can I assist you today?", | |
| categories: ["intro"], | |
| pinned: true, | |
| createdAt: new Date().toISOString(), | |
| updatedAt: null | |
| }, | |
| { | |
| id: 2, | |
| title: "Follow-up Email", | |
| content: "Dear [Name],\n\nI wanted to follow up on our previous conversation. Please let me know if you have any questions or need further assistance.\n\nBest regards,\n[Your Name]", | |
| categories: ["email"], | |
| pinned: false, | |
| createdAt: new Date().toISOString(), | |
| updatedAt: null | |
| }, | |
| { | |
| id: 3, | |
| title: "Document Request", | |
| content: "Hi [Name], could you please send me the following documents: [list documents]. Thank you!", | |
| categories: ["documents_requested"], | |
| pinned: true, | |
| createdAt: new Date().toISOString(), | |
| updatedAt: null | |
| } | |
| ]; | |
| let labels = JSON.parse(localStorage.getItem('labels')) || [ | |
| "⦁ NEED TO LABEL🎁", | |
| "⦁ EASTERN 0️⃣", | |
| "⦁ CENTRAL 1️⃣", | |
| "⦁ MOUNTAIN 2️⃣", | |
| "⦁ PACIFIC 3️⃣", | |
| "⦁ CASH OUT💰", | |
| "⦁ CODE WORD🔐", | |
| "⦁ CONVENTIONAL 🍦", | |
| "⦁ CREDIT TRIGGER 😱", | |
| "⦁ DEAD 🤮", | |
| "⦁ DIVORCE➗", | |
| "⦁ FHA �🎈", | |
| "⦁ FUNDED 💲", | |
| "⦁ HELOC 🤖", | |
| "⦁ HOT 🔥", | |
| "⦁ LOST CONTACT 💔", | |
| "⦁ MICHIGAN 〽", | |
| "⦁ NFL FAN 🏈", | |
| "⦁ NOT READY ❌", | |
| "⦁ PROSPECT 📈", | |
| "⦁ PURCHASE 🏠", | |
| "⦁ SPRINGEQ 🌷", | |
| "⦁ SUB ✔", | |
| "⦁ TBD ⏲", | |
| "⦁ VA ⚓", | |
| "⦁ WIFE 👧", | |
| "⦁ XFER ⏩" | |
| ]; | |
| const allCategories = [ | |
| "Call Script", "documents_requested", "Funny", "General", | |
| "intro", "Lost Contact/Gaslight", "Power Play", "Preapproval/Purchase", "VA IRRRL" | |
| ]; | |
| // Predefined colors that match the color palette options | |
| const predefinedColors = [ | |
| '#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff', '#ffff00', | |
| '#ff00ff', '#00ffff', '#800000', '#008000', '#000080', '#808000', | |
| '#800080', '#008080', '#c0c0c0', '#808080', '#ffa500', '#ff6347', | |
| '#ff69b4', '#9370db', '#7fffd4', '#ffd700', '#cd5c5c', '#4b0082', | |
| '#f08080' | |
| ]; | |
| // DOM Elements | |
| const scriptForm = document.getElementById('scriptForm'); | |
| const scriptModal = document.getElementById('scriptModal'); | |
| const scriptTitle = document.getElementById('scriptTitle'); | |
| const scriptContent = document.getElementById('scriptContent'); | |
| const scriptPinned = document.getElementById('scriptPinned'); | |
| const scriptId = document.getElementById('scriptId'); | |
| const modalTitle = document.getElementById('modalTitle'); | |
| const addLabelBtn = document.getElementById('addLabelBtn'); | |
| const addLabelForm = document.getElementById('addLabelForm'); | |
| const newLabelInput = document.getElementById('newLabelInput'); | |
| const labelsContainer = document.getElementById('labelsContainer'); | |
| const scriptsContainer = document.getElementById('scriptsContainer'); | |
| const pinnedScriptsContainer = document.getElementById('pinnedScriptsContainer'); | |
| const addScriptBtn = document.getElementById('addScriptBtn'); | |
| const copyNotification = document.getElementById('copyNotification'); | |
| const searchInput = document.getElementById('searchInput'); | |
| const categorySelector = document.getElementById('categorySelector'); | |
| const saveLabelBtn = document.getElementById('saveLabelBtn'); | |
| const cancelLabelBtn = document.getElementById('cancelLabelBtn'); | |
| const closeModalBtn = document.getElementById('closeModalBtn'); | |
| const cancelScriptBtn = document.getElementById('cancelScriptBtn'); | |
| const saveScriptBtn = document.getElementById('saveScriptBtn'); | |
| const boldBtn = document.getElementById('boldBtn'); | |
| const italicBtn = document.getElementById('italicBtn'); | |
| const underlineBtn = document.getElementById('underlineBtn'); | |
| const placeholderBtn = document.getElementById('placeholderBtn'); | |
| const toggleDatesBtn = document.getElementById('toggleDatesBtn'); | |
| const togglePinnedDatesBtn = document.getElementById('togglePinnedDatesBtn'); | |
| const contentPreview = document.getElementById('contentPreview'); | |
| const colorPickerBtn = document.getElementById('colorPickerBtn'); | |
| const colorPalette = document.getElementById('colorPalette'); | |
| const customColorInput = document.getElementById('customColorInput'); | |
| const applyCustomColor = document.getElementById('applyCustomColor'); | |
| // New elements for time zone converter | |
| const appointmentDate = document.getElementById('appointmentDate'); | |
| const appointmentTime = document.getElementById('appointmentTime'); | |
| const timezoneSelect = document.getElementById('timezoneSelect'); | |
| const generateScriptBtn = document.getElementById('generateScriptBtn'); | |
| const downloadCalendarBtn = document.getElementById('downloadCalendarBtn'); | |
| const generatedScriptContainer = document.getElementById('generatedScriptContainer'); | |
| const generatedScript = document.getElementById('generatedScript'); | |
| const copyGeneratedScriptBtn = document.getElementById('copyGeneratedScriptBtn'); | |
| // Import/Export elements | |
| const exportLabelsBtn = document.getElementById('exportLabelsBtn'); | |
| const importLabelsBtn = document.getElementById('importLabelsBtn'); | |
| const labelsFileInput = document.getElementById('labelsFileInput'); | |
| const exportScriptsBtn = document.getElementById('exportScriptsBtn'); | |
| const importScriptsBtn = document.getElementById('importScriptsBtn'); | |
| const scriptsFileInput = document.getElementById('scriptsFileInput'); | |
| const exportAllDataBtn = document.getElementById('exportAllDataBtn'); | |
| const importAllDataBtn = document.getElementById('importAllDataBtn'); | |
| const allDataFileInput = document.getElementById('allDataFileInput'); | |
| // Category filter elements | |
| const categoryFilterButtons = document.querySelector('.category-filter-buttons'); | |
| // State | |
| let showDates = false; | |
| let currentColor = '#000000'; | |
| let selectedCategory = 'all'; // For category filtering | |
| let currentAppointmentDate = null; // Store the current appointment date for calendar download | |
| // Save data to localStorage | |
| function saveData() { | |
| localStorage.setItem('scripts', JSON.stringify(scripts)); | |
| localStorage.setItem('labels', JSON.stringify(labels)); | |
| } | |
| // Populate appointment time dropdown with 15-minute increments | |
| function populateTimeDropdown() { | |
| const timeSelect = document.getElementById('appointmentTime'); | |
| timeSelect.innerHTML = ''; | |
| for (let h = 0; h < 24; h++) { | |
| for (let m = 0; m < 60; m += 15) { | |
| const hours24 = h.toString().padStart(2, '0'); | |
| const mins = m.toString().padStart(2, '0'); | |
| const value = `${hours24}:${mins}`; | |
| const ampm = h >= 12 ? 'PM' : 'AM'; | |
| let displayHour = h % 12; | |
| displayHour = displayHour ? displayHour : 12; | |
| const displayTime = `${displayHour}:${mins} ${ampm}`; | |
| const option = document.createElement('option'); | |
| option.value = value; | |
| option.textContent = displayTime; | |
| timeSelect.appendChild(option); | |
| } | |
| } | |
| } | |
| // Initialize the app | |
| document.addEventListener('DOMContentLoaded', function() { | |
| renderScripts(); | |
| renderPinnedScripts(); | |
| renderLabels(); | |
| renderCategoryButtons(); | |
| setupEventListeners(); | |
| // Populate time dropdown with 15-minute increments | |
| populateTimeDropdown(); | |
| // Set default date to today | |
| const today = new Date(); | |
| const formattedDate = today.toISOString().split('T')[0]; | |
| appointmentDate.value = formattedDate; | |
| // Set default time to current time rounded to next 15 minutes | |
| const minutes = today.getMinutes(); | |
| const roundedMinutes = Math.ceil(minutes / 15) * 15; | |
| today.setMinutes(roundedMinutes); | |
| const hours = today.getHours().toString().padStart(2, '0'); | |
| const mins = today.getMinutes().toString().padStart(2, '0'); | |
| appointmentTime.value = `${hours}:${mins}`; | |
| }); | |
| // Set up event listeners | |
| function setupEventListeners() { | |
| scriptForm.addEventListener('submit', handleFormSubmit); | |
| addLabelBtn.addEventListener('click', showAddLabelForm); | |
| saveLabelBtn.addEventListener('click', saveNewLabel); | |
| cancelLabelBtn.addEventListener('click', hideAddLabelForm); | |
| addScriptBtn.addEventListener('click', showAddScriptModal); | |
| searchInput.addEventListener('input', handleSearch); | |
| closeModalBtn.addEventListener('click', closeModalHandler); | |
| cancelScriptBtn.addEventListener('click', closeModalHandler); | |
| boldBtn.addEventListener('click', () => applyFormatting('bold')); | |
| italicBtn.addEventListener('click', () => applyFormatting('italic')); | |
| underlineBtn.addEventListener('click', () => applyFormatting('underline')); | |
| placeholderBtn.addEventListener('click', insertPlaceholder); | |
| toggleDatesBtn.addEventListener('click', toggleDatesVisibility); | |
| togglePinnedDatesBtn.addEventListener('click', toggleDatesVisibility); | |
| scriptContent.addEventListener('input', updateContentPreview); | |
| // Color picker events | |
| colorPickerBtn.addEventListener('click', toggleColorPalette); | |
| document.addEventListener('click', (e) => { | |
| if (!colorPalette.contains(e.target) && e.target !== colorPickerBtn) { | |
| colorPalette.classList.remove('show'); | |
| } | |
| }); | |
| // Color selection | |
| document.querySelectorAll('.color-option').forEach(option => { | |
| option.addEventListener('click', () => { | |
| currentColor = option.dataset.color; | |
| applyFormatting('color', currentColor); | |
| colorPalette.classList.remove('show'); | |
| }); | |
| }); | |
| // Custom color | |
| applyCustomColor.addEventListener('click', () => { | |
| const color = customColorInput.value.trim(); | |
| if (color) { | |
| currentColor = color; | |
| applyFormatting('color', currentColor); | |
| colorPalette.classList.remove('show'); | |
| } | |
| }); | |
| // Time zone converter events | |
| generateScriptBtn.addEventListener('click', generateAppointmentScript); | |
| copyGeneratedScriptBtn.addEventListener('click', copyGeneratedScript); | |
| downloadCalendarBtn.addEventListener('click', downloadCalendarFile); | |
| // Sort labels button | |
| document.getElementById('sortLabelsBtn').addEventListener('click', sortLabelsAlphabetically); | |
| // Import/Export events | |
| exportLabelsBtn.addEventListener('click', exportLabels); | |
| importLabelsBtn.addEventListener('click', () => labelsFileInput.click()); | |
| labelsFileInput.addEventListener('change', importLabels); | |
| exportScriptsBtn.addEventListener('click', exportScripts); | |
| importScriptsBtn.addEventListener('click', () => scriptsFileInput.click()); | |
| scriptsFileInput.addEventListener('change', importScripts); | |
| exportAllDataBtn.addEventListener('click', exportAllData); | |
| importAllDataBtn.addEventListener('click', () => allDataFileInput.click()); | |
| allDataFileInput.addEventListener('change', importAllData); | |
| } | |
| // Export labels to JSON file | |
| function exportLabels() { | |
| const data = JSON.stringify(labels, null, 2); | |
| downloadFile(data, 'labels.json', 'application/json'); | |
| } | |
| // Import labels from JSON file | |
| function importLabels(event) { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| try { | |
| const importedLabels = JSON.parse(e.target.result); | |
| if (Array.isArray(importedLabels)) { | |
| if (confirm('This will replace your current labels. Continue?')) { | |
| // Migration: convert string labels to objects | |
| labels = importedLabels.map(item => { | |
| if (typeof item === 'string') { | |
| return { text: item, pinned: false }; | |
| } | |
| return item; | |
| }); | |
| saveData(); | |
| renderLabels(); | |
| showCopyNotification('Labels imported successfully!'); | |
| } | |
| } else { | |
| alert('Invalid labels file format'); | |
| } | |
| } catch (error) { | |
| alert('Error parsing labels file: ' + error.message); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| event.target.value = ''; // Reset file input | |
| } | |
| // Export scripts to JSON file | |
| function exportScripts() { | |
| const data = JSON.stringify(scripts, null, 2); | |
| downloadFile(data, 'scripts.json', 'application/json'); | |
| } | |
| // Import scripts from JSON file | |
| function importScripts(event) { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| try { | |
| const importedScripts = JSON.parse(e.target.result); | |
| if (Array.isArray(importedScripts)) { | |
| if (confirm('This will replace your current scripts. Continue?')) { | |
| scripts = importedScripts; | |
| saveData(); | |
| renderScripts(); | |
| renderPinnedScripts(); | |
| showCopyNotification('Scripts imported successfully!'); | |
| } | |
| } else { | |
| alert('Invalid scripts file format'); | |
| } | |
| } catch (error) { | |
| alert('Error parsing scripts file: ' + error.message); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| event.target.value = ''; // Reset file input | |
| } | |
| // Export all data (scripts and labels) to JSON file | |
| function exportAllData() { | |
| const data = { | |
| scripts: scripts, | |
| labels: labels | |
| }; | |
| const jsonData = JSON.stringify(data, null, 2); | |
| downloadFile(jsonData, 'script-manager-data.json', 'application/json'); | |
| } | |
| // Import all data (scripts and labels) from JSON file | |
| function importAllData(event) { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| try { | |
| const importedData = JSON.parse(e.target.result); | |
| if (importedData.scripts && importedData.labels) { | |
| if (confirm('This will replace ALL your current data. Continue?')) { | |
| scripts = importedData.scripts; | |
| labels = importedData.labels; | |
| saveData(); | |
| renderScripts(); | |
| renderPinnedScripts(); | |
| renderLabels(); | |
| showCopyNotification('All data imported successfully!'); | |
| } | |
| } else { | |
| alert('Invalid data file format - missing scripts or labels'); | |
| } | |
| } catch (error) { | |
| alert('Error parsing data file: ' + error.message); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| event.target.value = ''; // Reset file input | |
| } | |
| // Helper function to download a file | |
| function downloadFile(data, filename, type) { | |
| const blob = new Blob([data], { type: type }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| // Generate iCalendar (.ics) file for the appointment | |
| function downloadCalendarFile() { | |
| if (!currentAppointmentDate) { | |
| alert('No appointment time has been generated yet'); | |
| return; | |
| } | |
| const clientName = document.getElementById('clientName').value.trim(); | |
| const clientEmail = document.getElementById('clientEmail').value.trim(); | |
| // Format the date for the iCalendar file | |
| const icsDate = formatDateForICS(currentAppointmentDate); | |
| // Create the iCalendar content | |
| const icsContent = [ | |
| 'BEGIN:VCALENDAR', | |
| 'VERSION:2.0', | |
| 'PRODID:-//Script Manager//Appointment//EN', | |
| 'BEGIN:VEVENT', | |
| `UID:${Date.now()}@scriptmanager.com`, | |
| `DTSTAMP:${icsDate}`, | |
| `DTSTART:${icsDate}`, | |
| `DTEND:${icsDate}`, | |
| `SUMMARY:Appointment with Derrin at UML${clientName ? ' - ' + clientName : ''}`, | |
| 'DESCRIPTION:Your appointment with Derrin at UML', | |
| 'LOCATION:Phone/Online', | |
| clientEmail ? `ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN="${clientName || 'Client'}":mailto:${clientEmail}` : '', | |
| 'END:VEVENT', | |
| 'END:VCALENDAR' | |
| ].filter(line => line !== '').join('\r\n'); | |
| // Create a blob and download it | |
| const blob = new Blob([icsContent], { type: 'text/calendar;charset=utf-8' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'appointment.ics'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| // Format date for iCalendar (UTC format) | |
| function formatDateForICS(date) { | |
| const pad = n => n < 10 ? '0' + n : n; | |
| return date.getUTCFullYear() + | |
| pad(date.getUTCMonth() + 1) + | |
| pad(date.getUTCDate()) + | |
| 'T' + | |
| pad(date.getUTCHours()) + | |
| pad(date.getUTCMinutes()) + | |
| pad(date.getUTCSeconds()) + | |
| 'Z'; | |
| } | |
| // Render category filter buttons | |
| function renderCategoryButtons() { | |
| // Clear existing buttons except "All Categories" | |
| const allCategoriesBtn = categoryFilterButtons.querySelector('.all-categories'); | |
| categoryFilterButtons.innerHTML = ''; | |
| categoryFilterButtons.appendChild(allCategoriesBtn); | |
| // Add buttons for each category | |
| allCategories.forEach(category => { | |
| const button = document.createElement('button'); | |
| button.className = 'category-filter-btn'; | |
| button.textContent = getCategoryName(category); | |
| button.dataset.category = category; | |
| if (selectedCategory === category) { | |
| button.classList.add('active'); | |
| } | |
| button.addEventListener('click', () => { | |
| selectedCategory = category; | |
| renderScripts(); | |
| updateActiveCategoryButtons(); | |
| }); | |
| categoryFilterButtons.appendChild(button); | |
| }); | |
| } | |
| // Update active state of category buttons | |
| function updateActiveCategoryButtons() { | |
| document.querySelectorAll('.category-filter-btn').forEach(btn => { | |
| btn.classList.remove('active'); | |
| if (btn.dataset.category === selectedCategory) { | |
| btn.classList.add('active'); | |
| } | |
| }); | |
| // Special handling for "All Categories" button | |
| const allCategoriesBtn = categoryFilterButtons.querySelector('.all-categories'); | |
| if (selectedCategory === 'all') { | |
| allCategoriesBtn.classList.add('active'); | |
| } else { | |
| allCategoriesBtn.classList.remove('active'); | |
| } | |
| } | |
| // Add event listener for "All Categories" button | |
| document.querySelector('.all-categories').addEventListener('click', () => { | |
| selectedCategory = 'all'; | |
| renderScripts(); | |
| updateActiveCategoryButtons(); | |
| }); | |
| // Generate appointment script with time zone conversion | |
| function generateAppointmentScript() { | |
| if (!appointmentDate.value || !appointmentTime.value) { | |
| alert('Please select both date and time'); | |
| return; | |
| } | |
| // Parse input date and time | |
| const dateParts = appointmentDate.value.split('-'); | |
| const timeParts = appointmentTime.value.split(':'); | |
| // Create date object in EST (assuming input is EST) | |
| const estDate = new Date( | |
| parseInt(dateParts[0]), | |
| parseInt(dateParts[1]) - 1, | |
| parseInt(dateParts[2]), | |
| parseInt(timeParts[0]), | |
| parseInt(timeParts[1]) | |
| ); | |
| // Store the EST date for calendar download | |
| currentAppointmentDate = new Date(estDate); | |
| // Convert to selected timezone | |
| const targetTimezone = timezoneSelect.value; | |
| let convertedTime = ''; | |
| let timezoneLabel = ''; | |
| switch(targetTimezone) { | |
| case 'EST': | |
| // No conversion needed | |
| convertedTime = formatTime(estDate); | |
| timezoneLabel = 'EST'; | |
| break; | |
| case 'CST': | |
| // Subtract 1 hour | |
| estDate.setHours(estDate.getHours() - 1); | |
| convertedTime = formatTime(estDate); | |
| timezoneLabel = 'CST'; | |
| break; | |
| case 'MST': | |
| // Subtract 2 hours | |
| estDate.setHours(estDate.getHours() - 2); | |
| convertedTime = formatTime(estDate); | |
| timezoneLabel = 'MST'; | |
| break; | |
| case 'PST': | |
| // Subtract 3 hours | |
| estDate.setHours(estDate.getHours() - 3); | |
| convertedTime = formatTime(estDate); | |
| timezoneLabel = 'PST'; | |
| break; | |
| } | |
| // Format date as MM/DD/YYYY | |
| const formattedDate = `${(estDate.getMonth() + 1).toString().padStart(2, '0')}/${estDate.getDate().toString().padStart(2, '0')}/${estDate.getFullYear()}`; | |
| // Get day of the week | |
| const dayOfWeek = estDate.toLocaleDateString('en-US', { weekday: 'long' }); | |
| // Get client name if provided | |
| const clientName = document.getElementById('clientName').value.trim(); | |
| const greeting = clientName ? `Hello ${clientName},` : 'Hello,'; | |
| // Generate the script | |
| const script = `${greeting} your appointment with Derrin at UML is booked for ${dayOfWeek} ${formattedDate} at ${convertedTime} ${timezoneLabel}. Please reply with C to confirm or R to reschedule. Call or text this number (313) 490-3990 to reschedule.`; | |
| // Display the generated script | |
| generatedScript.textContent = script; | |
| generatedScriptContainer.classList.remove('hidden'); | |
| downloadCalendarBtn.classList.remove('hidden'); | |
| } | |
| // Format time as AM/PM | |
| function formatTime(date) { | |
| let hours = date.getHours(); | |
| const minutes = date.getMinutes(); | |
| const ampm = hours >= 12 ? 'pm' : 'am'; | |
| hours = hours % 12; | |
| hours = hours ? hours : 12; // the hour '0' should be '12' | |
| return `${hours}:${minutes.toString().padStart(2, '0')} ${ampm}`; | |
| } | |
| // Copy generated script to clipboard | |
| function copyGeneratedScript() { | |
| navigator.clipboard.writeText(generatedScript.textContent).then(() => { | |
| showCopyNotification(); | |
| }).catch(err => { | |
| console.error('Failed to copy text: ', err); | |
| }); | |
| } | |
| // Toggle color palette visibility | |
| function toggleColorPalette() { | |
| colorPalette.classList.toggle('show'); | |
| } | |
| // Apply formatting to selected text | |
| function applyFormatting(formatType, color = null) { | |
| const textarea = scriptContent; | |
| const start = textarea.selectionStart; | |
| const end = textarea.selectionEnd; | |
| const selectedText = textarea.value.substring(start, end); | |
| const beforeText = textarea.value.substring(0, start); | |
| const afterText = textarea.value.substring(end); | |
| let formattedText = ''; | |
| let cursorOffset = 0; | |
| switch(formatType) { | |
| case 'bold': | |
| formattedText = `**${selectedText}**`; | |
| cursorOffset = 2; | |
| break; | |
| case 'italic': | |
| formattedText = `_${selectedText}_`; | |
| cursorOffset = 1; | |
| break; | |
| case 'underline': | |
| formattedText = `<u>${selectedText}</u>`; | |
| cursorOffset = 4; | |
| break; | |
| case 'color': | |
| // Check if the color is predefined or custom | |
| const isPredefined = predefinedColors.includes(color.toLowerCase()); | |
| if (isPredefined) { | |
| formattedText = `<span class="text-color-${color.replace('#', '')}">${selectedText}</span>`; | |
| } else { | |
| formattedText = `<span style="color: ${color}">${selectedText}</span>`; | |
| } | |
| cursorOffset = 0; | |
| break; | |
| } | |
| if (start !== end) { | |
| // Text is selected, wrap it with formatting | |
| textarea.value = beforeText + formattedText + afterText; | |
| // Set cursor position after the formatted text | |
| textarea.setSelectionRange( | |
| start + cursorOffset, | |
| end + cursorOffset | |
| ); | |
| } else { | |
| // No text selected, insert formatting tags and place cursor between them | |
| textarea.value = beforeText + formattedText + afterText; | |
| textarea.setSelectionRange( | |
| start + cursorOffset, | |
| start + cursorOffset | |
| ); | |
| } | |
| textarea.focus(); | |
| updateContentPreview(); | |
| } | |
| // Update content preview | |
| function updateContentPreview() { | |
| // Convert markdown-style formatting to HTML | |
| let previewText = scriptContent.value | |
| .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // bold | |
| .replace(/\_(.*?)\_/g, '<em>$1</em>') // italic | |
| .replace(/<u>(.*?)<\/u>/g, '<u>$1</u>') // underline | |
| .replace(/<span class="text-color-(.*?)">(.*?)<\/span>/g, '<span style="color: #$1">$2</span>') // predefined colors | |
| .replace(/<span style="color: (.*?)">(.*?)<\/span>/g, '<span style="color: $1">$2</span>') // custom colors | |
| .replace(/\n/g, '<br>') // line breaks | |
| .replace(/\[(.*?)\]/g, '<span class="bg-blue-100 text-blue-800 px-1 rounded">[$1]</span>'); // placeholders | |
| contentPreview.innerHTML = previewText; | |
| } | |
| // Toggle dates visibility | |
| function toggleDatesVisibility() { | |
| showDates = !showDates; | |
| if (showDates) { | |
| toggleDatesBtn.classList.add('active'); | |
| togglePinnedDatesBtn.classList.add('active'); | |
| togglePinnedDatesBtn.innerHTML = '<i class="fa-solid fa-eye-slash"></i>'; | |
| } else { | |
| toggleDatesBtn.classList.remove('active'); | |
| togglePinnedDatesBtn.classList.remove('active'); | |
| togglePinnedDatesBtn.innerHTML = '<i class="fa-solid fa-eye"></i>'; | |
| } | |
| renderScripts(); | |
| renderPinnedScripts(); | |
| } | |
| // Move label up in the list | |
| function moveLabelUp(index) { | |
| if (index > 0) { | |
| // Swap with previous item | |
| [labels[index], labels[index - 1]] = [labels[index - 1], labels[index]]; | |
| saveData(); | |
| renderLabels(); | |
| } | |
| } | |
| // Move label down in the list | |
| function moveLabelDown(index) { | |
| if (index < labels.length - 1) { | |
| // Swap with next item | |
| [labels[index], labels[index + 1]] = [labels[index + 1], labels[index]]; | |
| saveData(); | |
| renderLabels(); | |
| } | |
| } | |
| // Render category selector | |
| function renderCategorySelector(selectedCategories = []) { | |
| categorySelector.innerHTML = ''; | |
| allCategories.forEach(category => { | |
| const isSelected = selectedCategories.includes(category); | |
| const option = document.createElement('div'); | |
| option.className = `category-option ${isSelected ? 'selected' : ''}`; | |
| option.innerHTML = ` | |
| <input type="checkbox" id="cat-${category}" value="${category}" ${isSelected ? 'checked' : ''} style="display: none;"> | |
| ${getCategoryName(category)} | |
| `; | |
| option.addEventListener('click', () => { | |
| const checkbox = option.querySelector('input'); | |
| checkbox.checked = !checkbox.checked; | |
| option.classList.toggle('selected'); | |
| }); | |
| categorySelector.appendChild(option); | |
| }); | |
| } | |
| // Get selected categories | |
| function getSelectedCategories() { | |
| const checkboxes = categorySelector.querySelectorAll('input:checked'); | |
| return Array.from(checkboxes).map(cb => cb.value); | |
| } | |
| // Handle search input | |
| function handleSearch() { | |
| const searchTerm = searchInput.value.trim().toLowerCase(); | |
| if (searchTerm === '') { | |
| renderScripts(); | |
| return; | |
| } | |
| const filteredScripts = scripts.filter(script => { | |
| return script.title.toLowerCase().includes(searchTerm) || | |
| script.content.toLowerCase().includes(searchTerm); | |
| }); | |
| renderScripts(filteredScripts, searchTerm); | |
| } | |
| // Highlight search term in text | |
| function highlightText(text, searchTerm) { | |
| if (!searchTerm) return text; | |
| const regex = new RegExp(searchTerm, 'gi'); | |
| return text.replace(regex, match => `<span class="highlight">${match}</span>`); | |
| } | |
| // Render all scripts (with optional filtering for search) | |
| function renderScripts(scriptsToRender = null, searchTerm = '') { | |
| scriptsContainer.innerHTML = ''; | |
| let scriptsToDisplay = scriptsToRender || scripts; | |
| // Apply category filter if a specific category is selected | |
| if (selectedCategory !== 'all') { | |
| scriptsToDisplay = scriptsToDisplay.filter(script => { | |
| return script.categories.includes(selectedCategory); | |
| }); | |
| } | |
| scriptsToDisplay.sort((a, b) => { | |
| return new Date(b.createdAt) - new Date(a.createdAt); | |
| }).forEach(script => { | |
| const scriptElement = document.createElement('div'); | |
| scriptElement.className = 'script-card bg-gray-50 p-4 rounded-lg border'; | |
| // Highlight search terms in title and content | |
| const highlightedTitle = highlightText(script.title, searchTerm); | |
| const highlightedContent = highlightText(script.content, searchTerm); | |
| // Render categories as tags | |
| const categoriesHtml = script.categories.map(cat => | |
| `<span class="category-tag">${getCategoryName(cat)}</span>` | |
| ).join(''); | |
| // Date information | |
| const dateInfo = showDates ? ` | |
| <div class="mt-3 text-xs text-gray-400"> | |
| Created: ${formatDate(script.createdAt)} | |
| ${script.updatedAt ? `<br>Updated: ${formatDate(script.updatedAt)}</br>` : ''} | |
| </div> | |
| ` : ''; | |
| scriptElement.innerHTML = ` | |
| <div class="flex justify-between items-start"> | |
| <div> | |
| <h3 class="font-medium text-lg">${highlightedTitle}</h3> | |
| <div class="text-sm text-gray-500 mb-2">${categoriesHtml}</div> | |
| </div> | |
| <div class="flex gap-2"> | |
| <button onclick="copyScript(${script.id})" class="text-gray-400 hover:text-blue-500" title="Copy script"> | |
| <i class="fas fa-copy"></i> | |
| </button> | |
| <button onclick="togglePinScript(${script.id})" class="text-gray-400 hover:text-yellow-500" title="${script.pinned ? 'Unpin' : 'Pin'}"> | |
| <i class="fas ${script.pinned ? 'fa-star text-yellow-400' : 'fa-star'}"></i> | |
| </button> | |
| <button onclick="editScript(${script.id})" class="text-gray-400 hover:text-blue-500" title="Edit"> | |
| <i class="fas fa-edit"></i> | |
| </button> | |
| <button onclick="deleteScript(${script.id})" class="text-gray-400 hover:text-red-500" title="Delete"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mt-2 whitespace-pre-line text-gray-700">${formatScriptContent(highlightedContent)}</div> | |
| ${dateInfo} | |
| `; | |
| scriptsContainer.appendChild(scriptElement); | |
| }); | |
| if (scriptsToDisplay.length === 0) { | |
| scriptsContainer.innerHTML = ` | |
| <div class="text-center py-8 text-gray-500"> | |
| <i class="fas fa-search text-2xl mb-2"></i> | |
| <p>No scripts found matching your ${selectedCategory !== 'all' ? 'category filter' : 'search'}.</p> | |
| </div> | |
| `; | |
| } | |
| } | |
| // Format script content with HTML tags | |
| function formatScriptContent(content) { | |
| return content | |
| .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // bold | |
| .replace(/\_(.*?)\_/g, '<em>$1</em>') // italic | |
| .replace(/<u>(.*?)<\/u>/g, '<u>$1</u>') // underline | |
| .replace(/<span class="text-color-(.*?)">(.*?)<\/span>/g, '<span style="color: #$1">$2</span>') // predefined colors | |
| .replace(/<span style="color: (.*?)">(.*?)<\/span>/g, '<span style="color: $1">$2</span>') // custom colors | |
| .replace(/\n/g, '<br>') // line breaks | |
| .replace(/\[(.*?)\]/g, '<span class="bg-blue-100 text-blue-800 px-1 rounded">[$1]</span>'); // placeholders | |
| } | |
| // Render pinned scripts | |
| function renderPinnedScripts() { | |
| pinnedScriptsContainer.innerHTML = ''; | |
| const pinnedScripts = scripts.filter(script => script.pinned); | |
| if (pinnedScripts.length === 0) { | |
| pinnedScriptsContainer.innerHTML = ` | |
| <div class="col-span-2 text-center py-8 text-gray-500"> | |
| <i class="fas fa-star text-2xl mb-2"></i> | |
| <p>No pinned scripts yet. Pin your favorite scripts for quick access.</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| pinnedScripts.forEach(script => { | |
| const scriptElement = document.createElement('div'); | |
| scriptElement.className = 'script-card bg-gray-50 p-4 rounded-lg border'; | |
| // Render categories as tags | |
| const categoriesHtml = script.categories.map(cat => | |
| `<span class="category-tag">${getCategoryName(cat)}</span>` | |
| ).join(''); | |
| // Date information | |
| const dateInfo = showDates ? ` | |
| <div class="mt-3 text-xs text-gray-400"> | |
| Created: ${formatDate(script.createdAt)} | |
| ${script.updatedAt ? `<br>Updated: ${formatDate(script.updatedAt)}</br>` : ''} | |
| </div> | |
| ` : ''; | |
| scriptElement.innerHTML = ` | |
| <div class="flex justify-between items-start"> | |
| <div> | |
| <h3 class="font-medium text-lg">${script.title}</h3> | |
| <div class="text-sm text-gray-500 mb-2">${categoriesHtml}</div> | |
| </div> | |
| <div class="flex gap-2"> | |
| <button onclick="copyScript(${script.id})" class="text-gray-400 hover:text-blue-500" title="Copy script"> | |
| <i class="fas fa-copy"></i> | |
| </button> | |
| <button onclick="moveScriptUp(${script.id})" class="move-btn text-gray-500 hover:text-blue-500" title="Move up"> | |
| <i class="fas fa-arrow-up text-xs"></i> | |
| </button> | |
| <button onclick="moveScriptDown(${script.id})" class="move-btn text-gray-500 hover:text-blue-500" title="Move down"> | |
| <i class="fas fa-arrow-down text-xs"></i> | |
| </button> | |
| <button onclick="togglePinScript(${script.id})" class="text-yellow-500 hover:text-yellow-600" title="Unpin"> | |
| <i class="fas fa-star"></i> | |
| </button> | |
| <button onclick="editScript(${script.id})" class="text-gray-400 hover:text-blue-500" title="Edit"> | |
| <i class="fas fa-edit"></i> | |
| </button> | |
| <button onclick="deleteScript(${script.id})" class="text-gray-400 hover:text-red-500" title="Delete"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mt-2 whitespace-pre-line text-gray-700">${formatScriptContent(script.content)}</div> | |
| ${dateInfo} | |
| `; | |
| pinnedScriptsContainer.appendChild(scriptElement); | |
| }); | |
| } | |
| // Render labels with up/down arrows and pin support | |
| function renderLabels() { | |
| labelsContainer.innerHTML = ''; | |
| // Sort: pinned labels first, then unpinned, maintaining relative order | |
| const sortedLabels = labels.map((label, originalIndex) => ({ ...label, originalIndex })); | |
| sortedLabels.sort((a, b) => { | |
| if (a.pinned && !b.pinned) return -1; | |
| if (!a.pinned && b.pinned) return 1; | |
| return 0; | |
| }); | |
| sortedLabels.forEach((label) => { | |
| const index = label.originalIndex; | |
| const labelElement = document.createElement('div'); | |
| labelElement.className = `label-item p-3 rounded flex justify-between items-center ${label.pinned ? 'bg-yellow-50 border-l-4 border-yellow-400' : 'bg-gray-100'}`; | |
| labelElement.dataset.labelIndex = index; | |
| labelElement.innerHTML = ` | |
| <div class="flex items-center"> | |
| <span>${label.text}</span> | |
| </div> | |
| <div class="flex gap-2"> | |
| <button onclick="togglePinLabel(${index})" class="${label.pinned ? 'text-yellow-500 hover:text-yellow-600' : 'text-gray-400 hover:text-yellow-500'}" title="${label.pinned ? 'Unpin label' : 'Pin label to top'}"> | |
| <i class="fas fa-thumbtack text-xs"></i> | |
| </button> | |
| <button onclick="moveLabelUp(${index})" class="move-btn text-gray-500 hover:text-blue-500" title="Move up"> | |
| <i class="fas fa-arrow-up text-xs"></i> | |
| </button> | |
| <button onclick="moveLabelDown(${index})" class="move-btn text-gray-500 hover:text-blue-500" title="Move down"> | |
| <i class="fas fa-arrow-down text-xs"></i> | |
| </button> | |
| <button onclick="editLabel(${index})" class="text-gray-500 hover:text-blue-500"> | |
| <i class="fas fa-edit text-xs"></i> | |
| </button> | |
| <button onclick="deleteLabel(${index})" class="text-gray-500 hover:text-red-500"> | |
| <i class="fas fa-trash text-xs"></i> | |
| </button> | |
| </div> | |
| `; | |
| labelsContainer.appendChild(labelElement); | |
| }); | |
| } | |
| // Copy script to clipboard | |
| function copyScript(id) { | |
| const script = scripts.find(s => s.id === id); | |
| if (script) { | |
| // Remove formatting tags when copying to clipboard | |
| const cleanContent = script.content | |
| .replace(/\*\*/g, '') | |
| .replace(/_/g, '') | |
| .replace(/<u>/g, '') | |
| .replace(/<\/u>/g, '') | |
| .replace(/<span class="text-color-(.*?)">/g, '') | |
| .replace(/<span style="color: (.*?)">/g, '') | |
| .replace(/<\/span>/g, ''); | |
| const textToCopy = `${script.title}\n\n${cleanContent.replace(/<[^>]*>/g, '')}`; | |
| navigator.clipboard.writeText(textToCopy).then(() => { | |
| showCopyNotification(); | |
| }).catch(err => { | |
| console.error('Failed to copy text: ', err); | |
| }); | |
| } | |
| } | |
| // Show copy notification | |
| function showCopyNotification(message = 'Script copied to clipboard!') { | |
| copyNotification.innerHTML = `<i class="fas fa-check-circle mr-2"></i> ${message}`; | |
| copyNotification.classList.add('show'); | |
| setTimeout(() => { | |
| copyNotification.classList.remove('show'); | |
| }, 2000); | |
| } | |
| // Show add script modal | |
| function showAddScriptModal() { | |
| scriptId.value = ''; | |
| scriptTitle.value = ''; | |
| scriptContent.value = ''; | |
| scriptPinned.checked = false; | |
| modalTitle.textContent = 'Add New Script'; | |
| renderCategorySelector(); | |
| scriptModal.classList.remove('hidden'); | |
| contentPreview.innerHTML = ''; | |
| } | |
| // Edit script | |
| function editScript(id) { | |
| const script = scripts.find(s => s.id === id); | |
| if (script) { | |
| scriptId.value = script.id; | |
| scriptTitle.value = script.title; | |
| scriptContent.value = script.content; | |
| scriptPinned.checked = script.pinned; | |
| modalTitle.textContent = 'Edit Script'; | |
| renderCategorySelector(script.categories); | |
| scriptModal.classList.remove('hidden'); | |
| updateContentPreview(); | |
| } | |
| } | |
| // Handle form submission | |
| function handleFormSubmit(e) { | |
| e.preventDefault(); | |
| const id = scriptId.value ? parseInt(scriptId.value) : Date.now(); | |
| const title = scriptTitle.value.trim(); | |
| const content = scriptContent.value.trim(); | |
| const categories = getSelectedCategories(); | |
| const pinned = scriptPinned.checked; | |
| if (!title || !content || categories.length === 0) { | |
| alert('Please fill in all fields and select at least one category'); | |
| return; | |
| } | |
| const now = new Date().toISOString(); | |
| if (scriptId.value) { | |
| // Update existing script | |
| const index = scripts.findIndex(s => s.id === id); | |
| if (index !== -1) { | |
| scripts[index] = { | |
| ...scripts[index], | |
| title, | |
| content, | |
| categories, | |
| pinned, | |
| updatedAt: now | |
| }; | |
| } | |
| } else { | |
| // Add new script | |
| scripts.push({ | |
| id, | |
| title, | |
| content, | |
| categories, | |
| pinned, | |
| createdAt: now, | |
| updatedAt: null | |
| }); | |
| } | |
| saveData(); | |
| renderScripts(); | |
| renderPinnedScripts(); | |
| closeModalHandler(); | |
| } | |
| // Delete script | |
| function deleteScript(id) { | |
| if (confirm('Are you sure you want to delete this script?')) { | |
| scripts = scripts.filter(script => script.id !== id); | |
| saveData(); | |
| renderScripts(); | |
| renderPinnedScripts(); | |
| } | |
| } | |
| // Toggle pin status | |
| function togglePinScript(id) { | |
| const script = scripts.find(s => s.id === id); | |
| if (script) { | |
| script.pinned = !script.pinned; | |
| script.updatedAt = new Date().toISOString(); | |
| saveData(); | |
| renderScripts(); | |
| renderPinnedScripts(); | |
| } | |
| } | |
| // Close modal | |
| function closeModalHandler() { | |
| scriptModal.classList.add('hidden'); | |
| } | |
| // Move script up in pinned list | |
| function moveScriptUp(id) { | |
| const pinnedScripts = scripts.filter(script => script.pinned); | |
| const scriptIndex = pinnedScripts.findIndex(s => s.id === id); | |
| if (scriptIndex > 0) { | |
| // Find the actual indices in the main scripts array | |
| const scriptMainIndex = scripts.findIndex(s => s.id === id); | |
| const prevScriptId = pinnedScripts[scriptIndex - 1].id; | |
| const prevScriptMainIndex = scripts.findIndex(s => s.id === prevScriptId); | |
| // Reorder directly in the scripts array | |
| [scripts[scriptMainIndex], scripts[prevScriptMainIndex]] = | |
| [scripts[prevScriptMainIndex], scripts[scriptMainIndex]]; | |
| // Update timestamps | |
| scripts[scriptMainIndex].updatedAt = new Date().toISOString(); | |
| scripts[prevScriptMainIndex].updatedAt = new Date().toISOString(); | |
| saveData(); | |
| renderPinnedScripts(); | |
| } | |
| } | |
| // Move script down in pinned list | |
| function moveScriptDown(id) { | |
| const pinnedScripts = scripts.filter(script => script.pinned); | |
| const scriptIndex = pinnedScripts.findIndex(s => s.id === id); | |
| if (scriptIndex < pinnedScripts.length - 1) { | |
| // Find the actual indices in the main scripts array | |
| const scriptMainIndex = scripts.findIndex(s => s.id === id); | |
| const nextScriptId = pinnedScripts[scriptIndex + 1].id; | |
| const nextScriptMainIndex = scripts.findIndex(s => s.id === nextScriptId); | |
| // Reorder directly in the scripts array | |
| [scripts[scriptMainIndex], scripts[nextScriptMainIndex]] = | |
| [scripts[nextScriptMainIndex], scripts[scriptMainIndex]]; | |
| // Update timestamps | |
| scripts[scriptMainIndex].updatedAt = new Date().toISOString(); | |
| scripts[nextScriptMainIndex].updatedAt = new Date().toISOString(); | |
| saveData(); | |
| renderPinnedScripts(); | |
| } | |
| } | |
| // Show add label form | |
| function showAddLabelForm() { | |
| addLabelForm.classList.remove('hidden'); | |
| newLabelInput.focus(); | |
| } | |
| // Hide add label form | |
| function hideAddLabelForm() { | |
| addLabelForm.classList.add('hidden'); | |
| newLabelInput.value = ''; | |
| } | |
| // Save new label | |
| function saveNewLabel() { | |
| const newLabel = newLabelInput.value.trim(); | |
| if (newLabel) { | |
| labels.push({ text: newLabel, pinned: false }); | |
| saveData(); | |
| renderLabels(); | |
| hideAddLabelForm(); | |
| } | |
| } | |
| // Edit label | |
| function editLabel(index) { | |
| const currentLabel = labels[index].text; | |
| const labelItem = document.querySelector(`.label-item[data-label-index="${index}"]`); | |
| const input = document.createElement('input'); | |
| input.type = 'text'; | |
| input.className = 'label-edit-input w-full px-2 py-1 border rounded'; | |
| input.value = currentLabel; | |
| labelItem.innerHTML = ''; | |
| labelItem.appendChild(input); | |
| input.focus(); | |
| const saveEdit = () => { | |
| const newLabel = input.value.trim(); | |
| if (newLabel) { | |
| labels[index].text = newLabel; | |
| saveData(); | |
| renderLabels(); | |
| } | |
| }; | |
| input.addEventListener('blur', saveEdit); | |
| input.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| saveEdit(); | |
| } | |
| }); | |
| } | |
| // Toggle pin status for a label | |
| function togglePinLabel(index) { | |
| labels[index].pinned = !labels[index].pinned; | |
| saveData(); | |
| renderLabels(); | |
| } | |
| // Delete label | |
| function deleteLabel(index) { | |
| if (confirm('Are you sure you want to delete this label?')) { | |
| labels.splice(index, 1); | |
| saveData(); | |
| renderLabels(); | |
| } | |
| } | |
| // Insert placeholder | |
| function insertPlaceholder() { | |
| const textarea = scriptContent; | |
| const start = textarea.selectionStart; | |
| const end = textarea.selectionEnd; | |
| const beforeText = textarea.value.substring(0, start); | |
| const afterText = textarea.value.substring(end); | |
| textarea.value = beforeText + '[Variable]' + afterText; | |
| textarea.focus(); | |
| textarea.setSelectionRange(start + '[Variable]'.length, start + '[Variable]'.length); | |
| updateContentPreview(); | |
| } | |
| // Get category name | |
| function getCategoryName(category) { | |
| const categories = { | |
| 'intro': 'Introduction', | |
| 'General': 'General', | |
| 'documents_requested': 'Documents Request', | |
| 'VA IRRRL': 'VA IRRRL', | |
| 'Lost Contact/Gaslight': 'Lost Contact', | |
| 'Preapproval/Purchase': 'Preapproval', | |
| 'Funny': 'Funny', | |
| 'Call Script': 'Call Script' | |
| }; | |
| return categories[category] || category; | |
| } | |
| // Format date | |
| function formatDate(dateString) { | |
| if (!dateString) return ''; | |
| const date = new Date(dateString); | |
| return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); | |
| } | |
| // Sort labels alphabetically (pinned labels keep their order; only unpinned are sorted) | |
| function sortLabelsAlphabetically() { | |
| const pinnedLabels = labels.filter(l => l.pinned); | |
| const unpinnedLabels = labels.filter(l => !l.pinned).sort((a, b) => a.text.localeCompare(b.text, undefined, { sensitivity: 'base' })); | |
| labels = [...pinnedLabels, ...unpinnedLabels]; | |
| saveData(); | |
| renderLabels(); | |
| showCopyNotification('Labels sorted A-Z!'); | |
| } | |
| // Make functions available globally | |
| window.copyScript = copyScript; | |
| window.moveScriptUp = moveScriptUp; | |
| window.moveScriptDown = moveScriptDown; | |
| window.togglePinScript = togglePinScript; | |
| window.editScript = editScript; | |
| window.deleteScript = deleteScript; | |
| window.editLabel = editLabel; | |
| window.deleteLabel = deleteLabel; | |
| window.moveLabelUp = moveLabelUp; | |
| window.moveLabelDown = moveLabelDown; | |
| window.togglePinLabel = togglePinLabel; | |
| window.closeModalHandler = closeModalHandler; | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Honda219/text-scripts" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |