[{"data":1,"prerenderedAt":5692},["ShallowReactive",2],{"navigation":3,"projects-page":14,"projects":29},[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,"description":18,"extension":19,"links":20,"meta":23,"navigation":24,"path":25,"seo":26,"stem":27,"__hash__":28},"pages/projects.yml","Projects & Case Studies",null,"A collection of projects I've built as a junior software engineer, showcasing my experience with frontend development, modern frameworks, and real-world problem-solving.","yml",[21],{"label":22},"Email me",{},true,"/projects",{"title":16,"description":18},"projects","qDP3bNr4WzfA00EkhbUkSdT_zbKXo9kgV5P5NwkOKqs",[30,641,2582,4523,5275,5571],{"id":31,"title":32,"body":33,"buttonLabel":17,"date":623,"description":624,"extension":625,"image":626,"meta":627,"navigation":24,"path":628,"seo":629,"stem":630,"tags":631,"__hash__":640},"projects/projects/old-web-diary.md","Oldweb Diary",{"type":34,"value":35,"toc":606},"minimark",[36,41,45,51,54,58,61,66,69,74,77,82,85,90,93,98,101,103,107,110,116,122,128,148,161,163,167,172,224,228,233,264,281,286,301,325,330,341,358,363,374,396,398,402,405,443,450,452,456,517,519,523,528,538,543,558,563,566,571,574,576,580,587,590,592,596],[37,38,40],"h2",{"id":39},"the-idea","THE IDEA",[42,43,44],"p",{},"The early internet was weird, wonderful, and almost entirely undocumented in one place. Rickroll origin stories, Flash animations nobody saved, GeoCities pages that vanished overnight, and the existential dread of the MySpace Top 8.",[42,46,47,50],{},[48,49,32],"strong",{}," is a digital museum dedicated to preserving that era — built as a full-stack web app styled to look and feel like the operating systems people were actually using at the time.",[52,53],"hr",{},[37,55,57],{"id":56},"the-product","THE PRODUCT",[42,59,60],{},"A fully functional nostalgia platform with five main sections:",[42,62,63],{},[48,64,65],{},"1. Meme Museum",[42,67,68],{},"50+ memes with full origin stories, era classification, tags, like counts, view counts, and a related memes section. Each meme has its own detail page with dynamic OG images for social sharing.",[42,70,71],{},[48,72,73],{},"2. This Day in Internet History",[42,75,76],{},"A date-matched events engine that shows what happened on today's date across internet history. Falls back to featured events when no match exists for the current date.",[42,78,79],{},[48,80,81],{},"3. Lost Media Vault",[42,83,84],{},"An archive of Flash animations, viral videos, old websites, and browser games that nearly disappeared. Each item is tagged as Available, Partially Lost, or Fully Lost, with related media suggestions for internal discovery.",[42,86,87],{},[48,88,89],{},"4. Memory Wall",[42,91,92],{},"A community submission system where users share their earliest internet memories. Includes a moderation queue, platform tags, like functionality, and pagination.",[42,94,95],{},[48,96,97],{},"5. Retro Assets Library",[42,99,100],{},"A downloadable collection of Win95 sounds, animated cursors, wallpapers, icons, and bitmap fonts from the era.",[52,102],{},[37,104,106],{"id":105},"the-retro-os-theme-system","THE RETRO OS THEME SYSTEM",[42,108,109],{},"This was the defining design challenge of the project. The entire UI is skinned to look like one of three operating systems — and users can switch between them at any time.",[42,111,112,115],{},[48,113,114],{},"Win95:"," Teal desktop, grey windows, navy titlebars, inset/raised border shadows, MS Sans Serif typography.",[42,117,118,121],{},[48,119,120],{},"WinXP:"," Bliss-inspired CSS gradient wallpaper, warm off-white windows, blue gradient titlebars, colored window buttons (yellow/green/red), rounded Start button.",[42,123,124,127],{},[48,125,126],{},"Mac OS9:"," Platinum windows, gradient titlebars, left-side close button with centered title, SVG minimize/maximize icons, Chicago font, Apple Menu in system tray.",[42,129,130,131,134,135,139,140,143,144,147],{},"The skin system uses ",[48,132,133],{},"CSS custom properties"," (",[136,137,138],"code",{},"--skin-*"," variables) so every component automatically adapts to any theme without conditional rendering. Switching themes updates a single ",[136,141,142],{},"data-skin"," attribute on the ",[136,145,146],{},"\u003Chtml>"," element.",[42,149,150,153,154,157,158,160],{},[48,151,152],{},"Zero theme flash on page load"," — achieved by reading the user's saved theme from an HTTP cookie server-side in ",[136,155,156],{},"layout.tsx"," and rendering the correct ",[136,159,142],{}," in the initial HTML response. No JavaScript needed for the first paint.",[52,162],{},[37,164,166],{"id":165},"the-technical-deep-dive","THE TECHNICAL DEEP DIVE",[168,169,171],"h3",{"id":170},"stack","Stack",[173,174,175,182,188,194,200,206,212,218],"ul",{},[176,177,178,181],"li",{},[48,179,180],{},"Framework:"," Next.js 16 (App Router, Turbopack)",[176,183,184,187],{},[48,185,186],{},"Language:"," TypeScript throughout",[176,189,190,193],{},[48,191,192],{},"API layer:"," tRPC v11 with type-safe client/server",[176,195,196,199],{},[48,197,198],{},"ORM:"," Prisma v5 with PostgreSQL (Supabase)",[176,201,202,205],{},[48,203,204],{},"Caching / counters:"," Upstash Redis",[176,207,208,211],{},[48,209,210],{},"Media storage:"," Cloudflare R2",[176,213,214,217],{},[48,215,216],{},"Deployment:"," Vercel (Singapore region)",[176,219,220,223],{},[48,221,222],{},"Monitoring:"," Sentry + PostHog",[168,225,227],{"id":226},"hardest-technical-problems","Hardest Technical Problems",[42,229,230],{},[48,231,232],{},"Problem 1: OG Image Generation in Edge Runtime",[42,234,235,236,239,240,243,244,247,248,251,252,255,256,259,260,263],{},"Each meme detail page generates a dynamic social share card via ",[136,237,238],{},"@vercel/og",". The challenge: the edge runtime doesn't have Node.js APIs (",[136,241,242],{},"Buffer",", ",[136,245,246],{},"Uint8Array"," workarounds required), ",[136,249,250],{},".webp"," images fail silently, ",[136,253,254],{},"ImageResponse"," crashes on JSX ",[136,257,258],{},".map()"," calls, and ",[136,261,262],{},"boxShadow"," is unsupported.",[42,265,266,269,270,273,274,277,278,280],{},[48,267,268],{},"Solution:"," Split into two routes — an edge runtime OG renderer that fetches image data from a separate Node.js route that handles Prisma. Used ",[136,271,272],{},"Array.from(bytes).map().join('')"," + ",[136,275,276],{},"btoa()"," for edge-safe base64 encoding. Skipped ",[136,279,250],{}," files entirely (showed title placeholder instead).",[42,282,283],{},[48,284,285],{},"Problem 2: Theme Flash Elimination",[42,287,288,289,292,293,296,297,300],{},"Initial approach — localStorage + ",[136,290,291],{},"useEffect"," — caused a visible flash of Win95 theme before the correct theme loaded. Several intermediate approaches failed (inline ",[136,294,295],{},"\u003Chead>"," script, ",[136,298,299],{},"useState"," lazy initializer) because React's hydration mismatch warnings persisted and the flash remained.",[42,302,303,306,307,310,311,313,314,316,317,320,321,324],{},[48,304,305],{},"Final solution:"," Cookie-based SSR. The server reads the ",[136,308,309],{},"oldweb-skin"," cookie in ",[136,312,156],{}," before rendering, sets ",[136,315,142],{}," in the HTML response, and passes ",[136,318,319],{},"initialSkin"," as a prop to ",[136,322,323],{},"SkinProvider",". The browser receives the correct theme in the first byte of HTML — JavaScript is never involved in the initial render.",[42,326,327],{},[48,328,329],{},"Problem 3: Prisma Sort by Month/Day",[42,331,332,333,336,337,340],{},"The admin events panel needed events sorted featured-first, then by calendar day (January before December) — not by year. Prisma's ",[136,334,335],{},"orderBy"," can't express ",[136,338,339],{},"EXTRACT(MONTH FROM eventDate)",".",[42,342,343,345,346,349,350,353,354,357],{},[48,344,268],{}," Raw SQL query using ",[136,347,348],{},"$queryRaw"," with ",[136,351,352],{},"EXTRACT(MONTH)"," and ",[136,355,356],{},"EXTRACT(DAY)"," ordering, properly typed with Prisma's raw query type system.",[42,359,360],{},[48,361,362],{},"Problem 4: ESLint Rule Compliance Without Breaking Functionality",[42,364,365,366,369,370,373],{},"Nine ESLint errors across the codebase — all ",[136,367,368],{},"setState in useEffect"," patterns in admin form components, and a ",[136,371,372],{},"window.location.hash"," mutation error in the tab component.",[42,375,376,378,379,381,382,385,386,388,389,392,393,395],{},[48,377,268],{}," Moved all state initialization to ",[136,380,299],{}," lazy initializers and event handler inline logic. Replaced the hash mutation in ",[136,383,384],{},"AdminTabs"," with a ",[136,387,291],{}," that watches ",[136,390,391],{},"active"," state and writes to ",[136,394,372],{}," from within an effect — satisfying the immutability rule while preserving tab persistence behavior.",[52,397],{},[37,399,401],{"id":400},"the-admin-cms","THE ADMIN CMS",[42,403,404],{},"A password-protected 6-tab admin panel at a secret URL, built entirely without a third-party CMS:",[173,406,407,413,419,425,431,437],{},[176,408,409,412],{},[48,410,411],{},"Memes"," — create, edit, delete, feature/unfeature with auto-slug generation",[176,414,415,418],{},[48,416,417],{},"Events"," — full CRUD, sorted by featured-first then month/day ascending",[176,420,421,424],{},[48,422,423],{},"Memories"," — approve or reject pending community submissions",[176,426,427,430],{},[48,428,429],{},"Lost Media"," — create and manage vault entries with status classification",[176,432,433,436],{},[48,434,435],{},"Retro Assets"," — manage downloadable asset library",[176,438,439,442],{},[48,440,441],{},"Site News"," — post and hide announcements shown on the homepage",[42,444,445,446,449],{},"Authentication uses ",[48,447,448],{},"iron-session"," with HTTP-only cookies and a secret admin URL. The session cookie is set on login and cleared on logout — a subtle bug where the logout route was creating the session on a different response object than was returned (causing the cleared cookie to be discarded) was the trickiest auth fix in the project.",[52,451],{},[37,453,455],{"id":454},"seo-setup","SEO SETUP",[173,457,458,465,471,477,483,486,493,503,510],{},[176,459,460,461,464],{},"Dynamic ",[136,462,463],{},"generateMetadata"," per meme and lost-media page (title, description, OpenGraph, Twitter card)",[176,466,467,468],{},"Dynamic OG images per meme via ",[136,469,470],{},"/api/og-meme/[slug]",[176,472,473,476],{},[136,474,475],{},"sitemap.xml"," auto-generated from all meme + lost-media slugs via Prisma",[176,478,479,482],{},[136,480,481],{},"robots.txt"," blocking admin path and API routes, pointing to sitemap",[176,484,485],{},"Canonical URLs on all dynamic pages",[176,487,488,489,492],{},"JSON-LD ",[136,490,491],{},"Article"," schema on meme and lost-media detail pages",[176,494,488,495,498,499,502],{},[136,496,497],{},"WebSite"," schema with ",[136,500,501],{},"SearchAction"," on homepage",[176,504,505,506,509],{},"Descriptive alt text via a shared ",[136,507,508],{},"altText.ts"," utility (e.g. \"Rickroll — viral internet meme from 2007, mid-2000s\")",[176,511,512,513,516],{},"PageSpeed Insights: ",[48,514,515],{},"100 SEO, 100 Best Practices"," on both mobile and desktop",[52,518],{},[37,520,522],{"id":521},"what-this-taught-me","WHAT THIS TAUGHT ME",[42,524,525],{},[48,526,527],{},"1. CSS custom properties are the right tool for theming at scale",[42,529,530,531,534,535,537],{},"Three completely different OS aesthetics from a single component library — no conditional rendering, no theme context drilling. Just ",[136,532,533],{},"var(--skin-*)"," variables and a ",[136,536,142],{}," attribute on the root element. Every new component automatically inherits the correct theme.",[42,539,540],{},[48,541,542],{},"2. Edge runtime has sharp edges",[42,544,545,547,548,550,551,550,553,550,555,557],{},[136,546,238],{}," is powerful but has real constraints that only surface in production: no ",[136,549,242],{},", no ",[136,552,250],{},[136,554,262],{},[136,556,258],{}," in JSX. Developing and testing in localhost masked these completely. The lesson: for edge functions, test in a Vercel preview deployment early.",[42,559,560],{},[48,561,562],{},"3. Cookie-based SSR is the correct solution for theme persistence",[42,564,565],{},"localStorage-based theming always causes a flash. The right mental model is: if you need something to affect the server-rendered HTML, it has to come from the server — and the only client data the server can see is cookies.",[42,567,568],{},[48,569,570],{},"4. Full-stack solo projects reveal the gaps you didn't know you had",[42,572,573],{},"Building the admin CMS, the authentication system, the Redis hit counter, the OG image pipeline, the SEO layer, and the deployment config — all on the same project — forces you to develop opinions about every layer of the stack. That breadth is worth more than going deep on any single piece.",[52,575],{},[37,577,579],{"id":578},"the-result","THE RESULT",[42,581,582,583,586],{},"A live, production site at ",[48,584,585],{},"oldwebdiary.com"," — fully deployed on Vercel with a custom domain, Supabase PostgreSQL database, Cloudflare R2 media storage, and Upstash Redis hit counter.",[42,588,589],{},"50+ memes documented. 50+ internet history events. 13 lost media items. 20 retro assets. A community memory wall. Three fully functional OS themes. Zero compromises on the nostalgia.",[52,591],{},[37,593,595],{"id":594},"visit-website","VISIT WEBSITE",[42,597,598],{},[48,599,600],{},[601,602,585],"a",{"href":603,"rel":604},"https://oldwebdiary.com/",[605],"nofollow",{"title":607,"searchDepth":608,"depth":608,"links":609},"",2,[610,611,612,613,618,619,620,621,622],{"id":39,"depth":608,"text":40},{"id":56,"depth":608,"text":57},{"id":105,"depth":608,"text":106},{"id":165,"depth":608,"text":166,"children":614},[615,617],{"id":170,"depth":616,"text":171},3,{"id":226,"depth":616,"text":227},{"id":400,"depth":608,"text":401},{"id":454,"depth":608,"text":455},{"id":521,"depth":608,"text":522},{"id":578,"depth":608,"text":579},{"id":594,"depth":608,"text":595},"2026","A digital museum of early internet culture. Built a full-stack nostalgia platform with three retro OS themes, 50+ meme origin stories, This Day in Internet History, a Lost Media Vault, and a community Memory Wall.","md","/projects/oldweb-diary.png",{},"/projects/old-web-diary",{"title":32,"description":624},"projects/old-web-diary",[632,633,634,635,636,637,638,639],"Next.js","TypeScript","tRPC","Prisma","Supabase","Tailwind CSS","Cloudflare R2","Vercel","fY7yeKWKaBg6228tzJuRxOb9FIVMbT86bRLW5sqclQw",{"id":642,"title":643,"body":644,"buttonLabel":2570,"date":623,"description":2571,"extension":625,"image":2572,"meta":2573,"navigation":24,"path":2574,"seo":2575,"stem":2576,"tags":2577,"__hash__":2581},"projects/projects/splitlog.md","Splitlog — Personal Run Management Platform",{"type":34,"value":645,"toc":2541},[646,650,653,671,674,677,680,685,687,691,694,728,733,736,738,742,745,748,751,756,759,762,764,766,773,779,785,791,797,799,803,807,812,815,818,823,826,846,851,854,871,874,879,884,887,913,918,921,929,932,937,940,965,968,970,972,976,979,984,1009,1012,1018,1123,1126,1130,1133,1136,1141,1445,1448,1453,1456,1459,1586,1590,1611,1614,1617,1621,1627,1856,1862,1873,2139,2141,2145,2149,2152,2155,2185,2190,2193,2197,2200,2203,2221,2224,2228,2231,2234,2245,2248,2252,2255,2258,2272,2277,2279,2283,2287,2293,2299,2303,2329,2333,2336,2368,2370,2374,2433,2435,2439,2442,2447,2450,2461,2464,2467,2475,2483,2488,2491,2494,2496,2500,2503,2506,2526,2531,2534,2537],[37,647,649],{"id":648},"the-backstory","THE BACKSTORY",[42,651,652],{},"Every time I finished a run, I had a ritual:",[654,655,656,659,662,665,668],"ol",{},[176,657,658],{},"Open Strava on my phone",[176,660,661],{},"Read off the distance, time, pace, heart rate",[176,663,664],{},"Open Notion",[176,666,667],{},"Find my training plan database",[176,669,670],{},"Manually type every stat into the right row",[42,672,673],{},"Every. Single. Run.",[42,675,676],{},"It was tedious. It was error-prone. And honestly, after a hard\nworkout — the last thing I wanted to do was data entry.",[42,678,679],{},"So I did what any developer would do.",[42,681,682],{},[48,683,684],{},"I built something to fix it.",[52,686],{},[37,688,690],{"id":689},"the-challenge","THE CHALLENGE",[42,692,693],{},"This wasn't just a personal productivity problem.\nIt was also my biggest technical challenge to date:",[173,695,696,703,709,715,721],{},[176,697,698,699,702],{},"🆕 ",[48,700,701],{},"First time with Next.js"," — I had 2 years of Vue/Nuxt\nprofessionally, but React and Next.js were new territory",[176,704,698,705,708],{},[48,706,707],{},"First time with Supabase"," — never built auth or a\ndatabase layer from scratch before",[176,710,698,711,714],{},[48,712,713],{},"First time with real API OAuth"," — Strava's OAuth 2.0\nflow, webhooks, token refresh",[176,716,698,717,720],{},[48,718,719],{},"First full-stack project"," — frontend, backend API routes,\ndatabase, deployment. All me.",[176,722,723,724,727],{},"🤖 ",[48,725,726],{},"First time thinking about AI integration"," — planning\nan AI coaching feature mid-build",[42,729,730],{},[48,731,732],{},"Almost every technology in this project was new to me.",[42,734,735],{},"I used AI tools (Claude) heavily to accelerate learning — but like\nmy previous projects, I made sure to understand every line.\nI'd ask \"why does this pattern work?\" before moving on.",[52,737],{},[37,739,741],{"id":740},"the-aha-moment","THE AHA MOMENT",[42,743,744],{},"I want to be honest about this.",[42,746,747],{},"There were a lot of \"first times\" in this project.\nFirst time setting up Supabase RLS policies.\nFirst time handling OAuth token refresh.\nFirst time building a webhook endpoint.",[42,749,750],{},"But the moment that made everything worth it?",[42,752,753],{},[48,754,755],{},"I finished a run. Opened my app. And it was already there.",[42,757,758],{},"Distance, pace, heart rate, elevation — all synced automatically\nthe moment Strava recorded my activity. No copy-pasting.\nNo manual entry. Just my data, in my own platform,\nthat I built from scratch.",[42,760,761],{},"That's when I understood what it meant to build something\nthat actually solves a real problem.",[52,763],{},[37,765,57],{"id":56},[42,767,768],{},[769,770],"img",{"alt":771,"src":772},"Splitlog Dashboard — real run data synced automatically","/projects/splitlog/dashboard.png",[42,774,775],{},[769,776],{"alt":777,"src":778},"Training Plan Manager with progress tracking","/projects/splitlog/training-plan.png",[42,780,781],{},[769,782],{"alt":783,"src":784},"Assign Run modal — one click to link a Strava run to a training day","/projects/splitlog/assign-run.png",[42,786,787],{},[769,788],{"alt":789,"src":790},"Analytics — training heatmap and pace trend","/projects/splitlog/analytics.png",[42,792,793],{},[769,794],{"alt":795,"src":796},"Personal Bests — auto-calculated from Strava data","/projects/splitlog/personal-bests.png",[52,798],{},[37,800,802],{"id":801},"what-i-built","WHAT I BUILT",[168,804,806],{"id":805},"core-features","Core Features",[42,808,809],{},[48,810,811],{},"1. Strava Auto-Sync via Webhooks",[42,813,814],{},"The heart of the whole app. When you finish a run,\nStrava sends a webhook event to my server within seconds.\nMy app catches it, fetches the full activity details,\nand saves everything to the database automatically.",[42,816,817],{},"No manual sync. No polling. Just instant, real data.",[42,819,820],{},[48,821,822],{},"2. Run Dashboard with TanStack Table",[42,824,825],{},"A powerful, filterable table of all your runs —\nsomething Strava's own UI makes surprisingly difficult.",[173,827,828,831,834,837,840,843],{},[176,829,830],{},"Sort by distance, pace, date, heart rate, elevation",[176,832,833],{},"Filter by distance range (under 5km, 5–10km, half marathon, etc.)",[176,835,836],{},"Filter by time period (last 30 days, 3 months, all time)",[176,838,839],{},"Global search across all runs",[176,841,842],{},"Pagination with configurable page size",[176,844,845],{},"Stats cards showing total runs, total distance,\nlongest run, total elevation",[42,847,848],{},[48,849,850],{},"3. Training Plan Manager",[42,852,853],{},"Create training plans (like \"Training for 1st Marathon\")\nwith scheduled workout days. Each day has:",[173,855,856,859,862,865,868],{},[176,857,858],{},"Workout type (Easy Run, Tempo, Long Run, Interval, Rest)",[176,860,861],{},"Target distance",[176,863,864],{},"Scheduled date",[176,866,867],{},"Notes",[176,869,870],{},"Assigned Strava run with auto-filled stats",[42,872,873],{},"The assign flow is the feature I'm most proud of:\nclick \"Assign run\" on any training day → modal shows all\nyour Strava runs with stats → click one →\ndistance, time, pace, HR all auto-fill instantly.",[42,875,876],{},[48,877,878],{},"That replaced my entire Notion workflow.",[42,880,881],{},[48,882,883],{},"4. Analytics Page",[42,885,886],{},"Four charts built with Recharts:",[173,888,889,895,901,907],{},[176,890,891,894],{},[48,892,893],{},"Training Heatmap"," — GitHub-style calendar showing\nwhich days you ran over the past year.\nDarker = longer run. Current streak tracked.",[176,896,897,900],{},[48,898,899],{},"Pace Trend"," — line chart showing if you're\ngetting faster over time (inverted Y-axis\nbecause lower pace = faster)",[176,902,903,906],{},[48,904,905],{},"Heart Rate Zones"," — donut chart showing\ntime spent in each of 5 HR zones",[176,908,909,912],{},[48,910,911],{},"Distance Distribution"," — bar chart grouping\nruns by distance range",[42,914,915],{},[48,916,917],{},"5. Personal Bests",[42,919,920],{},"Two dark cards showing your records:",[173,922,923,926],{},[176,924,925],{},"Race PRs (5K, 10K, Half Marathon, Marathon) —\nauto-calculated from your actual Strava data\nOR manually entered from official race results",[176,927,928],{},"Activity Records (Longest Run, Most Elevation,\nHighest Max HR, Best Week)",[42,930,931],{},"Manual official PRs override auto-calculated ones\nand show an \"Official\" badge.",[42,933,934],{},[48,935,936],{},"6. User Profile",[42,938,939],{},"Settings page with tabbed navigation:",[173,941,942,948,954,960],{},[176,943,944,947],{},[48,945,946],{},"Profile"," — personal info, running profile,\nofficial race PRs, avatar picker",[176,949,950,953],{},[48,951,952],{},"Integrations"," — Strava connect + sync",[176,955,956,959],{},[48,957,958],{},"Preferences"," — coming soon",[176,961,962,959],{},[48,963,964],{},"Data",[42,966,967],{},"Avatar picker has 18 preset avatars across 3 categories\n(Runners, Animals, Vibes). No file uploads —\nclean and simple.",[52,969],{},[37,971,166],{"id":165},[168,973,975],{"id":974},"_1-strava-oauth-20-webhook-pipeline","1. Strava OAuth 2.0 + Webhook Pipeline",[42,977,978],{},"This was the most technically complex part of the whole project.",[42,980,981],{},[48,982,983],{},"The OAuth flow:",[42,985,986,987,990,991,993,994,996,997,999,1000,1002,1003,1005,1006,1008],{},"You finish a run on Strava ",[988,989],"br",{},"\n→ Strava POST /api/webhooks/strava (within seconds) ",[988,992],{},"\n→ validate webhook signature ",[988,995],{},"\n→ fetch full activity from Strava API ",[988,998],{},"\n→ filter: only save runs (not rides, swims, etc.) ",[988,1001],{},"\n→ transform Strava data → our DB schema ",[988,1004],{},"\n→ upsert to Supabase activities table ",[988,1007],{},"\n→ appears in dashboard automatically",[42,1010,1011],{},"The hardest bug I fixed: the webhook handler was querying\nSupabase using the regular client — but webhooks run\nwithout an authenticated user session.\nRLS was blocking the query silently.",[42,1013,1014,1017],{},[48,1015,1016],{},"Fix:"," Created a separate service role client\nthat bypasses RLS for server-side operations:",[1019,1020,1024],"pre",{"className":1021,"code":1022,"language":1023,"meta":607,"style":607},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","// lib/supabase/service.ts\nexport function createServiceClient() {\n  return createClient(\n    process.env.NEXT_PUBLIC_SUPABASE_URL!,\n    process.env.SUPABASE_SERVICE_ROLE_KEY!, // bypasses RLS\n  );\n}\n","ts",[136,1025,1026,1035,1056,1068,1088,1108,1117],{"__ignoreMap":607},[1027,1028,1031],"span",{"class":1029,"line":1030},"line",1,[1027,1032,1034],{"class":1033},"sHwdD","// lib/supabase/service.ts\n",[1027,1036,1037,1041,1045,1049,1053],{"class":1029,"line":608},[1027,1038,1040],{"class":1039},"s7zQu","export",[1027,1042,1044],{"class":1043},"spNyl"," function",[1027,1046,1048],{"class":1047},"s2Zo4"," createServiceClient",[1027,1050,1052],{"class":1051},"sMK4o","()",[1027,1054,1055],{"class":1051}," {\n",[1027,1057,1058,1061,1064],{"class":1029,"line":616},[1027,1059,1060],{"class":1039},"  return",[1027,1062,1063],{"class":1047}," createClient",[1027,1065,1067],{"class":1066},"swJcz","(\n",[1027,1069,1071,1075,1077,1080,1082,1085],{"class":1029,"line":1070},4,[1027,1072,1074],{"class":1073},"sTEyZ","    process",[1027,1076,340],{"class":1051},[1027,1078,1079],{"class":1073},"env",[1027,1081,340],{"class":1051},[1027,1083,1084],{"class":1073},"NEXT_PUBLIC_SUPABASE_URL",[1027,1086,1087],{"class":1051},"!,\n",[1027,1089,1091,1093,1095,1097,1099,1102,1105],{"class":1029,"line":1090},5,[1027,1092,1074],{"class":1073},[1027,1094,340],{"class":1051},[1027,1096,1079],{"class":1073},[1027,1098,340],{"class":1051},[1027,1100,1101],{"class":1073},"SUPABASE_SERVICE_ROLE_KEY",[1027,1103,1104],{"class":1051},"!,",[1027,1106,1107],{"class":1033}," // bypasses RLS\n",[1027,1109,1111,1114],{"class":1029,"line":1110},6,[1027,1112,1113],{"class":1066},"  )",[1027,1115,1116],{"class":1051},";\n",[1027,1118,1120],{"class":1029,"line":1119},7,[1027,1121,1122],{"class":1051},"}\n",[42,1124,1125],{},"That one change made everything work.",[168,1127,1129],{"id":1128},"_2-tanstack-table-headless-table-with-custom-filters","2. TanStack Table — Headless Table with Custom Filters",[42,1131,1132],{},"I chose TanStack Table v8 because I needed full control\nover sorting, filtering, grouping, and pagination\nwithout fighting a pre-styled component library.",[42,1134,1135],{},"The learning curve was steep. But the power is real.",[42,1137,1138],{},[48,1139,1140],{},"Custom filter functions for non-standard comparisons:",[1019,1142,1144],{"className":1021,"code":1143,"language":1023,"meta":607,"style":607},"// Distance range filter — checks if value is within [min, max]\nconst rangeFilter: FilterFn\u003CRun> = (row, columnId, filterValue) => {\n  const val = row.getValue(columnId) as number;\n  const [min, max] = filterValue as [number, number];\n  return val >= min && val \u003C= max;\n};\n\n// Date filter — checks if run is after a cutoff date\nconst dateAfterFilter: FilterFn\u003CRun> = (row, columnId, filterValue) => {\n  const val = row.getValue(columnId) as string;\n  if (!val) return false;\n  return new Date(val) >= new Date(filterValue);\n};\n",[136,1145,1146,1151,1203,1238,1276,1300,1305,1310,1316,1354,1382,1407,1440],{"__ignoreMap":607},[1027,1147,1148],{"class":1029,"line":1030},[1027,1149,1150],{"class":1033},"// Distance range filter — checks if value is within [min, max]\n",[1027,1152,1153,1156,1159,1162,1166,1169,1172,1175,1178,1180,1184,1187,1190,1192,1195,1198,1201],{"class":1029,"line":608},[1027,1154,1155],{"class":1043},"const",[1027,1157,1158],{"class":1073}," rangeFilter",[1027,1160,1161],{"class":1051},":",[1027,1163,1165],{"class":1164},"sBMFI"," FilterFn",[1027,1167,1168],{"class":1051},"\u003C",[1027,1170,1171],{"class":1164},"Run",[1027,1173,1174],{"class":1051},">",[1027,1176,1177],{"class":1051}," =",[1027,1179,134],{"class":1051},[1027,1181,1183],{"class":1182},"sHdIc","row",[1027,1185,1186],{"class":1051},",",[1027,1188,1189],{"class":1182}," columnId",[1027,1191,1186],{"class":1051},[1027,1193,1194],{"class":1182}," filterValue",[1027,1196,1197],{"class":1051},")",[1027,1199,1200],{"class":1043}," =>",[1027,1202,1055],{"class":1051},[1027,1204,1205,1208,1211,1213,1216,1218,1221,1224,1227,1230,1233,1236],{"class":1029,"line":616},[1027,1206,1207],{"class":1043},"  const",[1027,1209,1210],{"class":1073}," val",[1027,1212,1177],{"class":1051},[1027,1214,1215],{"class":1073}," row",[1027,1217,340],{"class":1051},[1027,1219,1220],{"class":1047},"getValue",[1027,1222,1223],{"class":1066},"(",[1027,1225,1226],{"class":1073},"columnId",[1027,1228,1229],{"class":1066},") ",[1027,1231,1232],{"class":1039},"as",[1027,1234,1235],{"class":1164}," number",[1027,1237,1116],{"class":1051},[1027,1239,1240,1242,1245,1248,1250,1253,1256,1258,1260,1263,1265,1268,1270,1272,1274],{"class":1029,"line":1070},[1027,1241,1207],{"class":1043},[1027,1243,1244],{"class":1051}," [",[1027,1246,1247],{"class":1073},"min",[1027,1249,1186],{"class":1051},[1027,1251,1252],{"class":1073}," max",[1027,1254,1255],{"class":1051},"]",[1027,1257,1177],{"class":1051},[1027,1259,1194],{"class":1073},[1027,1261,1262],{"class":1039}," as",[1027,1264,1244],{"class":1066},[1027,1266,1267],{"class":1164},"number",[1027,1269,1186],{"class":1051},[1027,1271,1235],{"class":1164},[1027,1273,1255],{"class":1066},[1027,1275,1116],{"class":1051},[1027,1277,1278,1280,1282,1285,1288,1291,1293,1296,1298],{"class":1029,"line":1090},[1027,1279,1060],{"class":1039},[1027,1281,1210],{"class":1073},[1027,1283,1284],{"class":1051}," >=",[1027,1286,1287],{"class":1073}," min",[1027,1289,1290],{"class":1051}," &&",[1027,1292,1210],{"class":1073},[1027,1294,1295],{"class":1051}," \u003C=",[1027,1297,1252],{"class":1073},[1027,1299,1116],{"class":1051},[1027,1301,1302],{"class":1029,"line":1110},[1027,1303,1304],{"class":1051},"};\n",[1027,1306,1307],{"class":1029,"line":1119},[1027,1308,1309],{"emptyLinePlaceholder":24},"\n",[1027,1311,1313],{"class":1029,"line":1312},8,[1027,1314,1315],{"class":1033},"// Date filter — checks if run is after a cutoff date\n",[1027,1317,1319,1321,1324,1326,1328,1330,1332,1334,1336,1338,1340,1342,1344,1346,1348,1350,1352],{"class":1029,"line":1318},9,[1027,1320,1155],{"class":1043},[1027,1322,1323],{"class":1073}," dateAfterFilter",[1027,1325,1161],{"class":1051},[1027,1327,1165],{"class":1164},[1027,1329,1168],{"class":1051},[1027,1331,1171],{"class":1164},[1027,1333,1174],{"class":1051},[1027,1335,1177],{"class":1051},[1027,1337,134],{"class":1051},[1027,1339,1183],{"class":1182},[1027,1341,1186],{"class":1051},[1027,1343,1189],{"class":1182},[1027,1345,1186],{"class":1051},[1027,1347,1194],{"class":1182},[1027,1349,1197],{"class":1051},[1027,1351,1200],{"class":1043},[1027,1353,1055],{"class":1051},[1027,1355,1357,1359,1361,1363,1365,1367,1369,1371,1373,1375,1377,1380],{"class":1029,"line":1356},10,[1027,1358,1207],{"class":1043},[1027,1360,1210],{"class":1073},[1027,1362,1177],{"class":1051},[1027,1364,1215],{"class":1073},[1027,1366,340],{"class":1051},[1027,1368,1220],{"class":1047},[1027,1370,1223],{"class":1066},[1027,1372,1226],{"class":1073},[1027,1374,1229],{"class":1066},[1027,1376,1232],{"class":1039},[1027,1378,1379],{"class":1164}," string",[1027,1381,1116],{"class":1051},[1027,1383,1385,1388,1390,1393,1396,1398,1401,1405],{"class":1029,"line":1384},11,[1027,1386,1387],{"class":1039},"  if",[1027,1389,134],{"class":1066},[1027,1391,1392],{"class":1051},"!",[1027,1394,1395],{"class":1073},"val",[1027,1397,1229],{"class":1066},[1027,1399,1400],{"class":1039},"return",[1027,1402,1404],{"class":1403},"sfNiH"," false",[1027,1406,1116],{"class":1051},[1027,1408,1410,1412,1415,1418,1420,1422,1424,1427,1429,1431,1433,1436,1438],{"class":1029,"line":1409},12,[1027,1411,1060],{"class":1039},[1027,1413,1414],{"class":1051}," new",[1027,1416,1417],{"class":1047}," Date",[1027,1419,1223],{"class":1066},[1027,1421,1395],{"class":1073},[1027,1423,1229],{"class":1066},[1027,1425,1426],{"class":1051},">=",[1027,1428,1414],{"class":1051},[1027,1430,1417],{"class":1047},[1027,1432,1223],{"class":1066},[1027,1434,1435],{"class":1073},"filterValue",[1027,1437,1197],{"class":1066},[1027,1439,1116],{"class":1051},[1027,1441,1443],{"class":1029,"line":1442},13,[1027,1444,1304],{"class":1051},[42,1446,1447],{},"These let me build the distance range and time period\ndropdowns that filter the table instantly.",[42,1449,1450],{},[48,1451,1452],{},"The stats cards react to filters too:",[42,1454,1455],{},"When you filter to \"Last 30 days\" — the stats cards\n(Total Runs, Total Distance, etc.) update to reflect\nonly those filtered runs, not all runs.",[42,1457,1458],{},"I achieved this by passing filtered rows back up to\nthe parent via a callback:",[1019,1460,1462],{"className":1021,"code":1461,"language":1023,"meta":607,"style":607},"useEffect(() => {\n  if (onFilteredRowsChange) {\n    const filteredRows = table\n      .getFilteredRowModel()\n      .rows.map((row) => row.original);\n    onFilteredRowsChange(filteredRows);\n  }\n}, [columnFilters, globalFilter, data]);\n",[136,1463,1464,1476,1490,1503,1514,1547,1561,1566],{"__ignoreMap":607},[1027,1465,1466,1468,1470,1472,1474],{"class":1029,"line":1030},[1027,1467,291],{"class":1047},[1027,1469,1223],{"class":1073},[1027,1471,1052],{"class":1051},[1027,1473,1200],{"class":1043},[1027,1475,1055],{"class":1051},[1027,1477,1478,1480,1482,1485,1487],{"class":1029,"line":608},[1027,1479,1387],{"class":1039},[1027,1481,134],{"class":1066},[1027,1483,1484],{"class":1073},"onFilteredRowsChange",[1027,1486,1229],{"class":1066},[1027,1488,1489],{"class":1051},"{\n",[1027,1491,1492,1495,1498,1500],{"class":1029,"line":616},[1027,1493,1494],{"class":1043},"    const",[1027,1496,1497],{"class":1073}," filteredRows",[1027,1499,1177],{"class":1051},[1027,1501,1502],{"class":1073}," table\n",[1027,1504,1505,1508,1511],{"class":1029,"line":1070},[1027,1506,1507],{"class":1051},"      .",[1027,1509,1510],{"class":1047},"getFilteredRowModel",[1027,1512,1513],{"class":1066},"()\n",[1027,1515,1516,1518,1521,1523,1526,1528,1530,1532,1534,1536,1538,1540,1543,1545],{"class":1029,"line":1090},[1027,1517,1507],{"class":1051},[1027,1519,1520],{"class":1073},"rows",[1027,1522,340],{"class":1051},[1027,1524,1525],{"class":1047},"map",[1027,1527,1223],{"class":1066},[1027,1529,1223],{"class":1051},[1027,1531,1183],{"class":1182},[1027,1533,1197],{"class":1051},[1027,1535,1200],{"class":1043},[1027,1537,1215],{"class":1073},[1027,1539,340],{"class":1051},[1027,1541,1542],{"class":1073},"original",[1027,1544,1197],{"class":1066},[1027,1546,1116],{"class":1051},[1027,1548,1549,1552,1554,1557,1559],{"class":1029,"line":1110},[1027,1550,1551],{"class":1047},"    onFilteredRowsChange",[1027,1553,1223],{"class":1066},[1027,1555,1556],{"class":1073},"filteredRows",[1027,1558,1197],{"class":1066},[1027,1560,1116],{"class":1051},[1027,1562,1563],{"class":1029,"line":1119},[1027,1564,1565],{"class":1051},"  }\n",[1027,1567,1568,1571,1574,1576,1579,1581,1584],{"class":1029,"line":1312},[1027,1569,1570],{"class":1051},"},",[1027,1572,1573],{"class":1073}," [columnFilters",[1027,1575,1186],{"class":1051},[1027,1577,1578],{"class":1073}," globalFilter",[1027,1580,1186],{"class":1051},[1027,1582,1583],{"class":1073}," data])",[1027,1585,1116],{"class":1051},[168,1587,1589],{"id":1588},"_3-database-design-with-row-level-security","3. Database Design with Row Level Security",[42,1591,1592,1593,1595,1596,1598,1599,1601,1602,1604,1605,1607,1608,1610],{},"Five tables, all with Supabase RLS enabled:\n",[988,1594],{},"\nusers → extends Supabase auth ",[988,1597],{},"\ntraining_plans → user's training blocks ",[988,1600],{},"\ntraining_plan_days → scheduled workout days",[988,1603],{},"\nactivities → synced from Strava",[988,1606],{},"\nplan_activity_map → links runs to plan days",[988,1609],{},"\nuser_profiles → personal info + PRs + avatar",[42,1612,1613],{},"RLS policies mean every query is automatically\nscoped to the logged-in user at the database level —\nnot just in application code.",[42,1615,1616],{},"This makes the app SaaS-ready even though\nit's currently personal use only.",[168,1618,1620],{"id":1619},"_4-personal-bests-two-layer-system","4. Personal Bests — Two Layer System",[42,1622,1623,1626],{},[48,1624,1625],{},"Layer 1 (automatic):"," Calculates PRs from\nexisting Strava data using distance range matching:",[1019,1628,1630],{"className":1021,"code":1629,"language":1023,"meta":607,"style":607},"function findBestEffort(runs, minKm, maxKm) {\n  return runs\n    .filter(\n      (r) =>\n        r.distance_km >= minKm &&\n        r.distance_km \u003C= maxKm &&\n        r.avg_pace_sec_per_km > 0,\n    )\n    .sort((a, b) => a.avg_pace_sec_per_km - b.avg_pace_sec_per_km)[0];\n}\n\nconst best5k = findBestEffort(runs, 4.8, 5.5);\nconst best10k = findBestEffort(runs, 9.5, 11.0);\n",[136,1631,1632,1659,1666,1676,1689,1706,1720,1739,1744,1792,1796,1800,1829],{"__ignoreMap":607},[1027,1633,1634,1637,1640,1642,1645,1647,1650,1652,1655,1657],{"class":1029,"line":1030},[1027,1635,1636],{"class":1043},"function",[1027,1638,1639],{"class":1047}," findBestEffort",[1027,1641,1223],{"class":1051},[1027,1643,1644],{"class":1182},"runs",[1027,1646,1186],{"class":1051},[1027,1648,1649],{"class":1182}," minKm",[1027,1651,1186],{"class":1051},[1027,1653,1654],{"class":1182}," maxKm",[1027,1656,1197],{"class":1051},[1027,1658,1055],{"class":1051},[1027,1660,1661,1663],{"class":1029,"line":608},[1027,1662,1060],{"class":1039},[1027,1664,1665],{"class":1073}," runs\n",[1027,1667,1668,1671,1674],{"class":1029,"line":616},[1027,1669,1670],{"class":1051},"    .",[1027,1672,1673],{"class":1047},"filter",[1027,1675,1067],{"class":1066},[1027,1677,1678,1681,1684,1686],{"class":1029,"line":1070},[1027,1679,1680],{"class":1051},"      (",[1027,1682,1683],{"class":1182},"r",[1027,1685,1197],{"class":1051},[1027,1687,1688],{"class":1043}," =>\n",[1027,1690,1691,1694,1696,1699,1701,1703],{"class":1029,"line":1090},[1027,1692,1693],{"class":1073},"        r",[1027,1695,340],{"class":1051},[1027,1697,1698],{"class":1073},"distance_km",[1027,1700,1284],{"class":1051},[1027,1702,1649],{"class":1073},[1027,1704,1705],{"class":1051}," &&\n",[1027,1707,1708,1710,1712,1714,1716,1718],{"class":1029,"line":1110},[1027,1709,1693],{"class":1073},[1027,1711,340],{"class":1051},[1027,1713,1698],{"class":1073},[1027,1715,1295],{"class":1051},[1027,1717,1654],{"class":1073},[1027,1719,1705],{"class":1051},[1027,1721,1722,1724,1726,1729,1732,1736],{"class":1029,"line":1119},[1027,1723,1693],{"class":1073},[1027,1725,340],{"class":1051},[1027,1727,1728],{"class":1073},"avg_pace_sec_per_km",[1027,1730,1731],{"class":1051}," >",[1027,1733,1735],{"class":1734},"sbssI"," 0",[1027,1737,1738],{"class":1051},",\n",[1027,1740,1741],{"class":1029,"line":1312},[1027,1742,1743],{"class":1066},"    )\n",[1027,1745,1746,1748,1751,1753,1755,1757,1759,1762,1764,1766,1769,1771,1773,1776,1778,1780,1782,1785,1788,1790],{"class":1029,"line":1318},[1027,1747,1670],{"class":1051},[1027,1749,1750],{"class":1047},"sort",[1027,1752,1223],{"class":1066},[1027,1754,1223],{"class":1051},[1027,1756,601],{"class":1182},[1027,1758,1186],{"class":1051},[1027,1760,1761],{"class":1182}," b",[1027,1763,1197],{"class":1051},[1027,1765,1200],{"class":1043},[1027,1767,1768],{"class":1073}," a",[1027,1770,340],{"class":1051},[1027,1772,1728],{"class":1073},[1027,1774,1775],{"class":1051}," -",[1027,1777,1761],{"class":1073},[1027,1779,340],{"class":1051},[1027,1781,1728],{"class":1073},[1027,1783,1784],{"class":1066},")[",[1027,1786,1787],{"class":1734},"0",[1027,1789,1255],{"class":1066},[1027,1791,1116],{"class":1051},[1027,1793,1794],{"class":1029,"line":1356},[1027,1795,1122],{"class":1051},[1027,1797,1798],{"class":1029,"line":1384},[1027,1799,1309],{"emptyLinePlaceholder":24},[1027,1801,1802,1804,1807,1810,1812,1815,1817,1820,1822,1825,1827],{"class":1029,"line":1409},[1027,1803,1155],{"class":1043},[1027,1805,1806],{"class":1073}," best5k ",[1027,1808,1809],{"class":1051},"=",[1027,1811,1639],{"class":1047},[1027,1813,1814],{"class":1073},"(runs",[1027,1816,1186],{"class":1051},[1027,1818,1819],{"class":1734}," 4.8",[1027,1821,1186],{"class":1051},[1027,1823,1824],{"class":1734}," 5.5",[1027,1826,1197],{"class":1073},[1027,1828,1116],{"class":1051},[1027,1830,1831,1833,1836,1838,1840,1842,1844,1847,1849,1852,1854],{"class":1029,"line":1442},[1027,1832,1155],{"class":1043},[1027,1834,1835],{"class":1073}," best10k ",[1027,1837,1809],{"class":1051},[1027,1839,1639],{"class":1047},[1027,1841,1814],{"class":1073},[1027,1843,1186],{"class":1051},[1027,1845,1846],{"class":1734}," 9.5",[1027,1848,1186],{"class":1051},[1027,1850,1851],{"class":1734}," 11.0",[1027,1853,1197],{"class":1073},[1027,1855,1116],{"class":1051},[42,1857,1858,1861],{},[48,1859,1860],{},"Layer 2 (manual override):"," Official race PRs\nentered in the profile page. These override\nauto-calculated ones and show an \"Official\" badge.",[42,1863,1864,1865,1868,1869,1872],{},"Stored as seconds in the DB, displayed as ",[136,1866,1867],{},"mm:ss","\nor ",[136,1870,1871],{},"h:mm:ss"," — converted with a utility function:",[1019,1874,1876],{"className":1021,"code":1875,"language":1023,"meta":607,"style":607},"export function secondsToTimeString(seconds) {\n  const h = Math.floor(seconds / 3600);\n  const m = Math.floor((seconds % 3600) / 60);\n  const s = seconds % 60;\n  if (h > 0)\n    return `${h}:${m.toString().padStart(2, \"0\")}:\n                     ${s.toString().padStart(2, \"0\")}`;\n  return `${m}:${s.toString().padStart(2, \"0\")}`;\n}\n",[136,1877,1878,1896,1927,1964,1982,1998,2054,2091,2135],{"__ignoreMap":607},[1027,1879,1880,1882,1884,1887,1889,1892,1894],{"class":1029,"line":1030},[1027,1881,1040],{"class":1039},[1027,1883,1044],{"class":1043},[1027,1885,1886],{"class":1047}," secondsToTimeString",[1027,1888,1223],{"class":1051},[1027,1890,1891],{"class":1182},"seconds",[1027,1893,1197],{"class":1051},[1027,1895,1055],{"class":1051},[1027,1897,1898,1900,1903,1905,1908,1910,1913,1915,1917,1920,1923,1925],{"class":1029,"line":608},[1027,1899,1207],{"class":1043},[1027,1901,1902],{"class":1073}," h",[1027,1904,1177],{"class":1051},[1027,1906,1907],{"class":1073}," Math",[1027,1909,340],{"class":1051},[1027,1911,1912],{"class":1047},"floor",[1027,1914,1223],{"class":1066},[1027,1916,1891],{"class":1073},[1027,1918,1919],{"class":1051}," /",[1027,1921,1922],{"class":1734}," 3600",[1027,1924,1197],{"class":1066},[1027,1926,1116],{"class":1051},[1027,1928,1929,1931,1934,1936,1938,1940,1942,1945,1947,1950,1952,1954,1957,1960,1962],{"class":1029,"line":616},[1027,1930,1207],{"class":1043},[1027,1932,1933],{"class":1073}," m",[1027,1935,1177],{"class":1051},[1027,1937,1907],{"class":1073},[1027,1939,340],{"class":1051},[1027,1941,1912],{"class":1047},[1027,1943,1944],{"class":1066},"((",[1027,1946,1891],{"class":1073},[1027,1948,1949],{"class":1051}," %",[1027,1951,1922],{"class":1734},[1027,1953,1229],{"class":1066},[1027,1955,1956],{"class":1051},"/",[1027,1958,1959],{"class":1734}," 60",[1027,1961,1197],{"class":1066},[1027,1963,1116],{"class":1051},[1027,1965,1966,1968,1971,1973,1976,1978,1980],{"class":1029,"line":1070},[1027,1967,1207],{"class":1043},[1027,1969,1970],{"class":1073}," s",[1027,1972,1177],{"class":1051},[1027,1974,1975],{"class":1073}," seconds",[1027,1977,1949],{"class":1051},[1027,1979,1959],{"class":1734},[1027,1981,1116],{"class":1051},[1027,1983,1984,1986,1988,1991,1993,1995],{"class":1029,"line":1090},[1027,1985,1387],{"class":1039},[1027,1987,134],{"class":1066},[1027,1989,1990],{"class":1073},"h",[1027,1992,1731],{"class":1051},[1027,1994,1735],{"class":1734},[1027,1996,1997],{"class":1066},")\n",[1027,1999,2000,2003,2006,2008,2011,2014,2017,2020,2022,2025,2027,2029,2032,2034,2037,2039,2042,2044,2047,2049,2051],{"class":1029,"line":1110},[1027,2001,2002],{"class":1039},"    return",[1027,2004,2005],{"class":1051}," `${",[1027,2007,1990],{"class":1073},[1027,2009,2010],{"class":1051},"}",[1027,2012,1161],{"class":2013},"sfazB",[1027,2015,2016],{"class":1051},"${",[1027,2018,2019],{"class":1073},"m",[1027,2021,340],{"class":1051},[1027,2023,2024],{"class":1047},"toString",[1027,2026,1052],{"class":1073},[1027,2028,340],{"class":1051},[1027,2030,2031],{"class":1047},"padStart",[1027,2033,1223],{"class":1073},[1027,2035,2036],{"class":1734},"2",[1027,2038,1186],{"class":1051},[1027,2040,2041],{"class":1051}," \"",[1027,2043,1787],{"class":2013},[1027,2045,2046],{"class":1051},"\"",[1027,2048,1197],{"class":1073},[1027,2050,2010],{"class":1051},[1027,2052,2053],{"class":2013},":\n",[1027,2055,2056,2059,2062,2064,2066,2068,2070,2072,2074,2076,2078,2080,2082,2084,2086,2089],{"class":1029,"line":1119},[1027,2057,2058],{"class":1051},"                     ${",[1027,2060,2061],{"class":1073},"s",[1027,2063,340],{"class":1051},[1027,2065,2024],{"class":1047},[1027,2067,1052],{"class":1073},[1027,2069,340],{"class":1051},[1027,2071,2031],{"class":1047},[1027,2073,1223],{"class":1073},[1027,2075,2036],{"class":1734},[1027,2077,1186],{"class":1051},[1027,2079,2041],{"class":1051},[1027,2081,1787],{"class":2013},[1027,2083,2046],{"class":1051},[1027,2085,1197],{"class":1073},[1027,2087,2088],{"class":1051},"}`",[1027,2090,1116],{"class":1051},[1027,2092,2093,2095,2097,2099,2101,2103,2105,2107,2109,2111,2113,2115,2117,2119,2121,2123,2125,2127,2129,2131,2133],{"class":1029,"line":1312},[1027,2094,1060],{"class":1039},[1027,2096,2005],{"class":1051},[1027,2098,2019],{"class":1073},[1027,2100,2010],{"class":1051},[1027,2102,1161],{"class":2013},[1027,2104,2016],{"class":1051},[1027,2106,2061],{"class":1073},[1027,2108,340],{"class":1051},[1027,2110,2024],{"class":1047},[1027,2112,1052],{"class":1073},[1027,2114,340],{"class":1051},[1027,2116,2031],{"class":1047},[1027,2118,1223],{"class":1073},[1027,2120,2036],{"class":1734},[1027,2122,1186],{"class":1051},[1027,2124,2041],{"class":1051},[1027,2126,1787],{"class":2013},[1027,2128,2046],{"class":1051},[1027,2130,1197],{"class":1073},[1027,2132,2088],{"class":1051},[1027,2134,1116],{"class":1051},[1027,2136,2137],{"class":1029,"line":1318},[1027,2138,1122],{"class":1051},[52,2140],{},[37,2142,2144],{"id":2143},"what-i-learned","WHAT I LEARNED",[168,2146,2148],{"id":2147},"_1-new-tech-doesnt-have-to-be-scary","1. New Tech Doesn't Have to Be Scary",[42,2150,2151],{},"I came from Vue/Nuxt. React and Next.js felt foreign at first.",[42,2153,2154],{},"But the mental models transferred:",[173,2156,2157,2166,2176,2182],{},[176,2158,2159,2160,2163,2164],{},"Vue ",[136,2161,2162],{},"ref"," → React ",[136,2165,299],{},[176,2167,2168,2169,2172,2173],{},"Nuxt ",[136,2170,2171],{},"useFetch"," → TanStack Query ",[136,2174,2175],{},"useQuery",[176,2177,2159,2178,2181],{},[136,2179,2180],{},"composables"," → React custom hooks",[176,2183,2184],{},"Nuxt server routes → Next.js API routes",[42,2186,2187],{},[48,2188,2189],{},"The concepts were the same. Just different syntax.",[42,2191,2192],{},"By week 2 of building Splitlog, Next.js felt natural.\nBy week 4, I preferred the App Router pattern\nover anything I'd used before.",[168,2194,2196],{"id":2195},"_2-debugging-real-integrations-is-different","2. Debugging Real Integrations is Different",[42,2198,2199],{},"You can't mock a Strava webhook in your head.\nAt some point you have to run it and see what breaks.",[42,2201,2202],{},"I learned to:",[173,2204,2205,2208,2215,2218],{},[176,2206,2207],{},"Log everything during development",[176,2209,2210,2211,2214],{},"Read error codes carefully\n(",[136,2212,2213],{},"PGRST116"," = row not found in Supabase)",[176,2216,2217],{},"Check the obvious things first\n(is RLS blocking this? is the token expired?)",[176,2219,2220],{},"Trust the data, not my assumptions",[42,2222,2223],{},"The webhook RLS bug took me an hour to debug.\nBut I'll never forget how Supabase RLS works now.",[168,2225,2227],{"id":2226},"_3-ai-assisted-development-is-a-skill","3. AI-Assisted Development is a Skill",[42,2229,2230],{},"I used Claude heavily throughout this project —\nespecially for patterns I'd never seen before\n(OAuth flows, webhook handlers, TanStack Table config).",[42,2232,2233],{},"But I learned that AI assistance is most powerful when:",[173,2235,2236,2239,2242],{},[176,2237,2238],{},"You understand the problem first",[176,2240,2241],{},"You question the generated code",[176,2243,2244],{},"You break down \"why\" it works before moving on",[42,2246,2247],{},"Copy-pasting without understanding is technical debt.\nUnderstanding first, then using AI to accelerate —\nthat's the skill.",[168,2249,2251],{"id":2250},"_4-full-stack-changes-how-you-think","4. Full Stack Changes How You Think",[42,2253,2254],{},"Before Splitlog, I was a frontend developer\nwho consumed APIs built by someone else.",[42,2256,2257],{},"Now I think in systems:",[173,2259,2260,2263,2266,2269],{},[176,2261,2262],{},"How does the data flow from Strava → DB → UI?",[176,2264,2265],{},"What happens when the token expires mid-request?",[176,2267,2268],{},"How does RLS affect this query?",[176,2270,2271],{},"What if two webhook events arrive simultaneously?",[42,2273,2274],{},[48,2275,2276],{},"Full stack thinking made me a better frontend developer too.",[52,2278],{},[37,2280,2282],{"id":2281},"the-results","THE RESULTS",[168,2284,2286],{"id":2285},"what-i-replaced","What I Replaced",[42,2288,2289,2290,2292],{},"Before Splitlog — my post-run workflow:\n",[988,2291],{},"\nFinish run → open Strava → copy stats →\nopen Notion → find training plan →\nmanually type every field → done\n~5 minutes every run, error-prone",[42,2294,2295,2296,2298],{},"After Splitlog:\n",[988,2297],{},"\nFinish run → open Splitlog →\ndata is already there ✓\n~0 minutes, automatic",[168,2300,2302],{"id":2301},"technical-achievements","Technical Achievements",[173,2304,2305,2308,2311,2314,2317,2320,2323,2326],{},[176,2306,2307],{},"✅ Real-time Strava webhook sync\n(data appears within seconds of finishing a run)",[176,2309,2310],{},"✅ OAuth 2.0 with automatic token refresh",[176,2312,2313],{},"✅ Powerful run dashboard replacing Strava's\nlimited activity list",[176,2315,2316],{},"✅ Training plan manager replacing Notion workflow",[176,2318,2319],{},"✅ Analytics with 4 chart types + personal bests",[176,2321,2322],{},"✅ User profile with 2-layer PR system",[176,2324,2325],{},"✅ Mobile responsive",[176,2327,2328],{},"✅ Deployed to production on Vercel",[168,2330,2332],{"id":2331},"for-me","For Me",[42,2334,2335],{},"This project leveled me up across the board:",[173,2337,2338,2344,2350,2356,2362],{},[176,2339,2340,2343],{},[48,2341,2342],{},"Learned Next.js App Router"," from zero\nto production in 6 weeks",[176,2345,2346,2349],{},[48,2347,2348],{},"Built my first full auth system","\n(Supabase + middleware + protected routes)",[176,2351,2352,2355],{},[48,2353,2354],{},"Integrated a real third-party API","\nwith OAuth + webhooks",[176,2357,2358,2361],{},[48,2359,2360],{},"Deployed a full-stack app"," to production",[176,2363,2364,2367],{},[48,2365,2366],{},"Started planning AI features"," —\nnext version will include an AI coach\nthat generates training plans automatically\nbased on your Strava data and race goals",[52,2369],{},[37,2371,2373],{"id":2372},"tech-stack","TECH STACK",[42,2375,2376,2379,2380,2382,2385,2386,2388,2391,2392,2394,2397,2398,2400,2403,2404,2406,2409,2410,2412,2415,2416,2418,2421,2422,2424,2427,2428,2430,2432],{},[48,2377,2378],{},"Frontend:"," Next.js 15 (App Router), TypeScript",[988,2381],{},[48,2383,2384],{},"Styling:"," Tailwind CSS, shadcn/ui",[988,2387],{},[48,2389,2390],{},"Table:"," TanStack Table v8",[988,2393],{},[48,2395,2396],{},"Charts:"," Recharts",[988,2399],{},[48,2401,2402],{},"Data Fetching:"," TanStack Query",[988,2405],{},[48,2407,2408],{},"Database:"," Supabase (PostgreSQL)",[988,2411],{},[48,2413,2414],{},"Auth:"," Supabase Auth + middleware",[988,2417],{},[48,2419,2420],{},"API:"," Next.js API Routes (serverless)",[988,2423],{},[48,2425,2426],{},"Integration:"," Strava API v3 (OAuth 2.0 + Webhooks)",[988,2429],{},[48,2431,216],{}," Vercel",[52,2434],{},[37,2436,2438],{"id":2437},"whats-next","WHAT'S NEXT",[42,2440,2441],{},"Splitlog v2 is already planned:",[42,2443,2444],{},[48,2445,2446],{},"AI Coach (In Progress)",[42,2448,2449],{},"An AI coach that generates your full training plan\nautomatically based on:",[173,2451,2452,2455,2458],{},[176,2453,2454],{},"Your target race and goal time",[176,2456,2457],{},"Your current fitness from actual Strava data",[176,2459,2460],{},"Your weekly availability and preferences",[42,2462,2463],{},"No more manually adding training days one by one.\nJust tell the AI \"I want to run a sub-2:00 half marathon\nin 12 weeks\" and it builds the whole plan.",[42,2465,2466],{},"This will use the Claude API with a custom system prompt\ngiving the AI your real run history as context —\nmaking the plans genuinely personalized,\nnot just generic templates.",[42,2468,2469,2472,2474],{},[48,2470,2471],{},"Garmin Support",[988,2473],{},"\nExtend the integration to support Garmin Connect\nalongside Strava.",[42,2476,2477,2480,2482],{},[48,2478,2479],{},"Export to PDF/CSV",[988,2481],{},"\nDownload your training data for offline use or sharing.",[42,2484,2485],{},[48,2486,2487],{},"Mobile App (Future)",[42,2489,2490],{},"A dedicated iOS and Android app built with React Native —\nso you can check your dashboard, review your training plan,\nand assign runs directly from your phone right after\nyou finish a workout.",[42,2492,2493],{},"No more opening a browser. Just open the app,\nsee your run already synced, tap to assign it\nto your training plan, and close it.\nThe full Splitlog experience in your pocket.",[52,2495],{},[37,2497,2499],{"id":2498},"the-bigger-picture","THE BIGGER PICTURE",[42,2501,2502],{},"I started this project because I was frustrated\nwith manual data entry after runs.",[42,2504,2505],{},"I ended up building a full-stack platform with:",[173,2507,2508,2511,2514,2517,2520,2523],{},[176,2509,2510],{},"Real OAuth integrations",[176,2512,2513],{},"Webhook-driven automation",[176,2515,2516],{},"A database with proper security policies",[176,2518,2519],{},"Charts and analytics",[176,2521,2522],{},"A training plan system",[176,2524,2525],{},"AI features on the roadmap",[42,2527,2528],{},[48,2529,2530],{},"This is the project that made me a full-stack developer.",[42,2532,2533],{},"Not because it's the most complex app ever built.\nBut because I built every layer of it myself —\nand I understood every decision I made along the way.",[42,2535,2536],{},"The next run is already tracked. 🏃",[2538,2539,2540],"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 .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--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 .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}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 .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}",{"title":607,"searchDepth":608,"depth":608,"links":2542},[2543,2544,2545,2546,2547,2550,2556,2562,2567,2568,2569],{"id":648,"depth":608,"text":649},{"id":689,"depth":608,"text":690},{"id":740,"depth":608,"text":741},{"id":56,"depth":608,"text":57},{"id":801,"depth":608,"text":802,"children":2548},[2549],{"id":805,"depth":616,"text":806},{"id":165,"depth":608,"text":166,"children":2551},[2552,2553,2554,2555],{"id":974,"depth":616,"text":975},{"id":1128,"depth":616,"text":1129},{"id":1588,"depth":616,"text":1589},{"id":1619,"depth":616,"text":1620},{"id":2143,"depth":608,"text":2144,"children":2557},[2558,2559,2560,2561],{"id":2147,"depth":616,"text":2148},{"id":2195,"depth":616,"text":2196},{"id":2226,"depth":616,"text":2227},{"id":2250,"depth":616,"text":2251},{"id":2281,"depth":608,"text":2282,"children":2563},[2564,2565,2566],{"id":2285,"depth":616,"text":2286},{"id":2301,"depth":616,"text":2302},{"id":2331,"depth":616,"text":2332},{"id":2372,"depth":608,"text":2373},{"id":2437,"depth":608,"text":2438},{"id":2498,"depth":608,"text":2499},"View Case Study","I was tired of copy-pasting my Strava stats into Notion after every run. So I built my own platform to automate it. First full-stack project. First time with Next.js, Supabase, and real API integrations. The moment my run synced automatically? That's when it clicked.","/projects/splitlog.png",{},"/projects/splitlog",{"title":643,"description":2571},"projects/splitlog",[632,633,636,2578,2579,637,2580],"TanStack Table","Strava API","Recharts","yuBhEBDSu4Tbvbzbf54ietf_CKGyHh6RkN42WbLYAwA",{"id":2583,"title":2584,"body":2585,"buttonLabel":2570,"date":623,"description":4511,"extension":625,"image":4512,"meta":4513,"navigation":24,"path":4514,"seo":4515,"stem":4516,"tags":4517,"__hash__":4522},"projects/projects/three-years-later-rebuilding-my-college-capstone.md","Three Years Later: Rebuilding My College Capstone",{"type":34,"value":2586,"toc":4479},[2587,2593,2595,2602,2622,2629,2635,2640,2643,2645,2647,2665,2669,2674,2685,2690,2704,2709,2720,2725,2736,2741,2752,2754,2756,2760,2768,2772,2778,2780,2782,2786,2791,2842,2847,3030,3035,3049,3053,3058,3128,3133,3336,3340,3357,3361,3366,3395,3400,3565,3569,3583,3587,3592,3842,3847,3964,3968,3982,3984,3986,3990,3993,3998,4001,4026,4033,4037,4040,4060,4071,4075,4078,4102,4105,4134,4137,4141,4151,4154,4174,4177,4185,4192,4196,4202,4205,4229,4243,4246,4248,4250,4254,4271,4275,4298,4302,4309,4312,4326,4328,4330,4370,4372,4376,4393,4395,4399,4402,4408,4414,4420,4426,4432,4435,4437,4439,4445,4463,4466,4469,4471,4476],[42,2588,2589],{},[769,2590],{"alt":2591,"src":2592},"Dashboard","/projects/capstone-reimplementation.png",[37,2594,649],{"id":648},[42,2596,2597,2598,2601],{},"In ",[48,2599,2600],{},"2023",", I built a Commuter Security Management System for my college\ncapstone. It was a complete platform:",[173,2603,2604,2610,2616],{},[176,2605,2606,2609],{},[48,2607,2608],{},"Mobile app"," for passengers to register and scan QR codes",[176,2611,2612,2615],{},[48,2613,2614],{},"Backend API"," to process data and manage the system",[176,2617,2618,2621],{},[48,2619,2620],{},"Admin dashboard"," to monitor everything",[42,2623,2624,2625,2628],{},"I built the admin panel with ",[48,2626,2627],{},"PHP, vanilla JavaScript, Bootstrap, and MySQL",".\nIt worked. It got me my degree. But it was 2023 code.",[42,2630,2631,2632,2634],{},"Fast forward to ",[48,2633,623],{},". I've spent 2 years building enterprise systems at\nSummit Media with Vue 3, Nuxt 3, TypeScript, and modern patterns. Looking back\nat my capstone code?",[42,2636,2637],{},[48,2638,2639],{},"I could do so much better.",[42,2641,2642],{},"So I rebuilt the admin dashboard—not because I had to, but because I wanted to\nsee exactly how much I'd grown.",[52,2644],{},[37,2646,690],{"id":689},[42,2648,2649,2652,2653,2655,2658,2659,2661,2664],{},[48,2650,2651],{},"Constraint:"," Recreate the EXACT same features and UI",[988,2654],{},[48,2656,2657],{},"Goal:"," Use modern patterns I learned professionally",[988,2660],{},[48,2662,2663],{},"Tech Stack:"," Nuxt 4, Vue 3, TypeScript, Pinia, Tailwind CSS (Frontend) + Express.js, JWT, MySQL (Backend)",[168,2666,2668],{"id":2667},"features-to-recreate","Features to Recreate:",[42,2670,2671],{},[48,2672,2673],{},"Dashboard Analytics",[173,2675,2676,2679,2682],{},[176,2677,2678],{},"Real-time statistics for passengers, drivers, rides, and admins",[176,2680,2681],{},"Interactive charts and metrics",[176,2683,2684],{},"Live updates without page refresh",[42,2686,2687],{},[48,2688,2689],{},"User Management",[173,2691,2692,2695,2698,2701],{},[176,2693,2694],{},"Manage passenger accounts",[176,2696,2697],{},"Manage driver accounts",[176,2699,2700],{},"Manage admin accounts",[176,2702,2703],{},"Role-based access control",[42,2705,2706],{},[48,2707,2708],{},"Ride Tracking",[173,2710,2711,2714,2717],{},[176,2712,2713],{},"View all rides in real-time",[176,2715,2716],{},"Monitor ride status (pending, active, completed)",[176,2718,2719],{},"Detailed ride information and history",[42,2721,2722],{},[48,2723,2724],{},"Authentication",[173,2726,2727,2730,2733],{},[176,2728,2729],{},"Secure login system",[176,2731,2732],{},"Protected routes with middleware",[176,2734,2735],{},"Session management",[42,2737,2738],{},[48,2739,2740],{},"Responsive Design",[173,2742,2743,2746,2749],{},[176,2744,2745],{},"Mobile-friendly interface",[176,2747,2748],{},"Works on all screen sizes",[176,2750,2751],{},"Touch-optimized interactions",[52,2753],{},[37,2755,802],{"id":801},[168,2757,2759],{"id":2758},"the-old-way-2023-capstone","The Old Way (2023 Capstone)",[1019,2761,2766],{"className":2762,"code":2764,"language":2765},[2763],"language-text","Tech Stack:\n- PHP (backend + frontend logic)\n- Vanilla JavaScript (DOM manipulation)\n- Bootstrap (styling)\n- MySQL (database)\n\nArchitecture:\n- Monolithic PHP files\n- Mixed concerns (HTML + PHP + SQL)\n- Manual DOM updates\n- No type safety\n- ~2500 lines of mixed code\n","text",[136,2767,2764],{"__ignoreMap":607},[168,2769,2771],{"id":2770},"the-new-way-2026-rebuild","The New Way (2026 Rebuild)",[1019,2773,2776],{"className":2774,"code":2775,"language":2765},[2763],"Frontend:\n- Nuxt 4 (framework)\n- Vue 3 (reactive UI)\n- TypeScript (type safety)\n- Pinia (state management)\n- Tailwind CSS (utility-first styling)\n- Lucide Icons (modern iconography)\n- ~1400 lines of organized, typed code\n\nBackend (Separate Repository):\n- Express.js (Node.js framework)\n- JWT authentication with bcryptjs\n- MySQL database\n- RESTful API endpoints\n- CORS support\n- Comprehensive error handling\n\nArchitecture:\n- Decoupled frontend and backend\n- Component-based design (frontend)\n- MVC pattern (backend)\n- Reactive state management\n- Full type safety\n",[136,2777,2775],{"__ignoreMap":607},[52,2779],{},[37,2781,166],{"id":165},[168,2783,2785],{"id":2784},"_1-component-architecture-vs-procedural-code","1. Component Architecture vs Procedural Code",[42,2787,2788],{},[48,2789,2790],{},"Old approach (PHP):",[1019,2792,2796],{"className":2793,"code":2794,"language":2795,"meta":607,"style":607},"language-php shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003C?php\n// users.php - Everything in one file\nif ($_GET['action'] == 'list') {\n    $users = mysqli_query($conn, \"SELECT * FROM users\");\n    while ($row = mysqli_fetch_assoc($users)) {\n        echo \"\u003Ctr>\u003Ctd>\" . $row['name'] . \"\u003C/td>\u003C/tr>\";\n    }\n}\n?>\n","php",[136,2797,2798,2803,2808,2813,2818,2823,2828,2833,2837],{"__ignoreMap":607},[1027,2799,2800],{"class":1029,"line":1030},[1027,2801,2802],{},"\u003C?php\n",[1027,2804,2805],{"class":1029,"line":608},[1027,2806,2807],{},"// users.php - Everything in one file\n",[1027,2809,2810],{"class":1029,"line":616},[1027,2811,2812],{},"if ($_GET['action'] == 'list') {\n",[1027,2814,2815],{"class":1029,"line":1070},[1027,2816,2817],{},"    $users = mysqli_query($conn, \"SELECT * FROM users\");\n",[1027,2819,2820],{"class":1029,"line":1090},[1027,2821,2822],{},"    while ($row = mysqli_fetch_assoc($users)) {\n",[1027,2824,2825],{"class":1029,"line":1110},[1027,2826,2827],{},"        echo \"\u003Ctr>\u003Ctd>\" . $row['name'] . \"\u003C/td>\u003C/tr>\";\n",[1027,2829,2830],{"class":1029,"line":1119},[1027,2831,2832],{},"    }\n",[1027,2834,2835],{"class":1029,"line":1312},[1027,2836,1122],{},[1027,2838,2839],{"class":1029,"line":1318},[1027,2840,2841],{},"?>\n",[42,2843,2844],{},[48,2845,2846],{},"New approach (Vue/Nuxt):",[1019,2848,2852],{"className":2849,"code":2850,"language":2851,"meta":607,"style":607},"language-typescript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","// components/DataTable.vue - Reusable component\n\u003Ctemplate>\n  \u003CTable>\n    \u003CTableRow v-for=\"user in users\" :key=\"user.id\">\n      \u003CTableCell>{{ user.name }}\u003C/TableCell>\n    \u003C/TableRow>\n  \u003C/Table>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { users } = await useUsers(); // Type-safe composable\n\u003C/script>\n","typescript",[136,2853,2854,2859,2869,2879,2916,2944,2954,2963,2972,2976,2993,3021],{"__ignoreMap":607},[1027,2855,2856],{"class":1029,"line":1030},[1027,2857,2858],{"class":1033},"// components/DataTable.vue - Reusable component\n",[1027,2860,2861,2863,2866],{"class":1029,"line":608},[1027,2862,1168],{"class":1073},[1027,2864,2865],{"class":1164},"template",[1027,2867,2868],{"class":1073},">\n",[1027,2870,2871,2874,2877],{"class":1029,"line":616},[1027,2872,2873],{"class":1073},"  \u003C",[1027,2875,2876],{"class":1164},"Table",[1027,2878,2868],{"class":1073},[1027,2880,2881,2884,2887,2890,2893,2895,2897,2900,2902,2905,2907,2909,2912,2914],{"class":1029,"line":1070},[1027,2882,2883],{"class":1051},"    \u003C",[1027,2885,2886],{"class":1073},"TableRow v",[1027,2888,2889],{"class":1051},"-",[1027,2891,2892],{"class":1073},"for",[1027,2894,1809],{"class":1051},[1027,2896,2046],{"class":1051},[1027,2898,2899],{"class":2013},"user in users",[1027,2901,2046],{"class":1051},[1027,2903,2904],{"class":1073}," :key",[1027,2906,1809],{"class":1051},[1027,2908,2046],{"class":1051},[1027,2910,2911],{"class":2013},"user.id",[1027,2913,2046],{"class":1051},[1027,2915,2868],{"class":1051},[1027,2917,2918,2921,2924,2926,2929,2932,2934,2937,2940,2942],{"class":1029,"line":1090},[1027,2919,2920],{"class":1073},"      \u003C",[1027,2922,2923],{"class":1164},"TableCell",[1027,2925,1174],{"class":1073},[1027,2927,2928],{"class":1051},"{{",[1027,2930,2931],{"class":1073}," user",[1027,2933,340],{"class":1051},[1027,2935,2936],{"class":1073},"name",[1027,2938,2939],{"class":1051}," }}\u003C/",[1027,2941,2923],{"class":1073},[1027,2943,2868],{"class":1051},[1027,2945,2946,2949,2952],{"class":1029,"line":1110},[1027,2947,2948],{"class":1051},"    \u003C/",[1027,2950,2951],{"class":1073},"TableRow",[1027,2953,2868],{"class":1051},[1027,2955,2956,2959,2961],{"class":1029,"line":1119},[1027,2957,2958],{"class":1051},"  \u003C/",[1027,2960,2876],{"class":1073},[1027,2962,2868],{"class":1051},[1027,2964,2965,2968,2970],{"class":1029,"line":1312},[1027,2966,2967],{"class":1051},"\u003C/",[1027,2969,2865],{"class":1073},[1027,2971,2868],{"class":1051},[1027,2973,2974],{"class":1029,"line":1318},[1027,2975,1309],{"emptyLinePlaceholder":24},[1027,2977,2978,2980,2983,2985,2987,2989,2991],{"class":1029,"line":1356},[1027,2979,1168],{"class":1051},[1027,2981,2982],{"class":1073},"script setup lang",[1027,2984,1809],{"class":1051},[1027,2986,2046],{"class":1051},[1027,2988,1023],{"class":2013},[1027,2990,2046],{"class":1051},[1027,2992,2868],{"class":1051},[1027,2994,2995,2997,3000,3003,3005,3007,3010,3013,3015,3018],{"class":1029,"line":1384},[1027,2996,1155],{"class":1043},[1027,2998,2999],{"class":1051}," {",[1027,3001,3002],{"class":1073}," users ",[1027,3004,2010],{"class":1051},[1027,3006,1177],{"class":1051},[1027,3008,3009],{"class":1039}," await",[1027,3011,3012],{"class":1047}," useUsers",[1027,3014,1052],{"class":1073},[1027,3016,3017],{"class":1051},";",[1027,3019,3020],{"class":1033}," // Type-safe composable\n",[1027,3022,3023,3025,3028],{"class":1029,"line":1409},[1027,3024,2967],{"class":1051},[1027,3026,3027],{"class":1073},"script",[1027,3029,2868],{"class":1051},[42,3031,3032],{},[48,3033,3034],{},"Why this is better:",[173,3036,3037,3040,3043,3046],{},[176,3038,3039],{},"Reusable across the app",[176,3041,3042],{},"Type-safe (TypeScript catches errors)",[176,3044,3045],{},"Reactive (updates automatically when data changes)",[176,3047,3048],{},"Testable (can test components in isolation)",[168,3050,3052],{"id":3051},"_2-state-management-global-variables-pinia","2. State Management: Global Variables → Pinia",[42,3054,3055],{},[48,3056,3057],{},"Old approach:",[1019,3059,3063],{"className":3060,"code":3061,"language":3062,"meta":607,"style":607},"language-javascript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","// Global variables scattered everywhere\nvar currentUser = null;\nvar rideData = [];\n\nfunction loadRides() {\n  $.ajax({\n    url: \"api/rides.php\",\n    success: function (data) {\n      rideData = data;\n      updateUI();\n    },\n  });\n}\n","javascript",[136,3064,3065,3070,3075,3080,3084,3089,3094,3099,3104,3109,3114,3119,3124],{"__ignoreMap":607},[1027,3066,3067],{"class":1029,"line":1030},[1027,3068,3069],{},"// Global variables scattered everywhere\n",[1027,3071,3072],{"class":1029,"line":608},[1027,3073,3074],{},"var currentUser = null;\n",[1027,3076,3077],{"class":1029,"line":616},[1027,3078,3079],{},"var rideData = [];\n",[1027,3081,3082],{"class":1029,"line":1070},[1027,3083,1309],{"emptyLinePlaceholder":24},[1027,3085,3086],{"class":1029,"line":1090},[1027,3087,3088],{},"function loadRides() {\n",[1027,3090,3091],{"class":1029,"line":1110},[1027,3092,3093],{},"  $.ajax({\n",[1027,3095,3096],{"class":1029,"line":1119},[1027,3097,3098],{},"    url: \"api/rides.php\",\n",[1027,3100,3101],{"class":1029,"line":1312},[1027,3102,3103],{},"    success: function (data) {\n",[1027,3105,3106],{"class":1029,"line":1318},[1027,3107,3108],{},"      rideData = data;\n",[1027,3110,3111],{"class":1029,"line":1356},[1027,3112,3113],{},"      updateUI();\n",[1027,3115,3116],{"class":1029,"line":1384},[1027,3117,3118],{},"    },\n",[1027,3120,3121],{"class":1029,"line":1409},[1027,3122,3123],{},"  });\n",[1027,3125,3126],{"class":1029,"line":1442},[1027,3127,1122],{},[42,3129,3130],{},[48,3131,3132],{},"New approach:",[1019,3134,3136],{"className":2849,"code":3135,"language":2851,"meta":607,"style":607},"// stores/rides.ts - Centralized, typed state\nexport const useRidesStore = defineStore(\"rides\", () => {\n  const rides = ref\u003CRide[]>([]);\n  const loading = ref(false);\n\n  async function fetchRides() {\n    loading.value = true;\n    rides.value = await $fetch(\"/api/rides\");\n    loading.value = false;\n  }\n\n  return { rides, loading, fetchRides };\n});\n",[136,3137,3138,3143,3176,3203,3223,3227,3241,3258,3287,3301,3305,3309,3328],{"__ignoreMap":607},[1027,3139,3140],{"class":1029,"line":1030},[1027,3141,3142],{"class":1033},"// stores/rides.ts - Centralized, typed state\n",[1027,3144,3145,3147,3150,3153,3155,3158,3160,3162,3165,3167,3169,3172,3174],{"class":1029,"line":608},[1027,3146,1040],{"class":1039},[1027,3148,3149],{"class":1043}," const",[1027,3151,3152],{"class":1073}," useRidesStore ",[1027,3154,1809],{"class":1051},[1027,3156,3157],{"class":1047}," defineStore",[1027,3159,1223],{"class":1073},[1027,3161,2046],{"class":1051},[1027,3163,3164],{"class":2013},"rides",[1027,3166,2046],{"class":1051},[1027,3168,1186],{"class":1051},[1027,3170,3171],{"class":1051}," ()",[1027,3173,1200],{"class":1043},[1027,3175,1055],{"class":1051},[1027,3177,3178,3180,3183,3185,3188,3190,3193,3196,3198,3201],{"class":1029,"line":616},[1027,3179,1207],{"class":1043},[1027,3181,3182],{"class":1073}," rides",[1027,3184,1177],{"class":1051},[1027,3186,3187],{"class":1047}," ref",[1027,3189,1168],{"class":1051},[1027,3191,3192],{"class":1164},"Ride",[1027,3194,3195],{"class":1066},"[]",[1027,3197,1174],{"class":1051},[1027,3199,3200],{"class":1066},"([])",[1027,3202,1116],{"class":1051},[1027,3204,3205,3207,3210,3212,3214,3216,3219,3221],{"class":1029,"line":1070},[1027,3206,1207],{"class":1043},[1027,3208,3209],{"class":1073}," loading",[1027,3211,1177],{"class":1051},[1027,3213,3187],{"class":1047},[1027,3215,1223],{"class":1066},[1027,3217,3218],{"class":1403},"false",[1027,3220,1197],{"class":1066},[1027,3222,1116],{"class":1051},[1027,3224,3225],{"class":1029,"line":1090},[1027,3226,1309],{"emptyLinePlaceholder":24},[1027,3228,3229,3232,3234,3237,3239],{"class":1029,"line":1110},[1027,3230,3231],{"class":1043},"  async",[1027,3233,1044],{"class":1043},[1027,3235,3236],{"class":1047}," fetchRides",[1027,3238,1052],{"class":1051},[1027,3240,1055],{"class":1051},[1027,3242,3243,3246,3248,3251,3253,3256],{"class":1029,"line":1119},[1027,3244,3245],{"class":1073},"    loading",[1027,3247,340],{"class":1051},[1027,3249,3250],{"class":1073},"value",[1027,3252,1177],{"class":1051},[1027,3254,3255],{"class":1403}," true",[1027,3257,1116],{"class":1051},[1027,3259,3260,3263,3265,3267,3269,3271,3274,3276,3278,3281,3283,3285],{"class":1029,"line":1312},[1027,3261,3262],{"class":1073},"    rides",[1027,3264,340],{"class":1051},[1027,3266,3250],{"class":1073},[1027,3268,1177],{"class":1051},[1027,3270,3009],{"class":1039},[1027,3272,3273],{"class":1047}," $fetch",[1027,3275,1223],{"class":1066},[1027,3277,2046],{"class":1051},[1027,3279,3280],{"class":2013},"/api/rides",[1027,3282,2046],{"class":1051},[1027,3284,1197],{"class":1066},[1027,3286,1116],{"class":1051},[1027,3288,3289,3291,3293,3295,3297,3299],{"class":1029,"line":1318},[1027,3290,3245],{"class":1073},[1027,3292,340],{"class":1051},[1027,3294,3250],{"class":1073},[1027,3296,1177],{"class":1051},[1027,3298,1404],{"class":1403},[1027,3300,1116],{"class":1051},[1027,3302,3303],{"class":1029,"line":1356},[1027,3304,1565],{"class":1051},[1027,3306,3307],{"class":1029,"line":1384},[1027,3308,1309],{"emptyLinePlaceholder":24},[1027,3310,3311,3313,3315,3317,3319,3321,3323,3325],{"class":1029,"line":1409},[1027,3312,1060],{"class":1039},[1027,3314,2999],{"class":1051},[1027,3316,3182],{"class":1073},[1027,3318,1186],{"class":1051},[1027,3320,3209],{"class":1073},[1027,3322,1186],{"class":1051},[1027,3324,3236],{"class":1073},[1027,3326,3327],{"class":1051}," };\n",[1027,3329,3330,3332,3334],{"class":1029,"line":1442},[1027,3331,2010],{"class":1051},[1027,3333,1197],{"class":1073},[1027,3335,1116],{"class":1051},[42,3337,3338],{},[48,3339,3034],{},[173,3341,3342,3345,3351,3354],{},[176,3343,3344],{},"Single source of truth",[176,3346,3347,3348,3350],{},"Type-safe (Ride",[1027,3349],{}," enforces structure)",[176,3352,3353],{},"Reactive (components auto-update)",[176,3355,3356],{},"Predictable state flow",[168,3358,3360],{"id":3359},"_3-type-safety-runtime-errors-compile-time-errors","3. Type Safety: Runtime Errors → Compile-Time Errors",[42,3362,3363],{},[48,3364,3365],{},"Old approach (vanilla JS):",[1019,3367,3369],{"className":3060,"code":3368,"language":3062,"meta":607,"style":607},"function displayRide(ride) {\n  // What properties does ride have? Who knows!\n  document.getElementById(\"status\").innerHTML = ride.staus; // Typo!\n  // This error only shows up when you run the code\n}\n",[136,3370,3371,3376,3381,3386,3391],{"__ignoreMap":607},[1027,3372,3373],{"class":1029,"line":1030},[1027,3374,3375],{},"function displayRide(ride) {\n",[1027,3377,3378],{"class":1029,"line":608},[1027,3379,3380],{},"  // What properties does ride have? Who knows!\n",[1027,3382,3383],{"class":1029,"line":616},[1027,3384,3385],{},"  document.getElementById(\"status\").innerHTML = ride.staus; // Typo!\n",[1027,3387,3388],{"class":1029,"line":1070},[1027,3389,3390],{},"  // This error only shows up when you run the code\n",[1027,3392,3393],{"class":1029,"line":1090},[1027,3394,1122],{},[42,3396,3397],{},[48,3398,3399],{},"New approach (TypeScript):",[1019,3401,3403],{"className":2849,"code":3402,"language":2851,"meta":607,"style":607},"interface Ride {\n  id: string;\n  status: \"pending\" | \"active\" | \"completed\";\n  passenger: User;\n  driver: User;\n}\n\nfunction displayRide(ride: Ride) {\n  console.log(ride.staus); // ❌ TypeScript error: Property 'staus' does not exist\n  console.log(ride.status); // ✅ Correct\n}\n",[136,3404,3405,3415,3426,3460,3472,3483,3487,3491,3511,3537,3561],{"__ignoreMap":607},[1027,3406,3407,3410,3413],{"class":1029,"line":1030},[1027,3408,3409],{"class":1043},"interface",[1027,3411,3412],{"class":1164}," Ride",[1027,3414,1055],{"class":1051},[1027,3416,3417,3420,3422,3424],{"class":1029,"line":608},[1027,3418,3419],{"class":1066},"  id",[1027,3421,1161],{"class":1051},[1027,3423,1379],{"class":1164},[1027,3425,1116],{"class":1051},[1027,3427,3428,3431,3433,3435,3438,3440,3443,3445,3447,3449,3451,3453,3456,3458],{"class":1029,"line":616},[1027,3429,3430],{"class":1066},"  status",[1027,3432,1161],{"class":1051},[1027,3434,2041],{"class":1051},[1027,3436,3437],{"class":2013},"pending",[1027,3439,2046],{"class":1051},[1027,3441,3442],{"class":1051}," |",[1027,3444,2041],{"class":1051},[1027,3446,391],{"class":2013},[1027,3448,2046],{"class":1051},[1027,3450,3442],{"class":1051},[1027,3452,2041],{"class":1051},[1027,3454,3455],{"class":2013},"completed",[1027,3457,2046],{"class":1051},[1027,3459,1116],{"class":1051},[1027,3461,3462,3465,3467,3470],{"class":1029,"line":1070},[1027,3463,3464],{"class":1066},"  passenger",[1027,3466,1161],{"class":1051},[1027,3468,3469],{"class":1164}," User",[1027,3471,1116],{"class":1051},[1027,3473,3474,3477,3479,3481],{"class":1029,"line":1090},[1027,3475,3476],{"class":1066},"  driver",[1027,3478,1161],{"class":1051},[1027,3480,3469],{"class":1164},[1027,3482,1116],{"class":1051},[1027,3484,3485],{"class":1029,"line":1110},[1027,3486,1122],{"class":1051},[1027,3488,3489],{"class":1029,"line":1119},[1027,3490,1309],{"emptyLinePlaceholder":24},[1027,3492,3493,3495,3498,3500,3503,3505,3507,3509],{"class":1029,"line":1312},[1027,3494,1636],{"class":1043},[1027,3496,3497],{"class":1047}," displayRide",[1027,3499,1223],{"class":1051},[1027,3501,3502],{"class":1182},"ride",[1027,3504,1161],{"class":1051},[1027,3506,3412],{"class":1164},[1027,3508,1197],{"class":1051},[1027,3510,1055],{"class":1051},[1027,3512,3513,3516,3518,3521,3523,3525,3527,3530,3532,3534],{"class":1029,"line":1318},[1027,3514,3515],{"class":1073},"  console",[1027,3517,340],{"class":1051},[1027,3519,3520],{"class":1047},"log",[1027,3522,1223],{"class":1066},[1027,3524,3502],{"class":1073},[1027,3526,340],{"class":1051},[1027,3528,3529],{"class":1073},"staus",[1027,3531,1197],{"class":1066},[1027,3533,3017],{"class":1051},[1027,3535,3536],{"class":1033}," // ❌ TypeScript error: Property 'staus' does not exist\n",[1027,3538,3539,3541,3543,3545,3547,3549,3551,3554,3556,3558],{"class":1029,"line":1356},[1027,3540,3515],{"class":1073},[1027,3542,340],{"class":1051},[1027,3544,3520],{"class":1047},[1027,3546,1223],{"class":1066},[1027,3548,3502],{"class":1073},[1027,3550,340],{"class":1051},[1027,3552,3553],{"class":1073},"status",[1027,3555,1197],{"class":1066},[1027,3557,3017],{"class":1051},[1027,3559,3560],{"class":1033}," // ✅ Correct\n",[1027,3562,3563],{"class":1029,"line":1384},[1027,3564,1122],{"class":1051},[42,3566,3567],{},[48,3568,3034],{},[173,3570,3571,3574,3577,3580],{},[176,3572,3573],{},"Catches typos before running code",[176,3575,3576],{},"Self-documenting (you know what properties exist)",[176,3578,3579],{},"Better IDE autocomplete",[176,3581,3582],{},"Refactoring is safer",[168,3584,3586],{"id":3585},"_4-styling-custom-css-tailwind-utility-classes","4. Styling: Custom CSS → Tailwind Utility Classes",[42,3588,3589],{},[48,3590,3591],{},"Old approach (Bootstrap + custom CSS):",[1019,3593,3597],{"className":3594,"code":3595,"language":3596,"meta":607,"style":607},"language-html shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003Cdiv class=\"card user-card\">\n  \u003Cdiv class=\"card-header bg-primary\">\n    \u003Ch3 class=\"card-title\">Users\u003C/h3>\n  \u003C/div>\n  \u003Cdiv class=\"card-body\">\n    \u003C!-- content -->\n  \u003C/div>\n\u003C/div>\n\n\u003Cstyle>\n  .user-card {\n    margin: 20px;\n    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n  }\n  .card-title {\n    font-weight: 600;\n    color: white;\n  }\n\u003C/style>\n","html",[136,3598,3599,3620,3639,3667,3675,3694,3699,3707,3715,3719,3727,3737,3750,3788,3793,3802,3815,3828,3833],{"__ignoreMap":607},[1027,3600,3601,3603,3606,3609,3611,3613,3616,3618],{"class":1029,"line":1030},[1027,3602,1168],{"class":1051},[1027,3604,3605],{"class":1066},"div",[1027,3607,3608],{"class":1043}," class",[1027,3610,1809],{"class":1051},[1027,3612,2046],{"class":1051},[1027,3614,3615],{"class":2013},"card user-card",[1027,3617,2046],{"class":1051},[1027,3619,2868],{"class":1051},[1027,3621,3622,3624,3626,3628,3630,3632,3635,3637],{"class":1029,"line":608},[1027,3623,2873],{"class":1051},[1027,3625,3605],{"class":1066},[1027,3627,3608],{"class":1043},[1027,3629,1809],{"class":1051},[1027,3631,2046],{"class":1051},[1027,3633,3634],{"class":2013},"card-header bg-primary",[1027,3636,2046],{"class":1051},[1027,3638,2868],{"class":1051},[1027,3640,3641,3643,3645,3647,3649,3651,3654,3656,3658,3661,3663,3665],{"class":1029,"line":616},[1027,3642,2883],{"class":1051},[1027,3644,168],{"class":1066},[1027,3646,3608],{"class":1043},[1027,3648,1809],{"class":1051},[1027,3650,2046],{"class":1051},[1027,3652,3653],{"class":2013},"card-title",[1027,3655,2046],{"class":1051},[1027,3657,1174],{"class":1051},[1027,3659,3660],{"class":1073},"Users",[1027,3662,2967],{"class":1051},[1027,3664,168],{"class":1066},[1027,3666,2868],{"class":1051},[1027,3668,3669,3671,3673],{"class":1029,"line":1070},[1027,3670,2958],{"class":1051},[1027,3672,3605],{"class":1066},[1027,3674,2868],{"class":1051},[1027,3676,3677,3679,3681,3683,3685,3687,3690,3692],{"class":1029,"line":1090},[1027,3678,2873],{"class":1051},[1027,3680,3605],{"class":1066},[1027,3682,3608],{"class":1043},[1027,3684,1809],{"class":1051},[1027,3686,2046],{"class":1051},[1027,3688,3689],{"class":2013},"card-body",[1027,3691,2046],{"class":1051},[1027,3693,2868],{"class":1051},[1027,3695,3696],{"class":1029,"line":1110},[1027,3697,3698],{"class":1033},"    \u003C!-- content -->\n",[1027,3700,3701,3703,3705],{"class":1029,"line":1119},[1027,3702,2958],{"class":1051},[1027,3704,3605],{"class":1066},[1027,3706,2868],{"class":1051},[1027,3708,3709,3711,3713],{"class":1029,"line":1312},[1027,3710,2967],{"class":1051},[1027,3712,3605],{"class":1066},[1027,3714,2868],{"class":1051},[1027,3716,3717],{"class":1029,"line":1318},[1027,3718,1309],{"emptyLinePlaceholder":24},[1027,3720,3721,3723,3725],{"class":1029,"line":1356},[1027,3722,1168],{"class":1051},[1027,3724,2538],{"class":1066},[1027,3726,2868],{"class":1051},[1027,3728,3729,3732,3735],{"class":1029,"line":1384},[1027,3730,3731],{"class":1051},"  .",[1027,3733,3734],{"class":1164},"user-card",[1027,3736,1055],{"class":1051},[1027,3738,3739,3743,3745,3748],{"class":1029,"line":1409},[1027,3740,3742],{"class":3741},"sqsOY","    margin",[1027,3744,1161],{"class":1051},[1027,3746,3747],{"class":1734}," 20px",[1027,3749,1116],{"class":1051},[1027,3751,3752,3755,3757,3759,3762,3765,3768,3770,3772,3774,3776,3778,3780,3782,3785],{"class":1029,"line":1442},[1027,3753,3754],{"class":3741},"    box-shadow",[1027,3756,1161],{"class":1051},[1027,3758,1735],{"class":1734},[1027,3760,3761],{"class":1734}," 2px",[1027,3763,3764],{"class":1734}," 4px",[1027,3766,3767],{"class":1047}," rgba",[1027,3769,1223],{"class":1051},[1027,3771,1787],{"class":1734},[1027,3773,1186],{"class":1051},[1027,3775,1735],{"class":1734},[1027,3777,1186],{"class":1051},[1027,3779,1735],{"class":1734},[1027,3781,1186],{"class":1051},[1027,3783,3784],{"class":1734}," 0.1",[1027,3786,3787],{"class":1051},");\n",[1027,3789,3791],{"class":1029,"line":3790},14,[1027,3792,1565],{"class":1051},[1027,3794,3796,3798,3800],{"class":1029,"line":3795},15,[1027,3797,3731],{"class":1051},[1027,3799,3653],{"class":1164},[1027,3801,1055],{"class":1051},[1027,3803,3805,3808,3810,3813],{"class":1029,"line":3804},16,[1027,3806,3807],{"class":3741},"    font-weight",[1027,3809,1161],{"class":1051},[1027,3811,3812],{"class":1734}," 600",[1027,3814,1116],{"class":1051},[1027,3816,3818,3821,3823,3826],{"class":1029,"line":3817},17,[1027,3819,3820],{"class":3741},"    color",[1027,3822,1161],{"class":1051},[1027,3824,3825],{"class":1073}," white",[1027,3827,1116],{"class":1051},[1027,3829,3831],{"class":1029,"line":3830},18,[1027,3832,1565],{"class":1051},[1027,3834,3836,3838,3840],{"class":1029,"line":3835},19,[1027,3837,2967],{"class":1051},[1027,3839,2538],{"class":1066},[1027,3841,2868],{"class":1051},[42,3843,3844],{},[48,3845,3846],{},"New approach (Tailwind CSS):",[1019,3848,3850],{"className":3594,"code":3849,"language":3596,"meta":607,"style":607},"\u003Cdiv class=\"m-5 rounded-lg shadow-sm bg-white\">\n  \u003Cdiv class=\"bg-blue-600 p-4 rounded-t-lg\">\n    \u003Ch3 class=\"font-semibold text-white\">Users\u003C/h3>\n  \u003C/div>\n  \u003Cdiv class=\"p-4\">\n    \u003C!-- content -->\n  \u003C/div>\n\u003C/div>\n",[136,3851,3852,3871,3890,3917,3925,3944,3948,3956],{"__ignoreMap":607},[1027,3853,3854,3856,3858,3860,3862,3864,3867,3869],{"class":1029,"line":1030},[1027,3855,1168],{"class":1051},[1027,3857,3605],{"class":1066},[1027,3859,3608],{"class":1043},[1027,3861,1809],{"class":1051},[1027,3863,2046],{"class":1051},[1027,3865,3866],{"class":2013},"m-5 rounded-lg shadow-sm bg-white",[1027,3868,2046],{"class":1051},[1027,3870,2868],{"class":1051},[1027,3872,3873,3875,3877,3879,3881,3883,3886,3888],{"class":1029,"line":608},[1027,3874,2873],{"class":1051},[1027,3876,3605],{"class":1066},[1027,3878,3608],{"class":1043},[1027,3880,1809],{"class":1051},[1027,3882,2046],{"class":1051},[1027,3884,3885],{"class":2013},"bg-blue-600 p-4 rounded-t-lg",[1027,3887,2046],{"class":1051},[1027,3889,2868],{"class":1051},[1027,3891,3892,3894,3896,3898,3900,3902,3905,3907,3909,3911,3913,3915],{"class":1029,"line":616},[1027,3893,2883],{"class":1051},[1027,3895,168],{"class":1066},[1027,3897,3608],{"class":1043},[1027,3899,1809],{"class":1051},[1027,3901,2046],{"class":1051},[1027,3903,3904],{"class":2013},"font-semibold text-white",[1027,3906,2046],{"class":1051},[1027,3908,1174],{"class":1051},[1027,3910,3660],{"class":1073},[1027,3912,2967],{"class":1051},[1027,3914,168],{"class":1066},[1027,3916,2868],{"class":1051},[1027,3918,3919,3921,3923],{"class":1029,"line":1070},[1027,3920,2958],{"class":1051},[1027,3922,3605],{"class":1066},[1027,3924,2868],{"class":1051},[1027,3926,3927,3929,3931,3933,3935,3937,3940,3942],{"class":1029,"line":1090},[1027,3928,2873],{"class":1051},[1027,3930,3605],{"class":1066},[1027,3932,3608],{"class":1043},[1027,3934,1809],{"class":1051},[1027,3936,2046],{"class":1051},[1027,3938,3939],{"class":2013},"p-4",[1027,3941,2046],{"class":1051},[1027,3943,2868],{"class":1051},[1027,3945,3946],{"class":1029,"line":1110},[1027,3947,3698],{"class":1033},[1027,3949,3950,3952,3954],{"class":1029,"line":1119},[1027,3951,2958],{"class":1051},[1027,3953,3605],{"class":1066},[1027,3955,2868],{"class":1051},[1027,3957,3958,3960,3962],{"class":1029,"line":1312},[1027,3959,2967],{"class":1051},[1027,3961,3605],{"class":1066},[1027,3963,2868],{"class":1051},[42,3965,3966],{},[48,3967,3034],{},[173,3969,3970,3973,3976,3979],{},[176,3971,3972],{},"No custom CSS files to maintain",[176,3974,3975],{},"Consistent spacing/colors (design system)",[176,3977,3978],{},"Faster to write (utility classes)",[176,3980,3981],{},"Smaller bundle size (unused classes purged)",[52,3983],{},[37,3985,2144],{"id":2143},[168,3987,3989],{"id":3988},"_1-the-power-of-typescript","1. The Power of TypeScript",[42,3991,3992],{},"I didn't use TypeScript in my capstone. Now I can't imagine building without it.",[42,3994,3995],{},[48,3996,3997],{},"Real example from this rebuild:",[42,3999,4000],{},"I was building the user management table. In vanilla JS, I kept making mistakes:",[173,4002,4003,4013,4023],{},[176,4004,4005,4006,4009,4010],{},"Accessing ",[136,4007,4008],{},"user.fullName"," when it was actually ",[136,4011,4012],{},"user.full_name",[176,4014,4015,4016,4019,4020],{},"Trying to display ",[136,4017,4018],{},"ride.driver"," when it was sometimes ",[136,4021,4022],{},"null",[176,4024,4025],{},"Not knowing what properties existed without checking the API response",[42,4027,4028,4029,4032],{},"TypeScript caught all of these ",[48,4030,4031],{},"before I even ran the code",". That's powerful.",[168,4034,4036],{"id":4035},"_2-component-based-thinking","2. Component-Based Thinking",[42,4038,4039],{},"In my original capstone, every page was a separate PHP file with duplicated code:",[173,4041,4042,4048,4054],{},[176,4043,4044,4047],{},[136,4045,4046],{},"users.php"," had a table",[176,4049,4050,4053],{},[136,4051,4052],{},"drivers.php"," had the same table with different data",[176,4055,4056,4059],{},[136,4057,4058],{},"rides.php"," had the same table again",[42,4061,4062,4063,4066,4067,4070],{},"In Vue, I built ",[48,4064,4065],{},"one"," ",[136,4068,4069],{},"\u003CDataTable>"," component and reused it everywhere with\ndifferent data. Change one component, update all pages. That's the power of\ncomponents.",[168,4072,4074],{"id":4073},"_3-state-management-at-scale","3. State Management at Scale",[42,4076,4077],{},"In vanilla JS, I had data scattered everywhere:",[1019,4079,4081],{"className":3060,"code":4080,"language":3062,"meta":607,"style":607},"var currentUser = null;\nvar allRides = [];\nvar filteredRides = [];\nvar selectedRide = null;\n",[136,4082,4083,4087,4092,4097],{"__ignoreMap":607},[1027,4084,4085],{"class":1029,"line":1030},[1027,4086,3074],{},[1027,4088,4089],{"class":1029,"line":608},[1027,4090,4091],{},"var allRides = [];\n",[1027,4093,4094],{"class":1029,"line":616},[1027,4095,4096],{},"var filteredRides = [];\n",[1027,4098,4099],{"class":1029,"line":1070},[1027,4100,4101],{},"var selectedRide = null;\n",[42,4103,4104],{},"With Pinia, everything is organized:",[1019,4106,4108],{"className":2849,"code":4107,"language":2851,"meta":607,"style":607},"useAuthStore(); // currentUser\nuseRidesStore(); // allRides, filteredRides, selectedRide\n",[136,4109,4110,4122],{"__ignoreMap":607},[1027,4111,4112,4115,4117,4119],{"class":1029,"line":1030},[1027,4113,4114],{"class":1047},"useAuthStore",[1027,4116,1052],{"class":1073},[1027,4118,3017],{"class":1051},[1027,4120,4121],{"class":1033}," // currentUser\n",[1027,4123,4124,4127,4129,4131],{"class":1029,"line":608},[1027,4125,4126],{"class":1047},"useRidesStore",[1027,4128,1052],{"class":1073},[1027,4130,3017],{"class":1051},[1027,4132,4133],{"class":1033}," // allRides, filteredRides, selectedRide\n",[42,4135,4136],{},"One source of truth. Predictable updates. Way easier to debug.",[168,4138,4140],{"id":4139},"_4-modern-css-is-fast","4. Modern CSS is Fast",[42,4142,4143,4144,4147,4148,340],{},"Tailwind CSS let me recreate the ",[48,4145,4146],{},"exact same UI"," in about ",[48,4149,4150],{},"1/3 the time",[42,4152,4153],{},"Instead of:",[654,4155,4156,4159,4162,4165,4168,4171],{},[176,4157,4158],{},"Write HTML",[176,4160,4161],{},"Add class names",[176,4163,4164],{},"Switch to CSS file",[176,4166,4167],{},"Write styles",[176,4169,4170],{},"Go back to HTML",[176,4172,4173],{},"Adjust class names",[42,4175,4176],{},"I just:",[654,4178,4179,4182],{},[176,4180,4181],{},"Write HTML with utility classes",[176,4183,4184],{},"Done",[42,4186,4187,4188,4191],{},"The final bundle is actually ",[48,4189,4190],{},"smaller"," because Tailwind purges unused classes.",[168,4193,4195],{"id":4194},"_5-the-value-of-rebuilding","5. The Value of Rebuilding",[42,4197,4198,4199,340],{},"This wasn't just a \"portfolio project.\" It was a ",[48,4200,4201],{},"learning checkpoint",[42,4203,4204],{},"By rebuilding something I knew intimately, I could focus on:",[173,4206,4207,4217,4223],{},[176,4208,4209,4212,4213,4216],{},[48,4210,4211],{},"How"," to solve problems, not ",[48,4214,4215],{},"what"," to build",[176,4218,4219,4222],{},[48,4220,4221],{},"Why"," modern patterns are better, not just that they exist",[176,4224,4225,4228],{},[48,4226,4227],{},"Where"," I've grown as a developer",[42,4230,4231,4232,4235,4236,4238,4239,4242],{},"The original took me ",[48,4233,4234],{},"2 months"," in 2023.",[988,4237],{},"\nThis rebuild took me ",[48,4240,4241],{},"2 weeks"," in 2026.",[42,4244,4245],{},"That's growth.",[52,4247],{},[37,4249,2282],{"id":2281},[168,4251,4253],{"id":4252},"before-2023-phpvanilla-js","Before (2023 - PHP/Vanilla JS)",[173,4255,4256,4259,4262,4265,4268],{},[176,4257,4258],{},"~2500 lines of mixed HTML/PHP/JavaScript",[176,4260,4261],{},"No type safety",[176,4263,4264],{},"Manual DOM manipulation",[176,4266,4267],{},"Duplicated code across pages",[176,4269,4270],{},"Hard to maintain and extend",[168,4272,4274],{"id":4273},"after-2026-nuxt-4typescript","After (2026 - Nuxt 4/TypeScript)",[173,4276,4277,4280,4283,4286,4289,4292,4295],{},[176,4278,4279],{},"~1400 lines of organized, typed code",[176,4281,4282],{},"Full TypeScript type safety",[176,4284,4285],{},"Component-based architecture",[176,4287,4288],{},"Reactive state management",[176,4290,4291],{},"Pinia for predictable data flow",[176,4293,4294],{},"Tailwind for consistent styling",[176,4296,4297],{},"Easy to maintain and extend",[168,4299,4301],{"id":4300},"what-this-proves","What This Proves",[42,4303,4304,4305,4308],{},"Not just that I can learn new tech, but that I understand ",[48,4306,4307],{},"why"," modern\npatterns exist.",[42,4310,4311],{},"I didn't just follow a tutorial. I rebuilt something I knew deeply and saw\nthe improvements firsthand:",[173,4313,4314,4317,4320,4323],{},[176,4315,4316],{},"Faster development",[176,4318,4319],{},"Fewer bugs",[176,4321,4322],{},"Easier maintenance",[176,4324,4325],{},"Better developer experience",[52,4327],{},[37,4329,2373],{"id":2372},[42,4331,4332,4335,4336,4338,4341,4342,4344,4346,4347,4349,4352,4353,4355,4357,4358,4360,4363,4364,4366,4369],{},[48,4333,4334],{},"Frontend Framework:"," Nuxt 4",[988,4337],{},[48,4339,4340],{},"UI Framework:"," Vue 3 (Composition API)",[988,4343],{},[48,4345,186],{}," TypeScript",[988,4348],{},[48,4350,4351],{},"State Management:"," Pinia",[988,4354],{},[48,4356,2384],{}," Tailwind CSS",[988,4359],{},[48,4361,4362],{},"Icons:"," Lucide Vue Next",[988,4365],{},[48,4367,4368],{},"API Client:"," Custom composables with error handling",[52,4371],{},[37,4373,4375],{"id":4374},"try-it-yourself","TRY IT YOURSELF",[42,4377,4378,4385,4386],{},[48,4379,4380],{},[601,4381,4384],{"href":4382,"rel":4383},"https://csa-management-system.vercel.app/",[605],"View Live Demo","   ",[48,4387,4388],{},[601,4389,4392],{"href":4390,"rel":4391},"https://github.com/aarunzetta/csa-capstoneVersion",[605],"View Source",[52,4394],{},[37,4396,4398],{"id":4397},"what-id-do-differently-next-time","WHAT I'D DO DIFFERENTLY NEXT TIME",[42,4400,4401],{},"If I rebuilt this again today:",[42,4403,4404,4407],{},[48,4405,4406],{},"Add Real-Time Updates","\nUse WebSockets instead of polling for truly real-time ride tracking",[42,4409,4410,4413],{},[48,4411,4412],{},"Implement E2E Testing","\nAdd Playwright tests to ensure features don't break",[42,4415,4416,4419],{},[48,4417,4418],{},"Add Data Visualization","\nMore interactive charts using D3.js or Chart.js",[42,4421,4422,4425],{},[48,4423,4424],{},"Optimize Bundle Size","\nImplement lazy loading for routes and components",[42,4427,4428,4431],{},[48,4429,4430],{},"Add Offline Support","\nService workers for offline dashboard access",[42,4433,4434],{},"But honestly? For a personal learning project, I'm proud of where it is.",[52,4436],{},[37,4438,2499],{"id":2498},[42,4440,4441,4442,1161],{},"This project represents ",[48,4443,4444],{},"3 years of growth",[42,4446,4447,4450,4451,4453,4456,4457,4459,4462],{},[48,4448,4449],{},"2023:"," Built my capstone with the tools I knew",[988,4452],{},[48,4454,4455],{},"2024:"," Learned modern frameworks professionally",[988,4458],{},[48,4460,4461],{},"2026:"," Rebuilt my capstone with everything I've learned",[42,4464,4465],{},"The cycle continues. What will I know in 2029 that makes this rebuild look\noutdated?",[42,4467,4468],{},"That's the exciting part—there's always more to learn.",[52,4470],{},[42,4472,4473],{},[1027,4474,4475],{},"Want to discuss modern refactoring or TypeScript patterns? Let's connect →",[2538,4477,4478],{},"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 .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 .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}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 .sqsOY, html code.shiki .sqsOY{--shiki-light:#8796B0;--shiki-default:#B2CCD6;--shiki-dark:#B2CCD6}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":607,"searchDepth":608,"depth":608,"links":4480},[4481,4482,4485,4489,4495,4502,4507,4508,4509,4510],{"id":648,"depth":608,"text":649},{"id":689,"depth":608,"text":690,"children":4483},[4484],{"id":2667,"depth":616,"text":2668},{"id":801,"depth":608,"text":802,"children":4486},[4487,4488],{"id":2758,"depth":616,"text":2759},{"id":2770,"depth":616,"text":2771},{"id":165,"depth":608,"text":166,"children":4490},[4491,4492,4493,4494],{"id":2784,"depth":616,"text":2785},{"id":3051,"depth":616,"text":3052},{"id":3359,"depth":616,"text":3360},{"id":3585,"depth":616,"text":3586},{"id":2143,"depth":608,"text":2144,"children":4496},[4497,4498,4499,4500,4501],{"id":3988,"depth":616,"text":3989},{"id":4035,"depth":616,"text":4036},{"id":4073,"depth":616,"text":4074},{"id":4139,"depth":616,"text":4140},{"id":4194,"depth":616,"text":4195},{"id":2281,"depth":608,"text":2282,"children":4503},[4504,4505,4506],{"id":4252,"depth":616,"text":4253},{"id":4273,"depth":616,"text":4274},{"id":4300,"depth":616,"text":4301},{"id":2372,"depth":608,"text":2373},{"id":4374,"depth":608,"text":4375},{"id":4397,"depth":608,"text":4398},{"id":2498,"depth":608,"text":2499},"From PHP/vanilla JS to Nuxt 4/TypeScript—what I learned by recreating my own work","/projects/commuter-security-app.png",{},"/projects/three-years-later-rebuilding-my-college-capstone",{"title":2584,"description":4511},"projects/three-years-later-rebuilding-my-college-capstone",[4518,633,4519,637,4520,4521],"Nuxt.js","Pinia","Express.js","MySQL","koKSodhS7uw_TxzKMmEtKYZXY1r9FdBBxrU7C_ROTgQ",{"id":4524,"title":4525,"body":4526,"buttonLabel":17,"date":5264,"description":5265,"extension":625,"image":5266,"meta":5267,"navigation":24,"path":5268,"seo":5269,"stem":5270,"tags":5271,"__hash__":5274},"projects/projects/crm-platform.md","Enterprise CRM Platform for Advertising Sales",{"type":34,"value":4527,"toc":5246},[4528,4532,4539,4545,4547,4551,4558,4594,4600,4602,4604,4607,4612,4623,4628,4639,4644,4651,4657,4662,4679,4684,4690,4693,4698,4712,4717,4728,4730,4732,4736,4768,4771,4776,4779,4787,4794,4869,4872,4877,4880,4891,4896,4901,4904,4930,4935,4938,4949,4952,4954,4958,4962,4994,4997,5004,5036,5038,5040,5073,5075,5077,5082,5085,5099,5110,5115,5118,5144,5150,5155,5158,5169,5176,5181,5192,5198,5200,5202,5215,5218,5232,5238,5241,5243],[37,4529,4531],{"id":4530},"the-context","THE CONTEXT",[42,4533,4534,4535,4538],{},"Summit Media's advertising sales team was managing campaigns across ",[48,4536,4537],{},"20+ industries"," using spreadsheets and a legacy platform. Creating proposals, tracking versions, and generating reports was manual and error-prone.",[42,4540,4541,4544],{},[48,4542,4543],{},"The stakes:"," Ad sales is revenue. Slow tools slow down the entire sales process.",[52,4546],{},[37,4548,4550],{"id":4549},"the-challenge-and-why-it-was-brutal","THE CHALLENGE (And Why It Was Brutal)",[42,4552,4553,4554,4557],{},"This was my ",[48,4555,4556],{},"biggest technical challenge"," to date:",[173,4559,4560,4567,4574,4580,4587],{},[176,4561,4562,4563,4566],{},"⏰ ",[48,4564,4565],{},"Tight deadlines"," - Fast-paced, production-critical",[176,4568,4569,4570,4573],{},"🎯 ",[48,4571,4572],{},"Solo frontend dev"," - I was responsible for implementing most of the UI/UX, translating provided mockups into a functional system",[176,4575,698,4576,4579],{},[48,4577,4578],{},"First-time everything"," - Most patterns were new to me",[176,4581,4582,4583,4586],{},"🎨 ",[48,4584,4585],{},"Complex UI"," - 20+ industry filters, dynamic tables, conditional actions",[176,4588,4589,4590,4593],{},"🔗 ",[48,4591,4592],{},"Heavy API integration"," - Real-time data syncing",[42,4595,4596,4599],{},[48,4597,4598],{},"Translation:"," I had to learn while building, fast.",[52,4601],{},[37,4603,802],{"id":801},[168,4605,4606],{"id":805},"Core Features:",[42,4608,4609],{},[48,4610,4611],{},"1. Campaign Management System",[173,4613,4614,4617,4620],{},[176,4615,4616],{},"Create, track, and manage campaigns across multiple industries",[176,4618,4619],{},"Real-time cost estimation and proposal generation",[176,4621,4622],{},"Version control for proposals (sales teams iterate constantly)",[42,4624,4625],{},[48,4626,4627],{},"2. Advanced Filtering System",[173,4629,4630,4633,4636],{},[176,4631,4632],{},"20+ industry categories with nested filters",[176,4634,4635],{},"Search across campaigns, proposals, and cost estimation",[176,4637,4638],{},"Instant results with performance optimization",[42,4640,4641],{},[48,4642,4643],{},"3. Dynamic Table System with Smart Actions",[42,4645,4646,4647,4650],{},"This was the ",[48,4648,4649],{},"heart of the CRM"," and my proudest technical achievement:",[42,4652,4653,4656],{},[48,4654,4655],{},"The Challenge:"," Different campaign statuses require different actions. A draft proposal needs \"Create Proposal,\" an approved proposal needs \"Create CE,\" a completed campaign needs \"Create Job Order\"—and the UI had to make this intuitive.",[42,4658,4659],{},[48,4660,4661],{},"What I Built:",[173,4663,4664,4667,4673,4676],{},[176,4665,4666],{},"Multiple interconnected tables (Campaigns, Proposals, Cost Estimate, Job Orders)",[176,4668,4669,4672],{},[48,4670,4671],{},"Status-driven action system"," - buttons and options that dynamically change based on campaign state",[176,4674,4675],{},"Complex conditional logic determining what actions are available",[176,4677,4678],{},"Real-time status updates reflected across all related tables",[42,4680,4681],{},[48,4682,4683],{},"Example Flow:",[1019,4685,4688],{"className":4686,"code":4687,"language":2765},[2763],"Draft → [Edit | Create Proposal]\n  ↓\nProposal → [Create another version | Create CE | Edit | Generate PDF]\n  ↓\nCost Estimate → [Create another version | Create Job Order | Generate PDF]\n  ↓\nJob Order → [Publish | Add Notes | Start Campaign | Stop Campaign]\n  ↓\nCompleted → [Generate Invoice | Archive | View Report]\n",[136,4689,4687],{"__ignoreMap":607},[42,4691,4692],{},"Each status transition triggered different API calls, updated multiple tables, and recalculated related data in real-time.",[42,4694,4695],{},[48,4696,4697],{},"Technical Complexity:",[173,4699,4700,4703,4706,4709],{},[176,4701,4702],{},"Conditional rendering based on user permissions AND campaign status",[176,4704,4705],{},"Optimistic UI updates (show changes immediately, sync with backend)",[176,4707,4708],{},"Cascading updates (changing one campaign affects related proposals, job orders)",[176,4710,4711],{},"State synchronization across multiple table views",[42,4713,4714],{},[48,4715,4716],{},"4. PDF Generation",[173,4718,4719,4722,4725],{},[176,4720,4721],{},"One-click proposal and invoice exports",[176,4723,4724],{},"Professional formatting with company branding",[176,4726,4727],{},"Dynamic content based on campaign configuration",[52,4729],{},[37,4731,166],{"id":165},[168,4733,4735],{"id":4734},"my-responsibilities","My Responsibilities:",[42,4737,4738,4739,4742,4743,4745,4746,4749,4750,4745,4752,4755,4756,4745,4758,4761,4762,4745,4764,4767],{},"✅ ",[48,4740,4741],{},"Frontend Architecture"," - Component structure and routing",[988,4744],{},"\n✅ ",[48,4747,4748],{},"UI/UX Implementation"," - Pixel-perfect from Figma designs",[988,4751],{},[48,4753,4754],{},"API Integration"," - RESTful endpoints, state synchronization",[988,4757],{},[48,4759,4760],{},"Business Logic"," - Calculations, validations, conditional flows",[988,4763],{},[48,4765,4766],{},"State Management"," - Pinia for complex, multi-view data",[168,4769,4770],{"id":226},"Hardest Technical Problems:",[42,4772,4773],{},[48,4774,4775],{},"Problem 1: Status-Dependent Action System",[42,4777,4778],{},"The most complex part was managing actions that depend on:",[173,4780,4781,4784],{},[176,4782,4783],{},"Campaign status (Draft, In Progress, For Proposal, Proposal Sent, etc)",[176,4785,4786],{},"Related data state (Does this campaign have a proposal? cost estimate?)",[42,4788,4789,4790,4793],{},"I built a ",[48,4791,4792],{},"computed action system"," that evaluates all conditions and renders only valid actions:",[1019,4795,4797],{"className":3060,"code":4796,"language":3062,"meta":607,"style":607},"// Simplified example of the pattern I used\nconst availableActions = computed(() => {\n  const actions = [];\n\n  if (campaign.status === \"draft\" && hasPermission(\"edit\")) {\n    actions.push({ label: \"Edit\", handler: editCampaign });\n  }\n\n  if (campaign.status === \"approved\" && !campaign.hasJobOrder) {\n    actions.push({ label: \"Create Job Order\", handler: createJobOrder });\n  }\n\n  // ... many more conditions\n  return actions;\n});\n",[136,4798,4799,4804,4809,4814,4818,4823,4828,4832,4836,4841,4846,4850,4854,4859,4864],{"__ignoreMap":607},[1027,4800,4801],{"class":1029,"line":1030},[1027,4802,4803],{},"// Simplified example of the pattern I used\n",[1027,4805,4806],{"class":1029,"line":608},[1027,4807,4808],{},"const availableActions = computed(() => {\n",[1027,4810,4811],{"class":1029,"line":616},[1027,4812,4813],{},"  const actions = [];\n",[1027,4815,4816],{"class":1029,"line":1070},[1027,4817,1309],{"emptyLinePlaceholder":24},[1027,4819,4820],{"class":1029,"line":1090},[1027,4821,4822],{},"  if (campaign.status === \"draft\" && hasPermission(\"edit\")) {\n",[1027,4824,4825],{"class":1029,"line":1110},[1027,4826,4827],{},"    actions.push({ label: \"Edit\", handler: editCampaign });\n",[1027,4829,4830],{"class":1029,"line":1119},[1027,4831,1565],{},[1027,4833,4834],{"class":1029,"line":1312},[1027,4835,1309],{"emptyLinePlaceholder":24},[1027,4837,4838],{"class":1029,"line":1318},[1027,4839,4840],{},"  if (campaign.status === \"approved\" && !campaign.hasJobOrder) {\n",[1027,4842,4843],{"class":1029,"line":1356},[1027,4844,4845],{},"    actions.push({ label: \"Create Job Order\", handler: createJobOrder });\n",[1027,4847,4848],{"class":1029,"line":1384},[1027,4849,1565],{},[1027,4851,4852],{"class":1029,"line":1409},[1027,4853,1309],{"emptyLinePlaceholder":24},[1027,4855,4856],{"class":1029,"line":1442},[1027,4857,4858],{},"  // ... many more conditions\n",[1027,4860,4861],{"class":1029,"line":3790},[1027,4862,4863],{},"  return actions;\n",[1027,4865,4866],{"class":1029,"line":3795},[1027,4867,4868],{},"});\n",[42,4870,4871],{},"This kept the UI clean while handling incredibly complex business logic.",[42,4873,4874],{},[48,4875,4876],{},"Problem 2: Real-Time Data Synchronization",[42,4878,4879],{},"Multiple users editing campaigns simultaneously required:",[173,4881,4882,4885,4888],{},[176,4883,4884],{},"Optimistic updates (show changes immediately)",[176,4886,4887],{},"Conflict resolution when data changes server-side",[176,4889,4890],{},"Automatic table refreshes without losing user context (filters, sort, pagination)",[42,4892,4893,4895],{},[48,4894,268],{}," Built a smart polling system that checks for updates without disrupting user workflow.",[42,4897,4898],{},[48,4899,4900],{},"Problem 3: Performance with Large Datasets",[42,4902,4903],{},"Rendering 100+ campaigns with filters, search, and dynamic actions without lag required:",[173,4905,4906,4912,4918,4924],{},[176,4907,4908,4911],{},[48,4909,4910],{},"Virtual scrolling"," for large tables",[176,4913,4914,4917],{},[48,4915,4916],{},"Debounced search"," inputs (wait 300ms after typing stops)",[176,4919,4920,4923],{},[48,4921,4922],{},"Optimized computed properties"," (cache results, only recalculate when dependencies change)",[176,4925,4926,4929],{},[48,4927,4928],{},"Lazy loading"," related data (load job orders only when row is expanded)",[42,4931,4932],{},[48,4933,4934],{},"Problem 4: Complex Form Validation",[42,4936,4937],{},"Campaign creation had interdependent fields:",[173,4939,4940,4943,4946],{},[176,4941,4942],{},"Industry selection → determines available ad types",[176,4944,4945],{},"Location selection → determines pricing structure",[176,4947,4948],{},"Pricing changes → recalculates total cost in real-time",[42,4950,4951],{},"All while maintaining a smooth UX and clear error messages.",[52,4953],{},[37,4955,4957],{"id":4956},"the-impact","THE IMPACT",[168,4959,4961],{"id":4960},"for-the-sales-team","For the Sales Team:",[173,4963,4964,4970,4976,4982,4988],{},[176,4965,4738,4966,4969],{},[48,4967,4968],{},"Faster proposal creation"," (estimated 60% time savings)",[176,4971,4738,4972,4975],{},[48,4973,4974],{},"Better version tracking"," (no more \"Final_v3_REAL_Final.xlsx\")",[176,4977,4738,4978,4981],{},[48,4979,4980],{},"Clear action visibility"," - always know what you can do next",[176,4983,4738,4984,4987],{},[48,4985,4986],{},"Professional PDF exports"," for clients",[176,4989,4738,4990,4993],{},[48,4991,4992],{},"Real-time pipeline visibility"," across all campaigns",[168,4995,4996],{"id":2331},"For Me:",[42,4998,4999,5000,5003],{},"This project ",[48,5001,5002],{},"leveled me up"," as a developer:",[173,5005,5006,5012,5018,5024,5030],{},[176,5007,5008,5011],{},[48,5009,5010],{},"Learned to work under pressure"," without sacrificing quality",[176,5013,5014,5017],{},[48,5015,5016],{},"Gained confidence in complex state management"," (Pinia for multi-view data)",[176,5019,5020,5023],{},[48,5021,5022],{},"Mastered conditional logic"," at scale (status-driven UIs are everywhere now)",[176,5025,5026,5029],{},[48,5027,5028],{},"Understood how frontend decisions impact business"," (slow CRM = lost revenue)",[176,5031,5032,5035],{},[48,5033,5034],{},"Discovered I thrive in fast-paced environments"," (tight deadlines became my fuel)",[52,5037],{},[37,5039,2373],{"id":2372},[173,5041,5042,5047,5051,5056,5062,5068],{},[176,5043,5044,5046],{},[48,5045,2378],{}," Vue 3 (Composition API), Nuxt 3",[176,5048,5049,4352],{},[48,5050,4351],{},[176,5052,5053,4357],{},[48,5054,5055],{},"UI:",[176,5057,5058,5061],{},[48,5059,5060],{},"Tables:"," Custom-built with virtual scrolling",[176,5063,5064,5067],{},[48,5065,5066],{},"PDF:"," API-generated PDFs with in-app modal viewer",[176,5069,5070,5072],{},[48,5071,2420],{}," RESTful integration with Node.js backend",[52,5074],{},[37,5076,522],{"id":521},[42,5078,5079],{},[48,5080,5081],{},"1. You Don't Need to Know Everything to Start",[42,5083,5084],{},"Most patterns were new to me:",[173,5086,5087,5090,5093,5096],{},[176,5088,5089],{},"Complex state management across multiple views",[176,5091,5092],{},"Conditional action systems",[176,5094,5095],{},"Virtual scrolling for performance",[176,5097,5098],{},"Real-time data synchronization",[42,5100,5101,5102,5105,5106,5109],{},"I used ",[48,5103,5104],{},"AI tools"," (Claude, ChatGPT) to accelerate learning, but I made sure to ",[48,5107,5108],{},"understand"," every solution, not just copy-paste. I'd ask \"Why does this pattern work?\" not just \"Give me the code.\"",[42,5111,5112],{},[48,5113,5114],{},"2. Clean Code Under Pressure is Possible",[42,5116,5117],{},"Even with tight deadlines, I prioritized:",[173,5119,5120,5132,5138],{},[176,5121,5122,134,5125,5128,5129,1197],{},[48,5123,5124],{},"Meaningful component names",[136,5126,5127],{},"CampaignActionButton"," not ",[136,5130,5131],{},"Button2",[176,5133,5134,5137],{},[48,5135,5136],{},"Clear state structure"," (organized Pinia stores by feature)",[176,5139,5140,5143],{},[48,5141,5142],{},"Comments on complex logic"," (especially the status-action mapping)",[42,5145,5146,5149],{},[48,5147,5148],{},"Why?"," Other devs need to maintain this. Rushing today creates problems tomorrow. Plus, clean code is faster to debug when (not if) bugs appear.",[42,5151,5152],{},[48,5153,5154],{},"3. Frontend Impacts Revenue",[42,5156,5157],{},"A slow, buggy CRM doesn't just annoy users—it costs money:",[173,5159,5160,5163,5166],{},[176,5161,5162],{},"Slow filters = sales team wastes time",[176,5164,5165],{},"Broken action buttons = proposals don't get sent",[176,5167,5168],{},"Confusing UI = mistakes in pricing",[42,5170,5171,5172,5175],{},"This shifted how I think about ",[48,5173,5174],{},"performance and UX as business priorities",", not just nice-to-haves.",[42,5177,5178],{},[48,5179,5180],{},"4. Complex UIs Need Simple Patterns",[42,5182,5183,5184,5187,5188,5191],{},"The status-action system could have been a nightmare of nested ",[136,5185,5186],{},"if"," statements. Instead, I built a ",[48,5189,5190],{},"declarative pattern"," that made adding new statuses or actions predictable and safe.",[42,5193,5194,5197],{},[48,5195,5196],{},"Key principle:"," Make the complex logic look simple in the code.",[52,5199],{},[37,5201,2499],{"id":2498},[42,5203,5204,5205,5208,5209,5212,5213,340],{},"This CRM manages ",[48,5206,5207],{},"100+ campaigns"," for ",[48,5210,5211],{},"15+ sales team members"," across ",[48,5214,4537],{},[42,5216,5217],{},"But more than that, it taught me:",[173,5219,5220,5223,5226,5229],{},[176,5221,5222],{},"How to deliver under pressure",[176,5224,5225],{},"How to learn while building",[176,5227,5228],{},"How frontend choices impact real business outcomes",[176,5230,5231],{},"That I love complex, high-stakes projects",[42,5233,5234,5237],{},[48,5235,5236],{},"This is the project that made me want to learn backend","—because I kept thinking, \"How does the API handle this? Could I build a better sync system?\"",[42,5239,5240],{},"Now I'm learning Node.js, Express, and database design to answer those questions.",[52,5242],{},[2538,5244,5245],{},"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);}",{"title":607,"searchDepth":608,"depth":608,"links":5247},[5248,5249,5250,5253,5257,5261,5262,5263],{"id":4530,"depth":608,"text":4531},{"id":4549,"depth":608,"text":4550},{"id":801,"depth":608,"text":802,"children":5251},[5252],{"id":805,"depth":616,"text":4606},{"id":165,"depth":608,"text":166,"children":5254},[5255,5256],{"id":4734,"depth":616,"text":4735},{"id":226,"depth":616,"text":4770},{"id":4956,"depth":608,"text":4957,"children":5258},[5259,5260],{"id":4960,"depth":616,"text":4961},{"id":2331,"depth":616,"text":4996},{"id":2372,"depth":608,"text":2373},{"id":521,"depth":608,"text":522},{"id":2498,"depth":608,"text":2499},"2025","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.","/projects/crm.png",{},"/projects/crm-platform",{"title":4525,"description":5265},"projects/crm-platform",[5272,4518,4519,637,5273],"Vue.js","API integration","psi-OYfWrxFMfHzack2EKqWbbK5vsaVqGGSmT8DJWf0",{"id":5276,"title":5277,"body":5278,"buttonLabel":2570,"date":5561,"description":5562,"extension":625,"image":5563,"meta":5564,"navigation":24,"path":5565,"seo":5566,"stem":5567,"tags":5568,"__hash__":5570},"projects/projects/dynamic-page-builder.md","Dynamic Content Page Builder",{"type":34,"value":5279,"toc":5548},[5280,5282,5285,5288,5294,5296,5300,5303,5307,5312,5323,5328,5339,5344,5351,5365,5372,5376,5415,5417,5419,5423,5443,5447,5450,5452,5454,5457,5463,5469,5479,5489,5500,5503,5509,5520,5522,5524,5544,5546],[37,5281,690],{"id":689},[42,5283,5284],{},"At Summit Media, every article looked the same. Writers were locked into rigid layouts—no matter if they were writing a photo essay, a breaking news story, or an in-depth feature. They all got the same template.",[42,5286,5287],{},"The problem? Writers are creative. They had visions for how their stories should look, but no way to execute them without waiting on developers.",[42,5289,5290,5293],{},[48,5291,5292],{},"Our mission:"," Give them the tools to build their own story layouts.",[52,5295],{},[37,5297,5299],{"id":5298},"the-solution","THE SOLUTION",[42,5301,5302],{},"We built a component-based page builder with 15+ reusable blocks that writers could mix and match:",[168,5304,5306],{"id":5305},"technical-approach","Technical Approach",[42,5308,5309],{},[48,5310,5311],{},"Architecture Decision:",[173,5313,5314,5317,5320],{},[176,5315,5316],{},"Component-driven design using Vue 3 Composition API",[176,5318,5319],{},"Centralized state management with Pinia for layout configuration",[176,5321,5322],{},"JSON schema storage for each article's unique layout",[42,5324,5325],{},[48,5326,5327],{},"Key Components I Built:",[173,5329,5330,5333,5336],{},[176,5331,5332],{},"📸 Photo & Video Carousel with inline media player",[176,5334,5335],{},"🎯 Pull quotes and callouts",[176,5337,5338],{},"🔗 Interactive embed blocks",[42,5340,5341],{},[48,5342,5343],{},"The Hardest Part (and what I'm proudest of):",[42,5345,5346,5347,5350],{},"The ",[48,5348,5349],{},"state management"," was complex. Each component needed to:",[173,5352,5353,5356,5359,5362],{},[176,5354,5355],{},"Maintain its own configuration",[176,5357,5358],{},"Communicate with parent layout",[176,5360,5361],{},"Preserve state during drag/reorder operations",[176,5363,5364],{},"Render consistently across edit/preview modes",[42,5366,5367,5368,5371],{},"I also built a ",[48,5369,5370],{},"custom media carousel"," that handles both photos and videos with smooth transitions and inline playback—this was my first time building something like this from scratch.",[168,5373,5375],{"id":5374},"code-philosophy","Code Philosophy",[1019,5377,5379],{"className":3060,"code":5378,"language":3062,"meta":607,"style":607},"// I focused on making components reusable and predictable\n// Each block follows the same contract:\nconst ComponentBlock = {\n  props: [\"config\", \"content\", \"isEditing\"],\n  emits: [\"update:config\", \"update:content\"],\n  // Clean, consistent interface for every component\n};\n",[136,5380,5381,5386,5391,5396,5401,5406,5411],{"__ignoreMap":607},[1027,5382,5383],{"class":1029,"line":1030},[1027,5384,5385],{},"// I focused on making components reusable and predictable\n",[1027,5387,5388],{"class":1029,"line":608},[1027,5389,5390],{},"// Each block follows the same contract:\n",[1027,5392,5393],{"class":1029,"line":616},[1027,5394,5395],{},"const ComponentBlock = {\n",[1027,5397,5398],{"class":1029,"line":1070},[1027,5399,5400],{},"  props: [\"config\", \"content\", \"isEditing\"],\n",[1027,5402,5403],{"class":1029,"line":1090},[1027,5404,5405],{},"  emits: [\"update:config\", \"update:content\"],\n",[1027,5407,5408],{"class":1029,"line":1110},[1027,5409,5410],{},"  // Clean, consistent interface for every component\n",[1027,5412,5413],{"class":1029,"line":1119},[1027,5414,1304],{},[52,5416],{},[37,5418,4957],{"id":4956},[168,5420,5422],{"id":5421},"results","Results:",[173,5424,5425,5431,5437],{},[176,5426,4738,5427,5430],{},[48,5428,5429],{},"50% reduction"," in content creation time",[176,5432,4738,5433,5436],{},[48,5434,5435],{},"15+ reusable components"," in the library",[176,5438,4738,5439,5442],{},[48,5440,5441],{},"Zero developer intervention"," needed after launch",[168,5444,5446],{"id":5445},"what-this-means","What This Means:",[42,5448,5449],{},"Writers went from \"Can you help me add a photo gallery?\" to \"I just published a multimedia story with video, pull quotes, and an interactive timeline—by myself.\"",[52,5451],{},[37,5453,2144],{"id":2143},[42,5455,5456],{},"This project transformed how I think about frontend development:",[42,5458,5459,5462],{},[48,5460,5461],{},"1. User-Centric Design Matters","\nEven though I never talked directly to writers, building tools FOR them (not just for specs) changed my approach. I had to imagine: \"If I were writing a story, what would I want?\"",[42,5464,5465,5468],{},[48,5466,5467],{},"2. Component Architecture at Scale","\nManaging 15+ components taught me patterns I now use everywhere:",[173,5470,5471,5474,5476],{},[176,5472,5473],{},"Consistent prop interfaces",[176,5475,3356],{},[176,5477,5478],{},"Reusability > quick hacks",[42,5480,5481,5484,5485,5488],{},[48,5482,5483],{},"3. Fast-Paced Learning","\nMost of the patterns here were ",[48,5486,5487],{},"first-time"," for me:",[173,5490,5491,5494,5497],{},[176,5492,5493],{},"Dynamic component rendering",[176,5495,5496],{},"Complex state management across nested components",[176,5498,5499],{},"Building accessible media players",[42,5501,5502],{},"I used AI (Claude, ChatGPT) to accelerate learning while understanding the \"why\" behind patterns. This let me deliver fast without sacrificing code quality.",[42,5504,5505,5508],{},[48,5506,5507],{},"4. Clean Code is a Gift","\nKnowing other devs would maintain this, I obsessed over:",[173,5510,5511,5514,5517],{},[176,5512,5513],{},"Clear variable names",[176,5515,5516],{},"Commented complex logic",[176,5518,5519],{},"Consistent formatting",[52,5521],{},[37,5523,2373],{"id":2372},[173,5525,5526,5530,5534,5538],{},[176,5527,5528,5046],{},[48,5529,180],{},[176,5531,5532,4352],{},[48,5533,4351],{},[176,5535,5536,4357],{},[48,5537,2384],{},[176,5539,5540,5543],{},[48,5541,5542],{},"Storage:"," JSON schema for layout persistence",[52,5545],{},[2538,5547,5245],{},{"title":607,"searchDepth":608,"depth":608,"links":5549},[5550,5551,5555,5559,5560],{"id":689,"depth":608,"text":690},{"id":5298,"depth":608,"text":5299,"children":5552},[5553,5554],{"id":5305,"depth":616,"text":5306},{"id":5374,"depth":616,"text":5375},{"id":4956,"depth":608,"text":4957,"children":5556},[5557,5558],{"id":5421,"depth":616,"text":5422},{"id":5445,"depth":616,"text":5446},{"id":2143,"depth":608,"text":2144},{"id":2372,"depth":608,"text":2373},"2024","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.","/projects/drag&drop.gif",{},"/projects/dynamic-page-builder",{"title":5277,"description":5562},"projects/dynamic-page-builder",[5272,4518,5569,637],"Component Architecture","_E77UCsuAg-nEi4yNIelKge7cIeS1ddrgzUo9o-oF90",{"id":5572,"title":5573,"body":5574,"buttonLabel":5680,"date":5561,"description":5681,"extension":625,"image":5682,"meta":5683,"navigation":24,"path":5684,"seo":5685,"stem":5686,"tags":5687,"__hash__":5691},"projects/projects/homepage-revamp.md","Website Performance Optimization",{"type":34,"value":5575,"toc":5672},[5576,5580,5583,5597,5599,5601,5605,5631,5633,5636,5641,5658,5663,5665,5667,5670],[37,5577,5579],{"id":5578},"the-mission","THE MISSION",[42,5581,5582],{},"Redesign and modernize Summit Media's website properties with focus on:",[173,5584,5585,5588,5591,5594],{},[176,5586,5587],{},"⚡ Performance (PageSpeed scores)",[176,5589,5590],{},"🎨 Modern UI/UX patterns",[176,5592,5593],{},"🔍 SEO best practices",[176,5595,5596],{},"📱 Mobile responsiveness",[52,5598],{},[37,5600,2282],{"id":2281},[168,5602,5604],{"id":5603},"performance-wins","Performance Wins:",[173,5606,5607,5613,5619,5625],{},[176,5608,5609,5612],{},[48,5610,5611],{},"PageSpeed Improvement:"," 5% → 30%+ increase in scores",[176,5614,5615,5618],{},[48,5616,5617],{},"Faster load times"," through code splitting and lazy loading",[176,5620,5621,5624],{},[48,5622,5623],{},"Better SEO"," with server-side rendering (Nuxt 3)",[176,5626,5627,5630],{},[48,5628,5629],{},"Enhanced user engagement"," with modern, intuitive interfaces",[52,5632],{},[37,5634,5635],{"id":5305},"TECHNICAL APPROACH",[42,5637,5638],{},[48,5639,5640],{},"Key Optimizations:",[173,5642,5643,5646,5649,5652,5655],{},[176,5644,5645],{},"Server-side rendering with Nuxt 3",[176,5647,5648],{},"Image optimization and lazy loading",[176,5650,5651],{},"Code splitting and tree shaking",[176,5653,5654],{},"Critical CSS inlining",[176,5656,5657],{},"Modern responsive layouts with Tailwind CSS",[42,5659,5660,5662],{},[48,5661,4655],{},"\nTight deadlines required rapid iteration while maintaining quality. I focused on proven patterns and reusable components to move fast without technical debt.",[52,5664],{},[37,5666,2373],{"id":2372},[42,5668,5669],{},"Nuxt 3 • Tailwind CSS • SEO Optimization • Performance Best Practices",[52,5671],{},{"title":607,"searchDepth":608,"depth":608,"links":5673},[5674,5675,5678,5679],{"id":5578,"depth":608,"text":5579},{"id":2281,"depth":608,"text":2282,"children":5676},[5677],{"id":5603,"depth":616,"text":5604},{"id":5305,"depth":608,"text":5635},{"id":2372,"depth":608,"text":2373},"Quick Overview","25%+ PageSpeed improvements across multiple sites. Server-side rendering, code optimization, and modern UI/UX. Fast delivery without technical debt.","/projects/performance.png",{},"/projects/homepage-revamp",{"title":5573,"description":5681},"projects/homepage-revamp",[4518,5688,5689,2740,5690],"SEO Optimization","Performance","Quasar Framework","TROzS1-ZnpX5ku7ur5osX7elSX-G4DAgr4IVA6wufXA",1782865071012]