From 3fc1fd34786c8f5a1f9cba521944002fa4373acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20K=C3=B6ves?= <3187531+vkoves@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:30:49 -0500 Subject: [PATCH] Start EV Road Trip Article w/ Progress Bar --- .eslintrc.js | 3 + ...26-03-15-chicago-to-galena-ev-road-trip.md | 61 +++++ css/articles/ev-road-trip.scss | 229 ++++++++++++++++++ js/articles/ev-road-trip.js | 119 +++++++++ .../chi-gal-ev-roadtrip/ev_station.svg | 6 + .../chi-gal-ev-roadtrip/location_on.svg | 6 + .../chi-gal-ev-roadtrip/tesla-model3.png | Bin 0 -> 143359 bytes 7 files changed, 424 insertions(+) create mode 100644 _posts/2026-03-15-chicago-to-galena-ev-road-trip.md create mode 100644 css/articles/ev-road-trip.scss create mode 100644 js/articles/ev-road-trip.js create mode 100644 post-assets/chi-gal-ev-roadtrip/ev_station.svg create mode 100644 post-assets/chi-gal-ev-roadtrip/location_on.svg create mode 100644 post-assets/chi-gal-ev-roadtrip/tesla-model3.png 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 0000000000000000000000000000000000000000..f6c689039849c69784daba2a3e63a1918cdbb5d6 GIT binary patch literal 143359 zcmYg&2Rzp68~0;mXJ?n0Q8w8-E0Gap%TD$xo3b~N`PiXUgoA9dvq_RYAF{Iv@AaJj z|Np+PqmMkDbNW5^eP7r0UAIWB2P#(y=m-!9#MOJMiaH1cmNWu^aTN~-Ubz`dxC>vf zZRIuO5s315!oTKM;Cl>r9Tf$Pi5~7i_=d?+RYwDX@L@+FfY8gduu^jF@g7$n|1{o=Fx*<_s(~@j~NpX3@0pY30|Lw@cuZG9TU8J9qfi7T_F(E zKvYp1Wc-_>w8Q@IcAL0L@u>Ji!*RkO?EMIb)T!HSshK@;%!qp*-CfO0FENqrBBZpe z7WVm_>{;}mdR1!O>NuX7iMQ?Z>W%PC3|wa zDcq|}Q@hr ^k^kTyDKld!Vkq z1P8oANJyv_KE7v3z95ILOimmg>m^m%x#+rDccF4l?v@k5@b=LMJ%;Po^Ne{d?d=Ia zeE5K_z1>}b2b!AFi$Tf-^VWEZ=?~#-iDVvp%fX(*OCVde&i5no+WvV{@G7)v!h9th zc^nGuUpGxrgPq-t+${W5K`g-vWzjV+r_atSMBJ*|XKProV!DU)Q$nEy$;UIkTN*sL zFQ-#KefVwP$G@oIBg<{JO!;H;ds3P>_=z`PZ})y?>myVAp^^NdXZe*yJptY|e%-S1 z#`6!xyo;-=cwJpxfG}?`MeEc;vo_`p_mGI4m@rV+)MS05W^QGL-D5R!JCh=pU2fa5 zQX$3KySKW&KAM`(^oh6k#N-iuEDjtb?p_HLM~7rgdC8)fGp%K2ighEu#$KW0PAMO@NdL(s1@pL{aNlUOixem zi=`LU880)7r~Q}tjPVoQq>H`K?z{50&}8h{HF#_>35l<8WM-QDqaS?;g_>*dKciAn zQSmxE WTsq|jofCqCv$JzsAj;lDtgUIyf4y~Pzik!`Es{=V)>!sgTZn(^J= zCsrBkVkm&?jLBleuY_cc4-7Q}DzPUx<%3cYYzW&R)#qLm+k6#1EJ+-;4vHzA)sAI~ z4}ylE0RoVa9)fkBIn!2?e%j=BBIq>r<>kqVH)XU^SvV)4lAcHS_FDn7^{F(LaGGbD z>i+pBghMlXZn-!0=dWLX I}Ve)l5v%jYaWjzvj%Q={$vUAL~I)) z2sS=rU4aIUX4@C5`CiVP3Gy%q<>Jy}3$)elwDJD~=z@nT`sDrl_lfZ9D>Rn?XfEDo zhnvvb$30An^R!mu1&@?@ho99+uL#SaN~2w>0Vnu ze;6*P5QH1&g{~Jz8&%ooaz6-v;9#UTI4zxPe3VV(BCm{J@?>tghwy+V!kc ajlyF#y*I7IteX5F zJ3D(}?89TPzsCaEvVn022M0nNcuSr >^N^;9}xaI|IA{` zw8Zp~*EI=4D`0?W5hKtn_L8Vczf-=@!YT3^b!$~Vr(y$#CDDP;b%Ye>>d`c=yjK0R zvKRw=ULY`+ttkf_{~%c}-Hu+}Z}zWgRvfy_^0iL1;(T3pWvIJ)-olKGwpo?!l{$N{ zhs0*9mN4Cc+Iag^p<8WCR@QX~XXnMaIRw19>|q6%xOrMFZ*D*47ZC}IjKl{}!IU#t zpnHq?jan-|(crUsv!c@3?EzN?-CNhB{7)(G-@k9lH`LmS0lnwb7GOqHEEViEF0bw9L1Et;+B!w62-6 z6D+;mtaBzN+`&e Q&;U{>N^Ix3p= z0EK5l{RI((-lBh7R@kpVJ!%gNubiHqx&(?Vr1{BV6p@M^=H}*BAU?cU8!7x+ct14? z pi_ecrdp`ez3c{C zLv#IlM|XFGEj{-srDA;SXL?Zb4hE8=XK05D=4I)(aWhSWRtAT E;kiJ+Xnq&D|@``?ojolH_?n;T?z8<5W2k)Kn+TXPwh#Sp_7 z@@S? ?h7X8g z*9b!QH#QtrepT6bz~-RXIXK*Fjd?{xM2ZSW*P7&iAKe^Y3h?j<0T{ix>FBrHL5=2E zlQqsj4* 1=XE}-S#W-p&_ISI`6+Lup}OvDODGbuH`~a|L8>> z4D3HF9^IW9S@N_rpaPsN>q3*1n@FMX4bkMzVEe{BV4+lYE1;V5;4SZWuN|;Qem3$| zyp^bX5GsL^iaEma@>Ga;fH%xlNPxglVYG^958yCzI;#x1I9HX@+{(h3>p#6$sAHNU z^hS+2QB`i-6W||gYZ VhbKW=kS%H8R6KJCvG z^|Q4t-aX!3d=Y;``j^-4LdQI)Z ?xirUE$<}~G?R+#DQbDA{zcrLB Y!^i&3QfIW%WBFu4Nhus`PSQq3Ll5mVw$>r+cK^7y_8;RzpnwalTW& z6_g>1Z8q+^FSOaiRN3Hx1uyIKISZ0?%J$X8at@eo1`bUMP2T)b!>LIrs6heX^0VVv zjCkl<5!7GNm~sx}-hg=5bKWwnqrFI@->6nHW<^)$517vpG`tL<;wWZ?2zk87+)V4M z6bfHMNMlLKF>@cP>5aU>m8 ?Mff|lV1d27 zo__lDDeLdWQS+?tpQWB33kwTCm~c>1AMAepQ&N(YJn|) %=$T|g%S`EXNQ+ggwojzAgT($3CM<}JR#J}~CykU6fn6c}&uT6_)~x&hQY zT+E4Om93%vNmMB@^(5~8lTlYb*ySX5Q!K4zf9W0(~~@N z@N)~04+6;Mr&lkhj *50+ z=_Rp&FC``JB_(lMYIjXm@-8k|GTj5d(M-CY1vUw2?4>(S+9SYjzKT7O^DpJi