[{"data":1,"prerenderedAt":231},["ShallowReactive",2],{"i-lucide:arrow-right":3,"i-simple-icons:linkedin":8,"i-simple-icons:github":11,"i-lucide:file-text":13,"i-lucide:arrow-up":15,"project-en-leathermaster":17,"projects-en":209,"i-lucide:lock":215,"i-lucide:chevron-left":217,"i-lucide:chevron-right":219,"i-lucide:credit-card":221,"i-lucide:layout-dashboard":223,"i-lucide:key-round":225,"i-lucide:git-branch":227,"i-lucide:arrow-up-right":229},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,24,false,"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 12h14m-7-7l7 7l-7 7\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9,"hidden":10},"\u003Cpath fill=\"currentColor\" d=\"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037c-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85c3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.06 2.06 0 0 1-2.063-2.065a2.064 2.064 0 1 1 2.063 2.065m1.782 13.019H3.555V9h3.564zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0z\"\u002F>",true,{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":12},"\u003Cpath fill=\"currentColor\" d=\"M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":14},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\"\u002F>\u003Cpath d=\"M14 2v5a1 1 0 0 0 1 1h5M10 9H8m8 4H8m8 4H8\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":16},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m5 12l7-7l7 7m-7 7V5\"\u002F>",{"id":18,"title":19,"body":20,"description":111,"extension":124,"facts":125,"gallery":138,"highlights":163,"image":178,"meta":181,"navigation":10,"order":182,"path":183,"role":184,"screens":185,"seo":197,"stack":198,"stem":205,"summary":206,"url":105,"year":207,"__hash__":208},"projects_en\u002Fprojects\u002Fleathermaster.md","LeatherMaster",{"type":21,"value":22,"toc":110},"minimark",[23,28,32,36,39,43,48,56,59,63,66,73,77,84,88,95,99],[24,25,27],"h2",{"id":26},"the-brief","The brief",[29,30,31],"p",{},"A workshop near Poznań needed a website that shows its services and before-and-after makeovers, takes training bookings with online payment, and can be edited without a developer.",[24,33,35],{"id":34},"what-i-did","What I did",[29,37,38],{},"All of it myself. I talked it through with the owner, designed the site, wrote the Laravel backend, the Blade and Tailwind front end and the Livewire panel, hooked up payments and set up CI\u002FCD.",[24,40,42],{"id":41},"details","Details",[44,45,47],"h3",{"id":46},"bookings-and-payments","Bookings and payments",[29,49,50,51,55],{},"One booking can hold several seats, with a name for each participant. The hold has a countdown and expires when it runs out, and once paid the customer gets an ",[52,53,54],"code",{},".ics"," file for their calendar. Company details fill in by themselves from the tax ID. They come from the Polish VAT registry and I cache them for 24 hours, because the API has a daily request limit.",[29,57,58],{},"Payments go through ING Pay, by BLIK or bank transfer. The webhook checks the signature, and a rejected or expired payment can be retried. The sandbox behaves differently from the docs in a few places, so I wrote that down in the README.",[44,60,62],{"id":61},"panel","Panel",[29,64,65],{},"Deleted items stay in the panel's bin for 30 days and can be restored. Every record has a change history, screens can be found with ⌘K, and on a phone the menu sits in a bottom bar.",[29,67,68],{},[69,70],"img",{"alt":71,"src":72},"LeatherMaster panel dashboard: upcoming training dates with booked seats and recent bookings with payment status (sample data)","\u002Fimages\u002Fprojects\u002Fleathermaster-panel.webp",[44,74,76],{"id":75},"security","Security",[29,78,79,80,83],{},"The panel uses passkey and 2FA sign-in. I sign passkey handles with a separate secret so that rotating ",[52,81,82],{},"APP_KEY"," doesn't invalidate them.",[44,85,87],{"id":86},"seo","SEO",[29,89,90,91,94],{},"The sitemap and structured data are generated automatically. When a page's address changes, the old one redirects to the new one with a 301, and a switched-off site answers with a 503 and a ",[52,92,93],{},"Retry-After"," header.",[24,96,98],{"id":97},"the-result","The result",[29,100,101,102,109],{},"The site is live at ",[103,104,108],"a",{"href":105,"rel":106},"https:\u002F\u002Fwww.leathermaster.pl",[107],"nofollow","leathermaster.pl",". The owner edits the content and adds training dates, and handles bookings in the panel without needing me.",{"title":111,"searchDepth":112,"depth":112,"links":113},"",2,[114,115,116,123],{"id":26,"depth":112,"text":27},{"id":34,"depth":112,"text":35},{"id":41,"depth":112,"text":42,"children":117},[118,120,121,122],{"id":46,"depth":119,"text":47},3,{"id":61,"depth":119,"text":62},{"id":75,"depth":119,"text":76},{"id":86,"depth":119,"text":87},{"id":97,"depth":112,"text":98},"md",[126,129,132,135],{"value":127,"label":128},"BLIK","course payments through ING Pay",{"value":130,"label":131},"Passkey","panel sign-in, plus 2FA",{"value":133,"label":134},"250+","tests on every push",{"value":136,"label":137},"30 days","to restore deleted content",[139,144,149,153,158],{"src":140,"alt":141,"title":142,"text":143},"\u002Fimages\u002Fprojects\u002Fleathermaster-training.webp","LeatherMaster training page: a photo from a class, the next date, price per person and a booking button","Training page.","The next date, the price per person and the format. Customers book from here.",{"src":145,"alt":146,"title":147,"text":148},"\u002Fimages\u002Fprojects\u002Fleathermaster-booking.webp","Three-step training booking form, with a summary of the date and price next to it","Booking a date.","Three steps, details, payment and confirmation. Company details fill in from the tax ID.",{"src":72,"alt":150,"title":151,"text":152},"LeatherMaster panel dashboard: upcoming training dates and recent bookings with payment status (sample data)","Dashboard.","After signing in, the owner sees the upcoming dates, free seats and whether the latest bookings are paid.",{"src":154,"alt":155,"title":156,"text":157},"\u002Fimages\u002Fprojects\u002Fleathermaster-terms.webp","Calendar of training dates in the panel, with group and one-to-one dates and a visibility switch","Dates and bookings.","A calendar of group and one-to-one dates. Any date can be hidden from the site with one switch.",{"src":159,"alt":160,"title":161,"text":162},"\u002Fimages\u002Fprojects\u002Fleathermaster-editor.webp","Editing a training in the panel: description editor, image with an adjustable crop and a save button","Editing a training.","The description in an editor, an image with an adjustable crop, and ⌘S to save.",[164,167,171,174],{"icon":165,"title":47,"text":166},"lucide:credit-card","Seat booking for training courses and BLIK payments through ING Pay, with verified webhooks and payment retries.",{"icon":168,"title":169,"text":170},"lucide:layout-dashboard","Livewire 4 panel","Navigation without reloads, a ⌘K screen search, change history and a restorable bin.",{"icon":172,"title":76,"text":173},"lucide:key-round","Passkey and 2FA sign-in, a Content Security Policy, rate-limited forms and Turnstile.",{"icon":175,"title":176,"text":177},"lucide:git-branch","CI\u002FCD","Tests and static analysis on every push, deploys versioned with CalVer and checked by a smoke test.",{"src":179,"alt":180},"\u002Fimages\u002Fprojects\u002Fleathermaster.webp","LeatherMaster homepage: hero with the leather restoration offer and a contact button",{},"1","\u002Fprojects\u002Fleathermaster","Design, backend, frontend, deployment",[186,192],{"label":187,"path":188,"src":179,"mobile":189,"alt":180,"title":190,"text":191},"Website","\u002F","\u002Fimages\u002Fprojects\u002Fleathermaster-mobile.webp","Customers book and pay.","They pick a training date, fill in their details and pay by BLIK or bank transfer.",{"label":62,"path":193,"src":72,"mobile":194,"alt":150,"title":195,"text":196},"\u002Fpanel","\u002Fimages\u002Fprojects\u002Fleathermaster-panel-mobile.webp","The owner sees it in the panel.","The booking lands on the dashboard with its payment status, next to the upcoming dates.",{"title":19,"description":111},[199,200,201,202,203,204],"Laravel 13","Livewire 4","Tailwind CSS 4","MySQL","GitHub Actions","Cloudflare","projects\u002Fleathermaster","Website and a custom CMS for a leather upholstery restoration workshop. Customers book training courses and pay online, and the owner edits the content in the panel.","2026","VS5e3FkwD2PvSmgoIVsJSp7UbEwrURjLHLlYqyaHz-4",[210,211],{"path":183,"title":19,"summary":206},{"path":212,"title":213,"summary":214},"\u002Fprojects\u002Fportfolio","Portfolio","This website. Two languages, static HTML, case studies and a CV generated from the same data, and code written together with Claude Code.",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":216},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Crect width=\"18\" height=\"11\" x=\"3\" y=\"11\" rx=\"2\" ry=\"2\"\u002F>\u003Cpath d=\"M7 11V7a5 5 0 0 1 10 0v4\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":218},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m15 18l-6-6l6-6\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":220},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m9 18l6-6l-6-6\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":222},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Crect width=\"20\" height=\"14\" x=\"2\" y=\"5\" rx=\"2\"\u002F>\u003Cpath d=\"M2 10h20M6 14h2\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":224},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Crect width=\"7\" height=\"9\" x=\"3\" y=\"3\" rx=\"1\"\u002F>\u003Crect width=\"7\" height=\"5\" x=\"14\" y=\"3\" rx=\"1\"\u002F>\u003Crect width=\"7\" height=\"9\" x=\"14\" y=\"12\" rx=\"1\"\u002F>\u003Crect width=\"7\" height=\"5\" x=\"3\" y=\"16\" rx=\"1\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":226},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z\"\u002F>\u003Ccircle cx=\"16.5\" cy=\"7.5\" r=\".5\" fill=\"currentColor\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":228},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M15 6a9 9 0 0 0-9 9V3\"\u002F>\u003Ccircle cx=\"18\" cy=\"6\" r=\"3\"\u002F>\u003Ccircle cx=\"6\" cy=\"18\" r=\"3\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":230},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M7 7h10v10M7 17L17 7\"\u002F>",1792789524323]