[{"data":1,"prerenderedAt":254},["ShallowReactive",2],{"project-ru-product-options":3,"recommendation-mention-sources":192},{"id":4,"title":5,"body":6,"category":162,"context":163,"cover":164,"coverAlt":165,"demo":166,"description":167,"extension":168,"featured":169,"locale":170,"meta":171,"navigation":172,"order":173,"path":174,"published":172,"repository":166,"results":175,"role":176,"seo":177,"slug":178,"stack":179,"stem":189,"year":190,"__hash__":191},"projects\u002Fru\u002Fprojects\u002Fproduct-options.md","Товарные опции и дополнительные услуги в корзине витрины",{"type":7,"value":8,"toc":147},"minimark",[9,14,18,21,25,33,37,42,51,55,58,62,84,95,99,102,111,115,118,122,144],[10,11,13],"h2",{"id":12},"задача","Задача",[15,16,17],"p",{},"Магазины хотели продавать вместе с товаром дополнения и услуги: подарочную упаковку, открытку, ленту. Одни опции обязательны, другие нет, и у каждой группы свои ограничения. Итоговую цену покупатель должен видеть ещё до того, как положит товар в корзину.",[15,19,20],{},"Самое сложное было в корзине. Общая библиотека витрин CommonJS считала позицию корзины вариантом товара. С опциями это перестало быть правдой: букет Classic в обычной упаковке и тот же букет в подарочной коробке с открыткой — две разные покупки с разной ценой. Любое место, которое искало товар в корзине по ID варианта, теперь могло попасть не в ту позицию: изменение количества, удаление, счётчики на карточке и проверка остатков.",[10,22,24],{"id":23},"моя-роль","Моя роль",[15,26,27,28,32],{},"Я делал часть на витрине, в CommonJS: компонент ",[29,30,31],"code",{},"ui-accessories",", пересчёт цены, операции корзины, вывод опций в корзине и документацию для разработчиков магазинов. Метод корзины, который принимает выбранные опции, сделали бэкенд-разработчики. Мой код проходил ревью и QA.",[10,34,36],{"id":35},"что-я-сделал","Что я сделал",[38,39,41],"h3",{"id":40},"один-компонент-для-обязательных-и-необязательных-групп","Один компонент для обязательных и необязательных групп",[15,43,44,46,47,50],{},[29,45,31],{}," подключается через ",[29,48,49],{},"data-accessories=\"{{ product.id }}\""," и рендерит группы опций по шаблонам из разметки, поэтому работает и в стандартных виджетах, и в собственном дизайне магазина. У каждой группы есть минимальное и максимальное количество. В демо-магазине упаковку выбирают обязательно и только одну, а дополнений можно взять до двух. Я сделал рендер опций по data-атрибутам, а не привязал его к странице товара: тот же рендер был нужен AJAX-товарам, а чтобы добавить товар с обязательными опциями из превью в категории, данные опций тоже нужны.",[38,52,54],{"id":53},"цена-зависит-от-варианта-и-опций","Цена зависит от варианта и опций",[15,56,57],{},"Цена пересчитывается, когда покупатель меняет вариант или набор опций. В демо-магазине подарочная коробка (+$5) и открытка (+$3) поднимают цену букета Classic с $40 до $48, а те же дополнения для размера Grand — с $60 до $68. Ноль в проверках цены нельзя было считать отсутствием значения: вариант вполне может стоить 0, а обычная упаковка добавляет $0.",[38,59,61],{"id":60},"позиция-корзины-это-вариант-вместе-с-опциями","Позиция корзины — это вариант вместе с опциями",[15,63,64,65,68,69,68,72,75,76,79,80,83],{},"Я доработал ",[29,66,67],{},"Cart.add",", ",[29,70,71],{},"Cart.set",[29,73,74],{},"Cart.delete"," и очередь задач корзины, чтобы вместе с ID варианта они передавали ",[29,77,78],{},"accessory_value_ids",", сгруппированные в ",[29,81,82],{},"accessoriable_variant_ids",". Изменение количества и удаление теперь касаются одной конкретной комбинации, а не всех позиций с этим вариантом. В виджете корзины у каждой позиции видны названия выбранных опций и их цены.",[15,85,86,91],{},[87,88],"img",{"alt":89,"src":90},"Корзина, где один вариант букета лежит двумя позициями: в обычной упаковке и в подарочной коробке с открыткой","\u002Fimages\u002Fwork\u002Fproduct-options\u002Fcart-same-variant-two-lines.webp",[92,93,94],"em",{},"Демо-магазин: один вариант Classic, две позиции со своими опциями, количеством и ценой.",[38,96,98],{"id":97},"счётчики-и-остатки-для-разных-комбинаций","Счётчики и остатки для разных комбинаций",[15,100,101],{},"Раньше на два вопроса был один ответ, теперь ответы разошлись. Счётчик на карточке и кнопка «В корзину» следуют за выбранной комбинацией. Если переключиться с обычной упаковки на подарочную, счётчик меняется, а после удаления обычной позиции снова появляется кнопка добавления, хотя подарочная остаётся в корзине. Остаток же относится к варианту: позиции с разными опциями — это всё равно единицы одного товара, поэтому при проверке их количество складывается. Я прошёл по местам, где товар в корзине искали по ID варианта, и в каждом месте развёл эти два вопроса. В виджете уведомлений появилась отдельная ошибка про неверное количество выбранных опций.",[15,103,104,108],{},[87,105],{"alt":106,"src":107},"Корзина с 7 единицами в обычной упаковке и 9 в подарочной и предупреждением о максимальном количестве","\u002Fimages\u002Fwork\u002Fproduct-options\u002Fcart-combined-quantity-limit.webp",[92,109,110],{},"Демо-магазин: 7 + 9 единиц одного варианта в двух позициях. При попытке добавить ещё появляется предупреждение о лимите.",[38,112,114],{"id":113},"совместимость-и-документация","Совместимость и документация",[15,116,117],{},"Опции должны были работать в виджете карточки товара, AJAX-товарах, превью в категориях, модальных окнах, мультикорзинах и старых шаблонах. Я составил чек-лист проверки: добавление, изменение количества и удаление обычных товаров, товаров с мультискладом и товаров с опциями во всех этих местах, а также пересчёт цены при смене варианта и опций. Ещё я описал API опций для разработчиков, которые собирают карточки и корзины в собственных шаблонах.",[10,119,121],{"id":120},"результат","Результат",[123,124,125,129,132,135],"ul",{},[126,127,128],"li",{},"Покупатель выбирает обязательные и дополнительные опции, сразу видит итоговую цену и кладёт в корзину именно эту комбинацию.",[126,130,131],{},"Один вариант может лежать в корзине несколькими позициями, а изменение количества и удаление касаются только выбранной.",[126,133,134],{},"У разработчиков магазинов появились компонент и задокументированный API корзины для опций в собственных шаблонах.",[126,136,137,138,143],{},"Опции работают в товарных виджетах и AJAX-товарах. Про библиотеку, в которую всё это встроено, — ",[139,140,142],"a",{"href":141},"\u002Fru\u002Fwork\u002Fcommonjs-storefront-library\u002F","кейс о CommonJS",".",[15,145,146],{},"Скриншоты сделаны в демо-магазине inSales на фотографиях со свободной лицензией. Товары и цены придуманы для примера и не связаны с реальными продажами.",{"title":148,"searchDepth":149,"depth":149,"links":150},"",2,[151,152,153,161],{"id":12,"depth":149,"text":13},{"id":23,"depth":149,"text":24},{"id":35,"depth":149,"text":36,"children":154},[155,157,158,159,160],{"id":40,"depth":156,"text":41},3,{"id":53,"depth":156,"text":54},{"id":60,"depth":156,"text":61},{"id":97,"depth":156,"text":98},{"id":113,"depth":156,"text":114},{"id":120,"depth":149,"text":121},"Платформа витрин","inSales","\u002Fimages\u002Fwork\u002Fproduct-options\u002Fproduct-page-gift-box-and-card.webp","Карточка букета в демо-магазине: выбраны размер Classic, подарочная коробка и открытка, цена пересчитана до 48 долларов",null,"Сделал товарные опции в библиотеке витрин inSales: компонент выбора, пересчёт цены и операции корзины. Один и тот же вариант с разными дополнениями лежит в корзине отдельными позициями.","md",false,"ru",{},true,10,"\u002Fru\u002Fprojects\u002Fproduct-options",[],"Frontend-разработчик: компонент ui-accessories, цены, API корзины, документация",{"title":5,"description":167},"product-options",[180,181,182,183,184,185,186,187,188],"JavaScript","API Development","AJAX","Front-End Development","E-Commerce","Technical Documentation","HTML5","CSS","Debugging","ru\u002Fprojects\u002Fproduct-options","Январь – апрель 2025","DwntY1g957nhB5uqqCSeZjfK3d_2hTTYJzV_i1lyaLU",[193,204,209,224,234,239,244,249],{"stem":194,"name":195,"date":196,"featured":149,"published":172,"mentions":197},"recommendations\u002Fandrey-stikheev","Andrey Stikheev","2026-09-08",[198,201],{"project":199,"quote":200},"product-image-editor","Among his most notable projects was a sophisticated browser-based image editor built with TypeScript and Fabric.js, now used by thousands of merchants.",{"project":202,"quote":203},"abandoned-carts","He also developed the frontend for a paid abandoned-cart product and contributed to the production migration from Vue 2 to Vue 3.",{"stem":205,"name":206,"date":207,"featured":166,"published":172,"mentions":208},"recommendations\u002Fdaniil-churakov","Daniil Churakov","2026-08-27",[],{"stem":210,"name":211,"date":212,"featured":156,"published":172,"mentions":213},"recommendations\u002Fdmitry-sadovsky","Dmitry Sadovsky","2026-08-22",[214,217,219,222],{"project":215,"quote":216},"visual-storefront","For years he was the driving force behind our visual storefront editor — a large, complex application where merchants build their online stores.",{"project":199,"quote":218},"It became the tool merchants use to design product images right inside the admin panel — the kind of component companies usually buy rather than build.",{"project":220,"quote":221},"image-editor","What impressed me most was the infographic editor he built essentially from scratch: a full canvas-based graphic editor with artboards, shapes, text styling, templates, and undo\u002Fredo history, powered by an editor core he developed himself.",{"project":202,"quote":223},"Our abandoned carts functionality — with multi-channel customer messaging, communication history, and analytics — was largely his work, from UI down to the details of how requests are cancelled and states stay in sync.",{"stem":225,"name":226,"date":227,"featured":228,"published":172,"mentions":229},"recommendations\u002Fegor-fedkin","Egor Fedkin","2026-08-31",1,[230,232],{"project":199,"quote":231},"Today, the tool is actively used by around 3,000 users.",{"project":202,"quote":233},"It has already generated approximately twice the revenue needed to cover its development costs.",{"stem":235,"name":236,"date":237,"featured":166,"published":172,"mentions":238},"recommendations\u002Femmanuel-stephen","Emmanuel Stephen","2026-10-11",[],{"stem":240,"name":241,"date":242,"featured":166,"published":172,"mentions":243},"recommendations\u002Fevgeny-lonshakov","Евгений Лоншаков","2026-09-21",[],{"stem":245,"name":246,"date":247,"featured":166,"published":172,"mentions":248},"recommendations\u002Froman-gorbachev","Роман Горбачев","2026-09-17",[],{"stem":250,"name":251,"date":252,"featured":166,"published":172,"mentions":253},"recommendations\u002Fvladimir-bedarev","Bedarev Vladimir","2026-09-16",[],1793195987453]