File size: 18,389 Bytes
992c838
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
document.addEventListener('DOMContentLoaded', () => {
        const codeSnippet = `// HTML structure for this example:
//  <button id="counterBtn">Click me: 0</button>
//  <p id="message"></p>

const button = document.getElementById('counterBtn');
let count = 0;

button.addEventListener('click', () => {
  count++;
  button.textContent = \`Click me: \${count}\`;

  if (count === 5) {
    const messageEl = document.getElementById('message');
    messageEl.textContent = "You've reached 5 clicks!";
  } else if (count > 5) {
    const msgEl = document.getElementById('message');
    msgEl.textContent = "Keep clicking!";
  }
});`;

        const journeyStructure = [
          {
            groupTitle: "Setup & Initialization",
            steps: [
              { category: "Setup", title: "HTML Structure", lineRanges: [[0, 3]], explanation: "This JavaScript interacts with specific HTML elements. We're assuming a button with <code>id=\"counterBtn\"</code> and a paragraph with <code>id=\"message\"</code> exist on the page. The comments at the top outline this structure." },
              { category: "DOM Query", title: "Access Button Element", lineRanges: [[4, 4]], explanation: "<code>const button = document.getElementById('counterBtn');</code><br>This line uses the DOM API to find the HTML button element with ID 'counterBtn' and stores it in the 'button' constant." },
              { category: "Variable Declaration", title: "Initialize Click Counter", lineRanges: [[5, 5]], explanation: "<code>let count = 0;</code><br>A variable 'count' is declared and initialized to 0. This variable will track clicks." }
            ]
          },
          {
            groupTitle: "Core Interaction Logic",
            steps: [
              { category: "Event Handling", title: "Attach Click Listener", lineRanges: [[7, 7], [16,16]], explanation: "<code>button.addEventListener('click', () => { ... });</code><br>An event listener is attached to the 'button'. The arrow function <code>() => { ... }</code> executes on each click." },
              { category: "State Update", title: "Increment Counter", lineRanges: [[8, 8]], explanation: "<code>count++;</code><br>Inside the click handler, 'count' is incremented by 1." },
              { category: "UI Update", title: "Update Button Text", lineRanges: [[9, 9]], explanation: "<code>button.textContent = \`Click me: \${count}\`;</code><br>The button's text is updated to show the new 'count' using template literals." }
            ]
          },
          {
            groupTitle: "Conditional Feedback",
            steps: [
              { category: "Conditional Logic", title: "Check 5-Click Milestone", lineRanges: [[11, 11],[12,12],[13,13]], explanation: "<code>if (count === 5) { ... }</code><br>If 'count' reaches 5, a special message is displayed in the 'message' paragraph. Note that line 13 is the closing brace of this `if` block." },
              { category: "Conditional Logic", title: "Handle Subsequent Clicks", lineRanges: [[13,13],[14,14],[15,15]], explanation: "<code>else if (count > 5) { ... }</code><br>If 'count' is over 5, a different message encourages continued clicking. Note that line 13 is also the start of this `else if`." }
            ]
          },
          {
            groupTitle: "Conclusion",
            steps: [
              { category: "Summary", title: "Tour Completed!", lineRanges: [], explanation: "You've completed the tour! This example showed DOM manipulation, events, variables, and conditional logic. Click 'Reset' to restart." }
            ]
          }
        ];

        const categoryStyles = {
            "Setup": "bg-slate-200 text-slate-700",
            "DOM Query": "bg-sky-100 text-sky-700",
            "Variable Declaration": "bg-emerald-100 text-emerald-700",
            "Event Handling": "bg-violet-100 text-violet-700",
            "State Update": "bg-amber-100 text-amber-700",
            "UI Update": "bg-indigo-100 text-indigo-700",
            "Conditional Logic": "bg-rose-100 text-rose-700",
            "Summary": "bg-cyan-100 text-cyan-700",
            "Default": "bg-gray-200 text-gray-700" // Fallback
        };

        const codeLinesContainer = document.getElementById('codeLinesContainer');
        const explanationStepTitle = document.getElementById('explanationStepTitle');
        const explanationMeta = document.getElementById('explanationMeta');
        const explanationText = document.getElementById('explanationText');
        const explanationStepCategoryContainer = document.getElementById('explanationStepCategoryContainer');
        const prevBtn = document.getElementById('prevBtn');
        const nextBtn = document.getElementById('nextBtn');
        const resetBtn = document.getElementById('resetBtn');
        const progressIndicator = document.getElementById('progressIndicator');
        const stepsOutlineUl = document.getElementById('stepsOutline');
        const mobileCurrentGroupTextEl = document.getElementById('mobileCurrentGroupText');
        const mobileCurrentStepTextEl = document.getElementById('mobileCurrentStepText'); // New element

        let currentStepIndex = 0;
        let lineElements = [];
        let flatCodeSteps = [];

        // Flatten the journeyStructure for sequential navigation
        journeyStructure.forEach(group => {
          group.steps.forEach(step => {
            flatCodeSteps.push(step);
          });
        });

        function handleCodeLineClick(event) {
            const clickedLineDiv = event.currentTarget;
            const clickedLineIndex = parseInt(clickedLineDiv.dataset.lineIndex, 10);

            for (let i = 0; i < flatCodeSteps.length; i++) {
                const step = flatCodeSteps[i];
                if (step.lineRanges && step.lineRanges.length > 0) {
                    for (const range of step.lineRanges) {
                        if (clickedLineIndex >= range[0] && clickedLineIndex <= range[1]) {
                            currentStepIndex = i;
                            updateStep();
                            return;
                        }
                    }
                }
            }
        }

        function renderCode() {
            codeLinesContainer.innerHTML = '';
            lineElements = [];

            const tempCodeElement = document.createElement('code');
            tempCodeElement.className = 'language-javascript';
            tempCodeElement.textContent = codeSnippet;

            if (typeof Prism !== 'undefined' && Prism.highlightElement) {
                Prism.highlightElement(tempCodeElement);
            } else {
                console.warn("Prism.js not available. Syntax highlighting will be plain.");
            }

            const highlightedHtml = tempCodeElement.innerHTML;
            const linesHtml = highlightedHtml.split('\n');

            linesHtml.forEach((lineHtml, index) => {
                const lineDiv = document.createElement('div');
                lineDiv.classList.add('code-line');
                lineDiv.innerHTML = lineHtml || '&nbsp;';
                lineDiv.dataset.lineIndex = index;
                lineDiv.addEventListener('click', handleCodeLineClick);
                codeLinesContainer.appendChild(lineDiv);
                lineElements.push(lineDiv);
            });
        }

        function renderOutline() {
            stepsOutlineUl.innerHTML = '';
            let globalStepIndex = 0;

            journeyStructure.forEach((group, groupIndex) => {
                const groupLi = document.createElement('li');
                groupLi.className = `pt-3 pb-1 px-1 ${groupIndex > 0 ? 'mt-1' : ''}`;

                const groupTitleEl = document.createElement('h4');
                groupTitleEl.className = 'text-xs font-semibold text-slate-500 uppercase tracking-wider select-none';
                groupTitleEl.textContent = group.groupTitle;
                groupLi.appendChild(groupTitleEl);
                stepsOutlineUl.appendChild(groupLi);

                group.steps.forEach((step, stepIndexInGroup) => {
                    const li = document.createElement('li');
                    li.className = 'outline-step-item rounded-md ml-1';
                    li.dataset.stepIndex = globalStepIndex;

                    const button = document.createElement('button');
                    button.className = 'group flex items-start w-full text-left p-2 focus:outline-none focus:ring-2 focus:ring-sky-300 focus:z-10 rounded-md transition-colors hover:bg-slate-100';

                    const dotAndLineContainer = document.createElement('div');
                    dotAndLineContainer.className = 'flex flex-col items-center mr-3 mt-1 flex-shrink-0';

                    const dot = document.createElement('div');
                    dot.className = 'outline-dot w-3 h-3 rounded-full bg-slate-300 border-2 border-white group-hover:bg-sky-400 transition-all duration-150 ease-in-out';
                    dotAndLineContainer.appendChild(dot);

                    if (globalStepIndex < flatCodeSteps.length - 1) {
                        const isLastInGroup = stepIndexInGroup === group.steps.length - 1;
                        const isLastGroup = groupIndex === journeyStructure.length - 1;
                        if (isLastInGroup && !(isLastGroup && isLastInGroup) ) {
                            const line = document.createElement('div');
                            line.className = 'outline-connector w-0.5 h-2 bg-slate-300 mt-1 group-hover:bg-slate-400 transition-colors duration-150 ease-in-out';
                            dotAndLineContainer.appendChild(line);
                        } else if (!(isLastGroup && isLastInGroup)){
                            const line = document.createElement('div');
                            line.className = 'outline-connector w-0.5 h-5 bg-slate-300 mt-1 group-hover:bg-slate-400 transition-colors duration-150 ease-in-out';
                            dotAndLineContainer.appendChild(line);
                        } else {
                            const placeholder = document.createElement('div');
                            placeholder.className = 'h-5 mt-1';
                            dotAndLineContainer.appendChild(placeholder);
                        }
                    } else {
                        const placeholder = document.createElement('div');
                        placeholder.className = 'h-5 mt-1';
                        dotAndLineContainer.appendChild(placeholder);
                    }


                    const titleSpan = document.createElement('span');
                    titleSpan.className = 'outline-title text-sm text-slate-600 group-hover:text-sky-600 transition-colors duration-150 ease-in-out';
                    titleSpan.textContent = step.title;

                    button.appendChild(dotAndLineContainer);
                    button.appendChild(titleSpan);
                    li.appendChild(button);

                    button.addEventListener('click', () => {
                        currentStepIndex = parseInt(li.dataset.stepIndex, 10);
                        updateStep();
                    });
                    stepsOutlineUl.appendChild(li);
                    globalStepIndex++;
                });
            });
        }

        function updateOutlineActiveState() {
            const outlineItems = stepsOutlineUl.querySelectorAll('.outline-step-item[data-step-index]');
            const outlineNav = stepsOutlineUl.parentElement;

            outlineItems.forEach((itemLi) => {
                const idx = parseInt(itemLi.dataset.stepIndex, 10);
                const button = itemLi.querySelector('button');
                const dot = itemLi.querySelector('.outline-dot');
                const title = itemLi.querySelector('.outline-title');
                const connector = itemLi.querySelector('.outline-connector');

                if (idx === currentStepIndex) {
                    button.classList.add('bg-sky-50');
                    dot.classList.remove('bg-slate-300');
                    dot.classList.add('bg-sky-500', 'scale-125');
                     if (connector) connector.classList.add('bg-sky-400');
                    title.classList.remove('text-slate-600');
                    title.classList.add('text-sky-700', 'font-semibold');

                    if (outlineNav.scrollHeight > outlineNav.clientHeight && !outlineNav.classList.contains('hidden')) { // Only scroll if scrollable and visible
                        const itemRect = itemLi.getBoundingClientRect();
                        const navRect = outlineNav.getBoundingClientRect();
                        if (itemRect.top < navRect.top || itemRect.bottom > navRect.bottom) {
                           itemLi.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
                        }
                    }
                } else {
                    button.classList.remove('bg-sky-50');
                    dot.classList.add('bg-slate-300');
                    dot.classList.remove('bg-sky-500', 'scale-125');
                    if (connector) connector.classList.remove('bg-sky-400');
                    title.classList.add('text-slate-600');
                    title.classList.remove('text-sky-700', 'font-semibold');
                }
            });
        }

        function updateStep() {
          if (currentStepIndex < 0 || currentStepIndex >= flatCodeSteps.length) return;

          const step = flatCodeSteps[currentStepIndex];
          const hasLineFocus = step.lineRanges && step.lineRanges.length > 0;

          // Update mobile current group and step titles
          if (mobileCurrentGroupTextEl && mobileCurrentStepTextEl) {
            let currentGroupTitleForMobile = "Loading...";
            let stepsProcessed = 0;
            for (const group of journeyStructure) {
                const stepsInGroup = group.steps.length;
                if (currentStepIndex >= stepsProcessed && currentStepIndex < stepsProcessed + stepsInGroup) {
                    currentGroupTitleForMobile = group.groupTitle;
                    break;
                }
                stepsProcessed += stepsInGroup;
            }
            mobileCurrentGroupTextEl.textContent = currentGroupTitleForMobile;
            mobileCurrentStepTextEl.textContent = step.title;
          }


          explanationStepCategoryContainer.innerHTML = '';
          if (step.category) {
            const badge = document.createElement('span');
            const styleClasses = categoryStyles[step.category] || categoryStyles["Default"];
            badge.className = `${styleClasses} text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full inline-block`;
            badge.textContent = step.category;
            explanationStepCategoryContainer.appendChild(badge);
            explanationStepCategoryContainer.classList.remove('hidden');
          } else {
            explanationStepCategoryContainer.classList.add('hidden');
          }

          explanationStepTitle.textContent = step.title;
          explanationText.innerHTML = step.explanation;

          lineElements.forEach(el => {
            el.classList.remove('highlighted', 'code-line-dimmed');
          });

          let firstHighlightedLineElement = null;
          let highlightedLineNumbersForMeta = [];

          if (hasLineFocus) {
            const focusedIndices = new Set();
            step.lineRanges.forEach(range => {
              for (let i = range[0]; i <= range[1]; i++) {
                focusedIndices.add(i);
                if (lineElements[i]) {
                   if (!highlightedLineNumbersForMeta.includes(i + 1)) {
                       highlightedLineNumbersForMeta.push(i + 1);
                   }
                   if (!firstHighlightedLineElement) firstHighlightedLineElement = lineElements[i];
                }
              }
            });

            lineElements.forEach((el, idx) => {
                if (focusedIndices.has(idx)) {
                    el.classList.add('highlighted');
                } else {
                    el.classList.add('code-line-dimmed');
                }
            });
            highlightedLineNumbersForMeta.sort((a,b) => a - b);
            const rangesSummary = highlightedLineNumbersForMeta.length === 1 ? `Line ${highlightedLineNumbersForMeta[0]}` : `Lines ${highlightedLineNumbersForMeta.join(', ')}`;
            explanationMeta.textContent = `Focus: ${rangesSummary}`;

          } else {
             explanationMeta.textContent = "General overview for this step.";
          }

          if (firstHighlightedLineElement) {
            const codePane = codeLinesContainer.parentElement;
            if (codePane.scrollHeight > codePane.clientHeight) {
                const lineRect = firstHighlightedLineElement.getBoundingClientRect();
                const paneRect = codePane.getBoundingClientRect();
                if (lineRect.top < paneRect.top || lineRect.bottom > paneRect.bottom) {
                    firstHighlightedLineElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
                }
            }
          }


          prevBtn.disabled = currentStepIndex === 0;
          nextBtn.disabled = currentStepIndex === flatCodeSteps.length - 1;
          progressIndicator.textContent = `Step ${currentStepIndex + 1} / ${flatCodeSteps.length}`;
          updateOutlineActiveState();
        }

        nextBtn.addEventListener('click', () => {
          if (currentStepIndex < flatCodeSteps.length - 1) {
            currentStepIndex++;
            updateStep();
          }
        });

        prevBtn.addEventListener('click', () => {
          if (currentStepIndex > 0) {
            currentStepIndex--;
            updateStep();
          }
        });

        resetBtn.addEventListener('click', () => {
          currentStepIndex = 0;
          updateStep();
          const codePane = codeLinesContainer.parentElement;
          if (codePane) {
            codePane.scrollTop = 0;
          }
          const outlineNav = stepsOutlineUl.parentElement;
          if (outlineNav && !outlineNav.classList.contains('hidden')) { // Check if outlineNav is visible
            outlineNav.scrollTop = 0;
          }
        });

        renderCode();
        renderOutline();
        updateStep();
      });