@@ -433,7 +433,7 @@ <h3 class="text-base font-semibold font-mono text-zinc-900 dark:text-zinc-100 mb
433433 </ div >
434434 </ div >
435435</ div >
436- < div id ="project-ml-ticker " class ="project-card group relative flex flex-col bg-transparent border border-zinc-300 dark:border-zinc-700 rounded-none hover:border-accent dark:hover:border-accent transition-colors overflow-hidden " data-category ="aerpaw ">
436+ < div id ="project-ml-ticker " class ="project-card group relative flex flex-col bg-transparent border border-zinc-300 dark:border-zinc-700 rounded-none hover:border-accent dark:hover:border-accent transition-colors overflow-hidden " data-category ="other ">
437437 < img src ="https://placehold.co/400x250/34495E/FFFFFF?text=ML-Ticker " alt ="ML-Ticker " loading ="lazy " decoding ="async " class ="w-full h-48 object-cover border-b border-zinc-300 dark:border-zinc-700 ">
438438 < div class ="p-5 flex flex-col flex-grow ">
439439 < div class ="flex items-start justify-between ">
@@ -442,7 +442,7 @@ <h3 class="text-base font-semibold font-mono text-zinc-900 dark:text-zinc-100 mb
442442 < ion-icon name ="link " class ="text-lg "> </ ion-icon >
443443 </ button >
444444 </ div >
445- < span class ="text-xs font-medium font-mono text-zinc-500 dark:text-zinc-400 mb-2 block flex items-center "> < ion-icon name ="time-outline " class ="mr-1 "> </ ion-icon > 2026 - Present • AERPAW </ span >
445+ < span class ="text-xs font-medium font-mono text-zinc-500 dark:text-zinc-400 mb-2 block flex items-center "> < ion-icon name ="time-outline " class ="mr-1 "> </ ion-icon > 2026 - Present • Other </ span >
446446 < p class ="text-sm text-zinc-600 dark:text-zinc-400 mb-4 flex-grow leading-relaxed "> A Python command-line utility for retrieving equity research metrics via the FactSet FQL API.</ p >
447447 < div >
448448 < div class ="flex flex-wrap mt-3 "> < span class ="inline-flex items-center px-2 py-0.5 rounded-none border border-zinc-300 dark:border-zinc-700 text-xs font-medium font-mono bg-transparent text-zinc-600 dark:text-zinc-400 mr-1.5 mb-1.5 "> Python</ span > < span class ="inline-flex items-center px-2 py-0.5 rounded-none border border-zinc-300 dark:border-zinc-700 text-xs font-medium font-mono bg-transparent text-zinc-600 dark:text-zinc-400 mr-1.5 mb-1.5 "> FactSet FQL</ span > < span class ="inline-flex items-center px-2 py-0.5 rounded-none border border-zinc-300 dark:border-zinc-700 text-xs font-medium font-mono bg-transparent text-zinc-600 dark:text-zinc-400 mr-1.5 mb-1.5 "> Pydantic</ span > < span class ="inline-flex items-center px-2 py-0.5 rounded-none border border-zinc-300 dark:border-zinc-700 text-xs font-medium font-mono bg-transparent text-zinc-600 dark:text-zinc-400 mr-1.5 mb-1.5 "> openpyxl</ span > < span class ="inline-flex items-center px-2 py-0.5 rounded-none border border-zinc-300 dark:border-zinc-700 text-xs font-medium font-mono bg-transparent text-zinc-600 dark:text-zinc-400 mr-1.5 mb-1.5 "> CLI</ span > </ div >
@@ -1046,7 +1046,7 @@ <h2 class="text-2xl font-bold font-mono text-zinc-900 dark:text-zinc-100">aerpaw
10461046 < h2 class ="text-2xl font-bold font-mono text-zinc-900 dark:text-zinc-100 "> ML-Ticker</ h2 >
10471047 < button class ="modal-close-btn text-zinc-500 hover:text-accent dark:hover:text-accent " data-modal-id ="modal-project-ml-ticker "> ✕</ button >
10481048 </ div >
1049- < p class ="text-sm text-zinc-600 dark:text-zinc-400 leading-relaxed mb-4 "> ML-Ticker is a Python command-line tool built for the Merrill Lynch aerpaw to query FactSet FQL API data. It uses a sandboxed Python formula engine to compute post-query custom metrics, features an automatic dependency DAG, validates configuration files via Pydantic, and outputs colorized terminal tables or styled Excel spreadsheets.</ p >
1049+ < p class ="text-sm text-zinc-600 dark:text-zinc-400 leading-relaxed mb-4 "> ML-Ticker is a Python command-line tool built for Merrill Lynch to query FactSet FQL API data. It uses a sandboxed Python formula engine to compute post-query custom metrics, features an automatic dependency DAG, validates configuration files via Pydantic, and outputs colorized terminal tables or styled Excel spreadsheets.</ p >
10501050
10511051 < div class ="mt-4 "> < h4 class ="font-semibold font-mono text-zinc-900 dark:text-zinc-100 mb-2 "> Features</ h4 > < ul class ="space-y-1 "> < li class ="text-sm font-mono text-zinc-600 dark:text-zinc-400 flex items-start "> < span class ="mr-3 "> •</ span > < span > FactSet FQL API integration for snapshot and historical timeseries data</ span > </ li > < li class ="text-sm font-mono text-zinc-600 dark:text-zinc-400 flex items-start "> < span class ="mr-3 "> •</ span > < span > Sandboxed Python formula engine for post-query custom metrics</ span > </ li > < li class ="text-sm font-mono text-zinc-600 dark:text-zinc-400 flex items-start "> < span class ="mr-3 "> •</ span > < span > Dependency validation using directed acyclic graphs (DAGs) and Pydantic</ span > </ li > < li class ="text-sm font-mono text-zinc-600 dark:text-zinc-400 flex items-start "> < span class ="mr-3 "> •</ span > < span > Colorized terminal tables and styled multi-sheet Excel spreadsheet export</ span > </ li > </ ul > </ div >
10521052 < div class ="mt-6 flex gap-3 ">
@@ -1190,16 +1190,126 @@ <h2 class="text-2xl font-bold font-mono text-zinc-900 dark:text-zinc-100">ML-Tic
11901190 selectFilter ( initialFilter ) ;
11911191 }
11921192
1193+ // Media Carousel Logic
1194+ const initMediaCarousels = ( ) => {
1195+ const carousels = document . querySelectorAll ( ".media-carousel" ) ;
1196+ carousels . forEach ( ( carousel ) => {
1197+ const slidesContainer = carousel . querySelector ( ".carousel-slides" ) ;
1198+ const prevBtn = carousel . querySelector ( ".carousel-prev" ) ;
1199+ const nextBtn = carousel . querySelector ( ".carousel-next" ) ;
1200+ const dots = carousel . querySelectorAll ( ".carousel-dot" ) ;
1201+
1202+ if ( ! slidesContainer ) return ;
1203+
1204+ const totalSlides = slidesContainer . children . length ;
1205+ let currentIndex = 0 ;
1206+
1207+ const goToSlide = ( index ) => {
1208+ if ( index < 0 ) {
1209+ currentIndex = totalSlides - 1 ;
1210+ } else if ( index >= totalSlides ) {
1211+ currentIndex = 0 ;
1212+ } else {
1213+ currentIndex = index ;
1214+ }
1215+
1216+ slidesContainer . style . transform = `translateX(-${ currentIndex * 100 } %)` ;
1217+
1218+ // Update dots
1219+ dots . forEach ( ( dot , idx ) => {
1220+ if ( idx === currentIndex ) {
1221+ dot . classList . add ( "bg-accent" ) ;
1222+ dot . classList . remove ( "bg-white/40" , "hover:bg-white/70" ) ;
1223+ } else {
1224+ dot . classList . remove ( "bg-accent" ) ;
1225+ dot . classList . add ( "bg-white/40" , "hover:bg-white/70" ) ;
1226+ }
1227+ } ) ;
1228+
1229+ // Pause videos on inactive slides
1230+ Array . from ( slidesContainer . children ) . forEach ( ( slide , idx ) => {
1231+ const video = slide . querySelector ( "video" ) ;
1232+ if ( video && idx !== currentIndex ) {
1233+ video . pause ( ) ;
1234+ }
1235+ } ) ;
1236+ } ;
1237+
1238+ // Attach function to the element so we can invoke it on modal actions
1239+ carousel . goToSlide = goToSlide ;
1240+
1241+ if ( prevBtn ) {
1242+ prevBtn . addEventListener ( "click" , ( e ) => {
1243+ e . stopPropagation ( ) ;
1244+ goToSlide ( currentIndex - 1 ) ;
1245+ } ) ;
1246+ }
1247+
1248+ if ( nextBtn ) {
1249+ nextBtn . addEventListener ( "click" , ( e ) => {
1250+ e . stopPropagation ( ) ;
1251+ goToSlide ( currentIndex + 1 ) ;
1252+ } ) ;
1253+ }
1254+
1255+ dots . forEach ( ( dot ) => {
1256+ dot . addEventListener ( "click" , ( e ) => {
1257+ e . stopPropagation ( ) ;
1258+ const slideIndex = parseInt ( dot . getAttribute ( "data-slide-to" ) , 10 ) ;
1259+ if ( ! isNaN ( slideIndex ) ) {
1260+ goToSlide ( slideIndex ) ;
1261+ }
1262+ } ) ;
1263+ } ) ;
1264+
1265+ // Swipe Support for Touch Screens
1266+ let touchStartX = 0 ;
1267+ let touchEndX = 0 ;
1268+
1269+ carousel . addEventListener ( "touchstart" , ( e ) => {
1270+ touchStartX = e . changedTouches [ 0 ] . screenX ;
1271+ } , { passive : true } ) ;
1272+
1273+ carousel . addEventListener ( "touchend" , ( e ) => {
1274+ touchEndX = e . changedTouches [ 0 ] . screenX ;
1275+ const swipeThreshold = 50 ;
1276+ if ( touchEndX < touchStartX - swipeThreshold ) {
1277+ goToSlide ( currentIndex + 1 ) ;
1278+ } else if ( touchEndX > touchStartX + swipeThreshold ) {
1279+ goToSlide ( currentIndex - 1 ) ;
1280+ }
1281+ } , { passive : true } ) ;
1282+ } ) ;
1283+ } ;
1284+
1285+ initMediaCarousels ( ) ;
1286+
11931287 const detailsBtns = document . querySelectorAll ( ".project-details-btn" ) ;
11941288 const closeBtns = document . querySelectorAll ( ".modal-close-btn" ) ;
11951289
1290+ const pauseModalVideos = ( modal ) => {
1291+ if ( modal ) {
1292+ modal . querySelectorAll ( "video" ) . forEach ( ( v ) => v . pause ( ) ) ;
1293+ }
1294+ } ;
1295+
1296+ const resetCarousel = ( modal ) => {
1297+ if ( modal ) {
1298+ const carousel = modal . querySelector ( ".media-carousel" ) ;
1299+ if ( carousel && typeof carousel . goToSlide === "function" ) {
1300+ carousel . goToSlide ( 0 ) ;
1301+ }
1302+ }
1303+ } ;
1304+
11961305 detailsBtns . forEach ( ( btn ) => {
11971306 btn . addEventListener ( "click" , ( ) => {
11981307 const modalId = btn . getAttribute ( "data-modal-id" ) ;
11991308 const modal = document . getElementById ( modalId ) ;
12001309 if ( modal ) {
12011310 modal . classList . remove ( "hidden" ) ;
12021311 modal . style . display = "flex" ;
1312+ resetCarousel ( modal ) ;
12031313 }
12041314 } ) ;
12051315 } ) ;
@@ -1211,6 +1321,7 @@ <h2 class="text-2xl font-bold font-mono text-zinc-900 dark:text-zinc-100">ML-Tic
12111321 if ( modal ) {
12121322 modal . classList . add ( "hidden" ) ;
12131323 modal . style . display = "none" ;
1324+ pauseModalVideos ( modal ) ;
12141325 }
12151326 } ) ;
12161327 } ) ;
@@ -1220,6 +1331,7 @@ <h2 class="text-2xl font-bold font-mono text-zinc-900 dark:text-zinc-100">ML-Tic
12201331 if ( event . target === modal ) {
12211332 modal . classList . add ( "hidden" ) ;
12221333 modal . style . display = "none" ;
1334+ pauseModalVideos ( modal ) ;
12231335 }
12241336 } ) ;
12251337 } ) ;
@@ -1229,6 +1341,7 @@ <h2 class="text-2xl font-bold font-mono text-zinc-900 dark:text-zinc-100">ML-Tic
12291341 document . querySelectorAll ( '[id^="modal-"]' ) . forEach ( ( modal ) => {
12301342 modal . classList . add ( "hidden" ) ;
12311343 modal . style . display = "none" ;
1344+ pauseModalVideos ( modal ) ;
12321345 } ) ;
12331346 }
12341347 } ) ;
@@ -1519,6 +1632,9 @@ <h2 class="text-2xl font-bold font-mono text-zinc-900 dark:text-zinc-100">ML-Tic
15191632 background-color: rgb(212, 212, 216) !important;
15201633 color: #09090b !important;
15211634 }
1635+ .carousel-slides {
1636+ will-change: transform;
1637+ }
15221638 ` ;
15231639 document . head . appendChild ( style ) ;
15241640 } ) ;
0 commit comments