diff --git a/.eslintrc.js b/.eslintrc.js index 5850dc4..f4d8682 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,6 +7,8 @@ module.exports = { 'globals': { // We use jQuery, so $ is fine '$': 'readonly', + // Vue 3 is loaded via CDN as a global in some articles + 'Vue': 'readonly', 'Atomics': 'readonly', 'SharedArrayBuffer': 'readonly' }, @@ -14,6 +16,7 @@ module.exports = { 'ecmaVersion': 2018 }, 'rules': { + // TODO: Change this to indent of 2 'indent': [ 'error', 4 diff --git a/_posts/2026-03-15-chicago-to-galena-ev-road-trip.md b/_posts/2026-03-15-chicago-to-galena-ev-road-trip.md new file mode 100644 index 0000000..464f4e3 --- /dev/null +++ b/_posts/2026-03-15-chicago-to-galena-ev-road-trip.md @@ -0,0 +1,61 @@ +--- +layout: post +title: "Chicago to Galena: A Case Study in Seamless EV Road Tripping" +metadata: + image: + description: + Taking an EV on a road trip from Chicago to Galena - here's how it went, + what charging looked like, and why it was easier than I expected. +stylesheets: + - articles/ev-road-trip.css +scripts: + - articles/ev-road-trip.js +--- + +Intro paragraph here. + +## What I'll Cover + +## The Trip + +Let's get started! I've made a little progress bar to follow along on our journey + + + +
+ +## Day 1: Departing Chicago {#chicago-start} + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +## Rockford Stop {#rockford-day1} + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +## Arriving in Galena {#galena-arrival} + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +## Day 2: Leaving Galena {#galena-day2} + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +## Rockford Again {#rockford-day2} + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +## Back in Chicago {#chicago-return} + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. diff --git a/css/articles/ev-road-trip.scss b/css/articles/ev-road-trip.scss new file mode 100644 index 0000000..523e7cf --- /dev/null +++ b/css/articles/ev-road-trip.scss @@ -0,0 +1,229 @@ +--- +--- + +@import 'variables/colors'; +@import 'variables/sizing'; + +h2 { + border-left: 5px solid transparent; + padding-left: 0.5rem; + margin-left: -0.5rem; + transition: border-color 0.3s ease; + scroll-margin-top: 300px; +} + +h2.-active-section { + border-color: $brand-red; +} + +.trip-progress { + background: var(--light-bg-color); + border-bottom: 2px solid $mid-grey; + padding: 0.5rem 1rem 0.75rem; + margin-bottom: 2rem; + box-shadow: 0px 2px 3px #0000006b; + border-radius: 10rem; +} + +.trip-row { + display: flex; + align-items: center; + gap: 2.5rem; + margin: 0 auto; + padding: 0 1rem; +} + +.trip-days__label { + font-size: 0.65rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-color-faded); + white-space: nowrap; +} + +.trip-track { + position: relative; + height: 3.75rem; + flex: 1; + + // Line sits at the center of the dot row: 20px icon + 4px gap + 10px (half dot) = 34px + &__line { + position: absolute; + top: 33px; + left: 0; + right: 0; + height: 2px; + background: var(--dark-bg-color); + } + + &__progress { + position: absolute; + top: 33px; + left: 0; + height: 2px; + background: $brand-red; + transition: width 0.5s ease; + } + + &__divider { + position: absolute; + top: 0.25rem; + bottom: 0; + left: 50%; + width: 1px; + background: var(--dark-bg-color); + transform: translateX(-50%); + } + + &__car { + position: absolute; + top: 1.45rem; + width: 3rem; + transform: translateX(-50%); + transition: left 0.5s ease; + pointer-events: none; + z-index: 3; + } +} + +.trip-stop { + position: absolute; + top: 0; + display: flex; + flex-direction: column; + align-items: center; + background: none; + border: none; + cursor: pointer; + padding: 0; + transform: translateX(-50%); + text-decoration: none; + color: inherit; + + &:focus-visible { + outline: 2px solid $brand-red; + outline-offset: 2px; + border-radius: 2px; + } + + &__icon { + width: 20px; + height: 20px; + position: relative; + z-index: 2; + transition: filter 0.3s; + } + + &:hover .trip-stop__icon { + filter: invert(14%) sepia(89%) saturate(4680%) hue-rotate(3deg) brightness(85%) contrast(115%); + } + + &__dot { + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--light-bg-color); + border: 2px solid var(--dark-bg-color); + transition: border-color 0.3s; + margin-top: 4px; + position: relative; + z-index: 1; + } + + &__label { + font-size: 0.65rem; + color: var(--text-color-faded); + white-space: nowrap; + margin-top: 0.3rem; + transition: color 0.3s; + } + + &.-visited { + .trip-stop__dot { + background: $brand-red; + border-color: $brand-red; + + &::after { + content: '✓'; + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + color: $white; + font-size: 11px; + font-weight: 700; + line-height: 1; + } + } + + .trip-stop__icon { + filter: invert(14%) sepia(89%) saturate(4680%) hue-rotate(3deg) brightness(85%) contrast(115%); + } + + .trip-stop__label { + color: var(--text-color); + } + } + + &.-active { + .trip-stop__dot { + background: $brand-red; + border-color: $brand-red; + box-shadow: 0 0 0 3px rgba($brand-red, 0.2); + } + + .trip-stop__icon { + filter: drop-shadow(0 0 3px rgba($brand-red, 0.4)) invert(14%) sepia(89%) saturate(4680%) hue-rotate(3deg) brightness(85%) contrast(115%); + } + + .trip-stop__label { + font-weight: 700; + color: var(--brand-text-color); + } + } +} + +@media (prefers-color-scheme: dark) { + .trip-stop__icon { + filter: invert(1); + } + + // In dark mode --dark-bg-color == --light-bg-color, so use a lighter color + // for lines and borders to create contrast against the dark bar background + .trip-track__line, + .trip-track__divider { + background: var(--text-color-faded); + } + + .trip-stop__dot { + border-color: var(--text-color-faded); + } +} + +@media (max-width: $mobile-max-width) { + .trip-progress { + border-radius: 0.5rem; + margin: 0 -0.7rem; + } + + .trip-row { + padding: 0; + gap: 1rem; + flex-wrap: wrap; + } + + .trip-days__label { + width: 45%; + text-align: center; + } + + // Move track below Day 1 & Day 2 labels + .trip-track { + order: 3; + flex-basis: 100%; + max-width: 90%; + margin: auto; + } +} diff --git a/js/articles/ev-road-trip.js b/js/articles/ev-road-trip.js new file mode 100644 index 0000000..b9b79f1 --- /dev/null +++ b/js/articles/ev-road-trip.js @@ -0,0 +1,119 @@ +/** + * Trip progress component for the Chicago → Galena EV road trip article. + * Requires Vue 3 to be loaded globally before this script runs. + * + * Each stop's `id` should match a heading ID in the article markup, e.g.: + * ## Departing Chicago {#chicago-start} + */ + +const { createApp, ref, computed, onMounted, onUnmounted } = Vue; + +const STOPS = [ + { label: 'Chicago', id: 'chicago-start', day: 1 }, + { label: 'Rockford', id: 'rockford-day1', day: 1, charging: true }, + { label: 'Galena', id: 'galena-arrival', day: 1 }, + { label: 'Galena', id: 'galena-day2', day: 2 }, + { label: 'Rockford', id: 'rockford-day2', day: 2, charging: true }, + { label: 'Chicago', id: 'chicago-return', day: 2 }, +]; + +createApp({ + setup() { + const currentStop = ref(0); + + function updateCurrentStop() { + // At the bottom of the page, always activate the last stop + const atBottom = window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 10; + if (atBottom) { + const lastIdx = STOPS.length - 1; + if (lastIdx !== currentStop.value) { + const prev = document.getElementById(STOPS[currentStop.value].id); + if (prev) prev.classList.remove('-active-section'); + const next = document.getElementById(STOPS[lastIdx].id); + if (next) next.classList.add('-active-section'); + currentStop.value = lastIdx; + } + return; + } + + // Trigger when a section heading reaches 40% down the viewport + const threshold = window.scrollY + window.innerHeight * 0.4; + let active = 0; + for (let i = 0; i < STOPS.length; i++) { + const el = document.getElementById(STOPS[i].id); + if (el && el.getBoundingClientRect().top + window.scrollY <= threshold) { + active = i; + } + } + + if (active !== currentStop.value) { + const prev = document.getElementById(STOPS[currentStop.value].id); + if (prev) prev.classList.remove('-active-section'); + const next = document.getElementById(STOPS[active].id); + if (next) next.classList.add('-active-section'); + } + + currentStop.value = active; + } + + onMounted(() => { + window.addEventListener('scroll', updateCurrentStop, { passive: true }); + updateCurrentStop(); + }); + + onUnmounted(() => { + window.removeEventListener('scroll', updateCurrentStop); + }); + + const carLeftPercent = computed(() => + (currentStop.value / (STOPS.length - 1)) * 100 + ); + + function stopLeftPercent(i) { + return (i / (STOPS.length - 1)) * 100; + } + + return { STOPS, currentStop, carLeftPercent, stopLeftPercent }; + }, + + template: ` + + ` +}).mount('#trip-progress'); diff --git a/post-assets/chi-gal-ev-roadtrip/ev_station.svg b/post-assets/chi-gal-ev-roadtrip/ev_station.svg new file mode 100644 index 0000000..34e21a2 --- /dev/null +++ b/post-assets/chi-gal-ev-roadtrip/ev_station.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/post-assets/chi-gal-ev-roadtrip/location_on.svg b/post-assets/chi-gal-ev-roadtrip/location_on.svg new file mode 100644 index 0000000..2e0e6e7 --- /dev/null +++ b/post-assets/chi-gal-ev-roadtrip/location_on.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/post-assets/chi-gal-ev-roadtrip/tesla-model3.png b/post-assets/chi-gal-ev-roadtrip/tesla-model3.png new file mode 100644 index 0000000..f6c6890 Binary files /dev/null and b/post-assets/chi-gal-ev-roadtrip/tesla-model3.png differ