[{"data":1,"prerenderedAt":601},["ShallowReactive",2],{"project-bodylog":3},{"id":4,"title":5,"body":6,"description":580,"duration":581,"extension":582,"featured":583,"github":584,"image":585,"live":586,"meta":587,"navigation":427,"order":171,"path":588,"role":586,"seo":589,"status":590,"stem":591,"team_size":586,"tech":592,"type":598,"year":599,"__hash__":600},"projects\u002Fproject\u002Fbodylog.md","BodyLog: Serverless Fitness Tracker",{"type":7,"value":8,"toc":572},"minimark",[9,14,18,22,30,58,62,67,73,79,83,88,100,326,330,333,568],[10,11,13],"h2",{"id":12},"the-problem","The Problem",[15,16,17],"p",{},"The fitness app market is heavily saturated, yet fundamentally flawed. Most mainstream workout trackers lock users into expensive monthly subscriptions, hide their own workout data behind proprietary walled gardens, and feature bloated, overly complex interfaces that slow down logging mid-workout. Users need a system focused entirely on progressive overload, speed, and absolute data ownership.",[10,19,21],{"id":20},"my-solution","My Solution",[15,23,24,25,29],{},"I engineered ",[26,27,28],"strong",{},"BodyLog",", a high-contrast, brutalist web application that completely bypasses traditional databases in favor of a zero-cost, fully sovereign data architecture.",[31,32,33,40,46,52],"ul",{},[34,35,36,39],"li",{},[26,37,38],{},"Zero-DB Architecture:"," Integrated the Google Sheets v4 API via Google Cloud Service Accounts. Every set, rep, and weigh-in is instantly written to a private Google Sheet owned by the user, ensuring data is never trapped in a proprietary ecosystem.",[34,41,42,45],{},[26,43,44],{},"Dynamic Program Modes:"," Engineered a unified interface that seamlessly switches between Gym (barbell\u002Fmachine focus) and Calisthenics (bodyweight progression) tracking states.",[34,47,48,51],{},[26,49,50],{},"AI Coach Integration:"," Implemented a data-export pipeline that compiles weekly workout telemetry into a highly structured prompt, allowing users to leverage LLMs (like Gemini) for personalized progressive overload analysis and nutrition coaching.",[34,53,54,57],{},[26,55,56],{},"Automated Sheet Formatting:"," Wrote low-level API batch updates to automatically style, color-code, and format the raw Google Sheet whenever new weekly modules are generated.",[10,59,61],{"id":60},"technical-deep-dive","Technical Deep Dive",[63,64,66],"h3",{"id":65},"architecture-decisions","Architecture Decisions",[15,68,69,72],{},[26,70,71],{},"Why Google Sheets as a Database?","\nFor a personal tracking application, deploying a managed PostgreSQL instance is architectural overkill and incurs unnecessary cloud costs. Google Sheets natively supports tabular data (which perfectly matches set\u002Frep logs), provides a free visual GUI for manual edits, and costs exactly $0 to run. By communicating securely via server-side Nitro API routes, the Google Service Account credentials remain completely hidden from the client.",[15,74,75,78],{},[26,76,77],{},"Why Nuxt 4 & Serverless?","\nTo achieve an instantaneous, native-app feel on mobile browsers, the application requires heavy hydration and optimized chunking. Nuxt 4 paired with TailwindCSS v4 provides a blazing-fast frontend, while its integrated Nitro engine allows the backend API routes (which execute the Google Sheets payload) to be deployed seamlessly to Vercel as highly efficient Serverless Functions.",[63,80,82],{"id":81},"key-features-i-built","Key Features I Built",[84,85,87],"h4",{"id":86},"_1-automated-sheet-orchestration","1. Automated Sheet Orchestration",[15,89,90,91,95,96,99],{},"Writing raw strings to a spreadsheet is trivial, but keeping the sheet highly readable for the user is complex. I built a backend utility that intercepts the save event and sends a batch of ",[92,93,94],"code",{},"userEnteredFormat"," and ",[92,97,98],{},"addBanding"," requests to the Google Sheets API. This automatically resizes columns, adds borders, and applies alternating row colors dynamically without the user ever opening the sheet application.",[101,102,107],"pre",{"className":103,"code":104,"language":105,"meta":106,"style":106},"language-typescript shiki shiki-themes github-light github-dark","\u002F\u002F server\u002Futils\u002Fsheets.ts - Automating spreadsheet UI via API\nrequests.push({\n  addBanding: {\n    bandedRange: {\n      range: {\n        sheetId,\n        startRowIndex: 0,\n        endRowIndex: totalRows,\n        startColumnIndex: 0,\n        endColumnIndex: 10,\n      },\n      rowProperties: {\n        headerColor: { red: 0.13, green: 0.59, blue: 0.6 },\n        firstBandColor: { red: 1, green: 1, blue: 1 },\n        secondBandColor: { red: 0.95, green: 0.98, blue: 0.98 },\n      },\n    },\n  },\n});\nawait sheets.spreadsheets.batchUpdate({\n  spreadsheetId,\n  requestBody: { requests },\n});\n","typescript","",[92,108,109,118,132,138,144,150,156,169,175,185,196,202,208,232,251,271,276,282,288,294,309,315,321],{"__ignoreMap":106},[110,111,114],"span",{"class":112,"line":113},"line",1,[110,115,117],{"class":116},"sJ8bj","\u002F\u002F server\u002Futils\u002Fsheets.ts - Automating spreadsheet UI via API\n",[110,119,121,125,129],{"class":112,"line":120},2,[110,122,124],{"class":123},"sVt8B","requests.",[110,126,128],{"class":127},"sScJk","push",[110,130,131],{"class":123},"({\n",[110,133,135],{"class":112,"line":134},3,[110,136,137],{"class":123},"  addBanding: {\n",[110,139,141],{"class":112,"line":140},4,[110,142,143],{"class":123},"    bandedRange: {\n",[110,145,147],{"class":112,"line":146},5,[110,148,149],{"class":123},"      range: {\n",[110,151,153],{"class":112,"line":152},6,[110,154,155],{"class":123},"        sheetId,\n",[110,157,159,162,166],{"class":112,"line":158},7,[110,160,161],{"class":123},"        startRowIndex: ",[110,163,165],{"class":164},"sj4cs","0",[110,167,168],{"class":123},",\n",[110,170,172],{"class":112,"line":171},8,[110,173,174],{"class":123},"        endRowIndex: totalRows,\n",[110,176,178,181,183],{"class":112,"line":177},9,[110,179,180],{"class":123},"        startColumnIndex: ",[110,182,165],{"class":164},[110,184,168],{"class":123},[110,186,188,191,194],{"class":112,"line":187},10,[110,189,190],{"class":123},"        endColumnIndex: ",[110,192,193],{"class":164},"10",[110,195,168],{"class":123},[110,197,199],{"class":112,"line":198},11,[110,200,201],{"class":123},"      },\n",[110,203,205],{"class":112,"line":204},12,[110,206,207],{"class":123},"      rowProperties: {\n",[110,209,211,214,217,220,223,226,229],{"class":112,"line":210},13,[110,212,213],{"class":123},"        headerColor: { red: ",[110,215,216],{"class":164},"0.13",[110,218,219],{"class":123},", green: ",[110,221,222],{"class":164},"0.59",[110,224,225],{"class":123},", blue: ",[110,227,228],{"class":164},"0.6",[110,230,231],{"class":123}," },\n",[110,233,235,238,241,243,245,247,249],{"class":112,"line":234},14,[110,236,237],{"class":123},"        firstBandColor: { red: ",[110,239,240],{"class":164},"1",[110,242,219],{"class":123},[110,244,240],{"class":164},[110,246,225],{"class":123},[110,248,240],{"class":164},[110,250,231],{"class":123},[110,252,254,257,260,262,265,267,269],{"class":112,"line":253},15,[110,255,256],{"class":123},"        secondBandColor: { red: ",[110,258,259],{"class":164},"0.95",[110,261,219],{"class":123},[110,263,264],{"class":164},"0.98",[110,266,225],{"class":123},[110,268,264],{"class":164},[110,270,231],{"class":123},[110,272,274],{"class":112,"line":273},16,[110,275,201],{"class":123},[110,277,279],{"class":112,"line":278},17,[110,280,281],{"class":123},"    },\n",[110,283,285],{"class":112,"line":284},18,[110,286,287],{"class":123},"  },\n",[110,289,291],{"class":112,"line":290},19,[110,292,293],{"class":123},"});\n",[110,295,297,301,304,307],{"class":112,"line":296},20,[110,298,300],{"class":299},"szBVR","await",[110,302,303],{"class":123}," sheets.spreadsheets.",[110,305,306],{"class":127},"batchUpdate",[110,308,131],{"class":123},[110,310,312],{"class":112,"line":311},21,[110,313,314],{"class":123},"  spreadsheetId,\n",[110,316,318],{"class":112,"line":317},22,[110,319,320],{"class":123},"  requestBody: { requests },\n",[110,322,324],{"class":112,"line":323},23,[110,325,293],{"class":123},[84,327,329],{"id":328},"_2-cross-program-mode-switching","2. Cross-Program Mode Switching",[15,331,332],{},"To handle different fitness regimens, I utilized Vue 3 Composables to manage global state dynamically. The UI and the underlying data schema adapt instantly depending on whether the user is tracking heavy barbell lifts or bodyweight static holds.",[101,334,336],{"className":103,"code":335,"language":105,"meta":106,"style":106},"\u002F\u002F app\u002Fcomposables\u002FuseMode.ts - Global state management for workout modalities\nexport const useMode = () => {\n  const currentMode = useState\u003C\"gym\" | \"calist\" | null>(\n    \"workout_mode\",\n    () => null,\n  );\n\n  const setMode = (mode: \"gym\" | \"calist\") => {\n    currentMode.value = mode;\n    localStorage.setItem(\"workout_mode\", mode);\n  };\n\n  return {\n    currentMode,\n    isGym: computed(() => currentMode.value === \"gym\"),\n    isCalist: computed(() => currentMode.value === \"calist\"),\n    setMode,\n  };\n};\n",[92,337,338,343,366,400,407,418,423,429,462,473,490,495,499,506,511,535,554,559,563],{"__ignoreMap":106},[110,339,340],{"class":112,"line":113},[110,341,342],{"class":116},"\u002F\u002F app\u002Fcomposables\u002FuseMode.ts - Global state management for workout modalities\n",[110,344,345,348,351,354,357,360,363],{"class":112,"line":120},[110,346,347],{"class":299},"export",[110,349,350],{"class":299}," const",[110,352,353],{"class":127}," useMode",[110,355,356],{"class":299}," =",[110,358,359],{"class":123}," () ",[110,361,362],{"class":299},"=>",[110,364,365],{"class":123}," {\n",[110,367,368,371,374,376,379,382,386,389,392,394,397],{"class":112,"line":134},[110,369,370],{"class":299},"  const",[110,372,373],{"class":164}," currentMode",[110,375,356],{"class":299},[110,377,378],{"class":127}," useState",[110,380,381],{"class":123},"\u003C",[110,383,385],{"class":384},"sZZnC","\"gym\"",[110,387,388],{"class":299}," |",[110,390,391],{"class":384}," \"calist\"",[110,393,388],{"class":299},[110,395,396],{"class":164}," null",[110,398,399],{"class":123},">(\n",[110,401,402,405],{"class":112,"line":140},[110,403,404],{"class":384},"    \"workout_mode\"",[110,406,168],{"class":123},[110,408,409,412,414,416],{"class":112,"line":146},[110,410,411],{"class":123},"    () ",[110,413,362],{"class":299},[110,415,396],{"class":164},[110,417,168],{"class":123},[110,419,420],{"class":112,"line":152},[110,421,422],{"class":123},"  );\n",[110,424,425],{"class":112,"line":158},[110,426,428],{"emptyLinePlaceholder":427},true,"\n",[110,430,431,433,436,438,441,445,448,451,453,455,458,460],{"class":112,"line":171},[110,432,370],{"class":299},[110,434,435],{"class":127}," setMode",[110,437,356],{"class":299},[110,439,440],{"class":123}," (",[110,442,444],{"class":443},"s4XuR","mode",[110,446,447],{"class":299},":",[110,449,450],{"class":384}," \"gym\"",[110,452,388],{"class":299},[110,454,391],{"class":384},[110,456,457],{"class":123},") ",[110,459,362],{"class":299},[110,461,365],{"class":123},[110,463,464,467,470],{"class":112,"line":177},[110,465,466],{"class":123},"    currentMode.value ",[110,468,469],{"class":299},"=",[110,471,472],{"class":123}," mode;\n",[110,474,475,478,481,484,487],{"class":112,"line":187},[110,476,477],{"class":123},"    localStorage.",[110,479,480],{"class":127},"setItem",[110,482,483],{"class":123},"(",[110,485,486],{"class":384},"\"workout_mode\"",[110,488,489],{"class":123},", mode);\n",[110,491,492],{"class":112,"line":198},[110,493,494],{"class":123},"  };\n",[110,496,497],{"class":112,"line":204},[110,498,428],{"emptyLinePlaceholder":427},[110,500,501,504],{"class":112,"line":210},[110,502,503],{"class":299},"  return",[110,505,365],{"class":123},[110,507,508],{"class":112,"line":234},[110,509,510],{"class":123},"    currentMode,\n",[110,512,513,516,519,522,524,527,530,532],{"class":112,"line":253},[110,514,515],{"class":123},"    isGym: ",[110,517,518],{"class":127},"computed",[110,520,521],{"class":123},"(() ",[110,523,362],{"class":299},[110,525,526],{"class":123}," currentMode.value ",[110,528,529],{"class":299},"===",[110,531,450],{"class":384},[110,533,534],{"class":123},"),\n",[110,536,537,540,542,544,546,548,550,552],{"class":112,"line":273},[110,538,539],{"class":123},"    isCalist: ",[110,541,518],{"class":127},[110,543,521],{"class":123},[110,545,362],{"class":299},[110,547,526],{"class":123},[110,549,529],{"class":299},[110,551,391],{"class":384},[110,553,534],{"class":123},[110,555,556],{"class":112,"line":278},[110,557,558],{"class":123},"    setMode,\n",[110,560,561],{"class":112,"line":284},[110,562,494],{"class":123},[110,564,565],{"class":112,"line":290},[110,566,567],{"class":123},"};\n",[569,570,571],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":106,"searchDepth":120,"depth":120,"links":573},[574,575,576],{"id":12,"depth":120,"text":13},{"id":20,"depth":120,"text":21},{"id":60,"depth":120,"text":61,"children":577},[578,579],{"id":65,"depth":134,"text":66},{"id":81,"depth":134,"text":82},"A brutalist, serverless progressive overload tracker built with Nuxt 4, utilizing Google Sheets API as a zero-cost headless database for 100% data ownership.","1 week","md",false,"https:\u002F\u002Fgithub.com\u002Fszuryuu\u002Fbodylog","\u002Fimages\u002Fprojects\u002Fnuxt.png",null,{},"\u002Fproject\u002Fbodylog",{"title":5,"description":580},"Completed","project\u002Fbodylog",[593,594,595,596,597],"Nuxt 4","Vue 3","Tailwind CSS v4","Google Sheets API","Serverless","Solo Project","2026","ey7cQ1NieXq0ivJFB0SnsjORH_c3LXtgwNNPeoqM8Iw",1776582962961]