[{"data":1,"prerenderedAt":765},["ShallowReactive",2],{"navigation":3,"/projects/dalagan":14,"/projects/dalagan-surround":754},[4],{"title":5,"path":6,"stem":7,"children":8,"page":13},"Blog","/blog","blog",[9],{"title":10,"path":11,"stem":12},"My Experience as a Junior Software Engineer (First Year)","/blog/my-experience-as-a-junior-software-engineer","blog/my-experience-as-a-junior-software-engineer",false,{"id":15,"title":16,"body":17,"buttonLabel":736,"date":737,"description":738,"extension":739,"image":740,"meta":741,"navigation":360,"path":742,"seo":743,"stem":744,"tags":745,"__hash__":753},"projects/projects/dalagan.md","Dalagan — From a Strava Sync Script to a Coaching Platform",{"type":18,"value":19,"toc":718},"minimark",[20,25,29,32,40,43,46,50,57,74,92,106,116,126,136,150,152,156,159,165,170,181,368,375,379,390,404,408,411,418,422,425,427,431,441,447,453,459,461,465,471,477,481,497,501,515,627,629,633,667,669,673,687,693,699,701,705,708,711,714],[21,22,24],"h2",{"id":23},"the-backstory","THE BACKSTORY",[26,27,28],"p",{},"I built the first version — Splitlog — because I was tired of a five-minute ritual after every run: open Strava, read off the stats, open Notion, retype everything into a training log. So I automated it. Strava OAuth, webhooks, a dashboard, a training plan table, four analytics charts. First full-stack project, first time with Next.js and Supabase, first real OAuth integration. It worked. The moment a run synced automatically without me touching anything — that's when full-stack development clicked for me.",[26,30,31],{},"But \"it works\" and \"it's designed\" are different bars.",[26,33,34,35,39],{},"Splitlog was a spreadsheet with a login screen. Every page was a table or a form. The dashboard was a run log with four stat cards bolted on top. There was no sense of ",[36,37,38],"em",{},"where you were"," in training, no coaching, no product thinking beyond \"surface the data.\" It solved my original problem — manual data entry — and stopped there.",[26,41,42],{},"So I went back through the entire app, page by page, and rebuilt it as a real product.",[44,45],"hr",{},[21,47,49],{"id":48},"the-rebuild","THE REBUILD",[26,51,52,53,56],{},"I didn't touch code first. I went through every page and asked the same question each time: ",[36,54,55],{},"what is this page's actual job, and does its current design do that job?"," Usually the answer was no.",[26,58,59,64,68,69,73],{},[60,61],"img",{"alt":62,"src":63},"Dalagan Dashboard","/projects/splitlog/dashboard1.png",[65,66,67],"strong",{},"The dashboard wasn't a dashboard."," It was the run table with stat cards on top — total runs, total distance, total elevation, numbers that don't change what you do today. I split it in two: a ",[70,71,72],"code",{},"/runs"," page that's the real archive (grouped by week, filterable, sortable), and a dashboard that answers \"what do I do right now\" — today's scheduled workout, this week's progress, training load, one specific coaching observation instead of four vanity metrics.",[26,75,76,80,83,84,87,88,91],{},[60,77],{"alt":78,"src":79},"Dalagan Races","/projects/splitlog/races.png",[65,81,82],{},"There was no concept of a race."," Everything in a runner's training points at a goal, and the original app had nowhere to put one. I designed a ",[70,85,86],{},"/races"," page around a single idea: the next race is the whole point, so it gets hero treatment — a countdown, a projected finish time plotted against the goal over time, a generated race-day pace strategy. Training plans, which used to be freestanding objects, now belong ",[36,89,90],{},"to"," a race.",[26,93,94,98,101,102,105],{},[60,95],{"alt":96,"src":97},"Dalagan Bests","/projects/splitlog/personal-bests1.png",[65,99,100],{},"Personal bests were buried in a chart tab."," I pulled them into their own ",[70,103,104],{},"/records"," page — official race PRs separated from best efforts, a locked/aspirational card for every distance you haven't raced yet (\"Your first marathon is coming — Cebu Marathon, 97 days\"), and a shareable card, since a PR is the one thing in a running app people actually want to post.",[26,107,108,112,115],{},[60,109],{"alt":110,"src":111},"Dalagan Plans","/projects/splitlog/training-plan1.png",[65,113,114],{},"Training plans were a flat table of 64 rows."," I redesigned the plan detail view around weeks instead of days — collapsible week groups, phase labels (build/cutback/taper), the current week expanded by default instead of day one of a plan that started three months ago.",[26,117,118,122,125],{},[60,119],{"alt":120,"src":121},"Dalagan Coach","/projects/splitlog/coach-nash.png",[65,123,124],{},"The AI coach — the actual reason to pick this app over a spreadsheet — had no idea what plan you were on."," I rebuilt Coach Nash around a single context object assembled from your real training data (active plan, goal race, adherence, recent runs, PRs), had it generate one specific insight on sync instead of a chat box waiting to be prompted, and gave it the ability to propose concrete plan edits — move a workout, adjust a distance — that you approve with one click rather than a wall of text you have to interpret yourself.",[26,127,128,132,135],{},[60,129],{"alt":130,"src":131},"Dalagan Design","/projects/splitlog/weekstrip.png",[65,133,134],{},"A consistent design system replaced ad-hoc styling."," One color meant one thing across every page: a five-step intensity ramp (recovery → easy → steady → threshold → VO2) used identically on the runs table, the week strips, the plan calendar, and the coach's proposal cards. Purple — the brand color — became reserved for exactly two moments: race day and a broken record. Everywhere else, it's neutral.",[26,137,138,142,145,146,149],{},[60,139],{"alt":140,"src":141},"Dalagan Rebrand","/projects/splitlog/rebrand.png",[65,143,144],{},"Then I rebuilt the brand around what the app actually became."," \"Splitlog\" described a logging tool; the product had turned into a coaching platform. I renamed it Dalagan — Hiligaynon for ",[36,147,148],{},"to run"," — designed a new mark (a route curve arriving at a destination, echoing the idea that every training block points somewhere), and rewrote the positioning from \"personal run management\" to what the app is actually for: getting a runner from today's session to race day.",[44,151],{},[21,153,155],{"id":154},"the-technical-deep-dive","THE TECHNICAL DEEP DIVE",[26,157,158],{},"The redesign wasn't just visual — a lot of it forced real architectural decisions. Onboarding is the clearest example of how a \"just add a setup flow\" task turned into a structural fix.",[26,160,161],{},[60,162],{"alt":163,"src":164},"Dalagan Onboarding","/projects/splitlog/dalagan_onboarding_middleware_gate.png",[166,167,169],"h3",{"id":168},"_1-onboarding-had-to-move-out-of-the-app-entirely","1. Onboarding had to move out of the app entirely",[26,171,172,173,176,177,180],{},"The original onboarding page lived inside the main app's route group, which meant the sidebar and topbar rendered ",[36,174,175],{},"around"," the setup wizard — a runner who hadn't configured anything yet could navigate straight into an unconfigured app mid-flow. I moved it to its own route group with a bare layout (no nav, just the wizard) and made a Next.js middleware the single gate: any protected route, for a logged-in user who hasn't finished onboarding, redirects to ",[70,178,179],{},"/onboarding"," before the app shell ever renders. One redirect mechanism, checked on every protected request — not scattered across individual page components.",[182,183,188],"pre",{"className":184,"code":185,"language":186,"meta":187,"style":187},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","// middleware.ts — the only onboarding/auth redirect in the app\nconst isProtectedRoute =\n  pathname.startsWith(\"/dashboard\") || pathname.startsWith(\"/plans\") ||\n  pathname.startsWith(\"/coach\")     || pathname.startsWith(\"/races\")  ||\n  pathname.startsWith(\"/records\")   || pathname.startsWith(\"/gear\")   || /* … */;\n\n// logged in + protected route + !dismissed_onboarding → /onboarding\n","ts","",[70,189,190,199,214,266,308,355,362],{"__ignoreMap":187},[191,192,195],"span",{"class":193,"line":194},"line",1,[191,196,198],{"class":197},"sHwdD","// middleware.ts — the only onboarding/auth redirect in the app\n",[191,200,202,206,210],{"class":193,"line":201},2,[191,203,205],{"class":204},"spNyl","const",[191,207,209],{"class":208},"sTEyZ"," isProtectedRoute ",[191,211,213],{"class":212},"sMK4o","=\n",[191,215,217,220,223,227,230,233,237,239,242,245,248,250,252,254,256,259,261,263],{"class":193,"line":216},3,[191,218,219],{"class":208},"  pathname",[191,221,222],{"class":212},".",[191,224,226],{"class":225},"s2Zo4","startsWith",[191,228,229],{"class":208},"(",[191,231,232],{"class":212},"\"",[191,234,236],{"class":235},"sfazB","/dashboard",[191,238,232],{"class":212},[191,240,241],{"class":208},") ",[191,243,244],{"class":212},"||",[191,246,247],{"class":208}," pathname",[191,249,222],{"class":212},[191,251,226],{"class":225},[191,253,229],{"class":208},[191,255,232],{"class":212},[191,257,258],{"class":235},"/plans",[191,260,232],{"class":212},[191,262,241],{"class":208},[191,264,265],{"class":212},"||\n",[191,267,269,271,273,275,277,279,282,284,287,289,291,293,295,297,299,301,303,306],{"class":193,"line":268},4,[191,270,219],{"class":208},[191,272,222],{"class":212},[191,274,226],{"class":225},[191,276,229],{"class":208},[191,278,232],{"class":212},[191,280,281],{"class":235},"/coach",[191,283,232],{"class":212},[191,285,286],{"class":208},")     ",[191,288,244],{"class":212},[191,290,247],{"class":208},[191,292,222],{"class":212},[191,294,226],{"class":225},[191,296,229],{"class":208},[191,298,232],{"class":212},[191,300,86],{"class":235},[191,302,232],{"class":212},[191,304,305],{"class":208},")  ",[191,307,265],{"class":212},[191,309,311,313,315,317,319,321,323,325,328,330,332,334,336,338,340,343,345,347,349,352],{"class":193,"line":310},5,[191,312,219],{"class":208},[191,314,222],{"class":212},[191,316,226],{"class":225},[191,318,229],{"class":208},[191,320,232],{"class":212},[191,322,104],{"class":235},[191,324,232],{"class":212},[191,326,327],{"class":208},")   ",[191,329,244],{"class":212},[191,331,247],{"class":208},[191,333,222],{"class":212},[191,335,226],{"class":225},[191,337,229],{"class":208},[191,339,232],{"class":212},[191,341,342],{"class":235},"/gear",[191,344,232],{"class":212},[191,346,327],{"class":208},[191,348,244],{"class":212},[191,350,351],{"class":197}," /* … */",[191,353,354],{"class":212},";\n",[191,356,358],{"class":193,"line":357},6,[191,359,361],{"emptyLinePlaceholder":360},true,"\n",[191,363,365],{"class":193,"line":364},7,[191,366,367],{"class":197},"// logged in + protected route + !dismissed_onboarding → /onboarding\n",[26,369,370,371,374],{},"One route needed a carve-out even though its prefix is technically protected: the public PR share-card image has to render with no session at all, since it's what a ",[36,372,373],{},"stranger"," sees when someone shares a link. That edge case — \"protected route with one public sub-route\" — is the kind of thing you only find by actually building the gate, not by designing it on paper.",[166,376,378],{"id":377},"_2-the-rebuild-replaced-content-but-the-real-work-was-extracting-shared-components","2. The rebuild replaced content, but the real work was extracting shared components",[26,380,381,382,385,386,389],{},"Three fields the new onboarding needed — experience level, run-day selection, race distance — already existed elsewhere in the app, but as ",[36,383,384],{},"inline, non-reusable code",": a native ",[70,387,388],{},"\u003Cselect>"," buried in the Settings page, chip JSX duplicated in the same file, a ~45-line distance picker hardcoded inside the race form. None of it was a component you could just import.",[26,391,392,393,396,397,396,400,403],{},"Rather than write a second copy for onboarding, I pulled all three out into real shared components (",[70,394,395],{},"ExperienceLevelField",", ",[70,398,399],{},"RunDaysField",[70,401,402],{},"RaceDistancePicker",") and rewired the original pages onto them. One visible side effect: Settings' experience-level control changed from a dropdown to chips, purely as a consequence of making the field reusable — a small UI change that came from an architecture decision, not the other way around.",[166,405,407],{"id":406},"_3-a-real-bug-found-by-dogfooding-a-generated-plan","3. A real bug, found by dogfooding a generated plan",[26,409,410],{},"Testing a plan built through onboarding, I noticed Tempo and Interval sessions occasionally landed on days I hadn't marked as run days. The Long Run was pinned to an exact calendar date in the generation prompt — safe — but the rest of the week was only described to Claude as repeating prompt text, with nothing validating the output against the runner's actual selected days.",[26,412,413,414,417],{},"The fix, ",[70,415,416],{},"enforceRunDayPlacement()",", runs after every plan generation (shared by onboarding, the Races \"build a plan\" flow, and the Plans page's AI generator) and swaps content between any workout that landed on an unselected day and whichever selected day was sitting empty as rest — nothing invented, just relocated to where the runner actually said they'd run. It's a good example of why I always tested generated output against real constraints instead of trusting that a well-written prompt was sufficient on its own.",[166,419,421],{"id":420},"_4-reusing-generation-logic-instead-of-forking-it-for-a-no-race-path","4. Reusing generation logic instead of forking it for a \"no race\" path",[26,423,424],{},"Onboarding offers three goals: a specific race, a distance goal, or \"just staying consistent.\" The plan-generation endpoint only understands race-shaped requests — a distance, a start date, and an end date. Rather than add a second code path for the consistency case, I pass it a distance label the pace calculator doesn't recognize, which makes the same endpoint fall into its existing effort-based branch instead of a paced one — exactly the right output for base training, with zero new server-side logic.",[44,426],{},[21,428,430],{"id":429},"what-i-learned","WHAT I LEARNED",[26,432,433,436,437,440],{},[65,434,435],{},"Redesigning is a different skill from building."," Splitlog v1 taught me how to wire up an OAuth flow and a webhook pipeline. This rebuild taught me to ask what a page is ",[36,438,439],{},"for"," before touching its layout — and to notice when two pages were quietly doing the same job (a training-load number on both the dashboard and a plan page, computed two different ways) before that inconsistency became a bug report.",[26,442,443,446],{},[65,444,445],{},"Reuse is a design decision, not just a refactor."," Every shared component in this rebuild — the distance picker, the day-of-week chips, the plan-generation endpoint itself — started as \"onboarding needs this too\" and became \"so now there's one version instead of two.\" That discipline is what keeps a fast-moving redesign from quietly forking into inconsistent copies of the same feature.",[26,448,449,452],{},[65,450,451],{},"A good product decision usually shows up as an architecture decision."," Deciding the dashboard's job was \"what do I do right now\" meant precomputing insights on sync instead of generating them on every page load — a performance and cost decision that fell directly out of a UX decision.",[26,454,455,458],{},[65,456,457],{},"Write down what you didn't finish."," Every feature in this rebuild has a documented list of known gaps — onboarding has no draft persistence, the file-upload step is a visual stub waiting on a real importer, custom race distances lose their computed training paces. Writing those down explicitly, instead of letting them hide as undocumented behavior, is what made this codebase something I could actually hand off or pick back up months later.",[44,460],{},[21,462,464],{"id":463},"the-results","THE RESULTS",[26,466,467,470],{},[65,468,469],{},"Before this rebuild:"," a run tracker with a login screen — one dashboard page trying to be an archive, a stat summary, and a coaching surface at once, and an AI feature that couldn't see your training.",[26,472,473,476],{},[65,474,475],{},"After:"," eight purpose-built pages, each answering one question — Dashboard (what now), Runs (what happened), Races (what am I working toward), Plans (what's the block), Records (what have I earned), Gear (what am I running in), Coach Nash (what should change) — plus a rebrand that matches what the product actually does.",[166,478,480],{"id":479},"product-design","Product & design",[482,483,484,488,491,494],"ul",{},[485,486,487],"li",{},"Redesigned every core page from first principles, with a consistent color and typography system applied across all of it",[485,489,490],{},"Designed and shipped a full rebrand — name, logo, wordmark, color palette, tone of voice",[485,492,493],{},"Rebuilt the AI coach around real training context instead of a stateless chat box",[485,495,496],{},"Rebuilt onboarding as a focused, gated pre-app flow instead of a tour rendered inside the live product",[166,498,500],{"id":499},"engineering","Engineering",[482,502,503,506,509,512],{},[485,504,505],{},"Extracted three previously-inline UI patterns into genuinely shared components, with the original pages refactored onto them rather than forked",[485,507,508],{},"Built a single middleware-based gate governing every protected route and the one onboarding flow",[485,510,511],{},"Found and fixed a real plan-generation bug through dogfoating a generated plan against actual constraints",[485,513,514],{},"Kept the data model honest — new features reused existing tables and endpoints wherever the shape already fit, instead of growing a new one for every page",[182,516,520],{"className":517,"code":518,"language":519,"meta":187,"style":187},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","//RaceFormSheet Component\n\u003CRaceDistancePicker seedDistanceM={seedDistanceM} seedKey={formKey}\nonChange={setDistanceM}/>\n\n//StepGoal Component\n\u003CRaceDistancePicker onChange={(m) => onChange({ distanceM: m })} />\n\n","js",[70,521,522,527,558,571,575,580],{"__ignoreMap":187},[191,523,524],{"class":193,"line":194},[191,525,526],{"class":197},"//RaceFormSheet Component\n",[191,528,529,532,535,538,541,544,547,550,552,555],{"class":193,"line":201},[191,530,531],{"class":212},"\u003C",[191,533,402],{"class":534},"sBMFI",[191,536,537],{"class":204}," seedDistanceM",[191,539,540],{"class":212},"={",[191,542,543],{"class":208},"seedDistanceM",[191,545,546],{"class":212},"} ",[191,548,549],{"class":204},"seedKey",[191,551,540],{"class":212},[191,553,554],{"class":208},"formKey",[191,556,557],{"class":212},"}\n",[191,559,560,563,565,568],{"class":193,"line":216},[191,561,562],{"class":204},"onChange",[191,564,540],{"class":212},[191,566,567],{"class":208},"setDistanceM",[191,569,570],{"class":212},"}/>\n",[191,572,573],{"class":193,"line":268},[191,574,361],{"emptyLinePlaceholder":360},[191,576,577],{"class":193,"line":310},[191,578,579],{"class":197},"//StepGoal Component\n",[191,581,582,584,586,589,592,596,599,602,604,606,609,613,616,619,622,624],{"class":193,"line":357},[191,583,531],{"class":212},[191,585,402],{"class":534},[191,587,588],{"class":204}," onChange",[191,590,591],{"class":212},"={(",[191,593,595],{"class":594},"sHdIc","m",[191,597,598],{"class":212},")",[191,600,601],{"class":204}," =>",[191,603,588],{"class":225},[191,605,229],{"class":208},[191,607,608],{"class":212},"{",[191,610,612],{"class":611},"swJcz"," distanceM",[191,614,615],{"class":212},":",[191,617,618],{"class":208}," m ",[191,620,621],{"class":212},"}",[191,623,598],{"class":208},[191,625,626],{"class":212},"} />\n",[44,628],{},[21,630,632],{"id":631},"tech-stack","TECH STACK",[26,634,635,638,639,642,643,646,647,650,651,654,655,658,659,662,663,666],{},[65,636,637],{},"Frontend:"," Next.js (App Router), TypeScript\n",[65,640,641],{},"Styling:"," Tailwind CSS, a custom design-token system (color, spacing, typography)\n",[65,644,645],{},"Data Fetching:"," TanStack Query\n",[65,648,649],{},"Database:"," Supabase (PostgreSQL, Row Level Security)\n",[65,652,653],{},"Auth & Gating:"," Supabase Auth + Next.js middleware\n",[65,656,657],{},"AI:"," Anthropic Claude API — training plan generation and Coach Nash\n",[65,660,661],{},"Charts:"," Recharts\n",[65,664,665],{},"Deployment:"," Vercel",[44,668],{},[21,670,672],{"id":671},"whats-next","WHAT'S NEXT",[26,674,675,678,679,682,683,686],{},[65,676,677],{},"A real intake layer."," Run data currently comes from Strava's API; the plan is to decouple from any single source — file upload (",[70,680,681],{},".gpx","/",[70,684,685],{},".fit",") as the universal fallback, then direct integrations with watch platforms, so the product isn't dependent on one company's API.",[26,688,689,692],{},[65,690,691],{},"A native mobile app with its own run recorder."," The web app becomes the \"review and manage\" surface; a mobile app — the primary platform going forward — adds in-app GPS recording, so a runner never has to leave Dalagan to log a workout.",[26,694,695,698],{},[65,696,697],{},"Coach Nash, fully wired."," The context-aware version described above is built; extending it to propose full week rebuilds (not just single-workout moves) is the next step, once there's real usage data to validate the smaller edits first.",[44,700],{},[21,702,704],{"id":703},"the-bigger-picture","THE BIGGER PICTURE",[26,706,707],{},"Splitlog solved a real annoyance — I stopped retyping my own run data. But it was still a project built the way I knew how to build at the time: get the integration working, wire up a table, ship it.",[26,709,710],{},"This rebuild was different. Almost nothing about the underlying stack changed — same database, same auth, same core sync logic. What changed was that I stopped treating design as decoration on top of working code, and started treating it as the thing that decides what the code should even do. A page's layout, a color's meaning, an onboarding flow's placement in the route tree — all of it turned out to be product decisions with real technical consequences, not cosmetic ones.",[26,712,713],{},"That's the actual lesson from this version: building the feature is the easy half. Deciding what the feature is for is the part that took the redesign to learn.",[715,716,717],"style",{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"title":187,"searchDepth":201,"depth":201,"links":719},[720,721,722,728,729,733,734,735],{"id":23,"depth":201,"text":24},{"id":48,"depth":201,"text":49},{"id":154,"depth":201,"text":155,"children":723},[724,725,726,727],{"id":168,"depth":216,"text":169},{"id":377,"depth":216,"text":378},{"id":406,"depth":216,"text":407},{"id":420,"depth":216,"text":421},{"id":429,"depth":201,"text":430},{"id":463,"depth":201,"text":464,"children":730},[731,732],{"id":479,"depth":216,"text":480},{"id":499,"depth":216,"text":500},{"id":631,"depth":201,"text":632},{"id":671,"depth":201,"text":672},{"id":703,"depth":201,"text":704},"View Case Study","2026","I built Splitlog to stop copy-pasting my Strava stats into Notion. Then I sat down to actually design it — every page, the information architecture, the brand — and realized the app I'd built and the app runners actually needed were two different things. This is the story of that rebuild.","md","/projects/dalagan.png",{},"/projects/dalagan",{"title":16,"description":738},"projects/dalagan",[746,747,748,749,750,751,752],"Next.js","TypeScript","Supabase","Claude API","Product Design","Tailwind CSS","Recharts","1a8DjkZKkjj-aZAyMTA72vaXAQ8uMM5N0TIfpnhTJ9A",[755,760],{"title":756,"path":757,"stem":758,"description":759,"children":-1},"Enterprise CRM Platform for Advertising Sales","/projects/crm-platform","projects/crm-platform","Mission-critical platform serving 15+ sales team members. Built complex dashboards, filtering systems, and PDF generation. My biggest technical challenge: delivering enterprise features under tight deadlines.",{"title":761,"path":762,"stem":763,"description":764,"children":-1},"Dynamic Content Page Builder","/projects/dynamic-page-builder","projects/dynamic-page-builder","15+ reusable components giving writers creative freedom. Reduced content creation time by 50% and eliminated developer dependencies. Built with Vue 3 & Nuxt 3 component architecture and Pinia state management.",1785640273849]