diff --git a/.gitignore b/.gitignore index b921615..89060cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,123 +1,122 @@ -# Miscellaneous -*.class -*.lock -*.log -*.pyc -*.swp -.buildlog/ -.history - - - -# Flutter repo-specific -/bin/cache/ -/bin/internal/bootstrap.bat -/bin/internal/bootstrap.sh -/bin/mingit/ -/dev/benchmarks/mega_gallery/ -/dev/bots/.recipe_deps -/dev/bots/android_tools/ -/dev/devicelab/ABresults*.json -/dev/docs/doc/ -/dev/docs/flutter.docs.zip -/dev/docs/lib/ -/dev/docs/pubspec.yaml -/dev/integration_tests/**/xcuserdata -/dev/integration_tests/**/Pods -/packages/flutter/coverage/ -version -analysis_benchmark.json - -# packages file containing multi-root paths -.packages.generated - -# Flutter/Dart/Pub related -**/doc/api/ -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -**/generated_plugin_registrant.dart -.packages -.pub-preload-cache/ -.pub/ -build/ -flutter_*.png -linked_*.ds -unlinked.ds -unlinked_spec.ds - -# Android related -**/android/**/gradle-wrapper.jar -.gradle/ -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java -**/android/key.properties -*.jks - -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ -**/ios/Flutter/.last_build_id -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Flutter.podspec -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/ephemeral -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* - -# macOS -**/Flutter/ephemeral/ -**/Pods/ -**/macos/Flutter/GeneratedPluginRegistrant.swift -**/macos/Flutter/ephemeral -**/xcuserdata/ - -# Windows -**/windows/flutter/generated_plugin_registrant.cc -**/windows/flutter/generated_plugin_registrant.h -**/windows/flutter/generated_plugins.cmake - -# Linux -**/linux/flutter/generated_plugin_registrant.cc -**/linux/flutter/generated_plugin_registrant.h -**/linux/flutter/generated_plugins.cmake - -# Coverage -coverage/ - -# Symbols -app.*.symbols - -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 -!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages -!/dev/ci/**/Gemfile.lock.venv/ -# Python virtual environment -venv/ -.venv/ - +# Miscellaneous +*.class +*.lock +*.log +*.pyc +*.swp +.buildlog/ +.history + + + +# Flutter repo-specific +/bin/cache/ +/bin/internal/bootstrap.bat +/bin/internal/bootstrap.sh +/bin/mingit/ +/dev/benchmarks/mega_gallery/ +/dev/bots/.recipe_deps +/dev/bots/android_tools/ +/dev/devicelab/ABresults*.json +/dev/docs/doc/ +/dev/docs/flutter.docs.zip +/dev/docs/lib/ +/dev/docs/pubspec.yaml +/dev/integration_tests/**/xcuserdata +/dev/integration_tests/**/Pods +/packages/flutter/coverage/ +version +analysis_benchmark.json + +# packages file containing multi-root paths +.packages.generated + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +**/generated_plugin_registrant.dart +.packages +.pub-preload-cache/ +.pub/ +build/ +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds + +# Android related +**/android/**/gradle-wrapper.jar +.gradle/ +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +*.jks + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/.last_build_id +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# macOS +**/Flutter/ephemeral/ +**/Pods/ +**/macos/Flutter/GeneratedPluginRegistrant.swift +**/macos/Flutter/ephemeral +**/xcuserdata/ + +# Windows +**/windows/flutter/generated_plugin_registrant.cc +**/windows/flutter/generated_plugin_registrant.h +**/windows/flutter/generated_plugins.cmake + +# Linux +**/linux/flutter/generated_plugin_registrant.cc +**/linux/flutter/generated_plugin_registrant.h +**/linux/flutter/generated_plugins.cmake + +# Coverage +coverage/ + +# Symbols +app.*.symbols + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +!/dev/ci/**/Gemfile.lock.venv/ +# Python virtual environment +venv/ +.venv/ diff --git a/static/script.js b/static/script.js index 7366707..d6cfba4 100644 --- a/static/script.js +++ b/static/script.js @@ -971,6 +971,239 @@ if (isIndexPage) { // Show a loading message while we wait for the API response if (codeContentEl) codeContentEl.textContent = "Loading starter code..."; + + // ---------------------------------------------------------- + // Copy Code button + // ---------------------------------------------------------- + var btnCopyCode = document.getElementById("btn-copy-code"); + + // ============================================================ +// ROADMAP PROGRESS TRACKER +// ============================================================ + + +var roadmapCheckboxes = document.querySelectorAll( + ".roadmap-checkbox" +); + +var progressFill = document.getElementById( + "roadmap-progress-fill" +); + +var progressText = document.getElementById( + "roadmap-progress-text" +); + +var progressBar = document.querySelector( + ".roadmap-progress-bar" +); + +// Local storage key +var roadmapStorageKey = + `devpath-roadmap-progress-${PROJECT_ID}`; + + +// ------------------------------------------------------------ +// Restore saved roadmap state +// ------------------------------------------------------------ + +var savedRoadmapState = + localStorage.getItem( + roadmapStorageKey + ); + +if(savedRoadmapState){ + + try{ + + var parsedState = + JSON.parse(savedRoadmapState); + + roadmapCheckboxes.forEach( + function(cb,index){ + + cb.checked = + !!parsedState[index]; + + } + ); + + } catch(error){ + + console.error( + "Failed to restore roadmap progress", + error + ); + + } +} + + +// ------------------------------------------------------------ +// Update roadmap progress +// ------------------------------------------------------------ + +function updateRoadmapProgress(){ + + if(!roadmapCheckboxes.length){ + return; + } + + var completed = 0; + + roadmapCheckboxes.forEach(function(cb){ + + var step = cb.closest( + ".roadmap-step" + ); + + if(cb.checked){ + + completed++; + + if(step){ + step.classList.add( + "completed" + ); + } + + } else { + + if(step){ + step.classList.remove( + "completed" + ); + } + + } + + }); + + var percent = Math.round( + (completed / roadmapCheckboxes.length) + * 100 + ); + + // Update progress bar fill + if(progressFill){ + + progressFill.style.width = + percent + "%"; + + } + + // Update progress text + if(progressText){ + + progressText.textContent = + percent + "% completed"; + + } + + // Accessibility update + if(progressBar){ + + progressBar.setAttribute( + "aria-valuenow", + percent + ); + + } + + // Save checkbox state + var savedState = []; + + roadmapCheckboxes.forEach(function(cb){ + + savedState.push( + cb.checked + ); + + }); + + localStorage.setItem( + roadmapStorageKey, + JSON.stringify(savedState) + ); + +} + + +// ------------------------------------------------------------ +// Attach checkbox listeners +// ------------------------------------------------------------ + +roadmapCheckboxes.forEach(function(cb){ + + cb.addEventListener( + "change", + updateRoadmapProgress + ); + +}); + + +// ------------------------------------------------------------ +// Initial progress render +// ------------------------------------------------------------ + +updateRoadmapProgress(); + var copyToast = document.getElementById("copy-toast"); + var toastTimeout = null; + + var copyToast = document.getElementById("copy-toast"); //popup msg when copied + var toastTimeout = null; + + + //shows the "copied to clipboard" state on the button and the toast message, then resets after a short delay + function showCopySuccess() { + if (!btnCopyCode) return; + + // Swap icons on the button(copy and checkmark icons) + var copyIcon = btnCopyCode.querySelector(".copy-icon"); + var checkIcon = btnCopyCode.querySelector(".check-icon"); + var btnLabel = btnCopyCode.querySelector(".copy-btn-label"); + + if (copyIcon) copyIcon.style.display = "none"; + if (checkIcon) checkIcon.style.display = "inline"; + if (btnLabel) btnLabel.textContent = "Copied!"; + btnCopyCode.classList.add("copied"); + // Disable button so user can't spam click it while toast is showing + btnCopyCode.disabled = true; + + // Show toast + if (copyToast) { + copyToast.classList.add("show"); + } + + // Auto-reset after 2.5 s + // Clear any previous timeout first so timers don't stack up + clearTimeout(toastTimeout); + toastTimeout = setTimeout(function () { + if (copyIcon) copyIcon.style.display = "inline"; + if (checkIcon) checkIcon.style.display = "none"; + if (btnLabel) btnLabel.textContent = "Copy Code"; + btnCopyCode.classList.remove("copied"); + btnCopyCode.disabled = false; + if (copyToast) copyToast.classList.remove("show"); + }, 2500); + } + + if (btnCopyCode) { + btnCopyCode.addEventListener("click", function () { + var code = codeContentEl + ? Array.from(codeContentEl.querySelectorAll(".line-content")) + .map(function (el) { return el.textContent; }) + .join("\n") + : ""; + // Don't copy if the code hasn't loaded yet — just ignore the click + if (!code || code === "Loading..." || code === "Loading starter code...") return; + + // Use Clipboard API with textarea fallback + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(code).then(showCopySuccess).catch(function () { + fallbackCopy(code); // clipboard api failed, try the old way + fetch("/project/" + PROJECT_ID + "/code") .then(function (res) { return res.json(); }) .then(function (data) { diff --git a/static/style.css b/static/style.css index 3050d2e..2e3397d 100644 --- a/static/style.css +++ b/static/style.css @@ -2497,6 +2497,67 @@ input[type="text"]:not(.skill-input-wrap input):focus { margin: 4px 0; } +.roadmap-step:last-child .roadmap-line { display: none; } +/* Roadmap timeline structure */ +.roadmap-timeline{ + display:flex; + flex-direction:column; + width:100%; + padding:0; + margin:0; +} + +.roadmap-step{ + display:flex; + width:100%; + list-style:none; +} + +.roadmap-marker{ + display:flex; + flex-direction:column; + align-items:center; + flex-shrink:0; + width:36px; +} + +.roadmap-dot{ + width:14px; + height:14px; + border-radius:50%; + background:var(--indigo-600); + border:3px solid var(--indigo-100); + margin-top:4px; +} + +.roadmap-line{ + flex:1; + width:2px; + background:var(--border); + margin:4px 0; +} + +.roadmap-step:last-child .roadmap-line{ + display:none; +} +.roadmap-content{ + padding:0 0 28px 16px; + flex:1; +} + +/* keep checkbox row aligned */ +.roadmap-step-label{ + display:flex; + align-items:flex-start; + gap:12px; +} + +.roadmap-text-wrap{ + display:flex; + flex-direction:column; + gap:6px; +} + .roadmap-step:last-child .roadmap-line { display: none; } @@ -2506,6 +2567,7 @@ input[type="text"]:not(.skill-input-wrap input):focus { flex: 1; } + .roadmap-step-num { display: inline-block; font-size: 0.72rem; @@ -2524,6 +2586,71 @@ input[type="text"]:not(.skill-input-wrap input):focus { line-height: 1.65; } + +.roadmap-checkbox { + margin-top: 5px; + width: 18px; + height: 18px; + accent-color: var(--indigo-600); + cursor: pointer; + transform: scale(1.05); +} + +/* Completed state */ +.roadmap-step.completed .roadmap-step-text, +.roadmap-step.completed .roadmap-step-num { + text-decoration: line-through; + opacity: 0.55; +} + +/* Make completed node visually */ +.roadmap-step.completed .roadmap-dot { + background: var(--green-500); + box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.15); +} + +/* Progress container */ +.roadmap-progress-wrap{ + display:flex; + align-items:center; + gap:14px; + width:100%; + margin:18px 0 28px; +} + +/* Actual bar */ +.roadmap-progress-bar{ + flex:1; + width:100%; + max-width:100%; + height:10px; + background:#e5e7eb; + border-radius:999px; + overflow:hidden; + border:1px solid #d1d5db; +} + +/* Fill animation */ +#roadmap-progress-fill{ + height:100%; + width:0%; + border-radius:999px; + background:linear-gradient( + 90deg, + #4f46e5, + #9333ea + ); + transition:width .35s ease; +} + +/* Percentage text */ +#roadmap-progress-text{ + white-space:nowrap; + min-width:120px; + font-size:.85rem; + font-weight:600; + color:#6b7280; +} /* Resources list */ .resource-list { display: flex; diff --git a/templates/project.html b/templates/project.html index c9d49b8..94fda9b 100644 --- a/templates/project.html +++ b/templates/project.html @@ -165,6 +165,79 @@
+ Follow these steps in order. + Each one builds on the previous. +
+ + + +Follow these steps in order. Each one builds on the previous.
- - -{{ step | replace("Step " + loop.index|string + ": ", "") }}
-