this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor

alice 0dc8f24f 5ea1ec20

+421 -288
+10 -1
bun.lock
··· 8 8 "wrangler": "^4.7.0", 9 9 }, 10 10 "devDependencies": { 11 - "@types/bun": "latest", 11 + "@happy-dom/global-registrator": "^17.4.4", 12 + "@types/bun": "^1.2.8", 12 13 }, 13 14 "peerDependencies": { 14 15 "typescript": "^5", ··· 89 90 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="], 90 91 91 92 "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], 93 + 94 + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@17.4.4", "", { "dependencies": { "happy-dom": "^17.4.4" } }, "sha512-njrU74GrYVHO43upIJr96f7pEmUG7YLZbHCGiHALBECeVnDKpepzL9kVc7KIl8S2nQOkPA0rAA1EyC3xASb54w=="], 92 95 93 96 "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], 94 97 ··· 428 431 429 432 "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], 430 433 434 + "happy-dom": ["happy-dom@17.4.4", "", { "dependencies": { "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0" } }, "sha512-/Pb0ctk3HTZ5xEL3BZ0hK1AqDSAUuRQitOmROPHhfUYEWpmTImwfD8vFDGADmMAX0JYgbcgxWoLFKtsWhcpuVA=="], 435 + 431 436 "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 432 437 433 438 "htmlnano": ["htmlnano@2.1.1", "", { "dependencies": { "cosmiconfig": "^9.0.0", "posthtml": "^0.16.5", "timsort": "^0.3.0" }, "peerDependencies": { "cssnano": "^7.0.0", "postcss": "^8.3.11", "purgecss": "^6.0.0", "relateurl": "^0.2.7", "srcset": "5.0.1", "svgo": "^3.0.2", "terser": "^5.10.0", "uncss": "^0.17.3" }, "optionalPeers": ["cssnano", "postcss", "purgecss", "relateurl", "srcset", "svgo", "terser", "uncss"] }, "sha512-kAERyg/LuNZYmdqgCdYvugyLWNFAm8MWXpQMz1pLpetmCbFwoMxvkSoaAMlFrOC4OKTWI4KlZGT/RsNxg4ghOw=="], ··· 577 582 "utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="], 578 583 579 584 "weak-lru-cache": ["weak-lru-cache@1.2.2", "", {}, "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw=="], 585 + 586 + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], 587 + 588 + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], 580 589 581 590 "workerd": ["workerd@1.20250321.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250321.0", "@cloudflare/workerd-darwin-arm64": "1.20250321.0", "@cloudflare/workerd-linux-64": "1.20250321.0", "@cloudflare/workerd-linux-arm64": "1.20250321.0", "@cloudflare/workerd-windows-64": "1.20250321.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-vyuz9pdJ+7o1lC79vQ2UVRLXPARa2Lq94PbTfqEcYQeSxeR9X+YqhNq2yysv8Zs5vpokmexLCtMniPp9u+2LVQ=="], 582 591
+2
bunfig.toml
··· 1 + [test] 2 + preload = "./happydom.ts"
+3
happydom.ts
··· 1 + import { GlobalRegistrator } from "@happy-dom/global-registrator"; 2 + 3 + GlobalRegistrator.register();
+1 -1
index.html
··· 101 101 </section> 102 102 </div> 103 103 104 - <script src="./index.ts"></script> 104 + <script type="module" src="./index.ts"></script> 105 105 </body> 106 106 </html>
+218
index.test.ts
··· 1 + import { App } from "./index"; 2 + import { UndoManager } from "./undoManager"; 3 + import type { AppState, ValueCard } from "./index"; 4 + import { describe, test, expect, beforeEach } from "bun:test"; 5 + 6 + // Mock necessary DOM elements and event listeners 7 + beforeEach(() => { 8 + // The happy-dom environment should provide localStorage automatically. 9 + // We still need to clear it before each test if the environment doesn't do it automatically. 10 + if (typeof window !== 'undefined' && window.localStorage) { 11 + window.localStorage.clear(); 12 + } 13 + 14 + document.body.innerHTML = ` 15 + <div id="part1" class="exercise-part">Part 1 Content 16 + <div data-column="unassigned"><div id="part1-unassignedContainer" class="card-container"></div></div> 17 + <div data-column="veryImportant"><div id="part1-veryImportantContainer" class="card-container"></div></div> 18 + <div data-column="important"><div id="part1-importantContainer" class="card-container"></div></div> 19 + <div data-column="notImportant"><div id="part1-notImportantContainer" class="card-container"></div></div> 20 + <button id="toPart2"></button> 21 + </div> 22 + <div id="part2" class="exercise-part" style="display: none;">Part 2 Content 23 + <div data-column="unassigned"><div id="part2-unassignedContainer" class="card-container"></div></div> 24 + <div data-column="veryImportant"><div id="part2-veryImportantContainer" class="card-container"></div></div> 25 + <div data-column="important"><div id="part2-importantContainer" class="card-container"></div></div> 26 + <div data-column="notImportant"><div id="part2-notImportantContainer" class="card-container"></div></div> 27 + <button id="backToPart1"></button> 28 + <button id="toPart3"></button> 29 + </div> 30 + <div id="part3" class="exercise-part" style="display: none;">Part 3 Content 31 + <div data-column="core"><div id="coreContainer" class="card-container"></div></div> 32 + <div data-column="additional"><div id="additionalContainer" class="card-container"></div></div> 33 + <button id="backToPart2"></button> 34 + <button id="toPart4"></button> 35 + </div> 36 + <div id="part4" class="exercise-part" style="display: none;"> 37 + Part 4 Content 38 + <div id="finalStatements"></div> 39 + <button id="backToPart3"></button> 40 + <button id="finish"></button> 41 + </div> 42 + <div id="review" class="exercise-part" style="display: none;"> 43 + Part 5 Content 44 + <div id="reviewContent"></div> 45 + <button id="restart"></button> 46 + </div> 47 + <button id="undoBtn"></button> 48 + <button id="redoBtn"></button> 49 + <button id="clearStorageBtn"></button> 50 + `; 51 + }); 52 + 53 + describe('Values Exercise App', () => { 54 + let app: App; 55 + let initialState: AppState; 56 + 57 + beforeEach(() => { 58 + // Initialize app before each test, using the mocked DOM 59 + app = new App(); 60 + initialState = app.defaultState(); // Get initial state structure for comparison 61 + }); 62 + 63 + test('Initial state should be correct', () => { 64 + const state = app.undoManager.getState(); 65 + expect(state.currentPart).toBe('part1'); 66 + expect(state.cards.length).toBeGreaterThan(0); 67 + expect(state.cards.every(c => c.column === 'unassigned')).toBe(true); 68 + }); 69 + 70 + test('Part 1 to Part 2 transition moves only veryImportant cards', () => { 71 + // Arrange: Move some cards 72 + let state = app.undoManager.getState(); 73 + // Add checks to ensure cards exist at these indices before modification 74 + if (state.cards.length < 3) { 75 + throw new Error("Test setup failed: Initial state should have at least 3 cards for this test."); 76 + } 77 + state.cards[0].column = 'veryImportant'; 78 + state.cards[1].column = 'important'; 79 + state.cards[2].column = 'veryImportant'; 80 + app.updateState(state); 81 + 82 + // Act: Simulate clicking the button 83 + const button = document.getElementById('toPart2'); 84 + button?.click(); 85 + 86 + // Assert: Check the state after transition 87 + const part2State = app.undoManager.getState(); 88 + expect(part2State.currentPart).toBe('part2'); 89 + expect(part2State.cards.length).toBe(2); 90 + expect(part2State.cards.every(c => c.column === 'unassigned')).toBe(true); 91 + 92 + // Check names to be sure 93 + const cardNames = part2State.cards.map(c => c.name); 94 + // Add checks here too, although the length check above makes these safe 95 + if (initialState.cards.length < 3) { 96 + throw new Error("Test setup failed: Initial state lost cards unexpectedly."); 97 + } 98 + // Assign to variables first to help type inference 99 + const card0 = initialState.cards[0]; 100 + const card1 = initialState.cards[1]; 101 + const card2 = initialState.cards[2]; 102 + 103 + // Add explicit checks for the variables (though length check implies they exist) 104 + if (!card0 || !card1 || !card2) { 105 + throw new Error("Test setup failed: Card elements are unexpectedly undefined."); 106 + } 107 + 108 + expect(cardNames).toContain(card0.name); 109 + expect(cardNames).toContain(card2.name); 110 + expect(cardNames).not.toContain(card1.name); 111 + }); 112 + 113 + // Add more tests for other transitions (Part 2 -> 3, 3 -> 4, etc.) 114 + // Add tests for card movement logic (moveCard) 115 + // Add tests for final statement input 116 + // Add tests for review screen rendering 117 + }); 118 + 119 + describe('UndoManager', () => { 120 + let initialState: AppState; 121 + let um: UndoManager<AppState>; 122 + 123 + beforeEach(() => { 124 + // Create a sample initial state for testing UndoManager independently 125 + initialState = { 126 + currentPart: 'part1', 127 + cards: [ 128 + { id: 1, name: 'TEST1', column: 'unassigned', order: 0 }, 129 + { id: 2, name: 'TEST2', column: 'unassigned', order: 1 }, 130 + ], 131 + finalStatements: {}, 132 + }; 133 + um = new UndoManager(initialState); 134 + }); 135 + 136 + test('should return the initial state', () => { 137 + expect(um.getState()).toEqual(initialState); 138 + expect(um.canUndo()).toBe(false); 139 + expect(um.canRedo()).toBe(false); 140 + }); 141 + 142 + test('should execute a state change and update current state', () => { 143 + const newState = { ...initialState, currentPart: 'part2' as 'part2' }; // Explicit type assertion 144 + um.execute(newState); 145 + expect(um.getState()).toEqual(newState); 146 + expect(um.canUndo()).toBe(true); 147 + expect(um.canRedo()).toBe(false); 148 + }); 149 + 150 + test('should undo the last state change', () => { 151 + const newState = { ...initialState, currentPart: 'part2' as 'part2' }; 152 + um.execute(newState); 153 + const undoneState = um.undo(); 154 + expect(undoneState).toEqual(initialState); 155 + expect(um.getState()).toEqual(initialState); 156 + expect(um.canUndo()).toBe(false); 157 + expect(um.canRedo()).toBe(true); 158 + }); 159 + 160 + test('should redo the undone state change', () => { 161 + const newState = { ...initialState, currentPart: 'part2' as 'part2' }; 162 + um.execute(newState); 163 + um.undo(); 164 + const redoneState = um.redo(); 165 + expect(redoneState).toEqual(newState); 166 + expect(um.getState()).toEqual(newState); 167 + expect(um.canUndo()).toBe(true); 168 + expect(um.canRedo()).toBe(false); 169 + }); 170 + 171 + test('should clear redo stack on new execution after undo', () => { 172 + const state2 = { ...initialState, currentPart: 'part2' as 'part2' }; 173 + const state3 = { ...initialState, currentPart: 'part3' as 'part3' }; 174 + um.execute(state2); 175 + um.undo(); // Back to initialState, state2 is in redo stack 176 + um.execute(state3); // Execute a new change 177 + 178 + expect(um.getState()).toEqual(state3); 179 + expect(um.canUndo()).toBe(true); // Can undo state3 180 + expect(um.canRedo()).toBe(false); // Redo stack (state2) should be cleared 181 + 182 + // Check undo goes back to initial state, not state 2 183 + const undoneState = um.undo(); 184 + expect(undoneState).toEqual(initialState); 185 + }); 186 + 187 + test('should handle multiple undo/redo operations', () => { 188 + const state2 = { ...initialState, currentPart: 'part2' as 'part2' }; 189 + const state3 = { ...initialState, currentPart: 'part3' as 'part3' }; 190 + um.execute(state2); 191 + um.execute(state3); 192 + 193 + expect(um.getState()).toEqual(state3); 194 + um.undo(); 195 + expect(um.getState()).toEqual(state2); 196 + um.undo(); 197 + expect(um.getState()).toEqual(initialState); 198 + expect(um.canUndo()).toBe(false); 199 + expect(um.canRedo()).toBe(true); 200 + 201 + um.redo(); 202 + expect(um.getState()).toEqual(state2); 203 + um.redo(); 204 + expect(um.getState()).toEqual(state3); 205 + expect(um.canRedo()).toBe(false); 206 + expect(um.canUndo()).toBe(true); 207 + }); 208 + 209 + test('undo/redo should return null when stacks are empty', () => { 210 + expect(um.undo()).toBeNull(); 211 + expect(um.redo()).toBeNull(); 212 + const state2 = { ...initialState, currentPart: 'part2' as 'part2' }; 213 + um.execute(state2); 214 + expect(um.redo()).toBeNull(); // Still no redo 215 + um.undo(); 216 + expect(um.undo()).toBeNull(); // Already at start 217 + }); 218 + });
+144 -284
index.ts
··· 1 1 // Define interfaces for our value cards and overall app state. 2 - interface ValueCard { 2 + export interface ValueCard { 3 3 id: number; 4 4 name: string; 5 5 column: string; // Part1: "unassigned", "notImportant", "important", "veryImportant" ··· 8 8 order: number; 9 9 } 10 10 11 - interface AppState { 11 + export interface AppState { 12 12 currentPart: "part1" | "part2" | "part3" | "part4" | "review"; 13 13 cards: ValueCard[]; 14 14 // In part 4, user can add final statements for each core value (by card id) 15 15 finalStatements: { [cardId: number]: string }; 16 16 } 17 17 18 - // A generic undo manager that stores state snapshots. 19 - class UndoManager<T> { 20 - private undoStack: T[] = []; 21 - private redoStack: T[] = []; 22 - private currentState: T; 23 - constructor(initialState: T) { 24 - this.currentState = this.deepCopy(initialState); 25 - } 26 - private deepCopy(state: T): T { 27 - return JSON.parse(JSON.stringify(state)); 28 - } 29 - execute(newState: T) { 30 - this.undoStack.push(this.deepCopy(this.currentState)); 31 - this.currentState = this.deepCopy(newState); 32 - this.redoStack = []; // clear redo on new action 33 - } 34 - undo(): T | null { 35 - if (!this.undoStack.length) return null; 36 - this.redoStack.push(this.deepCopy(this.currentState)); 37 - this.currentState = this.undoStack.pop()!; 38 - return this.deepCopy(this.currentState); 39 - } 40 - redo(): T | null { 41 - if (!this.redoStack.length) return null; 42 - this.undoStack.push(this.deepCopy(this.currentState)); 43 - this.currentState = this.redoStack.pop()!; 44 - return this.deepCopy(this.currentState); 45 - } 46 - getState(): T { 47 - return this.deepCopy(this.currentState); 48 - } 49 - canUndo(): boolean { 50 - return this.undoStack.length > 0; 51 - } 52 - canRedo(): boolean { 53 - return this.redoStack.length > 0; 54 - } 55 - } 18 + // Import the UndoManager from its own file 19 + import { UndoManager } from './undoManager'; 20 + 21 + // Top-level constant for default values 22 + const DEFAULT_VALUES = [ 23 + "ACCEPTANCE", 24 + "ACCURACY", 25 + "ACHIEVEMENT", 26 + "ADVENTURE", 27 + "ATTRACTIVENESS", 28 + "AUTHORITY", 29 + "AUTONOMY", 30 + "BEAUTY", 31 + "CARING", 32 + "CHALLENGE", 33 + "CHANGE", 34 + // "COMFORT", 35 + // "COMMITMENT", 36 + // "COMPASSION", 37 + // "CONTRIBUTION", 38 + // "COOPERATION", 39 + // "COURTESY", 40 + // "CREATIVITY", 41 + // "DEPENDABILITY", 42 + // "DUTY", 43 + // "ECOLOGY", 44 + // "EXCITEMENT", 45 + // "FAITHFULNESS", 46 + // "FAME", 47 + // "FAMILY", 48 + // "FITNESS", 49 + // "FLEXIBILITY", 50 + // "FORGIVENESS", 51 + // "FRIENDSHIP", 52 + // "FUN", 53 + // "GENEROSITY", 54 + // "GENUINENESS", 55 + // "GOD'S WILL", 56 + // "GROWTH", 57 + // "HEALTH", 58 + // "HELPFULNESS", 59 + // "HONESTY", 60 + // "HOPE", 61 + // "HUMILITY", 62 + // "HUMOR", 63 + // "INDEPENDENCE", 64 + // "INDUSTRY", 65 + // "INNER PEACE", 66 + // "INTIMACY", 67 + // "JUSTICE", 68 + // "KNOWLEDGE", 69 + // "LEISURE", 70 + // "LOVED", 71 + // "LOVING", 72 + // "MASTERY", 73 + // "MINDFULNESS", 74 + // "MODERATION", 75 + // "MONOGAMY", 76 + // "NONCONFORMITY", 77 + // "NURTURANCE", 78 + // "OPENNESS", 79 + // "ORDER", 80 + // "PASSION", 81 + // "PLEASURE", 82 + // "POPULARITY", 83 + // "POWER", 84 + // "PURPOSE", 85 + // "RATIONALITY", 86 + // "REALISM", 87 + // "RESPONSIBILITY", 88 + // "RISK", 89 + // "ROMANCE", 90 + // "SAFETY", 91 + // "SELF-ACCEPTANCE", 92 + // "SELF-CONTROL", 93 + // "SELF-ESTEEM", 94 + // "SELF-KNOWLEDGE", 95 + // "SERVICE", 96 + // "SEXUALITY", 97 + // "SIMPLICITY", 98 + // "SOLITUDE", 99 + // "SPIRITUALITY", 100 + // "STABILITY", 101 + // "TOLERANCE", 102 + // "TRADITION", 103 + // "VIRTUE", 104 + // "WEALTH", 105 + // "WORLD PEACE", 106 + ]; 56 107 57 108 // Main application class 58 - class App { 109 + export class App { 59 110 private state: AppState; 60 - private undoManager: UndoManager<AppState>; 111 + public undoManager: UndoManager<AppState>; 61 112 private storageKey: string = "valuesExerciseState"; 62 113 63 114 constructor() { ··· 79 130 } 80 131 81 132 // Default state with some sample value cards. 82 - // Replace the defaultState() function with the following code: 83 - 84 - private defaultState(): AppState { 85 - const values = [ 86 - "ACCEPTANCE", 87 - "ACCURACY", 88 - "ACHIEVEMENT", 89 - "ADVENTURE", 90 - "ATTRACTIVENESS", 91 - "AUTHORITY", 92 - "AUTONOMY", 93 - "BEAUTY", 94 - "CARING", 95 - "CHALLENGE", 96 - "CHANGE", 97 - // "COMFORT", 98 - // "COMMITMENT", 99 - // "COMPASSION", 100 - // "CONTRIBUTION", 101 - // "COOPERATION", 102 - // "COURTESY", 103 - // "CREATIVITY", 104 - // "DEPENDABILITY", 105 - // "DUTY", 106 - // "ECOLOGY", 107 - // "EXCITEMENT", 108 - // "FAITHFULNESS", 109 - // "FAME", 110 - // "FAMILY", 111 - // "FITNESS", 112 - // "FLEXIBILITY", 113 - // "FORGIVENESS", 114 - // "FRIENDSHIP", 115 - // "FUN", 116 - // "GENEROSITY", 117 - // "GENUINENESS", 118 - // "GOD'S WILL", 119 - // "GROWTH", 120 - // "HEALTH", 121 - // "HELPFULNESS", 122 - // "HONESTY", 123 - // "HOPE", 124 - // "HUMILITY", 125 - // "HUMOR", 126 - // "INDEPENDENCE", 127 - // "INDUSTRY", 128 - // "INNER PEACE", 129 - // "INTIMACY", 130 - // "JUSTICE", 131 - // "KNOWLEDGE", 132 - // "LEISURE", 133 - // "LOVED", 134 - // "LOVING", 135 - // "MASTERY", 136 - // "MINDFULNESS", 137 - // "MODERATION", 138 - // "MONOGAMY", 139 - // "NONCONFORMITY", 140 - // "NURTURANCE", 141 - // "OPENNESS", 142 - // "ORDER", 143 - // "PASSION", 144 - // "PLEASURE", 145 - // "POPULARITY", 146 - // "POWER", 147 - // "PURPOSE", 148 - // "RATIONALITY", 149 - // "REALISM", 150 - // "RESPONSIBILITY", 151 - // "RISK", 152 - // "ROMANCE", 153 - // "SAFETY", 154 - // "SELF-ACCEPTANCE", 155 - // "SELF-CONTROL", 156 - // "SELF-ESTEEM", 157 - // "SELF-KNOWLEDGE", 158 - // "SERVICE", 159 - // "SEXUALITY", 160 - // "SIMPLICITY", 161 - // "SOLITUDE", 162 - // "SPIRITUALITY", 163 - // "STABILITY", 164 - // "TOLERANCE", 165 - // "TRADITION", 166 - // "VIRTUE", 167 - // "WEALTH", 168 - // "WORLD PEACE", 169 - ]; 170 - const sampleCards: ValueCard[] = values.map((name, index) => ({ 133 + public defaultState(): AppState { 134 + const sampleCards: ValueCard[] = DEFAULT_VALUES.map((name, index) => ({ 171 135 id: index + 1, 172 136 name, 173 137 column: "unassigned", ··· 186 150 } 187 151 188 152 // Update state via the undoManager then re-render. 189 - private updateState(newState: AppState) { 153 + public updateState(newState: AppState) { 190 154 this.undoManager.execute(newState); 191 155 this.state = this.undoManager.getState(); 192 156 this.saveState(); ··· 200 164 document.getElementById("toPart2")?.addEventListener("click", () => { 201 165 const newState = this.undoManager.getState(); 202 166 newState.currentPart = "part2"; 203 - 204 - // Get all cards that were in "veryImportant" from Part 1 205 - const veryImportantCards = newState.cards.filter(card => card.column === "veryImportant"); 206 - 207 - // Move these cards to "unassigned" and remove all other cards 208 - newState.cards = veryImportantCards.map(card => ({ 209 - ...card, 210 - column: "unassigned" 211 - })); 212 - 213 - // Log the state for debugging 214 - console.log("Transitioning to Part 2:", { 215 - totalCards: newState.cards.length, 216 - cards: newState.cards.map(c => ({ name: c.name, column: c.column })) 217 - }); 218 - 219 - this.updateState(newState); 167 + // Filter and map cards in one step 168 + newState.cards = newState.cards 169 + .filter(card => card.column === "veryImportant") 170 + .map(card => ({ ...card, column: "unassigned" })); 171 + this.updateState(newState); // Call updateState directly 220 172 }); 221 173 document.getElementById("backToPart1")?.addEventListener("click", () => { 222 174 const newState = this.undoManager.getState(); 223 175 newState.currentPart = "part1"; 224 176 // Restore Part1: move cards back to their original positions 177 + // This logic seems potentially flawed - if a card started in 'important' but moved to 'unassigned' in part 2, 178 + // this moves it to 'veryImportant'. Revisiting Part 1 might require storing original Part 1 state. 179 + // For now, keeping the existing logic. 225 180 newState.cards.forEach((c) => { 226 181 if (c.column === "unassigned") { 227 182 c.column = "veryImportant"; 228 183 } 229 184 }); 230 - this.updateState(newState); 185 + this.updateState(newState); // Call updateState directly 231 186 }); 232 187 document.getElementById("toPart3")?.addEventListener("click", () => { 233 188 const newState = this.undoManager.getState(); 234 - const veryImportantCount = newState.cards.filter((c) => c.column === "veryImportant").length; 189 + const veryImportantCards = newState.cards.filter((c) => c.column === "veryImportant"); 190 + const veryImportantCount = veryImportantCards.length; 191 + 235 192 if (veryImportantCount <= 5) { 236 - // If 5 or fewer values in "Very important to me", skip to Part 4 237 193 newState.currentPart = "part4"; 238 - // Move all "veryImportant" cards to "core" 239 - newState.cards = newState.cards 240 - .filter((c) => c.column === "veryImportant") 241 - .map((c, idx) => ({ ...c, column: "core", order: idx })); 242 194 } else { 243 - // Otherwise, proceed to Part 3 244 195 newState.currentPart = "part3"; 245 - // Move all "veryImportant" cards to "core" 246 - newState.cards = newState.cards 247 - .filter((c) => c.column === "veryImportant") 248 - .map((c, idx) => ({ ...c, column: "core", order: idx })); 249 196 } 250 - this.updateState(newState); 197 + // Move all "veryImportant" cards to "core" regardless of the next part 198 + newState.cards = veryImportantCards.map((c, idx) => ({ ...c, column: "core", order: idx })); 199 + 200 + this.updateState(newState); // Call updateState directly 251 201 }); 252 202 document.getElementById("backToPart2")?.addEventListener("click", () => { 253 203 const newState = this.undoManager.getState(); 254 204 newState.currentPart = "part2"; 255 205 // Restore Part2: move cards back to their original positions 256 206 newState.cards.forEach((c) => { 207 + // This assumes cards in Part 3 only came from 'veryImportant' in Part 2 257 208 if (c.column === "core" || c.column === "additional") { 258 209 c.column = "veryImportant"; 259 210 } 260 211 }); 261 - this.updateState(newState); 212 + this.updateState(newState); // Call updateState directly 262 213 }); 263 214 document.getElementById("toPart4")?.addEventListener("click", () => { 264 215 const newState = this.undoManager.getState(); 265 216 const coreCount = newState.cards.filter((c) => c.column === "core").length; 266 217 if (coreCount > 5) { 267 218 alert("You can only have 5 core values! Please move some values to 'Also Something I Want' before continuing."); 268 - return; 219 + return; // Don't update state if validation fails 269 220 } 270 221 newState.currentPart = "part4"; 271 - this.updateState(newState); 222 + this.updateState(newState); // Call updateState directly 272 223 }); 273 224 document.getElementById("backToPart3")?.addEventListener("click", () => { 274 225 const newState = this.undoManager.getState(); 275 226 newState.currentPart = "part3"; 276 - this.updateState(newState); 227 + this.updateState(newState); // Call updateState directly 277 228 }); 278 229 document.getElementById("finish")?.addEventListener("click", () => { 279 230 const newState = this.undoManager.getState(); 280 231 newState.currentPart = "review"; 281 - this.updateState(newState); 232 + this.updateState(newState); // Call updateState directly 282 233 }); 283 234 document.getElementById("restart")?.addEventListener("click", () => { 284 235 const newState = this.defaultState(); 285 - this.updateState(newState); 236 + this.updateState(newState); // Call updateState directly 286 237 }); 287 238 288 239 // Undo/Redo buttons ··· 339 290 const newState = this.undoManager.getState(); 340 291 const card = newState.cards.find((c) => c.id === cardId); 341 292 if (card) { 342 - // If in Part2 and moving to core, enforce a maximum of 5 core cards. 343 - if (newState.currentPart === "part2" && newColumn === "core") { 293 + // If in Part3 and moving to the 'core' column, enforce a maximum of 5 core cards. 294 + if (newState.currentPart === "part3" && newColumn === "core") { 344 295 const coreCount = newState.cards.filter( 345 296 (c) => c.column === "core" 346 297 ).length; ··· 356 307 } 357 308 } 358 309 310 + // Creates a draggable card element. 311 + private createCardElement(card: ValueCard): HTMLElement { 312 + const cardElem = document.createElement("div"); 313 + cardElem.className = "card"; 314 + cardElem.draggable = true; 315 + cardElem.textContent = card.name; 316 + cardElem.dataset.cardId = card.id.toString(); 317 + cardElem.addEventListener("dragstart", (e) => { 318 + e.dataTransfer?.setData("text/plain", card.id.toString()); 319 + }); 320 + return cardElem; 321 + } 322 + 359 323 // Render the UI based on the current state. 360 324 private render() { 361 325 // Hide all parts first. ··· 384 348 const containerId = "part1-" + card.column + "Container"; // Use Part 1 prefix 385 349 const container = document.getElementById(containerId); 386 350 if (container) { 387 - const cardElem = document.createElement("div"); 388 - cardElem.className = "card"; 389 - cardElem.draggable = true; 390 - cardElem.textContent = card.name; 391 - cardElem.dataset.cardId = card.id.toString(); 392 - cardElem.addEventListener("dragstart", (e) => { 393 - e.dataTransfer?.setData("text/plain", card.id.toString()); 394 - }); 351 + const cardElem = this.createCardElement(card); // Use helper 395 352 container.appendChild(cardElem); 396 353 } 397 354 }); ··· 418 375 const containerId = "part2-" + card.column + "Container"; // Use Part 2 prefix 419 376 const container = document.getElementById(containerId); 420 377 if (container) { 421 - const cardElem = document.createElement("div"); 422 - cardElem.className = "card"; 423 - cardElem.draggable = true; 424 - cardElem.textContent = card.name; 425 - cardElem.dataset.cardId = card.id.toString(); 426 - cardElem.addEventListener("dragstart", (e) => { 427 - e.dataTransfer?.setData("text/plain", card.id.toString()); 428 - }); 378 + const cardElem = this.createCardElement(card); // Use helper 429 379 container.appendChild(cardElem); 430 380 } else { 431 381 // Log error if container not found (can be removed later) ··· 443 393 const containerId = card.column + "Container"; 444 394 const container = document.getElementById(containerId); 445 395 if (container) { 446 - const cardElem = document.createElement("div"); 447 - cardElem.className = "card"; 448 - cardElem.draggable = true; 449 - cardElem.textContent = card.name; 450 - cardElem.dataset.cardId = card.id.toString(); 451 - cardElem.addEventListener("dragstart", (e) => { 452 - e.dataTransfer?.setData("text/plain", card.id.toString()); 453 - }); 396 + const cardElem = this.createCardElement(card); // Use helper 454 397 container.appendChild(cardElem); 455 398 } 456 399 } ··· 469 412 const wrapper = document.createElement("div"); 470 413 wrapper.className = "final-statement"; 471 414 const label = document.createElement("label"); 472 - label.textContent = `I want ${card.name}: `; 415 + label.htmlFor = `statement-${card.id}`; 416 + label.textContent = `Describe what "${card.name}" means to you:`; 473 417 const input = document.createElement("input"); 474 418 input.type = "text"; 419 + input.id = `statement-${card.id}`; 475 420 input.value = this.state.finalStatements[card.id] || ""; 476 421 input.tabIndex = index + 1; // Set explicit tabindex for inputs (1 to N) 477 422 input.addEventListener("change", () => { ··· 540 485 const coreCards = this.state.cards.filter((c) => c.column === "core"); 541 486 coreCards.forEach((card) => { 542 487 const li = document.createElement("li"); 543 - const statement = this.state.finalStatements[card.id] || ""; 544 - li.textContent = `${card.name}: ${statement}`; 488 + const statement = this.state.finalStatements[card.id] || "(No statement written)"; 489 + li.textContent = `${statement} (${card.name})`; 545 490 list.appendChild(li); 546 491 }); 547 492 reviewContent.appendChild(list); ··· 558 503 } 559 504 } 560 505 561 - // ---------------------- 562 - // Minimal Test Suite (TDD style) 563 - // ---------------------- 564 - function runTests() { 565 - let testCount = 0; 566 - let passedCount = 0; 567 - function assert(condition: boolean, message: string) { 568 - testCount++; 569 - if (!condition) { 570 - console.error("Test failed:", message); 571 - } else { 572 - passedCount++; 573 - } 574 - } 575 - 576 - // Test Part 1 to Part 2 transition 577 - const app = new App(); 578 - const initialState = app.undoManager.getState(); 579 - 580 - // Test initial state 581 - assert(initialState.currentPart === "part1", "Initial part should be part1"); 582 - assert(initialState.cards.length > 0, "Should have cards in initial state"); 583 - assert(initialState.cards.every(c => c.column === "unassigned"), "All cards should start in unassigned"); 584 - 585 - // Test moving cards in Part 1 586 - const part1State = app.undoManager.getState(); 587 - part1State.cards[0].column = "veryImportant"; 588 - app.updateState(part1State); 589 - 590 - // Test transition to Part 2 591 - const toPart2State = app.undoManager.getState(); 592 - toPart2State.currentPart = "part2"; 593 - const veryImportantCards = toPart2State.cards.filter(card => card.column === "veryImportant"); 594 - toPart2State.cards = veryImportantCards.map(card => ({ 595 - ...card, 596 - column: "unassigned" 597 - })); 598 - app.updateState(toPart2State); 599 - 600 - const part2State = app.undoManager.getState(); 601 - assert(part2State.currentPart === "part2", "Should be in part2 after transition"); 602 - assert(part2State.cards.length > 0, "Should have cards in part2"); 603 - assert(part2State.cards.every(c => c.column === "unassigned"), "All cards should be in unassigned in part2"); 604 - 605 - // Test UndoManager 606 - const um = new UndoManager(initialState); 607 - let state = um.getState(); 608 - assert(state.value === 1, "Initial state should be 1"); 609 - 610 - // Execute a change. 611 - um.execute({ value: 2 }); 612 - state = um.getState(); 613 - assert(state.value === 2, "State should update to 2"); 614 - 615 - // Undo should bring back 1. 616 - const undone = um.undo(); 617 - assert( 618 - undone !== null && undone.value === 1, 619 - "Undo should revert to state 1" 620 - ); 621 - 622 - // Redo should bring state to 2. 623 - const redone = um.redo(); 624 - assert( 625 - redone !== null && redone.value === 2, 626 - "Redo should set state back to 2" 627 - ); 628 - 629 - // Test endless undo/redo by executing multiple changes. 630 - um.execute({ value: 3 }); 631 - um.execute({ value: 4 }); 632 - assert(um.getState().value === 4, "State should now be 4"); 633 - um.undo(); 634 - assert(um.getState().value === 3, "Undo should revert to 3"); 635 - um.undo(); 636 - assert(um.getState().value === 2, "Undo should revert to 2"); 637 - um.redo(); 638 - assert(um.getState().value === 3, "Redo should bring state to 3"); 639 - 640 - console.log(`Tests passed: ${passedCount}/${testCount}`); 641 - } 642 - 643 - // Run tests if URL contains ?test=1 644 - if (window.location.search.includes("test=1")) { 645 - runTests(); 646 - } else { 647 - // Initialize the app normally. 506 + // Initialize the app normally, only if in a browser environment 507 + if (typeof window !== 'undefined' && typeof document !== 'undefined') { 648 508 window.addEventListener("DOMContentLoaded", () => { 649 509 new App(); 650 510 });
+4 -2
package.json
··· 3 3 "type": "module", 4 4 "private": true, 5 5 "devDependencies": { 6 - "@types/bun": "latest" 6 + "@happy-dom/global-registrator": "^17.4.4", 7 + "@types/bun": "^1.2.8" 7 8 }, 8 9 "peerDependencies": { 9 10 "typescript": "^5" ··· 14 15 }, 15 16 "scripts": { 16 17 "dev": "parcel ./index.html", 17 - "build": "parcel build ./index.html" 18 + "build": "parcel build ./index.html", 19 + "test": "bun test" 18 20 } 19 21 }
+1
tsconfig.json
··· 7 7 "moduleDetection": "force", 8 8 "jsx": "react-jsx", 9 9 "allowJs": true, 10 + "types": ["bun"], 10 11 11 12 // Bundler mode 12 13 "moduleResolution": "bundler",
+38
undoManager.ts
··· 1 + // A generic undo manager that stores state snapshots. 2 + export class UndoManager<T> { 3 + private undoStack: T[] = []; 4 + private redoStack: T[] = []; 5 + private currentState: T; 6 + constructor(initialState: T) { 7 + this.currentState = this.deepCopy(initialState); 8 + } 9 + private deepCopy(state: T): T { 10 + return JSON.parse(JSON.stringify(state)); 11 + } 12 + execute(newState: T) { 13 + this.undoStack.push(this.deepCopy(this.currentState)); 14 + this.currentState = this.deepCopy(newState); 15 + this.redoStack = []; // clear redo on new action 16 + } 17 + undo(): T | null { 18 + if (!this.undoStack.length) return null; 19 + this.redoStack.push(this.deepCopy(this.currentState)); 20 + this.currentState = this.undoStack.pop()!; 21 + return this.deepCopy(this.currentState); 22 + } 23 + redo(): T | null { 24 + if (!this.redoStack.length) return null; 25 + this.undoStack.push(this.deepCopy(this.currentState)); 26 + this.currentState = this.redoStack.pop()!; 27 + return this.deepCopy(this.currentState); 28 + } 29 + getState(): T { 30 + return this.deepCopy(this.currentState); 31 + } 32 + canUndo(): boolean { 33 + return this.undoStack.length > 0; 34 + } 35 + canRedo(): boolean { 36 + return this.redoStack.length > 0; 37 + } 38 + }