this repo has no description
at react-mobile 706 lines 28 kB view raw
1import { UndoManager } from './undoManager'; 2import { VALUES } from './values'; 3 4// Define interfaces for our value cards and overall app state. 5export interface ValueCard { 6 id: number; 7 name: string; 8 column: string; // Part1: "unassigned", "notImportant", "important", "veryImportant" 9 // Part2: same as Part1 10 // Part3: "core" or "additional" (for cards carried over from veryImportant) 11 order: number; 12 description?: string; // Add optional description for custom cards 13 isCustom?: boolean; // Flag for custom cards 14} 15 16export interface AppState { 17 currentPart: 'part1' | 'part2' | 'part3' | 'part4' | 'review'; 18 cards: ValueCard[]; 19 // In part 4, user can add final statements for each core value (by card id) 20 finalStatements: Record<number, string>; 21 valueSet: 'limited' | 'all'; // Add state for current value set 22 editingDescriptionCardId: number | null; // ID of card whose description is being edited 23} 24 25// Use type-only import 26 27// Define the structure based on the imported VALUES 28interface ValueDefinition { 29 name: string; 30 description: string; 31} 32 33// Recreate ALL_VALUES and LIMITED_VALUES based on the import 34const ALL_VALUE_DEFINITIONS: ValueDefinition[] = VALUES; 35const LIMITED_VALUE_DEFINITIONS = ALL_VALUE_DEFINITIONS.slice(0, 10); 36 37// Create a global map for easy description lookup 38const valueDefinitionsMap = new Map<string, string>( 39 ALL_VALUE_DEFINITIONS.map((def: ValueDefinition) => [def.name, def.description]), 40); 41 42// Main application class 43export class App { 44 private state: AppState; 45 public undoManager: UndoManager<AppState>; 46 private storageKey = 'valuesExerciseState'; 47 private nextCustomCardId = -1; // Counter for unique negative IDs 48 49 constructor() { 50 // Load state from localStorage or initialize default state. 51 const saved = localStorage.getItem(this.storageKey); 52 let initialState: AppState; 53 if (saved) { 54 try { 55 initialState = JSON.parse(saved) as AppState; 56 // Ensure editing state is null initially 57 initialState.editingDescriptionCardId = null; 58 const minId = Math.min(0, ...initialState.cards.filter((c) => c.isCustom).map((c) => c.id)); 59 this.nextCustomCardId = minId - 1; 60 } catch { 61 initialState = this.defaultState('limited'); 62 this.nextCustomCardId = -1; // Reset on error 63 } 64 } else { 65 initialState = this.defaultState('limited'); 66 this.nextCustomCardId = -1; // Reset if no saved state 67 } 68 this.state = initialState; 69 this.undoManager = new UndoManager<AppState>(this.state); 70 71 this.bindEventListeners(); 72 this.render(); 73 this.updateUndoRedoButtons(); 74 } 75 76 // Default state with sample value cards based on the selected set 77 public defaultState(valueSet: 'limited' | 'all' = 'limited'): AppState { 78 const definitionsToUse = valueSet === 'all' ? ALL_VALUE_DEFINITIONS : LIMITED_VALUE_DEFINITIONS; 79 const sampleCards: ValueCard[] = definitionsToUse.map((definition: ValueDefinition, index: number) => ({ 80 id: index + 1, 81 name: definition.name, // Use name from definition 82 column: 'unassigned', 83 order: index, 84 description: undefined, // Ensure built-in cards start with no override 85 isCustom: false, 86 })); 87 return { 88 currentPart: 'part1', 89 cards: sampleCards, 90 finalStatements: {}, 91 valueSet: valueSet, // Store the set used 92 editingDescriptionCardId: null, // Start with no editing 93 }; 94 } 95 96 // Save the state to localStorage. 97 private saveState() { 98 localStorage.setItem(this.storageKey, JSON.stringify(this.state)); 99 } 100 101 // Update state via the undoManager then re-render. 102 public updateState(newState: AppState) { 103 this.undoManager.execute(newState); 104 this.state = this.undoManager.getState(); 105 this.saveState(); 106 this.render(); 107 this.updateUndoRedoButtons(); 108 } 109 110 // Method to toggle between limited and all values 111 public toggleValueSet() { 112 const currentState = this.undoManager.getState(); 113 const nextSet = currentState.valueSet === 'limited' ? 'all' : 'limited'; 114 // Generate a fresh default state for the *new* set, resetting progress 115 const newState = this.defaultState(nextSet); 116 // Execute this change through the undo manager 117 this.updateState(newState); 118 } 119 120 // --- Add Value Form Logic --- 121 public showAddValueForm() { 122 const form = document.getElementById('addValueForm') as HTMLDivElement | null; 123 const nameInput = document.getElementById('newValueName') as HTMLInputElement | null; 124 const descInput = document.getElementById('newValueDesc') as HTMLTextAreaElement | null; 125 if (form && nameInput && descInput) { 126 nameInput.value = ''; // Clear previous input 127 descInput.value = ''; 128 form.style.display = 'flex'; // Show the modal 129 nameInput.focus(); // Focus the name input 130 } 131 } 132 133 public hideAddValueForm() { 134 const form = document.getElementById('addValueForm') as HTMLDivElement | null; 135 if (form) { 136 form.style.display = 'none'; // Hide the modal 137 } 138 } 139 140 public saveNewValue() { 141 const nameInput = document.getElementById('newValueName') as HTMLInputElement | null; 142 const descInput = document.getElementById('newValueDesc') as HTMLTextAreaElement | null; 143 const name = nameInput?.value.trim().toUpperCase(); // Normalize name 144 const description = descInput?.value.trim(); 145 146 if (!name) { 147 alert('Please enter a name for the new value.'); 148 nameInput?.focus(); 149 return; 150 } 151 if (!description) { 152 alert('Please enter a description for the new value.'); 153 descInput?.focus(); 154 return; 155 } 156 157 const newState = this.undoManager.getState(); 158 159 // Check for duplicates (case-insensitive) 160 if (newState.cards.some((card) => card.name.toUpperCase() === name)) { 161 alert(`Value "${name}" already exists.`); 162 nameInput?.focus(); 163 return; 164 } 165 166 // Create new card 167 const newCard: ValueCard = { 168 id: this.nextCustomCardId, 169 name: name, 170 description: description, // Store description directly on card 171 column: 'unassigned', // Add to unassigned in current view 172 order: newState.cards.length, // Add to end 173 isCustom: true, 174 }; 175 176 this.nextCustomCardId--; // Decrement for next custom card 177 178 newState.cards.push(newCard); 179 this.updateState(newState); 180 this.hideAddValueForm(); 181 } 182 183 // --- Description Edit Logic --- 184 public startEditingDescription(cardId: number) { 185 const newState = this.undoManager.getState(); 186 newState.editingDescriptionCardId = cardId; 187 this.updateState(newState); // Re-render to show the textarea 188 } 189 190 public saveDescriptionEdit(cardId: number, newDescription: string) { 191 const newState = this.undoManager.getState(); 192 const cardToUpdate = newState.cards.find((c) => c.id === cardId); 193 if (cardToUpdate) { 194 cardToUpdate.description = newDescription.trim(); // Save trimmed description 195 } 196 newState.editingDescriptionCardId = null; // Stop editing 197 this.updateState(newState); 198 } 199 200 public cancelDescriptionEdit() { 201 const newState = this.undoManager.getState(); 202 newState.editingDescriptionCardId = null; // Just stop editing, don't save 203 this.updateState(newState); 204 } 205 206 // Bind event listeners for UI interactions. 207 private bindEventListeners() { 208 // Remove alias: Arrow functions capture 'this' correctly 209 // const appInstance = this; 210 211 // --- Add Value Form Listeners --- 212 document.getElementById('showAddValueFormBtn')?.addEventListener('click', () => { 213 this.showAddValueForm(); 214 }); 215 document.getElementById('saveNewValueBtn')?.addEventListener('click', () => { 216 this.saveNewValue(); 217 }); 218 document.getElementById('cancelNewValueBtn')?.addEventListener('click', () => { 219 this.hideAddValueForm(); 220 }); 221 // Close modal if clicking outside the content 222 document.getElementById('addValueForm')?.addEventListener('click', (e) => { 223 if (e.target === e.currentTarget) { 224 this.hideAddValueForm(); 225 } 226 }); 227 228 // Navigation buttons 229 document.getElementById('toPart2')?.addEventListener('click', () => { 230 const newState = this.undoManager.getState(); 231 // Check for unassigned cards before proceeding 232 const unassignedCount = newState.cards.filter((card) => card.column === 'unassigned').length; 233 if (unassignedCount > 0) { 234 alert(`Please sort all ${unassignedCount} unassigned value(s) before proceeding to Part 2.`); 235 return; 236 } 237 newState.currentPart = 'part2'; 238 newState.cards = newState.cards 239 .filter((card) => card.column === 'veryImportant') 240 .map((card) => ({ ...card, column: 'unassigned' })); 241 this.updateState(newState); // Call updateState directly 242 }); 243 document.getElementById('toPart3')?.addEventListener('click', () => { 244 const newState = this.undoManager.getState(); 245 // Check for unassigned cards before proceeding 246 const unassignedCount = newState.cards.filter((card) => card.column === 'unassigned').length; 247 if (unassignedCount > 0) { 248 alert(`Please sort all ${unassignedCount} unassigned value(s) before proceeding.`); 249 return; 250 } 251 const veryImportantCards = newState.cards.filter((c) => c.column === 'veryImportant'); 252 const veryImportantCount = veryImportantCards.length; 253 254 if (veryImportantCount <= 5) { 255 newState.currentPart = 'part4'; 256 } else { 257 newState.currentPart = 'part3'; 258 } 259 // Move all "veryImportant" cards to "core" regardless of the next part 260 newState.cards = veryImportantCards.map((c, idx) => ({ ...c, column: 'core', order: idx })); 261 262 this.updateState(newState); // Call updateState directly 263 }); 264 document.getElementById('toPart4')?.addEventListener('click', () => { 265 const newState = this.undoManager.getState(); 266 const coreCount = newState.cards.filter((c) => c.column === 'core').length; 267 if (coreCount > 5) { 268 alert("You can only have 5 core values! Please move some values to 'Also Something I Want' before continuing."); 269 return; // Don't update state if validation fails 270 } 271 newState.currentPart = 'part4'; 272 this.updateState(newState); // Call updateState directly 273 }); 274 document.getElementById('finish')?.addEventListener('click', () => { 275 const newState = this.undoManager.getState(); 276 // Check if all core values have statements 277 const coreCards = newState.cards.filter((c) => c.column === 'core'); 278 const missingStatements = coreCards.filter((card) => !newState.finalStatements[card.id]?.trim()); 279 280 if (missingStatements.length > 0) { 281 alert( 282 `Please provide a statement for all core values. Missing: ${missingStatements.map((c) => c.name).join(', ')}`, 283 ); 284 return; // Prevent transition 285 } 286 287 // Original transition logic 288 newState.currentPart = 'review'; 289 this.updateState(newState); // Call updateState directly 290 }); 291 document.getElementById('restart')?.addEventListener('click', () => { 292 const newState = this.defaultState(); 293 this.updateState(newState); // Call updateState directly 294 }); 295 296 // --- Add listeners for the toggle buttons --- 297 document.getElementById('useLimitedValuesBtn')?.addEventListener('click', () => { 298 // Use captured instance 299 const currentSet = this.undoManager.getState().valueSet; 300 if (currentSet !== 'limited') { 301 if (confirm('Switching value sets will reset your current progress. Are you sure?')) { 302 this.toggleValueSet(); 303 } 304 } 305 }); 306 document.getElementById('useAllValuesBtn')?.addEventListener('click', () => { 307 // Use captured instance 308 const currentSet = this.undoManager.getState().valueSet; 309 if (currentSet !== 'all') { 310 if (confirm('Switching value sets will reset your current progress. Are you sure?')) { 311 this.toggleValueSet(); 312 } 313 } 314 }); 315 316 // Undo/Redo buttons 317 document.getElementById('undoBtn')?.addEventListener('click', () => { 318 const prev = this.undoManager.undo(); 319 if (prev) { 320 this.state = prev; 321 this.saveState(); 322 this.render(); 323 this.updateUndoRedoButtons(); 324 } 325 }); 326 document.getElementById('redoBtn')?.addEventListener('click', () => { 327 const next = this.undoManager.redo(); 328 if (next) { 329 this.state = next; 330 this.saveState(); 331 this.render(); 332 this.updateUndoRedoButtons(); 333 } 334 }); 335 336 // Clear storage button (renamed to Restart exercise) 337 document.getElementById('clearStorageBtn')?.addEventListener('click', () => { 338 // Update confirmation message 339 if ( 340 confirm( 341 'Are you sure you want to restart the exercise? All progress will be lost. This action cannot be undone.', 342 ) 343 ) { 344 localStorage.removeItem(this.storageKey); 345 // Reset to the default state using the *current* value set preference 346 const newState = this.defaultState(this.state.valueSet); 347 this.updateState(newState); 348 } 349 }); 350 351 // Set up drag and drop for card movement in all card-container elements. 352 const containers = document.querySelectorAll('.card-container'); 353 containers.forEach((container) => { 354 container.addEventListener('dragover', (e) => { 355 e.preventDefault(); 356 }); 357 container.addEventListener('drop', (e) => { 358 e.preventDefault(); 359 const dragEvent = e as DragEvent; 360 const cardId = Number(dragEvent.dataTransfer?.getData('text/plain')); 361 const targetColumn = container.parentElement!.getAttribute('data-column'); 362 if (targetColumn) { 363 this.moveCard(cardId, targetColumn); 364 } 365 }); 366 }); 367 } 368 369 // Moves a card (by id) to a new column. 370 private moveCard(cardId: number, newColumn: string) { 371 const newState = this.undoManager.getState(); 372 const card = newState.cards.find((c) => c.id === cardId); 373 if (card) { 374 // If in Part3 and moving to the 'core' column, enforce a maximum of 5 core cards. 375 if (newState.currentPart === 'part3' && newColumn === 'core') { 376 const coreCount = newState.cards.filter((c) => c.column === 'core').length; 377 if (coreCount >= 5 && card.column !== 'core') { 378 alert('You can only have 5 core values!'); 379 return; 380 } 381 } 382 card.column = newColumn; 383 // Optionally, update order if needed (here we simply set order to current timestamp). 384 card.order = Date.now(); 385 this.updateState(newState); 386 } 387 } 388 389 // Creates a draggable card element, handles description editing UI. 390 private createCardElement(card: ValueCard): HTMLElement { 391 const cardElem = document.createElement('div'); 392 cardElem.className = 'card'; 393 cardElem.draggable = true; 394 cardElem.dataset.cardId = card.id.toString(); 395 396 const nameElem = document.createElement('span'); 397 nameElem.className = 'card-name'; 398 nameElem.textContent = card.name; 399 cardElem.appendChild(nameElem); 400 401 // Container for description/edit area 402 const descriptionContainer = document.createElement('div'); 403 descriptionContainer.className = 'card-description-container'; 404 405 if (this.state.editingDescriptionCardId === card.id) { 406 // RENDER EDIT TEXTAREA 407 const textarea = document.createElement('textarea'); 408 textarea.className = 'card-description-edit'; 409 textarea.value = card.description ?? valueDefinitionsMap.get(card.name) ?? ''; 410 textarea.rows = 3; 411 412 textarea.addEventListener('blur', () => { 413 this.saveDescriptionEdit(card.id, textarea.value); 414 }); 415 416 textarea.addEventListener('keydown', (e) => { 417 if (e.key === 'Enter' && !e.shiftKey) { 418 e.preventDefault(); // Prevent newline 419 this.saveDescriptionEdit(card.id, textarea.value); 420 } else if (e.key === 'Escape') { 421 this.cancelDescriptionEdit(); 422 } 423 }); 424 425 descriptionContainer.appendChild(textarea); 426 // Auto-focus the textarea after it's rendered 427 setTimeout(() => { 428 textarea.focus(); 429 }, 0); 430 } else { 431 // RENDER DESCRIPTION TEXT (Clickable) 432 const descriptionElem = document.createElement('span'); 433 descriptionElem.className = 'card-description clickable'; // Add clickable class 434 descriptionElem.textContent = 435 card.description ?? valueDefinitionsMap.get(card.name) ?? '(Click to add description)'; 436 descriptionElem.title = 'Click to edit description'; // Tooltip 437 438 descriptionElem.addEventListener('click', (e) => { 439 e.stopPropagation(); // Prevent card drag start if clicking text 440 this.startEditingDescription(card.id); 441 }); 442 descriptionContainer.appendChild(descriptionElem); 443 } 444 445 cardElem.appendChild(descriptionContainer); 446 447 cardElem.addEventListener('dragstart', (e) => { 448 // Only allow drag if not editing description 449 if (this.state.editingDescriptionCardId === null) { 450 e.dataTransfer?.setData('text/plain', card.id.toString()); 451 } else { 452 e.preventDefault(); // Prevent drag while editing 453 } 454 }); 455 456 return cardElem; 457 } 458 459 // Render the UI based on the current state. 460 private render() { 461 // Manage tabindex for global controls based on current part 462 const isPart4Active = this.state.currentPart === 'part4'; 463 const globalControls = document.querySelectorAll('#global-controls button'); 464 globalControls.forEach((btn) => { 465 if (isPart4Active) { 466 (btn as HTMLElement).tabIndex = -1; // Make header buttons non-tabbable during Part 4 467 } else { 468 (btn as HTMLElement).removeAttribute('tabindex'); // Restore default tabbability 469 } 470 }); 471 472 // Hide all parts first. 473 document.querySelectorAll('.exercise-part').forEach((section) => { 474 (section as HTMLElement).style.display = 'none'; 475 }); 476 // Show the current part. 477 const partElem = document.getElementById(this.state.currentPart); 478 if (partElem) { 479 partElem.style.display = 'block'; 480 } 481 // Render cards for the current part. 482 if (this.state.currentPart === 'part1') { 483 // Clear containers for Part 1 484 [ 485 'part1-unassignedContainer', 486 'part1-veryImportantContainer', 487 'part1-importantContainer', 488 'part1-notImportantContainer', 489 ].forEach((id) => { 490 const container = document.getElementById(id); 491 if (container) container.innerHTML = ''; 492 }); 493 // Render each card into its Part 1 container. 494 this.state.cards.forEach((card) => { 495 const containerId = 'part1-' + card.column + 'Container'; // Use Part 1 prefix 496 const container = document.getElementById(containerId); 497 if (container) { 498 const cardElem = this.createCardElement(card); // Use helper 499 container.appendChild(cardElem); 500 } 501 }); 502 } else if (this.state.currentPart === 'part2') { 503 // Clear containers for Part 2 504 [ 505 'part2-unassignedContainer', 506 'part2-veryImportantContainer', 507 'part2-importantContainer', 508 'part2-notImportantContainer', 509 ].forEach((id) => { 510 const container = document.getElementById(id); 511 if (container) container.innerHTML = ''; 512 }); 513 514 // Log the state for debugging (can be removed later) 515 // console.log("Rendering Part 2:", { 516 // totalCards: this.state.cards.length, 517 // cards: this.state.cards.map(c => ({ name: c.name, column: c.column })) 518 // }); 519 520 // In Part 2, show all cards in their current Part 2 columns 521 this.state.cards.forEach((card) => { 522 const containerId = 'part2-' + card.column + 'Container'; // Use Part 2 prefix 523 const container = document.getElementById(containerId); 524 if (container) { 525 const cardElem = this.createCardElement(card); // Use helper 526 container.appendChild(cardElem); 527 } else { 528 // Log error if container not found (can be removed later) 529 // console.error(`Container not found for card ${card.name} in column ${card.column}`); 530 } 531 }); 532 } else if (this.state.currentPart === 'part3') { 533 // Clear containers for part3 534 ['coreContainer', 'additionalContainer'].forEach((id) => { 535 const container = document.getElementById(id); 536 if (container) container.innerHTML = ''; 537 }); 538 this.state.cards.forEach((card) => { 539 if (card.column === 'core' || card.column === 'additional') { 540 const containerId = card.column + 'Container'; 541 const container = document.getElementById(containerId); 542 if (container) { 543 const cardElem = this.createCardElement(card); // Use helper 544 container.appendChild(cardElem); 545 } 546 } 547 }); 548 } else if (this.state.currentPart === 'part4') { 549 // Render text inputs for each core value. 550 const finalStatementsContainer = document.getElementById('finalStatements'); 551 if (finalStatementsContainer) { 552 const coreCards = this.state.cards.filter((c) => c.column === 'core'); 553 coreCards.sort((a, b) => a.name.localeCompare(b.name)); // Keep sorting for consistent order 554 555 // Check if inputs matching core cards already exist 556 const existingInputs = finalStatementsContainer.querySelectorAll<HTMLInputElement>('input[type="text"]'); 557 let inputsMatch = existingInputs.length === coreCards.length; 558 if (inputsMatch) { 559 existingInputs.forEach((input, index) => { 560 // Verify the input corresponds to the correct card (using id or other attribute if needed) 561 // For simplicity, we assume order matches due to sorting if length is correct 562 if (coreCards[index]?.id.toString() !== input.id.replace('statement-', '')) { 563 inputsMatch = false; 564 } 565 }); 566 } 567 568 if (inputsMatch) { 569 // Inputs exist and match: Just update their values 570 existingInputs.forEach((input) => { 571 const cardId = Number(input.id.replace('statement-', '')); 572 const currentValue = this.state.finalStatements[cardId] ?? ''; 573 if (input.value !== currentValue) { 574 input.value = currentValue; 575 } 576 }); 577 } else { 578 // Inputs don't exist or don't match: Clear and recreate them 579 finalStatementsContainer.innerHTML = ''; 580 coreCards.forEach((card) => { 581 const wrapper = document.createElement('div'); 582 wrapper.className = 'final-statement'; 583 const label = document.createElement('label'); 584 label.htmlFor = `statement-${card.id}`; 585 label.textContent = `Describe what "${card.name}" means to you:`; 586 const input = document.createElement('input'); 587 input.type = 'text'; 588 input.id = `statement-${card.id}`; 589 input.value = this.state.finalStatements[card.id] ?? ''; 590 // Use 'blur' event to update state directly when field loses focus 591 input.addEventListener('blur', () => { 592 const currentState = this.undoManager.getState(); // Get latest state 593 // Avoid modifying the state if it hasn't actually changed 594 if (currentState.finalStatements[card.id] !== input.value) { 595 const newState = this.undoManager.getState(); // Get a fresh copy to modify 596 newState.finalStatements[card.id] = input.value; 597 this.updateState(newState); // Update authoritative state 598 } 599 }); 600 wrapper.appendChild(label); 601 wrapper.appendChild(input); 602 finalStatementsContainer.appendChild(wrapper); 603 }); 604 } 605 } 606 } 607 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 608 else if (this.state.currentPart === 'review') { 609 const reviewContent = document.getElementById('reviewContent'); 610 if (reviewContent) { 611 reviewContent.innerHTML = ''; 612 613 // Add the visualization grid 614 const grid = document.createElement('div'); 615 grid.className = 'values-grid'; 616 617 // Create sections for core values and additional values 618 const categories = [ 619 { title: 'Core Values (F*CK YEAH)', column: 'core' }, 620 { title: 'Also Something I Want', column: 'additional' }, 621 ]; 622 623 categories.forEach((category) => { 624 const section = document.createElement('div'); 625 section.className = 'grid-section'; 626 const title = document.createElement('h3'); 627 title.textContent = category.title; 628 section.appendChild(title); 629 630 // Get the actual card objects, not just names 631 const cardsInCategory = this.state.cards.filter((c) => c.column === category.column); 632 // Sort them alphabetically by name for consistent display 633 cardsInCategory.sort((a, b) => a.name.localeCompare(b.name)); 634 635 if (cardsInCategory.length > 0) { 636 const list = document.createElement('ul'); 637 // Iterate over the card objects 638 cardsInCategory.forEach((card) => { 639 const li = document.createElement('li'); 640 const nameSpan = document.createElement('span'); 641 nameSpan.className = 'review-value-name'; 642 nameSpan.textContent = card.name; // Use name from card object 643 644 const descSpan = document.createElement('span'); 645 descSpan.className = 'review-value-description'; 646 // Prioritize card.description, fall back to map 647 descSpan.textContent = card.description ?? valueDefinitionsMap.get(card.name) ?? '(Description missing)'; 648 649 li.appendChild(nameSpan); 650 li.appendChild(descSpan); 651 list.appendChild(li); 652 }); 653 section.appendChild(list); 654 } 655 656 grid.appendChild(section); 657 }); 658 659 reviewContent.appendChild(grid); 660 661 // Add the final statements 662 const title = document.createElement('h3'); 663 title.textContent = 'Your Core Values & Statements:'; 664 reviewContent.appendChild(title); 665 const list = document.createElement('ul'); 666 const coreCards = this.state.cards.filter((c) => c.column === 'core'); 667 coreCards.forEach((card) => { 668 const li = document.createElement('li'); 669 const statement = this.state.finalStatements[card.id] ?? '(No statement written)'; 670 li.textContent = `${statement} (${card.name})`; 671 list.appendChild(li); 672 }); 673 reviewContent.appendChild(list); 674 } 675 } 676 677 // -- Update toggle button appearance based on current state -- 678 const limitedBtn = document.getElementById('useLimitedValuesBtn') as HTMLButtonElement | null; 679 const allBtn = document.getElementById('useAllValuesBtn') as HTMLButtonElement | null; 680 if (limitedBtn) { 681 limitedBtn.classList.toggle('active', this.state.valueSet === 'limited'); 682 limitedBtn.disabled = this.state.valueSet === 'limited'; // Disable active button 683 } 684 if (allBtn) { 685 allBtn.classList.toggle('active', this.state.valueSet === 'all'); 686 allBtn.disabled = this.state.valueSet === 'all'; // Disable active button 687 } 688 } 689 690 // Update the disabled state of the undo/redo buttons. 691 private updateUndoRedoButtons() { 692 const undoBtn = document.getElementById('undoBtn') as HTMLButtonElement; 693 const redoBtn = document.getElementById('redoBtn') as HTMLButtonElement; 694 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 695 if (undoBtn) undoBtn.disabled = !this.undoManager.canUndo(); 696 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 697 if (redoBtn) redoBtn.disabled = !this.undoManager.canRedo(); 698 } 699} 700 701// Initialize the app normally, only if in a browser environment 702if (typeof window !== 'undefined' && typeof document !== 'undefined') { 703 window.addEventListener('DOMContentLoaded', () => { 704 new App(); 705 }); 706}