[{"data":1,"prerenderedAt":442},["ShallowReactive",2],{"project-case-cdek-shopping":3},{"blocks":4,"person":378},[5,15,50,100,116,133,173,304,336,366,372],{"component":6,"data":7},"ProjectCaseHeader",{"index":8,"subtitle":9,"name":10,"headline":11,"description":12,"url":13,"tone":14},"08","SHOPPING FROM ABROAD · CUSTOMER ACCOUNT","CDEK.Shopping","37% fewer regression bugs","A CDEK group agent service for ordering products of foreign brands from 14 countries, paid with a Russian card at prices in rubles.","https:\u002F\u002Fcdek.shopping\u002F","secondary",{"component":16,"data":17},"ProjectCaseCover",{"image":18,"imageMobile":36,"imageAlt":48,"frame":49},{"src":19,"width":20,"height":21,"variants":22},"\u002F_nuxt\u002Fpreview.BtfYmZXg.webp",1440,900,[23,26,29,32,35],{"src":24,"width":25},"\u002F_nuxt\u002Fpreview.IjjyXAk1.webp",480,{"src":27,"width":28},"\u002F_nuxt\u002Fpreview.Dnde0-O5.webp",720,{"src":30,"width":31},"\u002F_nuxt\u002Fpreview.CdA0MpR5.webp",960,{"src":33,"width":34},"\u002F_nuxt\u002Fpreview.Cd-F3LTm.webp",1200,{"src":19,"width":20},{"src":37,"width":38,"height":39,"variants":40},"\u002F_nuxt\u002Fpreview-mobile.qB4EOKla.webp",600,1298,[41,44,47],{"src":42,"width":43},"\u002F_nuxt\u002Fpreview-mobile.ClLqQ63Q.webp",160,{"src":45,"width":46},"\u002F_nuxt\u002Fpreview-mobile.eVrVSYYt.webp",300,{"src":37,"width":38},"Home page of the CDEK.Shopping website","devices",{"component":51,"data":52},"ProjectCaseMeta",{"tags":53,"stack":56},[54,55],"E-commerce","Accounts",[57,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98],{"label":58,"own":59},"JavaScript",true,{"label":61,"own":59},"jQuery",{"label":63,"own":59},"Vue 2",{"label":65,"own":59},"Vuex",{"label":67,"own":59},"SCSS",{"label":69,"own":59},"Webpack",{"label":71,"own":59},"Cypress",{"label":73,"own":59},"Jest",{"label":75,"own":59},"axios",{"label":77,"own":59},"portal-vue",{"label":79,"own":59},"Bootstrap",{"label":81,"own":59},"Popper",{"label":83,"own":59},"Owl Carousel",{"label":85,"own":59},"Slick",{"label":87,"own":59},"lightGallery",{"label":89,"own":59},"noUiSlider",{"label":91,"own":59},"jQuery Masked Input",{"label":93,"own":59},"Laravel",{"label":95,"own":59},"Blade",{"label":97},"Sentry",{"label":99},"Diginetica",{"component":101,"data":102},"ProjectCaseFacts",{"facts":103},[104,107,110,113],{"key":105,"value":106},"role","Middle Frontend Developer",{"key":108,"value":109},"team","4 frontend, 8 backend, 2 QA and a designer",{"key":111,"value":112},"duration","2 years, 10.2021 – 10.2023",{"key":114,"value":115},"status","The site is live, my work on the project is over",{"component":117,"data":118},"ProjectCaseRole",{"id":105,"name":119,"items":120},"What I did",[121,124,127,130],{"title":122,"text":123},"Customer account","The entire frontend of the account in Vue 2 and Vuex: five sections in one app with no page reloads. I wrote every component of the account from the team designer's mockups.",{"title":125,"text":126},"Vue inside Blade","Convinced the team to adopt Vue 2 and came up with a way to embed the SPA account into Laravel + Blade without a separate frontend project.",{"title":128,"text":129},"Legacy refactoring","Covered the jQuery code with Cypress E2E tests and visual baselines, then split files of 6,000+ lines into modules with Jest unit tests.",{"title":131,"text":132},"Laravel and analytics","Blade templates, routes and account data in API responses. Set up Yandex Metrica and added events. Reviewed the tasks of the other frontend developers.",{"component":134,"data":135},"ProjectCaseChapter",{"id":136,"name":137,"blocks":138},"context","Context",[139,154,165],{"component":134,"data":140},{"id":141,"name":142,"blocks":143},"business","Business",[144,148,151],{"component":145,"data":146},"ProjectCaseText",{"text":147},"CDEK.Shopping is an agent for shopping abroad run by CDEK.Market LLC, a service of the CDEK group. Customers in Russia order products of brands from the US, Europe and Asia: they pay with a Russian card and see prices in rubles right away.",{"component":145,"data":149},{"text":150},"The service handles customs clearance and logistics, and orders are delivered by the group's own logistics company.",{"component":145,"data":152},{"text":153},"If the product is not in the catalog, the customer leaves a purchase request: sends a link to the product in a foreign store and specifies the size, color and quantity. A manager checks availability and calculates the final price in rubles with delivery and the service fee. After confirmation and payment, the service buys the product, and the order appears in the account on the “By purchase request” tab.",{"component":134,"data":155},{"id":156,"name":157,"blocks":158},"situation","The situation",[159,162],{"component":145,"data":160},{"text":161},"The business wanted to launch as soon as possible, so the new service was built on the CDEK Market codebase: jQuery and Blade, files of 6,000+ lines. The customer account there was built on server templates with no interactivity: every action reloaded the page, and the customer waited for the server at every step.",{"component":145,"data":163},{"text":164},"The accumulated tech debt slowed development down and was waiting to be solved, but the plan had no separate time for refactoring: it had to run alongside the launch, on the same deadlines.",{"component":134,"data":166},{"id":167,"name":168,"blocks":169},"goal","Goal",[170],{"component":145,"data":171},{"text":172},"Build an interactive customer account by the launch, where no action reloads the page. CDEK.Shopping users never saw the old account: they started right away in the new one.",{"component":134,"data":174},{"id":175,"name":176,"blocks":177},"process","Process, architecture and decisions",[178,207,241,252,269,277],{"component":134,"data":179},{"id":180,"name":122,"blocks":181},"account",[182,185],{"component":145,"data":183},{"text":184},"The account is built as one Vue app: each section is a separate module with its own Vuex state and API requests. So the customer moves between sections without a page reload, and a new section can be added without touching the others.",{"component":186,"data":187},"ProjectCaseSteps",{"steps":188},[189,192,195,198,201,204],{"title":190,"text":191},"My details","Forms for contacts and customs clearance details, with the email and phone change moved into a separate flow",{"title":193,"text":194},"Orders","“All”, “Unpaid” and “By purchase request” tabs, a filter and pagination with no reload, the status of every item, a separate order page",{"title":196,"text":197},"My points","Active, accrued and frozen points from the loyalty program API, the points history",{"title":199,"text":200},"Delivery address","Choosing the default address, switching between delivery to the door and to a pickup point",{"title":202,"text":203},"Reviews","Products waiting for a review and reviews already written, in one section",{"title":205,"text":206},"Navigation","Counters of unpaid orders and products without a review on the tabs, hash routing, a separate mobile navigation",{"component":134,"data":208},{"id":209,"name":210,"blocks":211},"architecture","Architecture",[212,215],{"component":145,"data":213},{"text":214},"The storefront stayed server-rendered, while the account runs as a single-page app inside the same site. It did not need a separate frontend project.",{"component":216,"data":217},"ProjectCaseArchitecture",{"items":218},[219,223,229,237],{"label":220,"title":221,"text":222},"STOREFRONT","Storefront","Server rendering with Blade, interactivity with jQuery.",{"label":224,"title":122,"text":225,"code":226},"ACCOUNT","Vue 2 and Vuex. The backend serves an #app container, the app mounts inside it, routing goes through the hash.",[180,227,228],"orderListing","orderDetailsPage",{"label":230,"title":231,"text":232,"code":233},"API","REST on Laravel","The account gets its data through axios: profile, orders, points, delivery and reviews.",[234,235,236],"\u002Fapi\u002Fv1\u002Faccount","\u002Fapi\u002Fv1\u002Floyalty","\u002Fapi\u002Fv1\u002Fcustomer",{"label":238,"title":239,"text":240},"SERVICES","External services","Payments, the Mindbox marketing tracker, Diginetica search, Yandex Metrica.",{"component":134,"data":242},{"id":243,"name":244,"blocks":245},"components","Account components",[246,249],{"component":145,"data":247},{"text":248},"We designed the component system together with the team designer, with BEM naming. The account has 16 base UI components: buttons, a checkbox, tabs, a select, a filter, a modal, a notification, a tooltip, pagination and others.",{"component":145,"data":250},{"text":251},"They make up about 40 section blocks, and the icons live in a sprite of about 30. I built the account responsive from the start, with a separate mobile navigation.",{"component":134,"data":253},{"id":254,"name":255,"blocks":256},"integrations","Integrations",[257],{"component":258,"data":259},"ProjectCaseIntegrations",{"items":260},[261,263,265,267],{"label":262,"own":59},"Yandex Metrica · events",{"label":264},"Online payments",{"label":266},"Mindbox · marketing tracker",{"label":268},"Diginetica · search with autocomplete",{"component":134,"data":270},{"id":271,"name":272,"blocks":273},"security","Data security",[274],{"component":145,"data":275},{"text":276},"The account works with customers' personal data, including passport details for customs clearance. They are stored encrypted in the database, and the profile API responses return them only masked. To see the data in full, the account makes a separate request to a protected endpoint. The server checks the user's session and permissions, decrypts the data and returns it only in that response. On the client it is not stored in Vuex or cached: once the block is closed, the mask is shown again.",{"component":134,"data":278},{"id":279,"name":280,"blocks":281},"quality","Quality and releases",[282,285,288,301],{"component":145,"data":283},{"text":284},"Before the refactoring I covered the store's critical scenarios with Cypress E2E tests and took visual baselines of the components. The modules that appeared after the refactoring are covered with Jest unit tests.",{"component":145,"data":286},{"text":287},"Every task goes through CI checks and a test stand, then on the planned release date it lands on a preprod identical to production. After acceptance the release rolls out to production with no downtime, and if something goes wrong we can quickly roll back to the previous stable version.",{"component":289,"data":290},"ProjectCasePipeline",{"stages":291},[292,295,298],{"name":293,"next":294},"develop","test stand",{"name":296,"next":297},"preprod","acceptance",{"name":299,"next":300},"master","production, no downtime",{"component":145,"data":302},{"text":303},"We worked in two-week sprints with a demo at the end of each. Every task went through code review, and I reviewed the tasks of the other frontend developers.",{"component":134,"data":305},{"id":306,"name":307,"blocks":308},"challenges","Challenges",[309],{"component":310,"data":311},"ProjectCaseChallenges",{"items":312},[313,326],{"id":314,"name":315,"rows":316},"legacy","Legacy files of 6,000+ lines",[317,320,323],{"key":318,"text":319},"problem","The jQuery client code lived in files of 6,000+ lines. Writing unit tests for such spaghetti was impossible, and touching it without tests was risky.",{"key":321,"text":322},"solution","First I covered the critical scenarios with Cypress E2E tests and took visual baselines, so the refactoring would break neither the logic nor the layout. Then, following the Refactoring.Guru principles, I moved repeated code into modules and kept splitting them until only pure functions covered with Jest unit tests were left.",{"key":324,"text":325},"result","Regression bugs dropped by 37% according to QA reports. Features started shipping faster, and sprint overruns went down.",{"id":327,"name":328,"rows":329},"vue","Vue 2 in a jQuery project",[330,332,334],{"key":318,"text":331},"The whole project was written in jQuery and Blade. An interactive account needed a modern framework, and the team had to be convinced to adopt one.",{"key":321,"text":333},"Got approval to add Vue 2 to the account and embedded it without a separate project: the backend serves an #app container, the app mounts inside it, routing goes through the hash. The account was meant to show on a live example that writing in Vue is simpler and faster than maintaining the legacy code.",{"key":324,"text":335},"The new service's account launched on Vue 2 and Vuex and became an argument for leaving the legacy code: the team decided to rewrite the project in Nuxt.",{"component":134,"data":337},{"id":338,"name":339,"blocks":340},"results","Metrics and results",[341,363],{"component":342,"data":343},"ProjectCaseResults",{"stats":344},[345,348,351,354,357,360],{"value":346,"label":347},"−37%","regression bugs after the refactoring",{"value":349,"label":350},"5","account sections in one app",{"value":352,"label":353},"16","base UI components of the account",{"value":355,"label":356},"≤ 2.5 s","LCP (Largest Contentful Paint)",{"value":358,"label":359},"≤ 200 ms","INP (Interaction to Next Paint)",{"value":361,"label":362},"≤ 0.1","CLS (Cumulative Layout Shift)",{"component":145,"data":364},{"text":365},"CDEK.Shopping customers worked in the new account from day one, and the legacy refactoring cut regressions and sped up feature delivery.",{"component":367,"data":368},"ProjectCaseCta",{"id":369,"title":370,"text":371,"url":13},"cta","Need a modern customer account in a legacy project?","I will embed an interactive account into your existing site without a separate frontend project, cover the legacy code with tests and split it into modules.",{"component":373,"data":374},"ProjectCaseNext",{"slug":375,"name":376,"index":377,"tone":14},"fly-post","Fly Post","09",{"name":379,"alternateNames":380,"role":383,"skills":384},"Michael Malakhov",[381,382],"Михаил Малахов","Mikhail Malakhov","Fullstack Developer",[385,386,387,388,389,390,391,392,393,394,395,79,396,397,398,399,400,401,402,403,93,404,405,406,407,408,409,410,411,412,69,413,414,415,416,417,418,73,419,420,421,422,423,424,425,426,427,428,429,430,431,65,432,433,434,435,436,437,438,439,440,441],"JavaScript (ES6+)","TypeScript","HTML5","CSS3","Sass","Vue.js (Vue 2\u002FVue 3, Composition API)","Nuxt 3\u002F4","React","Next.js","Vue Router","Tailwind CSS","SPA","SSR","MPA","SEO","Chrome DevTools","Node.js","NestJS","Express","Rust","PostgreSQL","Redis","REST API","Docker","Nginx","CI\u002FCD","GitHub Actions","Vite","Git","GitHub","GitLab","Figma","Redux","Vitest","Tauri","Prometheus","Storybook","Jira","Claude Code","Codex","Gemini","MCP","Atlassian Rovo","Zustand","SWR","TanStack Query","Pinia","React Hook Form","Zod","Capacitor","GraphQL","WebSocket","Clinic.js","autocannon","Lighthouse","GitHub Copilot","AI agents",1793309471858]