[{"data":1,"prerenderedAt":634},["ShallowReactive",2],{"project-notori":3},{"id":4,"title":5,"body":6,"description":616,"duration":617,"extension":618,"featured":619,"github":620,"image":621,"live":622,"meta":623,"navigation":245,"order":282,"path":624,"role":625,"seo":626,"status":627,"stem":628,"team_size":625,"tech":629,"type":631,"year":632,"__hash__":633},"projects\u002Fproject\u002Fnotori.md","Notori - Notes App",{"type":7,"value":8,"toc":608},"minimark",[9,14,18,22,38,86,90,95,100,125,130,138,142,147,150,390,394,397,604],[10,11,13],"h2",{"id":12},"the-problem","The Problem",[15,16,17],"p",{},"Building complex user interfaces with \"Vanilla\" JavaScript often results in unmaintainable \"spaghetti code\" and global CSS conflicts. Developers need a way to encapsulate logic, templates, and styles strictly without the overhead of heavy frameworks like React or Vue for smaller applications.",[10,19,21],{"id":20},"my-solution","My Solution",[15,23,24,25,29,30,33,34,37],{},"I developed ",[26,27,28],"strong",{},"Notori",", a Single Page Application (SPA) that leverages the browser's native ",[26,31,32],{},"Custom Elements API"," and ",[26,35,36],{},"Shadow DOM"," to create truly encapsulated UI components.",[39,40,41,56,66,80],"ul",{},[42,43,44,47,48,33,52,55],"li",{},[26,45,46],{},"Component-Based Architecture:"," Broken down into reusable custom elements like ",[49,50,51],"code",{},"\u003Cnotes-list>",[49,53,54],{},"\u003Cnotes-item>",", ensuring style isolation and reusability.",[42,57,58,61,62,65],{},[26,59,60],{},"Modern Tooling Pipeline:"," Configured a custom ",[26,63,64],{},"Webpack"," environment from scratch to handle asset bundling, CSS extraction, and ES6+ transpilation via Babel.",[42,67,68,71,72,75,76,79],{},[26,69,70],{},"Interactive UX:"," Integrated ",[26,73,74],{},"SweetAlert2"," for modal dialogs and ",[26,77,78],{},"Anime.js"," for fluid micro-interactions, enhancing the native feel of the app.",[42,81,82,85],{},[26,83,84],{},"Robust Data Layer:"," Implemented a dedicated API service layer to handle asynchronous CRUD operations with the backend.",[10,87,89],{"id":88},"technical-deep-dive","Technical Deep Dive",[91,92,94],"h3",{"id":93},"architecture-decisions","Architecture Decisions",[15,96,97],{},[26,98,99],{},"Why Web Components over React?",[39,101,102,115],{},[42,103,104,107,108,33,111,114],{},[26,105,106],{},"Native Performance:"," By using ",[49,109,110],{},"HTMLElement",[49,112,113],{},"attachShadow({ mode: \"open\" })",", the app runs directly on browser standards with zero runtime overhead from a virtual DOM.",[42,116,117,120,121,124],{},[26,118,119],{},"Scoped Styling:"," Each component, such as the note list, injects its own ",[49,122,123],{},"\u003Cstyle>"," tag into its Shadow Root. This guarantees that CSS written for the list grid never leaks out to affect other parts of the application.",[15,126,127],{},[26,128,129],{},"Why Custom Webpack Config?",[39,131,132],{},[42,133,134,137],{},[26,135,136],{},"Control:"," Instead of using tools like CRA or Vite, I manually configured Webpack to understand the compilation process deeply—managing loaders for CSS, images, and Javascript modules explicitly.",[91,139,141],{"id":140},"key-features-i-built","Key Features I Built",[143,144,146],"h4",{"id":145},"_1-encapsulated-grid-layout-shadow-dom","1. Encapsulated Grid Layout (Shadow DOM)",[15,148,149],{},"I implemented a responsive grid system that adapts its column count based on attributes, isolated entirely within the Shadow DOM.",[151,152,157],"pre",{"className":153,"code":154,"language":155,"meta":156,"style":156},"language-bash shiki shiki-themes github-light github-dark","# javacript\n# src\u002Fscript\u002Fcomponents\u002Fnotes-list.js\nclass NotesList extends HTMLElement {\n  constructor() {\n    super();\n    # Create a shadow root for style isolation\n    this._shadowRoot = this.attachShadow({ mode: \"open\" });\n  }\n\n  render() {\n    # Dynamic grid template based on 'column' property\n    this._style.textContent = `\n      .list {\n        display: grid;\n        grid-template-columns: ${\"1fr \".repeat(this._column).trim()};\n        gap: ${this._gutter}px;\n      }\n    `;\n    # ...\n  }\n}\ncustomElements.define(\"notes-list\", NotesList);\n","bash","",[49,158,159,168,174,194,204,213,219,234,240,247,255,261,272,280,289,322,341,347,356,362,367,373],{"__ignoreMap":156},[160,161,164],"span",{"class":162,"line":163},"line",1,[160,165,167],{"class":166},"sJ8bj","# javacript\n",[160,169,171],{"class":162,"line":170},2,[160,172,173],{"class":166},"# src\u002Fscript\u002Fcomponents\u002Fnotes-list.js\n",[160,175,177,181,185,188,191],{"class":162,"line":176},3,[160,178,180],{"class":179},"sScJk","class",[160,182,184],{"class":183},"sZZnC"," NotesList",[160,186,187],{"class":183}," extends",[160,189,190],{"class":183}," HTMLElement",[160,192,193],{"class":183}," {\n",[160,195,197,200],{"class":162,"line":196},4,[160,198,199],{"class":179},"  constructor",[160,201,203],{"class":202},"sVt8B","() {\n",[160,205,207,210],{"class":162,"line":206},5,[160,208,209],{"class":179},"    super",[160,211,212],{"class":202},"();\n",[160,214,216],{"class":162,"line":215},6,[160,217,218],{"class":166},"    # Create a shadow root for style isolation\n",[160,220,222,225,228,231],{"class":162,"line":221},7,[160,223,224],{"class":179},"    this._shadowRoot",[160,226,227],{"class":183}," =",[160,229,230],{"class":183}," this.attachShadow",[160,232,233],{"class":202},"({ mode: \"open\" });\n",[160,235,237],{"class":162,"line":236},8,[160,238,239],{"class":202},"  }\n",[160,241,243],{"class":162,"line":242},9,[160,244,246],{"emptyLinePlaceholder":245},true,"\n",[160,248,250,253],{"class":162,"line":249},10,[160,251,252],{"class":179},"  render",[160,254,203],{"class":202},[160,256,258],{"class":162,"line":257},11,[160,259,260],{"class":166},"    # Dynamic grid template based on 'column' property\n",[160,262,264,267,269],{"class":162,"line":263},12,[160,265,266],{"class":179},"    this._style.textContent",[160,268,227],{"class":183},[160,270,271],{"class":183}," `\n",[160,273,275,278],{"class":162,"line":274},13,[160,276,277],{"class":179},"      .list",[160,279,193],{"class":183},[160,281,283,286],{"class":162,"line":282},14,[160,284,285],{"class":179},"        display:",[160,287,288],{"class":183}," grid;\n",[160,290,292,295,298,301,304,307,310,313,316,319],{"class":162,"line":291},15,[160,293,294],{"class":179},"        grid-template-columns:",[160,296,297],{"class":183}," ${\"1fr \".",[160,299,300],{"class":202},"repeat",[160,302,303],{"class":183},"(",[160,305,306],{"class":202},"this",[160,308,309],{"class":183},".",[160,311,312],{"class":202},"_column",[160,314,315],{"class":183},").",[160,317,318],{"class":202},"trim",[160,320,321],{"class":183},"()};\n",[160,323,325,328,331,333,335,338],{"class":162,"line":324},16,[160,326,327],{"class":179},"        gap:",[160,329,330],{"class":183}," ${",[160,332,306],{"class":202},[160,334,309],{"class":183},[160,336,337],{"class":202},"_gutter",[160,339,340],{"class":183},"}px;\n",[160,342,344],{"class":162,"line":343},17,[160,345,346],{"class":183},"      }\n",[160,348,350,353],{"class":162,"line":349},18,[160,351,352],{"class":183},"    `",[160,354,355],{"class":202},";\n",[160,357,359],{"class":162,"line":358},19,[160,360,361],{"class":166},"    # ...\n",[160,363,365],{"class":162,"line":364},20,[160,366,239],{"class":202},[160,368,370],{"class":162,"line":369},21,[160,371,372],{"class":202},"}\n",[160,374,376,379,382,385,387],{"class":162,"line":375},22,[160,377,378],{"class":179},"customElements.define(",[160,380,381],{"class":179},"\"notes-list\"",[160,383,384],{"class":179},",",[160,386,184],{"class":183},[160,388,389],{"class":202},");\n",[143,391,393],{"id":392},"_2-service-layer-pattern","2. Service Layer Pattern",[15,395,396],{},"To keep the UI components clean, I separated all network logic into a dedicated service module. This module handles error parsing and JSON serialization for the external Notes API.",[151,398,400],{"className":153,"code":399,"language":155,"meta":156,"style":156},"# javascript\n# src\u002Fscript\u002Fdata\u002Fnotes-data-api.js\nconst addNotes = async (note) => {\n  try {\n    const response = await fetch(`${baseUrl}\u002Fnotes`, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application\u002Fjson\" },\n      body: JSON.stringify(note),\n    });\n    # Centralized error handling\n    if (!response.ok) throw new Error(responseJson.message);\n    return responseJson.data;\n  } catch (error) {\n    console.error(\"Failed to add data:\", error);\n  }\n};\n",[49,401,402,407,412,434,441,472,480,497,516,521,526,559,569,580,595,599],{"__ignoreMap":156},[160,403,404],{"class":162,"line":163},[160,405,406],{"class":166},"# javascript\n",[160,408,409],{"class":162,"line":170},[160,410,411],{"class":166},"# src\u002Fscript\u002Fdata\u002Fnotes-data-api.js\n",[160,413,414,417,420,422,425,428,432],{"class":162,"line":176},[160,415,416],{"class":179},"const",[160,418,419],{"class":183}," addNotes",[160,421,227],{"class":183},[160,423,424],{"class":183}," async",[160,426,427],{"class":202}," (note) =",[160,429,431],{"class":430},"szBVR",">",[160,433,193],{"class":202},[160,435,436,439],{"class":162,"line":196},[160,437,438],{"class":179},"  try",[160,440,193],{"class":183},[160,442,443,446,449,451,454,457,459,462,465,468,470],{"class":162,"line":206},[160,444,445],{"class":179},"    const",[160,447,448],{"class":183}," response",[160,450,227],{"class":183},[160,452,453],{"class":183}," await",[160,455,456],{"class":183}," fetch",[160,458,303],{"class":202},[160,460,461],{"class":183},"`${",[160,463,464],{"class":202},"baseUrl",[160,466,467],{"class":183},"}\u002Fnotes`",[160,469,384],{"class":179},[160,471,193],{"class":183},[160,473,474,477],{"class":162,"line":215},[160,475,476],{"class":179},"      method:",[160,478,479],{"class":183}," \"POST\",\n",[160,481,482,485,488,491,494],{"class":162,"line":221},[160,483,484],{"class":179},"      headers:",[160,486,487],{"class":183}," {",[160,489,490],{"class":183}," \"Content-Type\":",[160,492,493],{"class":183}," \"application\u002Fjson\"",[160,495,496],{"class":183}," },\n",[160,498,499,502,505,507,510,513],{"class":162,"line":236},[160,500,501],{"class":179},"      body:",[160,503,504],{"class":183}," JSON.stringify",[160,506,303],{"class":202},[160,508,509],{"class":179},"note",[160,511,512],{"class":202},")",[160,514,515],{"class":183},",\n",[160,517,518],{"class":162,"line":242},[160,519,520],{"class":202},"    });\n",[160,522,523],{"class":162,"line":249},[160,524,525],{"class":166},"    # Centralized error handling\n",[160,527,528,531,534,537,540,543,546,549,552,554,557],{"class":162,"line":257},[160,529,530],{"class":430},"    if",[160,532,533],{"class":202}," (",[160,535,536],{"class":430},"!",[160,538,539],{"class":179},"response.ok",[160,541,542],{"class":202},") ",[160,544,545],{"class":179},"throw",[160,547,548],{"class":183}," new",[160,550,551],{"class":183}," Error",[160,553,303],{"class":202},[160,555,556],{"class":179},"responseJson.message",[160,558,389],{"class":202},[160,560,561,564,567],{"class":162,"line":263},[160,562,563],{"class":430},"    return",[160,565,566],{"class":183}," responseJson.data",[160,568,355],{"class":202},[160,570,571,574,577],{"class":162,"line":274},[160,572,573],{"class":202},"  } catch (",[160,575,576],{"class":179},"error",[160,578,579],{"class":202},") {\n",[160,581,582,585,588,590,593],{"class":162,"line":282},[160,583,584],{"class":179},"    console.error(",[160,586,587],{"class":179},"\"Failed to add data:\"",[160,589,384],{"class":179},[160,591,592],{"class":183}," error",[160,594,389],{"class":202},[160,596,597],{"class":162,"line":291},[160,598,239],{"class":202},[160,600,601],{"class":162,"line":324},[160,602,603],{"class":202},"};\n",[605,606,607],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":156,"searchDepth":170,"depth":170,"links":609},[610,611,612],{"id":12,"depth":170,"text":13},{"id":20,"depth":170,"text":21},{"id":88,"depth":170,"text":89,"children":613},[614,615],{"id":93,"depth":176,"text":94},{"id":140,"depth":176,"text":141},"A lightweight, modular note-taking application engineered with Native Web Components and Webpack to demonstrate framework-agnostic component architecture.","2 months","md",false,"https:\u002F\u002Fgithub.com\u002Fszuryuu\u002Fnotori","\u002Fimages\u002Fprojects\u002Fjavascript.png","https:\u002F\u002Fnotori.vercel.app\u002F",{},"\u002Fproject\u002Fnotori",null,{"title":5,"description":616},"Completed","project\u002Fnotori",[630,64,78],"JavaScript","Solo Project","2025","eSBtj7BIXVxhcJ9cG6v0pqhKPUkmhQNLO_RM-0l_yK4",1776582963000]