Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee

feat: snapshot testing improvements

pdewey.com 918a7566 3ad8ad2e

verified
+9155 -37
+2
go.mod
··· 32 32 github.com/prometheus/common v0.45.0 // indirect 33 33 github.com/prometheus/procfs v0.12.0 // indirect 34 34 github.com/ptdewey/shutter v0.1.4 // indirect 35 + github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 // indirect 35 36 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 36 37 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 37 38 golang.org/x/crypto v0.21.0 // indirect 39 + golang.org/x/net v0.23.0 // indirect 38 40 golang.org/x/sys v0.36.0 // indirect 39 41 golang.org/x/time v0.3.0 // indirect 40 42 google.golang.org/protobuf v1.33.0 // indirect
+4
go.sum
··· 79 79 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 80 80 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 81 81 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 82 + github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts= 83 + github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE= 82 84 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 83 85 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 84 86 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= ··· 87 89 go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= 88 90 golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 89 91 golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 92 + golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 93 + golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 90 94 golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 91 95 golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 92 96 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+53
internal/bff/__snapshots__/bean_form_with_nil_roasters.snap
··· 1 + --- 2 + title: bean form with nil roasters 3 + test_name: TestNewBeanForm_Snapshot/bean_form_with_nil_roasters 4 + file_name: form_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 8 + <h4 class="font-medium mb-3 text-gray-800"> 9 + Add New Bean 10 + </h4> 11 + <div class="space-y-3"> 12 + <input type="text" x-model="newBean.name" placeholder="Name (e.g. Morning Blend, House Espresso) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 13 + <input type="text" x-model="newBean.origin" placeholder="Origin (e.g. Ethiopia) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 14 + <select x-model="newBean.roasterRKey" name="roaster_rkey_modal" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 15 + <option value=""> 16 + Select Roaster (Optional) 17 + </option> 18 + </select> 19 + <select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 20 + <option value=""> 21 + Select Roast Level (Optional) 22 + </option> 23 + <option value="Ultra-Light"> 24 + Ultra-Light 25 + </option> 26 + <option value="Light"> 27 + Light 28 + </option> 29 + <option value="Medium-Light"> 30 + Medium-Light 31 + </option> 32 + <option value="Medium"> 33 + Medium 34 + </option> 35 + <option value="Medium-Dark"> 36 + Medium-Dark 37 + </option> 38 + <option value="Dark"> 39 + Dark 40 + </option> 41 + </select> 42 + <input type="text" x-model="newBean.process" placeholder="Process (e.g. Washed, Natural, Honey)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 43 + <input type="text" x-model="newBean.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 44 + <div class="flex gap-2"> 45 + <button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 46 + Add 47 + </button> 48 + <button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 49 + Cancel 50 + </button> 51 + </div> 52 + </div> 53 + </div>
+62
internal/bff/__snapshots__/bean_form_with_roasters.snap
··· 1 + --- 2 + title: bean form with roasters 3 + test_name: TestNewBeanForm_Snapshot/bean_form_with_roasters 4 + file_name: form_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 8 + <h4 class="font-medium mb-3 text-gray-800"> 9 + Add New Bean 10 + </h4> 11 + <div class="space-y-3"> 12 + <input type="text" x-model="newBean.name" placeholder="Name (e.g. Morning Blend, House Espresso) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 13 + <input type="text" x-model="newBean.origin" placeholder="Origin (e.g. Ethiopia) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 14 + <select x-model="newBean.roasterRKey" name="roaster_rkey_modal" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 15 + <option value=""> 16 + Select Roaster (Optional) 17 + </option> 18 + <option value="roaster1"> 19 + Blue Bottle Coffee 20 + </option> 21 + <option value="roaster2"> 22 + Counter Culture Coffee 23 + </option> 24 + <option value="roaster3"> 25 + Stumptown Coffee Roasters 26 + </option> 27 + </select> 28 + <select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 29 + <option value=""> 30 + Select Roast Level (Optional) 31 + </option> 32 + <option value="Ultra-Light"> 33 + Ultra-Light 34 + </option> 35 + <option value="Light"> 36 + Light 37 + </option> 38 + <option value="Medium-Light"> 39 + Medium-Light 40 + </option> 41 + <option value="Medium"> 42 + Medium 43 + </option> 44 + <option value="Medium-Dark"> 45 + Medium-Dark 46 + </option> 47 + <option value="Dark"> 48 + Dark 49 + </option> 50 + </select> 51 + <input type="text" x-model="newBean.process" placeholder="Process (e.g. Washed, Natural, Honey)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 52 + <input type="text" x-model="newBean.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 53 + <div class="flex gap-2"> 54 + <button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 55 + Add 56 + </button> 57 + <button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 58 + Cancel 59 + </button> 60 + </div> 61 + </div> 62 + </div>
+53
internal/bff/__snapshots__/bean_form_without_roasters.snap
··· 1 + --- 2 + title: bean form without roasters 3 + test_name: TestNewBeanForm_Snapshot/bean_form_without_roasters 4 + file_name: form_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 8 + <h4 class="font-medium mb-3 text-gray-800"> 9 + Add New Bean 10 + </h4> 11 + <div class="space-y-3"> 12 + <input type="text" x-model="newBean.name" placeholder="Name (e.g. Morning Blend, House Espresso) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 13 + <input type="text" x-model="newBean.origin" placeholder="Origin (e.g. Ethiopia) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 14 + <select x-model="newBean.roasterRKey" name="roaster_rkey_modal" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 15 + <option value=""> 16 + Select Roaster (Optional) 17 + </option> 18 + </select> 19 + <select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 20 + <option value=""> 21 + Select Roast Level (Optional) 22 + </option> 23 + <option value="Ultra-Light"> 24 + Ultra-Light 25 + </option> 26 + <option value="Light"> 27 + Light 28 + </option> 29 + <option value="Medium-Light"> 30 + Medium-Light 31 + </option> 32 + <option value="Medium"> 33 + Medium 34 + </option> 35 + <option value="Medium-Dark"> 36 + Medium-Dark 37 + </option> 38 + <option value="Dark"> 39 + Dark 40 + </option> 41 + </select> 42 + <input type="text" x-model="newBean.process" placeholder="Process (e.g. Washed, Natural, Honey)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 43 + <input type="text" x-model="newBean.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 44 + <div class="flex gap-2"> 45 + <button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 46 + Add 47 + </button> 48 + <button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 49 + Cancel 50 + </button> 51 + </div> 52 + </div> 53 + </div>
+100 -1
internal/bff/__snapshots__/bean_with_missing_optional_fields.snap
··· 4 4 file_name: profile_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n\n<div id=\"profile-stats-data\" \n data-brews=\"0\" \n data-beans=\"1\" \n data-roasters=\"0\" \n data-grinders=\"0\" \n data-brewers=\"0\"\n style=\"display: none;\"></div>\n\n\n<div x-show=\"activeTab === 'brews'\">\n \n\n<div class=\"bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300\">\n \n <p class=\"text-brown-800 text-lg mb-4 font-medium\">No brews yet! Start tracking your coffee journey.</p>\n <a href=\"/brews/new\"\n class=\"inline-block bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all shadow-lg hover:shadow-xl font-medium\">\n Add Your First Brew\n </a>\n \n</div>\n\n\n</div>\n\n\n<div x-show=\"activeTab === 'beans'\" x-cloak class=\"space-y-6\">\n \n \n <div>\n <h3 class=\"text-lg font-semibold text-brown-900 mb-3\">☕ Coffee Beans</h3>\n <div class=\"overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300\">\n <table class=\"min-w-full divide-y divide-brown-300\">\n <thead class=\"bg-brown-200/80\">\n <tr>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">Name</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">☕ Roaster</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">📍 Origin</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">🔥 Roast</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">🌱 Process</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">📝 Description</th>\n </tr>\n </thead>\n <tbody class=\"bg-brown-50/60 divide-y divide-brown-200\">\n \n <tr class=\"hover:bg-brown-100/60 transition-colors\">\n <td class=\"px-6 py-4 text-sm font-bold text-brown-900\">\n Mystery Bean\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n \n <span class=\"text-brown-400\">-</span>\n \n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n <span class=\"text-brown-400\">-</span>\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n <span class=\"text-brown-400\">-</span>\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n <span class=\"text-brown-400\">-</span>\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-700 italic max-w-xs\">\n <span class=\"text-brown-400 not-italic\">-</span>\n </td>\n </tr>\n \n </tbody>\n </table>\n </div>\n \n <div class=\"mt-3 text-center\">\n <button @click=\"editBean('', '', '', '', '', '', '')\" class=\"inline-flex items-center gap-2 bg-brown-600 text-white px-4 py-2 rounded-lg hover:bg-brown-700 transition-all shadow-md hover:shadow-lg text-sm font-medium\">\n <span>+</span>\n <span>Add New Bean</span>\n </button>\n </div>\n \n </div>\n \n\n \n \n\n \n</div>\n\n\n<div x-show=\"activeTab === 'gear'\" x-cloak class=\"space-y-6\">\n \n \n\n \n \n\n \n <div class=\"bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center text-brown-800 border border-brown-300\">\n <p class=\"font-medium\">No gear added yet.</p>\n </div>\n \n</div>\n" 7 + <div id="profile-stats-data" 8 + data-brews="0" 9 + data-beans="1" 10 + data-roasters="0" 11 + data-grinders="0" 12 + data-brewers="0" 13 + style="display: none;"></div> 14 + <div x-show="activeTab === 'brews'"> 15 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300"> 16 + <p class="text-brown-800 text-lg mb-4 font-medium"> 17 + No brews yet! Start tracking your coffee journey. 18 + </p> 19 + <a href="/brews/new" 20 + class="inline-block bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all shadow-lg hover:shadow-xl font-medium"> 21 + Add Your First Brew 22 + </a> 23 + </div> 24 + </div> 25 + <div x-show="activeTab === 'beans'" x-cloak class="space-y-6"> 26 + <div> 27 + <h3 class="text-lg font-semibold text-brown-900 mb-3"> 28 + ☕ Coffee Beans 29 + </h3> 30 + <div class="overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300"> 31 + <table class="min-w-full divide-y divide-brown-300"> 32 + <thead class="bg-brown-200/80"> 33 + <tr> 34 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 35 + Name 36 + </th> 37 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 38 + ☕ Roaster 39 + </th> 40 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 41 + 📍 Origin 42 + </th> 43 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 44 + 🔥 Roast 45 + </th> 46 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 47 + 🌱 Process 48 + </th> 49 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 50 + 📝 Description 51 + </th> 52 + </tr> 53 + </thead> 54 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 55 + <tr class="hover:bg-brown-100/60 transition-colors"> 56 + <td class="px-6 py-4 text-sm font-bold text-brown-900"> 57 + Mystery Bean 58 + </td> 59 + <td class="px-6 py-4 text-sm text-brown-900"> 60 + <span class="text-brown-400"> 61 + - 62 + </span> 63 + </td> 64 + <td class="px-6 py-4 text-sm text-brown-900"> 65 + <span class="text-brown-400"> 66 + - 67 + </span> 68 + </td> 69 + <td class="px-6 py-4 text-sm text-brown-900"> 70 + <span class="text-brown-400"> 71 + - 72 + </span> 73 + </td> 74 + <td class="px-6 py-4 text-sm text-brown-900"> 75 + <span class="text-brown-400"> 76 + - 77 + </span> 78 + </td> 79 + <td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs"> 80 + <span class="text-brown-400 not-italic"> 81 + - 82 + </span> 83 + </td> 84 + </tr> 85 + </tbody> 86 + </table> 87 + </div> 88 + <div class="mt-3 text-center"> 89 + <button @click="editBean('', '', '', '', '', '', '')" class="inline-flex items-center gap-2 bg-brown-600 text-white px-4 py-2 rounded-lg hover:bg-brown-700 transition-all shadow-md hover:shadow-lg text-sm font-medium"> 90 + <span> 91 + + 92 + </span> 93 + <span> 94 + Add New Bean 95 + </span> 96 + </button> 97 + </div> 98 + </div> 99 + </div> 100 + <div x-show="activeTab === 'gear'" x-cloak class="space-y-6"> 101 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center text-brown-800 border border-brown-300"> 102 + <p class="font-medium"> 103 + No gear added yet. 104 + </p> 105 + </div> 106 + </div>
+62 -1
internal/bff/__snapshots__/bean_with_roaster.snap
··· 4 4 file_name: feed_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n<div class=\"space-y-4\">\n \n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/roaster.pro\" class=\"flex-shrink-0\">\n \n <div class=\"w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition\">\n <span class=\"text-brown-600 text-sm\">?</span>\n </div>\n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/roaster.pro\" class=\"font-medium text-brown-900 truncate hover:text-brown-700 hover:underline\">Pro Roaster</a>\n \n <a href=\"/profile/roaster.pro\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@roaster.pro</a>\n </div>\n <span class=\"text-brown-500 text-sm\">5 minutes ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n 🫘 added a new bean\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200\">\n <div class=\"text-base mb-2\">\n <span class=\"font-bold text-brown-900\">\n Kenya AA\n </span>\n \n <span class=\"text-brown-700\"> from Onyx Coffee Lab</span>\n \n </div>\n <div class=\"text-sm text-brown-700 space-y-1\">\n \n <div><span class=\"text-brown-600\">Origin:</span> Kenya</div>\n \n \n <div><span class=\"text-brown-600\">Roast:</span> Medium</div>\n \n \n <div><span class=\"text-brown-600\">Process:</span> Natural</div>\n \n \n <div class=\"mt-2 text-brown-800 italic\">\"Sweet and fruity with notes of blueberry\"</div>\n \n </div>\n </div>\n \n </div>\n \n \n \n</div>\n" 7 + <div class="space-y-4"> 8 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 9 + <div class="flex items-center gap-3 mb-3"> 10 + <a href="/profile/roaster.pro" class="flex-shrink-0"> 11 + <div class="w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition"> 12 + <span class="text-brown-600 text-sm"> 13 + ? 14 + </span> 15 + </div> 16 + </a> 17 + <div class="flex-1 min-w-0"> 18 + <div class="flex items-center gap-2"> 19 + <a href="/profile/roaster.pro" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline"> 20 + Pro Roaster 21 + </a> 22 + <a href="/profile/roaster.pro" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 23 + @roaster.pro 24 + </a> 25 + </div> 26 + <span class="text-brown-500 text-sm"> 27 + 5 minutes ago 28 + </span> 29 + </div> 30 + </div> 31 + <div class="mb-2 text-sm text-brown-700"> 32 + 🫘 added a new bean 33 + </div> 34 + <div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200"> 35 + <div class="text-base mb-2"> 36 + <span class="font-bold text-brown-900"> 37 + Kenya AA 38 + </span> 39 + <span class="text-brown-700"> 40 + from Onyx Coffee Lab 41 + </span> 42 + </div> 43 + <div class="text-sm text-brown-700 space-y-1"> 44 + <div> 45 + <span class="text-brown-600"> 46 + Origin: 47 + </span> 48 + Kenya 49 + </div> 50 + <div> 51 + <span class="text-brown-600"> 52 + Roast: 53 + </span> 54 + Medium 55 + </div> 56 + <div> 57 + <span class="text-brown-600"> 58 + Process: 59 + </span> 60 + Natural 61 + </div> 62 + <div class="mt-2 text-brown-800 italic"> 63 + "Sweet and fruity with notes of blueberry" 64 + </div> 65 + </div> 66 + </div> 67 + </div> 68 + </div>
+59 -1
internal/bff/__snapshots__/bean_without_roaster.snap
··· 4 4 file_name: feed_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n<div class=\"space-y-4\">\n \n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/homebrewer\" class=\"flex-shrink-0\">\n \n <div class=\"w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition\">\n <span class=\"text-brown-600 text-sm\">?</span>\n </div>\n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/homebrewer\" class=\"font-medium text-brown-900 truncate hover:text-brown-700 hover:underline\">Home Brewer</a>\n \n <a href=\"/profile/homebrewer\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@homebrewer</a>\n </div>\n <span class=\"text-brown-500 text-sm\">1 day ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n 🫘 added a new bean\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200\">\n <div class=\"text-base mb-2\">\n <span class=\"font-bold text-brown-900\">\n Colombian Supremo\n </span>\n \n </div>\n <div class=\"text-sm text-brown-700 space-y-1\">\n \n <div><span class=\"text-brown-600\">Origin:</span> Colombia</div>\n \n \n <div><span class=\"text-brown-600\">Roast:</span> Medium</div>\n \n \n <div><span class=\"text-brown-600\">Process:</span> Natural</div>\n \n \n <div class=\"mt-2 text-brown-800 italic\">\"Sweet and fruity with notes of blueberry\"</div>\n \n </div>\n </div>\n \n </div>\n \n \n \n</div>\n" 7 + <div class="space-y-4"> 8 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 9 + <div class="flex items-center gap-3 mb-3"> 10 + <a href="/profile/homebrewer" class="flex-shrink-0"> 11 + <div class="w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition"> 12 + <span class="text-brown-600 text-sm"> 13 + ? 14 + </span> 15 + </div> 16 + </a> 17 + <div class="flex-1 min-w-0"> 18 + <div class="flex items-center gap-2"> 19 + <a href="/profile/homebrewer" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline"> 20 + Home Brewer 21 + </a> 22 + <a href="/profile/homebrewer" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 23 + @homebrewer 24 + </a> 25 + </div> 26 + <span class="text-brown-500 text-sm"> 27 + 1 day ago 28 + </span> 29 + </div> 30 + </div> 31 + <div class="mb-2 text-sm text-brown-700"> 32 + 🫘 added a new bean 33 + </div> 34 + <div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200"> 35 + <div class="text-base mb-2"> 36 + <span class="font-bold text-brown-900"> 37 + Colombian Supremo 38 + </span> 39 + </div> 40 + <div class="text-sm text-brown-700 space-y-1"> 41 + <div> 42 + <span class="text-brown-600"> 43 + Origin: 44 + </span> 45 + Colombia 46 + </div> 47 + <div> 48 + <span class="text-brown-600"> 49 + Roast: 50 + </span> 51 + Medium 52 + </div> 53 + <div> 54 + <span class="text-brown-600"> 55 + Process: 56 + </span> 57 + Natural 58 + </div> 59 + <div class="mt-2 text-brown-800 italic"> 60 + "Sweet and fruity with notes of blueberry" 61 + </div> 62 + </div> 63 + </div> 64 + </div> 65 + </div>
+210
internal/bff/__snapshots__/beans_empty.snap
··· 1 + --- 2 + title: beans empty 3 + test_name: TestManageContent_BeansTab_Snapshot/beans_empty 4 + file_name: partial_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="tab === 'beans'"> 8 + <div class="mb-4 flex justify-between items-center"> 9 + <h3 class="text-xl font-semibold text-brown-900"> 10 + Coffee Beans 11 + </h3> 12 + <button 13 + @click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}" 14 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 15 + + Add Bean 16 + </button> 17 + </div> 18 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 19 + No beans yet. Add your first bean to get started! 20 + </div> 21 + </div> 22 + <div x-show="tab === 'roasters'"> 23 + <div class="mb-4 flex justify-between items-center"> 24 + <h3 class="text-xl font-semibold text-brown-900"> 25 + Roasters 26 + </h3> 27 + <button 28 + @click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}" 29 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 30 + + Add Roaster 31 + </button> 32 + </div> 33 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 34 + No roasters yet. Add your first roaster! 35 + </div> 36 + </div> 37 + <div x-show="tab === 'grinders'"> 38 + <div class="mb-4 flex justify-between items-center"> 39 + <h3 class="text-xl font-semibold text-brown-900"> 40 + Grinders 41 + </h3> 42 + <button 43 + @click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}" 44 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 45 + + Add Grinder 46 + </button> 47 + </div> 48 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 49 + No grinders yet. Add your first grinder! 50 + </div> 51 + </div> 52 + <div x-show="tab === 'brewers'"> 53 + <div class="mb-4 flex justify-between items-center"> 54 + <h3 class="text-xl font-semibold text-brown-900"> 55 + Brewers 56 + </h3> 57 + <button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}" 58 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 59 + + Add Brewer 60 + </button> 61 + </div> 62 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 63 + No brewers yet. Add your first brewer! 64 + </div> 65 + </div> 66 + <div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 67 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 68 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3> 69 + <div class="space-y-4"> 70 + <input type="text" x-model="beanForm.name" placeholder="Name *" 71 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 72 + <input type="text" x-model="beanForm.origin" placeholder="Origin *" 73 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 74 + <select x-model="beanForm.roaster_rkey" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 75 + <option value=""> 76 + Select Roaster (Optional) 77 + </option> 78 + </select> 79 + <select x-model="beanForm.roast_level" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 80 + <option value=""> 81 + Select Roast Level (Optional) 82 + </option> 83 + <option value="Ultra-Light"> 84 + Ultra-Light 85 + </option> 86 + <option value="Light"> 87 + Light 88 + </option> 89 + <option value="Medium-Light"> 90 + Medium-Light 91 + </option> 92 + <option value="Medium"> 93 + Medium 94 + </option> 95 + <option value="Medium-Dark"> 96 + Medium-Dark 97 + </option> 98 + <option value="Dark"> 99 + Dark 100 + </option> 101 + </select> 102 + <input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)" 103 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 104 + <textarea x-model="beanForm.description" placeholder="Description" rows="3" 105 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 106 + <div class="flex gap-2"> 107 + <button @click="saveBean()" 108 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 109 + Save 110 + </button> 111 + <button @click="showBeanForm = false" 112 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 113 + Cancel 114 + </button> 115 + </div> 116 + </div> 117 + </div> 118 + </div> 119 + <div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 120 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 121 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3> 122 + <div class="space-y-4"> 123 + <input type="text" x-model="roasterForm.name" placeholder="Name *" 124 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 125 + <input type="text" x-model="roasterForm.location" placeholder="Location" 126 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 127 + <input type="url" x-model="roasterForm.website" placeholder="Website" 128 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 129 + <div class="flex gap-2"> 130 + <button @click="saveRoaster()" 131 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 132 + Save 133 + </button> 134 + <button @click="showRoasterForm = false" 135 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 136 + Cancel 137 + </button> 138 + </div> 139 + </div> 140 + </div> 141 + </div> 142 + <div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 143 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 144 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3> 145 + <div class="space-y-4"> 146 + <input type="text" x-model="grinderForm.name" placeholder="Name *" 147 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 148 + <select x-model="grinderForm.grinder_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 149 + <option value=""> 150 + Select Grinder Type * 151 + </option> 152 + <option value="Hand"> 153 + Hand 154 + </option> 155 + <option value="Electric"> 156 + Electric 157 + </option> 158 + <option value="Portable Electric"> 159 + Portable Electric 160 + </option> 161 + </select> 162 + <select x-model="grinderForm.burr_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 163 + <option value=""> 164 + Select Burr Type (Optional) 165 + </option> 166 + <option value="Conical"> 167 + Conical 168 + </option> 169 + <option value="Flat"> 170 + Flat 171 + </option> 172 + </select> 173 + <textarea x-model="grinderForm.notes" placeholder="Notes" rows="3" 174 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 175 + <div class="flex gap-2"> 176 + <button @click="saveGrinder()" 177 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 178 + Save 179 + </button> 180 + <button @click="showGrinderForm = false" 181 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 182 + Cancel 183 + </button> 184 + </div> 185 + </div> 186 + </div> 187 + </div> 188 + <div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 189 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 190 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3> 191 + <div class="space-y-4"> 192 + <input type="text" x-model="brewerForm.name" placeholder="Name *" 193 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 194 + <input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)" 195 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 196 + <textarea x-model="brewerForm.description" placeholder="Description" rows="3" 197 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 198 + <div class="flex gap-2"> 199 + <button @click="saveBrewer()" 200 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 201 + Save 202 + </button> 203 + <button @click="showBrewerForm = false" 204 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 205 + Cancel 206 + </button> 207 + </div> 208 + </div> 209 + </div> 210 + </div>
+353
internal/bff/__snapshots__/beans_with_roaster.snap
··· 1 + --- 2 + title: beans with roaster 3 + test_name: TestManageContent_BeansTab_Snapshot/beans_with_roaster 4 + file_name: partial_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="tab === 'beans'"> 8 + <div class="mb-4 flex justify-between items-center"> 9 + <h3 class="text-xl font-semibold text-brown-900"> 10 + Coffee Beans 11 + </h3> 12 + <button 13 + @click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}" 14 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 15 + + Add Bean 16 + </button> 17 + </div> 18 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 shadow-xl rounded-xl overflow-x-auto border border-brown-300"> 19 + <table class="min-w-full divide-y divide-brown-300"> 20 + <thead class="bg-brown-200/80"> 21 + <tr> 22 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap"> 23 + Name 24 + </th> 25 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap"> 26 + 📍 Origin 27 + </th> 28 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap"> 29 + ☕ Roaster 30 + </th> 31 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap"> 32 + 🔥 Roast Level 33 + </th> 34 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap"> 35 + 🌱 Process 36 + </th> 37 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap"> 38 + 📝 Description 39 + </th> 40 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap"> 41 + Actions 42 + </th> 43 + </tr> 44 + </thead> 45 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 46 + <tr class="hover:bg-brown-100/60 transition-colors"> 47 + <td class="px-6 py-4 text-sm font-medium text-brown-900"> 48 + Ethiopian Yirgacheffe 49 + </td> 50 + <td class="px-6 py-4 text-sm text-brown-900"> 51 + Ethiopia 52 + </td> 53 + <td class="px-6 py-4 text-sm text-brown-900"> 54 + Onyx Coffee Lab 55 + </td> 56 + <td class="px-6 py-4 text-sm text-brown-900"> 57 + Light 58 + </td> 59 + <td class="px-6 py-4 text-sm text-brown-900"> 60 + Washed 61 + </td> 62 + <td class="px-6 py-4 text-sm text-brown-700"> 63 + Bright and fruity with notes of blueberry 64 + </td> 65 + <td class="px-6 py-4 text-sm font-medium space-x-2"> 66 + <button @click="editBean('bean1', 'Ethiopian Yirgacheffe', 'Ethiopia', 'Light', 'Washed', 'Bright and fruity with notes of blueberry', 'roaster1')" 67 + class="text-brown-700 hover:text-brown-900 font-medium"> 68 + Edit 69 + </button> 70 + <button @click="deleteBean('bean1')" 71 + class="text-brown-600 hover:text-brown-800 font-medium"> 72 + Delete 73 + </button> 74 + </td> 75 + </tr> 76 + <tr class="hover:bg-brown-100/60 transition-colors"> 77 + <td class="px-6 py-4 text-sm font-medium text-brown-900"></td> 78 + <td class="px-6 py-4 text-sm text-brown-900"> 79 + Colombia 80 + </td> 81 + <td class="px-6 py-4 text-sm text-brown-900"> 82 + <span class="text-brown-400"> 83 + - 84 + </span> 85 + </td> 86 + <td class="px-6 py-4 text-sm text-brown-900"> 87 + Medium 88 + </td> 89 + <td class="px-6 py-4 text-sm text-brown-900"></td> 90 + <td class="px-6 py-4 text-sm text-brown-700"></td> 91 + <td class="px-6 py-4 text-sm font-medium space-x-2"> 92 + <button @click="editBean('bean2', '', 'Colombia', 'Medium', '', '', '')" 93 + class="text-brown-700 hover:text-brown-900 font-medium"> 94 + Edit 95 + </button> 96 + <button @click="deleteBean('bean2')" 97 + class="text-brown-600 hover:text-brown-800 font-medium"> 98 + Delete 99 + </button> 100 + </td> 101 + </tr> 102 + </tbody> 103 + </table> 104 + </div> 105 + </div> 106 + <div x-show="tab === 'roasters'"> 107 + <div class="mb-4 flex justify-between items-center"> 108 + <h3 class="text-xl font-semibold text-brown-900"> 109 + Roasters 110 + </h3> 111 + <button 112 + @click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}" 113 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 114 + + Add Roaster 115 + </button> 116 + </div> 117 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 shadow-xl rounded-xl overflow-x-auto border border-brown-300"> 118 + <table class="min-w-full divide-y divide-brown-300"> 119 + <thead class="bg-brown-200/80"> 120 + <tr> 121 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 122 + Name 123 + </th> 124 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 125 + 📍 Location 126 + </th> 127 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 128 + 🌐 Website 129 + </th> 130 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 131 + Actions 132 + </th> 133 + </tr> 134 + </thead> 135 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 136 + <tr class="hover:bg-brown-100/60 transition-colors"> 137 + <td class="px-6 py-4 text-sm font-medium text-brown-900"> 138 + Onyx Coffee Lab 139 + </td> 140 + <td class="px-6 py-4 text-sm text-brown-900"></td> 141 + <td class="px-6 py-4 text-sm text-brown-900"></td> 142 + <td class="px-6 py-4 text-sm font-medium space-x-2"> 143 + <button @click="editRoaster('roaster1', 'Onyx Coffee Lab', '', '')" 144 + class="text-brown-700 hover:text-brown-900 font-medium"> 145 + Edit 146 + </button> 147 + <button @click="deleteRoaster('roaster1')" 148 + class="text-brown-600 hover:text-brown-800 font-medium"> 149 + Delete 150 + </button> 151 + </td> 152 + </tr> 153 + <tr class="hover:bg-brown-100/60 transition-colors"> 154 + <td class="px-6 py-4 text-sm font-medium text-brown-900"> 155 + Counter Culture 156 + </td> 157 + <td class="px-6 py-4 text-sm text-brown-900"></td> 158 + <td class="px-6 py-4 text-sm text-brown-900"></td> 159 + <td class="px-6 py-4 text-sm font-medium space-x-2"> 160 + <button @click="editRoaster('roaster2', 'Counter Culture', '', '')" 161 + class="text-brown-700 hover:text-brown-900 font-medium"> 162 + Edit 163 + </button> 164 + <button @click="deleteRoaster('roaster2')" 165 + class="text-brown-600 hover:text-brown-800 font-medium"> 166 + Delete 167 + </button> 168 + </td> 169 + </tr> 170 + </tbody> 171 + </table> 172 + </div> 173 + </div> 174 + <div x-show="tab === 'grinders'"> 175 + <div class="mb-4 flex justify-between items-center"> 176 + <h3 class="text-xl font-semibold text-brown-900"> 177 + Grinders 178 + </h3> 179 + <button 180 + @click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}" 181 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 182 + + Add Grinder 183 + </button> 184 + </div> 185 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 186 + No grinders yet. Add your first grinder! 187 + </div> 188 + </div> 189 + <div x-show="tab === 'brewers'"> 190 + <div class="mb-4 flex justify-between items-center"> 191 + <h3 class="text-xl font-semibold text-brown-900"> 192 + Brewers 193 + </h3> 194 + <button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}" 195 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 196 + + Add Brewer 197 + </button> 198 + </div> 199 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 200 + No brewers yet. Add your first brewer! 201 + </div> 202 + </div> 203 + <div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 204 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 205 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3> 206 + <div class="space-y-4"> 207 + <input type="text" x-model="beanForm.name" placeholder="Name *" 208 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 209 + <input type="text" x-model="beanForm.origin" placeholder="Origin *" 210 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 211 + <select x-model="beanForm.roaster_rkey" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 212 + <option value=""> 213 + Select Roaster (Optional) 214 + </option> 215 + <option value="roaster1"> 216 + Onyx Coffee Lab 217 + </option> 218 + <option value="roaster2"> 219 + Counter Culture 220 + </option> 221 + </select> 222 + <select x-model="beanForm.roast_level" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 223 + <option value=""> 224 + Select Roast Level (Optional) 225 + </option> 226 + <option value="Ultra-Light"> 227 + Ultra-Light 228 + </option> 229 + <option value="Light"> 230 + Light 231 + </option> 232 + <option value="Medium-Light"> 233 + Medium-Light 234 + </option> 235 + <option value="Medium"> 236 + Medium 237 + </option> 238 + <option value="Medium-Dark"> 239 + Medium-Dark 240 + </option> 241 + <option value="Dark"> 242 + Dark 243 + </option> 244 + </select> 245 + <input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)" 246 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 247 + <textarea x-model="beanForm.description" placeholder="Description" rows="3" 248 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 249 + <div class="flex gap-2"> 250 + <button @click="saveBean()" 251 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 252 + Save 253 + </button> 254 + <button @click="showBeanForm = false" 255 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 256 + Cancel 257 + </button> 258 + </div> 259 + </div> 260 + </div> 261 + </div> 262 + <div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 263 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 264 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3> 265 + <div class="space-y-4"> 266 + <input type="text" x-model="roasterForm.name" placeholder="Name *" 267 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 268 + <input type="text" x-model="roasterForm.location" placeholder="Location" 269 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 270 + <input type="url" x-model="roasterForm.website" placeholder="Website" 271 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 272 + <div class="flex gap-2"> 273 + <button @click="saveRoaster()" 274 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 275 + Save 276 + </button> 277 + <button @click="showRoasterForm = false" 278 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 279 + Cancel 280 + </button> 281 + </div> 282 + </div> 283 + </div> 284 + </div> 285 + <div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 286 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 287 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3> 288 + <div class="space-y-4"> 289 + <input type="text" x-model="grinderForm.name" placeholder="Name *" 290 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 291 + <select x-model="grinderForm.grinder_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 292 + <option value=""> 293 + Select Grinder Type * 294 + </option> 295 + <option value="Hand"> 296 + Hand 297 + </option> 298 + <option value="Electric"> 299 + Electric 300 + </option> 301 + <option value="Portable Electric"> 302 + Portable Electric 303 + </option> 304 + </select> 305 + <select x-model="grinderForm.burr_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 306 + <option value=""> 307 + Select Burr Type (Optional) 308 + </option> 309 + <option value="Conical"> 310 + Conical 311 + </option> 312 + <option value="Flat"> 313 + Flat 314 + </option> 315 + </select> 316 + <textarea x-model="grinderForm.notes" placeholder="Notes" rows="3" 317 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 318 + <div class="flex gap-2"> 319 + <button @click="saveGrinder()" 320 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 321 + Save 322 + </button> 323 + <button @click="showGrinderForm = false" 324 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 325 + Cancel 326 + </button> 327 + </div> 328 + </div> 329 + </div> 330 + </div> 331 + <div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 332 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 333 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3> 334 + <div class="space-y-4"> 335 + <input type="text" x-model="brewerForm.name" placeholder="Name *" 336 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 337 + <input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)" 338 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 339 + <textarea x-model="brewerForm.description" placeholder="Description" rows="3" 340 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 341 + <div class="flex gap-2"> 342 + <button @click="saveBrewer()" 343 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 344 + Save 345 + </button> 346 + <button @click="showBrewerForm = false" 347 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 348 + Cancel 349 + </button> 350 + </div> 351 + </div> 352 + </div> 353 + </div>
+270
internal/bff/__snapshots__/beans_with_special_characters_and_html.snap
··· 1 + --- 2 + title: beans with special characters and html 3 + test_name: TestManageContent_SpecialCharacters_Snapshot/beans_with_special_characters_and_html 4 + file_name: partial_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="tab === 'beans'"> 8 + <div class="mb-4 flex justify-between items-center"> 9 + <h3 class="text-xl font-semibold text-brown-900"> 10 + Coffee Beans 11 + </h3> 12 + <button 13 + @click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}" 14 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 15 + + Add Bean 16 + </button> 17 + </div> 18 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 shadow-xl rounded-xl overflow-x-auto border border-brown-300"> 19 + <table class="min-w-full divide-y divide-brown-300"> 20 + <thead class="bg-brown-200/80"> 21 + <tr> 22 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap"> 23 + Name 24 + </th> 25 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap"> 26 + 📍 Origin 27 + </th> 28 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap"> 29 + ☕ Roaster 30 + </th> 31 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap"> 32 + 🔥 Roast Level 33 + </th> 34 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap"> 35 + 🌱 Process 36 + </th> 37 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap"> 38 + 📝 Description 39 + </th> 40 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap"> 41 + Actions 42 + </th> 43 + </tr> 44 + </thead> 45 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 46 + <tr class="hover:bg-brown-100/60 transition-colors"> 47 + <td class="px-6 py-4 text-sm font-medium text-brown-900"> 48 + Café &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt; Especial 49 + </td> 50 + <td class="px-6 py-4 text-sm text-brown-900"> 51 + Costa Rica™ 52 + </td> 53 + <td class="px-6 py-4 text-sm text-brown-900"> 54 + <span class="text-brown-400"> 55 + - 56 + </span> 57 + </td> 58 + <td class="px-6 py-4 text-sm text-brown-900"> 59 + Medium 60 + </td> 61 + <td class="px-6 py-4 text-sm text-brown-900"> 62 + Honey &amp; Washed 63 + </td> 64 + <td class="px-6 py-4 text-sm text-brown-700"> 65 + &#34;Amazing&#34; coffee with &lt;strong&gt;bold&lt;/strong&gt; flavor 66 + </td> 67 + <td class="px-6 py-4 text-sm font-medium space-x-2"> 68 + <button @click="editBean('bean1', 'Café &lt;script&gt;alert(\&#39;xss\&#39;)&lt;/script&gt; Especial', 'Costa Rica™', 'Medium', 'Honey &amp; Washed', '\&#34;Amazing\&#34; coffee with &lt;strong&gt;bold&lt;/strong&gt; flavor', '')" 69 + class="text-brown-700 hover:text-brown-900 font-medium"> 70 + Edit 71 + </button> 72 + <button @click="deleteBean('bean1')" 73 + class="text-brown-600 hover:text-brown-800 font-medium"> 74 + Delete 75 + </button> 76 + </td> 77 + </tr> 78 + </tbody> 79 + </table> 80 + </div> 81 + </div> 82 + <div x-show="tab === 'roasters'"> 83 + <div class="mb-4 flex justify-between items-center"> 84 + <h3 class="text-xl font-semibold text-brown-900"> 85 + Roasters 86 + </h3> 87 + <button 88 + @click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}" 89 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 90 + + Add Roaster 91 + </button> 92 + </div> 93 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 94 + No roasters yet. Add your first roaster! 95 + </div> 96 + </div> 97 + <div x-show="tab === 'grinders'"> 98 + <div class="mb-4 flex justify-between items-center"> 99 + <h3 class="text-xl font-semibold text-brown-900"> 100 + Grinders 101 + </h3> 102 + <button 103 + @click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}" 104 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 105 + + Add Grinder 106 + </button> 107 + </div> 108 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 109 + No grinders yet. Add your first grinder! 110 + </div> 111 + </div> 112 + <div x-show="tab === 'brewers'"> 113 + <div class="mb-4 flex justify-between items-center"> 114 + <h3 class="text-xl font-semibold text-brown-900"> 115 + Brewers 116 + </h3> 117 + <button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}" 118 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 119 + + Add Brewer 120 + </button> 121 + </div> 122 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 123 + No brewers yet. Add your first brewer! 124 + </div> 125 + </div> 126 + <div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 127 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 128 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3> 129 + <div class="space-y-4"> 130 + <input type="text" x-model="beanForm.name" placeholder="Name *" 131 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 132 + <input type="text" x-model="beanForm.origin" placeholder="Origin *" 133 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 134 + <select x-model="beanForm.roaster_rkey" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 135 + <option value=""> 136 + Select Roaster (Optional) 137 + </option> 138 + </select> 139 + <select x-model="beanForm.roast_level" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 140 + <option value=""> 141 + Select Roast Level (Optional) 142 + </option> 143 + <option value="Ultra-Light"> 144 + Ultra-Light 145 + </option> 146 + <option value="Light"> 147 + Light 148 + </option> 149 + <option value="Medium-Light"> 150 + Medium-Light 151 + </option> 152 + <option value="Medium"> 153 + Medium 154 + </option> 155 + <option value="Medium-Dark"> 156 + Medium-Dark 157 + </option> 158 + <option value="Dark"> 159 + Dark 160 + </option> 161 + </select> 162 + <input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)" 163 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 164 + <textarea x-model="beanForm.description" placeholder="Description" rows="3" 165 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 166 + <div class="flex gap-2"> 167 + <button @click="saveBean()" 168 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 169 + Save 170 + </button> 171 + <button @click="showBeanForm = false" 172 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 173 + Cancel 174 + </button> 175 + </div> 176 + </div> 177 + </div> 178 + </div> 179 + <div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 180 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 181 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3> 182 + <div class="space-y-4"> 183 + <input type="text" x-model="roasterForm.name" placeholder="Name *" 184 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 185 + <input type="text" x-model="roasterForm.location" placeholder="Location" 186 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 187 + <input type="url" x-model="roasterForm.website" placeholder="Website" 188 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 189 + <div class="flex gap-2"> 190 + <button @click="saveRoaster()" 191 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 192 + Save 193 + </button> 194 + <button @click="showRoasterForm = false" 195 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 196 + Cancel 197 + </button> 198 + </div> 199 + </div> 200 + </div> 201 + </div> 202 + <div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 203 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 204 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3> 205 + <div class="space-y-4"> 206 + <input type="text" x-model="grinderForm.name" placeholder="Name *" 207 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 208 + <select x-model="grinderForm.grinder_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 209 + <option value=""> 210 + Select Grinder Type * 211 + </option> 212 + <option value="Hand"> 213 + Hand 214 + </option> 215 + <option value="Electric"> 216 + Electric 217 + </option> 218 + <option value="Portable Electric"> 219 + Portable Electric 220 + </option> 221 + </select> 222 + <select x-model="grinderForm.burr_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 223 + <option value=""> 224 + Select Burr Type (Optional) 225 + </option> 226 + <option value="Conical"> 227 + Conical 228 + </option> 229 + <option value="Flat"> 230 + Flat 231 + </option> 232 + </select> 233 + <textarea x-model="grinderForm.notes" placeholder="Notes" rows="3" 234 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 235 + <div class="flex gap-2"> 236 + <button @click="saveGrinder()" 237 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 238 + Save 239 + </button> 240 + <button @click="showGrinderForm = false" 241 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 242 + Cancel 243 + </button> 244 + </div> 245 + </div> 246 + </div> 247 + </div> 248 + <div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 249 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 250 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3> 251 + <div class="space-y-4"> 252 + <input type="text" x-model="brewerForm.name" placeholder="Name *" 253 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 254 + <input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)" 255 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 256 + <textarea x-model="brewerForm.description" placeholder="Description" rows="3" 257 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 258 + <div class="flex gap-2"> 259 + <button @click="saveBrewer()" 260 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 261 + Save 262 + </button> 263 + <button @click="showBrewerForm = false" 264 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 265 + Cancel 266 + </button> 267 + </div> 268 + </div> 269 + </div> 270 + </div>
+76
internal/bff/__snapshots__/brew_list_minimal_data.snap
··· 1 + --- 2 + title: brew list minimal data 3 + test_name: TestBrewListContent_Snapshot/brew_list_minimal_data 4 + file_name: partial_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div class="overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300"> 8 + <table class="min-w-full divide-y divide-brown-300"> 9 + <thead class="bg-brown-200/80"> 10 + <tr> 11 + <th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 12 + Date 13 + </th> 14 + <th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 15 + Bean 16 + </th> 17 + <th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 18 + Brewer 19 + </th> 20 + <th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 21 + Variables 22 + </th> 23 + <th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 24 + Notes 25 + </th> 26 + <th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 27 + Rating 28 + </th> 29 + <th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 30 + Actions 31 + </th> 32 + </tr> 33 + </thead> 34 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 35 + <tr class="hover:bg-brown-100/60 transition-colors"> 36 + <td class="px-4 py-4 whitespace-nowrap text-sm text-brown-900 font-medium align-top"> 37 + <div> 38 + Jan 15 39 + </div> 40 + <div class="text-xs text-brown-600"> 41 + 2024 42 + </div> 43 + </td> 44 + <td class="px-4 py-4 text-sm text-brown-900 align-top"> 45 + <span class="text-brown-400"> 46 + - 47 + </span> 48 + </td> 49 + <td class="px-4 py-4 text-sm text-brown-900 align-top"> 50 + <span class="text-brown-400"> 51 + - 52 + </span> 53 + </td> 54 + <td class="px-4 py-4 text-xs text-brown-700 align-top"> 55 + <div class="space-y-1"></div> 56 + </td> 57 + <td class="px-4 py-4 text-xs text-brown-800 align-top max-w-xs"> 58 + <span class="text-brown-400"> 59 + - 60 + </span> 61 + </td> 62 + <td class="px-4 py-4 whitespace-nowrap text-sm text-brown-900 align-top"> 63 + <span class="text-brown-400"> 64 + - 65 + </span> 66 + </td> 67 + <td class="px-4 py-4 whitespace-nowrap text-sm font-medium space-x-2 align-top"> 68 + <a href="/brews/brew3" 69 + class="text-brown-700 hover:text-brown-900 font-medium"> 70 + View 71 + </a> 72 + </td> 73 + </tr> 74 + </tbody> 75 + </table> 76 + </div>
+182
internal/bff/__snapshots__/brew_list_with_complete_data.snap
··· 1 + --- 2 + title: brew list with complete data 3 + test_name: TestBrewListContent_Snapshot/brew_list_with_complete_data 4 + file_name: partial_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div class="overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300"> 8 + <table class="min-w-full divide-y divide-brown-300"> 9 + <thead class="bg-brown-200/80"> 10 + <tr> 11 + <th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 12 + Date 13 + </th> 14 + <th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 15 + Bean 16 + </th> 17 + <th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 18 + Brewer 19 + </th> 20 + <th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 21 + Variables 22 + </th> 23 + <th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 24 + Notes 25 + </th> 26 + <th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 27 + Rating 28 + </th> 29 + <th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 30 + Actions 31 + </th> 32 + </tr> 33 + </thead> 34 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 35 + <tr class="hover:bg-brown-100/60 transition-colors"> 36 + <td class="px-4 py-4 whitespace-nowrap text-sm text-brown-900 font-medium align-top"> 37 + <div> 38 + Jan 15 39 + </div> 40 + <div class="text-xs text-brown-600"> 41 + 2024 42 + </div> 43 + </td> 44 + <td class="px-4 py-4 text-sm text-brown-900 align-top"> 45 + <div class="font-bold text-brown-900"> 46 + Ethiopian Yirgacheffe 47 + </div> 48 + <div class="text-xs text-brown-700 mt-0.5"> 49 + <span class="font-medium"> 50 + Onyx Coffee Lab 51 + </span> 52 + </div> 53 + <div class="text-xs text-brown-600 mt-0.5 flex flex-wrap gap-x-2 gap-y-0.5"> 54 + <span class="inline-flex items-center gap-0.5"> 55 + 📍 Ethiopia 56 + </span> 57 + <span class="inline-flex items-center gap-0.5"> 58 + 🔥 Light 59 + </span> 60 + <span class="inline-flex items-center gap-0.5"> 61 + ⚖️ 18g 62 + </span> 63 + </div> 64 + </td> 65 + <td class="px-4 py-4 text-sm text-brown-900 align-top"> 66 + <div class="font-medium text-brown-900"> 67 + Hario V60 68 + </div> 69 + </td> 70 + <td class="px-4 py-4 text-xs text-brown-700 align-top"> 71 + <div class="space-y-1"> 72 + <div> 73 + <span class="text-brown-600"> 74 + Grinder: 75 + </span> 76 + Comandante C40 (Medium-fine) 77 + </div> 78 + <div> 79 + <span class="text-brown-600"> 80 + Temp: 81 + </span> 82 + 93.0°C 83 + </div> 84 + <div> 85 + <span class="text-brown-600"> 86 + Pours: 87 + </span> 88 + </div> 89 + <div class="pl-2 text-brown-600"> 90 + • 50g @ 30s 91 + </div> 92 + <div class="pl-2 text-brown-600"> 93 + • 100g @ 45s 94 + </div> 95 + <div class="pl-2 text-brown-600"> 96 + • 100g @ 1m 97 + </div> 98 + <div> 99 + <span class="text-brown-600"> 100 + Time: 101 + </span> 102 + 3m 103 + </div> 104 + </div> 105 + </td> 106 + <td class="px-4 py-4 text-xs text-brown-800 align-top max-w-xs"> 107 + <div class="italic line-clamp-3"> 108 + Bright citrus notes with floral aroma. Clean finish. 109 + </div> 110 + </td> 111 + <td class="px-4 py-4 whitespace-nowrap text-sm text-brown-900 align-top"> 112 + <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-900"> 113 + ⭐ 8/10 114 + </span> 115 + </td> 116 + <td class="px-4 py-4 whitespace-nowrap text-sm font-medium space-x-2 align-top"> 117 + <a href="/brews/brew1" 118 + class="text-brown-700 hover:text-brown-900 font-medium"> 119 + View 120 + </a> 121 + <button hx-delete="/brews/brew1" 122 + hx-confirm="Are you sure you want to delete this brew?" hx-target="closest tr" 123 + hx-swap="outerHTML swap:1s" class="text-brown-600 hover:text-brown-800 font-medium"> 124 + Delete 125 + </button> 126 + </td> 127 + </tr> 128 + <tr class="hover:bg-brown-100/60 transition-colors"> 129 + <td class="px-4 py-4 whitespace-nowrap text-sm text-brown-900 font-medium align-top"> 130 + <div> 131 + Jan 14 132 + </div> 133 + <div class="text-xs text-brown-600"> 134 + 2024 135 + </div> 136 + </td> 137 + <td class="px-4 py-4 text-sm text-brown-900 align-top"> 138 + <div class="font-bold text-brown-900"> 139 + Colombia 140 + </div> 141 + <div class="text-xs text-brown-600 mt-0.5 flex flex-wrap gap-x-2 gap-y-0.5"> 142 + <span class="inline-flex items-center gap-0.5"> 143 + 📍 Colombia 144 + </span> 145 + <span class="inline-flex items-center gap-0.5"> 146 + 🔥 Medium 147 + </span> 148 + </div> 149 + </td> 150 + <td class="px-4 py-4 text-sm text-brown-900 align-top"> 151 + <div class="font-medium text-brown-900"> 152 + AeroPress 153 + </div> 154 + </td> 155 + <td class="px-4 py-4 text-xs text-brown-700 align-top"> 156 + <div class="space-y-1"></div> 157 + </td> 158 + <td class="px-4 py-4 text-xs text-brown-800 align-top max-w-xs"> 159 + <span class="text-brown-400"> 160 + - 161 + </span> 162 + </td> 163 + <td class="px-4 py-4 whitespace-nowrap text-sm text-brown-900 align-top"> 164 + <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-900"> 165 + ⭐ 6/10 166 + </span> 167 + </td> 168 + <td class="px-4 py-4 whitespace-nowrap text-sm font-medium space-x-2 align-top"> 169 + <a href="/brews/brew2" 170 + class="text-brown-700 hover:text-brown-900 font-medium"> 171 + View 172 + </a> 173 + <button hx-delete="/brews/brew2" 174 + hx-confirm="Are you sure you want to delete this brew?" hx-target="closest tr" 175 + hx-swap="outerHTML swap:1s" class="text-brown-600 hover:text-brown-800 font-medium"> 176 + Delete 177 + </button> 178 + </td> 179 + </tr> 180 + </tbody> 181 + </table> 182 + </div>
+348
internal/bff/__snapshots__/brew_with_html_in_tasting_notes.snap
··· 1 + --- 2 + title: brew with html in tasting notes 3 + test_name: TestBrewForm_SpecialCharacters_Snapshot/brew_with_html_in_tasting_notes 4 + file_name: form_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <script src="/static/js/brew-form.js"></script> 8 + <div class="max-w-2xl mx-auto"> 9 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300"> 10 + <h2 class="text-3xl font-bold text-brown-900 mb-6"> 11 + Edit Brew 12 + </h2> 13 + <form 14 + 15 + hx-put="/brews/brew1" 16 + 17 + hx-target="body" 18 + class="space-y-6" 19 + x-data="brewForm()" 20 + > 21 + <div> 22 + <label class="block text-sm font-medium text-brown-900 mb-2"> 23 + Coffee Bean 24 + </label> 25 + <div class="flex gap-2"> 26 + <select 27 + name="bean_rkey" 28 + required 29 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 30 + <option value=""> 31 + Select a bean... 32 + </option> 33 + <option 34 + value="bean1" 35 + selected 36 + class="truncate"> 37 + Test &lt;strong&gt;Bean&lt;/strong&gt; (Ethiopia - Light) 38 + </option> 39 + </select> 40 + <button 41 + type="button" 42 + @click="showNewBean = true" 43 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 44 + + New 45 + </button> 46 + </div> 47 + <div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 48 + <h4 class="font-medium mb-3 text-gray-800"> 49 + Add New Bean 50 + </h4> 51 + <div class="space-y-3"> 52 + <input type="text" x-model="newBean.name" placeholder="Name (e.g. Morning Blend, House Espresso) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 53 + <input type="text" x-model="newBean.origin" placeholder="Origin (e.g. Ethiopia) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 54 + <select x-model="newBean.roasterRKey" name="roaster_rkey_modal" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 55 + <option value=""> 56 + Select Roaster (Optional) 57 + </option> 58 + </select> 59 + <select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 60 + <option value=""> 61 + Select Roast Level (Optional) 62 + </option> 63 + <option value="Ultra-Light"> 64 + Ultra-Light 65 + </option> 66 + <option value="Light"> 67 + Light 68 + </option> 69 + <option value="Medium-Light"> 70 + Medium-Light 71 + </option> 72 + <option value="Medium"> 73 + Medium 74 + </option> 75 + <option value="Medium-Dark"> 76 + Medium-Dark 77 + </option> 78 + <option value="Dark"> 79 + Dark 80 + </option> 81 + </select> 82 + <input type="text" x-model="newBean.process" placeholder="Process (e.g. Washed, Natural, Honey)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 83 + <input type="text" x-model="newBean.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 84 + <div class="flex gap-2"> 85 + <button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 86 + Add 87 + </button> 88 + <button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 89 + Cancel 90 + </button> 91 + </div> 92 + </div> 93 + </div> 94 + </div> 95 + <div> 96 + <label class="block text-sm font-medium text-brown-900 mb-2"> 97 + Coffee Amount (grams) 98 + </label> 99 + <input 100 + type="number" 101 + name="coffee_amount" 102 + step="0.1" 103 + 104 + placeholder="e.g. 18" 105 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 106 + <p class="text-sm text-brown-700 mt-1"> 107 + Amount of ground coffee used 108 + </p> 109 + </div> 110 + <div> 111 + <label class="block text-sm font-medium text-brown-900 mb-2"> 112 + Grinder 113 + </label> 114 + <div class="flex gap-2"> 115 + <select 116 + name="grinder_rkey" 117 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 118 + <option value=""> 119 + Select a grinder... 120 + </option> 121 + </select> 122 + <button 123 + type="button" 124 + @click="showNewGrinder = true" 125 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 126 + + New 127 + </button> 128 + </div> 129 + <div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 130 + <h4 class="font-medium mb-3 text-gray-800"> 131 + Add New Grinder 132 + </h4> 133 + <div class="space-y-3"> 134 + <input type="text" x-model="newGrinder.name" placeholder="Name (e.g. Baratza Encore) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 135 + <select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 136 + <option value=""> 137 + Grinder Type (Optional) 138 + </option> 139 + <option value="Hand"> 140 + Hand 141 + </option> 142 + <option value="Electric"> 143 + Electric 144 + </option> 145 + <option value="Electric Hand"> 146 + Electric Hand 147 + </option> 148 + </select> 149 + <select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 150 + <option value=""> 151 + Burr Type (Optional) 152 + </option> 153 + <option value="Conical"> 154 + Conical 155 + </option> 156 + <option value="Flat"> 157 + Flat 158 + </option> 159 + </select> 160 + <input type="text" x-model="newGrinder.notes" placeholder="Notes (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 161 + <div class="flex gap-2"> 162 + <button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 163 + Add 164 + </button> 165 + <button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 166 + Cancel 167 + </button> 168 + </div> 169 + </div> 170 + </div> 171 + </div> 172 + <div> 173 + <label class="block text-sm font-medium text-brown-900 mb-2"> 174 + Grind Size 175 + </label> 176 + <input 177 + type="text" 178 + name="grind_size" 179 + value="" 180 + placeholder="e.g. 18, Medium, 3.5, Fine" 181 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 182 + <p class="text-sm text-brown-700 mt-1"> 183 + Enter a number (grinder setting) or description (e.g. "Medium", "Fine") 184 + </p> 185 + </div> 186 + <div> 187 + <label class="block text-sm font-medium text-brown-900 mb-2"> 188 + Brew Method 189 + </label> 190 + <div class="flex gap-2"> 191 + <select 192 + name="brewer_rkey" 193 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 194 + <option value=""> 195 + Select brew method... 196 + </option> 197 + </select> 198 + <button 199 + type="button" 200 + @click="showNewBrewer = true" 201 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 202 + + New 203 + </button> 204 + </div> 205 + <div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 206 + <h4 class="font-medium mb-3 text-gray-800"> 207 + Add New Brewer 208 + </h4> 209 + <div class="space-y-3"> 210 + <input type="text" x-model="newBrewer.name" placeholder="Name (e.g. V60, AeroPress) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 211 + <input type="text" x-model="newBrewer.brewer_type" placeholder="Type (e.g. Pour-Over, Immersion)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 212 + <input type="text" x-model="newBrewer.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 213 + <div class="flex gap-2"> 214 + <button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 215 + Add 216 + </button> 217 + <button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 218 + Cancel 219 + </button> 220 + </div> 221 + </div> 222 + </div> 223 + </div> 224 + <div> 225 + <label class="block text-sm font-medium text-brown-900 mb-2"> 226 + Water Amount (grams) 227 + </label> 228 + <input 229 + type="number" 230 + name="water_amount" 231 + step="1" 232 + 233 + placeholder="e.g. 250" 234 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 235 + <p class="text-sm text-brown-700 mt-1"> 236 + Total water used (or leave empty if using pours below) 237 + </p> 238 + </div> 239 + <div> 240 + <div class="flex items-center justify-between mb-2"> 241 + <label class="block text-sm font-medium text-brown-900"> 242 + Pours (Optional) 243 + </label> 244 + <button 245 + type="button" 246 + @click="addPour()" 247 + class="text-sm bg-brown-300 text-brown-900 px-3 py-1 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 248 + + Add Pour 249 + </button> 250 + </div> 251 + <p class="text-sm text-brown-700 mb-3"> 252 + Track individual pours for bloom and subsequent additions 253 + </p> 254 + <div class="space-y-3"> 255 + <template x-for="(pour, index) in pours" :key="index"> 256 + <div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200"> 257 + <div class="flex-1"> 258 + <label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label> 259 + <input 260 + type="number" 261 + :name="'pour_water_' + index" 262 + x-model="pour.water" 263 + placeholder="Water (g)" 264 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 265 + </div> 266 + <div class="flex-1"> 267 + <label class="text-xs text-brown-700 font-medium"> 268 + Time (sec) 269 + </label> 270 + <input 271 + type="number" 272 + :name="'pour_time_' + index" 273 + x-model="pour.time" 274 + placeholder="e.g. 45" 275 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 276 + </div> 277 + <button 278 + type="button" 279 + @click="removePour(index)" 280 + class="text-brown-700 hover:text-brown-900 mt-5 font-bold" 281 + x-show="pours.length > 0"> 282 + 283 + </button> 284 + </div> 285 + </template> 286 + </div> 287 + </div> 288 + <div> 289 + <label class="block text-sm font-medium text-brown-900 mb-2"> 290 + Temperature 291 + </label> 292 + <input 293 + type="number" 294 + name="temperature" 295 + step="0.1" 296 + 297 + placeholder="e.g. 93.5" 298 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 299 + </div> 300 + <div> 301 + <label class="block text-sm font-medium text-brown-900 mb-2"> 302 + Brew Time (seconds) 303 + </label> 304 + <input 305 + type="number" 306 + name="time_seconds" 307 + 308 + placeholder="e.g. 180" 309 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 310 + </div> 311 + <div> 312 + <label class="block text-sm font-medium text-brown-900 mb-2"> 313 + Tasting Notes 314 + </label> 315 + <textarea 316 + name="tasting_notes" 317 + rows="4" 318 + placeholder="Describe the flavors, aroma, and your thoughts..." 319 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white">&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;Bright &amp; fruity, &#34;amazing&#34; taste</textarea> 320 + </div> 321 + <div> 322 + <label class="block text-sm font-medium text-brown-900 mb-2"> 323 + Rating 324 + </label> 325 + <input 326 + type="range" 327 + name="rating" 328 + min="1" 329 + max="10" 330 + value="8" 331 + x-model="rating" 332 + x-init="rating = $el.value" 333 + class="w-full accent-brown-700"/> 334 + <div class="text-center text-2xl font-bold text-brown-800"> 335 + <span x-text="rating"></span> 336 + /10 337 + </div> 338 + </div> 339 + <div> 340 + <button 341 + type="submit" 342 + class="w-full bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-xl hover:from-brown-800 hover:to-brown-900 transition-all font-semibold text-lg shadow-lg hover:shadow-xl"> 343 + Update Brew 344 + </button> 345 + </div> 346 + </form> 347 + </div> 348 + </div>
+37 -1
internal/bff/__snapshots__/brew_with_minimal_data.snap
··· 4 4 file_name: feed_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n<div class=\"space-y-4\">\n \n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/newbie\" class=\"flex-shrink-0\">\n \n <div class=\"w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition\">\n <span class=\"text-brown-600 text-sm\">?</span>\n </div>\n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/newbie\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@newbie</a>\n </div>\n <span class=\"text-brown-500 text-sm\">1 minute ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n ☕ added a new brew\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-4 border border-brown-200\">\n \n <div class=\"flex items-start justify-between gap-3 mb-3\">\n <div class=\"flex-1 min-w-0\">\n \n <div class=\"font-bold text-brown-900 text-base\">\n House Blend\n </div>\n \n <div class=\"text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5\">\n \n \n \n \n </div>\n \n </div>\n \n </div>\n \n \n \n \n \n <div class=\"grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700\">\n \n \n \n \n </div>\n\n \n </div>\n \n </div>\n \n \n \n</div>\n" 7 + <div class="space-y-4"> 8 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 9 + <div class="flex items-center gap-3 mb-3"> 10 + <a href="/profile/newbie" class="flex-shrink-0"> 11 + <div class="w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition"> 12 + <span class="text-brown-600 text-sm"> 13 + ? 14 + </span> 15 + </div> 16 + </a> 17 + <div class="flex-1 min-w-0"> 18 + <div class="flex items-center gap-2"> 19 + <a href="/profile/newbie" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 20 + @newbie 21 + </a> 22 + </div> 23 + <span class="text-brown-500 text-sm"> 24 + 1 minute ago 25 + </span> 26 + </div> 27 + </div> 28 + <div class="mb-2 text-sm text-brown-700"> 29 + ☕ added a new brew 30 + </div> 31 + <div class="bg-white/60 backdrop-blur rounded-lg p-4 border border-brown-200"> 32 + <div class="flex items-start justify-between gap-3 mb-3"> 33 + <div class="flex-1 min-w-0"> 34 + <div class="font-bold text-brown-900 text-base"> 35 + House Blend 36 + </div> 37 + <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"></div> 38 + </div> 39 + </div> 40 + <div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700"></div> 41 + </div> 42 + </div> 43 + </div>
+47 -1
internal/bff/__snapshots__/brew_with_unicode_bean_name.snap
··· 4 4 file_name: feed_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n<div class=\"space-y-4\">\n \n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/japan.coffee\" class=\"flex-shrink-0\">\n \n <div class=\"w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition\">\n <span class=\"text-brown-600 text-sm\">?</span>\n </div>\n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/japan.coffee\" class=\"font-medium text-brown-900 truncate hover:text-brown-700 hover:underline\">日本のコーヒー</a>\n \n <a href=\"/profile/japan.coffee\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@japan.coffee</a>\n </div>\n <span class=\"text-brown-500 text-sm\">3 hours ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n ☕ added a new brew\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-4 border border-brown-200\">\n \n <div class=\"flex items-start justify-between gap-3 mb-3\">\n <div class=\"flex-1 min-w-0\">\n \n <div class=\"font-bold text-brown-900 text-base\">\n コーヒー豆\n </div>\n \n <div class=\"text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5\">\n <span class=\"inline-flex items-center gap-0.5\">📍 日本</span>\n \n \n \n </div>\n \n </div>\n \n <span class=\"inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-amber-100 text-amber-900 flex-shrink-0\">\n ⭐ 8/10\n </span>\n \n </div>\n \n \n \n \n \n <div class=\"grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700\">\n \n \n \n \n </div>\n\n \n </div>\n \n </div>\n \n \n \n</div>\n" 7 + <div class="space-y-4"> 8 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 9 + <div class="flex items-center gap-3 mb-3"> 10 + <a href="/profile/japan.coffee" class="flex-shrink-0"> 11 + <div class="w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition"> 12 + <span class="text-brown-600 text-sm"> 13 + ? 14 + </span> 15 + </div> 16 + </a> 17 + <div class="flex-1 min-w-0"> 18 + <div class="flex items-center gap-2"> 19 + <a href="/profile/japan.coffee" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline"> 20 + 日本のコーヒー 21 + </a> 22 + <a href="/profile/japan.coffee" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 23 + @japan.coffee 24 + </a> 25 + </div> 26 + <span class="text-brown-500 text-sm"> 27 + 3 hours ago 28 + </span> 29 + </div> 30 + </div> 31 + <div class="mb-2 text-sm text-brown-700"> 32 + ☕ added a new brew 33 + </div> 34 + <div class="bg-white/60 backdrop-blur rounded-lg p-4 border border-brown-200"> 35 + <div class="flex items-start justify-between gap-3 mb-3"> 36 + <div class="flex-1 min-w-0"> 37 + <div class="font-bold text-brown-900 text-base"> 38 + コーヒー豆 39 + </div> 40 + <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 41 + <span class="inline-flex items-center gap-0.5"> 42 + 📍 日本 43 + </span> 44 + </div> 45 + </div> 46 + <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-amber-100 text-amber-900 flex-shrink-0"> 47 + ⭐ 8/10 48 + </span> 49 + </div> 50 + <div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700"></div> 51 + </div> 52 + </div> 53 + </div>
+364
internal/bff/__snapshots__/brew_with_unicode_characters.snap
··· 1 + --- 2 + title: brew with unicode characters 3 + test_name: TestBrewForm_SpecialCharacters_Snapshot/brew_with_unicode_characters 4 + file_name: form_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <script src="/static/js/brew-form.js"></script> 8 + <div class="max-w-2xl mx-auto"> 9 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300"> 10 + <h2 class="text-3xl font-bold text-brown-900 mb-6"> 11 + Edit Brew 12 + </h2> 13 + <form 14 + 15 + hx-put="/brews/brew2" 16 + 17 + hx-target="body" 18 + class="space-y-6" 19 + x-data="brewForm()" 20 + > 21 + <div> 22 + <label class="block text-sm font-medium text-brown-900 mb-2"> 23 + Coffee Bean 24 + </label> 25 + <div class="flex gap-2"> 26 + <select 27 + name="bean_rkey" 28 + required 29 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 30 + <option value=""> 31 + Select a bean... 32 + </option> 33 + <option 34 + value="bean1" 35 + selected 36 + class="truncate"> 37 + Café Especial™ (Costa Rica - Medium) 38 + </option> 39 + </select> 40 + <button 41 + type="button" 42 + @click="showNewBean = true" 43 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 44 + + New 45 + </button> 46 + </div> 47 + <div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 48 + <h4 class="font-medium mb-3 text-gray-800"> 49 + Add New Bean 50 + </h4> 51 + <div class="space-y-3"> 52 + <input type="text" x-model="newBean.name" placeholder="Name (e.g. Morning Blend, House Espresso) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 53 + <input type="text" x-model="newBean.origin" placeholder="Origin (e.g. Ethiopia) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 54 + <select x-model="newBean.roasterRKey" name="roaster_rkey_modal" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 55 + <option value=""> 56 + Select Roaster (Optional) 57 + </option> 58 + </select> 59 + <select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 60 + <option value=""> 61 + Select Roast Level (Optional) 62 + </option> 63 + <option value="Ultra-Light"> 64 + Ultra-Light 65 + </option> 66 + <option value="Light"> 67 + Light 68 + </option> 69 + <option value="Medium-Light"> 70 + Medium-Light 71 + </option> 72 + <option value="Medium"> 73 + Medium 74 + </option> 75 + <option value="Medium-Dark"> 76 + Medium-Dark 77 + </option> 78 + <option value="Dark"> 79 + Dark 80 + </option> 81 + </select> 82 + <input type="text" x-model="newBean.process" placeholder="Process (e.g. Washed, Natural, Honey)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 83 + <input type="text" x-model="newBean.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 84 + <div class="flex gap-2"> 85 + <button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 86 + Add 87 + </button> 88 + <button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 89 + Cancel 90 + </button> 91 + </div> 92 + </div> 93 + </div> 94 + </div> 95 + <div> 96 + <label class="block text-sm font-medium text-brown-900 mb-2"> 97 + Coffee Amount (grams) 98 + </label> 99 + <input 100 + type="number" 101 + name="coffee_amount" 102 + step="0.1" 103 + 104 + placeholder="e.g. 18" 105 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 106 + <p class="text-sm text-brown-700 mt-1"> 107 + Amount of ground coffee used 108 + </p> 109 + </div> 110 + <div> 111 + <label class="block text-sm font-medium text-brown-900 mb-2"> 112 + Grinder 113 + </label> 114 + <div class="flex gap-2"> 115 + <select 116 + name="grinder_rkey" 117 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 118 + <option value=""> 119 + Select a grinder... 120 + </option> 121 + <option 122 + value="grinder1" 123 + 124 + class="truncate"> 125 + Comandante® C40 MK3 126 + </option> 127 + </select> 128 + <button 129 + type="button" 130 + @click="showNewGrinder = true" 131 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 132 + + New 133 + </button> 134 + </div> 135 + <div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 136 + <h4 class="font-medium mb-3 text-gray-800"> 137 + Add New Grinder 138 + </h4> 139 + <div class="space-y-3"> 140 + <input type="text" x-model="newGrinder.name" placeholder="Name (e.g. Baratza Encore) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 141 + <select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 142 + <option value=""> 143 + Grinder Type (Optional) 144 + </option> 145 + <option value="Hand"> 146 + Hand 147 + </option> 148 + <option value="Electric"> 149 + Electric 150 + </option> 151 + <option value="Electric Hand"> 152 + Electric Hand 153 + </option> 154 + </select> 155 + <select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 156 + <option value=""> 157 + Burr Type (Optional) 158 + </option> 159 + <option value="Conical"> 160 + Conical 161 + </option> 162 + <option value="Flat"> 163 + Flat 164 + </option> 165 + </select> 166 + <input type="text" x-model="newGrinder.notes" placeholder="Notes (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 167 + <div class="flex gap-2"> 168 + <button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 169 + Add 170 + </button> 171 + <button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 172 + Cancel 173 + </button> 174 + </div> 175 + </div> 176 + </div> 177 + </div> 178 + <div> 179 + <label class="block text-sm font-medium text-brown-900 mb-2"> 180 + Grind Size 181 + </label> 182 + <input 183 + type="text" 184 + name="grind_size" 185 + value="中挽き (medium)" 186 + placeholder="e.g. 18, Medium, 3.5, Fine" 187 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 188 + <p class="text-sm text-brown-700 mt-1"> 189 + Enter a number (grinder setting) or description (e.g. "Medium", "Fine") 190 + </p> 191 + </div> 192 + <div> 193 + <label class="block text-sm font-medium text-brown-900 mb-2"> 194 + Brew Method 195 + </label> 196 + <div class="flex gap-2"> 197 + <select 198 + name="brewer_rkey" 199 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 200 + <option value=""> 201 + Select brew method... 202 + </option> 203 + <option 204 + value="brewer1" 205 + 206 + class="truncate"> 207 + Hario V60 (02) 208 + </option> 209 + </select> 210 + <button 211 + type="button" 212 + @click="showNewBrewer = true" 213 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 214 + + New 215 + </button> 216 + </div> 217 + <div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 218 + <h4 class="font-medium mb-3 text-gray-800"> 219 + Add New Brewer 220 + </h4> 221 + <div class="space-y-3"> 222 + <input type="text" x-model="newBrewer.name" placeholder="Name (e.g. V60, AeroPress) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 223 + <input type="text" x-model="newBrewer.brewer_type" placeholder="Type (e.g. Pour-Over, Immersion)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 224 + <input type="text" x-model="newBrewer.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 225 + <div class="flex gap-2"> 226 + <button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 227 + Add 228 + </button> 229 + <button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 230 + Cancel 231 + </button> 232 + </div> 233 + </div> 234 + </div> 235 + </div> 236 + <div> 237 + <label class="block text-sm font-medium text-brown-900 mb-2"> 238 + Water Amount (grams) 239 + </label> 240 + <input 241 + type="number" 242 + name="water_amount" 243 + step="1" 244 + 245 + placeholder="e.g. 250" 246 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 247 + <p class="text-sm text-brown-700 mt-1"> 248 + Total water used (or leave empty if using pours below) 249 + </p> 250 + </div> 251 + <div> 252 + <div class="flex items-center justify-between mb-2"> 253 + <label class="block text-sm font-medium text-brown-900"> 254 + Pours (Optional) 255 + </label> 256 + <button 257 + type="button" 258 + @click="addPour()" 259 + class="text-sm bg-brown-300 text-brown-900 px-3 py-1 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 260 + + Add Pour 261 + </button> 262 + </div> 263 + <p class="text-sm text-brown-700 mb-3"> 264 + Track individual pours for bloom and subsequent additions 265 + </p> 266 + <div class="space-y-3"> 267 + <template x-for="(pour, index) in pours" :key="index"> 268 + <div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200"> 269 + <div class="flex-1"> 270 + <label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label> 271 + <input 272 + type="number" 273 + :name="'pour_water_' + index" 274 + x-model="pour.water" 275 + placeholder="Water (g)" 276 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 277 + </div> 278 + <div class="flex-1"> 279 + <label class="text-xs text-brown-700 font-medium"> 280 + Time (sec) 281 + </label> 282 + <input 283 + type="number" 284 + :name="'pour_time_' + index" 285 + x-model="pour.time" 286 + placeholder="e.g. 45" 287 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 288 + </div> 289 + <button 290 + type="button" 291 + @click="removePour(index)" 292 + class="text-brown-700 hover:text-brown-900 mt-5 font-bold" 293 + x-show="pours.length > 0"> 294 + 295 + </button> 296 + </div> 297 + </template> 298 + </div> 299 + </div> 300 + <div> 301 + <label class="block text-sm font-medium text-brown-900 mb-2"> 302 + Temperature 303 + </label> 304 + <input 305 + type="number" 306 + name="temperature" 307 + step="0.1" 308 + 309 + placeholder="e.g. 93.5" 310 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 311 + </div> 312 + <div> 313 + <label class="block text-sm font-medium text-brown-900 mb-2"> 314 + Brew Time (seconds) 315 + </label> 316 + <input 317 + type="number" 318 + name="time_seconds" 319 + 320 + placeholder="e.g. 180" 321 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 322 + </div> 323 + <div> 324 + <label class="block text-sm font-medium text-brown-900 mb-2"> 325 + Tasting Notes 326 + </label> 327 + <textarea 328 + name="tasting_notes" 329 + rows="4" 330 + placeholder="Describe the flavors, aroma, and your thoughts..." 331 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white">日本のコーヒー 🇯🇵 - フルーティーで酸味が強い 332 + 333 + Яркий вкус с цитрусовыми нотами 334 + 335 + Café con notas de caramelo</textarea> 336 + </div> 337 + <div> 338 + <label class="block text-sm font-medium text-brown-900 mb-2"> 339 + Rating 340 + </label> 341 + <input 342 + type="range" 343 + name="rating" 344 + min="1" 345 + max="10" 346 + value="9" 347 + x-model="rating" 348 + x-init="rating = $el.value" 349 + class="w-full accent-brown-700"/> 350 + <div class="text-center text-2xl font-bold text-brown-800"> 351 + <span x-text="rating"></span> 352 + /10 353 + </div> 354 + </div> 355 + <div> 356 + <button 357 + type="submit" 358 + class="w-full bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-xl hover:from-brown-800 hover:to-brown-900 transition-all font-semibold text-lg shadow-lg hover:shadow-xl"> 359 + Update Brew 360 + </button> 361 + </div> 362 + </form> 363 + </div> 364 + </div>
+24
internal/bff/__snapshots__/brewer_form_renders.snap
··· 1 + --- 2 + title: brewer_form_renders 3 + test_name: TestNewBrewerForm_Snapshot/brewer_form_renders 4 + file_name: form_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 8 + <h4 class="font-medium mb-3 text-gray-800"> 9 + Add New Brewer 10 + </h4> 11 + <div class="space-y-3"> 12 + <input type="text" x-model="newBrewer.name" placeholder="Name (e.g. V60, AeroPress) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 13 + <input type="text" x-model="newBrewer.brewer_type" placeholder="Type (e.g. Pour-Over, Immersion)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 14 + <input type="text" x-model="newBrewer.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 15 + <div class="flex gap-2"> 16 + <button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 17 + Add 18 + </button> 19 + <button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 20 + Cancel 21 + </button> 22 + </div> 23 + </div> 24 + </div>
+39 -1
internal/bff/__snapshots__/brewer_item.snap
··· 4 4 file_name: feed_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n<div class=\"space-y-4\">\n \n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/pourover.fan\" class=\"flex-shrink-0\">\n \n <div class=\"w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition\">\n <span class=\"text-brown-600 text-sm\">?</span>\n </div>\n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/pourover.fan\" class=\"font-medium text-brown-900 truncate hover:text-brown-700 hover:underline\">Pour Over Fan</a>\n \n <a href=\"/profile/pourover.fan\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@pourover.fan</a>\n </div>\n <span class=\"text-brown-500 text-sm\">2 days ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n ☕ added a new brewer\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200\">\n <div class=\"text-base mb-2\">\n <span class=\"font-bold text-brown-900\">Kalita Wave 185</span>\n </div>\n \n <div class=\"text-sm text-brown-800 italic\">\"Flat-bottom dripper with wave filters\"</div>\n \n </div>\n \n </div>\n \n \n \n</div>\n" 7 + <div class="space-y-4"> 8 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 9 + <div class="flex items-center gap-3 mb-3"> 10 + <a href="/profile/pourover.fan" class="flex-shrink-0"> 11 + <div class="w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition"> 12 + <span class="text-brown-600 text-sm"> 13 + ? 14 + </span> 15 + </div> 16 + </a> 17 + <div class="flex-1 min-w-0"> 18 + <div class="flex items-center gap-2"> 19 + <a href="/profile/pourover.fan" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline"> 20 + Pour Over Fan 21 + </a> 22 + <a href="/profile/pourover.fan" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 23 + @pourover.fan 24 + </a> 25 + </div> 26 + <span class="text-brown-500 text-sm"> 27 + 2 days ago 28 + </span> 29 + </div> 30 + </div> 31 + <div class="mb-2 text-sm text-brown-700"> 32 + ☕ added a new brewer 33 + </div> 34 + <div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200"> 35 + <div class="text-base mb-2"> 36 + <span class="font-bold text-brown-900"> 37 + Kalita Wave 185 38 + </span> 39 + </div> 40 + <div class="text-sm text-brown-800 italic"> 41 + "Flat-bottom dripper with wave filters" 42 + </div> 43 + </div> 44 + </div> 45 + </div>
+210
internal/bff/__snapshots__/brewers_empty.snap
··· 1 + --- 2 + title: brewers empty 3 + test_name: TestManageContent_BrewersTab_Snapshot/brewers_empty 4 + file_name: partial_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="tab === 'beans'"> 8 + <div class="mb-4 flex justify-between items-center"> 9 + <h3 class="text-xl font-semibold text-brown-900"> 10 + Coffee Beans 11 + </h3> 12 + <button 13 + @click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}" 14 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 15 + + Add Bean 16 + </button> 17 + </div> 18 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 19 + No beans yet. Add your first bean to get started! 20 + </div> 21 + </div> 22 + <div x-show="tab === 'roasters'"> 23 + <div class="mb-4 flex justify-between items-center"> 24 + <h3 class="text-xl font-semibold text-brown-900"> 25 + Roasters 26 + </h3> 27 + <button 28 + @click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}" 29 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 30 + + Add Roaster 31 + </button> 32 + </div> 33 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 34 + No roasters yet. Add your first roaster! 35 + </div> 36 + </div> 37 + <div x-show="tab === 'grinders'"> 38 + <div class="mb-4 flex justify-between items-center"> 39 + <h3 class="text-xl font-semibold text-brown-900"> 40 + Grinders 41 + </h3> 42 + <button 43 + @click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}" 44 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 45 + + Add Grinder 46 + </button> 47 + </div> 48 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 49 + No grinders yet. Add your first grinder! 50 + </div> 51 + </div> 52 + <div x-show="tab === 'brewers'"> 53 + <div class="mb-4 flex justify-between items-center"> 54 + <h3 class="text-xl font-semibold text-brown-900"> 55 + Brewers 56 + </h3> 57 + <button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}" 58 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 59 + + Add Brewer 60 + </button> 61 + </div> 62 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 63 + No brewers yet. Add your first brewer! 64 + </div> 65 + </div> 66 + <div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 67 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 68 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3> 69 + <div class="space-y-4"> 70 + <input type="text" x-model="beanForm.name" placeholder="Name *" 71 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 72 + <input type="text" x-model="beanForm.origin" placeholder="Origin *" 73 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 74 + <select x-model="beanForm.roaster_rkey" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 75 + <option value=""> 76 + Select Roaster (Optional) 77 + </option> 78 + </select> 79 + <select x-model="beanForm.roast_level" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 80 + <option value=""> 81 + Select Roast Level (Optional) 82 + </option> 83 + <option value="Ultra-Light"> 84 + Ultra-Light 85 + </option> 86 + <option value="Light"> 87 + Light 88 + </option> 89 + <option value="Medium-Light"> 90 + Medium-Light 91 + </option> 92 + <option value="Medium"> 93 + Medium 94 + </option> 95 + <option value="Medium-Dark"> 96 + Medium-Dark 97 + </option> 98 + <option value="Dark"> 99 + Dark 100 + </option> 101 + </select> 102 + <input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)" 103 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 104 + <textarea x-model="beanForm.description" placeholder="Description" rows="3" 105 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 106 + <div class="flex gap-2"> 107 + <button @click="saveBean()" 108 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 109 + Save 110 + </button> 111 + <button @click="showBeanForm = false" 112 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 113 + Cancel 114 + </button> 115 + </div> 116 + </div> 117 + </div> 118 + </div> 119 + <div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 120 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 121 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3> 122 + <div class="space-y-4"> 123 + <input type="text" x-model="roasterForm.name" placeholder="Name *" 124 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 125 + <input type="text" x-model="roasterForm.location" placeholder="Location" 126 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 127 + <input type="url" x-model="roasterForm.website" placeholder="Website" 128 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 129 + <div class="flex gap-2"> 130 + <button @click="saveRoaster()" 131 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 132 + Save 133 + </button> 134 + <button @click="showRoasterForm = false" 135 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 136 + Cancel 137 + </button> 138 + </div> 139 + </div> 140 + </div> 141 + </div> 142 + <div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 143 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 144 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3> 145 + <div class="space-y-4"> 146 + <input type="text" x-model="grinderForm.name" placeholder="Name *" 147 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 148 + <select x-model="grinderForm.grinder_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 149 + <option value=""> 150 + Select Grinder Type * 151 + </option> 152 + <option value="Hand"> 153 + Hand 154 + </option> 155 + <option value="Electric"> 156 + Electric 157 + </option> 158 + <option value="Portable Electric"> 159 + Portable Electric 160 + </option> 161 + </select> 162 + <select x-model="grinderForm.burr_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 163 + <option value=""> 164 + Select Burr Type (Optional) 165 + </option> 166 + <option value="Conical"> 167 + Conical 168 + </option> 169 + <option value="Flat"> 170 + Flat 171 + </option> 172 + </select> 173 + <textarea x-model="grinderForm.notes" placeholder="Notes" rows="3" 174 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 175 + <div class="flex gap-2"> 176 + <button @click="saveGrinder()" 177 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 178 + Save 179 + </button> 180 + <button @click="showGrinderForm = false" 181 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 182 + Cancel 183 + </button> 184 + </div> 185 + </div> 186 + </div> 187 + </div> 188 + <div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 189 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 190 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3> 191 + <div class="space-y-4"> 192 + <input type="text" x-model="brewerForm.name" placeholder="Name *" 193 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 194 + <input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)" 195 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 196 + <textarea x-model="brewerForm.description" placeholder="Description" rows="3" 197 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 198 + <div class="flex gap-2"> 199 + <button @click="saveBrewer()" 200 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 201 + Save 202 + </button> 203 + <button @click="showBrewerForm = false" 204 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 205 + Cancel 206 + </button> 207 + </div> 208 + </div> 209 + </div> 210 + </div>
+279
internal/bff/__snapshots__/brewers_with_data.snap
··· 1 + --- 2 + title: brewers with data 3 + test_name: TestManageContent_BrewersTab_Snapshot/brewers_with_data 4 + file_name: partial_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="tab === 'beans'"> 8 + <div class="mb-4 flex justify-between items-center"> 9 + <h3 class="text-xl font-semibold text-brown-900"> 10 + Coffee Beans 11 + </h3> 12 + <button 13 + @click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}" 14 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 15 + + Add Bean 16 + </button> 17 + </div> 18 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 19 + No beans yet. Add your first bean to get started! 20 + </div> 21 + </div> 22 + <div x-show="tab === 'roasters'"> 23 + <div class="mb-4 flex justify-between items-center"> 24 + <h3 class="text-xl font-semibold text-brown-900"> 25 + Roasters 26 + </h3> 27 + <button 28 + @click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}" 29 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 30 + + Add Roaster 31 + </button> 32 + </div> 33 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 34 + No roasters yet. Add your first roaster! 35 + </div> 36 + </div> 37 + <div x-show="tab === 'grinders'"> 38 + <div class="mb-4 flex justify-between items-center"> 39 + <h3 class="text-xl font-semibold text-brown-900"> 40 + Grinders 41 + </h3> 42 + <button 43 + @click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}" 44 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 45 + + Add Grinder 46 + </button> 47 + </div> 48 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 49 + No grinders yet. Add your first grinder! 50 + </div> 51 + </div> 52 + <div x-show="tab === 'brewers'"> 53 + <div class="mb-4 flex justify-between items-center"> 54 + <h3 class="text-xl font-semibold text-brown-900"> 55 + Brewers 56 + </h3> 57 + <button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}" 58 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 59 + + Add Brewer 60 + </button> 61 + </div> 62 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 shadow-xl rounded-xl overflow-x-auto border border-brown-300"> 63 + <table class="min-w-full divide-y divide-brown-300"> 64 + <thead class="bg-brown-200/80"> 65 + <tr> 66 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 67 + Name 68 + </th> 69 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 70 + 🔧 Type 71 + </th> 72 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 73 + 📝 Description 74 + </th> 75 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 76 + Actions 77 + </th> 78 + </tr> 79 + </thead> 80 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 81 + <tr class="hover:bg-brown-100/60 transition-colors" 82 + data-rkey="brewer1" 83 + data-name="Hario V60" 84 + data-brewer-type="Pour-Over" 85 + data-description="Cone-shaped dripper for clean, bright brews"> 86 + <td class="px-6 py-4 text-sm font-medium text-brown-900"> 87 + Hario V60 88 + </td> 89 + <td class="px-6 py-4 text-sm text-brown-900"> 90 + Pour-Over 91 + </td> 92 + <td class="px-6 py-4 text-sm text-brown-700"> 93 + Cone-shaped dripper for clean, bright brews 94 + </td> 95 + <td class="px-6 py-4 text-sm font-medium space-x-2"> 96 + <button @click="editBrewerFromRow($el.closest('tr'))" 97 + class="text-brown-700 hover:text-brown-900 font-medium"> 98 + Edit 99 + </button> 100 + <button @click="deleteBrewer($el.closest('tr').dataset.rkey)" 101 + class="text-brown-600 hover:text-brown-800 font-medium"> 102 + Delete 103 + </button> 104 + </td> 105 + </tr> 106 + <tr class="hover:bg-brown-100/60 transition-colors" 107 + data-rkey="brewer2" 108 + data-name="AeroPress" 109 + data-brewer-type="" 110 + data-description=""> 111 + <td class="px-6 py-4 text-sm font-medium text-brown-900"> 112 + AeroPress 113 + </td> 114 + <td class="px-6 py-4 text-sm text-brown-900"> 115 + <span class="text-brown-400"> 116 + - 117 + </span> 118 + </td> 119 + <td class="px-6 py-4 text-sm text-brown-700"></td> 120 + <td class="px-6 py-4 text-sm font-medium space-x-2"> 121 + <button @click="editBrewerFromRow($el.closest('tr'))" 122 + class="text-brown-700 hover:text-brown-900 font-medium"> 123 + Edit 124 + </button> 125 + <button @click="deleteBrewer($el.closest('tr').dataset.rkey)" 126 + class="text-brown-600 hover:text-brown-800 font-medium"> 127 + Delete 128 + </button> 129 + </td> 130 + </tr> 131 + </tbody> 132 + </table> 133 + </div> 134 + </div> 135 + <div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 136 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 137 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3> 138 + <div class="space-y-4"> 139 + <input type="text" x-model="beanForm.name" placeholder="Name *" 140 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 141 + <input type="text" x-model="beanForm.origin" placeholder="Origin *" 142 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 143 + <select x-model="beanForm.roaster_rkey" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 144 + <option value=""> 145 + Select Roaster (Optional) 146 + </option> 147 + </select> 148 + <select x-model="beanForm.roast_level" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 149 + <option value=""> 150 + Select Roast Level (Optional) 151 + </option> 152 + <option value="Ultra-Light"> 153 + Ultra-Light 154 + </option> 155 + <option value="Light"> 156 + Light 157 + </option> 158 + <option value="Medium-Light"> 159 + Medium-Light 160 + </option> 161 + <option value="Medium"> 162 + Medium 163 + </option> 164 + <option value="Medium-Dark"> 165 + Medium-Dark 166 + </option> 167 + <option value="Dark"> 168 + Dark 169 + </option> 170 + </select> 171 + <input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)" 172 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 173 + <textarea x-model="beanForm.description" placeholder="Description" rows="3" 174 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 175 + <div class="flex gap-2"> 176 + <button @click="saveBean()" 177 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 178 + Save 179 + </button> 180 + <button @click="showBeanForm = false" 181 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 182 + Cancel 183 + </button> 184 + </div> 185 + </div> 186 + </div> 187 + </div> 188 + <div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 189 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 190 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3> 191 + <div class="space-y-4"> 192 + <input type="text" x-model="roasterForm.name" placeholder="Name *" 193 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 194 + <input type="text" x-model="roasterForm.location" placeholder="Location" 195 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 196 + <input type="url" x-model="roasterForm.website" placeholder="Website" 197 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 198 + <div class="flex gap-2"> 199 + <button @click="saveRoaster()" 200 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 201 + Save 202 + </button> 203 + <button @click="showRoasterForm = false" 204 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 205 + Cancel 206 + </button> 207 + </div> 208 + </div> 209 + </div> 210 + </div> 211 + <div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 212 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 213 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3> 214 + <div class="space-y-4"> 215 + <input type="text" x-model="grinderForm.name" placeholder="Name *" 216 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 217 + <select x-model="grinderForm.grinder_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 218 + <option value=""> 219 + Select Grinder Type * 220 + </option> 221 + <option value="Hand"> 222 + Hand 223 + </option> 224 + <option value="Electric"> 225 + Electric 226 + </option> 227 + <option value="Portable Electric"> 228 + Portable Electric 229 + </option> 230 + </select> 231 + <select x-model="grinderForm.burr_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 232 + <option value=""> 233 + Select Burr Type (Optional) 234 + </option> 235 + <option value="Conical"> 236 + Conical 237 + </option> 238 + <option value="Flat"> 239 + Flat 240 + </option> 241 + </select> 242 + <textarea x-model="grinderForm.notes" placeholder="Notes" rows="3" 243 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 244 + <div class="flex gap-2"> 245 + <button @click="saveGrinder()" 246 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 247 + Save 248 + </button> 249 + <button @click="showGrinderForm = false" 250 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 251 + Cancel 252 + </button> 253 + </div> 254 + </div> 255 + </div> 256 + </div> 257 + <div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 258 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 259 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3> 260 + <div class="space-y-4"> 261 + <input type="text" x-model="brewerForm.name" placeholder="Name *" 262 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 263 + <input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)" 264 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 265 + <textarea x-model="brewerForm.description" placeholder="Description" rows="3" 266 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 267 + <div class="flex gap-2"> 268 + <button @click="saveBrewer()" 269 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 270 + Save 271 + </button> 272 + <button @click="showBrewerForm = false" 273 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 274 + Cancel 275 + </button> 276 + </div> 277 + </div> 278 + </div> 279 + </div>
+101 -1
internal/bff/__snapshots__/complete_brew_with_all_fields.snap
··· 4 4 file_name: feed_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n<div class=\"space-y-4\">\n \n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/coffee.lover\" class=\"flex-shrink-0\">\n \n \n \n <img src=\"https://cdn.bsky.app/avatar.jpg\" alt=\"\" class=\"w-10 h-10 rounded-full object-cover hover:ring-2 hover:ring-brown-600 transition\" />\n \n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/coffee.lover\" class=\"font-medium text-brown-900 truncate hover:text-brown-700 hover:underline\">Coffee Enthusiast</a>\n \n <a href=\"/profile/coffee.lover\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@coffee.lover</a>\n </div>\n <span class=\"text-brown-500 text-sm\">2 hours ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n ☕ added a new brew\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-4 border border-brown-200\">\n \n <div class=\"flex items-start justify-between gap-3 mb-3\">\n <div class=\"flex-1 min-w-0\">\n \n <div class=\"font-bold text-brown-900 text-base\">\n Ethiopian Yirgacheffe\n </div>\n \n <div class=\"text-sm text-brown-700 mt-0.5\">\n <span class=\"font-medium\">🏪 Onyx Coffee Lab</span>\n </div>\n \n <div class=\"text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5\">\n <span class=\"inline-flex items-center gap-0.5\">📍 Ethiopia</span>\n <span class=\"inline-flex items-center gap-0.5\">🔥 Light</span>\n <span class=\"inline-flex items-center gap-0.5\">🌱 Washed</span>\n <span class=\"inline-flex items-center gap-0.5\">⚖️ 16g</span>\n </div>\n \n </div>\n \n <span class=\"inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-amber-100 text-amber-900 flex-shrink-0\">\n ⭐ 9/10\n </span>\n \n </div>\n \n \n \n <div class=\"mb-2\">\n <span class=\"text-xs text-brown-600\">Brewer:</span>\n <span class=\"text-sm font-semibold text-brown-900\">\n Hario V60\n </span>\n </div>\n \n \n \n <div class=\"grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700\">\n \n <div>\n <span class=\"text-brown-600\">Grinder:</span> 1Zpresso JX-Pro (Medium-fine)\n </div>\n \n \n <div class=\"col-span-2\">\n <span class=\"text-brown-600\">Pours:</span>\n \n <div class=\"pl-2 text-brown-600\">• 50g @ 30s</div>\n \n <div class=\"pl-2 text-brown-600\">• 100g @ 45s</div>\n \n <div class=\"pl-2 text-brown-600\">• 100g @ 1m</div>\n \n </div>\n \n \n <div>\n <span class=\"text-brown-600\">Temp:</span> 93.0°C\n </div>\n \n \n <div>\n <span class=\"text-brown-600\">Time:</span> 3m\n </div>\n \n </div>\n\n \n <div class=\"mt-3 text-sm text-brown-800 italic border-t border-brown-200 pt-2\">\n \"Bright citrus notes with floral aroma\"\n </div>\n \n </div>\n \n </div>\n \n \n \n</div>\n" 7 + <div class="space-y-4"> 8 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 9 + <div class="flex items-center gap-3 mb-3"> 10 + <a href="/profile/coffee.lover" class="flex-shrink-0"> 11 + <img src="https://cdn.bsky.app/avatar.jpg" alt="" class="w-10 h-10 rounded-full object-cover hover:ring-2 hover:ring-brown-600 transition" /> 12 + </a> 13 + <div class="flex-1 min-w-0"> 14 + <div class="flex items-center gap-2"> 15 + <a href="/profile/coffee.lover" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline"> 16 + Coffee Enthusiast 17 + </a> 18 + <a href="/profile/coffee.lover" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 19 + @coffee.lover 20 + </a> 21 + </div> 22 + <span class="text-brown-500 text-sm"> 23 + 2 hours ago 24 + </span> 25 + </div> 26 + </div> 27 + <div class="mb-2 text-sm text-brown-700"> 28 + ☕ added a new brew 29 + </div> 30 + <div class="bg-white/60 backdrop-blur rounded-lg p-4 border border-brown-200"> 31 + <div class="flex items-start justify-between gap-3 mb-3"> 32 + <div class="flex-1 min-w-0"> 33 + <div class="font-bold text-brown-900 text-base"> 34 + Ethiopian Yirgacheffe 35 + </div> 36 + <div class="text-sm text-brown-700 mt-0.5"> 37 + <span class="font-medium"> 38 + 🏪 Onyx Coffee Lab 39 + </span> 40 + </div> 41 + <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 42 + <span class="inline-flex items-center gap-0.5"> 43 + 📍 Ethiopia 44 + </span> 45 + <span class="inline-flex items-center gap-0.5"> 46 + 🔥 Light 47 + </span> 48 + <span class="inline-flex items-center gap-0.5"> 49 + 🌱 Washed 50 + </span> 51 + <span class="inline-flex items-center gap-0.5"> 52 + ⚖️ 16g 53 + </span> 54 + </div> 55 + </div> 56 + <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-amber-100 text-amber-900 flex-shrink-0"> 57 + ⭐ 9/10 58 + </span> 59 + </div> 60 + <div class="mb-2"> 61 + <span class="text-xs text-brown-600"> 62 + Brewer: 63 + </span> 64 + <span class="text-sm font-semibold text-brown-900"> 65 + Hario V60 66 + </span> 67 + </div> 68 + <div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700"> 69 + <div> 70 + <span class="text-brown-600"> 71 + Grinder: 72 + </span> 73 + 1Zpresso JX-Pro (Medium-fine) 74 + </div> 75 + <div class="col-span-2"> 76 + <span class="text-brown-600"> 77 + Pours: 78 + </span> 79 + <div class="pl-2 text-brown-600"> 80 + • 50g @ 30s 81 + </div> 82 + <div class="pl-2 text-brown-600"> 83 + • 100g @ 45s 84 + </div> 85 + <div class="pl-2 text-brown-600"> 86 + • 100g @ 1m 87 + </div> 88 + </div> 89 + <div> 90 + <span class="text-brown-600"> 91 + Temp: 92 + </span> 93 + 93.0°C 94 + </div> 95 + <div> 96 + <span class="text-brown-600"> 97 + Time: 98 + </span> 99 + 3m 100 + </div> 101 + </div> 102 + <div class="mt-3 text-sm text-brown-800 italic border-t border-brown-200 pt-2"> 103 + "Bright citrus notes with floral aroma" 104 + </div> 105 + </div> 106 + </div> 107 + </div>
+383
internal/bff/__snapshots__/edit_brew_with_complete_data.snap
··· 1 + --- 2 + title: edit brew with complete data 3 + test_name: TestBrewForm_EditBrew_Snapshot/edit_brew_with_complete_data 4 + file_name: form_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <script src="/static/js/brew-form.js"></script> 8 + <div class="max-w-2xl mx-auto"> 9 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300"> 10 + <h2 class="text-3xl font-bold text-brown-900 mb-6"> 11 + Edit Brew 12 + </h2> 13 + <form 14 + 15 + hx-put="/brews/brew123" 16 + 17 + hx-target="body" 18 + class="space-y-6" 19 + x-data="brewForm()" 20 + 21 + data-pours='[{&#34;pourNumber&#34;:1,&#34;waterAmount&#34;:50,&#34;timeSeconds&#34;:30},{&#34;pourNumber&#34;:2,&#34;waterAmount&#34;:100,&#34;timeSeconds&#34;:45},{&#34;pourNumber&#34;:3,&#34;waterAmount&#34;:150,&#34;timeSeconds&#34;:60}]' 22 + > 23 + <div> 24 + <label class="block text-sm font-medium text-brown-900 mb-2"> 25 + Coffee Bean 26 + </label> 27 + <div class="flex gap-2"> 28 + <select 29 + name="bean_rkey" 30 + required 31 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 32 + <option value=""> 33 + Select a bean... 34 + </option> 35 + <option 36 + value="bean1" 37 + selected 38 + class="truncate"> 39 + Ethiopian Yirgacheffe (Ethiopia - Light) 40 + </option> 41 + <option 42 + value="bean2" 43 + 44 + class="truncate"> 45 + Colombian Supremo (Colombia - Medium) 46 + </option> 47 + </select> 48 + <button 49 + type="button" 50 + @click="showNewBean = true" 51 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 52 + + New 53 + </button> 54 + </div> 55 + <div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 56 + <h4 class="font-medium mb-3 text-gray-800"> 57 + Add New Bean 58 + </h4> 59 + <div class="space-y-3"> 60 + <input type="text" x-model="newBean.name" placeholder="Name (e.g. Morning Blend, House Espresso) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 61 + <input type="text" x-model="newBean.origin" placeholder="Origin (e.g. Ethiopia) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 62 + <select x-model="newBean.roasterRKey" name="roaster_rkey_modal" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 63 + <option value=""> 64 + Select Roaster (Optional) 65 + </option> 66 + <option value="roaster1"> 67 + Blue Bottle 68 + </option> 69 + </select> 70 + <select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 71 + <option value=""> 72 + Select Roast Level (Optional) 73 + </option> 74 + <option value="Ultra-Light"> 75 + Ultra-Light 76 + </option> 77 + <option value="Light"> 78 + Light 79 + </option> 80 + <option value="Medium-Light"> 81 + Medium-Light 82 + </option> 83 + <option value="Medium"> 84 + Medium 85 + </option> 86 + <option value="Medium-Dark"> 87 + Medium-Dark 88 + </option> 89 + <option value="Dark"> 90 + Dark 91 + </option> 92 + </select> 93 + <input type="text" x-model="newBean.process" placeholder="Process (e.g. Washed, Natural, Honey)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 94 + <input type="text" x-model="newBean.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 95 + <div class="flex gap-2"> 96 + <button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 97 + Add 98 + </button> 99 + <button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 100 + Cancel 101 + </button> 102 + </div> 103 + </div> 104 + </div> 105 + </div> 106 + <div> 107 + <label class="block text-sm font-medium text-brown-900 mb-2"> 108 + Coffee Amount (grams) 109 + </label> 110 + <input 111 + type="number" 112 + name="coffee_amount" 113 + step="0.1" 114 + value="18" 115 + placeholder="e.g. 18" 116 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 117 + <p class="text-sm text-brown-700 mt-1"> 118 + Amount of ground coffee used 119 + </p> 120 + </div> 121 + <div> 122 + <label class="block text-sm font-medium text-brown-900 mb-2"> 123 + Grinder 124 + </label> 125 + <div class="flex gap-2"> 126 + <select 127 + name="grinder_rkey" 128 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 129 + <option value=""> 130 + Select a grinder... 131 + </option> 132 + <option 133 + value="grinder1" 134 + selected 135 + class="truncate"> 136 + Baratza Encore 137 + </option> 138 + <option 139 + value="grinder2" 140 + 141 + class="truncate"> 142 + Comandante C40 143 + </option> 144 + </select> 145 + <button 146 + type="button" 147 + @click="showNewGrinder = true" 148 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 149 + + New 150 + </button> 151 + </div> 152 + <div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 153 + <h4 class="font-medium mb-3 text-gray-800"> 154 + Add New Grinder 155 + </h4> 156 + <div class="space-y-3"> 157 + <input type="text" x-model="newGrinder.name" placeholder="Name (e.g. Baratza Encore) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 158 + <select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 159 + <option value=""> 160 + Grinder Type (Optional) 161 + </option> 162 + <option value="Hand"> 163 + Hand 164 + </option> 165 + <option value="Electric"> 166 + Electric 167 + </option> 168 + <option value="Electric Hand"> 169 + Electric Hand 170 + </option> 171 + </select> 172 + <select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 173 + <option value=""> 174 + Burr Type (Optional) 175 + </option> 176 + <option value="Conical"> 177 + Conical 178 + </option> 179 + <option value="Flat"> 180 + Flat 181 + </option> 182 + </select> 183 + <input type="text" x-model="newGrinder.notes" placeholder="Notes (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 184 + <div class="flex gap-2"> 185 + <button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 186 + Add 187 + </button> 188 + <button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 189 + Cancel 190 + </button> 191 + </div> 192 + </div> 193 + </div> 194 + </div> 195 + <div> 196 + <label class="block text-sm font-medium text-brown-900 mb-2"> 197 + Grind Size 198 + </label> 199 + <input 200 + type="text" 201 + name="grind_size" 202 + value="18" 203 + placeholder="e.g. 18, Medium, 3.5, Fine" 204 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 205 + <p class="text-sm text-brown-700 mt-1"> 206 + Enter a number (grinder setting) or description (e.g. "Medium", "Fine") 207 + </p> 208 + </div> 209 + <div> 210 + <label class="block text-sm font-medium text-brown-900 mb-2"> 211 + Brew Method 212 + </label> 213 + <div class="flex gap-2"> 214 + <select 215 + name="brewer_rkey" 216 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 217 + <option value=""> 218 + Select brew method... 219 + </option> 220 + <option 221 + value="brewer1" 222 + selected 223 + class="truncate"> 224 + Hario V60 225 + </option> 226 + <option 227 + value="brewer2" 228 + 229 + class="truncate"> 230 + AeroPress 231 + </option> 232 + </select> 233 + <button 234 + type="button" 235 + @click="showNewBrewer = true" 236 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 237 + + New 238 + </button> 239 + </div> 240 + <div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 241 + <h4 class="font-medium mb-3 text-gray-800"> 242 + Add New Brewer 243 + </h4> 244 + <div class="space-y-3"> 245 + <input type="text" x-model="newBrewer.name" placeholder="Name (e.g. V60, AeroPress) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 246 + <input type="text" x-model="newBrewer.brewer_type" placeholder="Type (e.g. Pour-Over, Immersion)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 247 + <input type="text" x-model="newBrewer.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 248 + <div class="flex gap-2"> 249 + <button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 250 + Add 251 + </button> 252 + <button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 253 + Cancel 254 + </button> 255 + </div> 256 + </div> 257 + </div> 258 + </div> 259 + <div> 260 + <label class="block text-sm font-medium text-brown-900 mb-2"> 261 + Water Amount (grams) 262 + </label> 263 + <input 264 + type="number" 265 + name="water_amount" 266 + step="1" 267 + value="300" 268 + placeholder="e.g. 250" 269 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 270 + <p class="text-sm text-brown-700 mt-1"> 271 + Total water used (or leave empty if using pours below) 272 + </p> 273 + </div> 274 + <div> 275 + <div class="flex items-center justify-between mb-2"> 276 + <label class="block text-sm font-medium text-brown-900"> 277 + Pours (Optional) 278 + </label> 279 + <button 280 + type="button" 281 + @click="addPour()" 282 + class="text-sm bg-brown-300 text-brown-900 px-3 py-1 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 283 + + Add Pour 284 + </button> 285 + </div> 286 + <p class="text-sm text-brown-700 mb-3"> 287 + Track individual pours for bloom and subsequent additions 288 + </p> 289 + <div class="space-y-3"> 290 + <template x-for="(pour, index) in pours" :key="index"> 291 + <div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200"> 292 + <div class="flex-1"> 293 + <label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label> 294 + <input 295 + type="number" 296 + :name="'pour_water_' + index" 297 + x-model="pour.water" 298 + placeholder="Water (g)" 299 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 300 + </div> 301 + <div class="flex-1"> 302 + <label class="text-xs text-brown-700 font-medium"> 303 + Time (sec) 304 + </label> 305 + <input 306 + type="number" 307 + :name="'pour_time_' + index" 308 + x-model="pour.time" 309 + placeholder="e.g. 45" 310 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 311 + </div> 312 + <button 313 + type="button" 314 + @click="removePour(index)" 315 + class="text-brown-700 hover:text-brown-900 mt-5 font-bold" 316 + x-show="pours.length > 0"> 317 + 318 + </button> 319 + </div> 320 + </template> 321 + </div> 322 + </div> 323 + <div> 324 + <label class="block text-sm font-medium text-brown-900 mb-2"> 325 + Temperature 326 + </label> 327 + <input 328 + type="number" 329 + name="temperature" 330 + step="0.1" 331 + value="93.5" 332 + placeholder="e.g. 93.5" 333 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 334 + </div> 335 + <div> 336 + <label class="block text-sm font-medium text-brown-900 mb-2"> 337 + Brew Time (seconds) 338 + </label> 339 + <input 340 + type="number" 341 + name="time_seconds" 342 + value="180" 343 + placeholder="e.g. 180" 344 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 345 + </div> 346 + <div> 347 + <label class="block text-sm font-medium text-brown-900 mb-2"> 348 + Tasting Notes 349 + </label> 350 + <textarea 351 + name="tasting_notes" 352 + rows="4" 353 + placeholder="Describe the flavors, aroma, and your thoughts..." 354 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white">Bright citrus notes with floral aroma. Clean finish.</textarea> 355 + </div> 356 + <div> 357 + <label class="block text-sm font-medium text-brown-900 mb-2"> 358 + Rating 359 + </label> 360 + <input 361 + type="range" 362 + name="rating" 363 + min="1" 364 + max="10" 365 + value="8" 366 + x-model="rating" 367 + x-init="rating = $el.value" 368 + class="w-full accent-brown-700"/> 369 + <div class="text-center text-2xl font-bold text-brown-800"> 370 + <span x-text="rating"></span> 371 + /10 372 + </div> 373 + </div> 374 + <div> 375 + <button 376 + type="submit" 377 + class="w-full bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-xl hover:from-brown-800 hover:to-brown-900 transition-all font-semibold text-lg shadow-lg hover:shadow-xl"> 378 + Update Brew 379 + </button> 380 + </div> 381 + </form> 382 + </div> 383 + </div>
+348
internal/bff/__snapshots__/edit_brew_with_minimal_data.snap
··· 1 + --- 2 + title: edit brew with minimal data 3 + test_name: TestBrewForm_EditBrew_Snapshot/edit_brew_with_minimal_data 4 + file_name: form_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <script src="/static/js/brew-form.js"></script> 8 + <div class="max-w-2xl mx-auto"> 9 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300"> 10 + <h2 class="text-3xl font-bold text-brown-900 mb-6"> 11 + Edit Brew 12 + </h2> 13 + <form 14 + 15 + hx-put="/brews/brew456" 16 + 17 + hx-target="body" 18 + class="space-y-6" 19 + x-data="brewForm()" 20 + > 21 + <div> 22 + <label class="block text-sm font-medium text-brown-900 mb-2"> 23 + Coffee Bean 24 + </label> 25 + <div class="flex gap-2"> 26 + <select 27 + name="bean_rkey" 28 + required 29 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 30 + <option value=""> 31 + Select a bean... 32 + </option> 33 + <option 34 + value="bean1" 35 + selected 36 + class="truncate"> 37 + House Blend (Brazil - Medium) 38 + </option> 39 + </select> 40 + <button 41 + type="button" 42 + @click="showNewBean = true" 43 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 44 + + New 45 + </button> 46 + </div> 47 + <div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 48 + <h4 class="font-medium mb-3 text-gray-800"> 49 + Add New Bean 50 + </h4> 51 + <div class="space-y-3"> 52 + <input type="text" x-model="newBean.name" placeholder="Name (e.g. Morning Blend, House Espresso) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 53 + <input type="text" x-model="newBean.origin" placeholder="Origin (e.g. Ethiopia) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 54 + <select x-model="newBean.roasterRKey" name="roaster_rkey_modal" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 55 + <option value=""> 56 + Select Roaster (Optional) 57 + </option> 58 + </select> 59 + <select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 60 + <option value=""> 61 + Select Roast Level (Optional) 62 + </option> 63 + <option value="Ultra-Light"> 64 + Ultra-Light 65 + </option> 66 + <option value="Light"> 67 + Light 68 + </option> 69 + <option value="Medium-Light"> 70 + Medium-Light 71 + </option> 72 + <option value="Medium"> 73 + Medium 74 + </option> 75 + <option value="Medium-Dark"> 76 + Medium-Dark 77 + </option> 78 + <option value="Dark"> 79 + Dark 80 + </option> 81 + </select> 82 + <input type="text" x-model="newBean.process" placeholder="Process (e.g. Washed, Natural, Honey)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 83 + <input type="text" x-model="newBean.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 84 + <div class="flex gap-2"> 85 + <button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 86 + Add 87 + </button> 88 + <button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 89 + Cancel 90 + </button> 91 + </div> 92 + </div> 93 + </div> 94 + </div> 95 + <div> 96 + <label class="block text-sm font-medium text-brown-900 mb-2"> 97 + Coffee Amount (grams) 98 + </label> 99 + <input 100 + type="number" 101 + name="coffee_amount" 102 + step="0.1" 103 + 104 + placeholder="e.g. 18" 105 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 106 + <p class="text-sm text-brown-700 mt-1"> 107 + Amount of ground coffee used 108 + </p> 109 + </div> 110 + <div> 111 + <label class="block text-sm font-medium text-brown-900 mb-2"> 112 + Grinder 113 + </label> 114 + <div class="flex gap-2"> 115 + <select 116 + name="grinder_rkey" 117 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 118 + <option value=""> 119 + Select a grinder... 120 + </option> 121 + </select> 122 + <button 123 + type="button" 124 + @click="showNewGrinder = true" 125 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 126 + + New 127 + </button> 128 + </div> 129 + <div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 130 + <h4 class="font-medium mb-3 text-gray-800"> 131 + Add New Grinder 132 + </h4> 133 + <div class="space-y-3"> 134 + <input type="text" x-model="newGrinder.name" placeholder="Name (e.g. Baratza Encore) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 135 + <select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 136 + <option value=""> 137 + Grinder Type (Optional) 138 + </option> 139 + <option value="Hand"> 140 + Hand 141 + </option> 142 + <option value="Electric"> 143 + Electric 144 + </option> 145 + <option value="Electric Hand"> 146 + Electric Hand 147 + </option> 148 + </select> 149 + <select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 150 + <option value=""> 151 + Burr Type (Optional) 152 + </option> 153 + <option value="Conical"> 154 + Conical 155 + </option> 156 + <option value="Flat"> 157 + Flat 158 + </option> 159 + </select> 160 + <input type="text" x-model="newGrinder.notes" placeholder="Notes (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 161 + <div class="flex gap-2"> 162 + <button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 163 + Add 164 + </button> 165 + <button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 166 + Cancel 167 + </button> 168 + </div> 169 + </div> 170 + </div> 171 + </div> 172 + <div> 173 + <label class="block text-sm font-medium text-brown-900 mb-2"> 174 + Grind Size 175 + </label> 176 + <input 177 + type="text" 178 + name="grind_size" 179 + value="" 180 + placeholder="e.g. 18, Medium, 3.5, Fine" 181 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 182 + <p class="text-sm text-brown-700 mt-1"> 183 + Enter a number (grinder setting) or description (e.g. "Medium", "Fine") 184 + </p> 185 + </div> 186 + <div> 187 + <label class="block text-sm font-medium text-brown-900 mb-2"> 188 + Brew Method 189 + </label> 190 + <div class="flex gap-2"> 191 + <select 192 + name="brewer_rkey" 193 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 194 + <option value=""> 195 + Select brew method... 196 + </option> 197 + </select> 198 + <button 199 + type="button" 200 + @click="showNewBrewer = true" 201 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 202 + + New 203 + </button> 204 + </div> 205 + <div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 206 + <h4 class="font-medium mb-3 text-gray-800"> 207 + Add New Brewer 208 + </h4> 209 + <div class="space-y-3"> 210 + <input type="text" x-model="newBrewer.name" placeholder="Name (e.g. V60, AeroPress) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 211 + <input type="text" x-model="newBrewer.brewer_type" placeholder="Type (e.g. Pour-Over, Immersion)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 212 + <input type="text" x-model="newBrewer.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 213 + <div class="flex gap-2"> 214 + <button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 215 + Add 216 + </button> 217 + <button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 218 + Cancel 219 + </button> 220 + </div> 221 + </div> 222 + </div> 223 + </div> 224 + <div> 225 + <label class="block text-sm font-medium text-brown-900 mb-2"> 226 + Water Amount (grams) 227 + </label> 228 + <input 229 + type="number" 230 + name="water_amount" 231 + step="1" 232 + 233 + placeholder="e.g. 250" 234 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 235 + <p class="text-sm text-brown-700 mt-1"> 236 + Total water used (or leave empty if using pours below) 237 + </p> 238 + </div> 239 + <div> 240 + <div class="flex items-center justify-between mb-2"> 241 + <label class="block text-sm font-medium text-brown-900"> 242 + Pours (Optional) 243 + </label> 244 + <button 245 + type="button" 246 + @click="addPour()" 247 + class="text-sm bg-brown-300 text-brown-900 px-3 py-1 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 248 + + Add Pour 249 + </button> 250 + </div> 251 + <p class="text-sm text-brown-700 mb-3"> 252 + Track individual pours for bloom and subsequent additions 253 + </p> 254 + <div class="space-y-3"> 255 + <template x-for="(pour, index) in pours" :key="index"> 256 + <div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200"> 257 + <div class="flex-1"> 258 + <label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label> 259 + <input 260 + type="number" 261 + :name="'pour_water_' + index" 262 + x-model="pour.water" 263 + placeholder="Water (g)" 264 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 265 + </div> 266 + <div class="flex-1"> 267 + <label class="text-xs text-brown-700 font-medium"> 268 + Time (sec) 269 + </label> 270 + <input 271 + type="number" 272 + :name="'pour_time_' + index" 273 + x-model="pour.time" 274 + placeholder="e.g. 45" 275 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 276 + </div> 277 + <button 278 + type="button" 279 + @click="removePour(index)" 280 + class="text-brown-700 hover:text-brown-900 mt-5 font-bold" 281 + x-show="pours.length > 0"> 282 + 283 + </button> 284 + </div> 285 + </template> 286 + </div> 287 + </div> 288 + <div> 289 + <label class="block text-sm font-medium text-brown-900 mb-2"> 290 + Temperature 291 + </label> 292 + <input 293 + type="number" 294 + name="temperature" 295 + step="0.1" 296 + 297 + placeholder="e.g. 93.5" 298 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 299 + </div> 300 + <div> 301 + <label class="block text-sm font-medium text-brown-900 mb-2"> 302 + Brew Time (seconds) 303 + </label> 304 + <input 305 + type="number" 306 + name="time_seconds" 307 + 308 + placeholder="e.g. 180" 309 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 310 + </div> 311 + <div> 312 + <label class="block text-sm font-medium text-brown-900 mb-2"> 313 + Tasting Notes 314 + </label> 315 + <textarea 316 + name="tasting_notes" 317 + rows="4" 318 + placeholder="Describe the flavors, aroma, and your thoughts..." 319 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"></textarea> 320 + </div> 321 + <div> 322 + <label class="block text-sm font-medium text-brown-900 mb-2"> 323 + Rating 324 + </label> 325 + <input 326 + type="range" 327 + name="rating" 328 + min="1" 329 + max="10" 330 + value="5" 331 + x-model="rating" 332 + x-init="rating = $el.value" 333 + class="w-full accent-brown-700"/> 334 + <div class="text-center text-2xl font-bold text-brown-800"> 335 + <span x-text="rating"></span> 336 + /10 337 + </div> 338 + </div> 339 + <div> 340 + <button 341 + type="submit" 342 + class="w-full bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-xl hover:from-brown-800 hover:to-brown-900 transition-all font-semibold text-lg shadow-lg hover:shadow-xl"> 343 + Update Brew 344 + </button> 345 + </div> 346 + </form> 347 + </div> 348 + </div>
+350
internal/bff/__snapshots__/edit_brew_with_pours_json.snap
··· 1 + --- 2 + title: edit brew with pours json 3 + test_name: TestBrewForm_EditBrew_Snapshot/edit_brew_with_pours_json 4 + file_name: form_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <script src="/static/js/brew-form.js"></script> 8 + <div class="max-w-2xl mx-auto"> 9 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300"> 10 + <h2 class="text-3xl font-bold text-brown-900 mb-6"> 11 + Edit Brew 12 + </h2> 13 + <form 14 + 15 + hx-put="/brews/brew789" 16 + 17 + hx-target="body" 18 + class="space-y-6" 19 + x-data="brewForm()" 20 + 21 + data-pours='[{&#34;pourNumber&#34;:1,&#34;waterAmount&#34;:60,&#34;timeSeconds&#34;:30},{&#34;pourNumber&#34;:2,&#34;waterAmount&#34;:120,&#34;timeSeconds&#34;:60}]' 22 + > 23 + <div> 24 + <label class="block text-sm font-medium text-brown-900 mb-2"> 25 + Coffee Bean 26 + </label> 27 + <div class="flex gap-2"> 28 + <select 29 + name="bean_rkey" 30 + required 31 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 32 + <option value=""> 33 + Select a bean... 34 + </option> 35 + <option 36 + value="bean1" 37 + selected 38 + class="truncate"> 39 + Kenya - Light 40 + </option> 41 + </select> 42 + <button 43 + type="button" 44 + @click="showNewBean = true" 45 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 46 + + New 47 + </button> 48 + </div> 49 + <div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 50 + <h4 class="font-medium mb-3 text-gray-800"> 51 + Add New Bean 52 + </h4> 53 + <div class="space-y-3"> 54 + <input type="text" x-model="newBean.name" placeholder="Name (e.g. Morning Blend, House Espresso) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 55 + <input type="text" x-model="newBean.origin" placeholder="Origin (e.g. Ethiopia) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 56 + <select x-model="newBean.roasterRKey" name="roaster_rkey_modal" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 57 + <option value=""> 58 + Select Roaster (Optional) 59 + </option> 60 + </select> 61 + <select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 62 + <option value=""> 63 + Select Roast Level (Optional) 64 + </option> 65 + <option value="Ultra-Light"> 66 + Ultra-Light 67 + </option> 68 + <option value="Light"> 69 + Light 70 + </option> 71 + <option value="Medium-Light"> 72 + Medium-Light 73 + </option> 74 + <option value="Medium"> 75 + Medium 76 + </option> 77 + <option value="Medium-Dark"> 78 + Medium-Dark 79 + </option> 80 + <option value="Dark"> 81 + Dark 82 + </option> 83 + </select> 84 + <input type="text" x-model="newBean.process" placeholder="Process (e.g. Washed, Natural, Honey)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 85 + <input type="text" x-model="newBean.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 86 + <div class="flex gap-2"> 87 + <button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 88 + Add 89 + </button> 90 + <button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 91 + Cancel 92 + </button> 93 + </div> 94 + </div> 95 + </div> 96 + </div> 97 + <div> 98 + <label class="block text-sm font-medium text-brown-900 mb-2"> 99 + Coffee Amount (grams) 100 + </label> 101 + <input 102 + type="number" 103 + name="coffee_amount" 104 + step="0.1" 105 + 106 + placeholder="e.g. 18" 107 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 108 + <p class="text-sm text-brown-700 mt-1"> 109 + Amount of ground coffee used 110 + </p> 111 + </div> 112 + <div> 113 + <label class="block text-sm font-medium text-brown-900 mb-2"> 114 + Grinder 115 + </label> 116 + <div class="flex gap-2"> 117 + <select 118 + name="grinder_rkey" 119 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 120 + <option value=""> 121 + Select a grinder... 122 + </option> 123 + </select> 124 + <button 125 + type="button" 126 + @click="showNewGrinder = true" 127 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 128 + + New 129 + </button> 130 + </div> 131 + <div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 132 + <h4 class="font-medium mb-3 text-gray-800"> 133 + Add New Grinder 134 + </h4> 135 + <div class="space-y-3"> 136 + <input type="text" x-model="newGrinder.name" placeholder="Name (e.g. Baratza Encore) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 137 + <select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 138 + <option value=""> 139 + Grinder Type (Optional) 140 + </option> 141 + <option value="Hand"> 142 + Hand 143 + </option> 144 + <option value="Electric"> 145 + Electric 146 + </option> 147 + <option value="Electric Hand"> 148 + Electric Hand 149 + </option> 150 + </select> 151 + <select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 152 + <option value=""> 153 + Burr Type (Optional) 154 + </option> 155 + <option value="Conical"> 156 + Conical 157 + </option> 158 + <option value="Flat"> 159 + Flat 160 + </option> 161 + </select> 162 + <input type="text" x-model="newGrinder.notes" placeholder="Notes (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 163 + <div class="flex gap-2"> 164 + <button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 165 + Add 166 + </button> 167 + <button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 168 + Cancel 169 + </button> 170 + </div> 171 + </div> 172 + </div> 173 + </div> 174 + <div> 175 + <label class="block text-sm font-medium text-brown-900 mb-2"> 176 + Grind Size 177 + </label> 178 + <input 179 + type="text" 180 + name="grind_size" 181 + value="" 182 + placeholder="e.g. 18, Medium, 3.5, Fine" 183 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 184 + <p class="text-sm text-brown-700 mt-1"> 185 + Enter a number (grinder setting) or description (e.g. "Medium", "Fine") 186 + </p> 187 + </div> 188 + <div> 189 + <label class="block text-sm font-medium text-brown-900 mb-2"> 190 + Brew Method 191 + </label> 192 + <div class="flex gap-2"> 193 + <select 194 + name="brewer_rkey" 195 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 196 + <option value=""> 197 + Select brew method... 198 + </option> 199 + </select> 200 + <button 201 + type="button" 202 + @click="showNewBrewer = true" 203 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 204 + + New 205 + </button> 206 + </div> 207 + <div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 208 + <h4 class="font-medium mb-3 text-gray-800"> 209 + Add New Brewer 210 + </h4> 211 + <div class="space-y-3"> 212 + <input type="text" x-model="newBrewer.name" placeholder="Name (e.g. V60, AeroPress) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 213 + <input type="text" x-model="newBrewer.brewer_type" placeholder="Type (e.g. Pour-Over, Immersion)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 214 + <input type="text" x-model="newBrewer.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 215 + <div class="flex gap-2"> 216 + <button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 217 + Add 218 + </button> 219 + <button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 220 + Cancel 221 + </button> 222 + </div> 223 + </div> 224 + </div> 225 + </div> 226 + <div> 227 + <label class="block text-sm font-medium text-brown-900 mb-2"> 228 + Water Amount (grams) 229 + </label> 230 + <input 231 + type="number" 232 + name="water_amount" 233 + step="1" 234 + 235 + placeholder="e.g. 250" 236 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 237 + <p class="text-sm text-brown-700 mt-1"> 238 + Total water used (or leave empty if using pours below) 239 + </p> 240 + </div> 241 + <div> 242 + <div class="flex items-center justify-between mb-2"> 243 + <label class="block text-sm font-medium text-brown-900"> 244 + Pours (Optional) 245 + </label> 246 + <button 247 + type="button" 248 + @click="addPour()" 249 + class="text-sm bg-brown-300 text-brown-900 px-3 py-1 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 250 + + Add Pour 251 + </button> 252 + </div> 253 + <p class="text-sm text-brown-700 mb-3"> 254 + Track individual pours for bloom and subsequent additions 255 + </p> 256 + <div class="space-y-3"> 257 + <template x-for="(pour, index) in pours" :key="index"> 258 + <div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200"> 259 + <div class="flex-1"> 260 + <label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label> 261 + <input 262 + type="number" 263 + :name="'pour_water_' + index" 264 + x-model="pour.water" 265 + placeholder="Water (g)" 266 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 267 + </div> 268 + <div class="flex-1"> 269 + <label class="text-xs text-brown-700 font-medium"> 270 + Time (sec) 271 + </label> 272 + <input 273 + type="number" 274 + :name="'pour_time_' + index" 275 + x-model="pour.time" 276 + placeholder="e.g. 45" 277 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 278 + </div> 279 + <button 280 + type="button" 281 + @click="removePour(index)" 282 + class="text-brown-700 hover:text-brown-900 mt-5 font-bold" 283 + x-show="pours.length > 0"> 284 + 285 + </button> 286 + </div> 287 + </template> 288 + </div> 289 + </div> 290 + <div> 291 + <label class="block text-sm font-medium text-brown-900 mb-2"> 292 + Temperature 293 + </label> 294 + <input 295 + type="number" 296 + name="temperature" 297 + step="0.1" 298 + 299 + placeholder="e.g. 93.5" 300 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 301 + </div> 302 + <div> 303 + <label class="block text-sm font-medium text-brown-900 mb-2"> 304 + Brew Time (seconds) 305 + </label> 306 + <input 307 + type="number" 308 + name="time_seconds" 309 + 310 + placeholder="e.g. 180" 311 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 312 + </div> 313 + <div> 314 + <label class="block text-sm font-medium text-brown-900 mb-2"> 315 + Tasting Notes 316 + </label> 317 + <textarea 318 + name="tasting_notes" 319 + rows="4" 320 + placeholder="Describe the flavors, aroma, and your thoughts..." 321 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"></textarea> 322 + </div> 323 + <div> 324 + <label class="block text-sm font-medium text-brown-900 mb-2"> 325 + Rating 326 + </label> 327 + <input 328 + type="range" 329 + name="rating" 330 + min="1" 331 + max="10" 332 + value="7" 333 + x-model="rating" 334 + x-init="rating = $el.value" 335 + class="w-full accent-brown-700"/> 336 + <div class="text-center text-2xl font-bold text-brown-800"> 337 + <span x-text="rating"></span> 338 + /10 339 + </div> 340 + </div> 341 + <div> 342 + <button 343 + type="submit" 344 + class="w-full bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-xl hover:from-brown-800 hover:to-brown-900 transition-all font-semibold text-lg shadow-lg hover:shadow-xl"> 345 + Update Brew 346 + </button> 347 + </div> 348 + </form> 349 + </div> 350 + </div>
+351
internal/bff/__snapshots__/edit_brew_without_loaded_collections.snap
··· 1 + --- 2 + title: edit brew without loaded collections 3 + test_name: TestBrewForm_EditBrew_Snapshot/edit_brew_without_loaded_collections 4 + file_name: form_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <script src="/static/js/brew-form.js"></script> 8 + <div class="max-w-2xl mx-auto"> 9 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300"> 10 + <h2 class="text-3xl font-bold text-brown-900 mb-6"> 11 + Edit Brew 12 + </h2> 13 + <form 14 + 15 + hx-put="/brews/brew999" 16 + 17 + hx-target="body" 18 + class="space-y-6" 19 + x-data="brewForm()" 20 + > 21 + <div> 22 + <label class="block text-sm font-medium text-brown-900 mb-2"> 23 + Coffee Bean 24 + </label> 25 + <div class="flex gap-2"> 26 + <select 27 + name="bean_rkey" 28 + required 29 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 30 + <option value=""> 31 + Select a bean... 32 + </option> 33 + <option value="bean1" selected> 34 + Loading... 35 + </option> 36 + </select> 37 + <button 38 + type="button" 39 + @click="showNewBean = true" 40 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 41 + + New 42 + </button> 43 + </div> 44 + <div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 45 + <h4 class="font-medium mb-3 text-gray-800"> 46 + Add New Bean 47 + </h4> 48 + <div class="space-y-3"> 49 + <input type="text" x-model="newBean.name" placeholder="Name (e.g. Morning Blend, House Espresso) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 50 + <input type="text" x-model="newBean.origin" placeholder="Origin (e.g. Ethiopia) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 51 + <select x-model="newBean.roasterRKey" name="roaster_rkey_modal" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 52 + <option value=""> 53 + Select Roaster (Optional) 54 + </option> 55 + </select> 56 + <select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 57 + <option value=""> 58 + Select Roast Level (Optional) 59 + </option> 60 + <option value="Ultra-Light"> 61 + Ultra-Light 62 + </option> 63 + <option value="Light"> 64 + Light 65 + </option> 66 + <option value="Medium-Light"> 67 + Medium-Light 68 + </option> 69 + <option value="Medium"> 70 + Medium 71 + </option> 72 + <option value="Medium-Dark"> 73 + Medium-Dark 74 + </option> 75 + <option value="Dark"> 76 + Dark 77 + </option> 78 + </select> 79 + <input type="text" x-model="newBean.process" placeholder="Process (e.g. Washed, Natural, Honey)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 80 + <input type="text" x-model="newBean.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 81 + <div class="flex gap-2"> 82 + <button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 83 + Add 84 + </button> 85 + <button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 86 + Cancel 87 + </button> 88 + </div> 89 + </div> 90 + </div> 91 + </div> 92 + <div> 93 + <label class="block text-sm font-medium text-brown-900 mb-2"> 94 + Coffee Amount (grams) 95 + </label> 96 + <input 97 + type="number" 98 + name="coffee_amount" 99 + step="0.1" 100 + 101 + placeholder="e.g. 18" 102 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 103 + <p class="text-sm text-brown-700 mt-1"> 104 + Amount of ground coffee used 105 + </p> 106 + </div> 107 + <div> 108 + <label class="block text-sm font-medium text-brown-900 mb-2"> 109 + Grinder 110 + </label> 111 + <div class="flex gap-2"> 112 + <select 113 + name="grinder_rkey" 114 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 115 + <option value=""> 116 + Select a grinder... 117 + </option> 118 + <option value="grinder1" selected> 119 + Loading... 120 + </option> 121 + </select> 122 + <button 123 + type="button" 124 + @click="showNewGrinder = true" 125 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 126 + + New 127 + </button> 128 + </div> 129 + <div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 130 + <h4 class="font-medium mb-3 text-gray-800"> 131 + Add New Grinder 132 + </h4> 133 + <div class="space-y-3"> 134 + <input type="text" x-model="newGrinder.name" placeholder="Name (e.g. Baratza Encore) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 135 + <select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 136 + <option value=""> 137 + Grinder Type (Optional) 138 + </option> 139 + <option value="Hand"> 140 + Hand 141 + </option> 142 + <option value="Electric"> 143 + Electric 144 + </option> 145 + <option value="Electric Hand"> 146 + Electric Hand 147 + </option> 148 + </select> 149 + <select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 150 + <option value=""> 151 + Burr Type (Optional) 152 + </option> 153 + <option value="Conical"> 154 + Conical 155 + </option> 156 + <option value="Flat"> 157 + Flat 158 + </option> 159 + </select> 160 + <input type="text" x-model="newGrinder.notes" placeholder="Notes (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 161 + <div class="flex gap-2"> 162 + <button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 163 + Add 164 + </button> 165 + <button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 166 + Cancel 167 + </button> 168 + </div> 169 + </div> 170 + </div> 171 + </div> 172 + <div> 173 + <label class="block text-sm font-medium text-brown-900 mb-2"> 174 + Grind Size 175 + </label> 176 + <input 177 + type="text" 178 + name="grind_size" 179 + value="" 180 + placeholder="e.g. 18, Medium, 3.5, Fine" 181 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 182 + <p class="text-sm text-brown-700 mt-1"> 183 + Enter a number (grinder setting) or description (e.g. "Medium", "Fine") 184 + </p> 185 + </div> 186 + <div> 187 + <label class="block text-sm font-medium text-brown-900 mb-2"> 188 + Brew Method 189 + </label> 190 + <div class="flex gap-2"> 191 + <select 192 + name="brewer_rkey" 193 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 194 + <option value=""> 195 + Select brew method... 196 + </option> 197 + <option value="brewer1" selected> 198 + Loading... 199 + </option> 200 + </select> 201 + <button 202 + type="button" 203 + @click="showNewBrewer = true" 204 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 205 + + New 206 + </button> 207 + </div> 208 + <div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 209 + <h4 class="font-medium mb-3 text-gray-800"> 210 + Add New Brewer 211 + </h4> 212 + <div class="space-y-3"> 213 + <input type="text" x-model="newBrewer.name" placeholder="Name (e.g. V60, AeroPress) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 214 + <input type="text" x-model="newBrewer.brewer_type" placeholder="Type (e.g. Pour-Over, Immersion)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 215 + <input type="text" x-model="newBrewer.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 216 + <div class="flex gap-2"> 217 + <button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 218 + Add 219 + </button> 220 + <button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 221 + Cancel 222 + </button> 223 + </div> 224 + </div> 225 + </div> 226 + </div> 227 + <div> 228 + <label class="block text-sm font-medium text-brown-900 mb-2"> 229 + Water Amount (grams) 230 + </label> 231 + <input 232 + type="number" 233 + name="water_amount" 234 + step="1" 235 + 236 + placeholder="e.g. 250" 237 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 238 + <p class="text-sm text-brown-700 mt-1"> 239 + Total water used (or leave empty if using pours below) 240 + </p> 241 + </div> 242 + <div> 243 + <div class="flex items-center justify-between mb-2"> 244 + <label class="block text-sm font-medium text-brown-900"> 245 + Pours (Optional) 246 + </label> 247 + <button 248 + type="button" 249 + @click="addPour()" 250 + class="text-sm bg-brown-300 text-brown-900 px-3 py-1 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 251 + + Add Pour 252 + </button> 253 + </div> 254 + <p class="text-sm text-brown-700 mb-3"> 255 + Track individual pours for bloom and subsequent additions 256 + </p> 257 + <div class="space-y-3"> 258 + <template x-for="(pour, index) in pours" :key="index"> 259 + <div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200"> 260 + <div class="flex-1"> 261 + <label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label> 262 + <input 263 + type="number" 264 + :name="'pour_water_' + index" 265 + x-model="pour.water" 266 + placeholder="Water (g)" 267 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 268 + </div> 269 + <div class="flex-1"> 270 + <label class="text-xs text-brown-700 font-medium"> 271 + Time (sec) 272 + </label> 273 + <input 274 + type="number" 275 + :name="'pour_time_' + index" 276 + x-model="pour.time" 277 + placeholder="e.g. 45" 278 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 279 + </div> 280 + <button 281 + type="button" 282 + @click="removePour(index)" 283 + class="text-brown-700 hover:text-brown-900 mt-5 font-bold" 284 + x-show="pours.length > 0"> 285 + 286 + </button> 287 + </div> 288 + </template> 289 + </div> 290 + </div> 291 + <div> 292 + <label class="block text-sm font-medium text-brown-900 mb-2"> 293 + Temperature 294 + </label> 295 + <input 296 + type="number" 297 + name="temperature" 298 + step="0.1" 299 + 300 + placeholder="e.g. 93.5" 301 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 302 + </div> 303 + <div> 304 + <label class="block text-sm font-medium text-brown-900 mb-2"> 305 + Brew Time (seconds) 306 + </label> 307 + <input 308 + type="number" 309 + name="time_seconds" 310 + 311 + placeholder="e.g. 180" 312 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 313 + </div> 314 + <div> 315 + <label class="block text-sm font-medium text-brown-900 mb-2"> 316 + Tasting Notes 317 + </label> 318 + <textarea 319 + name="tasting_notes" 320 + rows="4" 321 + placeholder="Describe the flavors, aroma, and your thoughts..." 322 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"></textarea> 323 + </div> 324 + <div> 325 + <label class="block text-sm font-medium text-brown-900 mb-2"> 326 + Rating 327 + </label> 328 + <input 329 + type="range" 330 + name="rating" 331 + min="1" 332 + max="10" 333 + value="6" 334 + x-model="rating" 335 + x-init="rating = $el.value" 336 + class="w-full accent-brown-700"/> 337 + <div class="text-center text-2xl font-bold text-brown-800"> 338 + <span x-text="rating"></span> 339 + /10 340 + </div> 341 + </div> 342 + <div> 343 + <button 344 + type="submit" 345 + class="w-full bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-xl hover:from-brown-800 hover:to-brown-900 transition-all font-semibold text-lg shadow-lg hover:shadow-xl"> 346 + Update Brew 347 + </button> 348 + </div> 349 + </form> 350 + </div> 351 + </div>
+11
internal/bff/__snapshots__/empty_brew_list_other_profile.snap
··· 1 + --- 2 + title: empty brew list other profile 3 + test_name: TestBrewListContent_Snapshot/empty_brew_list_other_profile 4 + file_name: partial_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300"> 8 + <p class="text-brown-800 text-lg font-medium"> 9 + No brews yet. 10 + </p> 11 + </div>
+15
internal/bff/__snapshots__/empty_brew_list_own_profile.snap
··· 1 + --- 2 + title: empty brew list own profile 3 + test_name: TestBrewListContent_Snapshot/empty_brew_list_own_profile 4 + file_name: partial_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300"> 8 + <p class="text-brown-800 text-lg mb-4 font-medium"> 9 + No brews yet! Start tracking your coffee journey. 10 + </p> 11 + <a href="/brews/new" 12 + class="inline-block bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all shadow-lg hover:shadow-xl font-medium"> 13 + Add Your First Brew 14 + </a> 15 + </div>
+10 -1
internal/bff/__snapshots__/empty_feed_authenticated.snap
··· 4 4 file_name: feed_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n<div class=\"space-y-4\">\n \n <div class=\"bg-brown-100 rounded-lg p-6 text-center text-brown-700 border border-brown-200\">\n <p class=\"mb-2 font-medium\">No activity in the feed yet.</p>\n <p class=\"text-sm\">Be the first to add something!</p>\n </div>\n \n</div>\n" 7 + <div class="space-y-4"> 8 + <div class="bg-brown-100 rounded-lg p-6 text-center text-brown-700 border border-brown-200"> 9 + <p class="mb-2 font-medium"> 10 + No activity in the feed yet. 11 + </p> 12 + <p class="text-sm"> 13 + Be the first to add something! 14 + </p> 15 + </div> 16 + </div>
+10 -1
internal/bff/__snapshots__/empty_feed_unauthenticated.snap
··· 4 4 file_name: feed_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n<div class=\"space-y-4\">\n \n <div class=\"bg-brown-100 rounded-lg p-6 text-center text-brown-700 border border-brown-200\">\n <p class=\"mb-2 font-medium\">No activity in the feed yet.</p>\n <p class=\"text-sm\">Be the first to add something!</p>\n </div>\n \n</div>\n" 7 + <div class="space-y-4"> 8 + <div class="bg-brown-100 rounded-lg p-6 text-center text-brown-700 border border-brown-200"> 9 + <p class="mb-2 font-medium"> 10 + No activity in the feed yet. 11 + </p> 12 + <p class="text-sm"> 13 + Be the first to add something! 14 + </p> 15 + </div> 16 + </div>
+48
internal/bff/__snapshots__/grinder_form_renders.snap
··· 1 + --- 2 + title: grinder_form_renders 3 + test_name: TestNewGrinderForm_Snapshot/grinder_form_renders 4 + file_name: form_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 8 + <h4 class="font-medium mb-3 text-gray-800"> 9 + Add New Grinder 10 + </h4> 11 + <div class="space-y-3"> 12 + <input type="text" x-model="newGrinder.name" placeholder="Name (e.g. Baratza Encore) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 13 + <select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 14 + <option value=""> 15 + Grinder Type (Optional) 16 + </option> 17 + <option value="Hand"> 18 + Hand 19 + </option> 20 + <option value="Electric"> 21 + Electric 22 + </option> 23 + <option value="Electric Hand"> 24 + Electric Hand 25 + </option> 26 + </select> 27 + <select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 28 + <option value=""> 29 + Burr Type (Optional) 30 + </option> 31 + <option value="Conical"> 32 + Conical 33 + </option> 34 + <option value="Flat"> 35 + Flat 36 + </option> 37 + </select> 38 + <input type="text" x-model="newGrinder.notes" placeholder="Notes (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 39 + <div class="flex gap-2"> 40 + <button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 41 + Add 42 + </button> 43 + <button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 44 + Cancel 45 + </button> 46 + </div> 47 + </div> 48 + </div>
+53 -1
internal/bff/__snapshots__/grinder_item.snap
··· 4 4 file_name: feed_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n<div class=\"space-y-4\">\n \n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/gearhead\" class=\"flex-shrink-0\">\n \n <div class=\"w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition\">\n <span class=\"text-brown-600 text-sm\">?</span>\n </div>\n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/gearhead\" class=\"font-medium text-brown-900 truncate hover:text-brown-700 hover:underline\">Coffee Gear Head</a>\n \n <a href=\"/profile/gearhead\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@gearhead</a>\n </div>\n <span class=\"text-brown-500 text-sm\">30 minutes ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n ⚙️ added a new grinder\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200\">\n <div class=\"text-base mb-2\">\n <span class=\"font-bold text-brown-900\">Comandante C40</span>\n </div>\n <div class=\"text-sm text-brown-700 space-y-1\">\n \n <div><span class=\"text-brown-600\">Type:</span> Hand</div>\n \n \n <div><span class=\"text-brown-600\">Burr:</span> Conical</div>\n \n \n <div class=\"mt-2 text-brown-800 italic\">\"Excellent for pour over\"</div>\n \n </div>\n </div>\n \n </div>\n \n \n \n</div>\n" 7 + <div class="space-y-4"> 8 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 9 + <div class="flex items-center gap-3 mb-3"> 10 + <a href="/profile/gearhead" class="flex-shrink-0"> 11 + <div class="w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition"> 12 + <span class="text-brown-600 text-sm"> 13 + ? 14 + </span> 15 + </div> 16 + </a> 17 + <div class="flex-1 min-w-0"> 18 + <div class="flex items-center gap-2"> 19 + <a href="/profile/gearhead" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline"> 20 + Coffee Gear Head 21 + </a> 22 + <a href="/profile/gearhead" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 23 + @gearhead 24 + </a> 25 + </div> 26 + <span class="text-brown-500 text-sm"> 27 + 30 minutes ago 28 + </span> 29 + </div> 30 + </div> 31 + <div class="mb-2 text-sm text-brown-700"> 32 + ⚙️ added a new grinder 33 + </div> 34 + <div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200"> 35 + <div class="text-base mb-2"> 36 + <span class="font-bold text-brown-900"> 37 + Comandante C40 38 + </span> 39 + </div> 40 + <div class="text-sm text-brown-700 space-y-1"> 41 + <div> 42 + <span class="text-brown-600"> 43 + Type: 44 + </span> 45 + Hand 46 + </div> 47 + <div> 48 + <span class="text-brown-600"> 49 + Burr: 50 + </span> 51 + Conical 52 + </div> 53 + <div class="mt-2 text-brown-800 italic"> 54 + "Excellent for pour over" 55 + </div> 56 + </div> 57 + </div> 58 + </div> 59 + </div>
+210
internal/bff/__snapshots__/grinders_empty.snap
··· 1 + --- 2 + title: grinders empty 3 + test_name: TestManageContent_GrindersTab_Snapshot/grinders_empty 4 + file_name: partial_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="tab === 'beans'"> 8 + <div class="mb-4 flex justify-between items-center"> 9 + <h3 class="text-xl font-semibold text-brown-900"> 10 + Coffee Beans 11 + </h3> 12 + <button 13 + @click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}" 14 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 15 + + Add Bean 16 + </button> 17 + </div> 18 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 19 + No beans yet. Add your first bean to get started! 20 + </div> 21 + </div> 22 + <div x-show="tab === 'roasters'"> 23 + <div class="mb-4 flex justify-between items-center"> 24 + <h3 class="text-xl font-semibold text-brown-900"> 25 + Roasters 26 + </h3> 27 + <button 28 + @click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}" 29 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 30 + + Add Roaster 31 + </button> 32 + </div> 33 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 34 + No roasters yet. Add your first roaster! 35 + </div> 36 + </div> 37 + <div x-show="tab === 'grinders'"> 38 + <div class="mb-4 flex justify-between items-center"> 39 + <h3 class="text-xl font-semibold text-brown-900"> 40 + Grinders 41 + </h3> 42 + <button 43 + @click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}" 44 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 45 + + Add Grinder 46 + </button> 47 + </div> 48 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 49 + No grinders yet. Add your first grinder! 50 + </div> 51 + </div> 52 + <div x-show="tab === 'brewers'"> 53 + <div class="mb-4 flex justify-between items-center"> 54 + <h3 class="text-xl font-semibold text-brown-900"> 55 + Brewers 56 + </h3> 57 + <button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}" 58 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 59 + + Add Brewer 60 + </button> 61 + </div> 62 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 63 + No brewers yet. Add your first brewer! 64 + </div> 65 + </div> 66 + <div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 67 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 68 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3> 69 + <div class="space-y-4"> 70 + <input type="text" x-model="beanForm.name" placeholder="Name *" 71 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 72 + <input type="text" x-model="beanForm.origin" placeholder="Origin *" 73 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 74 + <select x-model="beanForm.roaster_rkey" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 75 + <option value=""> 76 + Select Roaster (Optional) 77 + </option> 78 + </select> 79 + <select x-model="beanForm.roast_level" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 80 + <option value=""> 81 + Select Roast Level (Optional) 82 + </option> 83 + <option value="Ultra-Light"> 84 + Ultra-Light 85 + </option> 86 + <option value="Light"> 87 + Light 88 + </option> 89 + <option value="Medium-Light"> 90 + Medium-Light 91 + </option> 92 + <option value="Medium"> 93 + Medium 94 + </option> 95 + <option value="Medium-Dark"> 96 + Medium-Dark 97 + </option> 98 + <option value="Dark"> 99 + Dark 100 + </option> 101 + </select> 102 + <input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)" 103 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 104 + <textarea x-model="beanForm.description" placeholder="Description" rows="3" 105 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 106 + <div class="flex gap-2"> 107 + <button @click="saveBean()" 108 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 109 + Save 110 + </button> 111 + <button @click="showBeanForm = false" 112 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 113 + Cancel 114 + </button> 115 + </div> 116 + </div> 117 + </div> 118 + </div> 119 + <div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 120 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 121 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3> 122 + <div class="space-y-4"> 123 + <input type="text" x-model="roasterForm.name" placeholder="Name *" 124 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 125 + <input type="text" x-model="roasterForm.location" placeholder="Location" 126 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 127 + <input type="url" x-model="roasterForm.website" placeholder="Website" 128 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 129 + <div class="flex gap-2"> 130 + <button @click="saveRoaster()" 131 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 132 + Save 133 + </button> 134 + <button @click="showRoasterForm = false" 135 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 136 + Cancel 137 + </button> 138 + </div> 139 + </div> 140 + </div> 141 + </div> 142 + <div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 143 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 144 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3> 145 + <div class="space-y-4"> 146 + <input type="text" x-model="grinderForm.name" placeholder="Name *" 147 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 148 + <select x-model="grinderForm.grinder_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 149 + <option value=""> 150 + Select Grinder Type * 151 + </option> 152 + <option value="Hand"> 153 + Hand 154 + </option> 155 + <option value="Electric"> 156 + Electric 157 + </option> 158 + <option value="Portable Electric"> 159 + Portable Electric 160 + </option> 161 + </select> 162 + <select x-model="grinderForm.burr_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 163 + <option value=""> 164 + Select Burr Type (Optional) 165 + </option> 166 + <option value="Conical"> 167 + Conical 168 + </option> 169 + <option value="Flat"> 170 + Flat 171 + </option> 172 + </select> 173 + <textarea x-model="grinderForm.notes" placeholder="Notes" rows="3" 174 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 175 + <div class="flex gap-2"> 176 + <button @click="saveGrinder()" 177 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 178 + Save 179 + </button> 180 + <button @click="showGrinderForm = false" 181 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 182 + Cancel 183 + </button> 184 + </div> 185 + </div> 186 + </div> 187 + </div> 188 + <div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 189 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 190 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3> 191 + <div class="space-y-4"> 192 + <input type="text" x-model="brewerForm.name" placeholder="Name *" 193 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 194 + <input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)" 195 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 196 + <textarea x-model="brewerForm.description" placeholder="Description" rows="3" 197 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 198 + <div class="flex gap-2"> 199 + <button @click="saveBrewer()" 200 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 201 + Save 202 + </button> 203 + <button @click="showBrewerForm = false" 204 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 205 + Cancel 206 + </button> 207 + </div> 208 + </div> 209 + </div> 210 + </div>
+278
internal/bff/__snapshots__/grinders_with_data.snap
··· 1 + --- 2 + title: grinders with data 3 + test_name: TestManageContent_GrindersTab_Snapshot/grinders_with_data 4 + file_name: partial_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="tab === 'beans'"> 8 + <div class="mb-4 flex justify-between items-center"> 9 + <h3 class="text-xl font-semibold text-brown-900"> 10 + Coffee Beans 11 + </h3> 12 + <button 13 + @click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}" 14 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 15 + + Add Bean 16 + </button> 17 + </div> 18 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 19 + No beans yet. Add your first bean to get started! 20 + </div> 21 + </div> 22 + <div x-show="tab === 'roasters'"> 23 + <div class="mb-4 flex justify-between items-center"> 24 + <h3 class="text-xl font-semibold text-brown-900"> 25 + Roasters 26 + </h3> 27 + <button 28 + @click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}" 29 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 30 + + Add Roaster 31 + </button> 32 + </div> 33 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 34 + No roasters yet. Add your first roaster! 35 + </div> 36 + </div> 37 + <div x-show="tab === 'grinders'"> 38 + <div class="mb-4 flex justify-between items-center"> 39 + <h3 class="text-xl font-semibold text-brown-900"> 40 + Grinders 41 + </h3> 42 + <button 43 + @click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}" 44 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 45 + + Add Grinder 46 + </button> 47 + </div> 48 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 shadow-xl rounded-xl overflow-x-auto border border-brown-300"> 49 + <table class="min-w-full divide-y divide-brown-300"> 50 + <thead class="bg-brown-200/80"> 51 + <tr> 52 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 53 + Name 54 + </th> 55 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 56 + 🔧 Grinder Type 57 + </th> 58 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 59 + 💎 Burr Type 60 + </th> 61 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 62 + 📝 Notes 63 + </th> 64 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 65 + Actions 66 + </th> 67 + </tr> 68 + </thead> 69 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 70 + <tr class="hover:bg-brown-100/60 transition-colors"> 71 + <td class="px-6 py-4 text-sm font-medium text-brown-900"> 72 + Comandante C40 MK3 73 + </td> 74 + <td class="px-6 py-4 text-sm text-brown-900"> 75 + Hand 76 + </td> 77 + <td class="px-6 py-4 text-sm text-brown-900"> 78 + Conical 79 + </td> 80 + <td class="px-6 py-4 text-sm text-brown-700"> 81 + Excellent consistency, great for pour-over 82 + </td> 83 + <td class="px-6 py-4 text-sm font-medium space-x-2"> 84 + <button @click="editGrinder('grinder1', 'Comandante C40 MK3', 'Hand', 'Conical', 'Excellent consistency, great for pour-over')" 85 + class="text-brown-700 hover:text-brown-900 font-medium"> 86 + Edit 87 + </button> 88 + <button @click="deleteGrinder('grinder1')" 89 + class="text-brown-600 hover:text-brown-800 font-medium"> 90 + Delete 91 + </button> 92 + </td> 93 + </tr> 94 + <tr class="hover:bg-brown-100/60 transition-colors"> 95 + <td class="px-6 py-4 text-sm font-medium text-brown-900"> 96 + Baratza Encore 97 + </td> 98 + <td class="px-6 py-4 text-sm text-brown-900"> 99 + Electric 100 + </td> 101 + <td class="px-6 py-4 text-sm text-brown-900"> 102 + Conical 103 + </td> 104 + <td class="px-6 py-4 text-sm text-brown-700"></td> 105 + <td class="px-6 py-4 text-sm font-medium space-x-2"> 106 + <button @click="editGrinder('grinder2', 'Baratza Encore', 'Electric', 'Conical', '')" 107 + class="text-brown-700 hover:text-brown-900 font-medium"> 108 + Edit 109 + </button> 110 + <button @click="deleteGrinder('grinder2')" 111 + class="text-brown-600 hover:text-brown-800 font-medium"> 112 + Delete 113 + </button> 114 + </td> 115 + </tr> 116 + </tbody> 117 + </table> 118 + </div> 119 + </div> 120 + <div x-show="tab === 'brewers'"> 121 + <div class="mb-4 flex justify-between items-center"> 122 + <h3 class="text-xl font-semibold text-brown-900"> 123 + Brewers 124 + </h3> 125 + <button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}" 126 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 127 + + Add Brewer 128 + </button> 129 + </div> 130 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 131 + No brewers yet. Add your first brewer! 132 + </div> 133 + </div> 134 + <div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 135 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 136 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3> 137 + <div class="space-y-4"> 138 + <input type="text" x-model="beanForm.name" placeholder="Name *" 139 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 140 + <input type="text" x-model="beanForm.origin" placeholder="Origin *" 141 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 142 + <select x-model="beanForm.roaster_rkey" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 143 + <option value=""> 144 + Select Roaster (Optional) 145 + </option> 146 + </select> 147 + <select x-model="beanForm.roast_level" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 148 + <option value=""> 149 + Select Roast Level (Optional) 150 + </option> 151 + <option value="Ultra-Light"> 152 + Ultra-Light 153 + </option> 154 + <option value="Light"> 155 + Light 156 + </option> 157 + <option value="Medium-Light"> 158 + Medium-Light 159 + </option> 160 + <option value="Medium"> 161 + Medium 162 + </option> 163 + <option value="Medium-Dark"> 164 + Medium-Dark 165 + </option> 166 + <option value="Dark"> 167 + Dark 168 + </option> 169 + </select> 170 + <input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)" 171 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 172 + <textarea x-model="beanForm.description" placeholder="Description" rows="3" 173 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 174 + <div class="flex gap-2"> 175 + <button @click="saveBean()" 176 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 177 + Save 178 + </button> 179 + <button @click="showBeanForm = false" 180 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 181 + Cancel 182 + </button> 183 + </div> 184 + </div> 185 + </div> 186 + </div> 187 + <div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 188 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 189 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3> 190 + <div class="space-y-4"> 191 + <input type="text" x-model="roasterForm.name" placeholder="Name *" 192 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 193 + <input type="text" x-model="roasterForm.location" placeholder="Location" 194 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 195 + <input type="url" x-model="roasterForm.website" placeholder="Website" 196 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 197 + <div class="flex gap-2"> 198 + <button @click="saveRoaster()" 199 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 200 + Save 201 + </button> 202 + <button @click="showRoasterForm = false" 203 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 204 + Cancel 205 + </button> 206 + </div> 207 + </div> 208 + </div> 209 + </div> 210 + <div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 211 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 212 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3> 213 + <div class="space-y-4"> 214 + <input type="text" x-model="grinderForm.name" placeholder="Name *" 215 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 216 + <select x-model="grinderForm.grinder_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 217 + <option value=""> 218 + Select Grinder Type * 219 + </option> 220 + <option value="Hand"> 221 + Hand 222 + </option> 223 + <option value="Electric"> 224 + Electric 225 + </option> 226 + <option value="Portable Electric"> 227 + Portable Electric 228 + </option> 229 + </select> 230 + <select x-model="grinderForm.burr_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 231 + <option value=""> 232 + Select Burr Type (Optional) 233 + </option> 234 + <option value="Conical"> 235 + Conical 236 + </option> 237 + <option value="Flat"> 238 + Flat 239 + </option> 240 + </select> 241 + <textarea x-model="grinderForm.notes" placeholder="Notes" rows="3" 242 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 243 + <div class="flex gap-2"> 244 + <button @click="saveGrinder()" 245 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 246 + Save 247 + </button> 248 + <button @click="showGrinderForm = false" 249 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 250 + Cancel 251 + </button> 252 + </div> 253 + </div> 254 + </div> 255 + </div> 256 + <div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 257 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 258 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3> 259 + <div class="space-y-4"> 260 + <input type="text" x-model="brewerForm.name" placeholder="Name *" 261 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 262 + <input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)" 263 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 264 + <textarea x-model="brewerForm.description" placeholder="Description" rows="3" 265 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 266 + <div class="flex gap-2"> 267 + <button @click="saveBrewer()" 268 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 269 + Save 270 + </button> 271 + <button @click="showBrewerForm = false" 272 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 273 + Cancel 274 + </button> 275 + </div> 276 + </div> 277 + </div> 278 + </div>
+256
internal/bff/__snapshots__/grinders_with_unicode.snap
··· 1 + --- 2 + title: grinders with unicode 3 + test_name: TestManageContent_SpecialCharacters_Snapshot/grinders_with_unicode 4 + file_name: partial_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="tab === 'beans'"> 8 + <div class="mb-4 flex justify-between items-center"> 9 + <h3 class="text-xl font-semibold text-brown-900"> 10 + Coffee Beans 11 + </h3> 12 + <button 13 + @click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}" 14 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 15 + + Add Bean 16 + </button> 17 + </div> 18 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 19 + No beans yet. Add your first bean to get started! 20 + </div> 21 + </div> 22 + <div x-show="tab === 'roasters'"> 23 + <div class="mb-4 flex justify-between items-center"> 24 + <h3 class="text-xl font-semibold text-brown-900"> 25 + Roasters 26 + </h3> 27 + <button 28 + @click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}" 29 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 30 + + Add Roaster 31 + </button> 32 + </div> 33 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 34 + No roasters yet. Add your first roaster! 35 + </div> 36 + </div> 37 + <div x-show="tab === 'grinders'"> 38 + <div class="mb-4 flex justify-between items-center"> 39 + <h3 class="text-xl font-semibold text-brown-900"> 40 + Grinders 41 + </h3> 42 + <button 43 + @click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}" 44 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 45 + + Add Grinder 46 + </button> 47 + </div> 48 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 shadow-xl rounded-xl overflow-x-auto border border-brown-300"> 49 + <table class="min-w-full divide-y divide-brown-300"> 50 + <thead class="bg-brown-200/80"> 51 + <tr> 52 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 53 + Name 54 + </th> 55 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 56 + 🔧 Grinder Type 57 + </th> 58 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 59 + 💎 Burr Type 60 + </th> 61 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 62 + 📝 Notes 63 + </th> 64 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 65 + Actions 66 + </th> 67 + </tr> 68 + </thead> 69 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 70 + <tr class="hover:bg-brown-100/60 transition-colors"> 71 + <td class="px-6 py-4 text-sm font-medium text-brown-900"> 72 + 手動コーヒーミル Comandante® C40 73 + </td> 74 + <td class="px-6 py-4 text-sm text-brown-900"> 75 + Hand 76 + </td> 77 + <td class="px-6 py-4 text-sm text-brown-900"> 78 + Conical 79 + </td> 80 + <td class="px-6 py-4 text-sm text-brown-700"> 81 + 日本語のノート - Отличная кофемолка 🇯🇵 82 + </td> 83 + <td class="px-6 py-4 text-sm font-medium space-x-2"> 84 + <button @click="editGrinder('grinder1', '手動コーヒーミル Comandante® C40', 'Hand', 'Conical', '日本語のノート - Отличная кофемолка 🇯🇵')" 85 + class="text-brown-700 hover:text-brown-900 font-medium"> 86 + Edit 87 + </button> 88 + <button @click="deleteGrinder('grinder1')" 89 + class="text-brown-600 hover:text-brown-800 font-medium"> 90 + Delete 91 + </button> 92 + </td> 93 + </tr> 94 + </tbody> 95 + </table> 96 + </div> 97 + </div> 98 + <div x-show="tab === 'brewers'"> 99 + <div class="mb-4 flex justify-between items-center"> 100 + <h3 class="text-xl font-semibold text-brown-900"> 101 + Brewers 102 + </h3> 103 + <button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}" 104 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 105 + + Add Brewer 106 + </button> 107 + </div> 108 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 109 + No brewers yet. Add your first brewer! 110 + </div> 111 + </div> 112 + <div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 113 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 114 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3> 115 + <div class="space-y-4"> 116 + <input type="text" x-model="beanForm.name" placeholder="Name *" 117 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 118 + <input type="text" x-model="beanForm.origin" placeholder="Origin *" 119 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 120 + <select x-model="beanForm.roaster_rkey" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 121 + <option value=""> 122 + Select Roaster (Optional) 123 + </option> 124 + </select> 125 + <select x-model="beanForm.roast_level" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 126 + <option value=""> 127 + Select Roast Level (Optional) 128 + </option> 129 + <option value="Ultra-Light"> 130 + Ultra-Light 131 + </option> 132 + <option value="Light"> 133 + Light 134 + </option> 135 + <option value="Medium-Light"> 136 + Medium-Light 137 + </option> 138 + <option value="Medium"> 139 + Medium 140 + </option> 141 + <option value="Medium-Dark"> 142 + Medium-Dark 143 + </option> 144 + <option value="Dark"> 145 + Dark 146 + </option> 147 + </select> 148 + <input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)" 149 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 150 + <textarea x-model="beanForm.description" placeholder="Description" rows="3" 151 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 152 + <div class="flex gap-2"> 153 + <button @click="saveBean()" 154 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 155 + Save 156 + </button> 157 + <button @click="showBeanForm = false" 158 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 159 + Cancel 160 + </button> 161 + </div> 162 + </div> 163 + </div> 164 + </div> 165 + <div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 166 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 167 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3> 168 + <div class="space-y-4"> 169 + <input type="text" x-model="roasterForm.name" placeholder="Name *" 170 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 171 + <input type="text" x-model="roasterForm.location" placeholder="Location" 172 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 173 + <input type="url" x-model="roasterForm.website" placeholder="Website" 174 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 175 + <div class="flex gap-2"> 176 + <button @click="saveRoaster()" 177 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 178 + Save 179 + </button> 180 + <button @click="showRoasterForm = false" 181 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 182 + Cancel 183 + </button> 184 + </div> 185 + </div> 186 + </div> 187 + </div> 188 + <div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 189 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 190 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3> 191 + <div class="space-y-4"> 192 + <input type="text" x-model="grinderForm.name" placeholder="Name *" 193 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 194 + <select x-model="grinderForm.grinder_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 195 + <option value=""> 196 + Select Grinder Type * 197 + </option> 198 + <option value="Hand"> 199 + Hand 200 + </option> 201 + <option value="Electric"> 202 + Electric 203 + </option> 204 + <option value="Portable Electric"> 205 + Portable Electric 206 + </option> 207 + </select> 208 + <select x-model="grinderForm.burr_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 209 + <option value=""> 210 + Select Burr Type (Optional) 211 + </option> 212 + <option value="Conical"> 213 + Conical 214 + </option> 215 + <option value="Flat"> 216 + Flat 217 + </option> 218 + </select> 219 + <textarea x-model="grinderForm.notes" placeholder="Notes" rows="3" 220 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 221 + <div class="flex gap-2"> 222 + <button @click="saveGrinder()" 223 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 224 + Save 225 + </button> 226 + <button @click="showGrinderForm = false" 227 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 228 + Cancel 229 + </button> 230 + </div> 231 + </div> 232 + </div> 233 + </div> 234 + <div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 235 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 236 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3> 237 + <div class="space-y-4"> 238 + <input type="text" x-model="brewerForm.name" placeholder="Name *" 239 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 240 + <input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)" 241 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 242 + <textarea x-model="brewerForm.description" placeholder="Description" rows="3" 243 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 244 + <div class="flex gap-2"> 245 + <button @click="saveBrewer()" 246 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 247 + Save 248 + </button> 249 + <button @click="showBrewerForm = false" 250 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 251 + Cancel 252 + </button> 253 + </div> 254 + </div> 255 + </div> 256 + </div>
+296 -1
internal/bff/__snapshots__/mixed_feed_all_types.snap
··· 4 4 file_name: feed_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n<div class=\"space-y-4\">\n \n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/user1\" class=\"flex-shrink-0\">\n \n <div class=\"w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition\">\n <span class=\"text-brown-600 text-sm\">?</span>\n </div>\n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/user1\" class=\"font-medium text-brown-900 truncate hover:text-brown-700 hover:underline\">User One</a>\n \n <a href=\"/profile/user1\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@user1</a>\n </div>\n <span class=\"text-brown-500 text-sm\">1 hour ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n ☕ added a new brew\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-4 border border-brown-200\">\n \n <div class=\"flex items-start justify-between gap-3 mb-3\">\n <div class=\"flex-1 min-w-0\">\n \n <div class=\"font-bold text-brown-900 text-base\">\n Ethiopian Yirgacheffe\n </div>\n \n <div class=\"text-sm text-brown-700 mt-0.5\">\n <span class=\"font-medium\">🏪 Onyx</span>\n </div>\n \n <div class=\"text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5\">\n <span class=\"inline-flex items-center gap-0.5\">📍 Ethiopia</span>\n <span class=\"inline-flex items-center gap-0.5\">🔥 Light</span>\n <span class=\"inline-flex items-center gap-0.5\">🌱 Washed</span>\n <span class=\"inline-flex items-center gap-0.5\">⚖️ 16g</span>\n </div>\n \n </div>\n \n <span class=\"inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-amber-100 text-amber-900 flex-shrink-0\">\n ⭐ 9/10\n </span>\n \n </div>\n \n \n \n <div class=\"mb-2\">\n <span class=\"text-xs text-brown-600\">Brewer:</span>\n <span class=\"text-sm font-semibold text-brown-900\">\n Hario V60\n </span>\n </div>\n \n \n \n <div class=\"grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700\">\n \n <div>\n <span class=\"text-brown-600\">Grinder:</span> 1Zpresso JX-Pro (Medium-fine)\n </div>\n \n \n <div class=\"col-span-2\">\n <span class=\"text-brown-600\">Pours:</span>\n \n <div class=\"pl-2 text-brown-600\">• 50g @ 30s</div>\n \n <div class=\"pl-2 text-brown-600\">• 100g @ 45s</div>\n \n <div class=\"pl-2 text-brown-600\">• 100g @ 1m</div>\n \n </div>\n \n \n <div>\n <span class=\"text-brown-600\">Temp:</span> 93.0°C\n </div>\n \n \n <div>\n <span class=\"text-brown-600\">Time:</span> 3m\n </div>\n \n </div>\n\n \n <div class=\"mt-3 text-sm text-brown-800 italic border-t border-brown-200 pt-2\">\n \"Bright citrus notes with floral aroma\"\n </div>\n \n </div>\n \n </div>\n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/user2\" class=\"flex-shrink-0\">\n \n \n \n <img src=\"https://cdn.bsky.app/avatar.jpg\" alt=\"\" class=\"w-10 h-10 rounded-full object-cover hover:ring-2 hover:ring-brown-600 transition\" />\n \n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/user2\" class=\"font-medium text-brown-900 truncate hover:text-brown-700 hover:underline\">User Two</a>\n \n <a href=\"/profile/user2\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@user2</a>\n </div>\n <span class=\"text-brown-500 text-sm\">1.5 hours ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n 🫘 added a new bean\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200\">\n <div class=\"text-base mb-2\">\n <span class=\"font-bold text-brown-900\">\n Kenya AA\n </span>\n \n <span class=\"text-brown-700\"> from Onyx Coffee Lab</span>\n \n </div>\n <div class=\"text-sm text-brown-700 space-y-1\">\n \n <div><span class=\"text-brown-600\">Origin:</span> Kenya</div>\n \n \n <div><span class=\"text-brown-600\">Roast:</span> Medium</div>\n \n \n <div><span class=\"text-brown-600\">Process:</span> Natural</div>\n \n \n <div class=\"mt-2 text-brown-800 italic\">\"Sweet and fruity with notes of blueberry\"</div>\n \n </div>\n </div>\n \n </div>\n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/user3\" class=\"flex-shrink-0\">\n \n <div class=\"w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition\">\n <span class=\"text-brown-600 text-sm\">?</span>\n </div>\n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/user3\" class=\"font-medium text-brown-900 truncate hover:text-brown-700 hover:underline\">User Three</a>\n \n <a href=\"/profile/user3\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@user3</a>\n </div>\n <span class=\"text-brown-500 text-sm\">2 hours ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n 🏪 added a new roaster\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200\">\n <div class=\"text-base mb-2\">\n <span class=\"font-bold text-brown-900\">Heart Coffee Roasters</span>\n </div>\n <div class=\"text-sm text-brown-700 space-y-1\">\n \n <div><span class=\"text-brown-600\">Location:</span> Portland, OR</div>\n \n \n \n \n <div><span class=\"text-brown-600\">Website:</span> <a href=\"https://heartroasters.com\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-brown-800 hover:underline\">https://heartroasters.com</a></div>\n \n \n </div>\n </div>\n \n </div>\n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/user4\" class=\"flex-shrink-0\">\n \n <div class=\"w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition\">\n <span class=\"text-brown-600 text-sm\">?</span>\n </div>\n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/user4\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@user4</a>\n </div>\n <span class=\"text-brown-500 text-sm\">2.5 hours ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n ⚙️ added a new grinder\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200\">\n <div class=\"text-base mb-2\">\n <span class=\"font-bold text-brown-900\">Comandante C40</span>\n </div>\n <div class=\"text-sm text-brown-700 space-y-1\">\n \n <div><span class=\"text-brown-600\">Type:</span> Hand</div>\n \n \n <div><span class=\"text-brown-600\">Burr:</span> Conical</div>\n \n \n <div class=\"mt-2 text-brown-800 italic\">\"Excellent for pour over\"</div>\n \n </div>\n </div>\n \n </div>\n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/user5\" class=\"flex-shrink-0\">\n \n <div class=\"w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition\">\n <span class=\"text-brown-600 text-sm\">?</span>\n </div>\n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/user5\" class=\"font-medium text-brown-900 truncate hover:text-brown-700 hover:underline\">User Five</a>\n \n <a href=\"/profile/user5\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@user5</a>\n </div>\n <span class=\"text-brown-500 text-sm\">3 hours ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n ☕ added a new brewer\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200\">\n <div class=\"text-base mb-2\">\n <span class=\"font-bold text-brown-900\">Kalita Wave 185</span>\n </div>\n \n <div class=\"text-sm text-brown-800 italic\">\"Flat-bottom dripper with wave filters\"</div>\n \n </div>\n \n </div>\n \n \n \n</div>\n" 7 + <div class="space-y-4"> 8 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 9 + <div class="flex items-center gap-3 mb-3"> 10 + <a href="/profile/user1" class="flex-shrink-0"> 11 + <div class="w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition"> 12 + <span class="text-brown-600 text-sm"> 13 + ? 14 + </span> 15 + </div> 16 + </a> 17 + <div class="flex-1 min-w-0"> 18 + <div class="flex items-center gap-2"> 19 + <a href="/profile/user1" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline"> 20 + User One 21 + </a> 22 + <a href="/profile/user1" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 23 + @user1 24 + </a> 25 + </div> 26 + <span class="text-brown-500 text-sm"> 27 + 1 hour ago 28 + </span> 29 + </div> 30 + </div> 31 + <div class="mb-2 text-sm text-brown-700"> 32 + ☕ added a new brew 33 + </div> 34 + <div class="bg-white/60 backdrop-blur rounded-lg p-4 border border-brown-200"> 35 + <div class="flex items-start justify-between gap-3 mb-3"> 36 + <div class="flex-1 min-w-0"> 37 + <div class="font-bold text-brown-900 text-base"> 38 + Ethiopian Yirgacheffe 39 + </div> 40 + <div class="text-sm text-brown-700 mt-0.5"> 41 + <span class="font-medium"> 42 + 🏪 Onyx 43 + </span> 44 + </div> 45 + <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 46 + <span class="inline-flex items-center gap-0.5"> 47 + 📍 Ethiopia 48 + </span> 49 + <span class="inline-flex items-center gap-0.5"> 50 + 🔥 Light 51 + </span> 52 + <span class="inline-flex items-center gap-0.5"> 53 + 🌱 Washed 54 + </span> 55 + <span class="inline-flex items-center gap-0.5"> 56 + ⚖️ 16g 57 + </span> 58 + </div> 59 + </div> 60 + <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-amber-100 text-amber-900 flex-shrink-0"> 61 + ⭐ 9/10 62 + </span> 63 + </div> 64 + <div class="mb-2"> 65 + <span class="text-xs text-brown-600"> 66 + Brewer: 67 + </span> 68 + <span class="text-sm font-semibold text-brown-900"> 69 + Hario V60 70 + </span> 71 + </div> 72 + <div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700"> 73 + <div> 74 + <span class="text-brown-600"> 75 + Grinder: 76 + </span> 77 + 1Zpresso JX-Pro (Medium-fine) 78 + </div> 79 + <div class="col-span-2"> 80 + <span class="text-brown-600"> 81 + Pours: 82 + </span> 83 + <div class="pl-2 text-brown-600"> 84 + • 50g @ 30s 85 + </div> 86 + <div class="pl-2 text-brown-600"> 87 + • 100g @ 45s 88 + </div> 89 + <div class="pl-2 text-brown-600"> 90 + • 100g @ 1m 91 + </div> 92 + </div> 93 + <div> 94 + <span class="text-brown-600"> 95 + Temp: 96 + </span> 97 + 93.0°C 98 + </div> 99 + <div> 100 + <span class="text-brown-600"> 101 + Time: 102 + </span> 103 + 3m 104 + </div> 105 + </div> 106 + <div class="mt-3 text-sm text-brown-800 italic border-t border-brown-200 pt-2"> 107 + "Bright citrus notes with floral aroma" 108 + </div> 109 + </div> 110 + </div> 111 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 112 + <div class="flex items-center gap-3 mb-3"> 113 + <a href="/profile/user2" class="flex-shrink-0"> 114 + <img src="https://cdn.bsky.app/avatar.jpg" alt="" class="w-10 h-10 rounded-full object-cover hover:ring-2 hover:ring-brown-600 transition" /> 115 + </a> 116 + <div class="flex-1 min-w-0"> 117 + <div class="flex items-center gap-2"> 118 + <a href="/profile/user2" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline"> 119 + User Two 120 + </a> 121 + <a href="/profile/user2" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 122 + @user2 123 + </a> 124 + </div> 125 + <span class="text-brown-500 text-sm"> 126 + 1.5 hours ago 127 + </span> 128 + </div> 129 + </div> 130 + <div class="mb-2 text-sm text-brown-700"> 131 + 🫘 added a new bean 132 + </div> 133 + <div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200"> 134 + <div class="text-base mb-2"> 135 + <span class="font-bold text-brown-900"> 136 + Kenya AA 137 + </span> 138 + <span class="text-brown-700"> 139 + from Onyx Coffee Lab 140 + </span> 141 + </div> 142 + <div class="text-sm text-brown-700 space-y-1"> 143 + <div> 144 + <span class="text-brown-600"> 145 + Origin: 146 + </span> 147 + Kenya 148 + </div> 149 + <div> 150 + <span class="text-brown-600"> 151 + Roast: 152 + </span> 153 + Medium 154 + </div> 155 + <div> 156 + <span class="text-brown-600"> 157 + Process: 158 + </span> 159 + Natural 160 + </div> 161 + <div class="mt-2 text-brown-800 italic"> 162 + "Sweet and fruity with notes of blueberry" 163 + </div> 164 + </div> 165 + </div> 166 + </div> 167 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 168 + <div class="flex items-center gap-3 mb-3"> 169 + <a href="/profile/user3" class="flex-shrink-0"> 170 + <div class="w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition"> 171 + <span class="text-brown-600 text-sm"> 172 + ? 173 + </span> 174 + </div> 175 + </a> 176 + <div class="flex-1 min-w-0"> 177 + <div class="flex items-center gap-2"> 178 + <a href="/profile/user3" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline"> 179 + User Three 180 + </a> 181 + <a href="/profile/user3" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 182 + @user3 183 + </a> 184 + </div> 185 + <span class="text-brown-500 text-sm"> 186 + 2 hours ago 187 + </span> 188 + </div> 189 + </div> 190 + <div class="mb-2 text-sm text-brown-700"> 191 + 🏪 added a new roaster 192 + </div> 193 + <div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200"> 194 + <div class="text-base mb-2"> 195 + <span class="font-bold text-brown-900"> 196 + Heart Coffee Roasters 197 + </span> 198 + </div> 199 + <div class="text-sm text-brown-700 space-y-1"> 200 + <div> 201 + <span class="text-brown-600"> 202 + Location: 203 + </span> 204 + Portland, OR 205 + </div> 206 + <div> 207 + <span class="text-brown-600"> 208 + Website: 209 + </span> 210 + <a href="https://heartroasters.com" target="_blank" rel="noopener noreferrer" class="text-brown-800 hover:underline"> 211 + https://heartroasters.com 212 + </a> 213 + </div> 214 + </div> 215 + </div> 216 + </div> 217 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 218 + <div class="flex items-center gap-3 mb-3"> 219 + <a href="/profile/user4" class="flex-shrink-0"> 220 + <div class="w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition"> 221 + <span class="text-brown-600 text-sm"> 222 + ? 223 + </span> 224 + </div> 225 + </a> 226 + <div class="flex-1 min-w-0"> 227 + <div class="flex items-center gap-2"> 228 + <a href="/profile/user4" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 229 + @user4 230 + </a> 231 + </div> 232 + <span class="text-brown-500 text-sm"> 233 + 2.5 hours ago 234 + </span> 235 + </div> 236 + </div> 237 + <div class="mb-2 text-sm text-brown-700"> 238 + ⚙️ added a new grinder 239 + </div> 240 + <div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200"> 241 + <div class="text-base mb-2"> 242 + <span class="font-bold text-brown-900"> 243 + Comandante C40 244 + </span> 245 + </div> 246 + <div class="text-sm text-brown-700 space-y-1"> 247 + <div> 248 + <span class="text-brown-600"> 249 + Type: 250 + </span> 251 + Hand 252 + </div> 253 + <div> 254 + <span class="text-brown-600"> 255 + Burr: 256 + </span> 257 + Conical 258 + </div> 259 + <div class="mt-2 text-brown-800 italic"> 260 + "Excellent for pour over" 261 + </div> 262 + </div> 263 + </div> 264 + </div> 265 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 266 + <div class="flex items-center gap-3 mb-3"> 267 + <a href="/profile/user5" class="flex-shrink-0"> 268 + <div class="w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition"> 269 + <span class="text-brown-600 text-sm"> 270 + ? 271 + </span> 272 + </div> 273 + </a> 274 + <div class="flex-1 min-w-0"> 275 + <div class="flex items-center gap-2"> 276 + <a href="/profile/user5" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline"> 277 + User Five 278 + </a> 279 + <a href="/profile/user5" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 280 + @user5 281 + </a> 282 + </div> 283 + <span class="text-brown-500 text-sm"> 284 + 3 hours ago 285 + </span> 286 + </div> 287 + </div> 288 + <div class="mb-2 text-sm text-brown-700"> 289 + ☕ added a new brewer 290 + </div> 291 + <div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200"> 292 + <div class="text-base mb-2"> 293 + <span class="font-bold text-brown-900"> 294 + Kalita Wave 185 295 + </span> 296 + </div> 297 + <div class="text-sm text-brown-800 italic"> 298 + "Flat-bottom dripper with wave filters" 299 + </div> 300 + </div> 301 + </div> 302 + </div>
+342
internal/bff/__snapshots__/new_brew_with_empty_selects.snap
··· 1 + --- 2 + title: new brew with empty selects 3 + test_name: TestBrewForm_NewBrew_Snapshot/new_brew_with_empty_selects 4 + file_name: form_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <script src="/static/js/brew-form.js"></script> 8 + <div class="max-w-2xl mx-auto"> 9 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300"> 10 + <h2 class="text-3xl font-bold text-brown-900 mb-6"> 11 + New Brew 12 + </h2> 13 + <form 14 + 15 + hx-post="/brews" 16 + 17 + hx-target="body" 18 + class="space-y-6" 19 + x-data="brewForm()" 20 + > 21 + <div> 22 + <label class="block text-sm font-medium text-brown-900 mb-2"> 23 + Coffee Bean 24 + </label> 25 + <div class="flex gap-2"> 26 + <select 27 + name="bean_rkey" 28 + required 29 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 30 + <option value=""> 31 + Select a bean... 32 + </option> 33 + </select> 34 + <button 35 + type="button" 36 + @click="showNewBean = true" 37 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 38 + + New 39 + </button> 40 + </div> 41 + <div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 42 + <h4 class="font-medium mb-3 text-gray-800"> 43 + Add New Bean 44 + </h4> 45 + <div class="space-y-3"> 46 + <input type="text" x-model="newBean.name" placeholder="Name (e.g. Morning Blend, House Espresso) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 47 + <input type="text" x-model="newBean.origin" placeholder="Origin (e.g. Ethiopia) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 48 + <select x-model="newBean.roasterRKey" name="roaster_rkey_modal" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 49 + <option value=""> 50 + Select Roaster (Optional) 51 + </option> 52 + </select> 53 + <select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 54 + <option value=""> 55 + Select Roast Level (Optional) 56 + </option> 57 + <option value="Ultra-Light"> 58 + Ultra-Light 59 + </option> 60 + <option value="Light"> 61 + Light 62 + </option> 63 + <option value="Medium-Light"> 64 + Medium-Light 65 + </option> 66 + <option value="Medium"> 67 + Medium 68 + </option> 69 + <option value="Medium-Dark"> 70 + Medium-Dark 71 + </option> 72 + <option value="Dark"> 73 + Dark 74 + </option> 75 + </select> 76 + <input type="text" x-model="newBean.process" placeholder="Process (e.g. Washed, Natural, Honey)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 77 + <input type="text" x-model="newBean.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 78 + <div class="flex gap-2"> 79 + <button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 80 + Add 81 + </button> 82 + <button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 83 + Cancel 84 + </button> 85 + </div> 86 + </div> 87 + </div> 88 + </div> 89 + <div> 90 + <label class="block text-sm font-medium text-brown-900 mb-2"> 91 + Coffee Amount (grams) 92 + </label> 93 + <input 94 + type="number" 95 + name="coffee_amount" 96 + step="0.1" 97 + 98 + placeholder="e.g. 18" 99 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 100 + <p class="text-sm text-brown-700 mt-1"> 101 + Amount of ground coffee used 102 + </p> 103 + </div> 104 + <div> 105 + <label class="block text-sm font-medium text-brown-900 mb-2"> 106 + Grinder 107 + </label> 108 + <div class="flex gap-2"> 109 + <select 110 + name="grinder_rkey" 111 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 112 + <option value=""> 113 + Select a grinder... 114 + </option> 115 + </select> 116 + <button 117 + type="button" 118 + @click="showNewGrinder = true" 119 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 120 + + New 121 + </button> 122 + </div> 123 + <div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 124 + <h4 class="font-medium mb-3 text-gray-800"> 125 + Add New Grinder 126 + </h4> 127 + <div class="space-y-3"> 128 + <input type="text" x-model="newGrinder.name" placeholder="Name (e.g. Baratza Encore) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 129 + <select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 130 + <option value=""> 131 + Grinder Type (Optional) 132 + </option> 133 + <option value="Hand"> 134 + Hand 135 + </option> 136 + <option value="Electric"> 137 + Electric 138 + </option> 139 + <option value="Electric Hand"> 140 + Electric Hand 141 + </option> 142 + </select> 143 + <select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 144 + <option value=""> 145 + Burr Type (Optional) 146 + </option> 147 + <option value="Conical"> 148 + Conical 149 + </option> 150 + <option value="Flat"> 151 + Flat 152 + </option> 153 + </select> 154 + <input type="text" x-model="newGrinder.notes" placeholder="Notes (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 155 + <div class="flex gap-2"> 156 + <button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 157 + Add 158 + </button> 159 + <button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 160 + Cancel 161 + </button> 162 + </div> 163 + </div> 164 + </div> 165 + </div> 166 + <div> 167 + <label class="block text-sm font-medium text-brown-900 mb-2"> 168 + Grind Size 169 + </label> 170 + <input 171 + type="text" 172 + name="grind_size" 173 + 174 + placeholder="e.g. 18, Medium, 3.5, Fine" 175 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 176 + <p class="text-sm text-brown-700 mt-1"> 177 + Enter a number (grinder setting) or description (e.g. "Medium", "Fine") 178 + </p> 179 + </div> 180 + <div> 181 + <label class="block text-sm font-medium text-brown-900 mb-2"> 182 + Brew Method 183 + </label> 184 + <div class="flex gap-2"> 185 + <select 186 + name="brewer_rkey" 187 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 188 + <option value=""> 189 + Select brew method... 190 + </option> 191 + </select> 192 + <button 193 + type="button" 194 + @click="showNewBrewer = true" 195 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 196 + + New 197 + </button> 198 + </div> 199 + <div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 200 + <h4 class="font-medium mb-3 text-gray-800"> 201 + Add New Brewer 202 + </h4> 203 + <div class="space-y-3"> 204 + <input type="text" x-model="newBrewer.name" placeholder="Name (e.g. V60, AeroPress) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 205 + <input type="text" x-model="newBrewer.brewer_type" placeholder="Type (e.g. Pour-Over, Immersion)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 206 + <input type="text" x-model="newBrewer.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 207 + <div class="flex gap-2"> 208 + <button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 209 + Add 210 + </button> 211 + <button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 212 + Cancel 213 + </button> 214 + </div> 215 + </div> 216 + </div> 217 + </div> 218 + <div> 219 + <label class="block text-sm font-medium text-brown-900 mb-2"> 220 + Water Amount (grams) 221 + </label> 222 + <input 223 + type="number" 224 + name="water_amount" 225 + step="1" 226 + 227 + placeholder="e.g. 250" 228 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 229 + <p class="text-sm text-brown-700 mt-1"> 230 + Total water used (or leave empty if using pours below) 231 + </p> 232 + </div> 233 + <div> 234 + <div class="flex items-center justify-between mb-2"> 235 + <label class="block text-sm font-medium text-brown-900"> 236 + Pours (Optional) 237 + </label> 238 + <button 239 + type="button" 240 + @click="addPour()" 241 + class="text-sm bg-brown-300 text-brown-900 px-3 py-1 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 242 + + Add Pour 243 + </button> 244 + </div> 245 + <p class="text-sm text-brown-700 mb-3"> 246 + Track individual pours for bloom and subsequent additions 247 + </p> 248 + <div class="space-y-3"> 249 + <template x-for="(pour, index) in pours" :key="index"> 250 + <div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200"> 251 + <div class="flex-1"> 252 + <label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label> 253 + <input 254 + type="number" 255 + :name="'pour_water_' + index" 256 + x-model="pour.water" 257 + placeholder="Water (g)" 258 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 259 + </div> 260 + <div class="flex-1"> 261 + <label class="text-xs text-brown-700 font-medium"> 262 + Time (sec) 263 + </label> 264 + <input 265 + type="number" 266 + :name="'pour_time_' + index" 267 + x-model="pour.time" 268 + placeholder="e.g. 45" 269 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 270 + </div> 271 + <button 272 + type="button" 273 + @click="removePour(index)" 274 + class="text-brown-700 hover:text-brown-900 mt-5 font-bold" 275 + x-show="pours.length > 0"> 276 + 277 + </button> 278 + </div> 279 + </template> 280 + </div> 281 + </div> 282 + <div> 283 + <label class="block text-sm font-medium text-brown-900 mb-2"> 284 + Temperature 285 + </label> 286 + <input 287 + type="number" 288 + name="temperature" 289 + step="0.1" 290 + 291 + placeholder="e.g. 93.5" 292 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 293 + </div> 294 + <div> 295 + <label class="block text-sm font-medium text-brown-900 mb-2"> 296 + Brew Time (seconds) 297 + </label> 298 + <input 299 + type="number" 300 + name="time_seconds" 301 + 302 + placeholder="e.g. 180" 303 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 304 + </div> 305 + <div> 306 + <label class="block text-sm font-medium text-brown-900 mb-2"> 307 + Tasting Notes 308 + </label> 309 + <textarea 310 + name="tasting_notes" 311 + rows="4" 312 + placeholder="Describe the flavors, aroma, and your thoughts..." 313 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"></textarea> 314 + </div> 315 + <div> 316 + <label class="block text-sm font-medium text-brown-900 mb-2"> 317 + Rating 318 + </label> 319 + <input 320 + type="range" 321 + name="rating" 322 + min="1" 323 + max="10" 324 + value="5" 325 + x-model="rating" 326 + x-init="rating = $el.value" 327 + class="w-full accent-brown-700"/> 328 + <div class="text-center text-2xl font-bold text-brown-800"> 329 + <span x-text="rating"></span> 330 + /10 331 + </div> 332 + </div> 333 + <div> 334 + <button 335 + type="submit" 336 + class="w-full bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-xl hover:from-brown-800 hover:to-brown-900 transition-all font-semibold text-lg shadow-lg hover:shadow-xl"> 337 + Save Brew 338 + </button> 339 + </div> 340 + </form> 341 + </div> 342 + </div>
+342
internal/bff/__snapshots__/new_brew_with_nil_collections.snap
··· 1 + --- 2 + title: new brew with nil collections 3 + test_name: TestBrewForm_NewBrew_Snapshot/new_brew_with_nil_collections 4 + file_name: form_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <script src="/static/js/brew-form.js"></script> 8 + <div class="max-w-2xl mx-auto"> 9 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300"> 10 + <h2 class="text-3xl font-bold text-brown-900 mb-6"> 11 + New Brew 12 + </h2> 13 + <form 14 + 15 + hx-post="/brews" 16 + 17 + hx-target="body" 18 + class="space-y-6" 19 + x-data="brewForm()" 20 + > 21 + <div> 22 + <label class="block text-sm font-medium text-brown-900 mb-2"> 23 + Coffee Bean 24 + </label> 25 + <div class="flex gap-2"> 26 + <select 27 + name="bean_rkey" 28 + required 29 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 30 + <option value=""> 31 + Select a bean... 32 + </option> 33 + </select> 34 + <button 35 + type="button" 36 + @click="showNewBean = true" 37 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 38 + + New 39 + </button> 40 + </div> 41 + <div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 42 + <h4 class="font-medium mb-3 text-gray-800"> 43 + Add New Bean 44 + </h4> 45 + <div class="space-y-3"> 46 + <input type="text" x-model="newBean.name" placeholder="Name (e.g. Morning Blend, House Espresso) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 47 + <input type="text" x-model="newBean.origin" placeholder="Origin (e.g. Ethiopia) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 48 + <select x-model="newBean.roasterRKey" name="roaster_rkey_modal" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 49 + <option value=""> 50 + Select Roaster (Optional) 51 + </option> 52 + </select> 53 + <select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 54 + <option value=""> 55 + Select Roast Level (Optional) 56 + </option> 57 + <option value="Ultra-Light"> 58 + Ultra-Light 59 + </option> 60 + <option value="Light"> 61 + Light 62 + </option> 63 + <option value="Medium-Light"> 64 + Medium-Light 65 + </option> 66 + <option value="Medium"> 67 + Medium 68 + </option> 69 + <option value="Medium-Dark"> 70 + Medium-Dark 71 + </option> 72 + <option value="Dark"> 73 + Dark 74 + </option> 75 + </select> 76 + <input type="text" x-model="newBean.process" placeholder="Process (e.g. Washed, Natural, Honey)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 77 + <input type="text" x-model="newBean.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 78 + <div class="flex gap-2"> 79 + <button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 80 + Add 81 + </button> 82 + <button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 83 + Cancel 84 + </button> 85 + </div> 86 + </div> 87 + </div> 88 + </div> 89 + <div> 90 + <label class="block text-sm font-medium text-brown-900 mb-2"> 91 + Coffee Amount (grams) 92 + </label> 93 + <input 94 + type="number" 95 + name="coffee_amount" 96 + step="0.1" 97 + 98 + placeholder="e.g. 18" 99 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 100 + <p class="text-sm text-brown-700 mt-1"> 101 + Amount of ground coffee used 102 + </p> 103 + </div> 104 + <div> 105 + <label class="block text-sm font-medium text-brown-900 mb-2"> 106 + Grinder 107 + </label> 108 + <div class="flex gap-2"> 109 + <select 110 + name="grinder_rkey" 111 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 112 + <option value=""> 113 + Select a grinder... 114 + </option> 115 + </select> 116 + <button 117 + type="button" 118 + @click="showNewGrinder = true" 119 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 120 + + New 121 + </button> 122 + </div> 123 + <div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 124 + <h4 class="font-medium mb-3 text-gray-800"> 125 + Add New Grinder 126 + </h4> 127 + <div class="space-y-3"> 128 + <input type="text" x-model="newGrinder.name" placeholder="Name (e.g. Baratza Encore) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 129 + <select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 130 + <option value=""> 131 + Grinder Type (Optional) 132 + </option> 133 + <option value="Hand"> 134 + Hand 135 + </option> 136 + <option value="Electric"> 137 + Electric 138 + </option> 139 + <option value="Electric Hand"> 140 + Electric Hand 141 + </option> 142 + </select> 143 + <select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 144 + <option value=""> 145 + Burr Type (Optional) 146 + </option> 147 + <option value="Conical"> 148 + Conical 149 + </option> 150 + <option value="Flat"> 151 + Flat 152 + </option> 153 + </select> 154 + <input type="text" x-model="newGrinder.notes" placeholder="Notes (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 155 + <div class="flex gap-2"> 156 + <button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 157 + Add 158 + </button> 159 + <button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 160 + Cancel 161 + </button> 162 + </div> 163 + </div> 164 + </div> 165 + </div> 166 + <div> 167 + <label class="block text-sm font-medium text-brown-900 mb-2"> 168 + Grind Size 169 + </label> 170 + <input 171 + type="text" 172 + name="grind_size" 173 + 174 + placeholder="e.g. 18, Medium, 3.5, Fine" 175 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 176 + <p class="text-sm text-brown-700 mt-1"> 177 + Enter a number (grinder setting) or description (e.g. "Medium", "Fine") 178 + </p> 179 + </div> 180 + <div> 181 + <label class="block text-sm font-medium text-brown-900 mb-2"> 182 + Brew Method 183 + </label> 184 + <div class="flex gap-2"> 185 + <select 186 + name="brewer_rkey" 187 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 188 + <option value=""> 189 + Select brew method... 190 + </option> 191 + </select> 192 + <button 193 + type="button" 194 + @click="showNewBrewer = true" 195 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 196 + + New 197 + </button> 198 + </div> 199 + <div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 200 + <h4 class="font-medium mb-3 text-gray-800"> 201 + Add New Brewer 202 + </h4> 203 + <div class="space-y-3"> 204 + <input type="text" x-model="newBrewer.name" placeholder="Name (e.g. V60, AeroPress) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 205 + <input type="text" x-model="newBrewer.brewer_type" placeholder="Type (e.g. Pour-Over, Immersion)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 206 + <input type="text" x-model="newBrewer.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 207 + <div class="flex gap-2"> 208 + <button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 209 + Add 210 + </button> 211 + <button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 212 + Cancel 213 + </button> 214 + </div> 215 + </div> 216 + </div> 217 + </div> 218 + <div> 219 + <label class="block text-sm font-medium text-brown-900 mb-2"> 220 + Water Amount (grams) 221 + </label> 222 + <input 223 + type="number" 224 + name="water_amount" 225 + step="1" 226 + 227 + placeholder="e.g. 250" 228 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 229 + <p class="text-sm text-brown-700 mt-1"> 230 + Total water used (or leave empty if using pours below) 231 + </p> 232 + </div> 233 + <div> 234 + <div class="flex items-center justify-between mb-2"> 235 + <label class="block text-sm font-medium text-brown-900"> 236 + Pours (Optional) 237 + </label> 238 + <button 239 + type="button" 240 + @click="addPour()" 241 + class="text-sm bg-brown-300 text-brown-900 px-3 py-1 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 242 + + Add Pour 243 + </button> 244 + </div> 245 + <p class="text-sm text-brown-700 mb-3"> 246 + Track individual pours for bloom and subsequent additions 247 + </p> 248 + <div class="space-y-3"> 249 + <template x-for="(pour, index) in pours" :key="index"> 250 + <div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200"> 251 + <div class="flex-1"> 252 + <label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label> 253 + <input 254 + type="number" 255 + :name="'pour_water_' + index" 256 + x-model="pour.water" 257 + placeholder="Water (g)" 258 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 259 + </div> 260 + <div class="flex-1"> 261 + <label class="text-xs text-brown-700 font-medium"> 262 + Time (sec) 263 + </label> 264 + <input 265 + type="number" 266 + :name="'pour_time_' + index" 267 + x-model="pour.time" 268 + placeholder="e.g. 45" 269 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 270 + </div> 271 + <button 272 + type="button" 273 + @click="removePour(index)" 274 + class="text-brown-700 hover:text-brown-900 mt-5 font-bold" 275 + x-show="pours.length > 0"> 276 + 277 + </button> 278 + </div> 279 + </template> 280 + </div> 281 + </div> 282 + <div> 283 + <label class="block text-sm font-medium text-brown-900 mb-2"> 284 + Temperature 285 + </label> 286 + <input 287 + type="number" 288 + name="temperature" 289 + step="0.1" 290 + 291 + placeholder="e.g. 93.5" 292 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 293 + </div> 294 + <div> 295 + <label class="block text-sm font-medium text-brown-900 mb-2"> 296 + Brew Time (seconds) 297 + </label> 298 + <input 299 + type="number" 300 + name="time_seconds" 301 + 302 + placeholder="e.g. 180" 303 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 304 + </div> 305 + <div> 306 + <label class="block text-sm font-medium text-brown-900 mb-2"> 307 + Tasting Notes 308 + </label> 309 + <textarea 310 + name="tasting_notes" 311 + rows="4" 312 + placeholder="Describe the flavors, aroma, and your thoughts..." 313 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"></textarea> 314 + </div> 315 + <div> 316 + <label class="block text-sm font-medium text-brown-900 mb-2"> 317 + Rating 318 + </label> 319 + <input 320 + type="range" 321 + name="rating" 322 + min="1" 323 + max="10" 324 + value="5" 325 + x-model="rating" 326 + x-init="rating = $el.value" 327 + class="w-full accent-brown-700"/> 328 + <div class="text-center text-2xl font-bold text-brown-800"> 329 + <span x-text="rating"></span> 330 + /10 331 + </div> 332 + </div> 333 + <div> 334 + <button 335 + type="submit" 336 + class="w-full bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-xl hover:from-brown-800 hover:to-brown-900 transition-all font-semibold text-lg shadow-lg hover:shadow-xl"> 337 + Save Brew 338 + </button> 339 + </div> 340 + </form> 341 + </div> 342 + </div>
+384
internal/bff/__snapshots__/new_brew_with_populated_selects.snap
··· 1 + --- 2 + title: new brew with populated selects 3 + test_name: TestBrewForm_NewBrew_Snapshot/new_brew_with_populated_selects 4 + file_name: form_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <script src="/static/js/brew-form.js"></script> 8 + <div class="max-w-2xl mx-auto"> 9 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300"> 10 + <h2 class="text-3xl font-bold text-brown-900 mb-6"> 11 + New Brew 12 + </h2> 13 + <form 14 + 15 + hx-post="/brews" 16 + 17 + hx-target="body" 18 + class="space-y-6" 19 + x-data="brewForm()" 20 + > 21 + <div> 22 + <label class="block text-sm font-medium text-brown-900 mb-2"> 23 + Coffee Bean 24 + </label> 25 + <div class="flex gap-2"> 26 + <select 27 + name="bean_rkey" 28 + required 29 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 30 + <option value=""> 31 + Select a bean... 32 + </option> 33 + <option 34 + value="bean1" 35 + 36 + class="truncate"> 37 + Ethiopian Yirgacheffe (Ethiopia - Light) 38 + </option> 39 + <option 40 + value="bean2" 41 + 42 + class="truncate"> 43 + Colombia - Medium 44 + </option> 45 + </select> 46 + <button 47 + type="button" 48 + @click="showNewBean = true" 49 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 50 + + New 51 + </button> 52 + </div> 53 + <div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 54 + <h4 class="font-medium mb-3 text-gray-800"> 55 + Add New Bean 56 + </h4> 57 + <div class="space-y-3"> 58 + <input type="text" x-model="newBean.name" placeholder="Name (e.g. Morning Blend, House Espresso) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 59 + <input type="text" x-model="newBean.origin" placeholder="Origin (e.g. Ethiopia) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 60 + <select x-model="newBean.roasterRKey" name="roaster_rkey_modal" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 61 + <option value=""> 62 + Select Roaster (Optional) 63 + </option> 64 + <option value="roaster1"> 65 + Blue Bottle 66 + </option> 67 + <option value="roaster2"> 68 + Counter Culture 69 + </option> 70 + </select> 71 + <select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 72 + <option value=""> 73 + Select Roast Level (Optional) 74 + </option> 75 + <option value="Ultra-Light"> 76 + Ultra-Light 77 + </option> 78 + <option value="Light"> 79 + Light 80 + </option> 81 + <option value="Medium-Light"> 82 + Medium-Light 83 + </option> 84 + <option value="Medium"> 85 + Medium 86 + </option> 87 + <option value="Medium-Dark"> 88 + Medium-Dark 89 + </option> 90 + <option value="Dark"> 91 + Dark 92 + </option> 93 + </select> 94 + <input type="text" x-model="newBean.process" placeholder="Process (e.g. Washed, Natural, Honey)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 95 + <input type="text" x-model="newBean.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 96 + <div class="flex gap-2"> 97 + <button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 98 + Add 99 + </button> 100 + <button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 101 + Cancel 102 + </button> 103 + </div> 104 + </div> 105 + </div> 106 + </div> 107 + <div> 108 + <label class="block text-sm font-medium text-brown-900 mb-2"> 109 + Coffee Amount (grams) 110 + </label> 111 + <input 112 + type="number" 113 + name="coffee_amount" 114 + step="0.1" 115 + 116 + placeholder="e.g. 18" 117 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 118 + <p class="text-sm text-brown-700 mt-1"> 119 + Amount of ground coffee used 120 + </p> 121 + </div> 122 + <div> 123 + <label class="block text-sm font-medium text-brown-900 mb-2"> 124 + Grinder 125 + </label> 126 + <div class="flex gap-2"> 127 + <select 128 + name="grinder_rkey" 129 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 130 + <option value=""> 131 + Select a grinder... 132 + </option> 133 + <option 134 + value="grinder1" 135 + 136 + class="truncate"> 137 + Baratza Encore 138 + </option> 139 + <option 140 + value="grinder2" 141 + 142 + class="truncate"> 143 + Comandante C40 144 + </option> 145 + </select> 146 + <button 147 + type="button" 148 + @click="showNewGrinder = true" 149 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 150 + + New 151 + </button> 152 + </div> 153 + <div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 154 + <h4 class="font-medium mb-3 text-gray-800"> 155 + Add New Grinder 156 + </h4> 157 + <div class="space-y-3"> 158 + <input type="text" x-model="newGrinder.name" placeholder="Name (e.g. Baratza Encore) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 159 + <select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 160 + <option value=""> 161 + Grinder Type (Optional) 162 + </option> 163 + <option value="Hand"> 164 + Hand 165 + </option> 166 + <option value="Electric"> 167 + Electric 168 + </option> 169 + <option value="Electric Hand"> 170 + Electric Hand 171 + </option> 172 + </select> 173 + <select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"> 174 + <option value=""> 175 + Burr Type (Optional) 176 + </option> 177 + <option value="Conical"> 178 + Conical 179 + </option> 180 + <option value="Flat"> 181 + Flat 182 + </option> 183 + </select> 184 + <input type="text" x-model="newGrinder.notes" placeholder="Notes (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 185 + <div class="flex gap-2"> 186 + <button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 187 + Add 188 + </button> 189 + <button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 190 + Cancel 191 + </button> 192 + </div> 193 + </div> 194 + </div> 195 + </div> 196 + <div> 197 + <label class="block text-sm font-medium text-brown-900 mb-2"> 198 + Grind Size 199 + </label> 200 + <input 201 + type="text" 202 + name="grind_size" 203 + 204 + placeholder="e.g. 18, Medium, 3.5, Fine" 205 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 206 + <p class="text-sm text-brown-700 mt-1"> 207 + Enter a number (grinder setting) or description (e.g. "Medium", "Fine") 208 + </p> 209 + </div> 210 + <div> 211 + <label class="block text-sm font-medium text-brown-900 mb-2"> 212 + Brew Method 213 + </label> 214 + <div class="flex gap-2"> 215 + <select 216 + name="brewer_rkey" 217 + class="flex-1 rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 truncate max-w-full bg-white"> 218 + <option value=""> 219 + Select brew method... 220 + </option> 221 + <option 222 + value="brewer1" 223 + 224 + class="truncate"> 225 + Hario V60 226 + </option> 227 + <option 228 + value="brewer2" 229 + 230 + class="truncate"> 231 + AeroPress 232 + </option> 233 + </select> 234 + <button 235 + type="button" 236 + @click="showNewBrewer = true" 237 + class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 238 + + New 239 + </button> 240 + </div> 241 + <div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300"> 242 + <h4 class="font-medium mb-3 text-gray-800"> 243 + Add New Brewer 244 + </h4> 245 + <div class="space-y-3"> 246 + <input type="text" x-model="newBrewer.name" placeholder="Name (e.g. V60, AeroPress) *" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 247 + <input type="text" x-model="newBrewer.brewer_type" placeholder="Type (e.g. Pour-Over, Immersion)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 248 + <input type="text" x-model="newBrewer.description" placeholder="Description (optional)" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3"/> 249 + <div class="flex gap-2"> 250 + <button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700"> 251 + Add 252 + </button> 253 + <button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"> 254 + Cancel 255 + </button> 256 + </div> 257 + </div> 258 + </div> 259 + </div> 260 + <div> 261 + <label class="block text-sm font-medium text-brown-900 mb-2"> 262 + Water Amount (grams) 263 + </label> 264 + <input 265 + type="number" 266 + name="water_amount" 267 + step="1" 268 + 269 + placeholder="e.g. 250" 270 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 271 + <p class="text-sm text-brown-700 mt-1"> 272 + Total water used (or leave empty if using pours below) 273 + </p> 274 + </div> 275 + <div> 276 + <div class="flex items-center justify-between mb-2"> 277 + <label class="block text-sm font-medium text-brown-900"> 278 + Pours (Optional) 279 + </label> 280 + <button 281 + type="button" 282 + @click="addPour()" 283 + class="text-sm bg-brown-300 text-brown-900 px-3 py-1 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 284 + + Add Pour 285 + </button> 286 + </div> 287 + <p class="text-sm text-brown-700 mb-3"> 288 + Track individual pours for bloom and subsequent additions 289 + </p> 290 + <div class="space-y-3"> 291 + <template x-for="(pour, index) in pours" :key="index"> 292 + <div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200"> 293 + <div class="flex-1"> 294 + <label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label> 295 + <input 296 + type="number" 297 + :name="'pour_water_' + index" 298 + x-model="pour.water" 299 + placeholder="Water (g)" 300 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 301 + </div> 302 + <div class="flex-1"> 303 + <label class="text-xs text-brown-700 font-medium"> 304 + Time (sec) 305 + </label> 306 + <input 307 + type="number" 308 + :name="'pour_time_' + index" 309 + x-model="pour.time" 310 + placeholder="e.g. 45" 311 + class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/> 312 + </div> 313 + <button 314 + type="button" 315 + @click="removePour(index)" 316 + class="text-brown-700 hover:text-brown-900 mt-5 font-bold" 317 + x-show="pours.length > 0"> 318 + 319 + </button> 320 + </div> 321 + </template> 322 + </div> 323 + </div> 324 + <div> 325 + <label class="block text-sm font-medium text-brown-900 mb-2"> 326 + Temperature 327 + </label> 328 + <input 329 + type="number" 330 + name="temperature" 331 + step="0.1" 332 + 333 + placeholder="e.g. 93.5" 334 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 335 + </div> 336 + <div> 337 + <label class="block text-sm font-medium text-brown-900 mb-2"> 338 + Brew Time (seconds) 339 + </label> 340 + <input 341 + type="number" 342 + name="time_seconds" 343 + 344 + placeholder="e.g. 180" 345 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"/> 346 + </div> 347 + <div> 348 + <label class="block text-sm font-medium text-brown-900 mb-2"> 349 + Tasting Notes 350 + </label> 351 + <textarea 352 + name="tasting_notes" 353 + rows="4" 354 + placeholder="Describe the flavors, aroma, and your thoughts..." 355 + class="w-full rounded-lg border-2 border-brown-300 shadow-sm focus:border-brown-600 focus:ring-brown-600 text-base py-3 px-4 bg-white"></textarea> 356 + </div> 357 + <div> 358 + <label class="block text-sm font-medium text-brown-900 mb-2"> 359 + Rating 360 + </label> 361 + <input 362 + type="range" 363 + name="rating" 364 + min="1" 365 + max="10" 366 + value="5" 367 + x-model="rating" 368 + x-init="rating = $el.value" 369 + class="w-full accent-brown-700"/> 370 + <div class="text-center text-2xl font-bold text-brown-800"> 371 + <span x-text="rating"></span> 372 + /10 373 + </div> 374 + </div> 375 + <div> 376 + <button 377 + type="submit" 378 + class="w-full bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-xl hover:from-brown-800 hover:to-brown-900 transition-all font-semibold text-lg shadow-lg hover:shadow-xl"> 379 + Save Brew 380 + </button> 381 + </div> 382 + </form> 383 + </div> 384 + </div>
+10 -1
internal/bff/__snapshots__/nil_feed.snap
··· 4 4 file_name: feed_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n<div class=\"space-y-4\">\n \n <div class=\"bg-brown-100 rounded-lg p-6 text-center text-brown-700 border border-brown-200\">\n <p class=\"mb-2 font-medium\">No activity in the feed yet.</p>\n <p class=\"text-sm\">Be the first to add something!</p>\n </div>\n \n</div>\n" 7 + <div class="space-y-4"> 8 + <div class="bg-brown-100 rounded-lg p-6 text-center text-brown-700 border border-brown-200"> 9 + <p class="mb-2 font-medium"> 10 + No activity in the feed yet. 11 + </p> 12 + <p class="text-sm"> 13 + Be the first to add something! 14 + </p> 15 + </div> 16 + </div>
+62 -1
internal/bff/__snapshots__/profile_roaster_with_invalid_url_protocol.snap
··· 4 4 file_name: profile_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n\n<div id=\"profile-stats-data\" \n data-brews=\"0\" \n data-beans=\"0\" \n data-roasters=\"1\" \n data-grinders=\"0\" \n data-brewers=\"0\"\n style=\"display: none;\"></div>\n\n\n<div x-show=\"activeTab === 'brews'\">\n \n\n<div class=\"bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300\">\n \n <p class=\"text-brown-800 text-lg font-medium\">No brews yet.</p>\n \n</div>\n\n\n</div>\n\n\n<div x-show=\"activeTab === 'beans'\" x-cloak class=\"space-y-6\">\n \n \n\n \n \n <div>\n <h3 class=\"text-lg font-semibold text-brown-900 mb-3\">🏪 Favorite Roasters</h3>\n <div class=\"overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300\">\n <table class=\"min-w-full divide-y divide-brown-300\">\n <thead class=\"bg-brown-200/80\">\n <tr>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">Name</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">📍 Location</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">🌐 Website</th>\n </tr>\n </thead>\n <tbody class=\"bg-brown-50/60 divide-y divide-brown-200\">\n \n <tr class=\"hover:bg-brown-100/60 transition-colors\">\n <td class=\"px-6 py-4 text-sm font-bold text-brown-900\">FTP Roaster</td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n <span class=\"text-brown-400\">-</span>\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n \n \n \n <span class=\"text-brown-400\">-</span>\n \n \n </td>\n </tr>\n \n </tbody>\n </table>\n </div>\n \n </div>\n \n\n \n</div>\n\n\n<div x-show=\"activeTab === 'gear'\" x-cloak class=\"space-y-6\">\n \n \n\n \n \n\n \n <div class=\"bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center text-brown-800 border border-brown-300\">\n <p class=\"font-medium\">No gear added yet.</p>\n </div>\n \n</div>\n" 7 + <div id="profile-stats-data" 8 + data-brews="0" 9 + data-beans="0" 10 + data-roasters="1" 11 + data-grinders="0" 12 + data-brewers="0" 13 + style="display: none;"></div> 14 + <div x-show="activeTab === 'brews'"> 15 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300"> 16 + <p class="text-brown-800 text-lg font-medium"> 17 + No brews yet. 18 + </p> 19 + </div> 20 + </div> 21 + <div x-show="activeTab === 'beans'" x-cloak class="space-y-6"> 22 + <div> 23 + <h3 class="text-lg font-semibold text-brown-900 mb-3"> 24 + 🏪 Favorite Roasters 25 + </h3> 26 + <div class="overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300"> 27 + <table class="min-w-full divide-y divide-brown-300"> 28 + <thead class="bg-brown-200/80"> 29 + <tr> 30 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 31 + Name 32 + </th> 33 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 34 + 📍 Location 35 + </th> 36 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 37 + 🌐 Website 38 + </th> 39 + </tr> 40 + </thead> 41 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 42 + <tr class="hover:bg-brown-100/60 transition-colors"> 43 + <td class="px-6 py-4 text-sm font-bold text-brown-900"> 44 + FTP Roaster 45 + </td> 46 + <td class="px-6 py-4 text-sm text-brown-900"> 47 + <span class="text-brown-400"> 48 + - 49 + </span> 50 + </td> 51 + <td class="px-6 py-4 text-sm text-brown-900"> 52 + <span class="text-brown-400"> 53 + - 54 + </span> 55 + </td> 56 + </tr> 57 + </tbody> 58 + </table> 59 + </div> 60 + </div> 61 + </div> 62 + <div x-show="activeTab === 'gear'" x-cloak class="space-y-6"> 63 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center text-brown-800 border border-brown-300"> 64 + <p class="font-medium"> 65 + No gear added yet. 66 + </p> 67 + </div> 68 + </div>
+60 -1
internal/bff/__snapshots__/profile_roaster_with_unsafe_website_url.snap
··· 4 4 file_name: profile_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n\n<div id=\"profile-stats-data\" \n data-brews=\"0\" \n data-beans=\"0\" \n data-roasters=\"1\" \n data-grinders=\"0\" \n data-brewers=\"0\"\n style=\"display: none;\"></div>\n\n\n<div x-show=\"activeTab === 'brews'\">\n \n\n<div class=\"bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300\">\n \n <p class=\"text-brown-800 text-lg font-medium\">No brews yet.</p>\n \n</div>\n\n\n</div>\n\n\n<div x-show=\"activeTab === 'beans'\" x-cloak class=\"space-y-6\">\n \n \n\n \n \n <div>\n <h3 class=\"text-lg font-semibold text-brown-900 mb-3\">🏪 Favorite Roasters</h3>\n <div class=\"overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300\">\n <table class=\"min-w-full divide-y divide-brown-300\">\n <thead class=\"bg-brown-200/80\">\n <tr>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">Name</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">📍 Location</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">🌐 Website</th>\n </tr>\n </thead>\n <tbody class=\"bg-brown-50/60 divide-y divide-brown-200\">\n \n <tr class=\"hover:bg-brown-100/60 transition-colors\">\n <td class=\"px-6 py-4 text-sm font-bold text-brown-900\">Sketchy Roaster</td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Unknown\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n \n \n \n <span class=\"text-brown-400\">-</span>\n \n \n </td>\n </tr>\n \n </tbody>\n </table>\n </div>\n \n </div>\n \n\n \n</div>\n\n\n<div x-show=\"activeTab === 'gear'\" x-cloak class=\"space-y-6\">\n \n \n\n \n \n\n \n <div class=\"bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center text-brown-800 border border-brown-300\">\n <p class=\"font-medium\">No gear added yet.</p>\n </div>\n \n</div>\n" 7 + <div id="profile-stats-data" 8 + data-brews="0" 9 + data-beans="0" 10 + data-roasters="1" 11 + data-grinders="0" 12 + data-brewers="0" 13 + style="display: none;"></div> 14 + <div x-show="activeTab === 'brews'"> 15 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300"> 16 + <p class="text-brown-800 text-lg font-medium"> 17 + No brews yet. 18 + </p> 19 + </div> 20 + </div> 21 + <div x-show="activeTab === 'beans'" x-cloak class="space-y-6"> 22 + <div> 23 + <h3 class="text-lg font-semibold text-brown-900 mb-3"> 24 + 🏪 Favorite Roasters 25 + </h3> 26 + <div class="overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300"> 27 + <table class="min-w-full divide-y divide-brown-300"> 28 + <thead class="bg-brown-200/80"> 29 + <tr> 30 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 31 + Name 32 + </th> 33 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 34 + 📍 Location 35 + </th> 36 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 37 + 🌐 Website 38 + </th> 39 + </tr> 40 + </thead> 41 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 42 + <tr class="hover:bg-brown-100/60 transition-colors"> 43 + <td class="px-6 py-4 text-sm font-bold text-brown-900"> 44 + Sketchy Roaster 45 + </td> 46 + <td class="px-6 py-4 text-sm text-brown-900"> 47 + Unknown 48 + </td> 49 + <td class="px-6 py-4 text-sm text-brown-900"> 50 + <span class="text-brown-400"> 51 + - 52 + </span> 53 + </td> 54 + </tr> 55 + </tbody> 56 + </table> 57 + </div> 58 + </div> 59 + </div> 60 + <div x-show="activeTab === 'gear'" x-cloak class="space-y-6"> 61 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center text-brown-800 border border-brown-300"> 62 + <p class="font-medium"> 63 + No gear added yet. 64 + </p> 65 + </div> 66 + </div>
+28 -1
internal/bff/__snapshots__/profile_with_empty_beans.snap
··· 4 4 file_name: profile_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n\n<div id=\"profile-stats-data\" \n data-brews=\"0\" \n data-beans=\"0\" \n data-roasters=\"0\" \n data-grinders=\"0\" \n data-brewers=\"0\"\n style=\"display: none;\"></div>\n\n\n<div x-show=\"activeTab === 'brews'\">\n \n\n<div class=\"bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300\">\n \n <p class=\"text-brown-800 text-lg font-medium\">No brews yet.</p>\n \n</div>\n\n\n</div>\n\n\n<div x-show=\"activeTab === 'beans'\" x-cloak class=\"space-y-6\">\n \n \n\n \n \n\n \n <div class=\"bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center text-brown-800 border border-brown-300\">\n <p class=\"font-medium\">No beans or roasters yet.</p>\n </div>\n \n</div>\n\n\n<div x-show=\"activeTab === 'gear'\" x-cloak class=\"space-y-6\">\n \n \n\n \n \n\n \n <div class=\"bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center text-brown-800 border border-brown-300\">\n <p class=\"font-medium\">No gear added yet.</p>\n </div>\n \n</div>\n" 7 + <div id="profile-stats-data" 8 + data-brews="0" 9 + data-beans="0" 10 + data-roasters="0" 11 + data-grinders="0" 12 + data-brewers="0" 13 + style="display: none;"></div> 14 + <div x-show="activeTab === 'brews'"> 15 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300"> 16 + <p class="text-brown-800 text-lg font-medium"> 17 + No brews yet. 18 + </p> 19 + </div> 20 + </div> 21 + <div x-show="activeTab === 'beans'" x-cloak class="space-y-6"> 22 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center text-brown-800 border border-brown-300"> 23 + <p class="font-medium"> 24 + No beans or roasters yet. 25 + </p> 26 + </div> 27 + </div> 28 + <div x-show="activeTab === 'gear'" x-cloak class="space-y-6"> 29 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center text-brown-800 border border-brown-300"> 30 + <p class="font-medium"> 31 + No gear added yet. 32 + </p> 33 + </div> 34 + </div>
+181 -1
internal/bff/__snapshots__/profile_with_gear_collection.snap
··· 4 4 file_name: profile_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n\n<div id=\"profile-stats-data\" \n data-brews=\"0\" \n data-beans=\"0\" \n data-roasters=\"1\" \n data-grinders=\"2\" \n data-brewers=\"1\"\n style=\"display: none;\"></div>\n\n\n<div x-show=\"activeTab === 'brews'\">\n \n\n<div class=\"bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300\">\n \n <p class=\"text-brown-800 text-lg mb-4 font-medium\">No brews yet! Start tracking your coffee journey.</p>\n <a href=\"/brews/new\"\n class=\"inline-block bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all shadow-lg hover:shadow-xl font-medium\">\n Add Your First Brew\n </a>\n \n</div>\n\n\n</div>\n\n\n<div x-show=\"activeTab === 'beans'\" x-cloak class=\"space-y-6\">\n \n \n\n \n \n <div>\n <h3 class=\"text-lg font-semibold text-brown-900 mb-3\">🏪 Favorite Roasters</h3>\n <div class=\"overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300\">\n <table class=\"min-w-full divide-y divide-brown-300\">\n <thead class=\"bg-brown-200/80\">\n <tr>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">Name</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">📍 Location</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">🌐 Website</th>\n </tr>\n </thead>\n <tbody class=\"bg-brown-50/60 divide-y divide-brown-200\">\n \n <tr class=\"hover:bg-brown-100/60 transition-colors\">\n <td class=\"px-6 py-4 text-sm font-bold text-brown-900\">Heart Coffee</td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Portland, OR\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n \n \n \n <a href=\"https://heartroasters.com\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-brown-700 hover:underline font-medium\">Visit Site</a>\n \n \n </td>\n </tr>\n \n </tbody>\n </table>\n </div>\n \n <div class=\"mt-3 text-center\">\n <button @click=\"editRoaster('', '', '', '')\" class=\"inline-flex items-center gap-2 bg-brown-600 text-white px-4 py-2 rounded-lg hover:bg-brown-700 transition-all shadow-md hover:shadow-lg text-sm font-medium\">\n <span>+</span>\n <span>Add New Roaster</span>\n </button>\n </div>\n \n </div>\n \n\n \n</div>\n\n\n<div x-show=\"activeTab === 'gear'\" x-cloak class=\"space-y-6\">\n \n \n <div>\n <h3 class=\"text-lg font-semibold text-brown-900 mb-3\">⚙️ Grinders</h3>\n <div class=\"overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300\">\n <table class=\"min-w-full divide-y divide-brown-300\">\n <thead class=\"bg-brown-200/80\">\n <tr>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">Name</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">🔧 Type</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">💎 Burrs</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">📝 Notes</th>\n </tr>\n </thead>\n <tbody class=\"bg-brown-50/60 divide-y divide-brown-200\">\n \n <tr class=\"hover:bg-brown-100/60 transition-colors\">\n <td class=\"px-6 py-4 text-sm font-bold text-brown-900\">Comandante C40</td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Hand\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Conical\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-700 italic max-w-xs\">\n Perfect for pour over\n </td>\n </tr>\n \n <tr class=\"hover:bg-brown-100/60 transition-colors\">\n <td class=\"px-6 py-4 text-sm font-bold text-brown-900\">Niche Zero</td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Electric\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Conical\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-700 italic max-w-xs\">\n <span class=\"text-brown-400 not-italic\">-</span>\n </td>\n </tr>\n \n </tbody>\n </table>\n </div>\n \n <div class=\"mt-3 text-center\">\n <button @click=\"editGrinder('', '', '', '', '')\" class=\"inline-flex items-center gap-2 bg-brown-600 text-white px-4 py-2 rounded-lg hover:bg-brown-700 transition-all shadow-md hover:shadow-lg text-sm font-medium\">\n <span>+</span>\n <span>Add New Grinder</span>\n </button>\n </div>\n \n </div>\n \n\n \n \n <div>\n <h3 class=\"text-lg font-semibold text-brown-900 mb-3\">☕ Brewers</h3>\n <div class=\"overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300\">\n <table class=\"min-w-full divide-y divide-brown-300\">\n <thead class=\"bg-brown-200/80\">\n <tr>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">Name</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">🔧 Type</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">📝 Description</th>\n </tr>\n </thead>\n <tbody class=\"bg-brown-50/60 divide-y divide-brown-200\">\n \n <tr class=\"hover:bg-brown-100/60 transition-colors\">\n <td class=\"px-6 py-4 text-sm font-bold text-brown-900\">Hario V60</td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Pour Over\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-700 italic max-w-xs\">\n Classic pour over cone\n </td>\n </tr>\n \n </tbody>\n </table>\n </div>\n \n <div class=\"mt-3 text-center\">\n <button @click=\"editBrewer('', '', '', '')\" class=\"inline-flex items-center gap-2 bg-brown-600 text-white px-4 py-2 rounded-lg hover:bg-brown-700 transition-all shadow-md hover:shadow-lg text-sm font-medium\">\n <span>+</span>\n <span>Add New Brewer</span>\n </button>\n </div>\n \n </div>\n \n\n \n</div>\n" 7 + <div id="profile-stats-data" 8 + data-brews="0" 9 + data-beans="0" 10 + data-roasters="1" 11 + data-grinders="2" 12 + data-brewers="1" 13 + style="display: none;"></div> 14 + <div x-show="activeTab === 'brews'"> 15 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300"> 16 + <p class="text-brown-800 text-lg mb-4 font-medium"> 17 + No brews yet! Start tracking your coffee journey. 18 + </p> 19 + <a href="/brews/new" 20 + class="inline-block bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all shadow-lg hover:shadow-xl font-medium"> 21 + Add Your First Brew 22 + </a> 23 + </div> 24 + </div> 25 + <div x-show="activeTab === 'beans'" x-cloak class="space-y-6"> 26 + <div> 27 + <h3 class="text-lg font-semibold text-brown-900 mb-3"> 28 + 🏪 Favorite Roasters 29 + </h3> 30 + <div class="overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300"> 31 + <table class="min-w-full divide-y divide-brown-300"> 32 + <thead class="bg-brown-200/80"> 33 + <tr> 34 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 35 + Name 36 + </th> 37 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 38 + 📍 Location 39 + </th> 40 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 41 + 🌐 Website 42 + </th> 43 + </tr> 44 + </thead> 45 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 46 + <tr class="hover:bg-brown-100/60 transition-colors"> 47 + <td class="px-6 py-4 text-sm font-bold text-brown-900"> 48 + Heart Coffee 49 + </td> 50 + <td class="px-6 py-4 text-sm text-brown-900"> 51 + Portland, OR 52 + </td> 53 + <td class="px-6 py-4 text-sm text-brown-900"> 54 + <a href="https://heartroasters.com" target="_blank" rel="noopener noreferrer" class="text-brown-700 hover:underline font-medium"> 55 + Visit Site 56 + </a> 57 + </td> 58 + </tr> 59 + </tbody> 60 + </table> 61 + </div> 62 + <div class="mt-3 text-center"> 63 + <button @click="editRoaster('', '', '', '')" class="inline-flex items-center gap-2 bg-brown-600 text-white px-4 py-2 rounded-lg hover:bg-brown-700 transition-all shadow-md hover:shadow-lg text-sm font-medium"> 64 + <span> 65 + + 66 + </span> 67 + <span> 68 + Add New Roaster 69 + </span> 70 + </button> 71 + </div> 72 + </div> 73 + </div> 74 + <div x-show="activeTab === 'gear'" x-cloak class="space-y-6"> 75 + <div> 76 + <h3 class="text-lg font-semibold text-brown-900 mb-3"> 77 + ⚙️ Grinders 78 + </h3> 79 + <div class="overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300"> 80 + <table class="min-w-full divide-y divide-brown-300"> 81 + <thead class="bg-brown-200/80"> 82 + <tr> 83 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 84 + Name 85 + </th> 86 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 87 + 🔧 Type 88 + </th> 89 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 90 + 💎 Burrs 91 + </th> 92 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 93 + 📝 Notes 94 + </th> 95 + </tr> 96 + </thead> 97 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 98 + <tr class="hover:bg-brown-100/60 transition-colors"> 99 + <td class="px-6 py-4 text-sm font-bold text-brown-900"> 100 + Comandante C40 101 + </td> 102 + <td class="px-6 py-4 text-sm text-brown-900"> 103 + Hand 104 + </td> 105 + <td class="px-6 py-4 text-sm text-brown-900"> 106 + Conical 107 + </td> 108 + <td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs"> 109 + Perfect for pour over 110 + </td> 111 + </tr> 112 + <tr class="hover:bg-brown-100/60 transition-colors"> 113 + <td class="px-6 py-4 text-sm font-bold text-brown-900"> 114 + Niche Zero 115 + </td> 116 + <td class="px-6 py-4 text-sm text-brown-900"> 117 + Electric 118 + </td> 119 + <td class="px-6 py-4 text-sm text-brown-900"> 120 + Conical 121 + </td> 122 + <td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs"> 123 + <span class="text-brown-400 not-italic"> 124 + - 125 + </span> 126 + </td> 127 + </tr> 128 + </tbody> 129 + </table> 130 + </div> 131 + <div class="mt-3 text-center"> 132 + <button @click="editGrinder('', '', '', '', '')" class="inline-flex items-center gap-2 bg-brown-600 text-white px-4 py-2 rounded-lg hover:bg-brown-700 transition-all shadow-md hover:shadow-lg text-sm font-medium"> 133 + <span> 134 + + 135 + </span> 136 + <span> 137 + Add New Grinder 138 + </span> 139 + </button> 140 + </div> 141 + </div> 142 + <div> 143 + <h3 class="text-lg font-semibold text-brown-900 mb-3"> 144 + ☕ Brewers 145 + </h3> 146 + <div class="overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300"> 147 + <table class="min-w-full divide-y divide-brown-300"> 148 + <thead class="bg-brown-200/80"> 149 + <tr> 150 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 151 + Name 152 + </th> 153 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 154 + 🔧 Type 155 + </th> 156 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 157 + 📝 Description 158 + </th> 159 + </tr> 160 + </thead> 161 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 162 + <tr class="hover:bg-brown-100/60 transition-colors"> 163 + <td class="px-6 py-4 text-sm font-bold text-brown-900"> 164 + Hario V60 165 + </td> 166 + <td class="px-6 py-4 text-sm text-brown-900"> 167 + Pour Over 168 + </td> 169 + <td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs"> 170 + Classic pour over cone 171 + </td> 172 + </tr> 173 + </tbody> 174 + </table> 175 + </div> 176 + <div class="mt-3 text-center"> 177 + <button @click="editBrewer('', '', '', '')" class="inline-flex items-center gap-2 bg-brown-600 text-white px-4 py-2 rounded-lg hover:bg-brown-700 transition-all shadow-md hover:shadow-lg text-sm font-medium"> 178 + <span> 179 + + 180 + </span> 181 + <span> 182 + Add New Brewer 183 + </span> 184 + </button> 185 + </div> 186 + </div> 187 + </div>
+114 -1
internal/bff/__snapshots__/profile_with_multiple_beans.snap
··· 4 4 file_name: profile_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n\n<div id=\"profile-stats-data\" \n data-brews=\"0\" \n data-beans=\"2\" \n data-roasters=\"0\" \n data-grinders=\"0\" \n data-brewers=\"0\"\n style=\"display: none;\"></div>\n\n\n<div x-show=\"activeTab === 'brews'\">\n \n\n<div class=\"bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300\">\n \n <p class=\"text-brown-800 text-lg mb-4 font-medium\">No brews yet! Start tracking your coffee journey.</p>\n <a href=\"/brews/new\"\n class=\"inline-block bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all shadow-lg hover:shadow-xl font-medium\">\n Add Your First Brew\n </a>\n \n</div>\n\n\n</div>\n\n\n<div x-show=\"activeTab === 'beans'\" x-cloak class=\"space-y-6\">\n \n \n <div>\n <h3 class=\"text-lg font-semibold text-brown-900 mb-3\">☕ Coffee Beans</h3>\n <div class=\"overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300\">\n <table class=\"min-w-full divide-y divide-brown-300\">\n <thead class=\"bg-brown-200/80\">\n <tr>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">Name</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">☕ Roaster</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">📍 Origin</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">🔥 Roast</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">🌱 Process</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">📝 Description</th>\n </tr>\n </thead>\n <tbody class=\"bg-brown-50/60 divide-y divide-brown-200\">\n \n <tr class=\"hover:bg-brown-100/60 transition-colors\">\n <td class=\"px-6 py-4 text-sm font-bold text-brown-900\">\n Ethiopian Yirgacheffe\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n \n Onyx Coffee Lab\n \n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Ethiopia\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Light\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Washed\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-700 italic max-w-xs\">\n Bright and floral with citrus notes\n </td>\n </tr>\n \n <tr class=\"hover:bg-brown-100/60 transition-colors\">\n <td class=\"px-6 py-4 text-sm font-bold text-brown-900\">\n Colombia Supremo\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n \n <span class=\"text-brown-400\">-</span>\n \n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Colombia\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Medium\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Natural\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-700 italic max-w-xs\">\n <span class=\"text-brown-400 not-italic\">-</span>\n </td>\n </tr>\n \n </tbody>\n </table>\n </div>\n \n <div class=\"mt-3 text-center\">\n <button @click=\"editBean('', '', '', '', '', '', '')\" class=\"inline-flex items-center gap-2 bg-brown-600 text-white px-4 py-2 rounded-lg hover:bg-brown-700 transition-all shadow-md hover:shadow-lg text-sm font-medium\">\n <span>+</span>\n <span>Add New Bean</span>\n </button>\n </div>\n \n </div>\n \n\n \n \n\n \n</div>\n\n\n<div x-show=\"activeTab === 'gear'\" x-cloak class=\"space-y-6\">\n \n \n\n \n \n\n \n <div class=\"bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center text-brown-800 border border-brown-300\">\n <p class=\"font-medium\">No gear added yet.</p>\n </div>\n \n</div>\n" 7 + <div id="profile-stats-data" 8 + data-brews="0" 9 + data-beans="2" 10 + data-roasters="0" 11 + data-grinders="0" 12 + data-brewers="0" 13 + style="display: none;"></div> 14 + <div x-show="activeTab === 'brews'"> 15 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300"> 16 + <p class="text-brown-800 text-lg mb-4 font-medium"> 17 + No brews yet! Start tracking your coffee journey. 18 + </p> 19 + <a href="/brews/new" 20 + class="inline-block bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all shadow-lg hover:shadow-xl font-medium"> 21 + Add Your First Brew 22 + </a> 23 + </div> 24 + </div> 25 + <div x-show="activeTab === 'beans'" x-cloak class="space-y-6"> 26 + <div> 27 + <h3 class="text-lg font-semibold text-brown-900 mb-3"> 28 + ☕ Coffee Beans 29 + </h3> 30 + <div class="overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300"> 31 + <table class="min-w-full divide-y divide-brown-300"> 32 + <thead class="bg-brown-200/80"> 33 + <tr> 34 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 35 + Name 36 + </th> 37 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 38 + ☕ Roaster 39 + </th> 40 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 41 + 📍 Origin 42 + </th> 43 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 44 + 🔥 Roast 45 + </th> 46 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 47 + 🌱 Process 48 + </th> 49 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 50 + 📝 Description 51 + </th> 52 + </tr> 53 + </thead> 54 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 55 + <tr class="hover:bg-brown-100/60 transition-colors"> 56 + <td class="px-6 py-4 text-sm font-bold text-brown-900"> 57 + Ethiopian Yirgacheffe 58 + </td> 59 + <td class="px-6 py-4 text-sm text-brown-900"> 60 + Onyx Coffee Lab 61 + </td> 62 + <td class="px-6 py-4 text-sm text-brown-900"> 63 + Ethiopia 64 + </td> 65 + <td class="px-6 py-4 text-sm text-brown-900"> 66 + Light 67 + </td> 68 + <td class="px-6 py-4 text-sm text-brown-900"> 69 + Washed 70 + </td> 71 + <td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs"> 72 + Bright and floral with citrus notes 73 + </td> 74 + </tr> 75 + <tr class="hover:bg-brown-100/60 transition-colors"> 76 + <td class="px-6 py-4 text-sm font-bold text-brown-900"> 77 + Colombia Supremo 78 + </td> 79 + <td class="px-6 py-4 text-sm text-brown-900"> 80 + <span class="text-brown-400"> 81 + - 82 + </span> 83 + </td> 84 + <td class="px-6 py-4 text-sm text-brown-900"> 85 + Colombia 86 + </td> 87 + <td class="px-6 py-4 text-sm text-brown-900"> 88 + Medium 89 + </td> 90 + <td class="px-6 py-4 text-sm text-brown-900"> 91 + Natural 92 + </td> 93 + <td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs"> 94 + <span class="text-brown-400 not-italic"> 95 + - 96 + </span> 97 + </td> 98 + </tr> 99 + </tbody> 100 + </table> 101 + </div> 102 + <div class="mt-3 text-center"> 103 + <button @click="editBean('', '', '', '', '', '', '')" class="inline-flex items-center gap-2 bg-brown-600 text-white px-4 py-2 rounded-lg hover:bg-brown-700 transition-all shadow-md hover:shadow-lg text-sm font-medium"> 104 + <span> 105 + + 106 + </span> 107 + <span> 108 + Add New Bean 109 + </span> 110 + </button> 111 + </div> 112 + </div> 113 + </div> 114 + <div x-show="activeTab === 'gear'" x-cloak class="space-y-6"> 115 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center text-brown-800 border border-brown-300"> 116 + <p class="font-medium"> 117 + No gear added yet. 118 + </p> 119 + </div> 120 + </div>
+146 -1
internal/bff/__snapshots__/profile_with_special_characters.snap
··· 4 4 file_name: profile_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n\n<div id=\"profile-stats-data\" \n data-brews=\"0\" \n data-beans=\"1\" \n data-roasters=\"0\" \n data-grinders=\"1\" \n data-brewers=\"0\"\n style=\"display: none;\"></div>\n\n\n<div x-show=\"activeTab === 'brews'\">\n \n\n<div class=\"bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300\">\n \n <p class=\"text-brown-800 text-lg mb-4 font-medium\">No brews yet! Start tracking your coffee journey.</p>\n <a href=\"/brews/new\"\n class=\"inline-block bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all shadow-lg hover:shadow-xl font-medium\">\n Add Your First Brew\n </a>\n \n</div>\n\n\n</div>\n\n\n<div x-show=\"activeTab === 'beans'\" x-cloak class=\"space-y-6\">\n \n \n <div>\n <h3 class=\"text-lg font-semibold text-brown-900 mb-3\">☕ Coffee Beans</h3>\n <div class=\"overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300\">\n <table class=\"min-w-full divide-y divide-brown-300\">\n <thead class=\"bg-brown-200/80\">\n <tr>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">Name</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">☕ Roaster</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">📍 Origin</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">🔥 Roast</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">🌱 Process</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">📝 Description</th>\n </tr>\n </thead>\n <tbody class=\"bg-brown-50/60 divide-y divide-brown-200\">\n \n <tr class=\"hover:bg-brown-100/60 transition-colors\">\n <td class=\"px-6 py-4 text-sm font-bold text-brown-900\">\n Bean with &lt;html&gt; &amp; &#34;quotes&#34;\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n \n <span class=\"text-brown-400\">-</span>\n \n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Colombia &amp; Peru\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n <span class=\"text-brown-400\">-</span>\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n <span class=\"text-brown-400\">-</span>\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-700 italic max-w-xs\">\n Description with &#39;single&#39; and &#34;double&#34; quotes\n </td>\n </tr>\n \n </tbody>\n </table>\n </div>\n \n <div class=\"mt-3 text-center\">\n <button @click=\"editBean('', '', '', '', '', '', '')\" class=\"inline-flex items-center gap-2 bg-brown-600 text-white px-4 py-2 rounded-lg hover:bg-brown-700 transition-all shadow-md hover:shadow-lg text-sm font-medium\">\n <span>+</span>\n <span>Add New Bean</span>\n </button>\n </div>\n \n </div>\n \n\n \n \n\n \n</div>\n\n\n<div x-show=\"activeTab === 'gear'\" x-cloak class=\"space-y-6\">\n \n \n <div>\n <h3 class=\"text-lg font-semibold text-brown-900 mb-3\">⚙️ Grinders</h3>\n <div class=\"overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300\">\n <table class=\"min-w-full divide-y divide-brown-300\">\n <thead class=\"bg-brown-200/80\">\n <tr>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">Name</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">🔧 Type</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">💎 Burrs</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">📝 Notes</th>\n </tr>\n </thead>\n <tbody class=\"bg-brown-50/60 divide-y divide-brown-200\">\n \n <tr class=\"hover:bg-brown-100/60 transition-colors\">\n <td class=\"px-6 py-4 text-sm font-bold text-brown-900\">Grinder &amp; Co.</td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n <span class=\"text-brown-400\">-</span>\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n <span class=\"text-brown-400\">-</span>\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-700 italic max-w-xs\">\n Notes with &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;\n </td>\n </tr>\n \n </tbody>\n </table>\n </div>\n \n <div class=\"mt-3 text-center\">\n <button @click=\"editGrinder('', '', '', '', '')\" class=\"inline-flex items-center gap-2 bg-brown-600 text-white px-4 py-2 rounded-lg hover:bg-brown-700 transition-all shadow-md hover:shadow-lg text-sm font-medium\">\n <span>+</span>\n <span>Add New Grinder</span>\n </button>\n </div>\n \n </div>\n \n\n \n \n\n \n</div>\n" 7 + <div id="profile-stats-data" 8 + data-brews="0" 9 + data-beans="1" 10 + data-roasters="0" 11 + data-grinders="1" 12 + data-brewers="0" 13 + style="display: none;"></div> 14 + <div x-show="activeTab === 'brews'"> 15 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300"> 16 + <p class="text-brown-800 text-lg mb-4 font-medium"> 17 + No brews yet! Start tracking your coffee journey. 18 + </p> 19 + <a href="/brews/new" 20 + class="inline-block bg-gradient-to-r from-brown-700 to-brown-800 text-white py-3 px-6 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all shadow-lg hover:shadow-xl font-medium"> 21 + Add Your First Brew 22 + </a> 23 + </div> 24 + </div> 25 + <div x-show="activeTab === 'beans'" x-cloak class="space-y-6"> 26 + <div> 27 + <h3 class="text-lg font-semibold text-brown-900 mb-3"> 28 + ☕ Coffee Beans 29 + </h3> 30 + <div class="overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300"> 31 + <table class="min-w-full divide-y divide-brown-300"> 32 + <thead class="bg-brown-200/80"> 33 + <tr> 34 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 35 + Name 36 + </th> 37 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 38 + ☕ Roaster 39 + </th> 40 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 41 + 📍 Origin 42 + </th> 43 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 44 + 🔥 Roast 45 + </th> 46 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 47 + 🌱 Process 48 + </th> 49 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 50 + 📝 Description 51 + </th> 52 + </tr> 53 + </thead> 54 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 55 + <tr class="hover:bg-brown-100/60 transition-colors"> 56 + <td class="px-6 py-4 text-sm font-bold text-brown-900"> 57 + Bean with &lt;html&gt; &amp; &#34;quotes&#34; 58 + </td> 59 + <td class="px-6 py-4 text-sm text-brown-900"> 60 + <span class="text-brown-400"> 61 + - 62 + </span> 63 + </td> 64 + <td class="px-6 py-4 text-sm text-brown-900"> 65 + Colombia &amp; Peru 66 + </td> 67 + <td class="px-6 py-4 text-sm text-brown-900"> 68 + <span class="text-brown-400"> 69 + - 70 + </span> 71 + </td> 72 + <td class="px-6 py-4 text-sm text-brown-900"> 73 + <span class="text-brown-400"> 74 + - 75 + </span> 76 + </td> 77 + <td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs"> 78 + Description with &#39;single&#39; and &#34;double&#34; quotes 79 + </td> 80 + </tr> 81 + </tbody> 82 + </table> 83 + </div> 84 + <div class="mt-3 text-center"> 85 + <button @click="editBean('', '', '', '', '', '', '')" class="inline-flex items-center gap-2 bg-brown-600 text-white px-4 py-2 rounded-lg hover:bg-brown-700 transition-all shadow-md hover:shadow-lg text-sm font-medium"> 86 + <span> 87 + + 88 + </span> 89 + <span> 90 + Add New Bean 91 + </span> 92 + </button> 93 + </div> 94 + </div> 95 + </div> 96 + <div x-show="activeTab === 'gear'" x-cloak class="space-y-6"> 97 + <div> 98 + <h3 class="text-lg font-semibold text-brown-900 mb-3"> 99 + ⚙️ Grinders 100 + </h3> 101 + <div class="overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300"> 102 + <table class="min-w-full divide-y divide-brown-300"> 103 + <thead class="bg-brown-200/80"> 104 + <tr> 105 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 106 + Name 107 + </th> 108 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 109 + 🔧 Type 110 + </th> 111 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 112 + 💎 Burrs 113 + </th> 114 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 115 + 📝 Notes 116 + </th> 117 + </tr> 118 + </thead> 119 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 120 + <tr class="hover:bg-brown-100/60 transition-colors"> 121 + <td class="px-6 py-4 text-sm font-bold text-brown-900"> 122 + Grinder &amp; Co. 123 + </td> 124 + <td class="px-6 py-4 text-sm text-brown-900"> 125 + <span class="text-brown-400"> 126 + - 127 + </span> 128 + </td> 129 + <td class="px-6 py-4 text-sm text-brown-900"> 130 + <span class="text-brown-400"> 131 + - 132 + </span> 133 + </td> 134 + <td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs"> 135 + Notes with &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt; 136 + </td> 137 + </tr> 138 + </tbody> 139 + </table> 140 + </div> 141 + <div class="mt-3 text-center"> 142 + <button @click="editGrinder('', '', '', '', '')" class="inline-flex items-center gap-2 bg-brown-600 text-white px-4 py-2 rounded-lg hover:bg-brown-700 transition-all shadow-md hover:shadow-lg text-sm font-medium"> 143 + <span> 144 + + 145 + </span> 146 + <span> 147 + Add New Grinder 148 + </span> 149 + </button> 150 + </div> 151 + </div> 152 + </div>
+145 -1
internal/bff/__snapshots__/profile_with_unicode_content.snap
··· 4 4 file_name: profile_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n\n<div id=\"profile-stats-data\" \n data-brews=\"0\" \n data-beans=\"2\" \n data-roasters=\"1\" \n data-grinders=\"0\" \n data-brewers=\"0\"\n style=\"display: none;\"></div>\n\n\n<div x-show=\"activeTab === 'brews'\">\n \n\n<div class=\"bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300\">\n \n <p class=\"text-brown-800 text-lg font-medium\">No brews yet.</p>\n \n</div>\n\n\n</div>\n\n\n<div x-show=\"activeTab === 'beans'\" x-cloak class=\"space-y-6\">\n \n \n <div>\n <h3 class=\"text-lg font-semibold text-brown-900 mb-3\">☕ Coffee Beans</h3>\n <div class=\"overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300\">\n <table class=\"min-w-full divide-y divide-brown-300\">\n <thead class=\"bg-brown-200/80\">\n <tr>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">Name</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">☕ Roaster</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">📍 Origin</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">🔥 Roast</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">🌱 Process</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap\">📝 Description</th>\n </tr>\n </thead>\n <tbody class=\"bg-brown-50/60 divide-y divide-brown-200\">\n \n <tr class=\"hover:bg-brown-100/60 transition-colors\">\n <td class=\"px-6 py-4 text-sm font-bold text-brown-900\">\n エチオピア イルガチェフェ\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n \n <span class=\"text-brown-400\">-</span>\n \n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n 日本\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n <span class=\"text-brown-400\">-</span>\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n <span class=\"text-brown-400\">-</span>\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-700 italic max-w-xs\">\n 明るく花のような香り\n </td>\n </tr>\n \n <tr class=\"hover:bg-brown-100/60 transition-colors\">\n <td class=\"px-6 py-4 text-sm font-bold text-brown-900\">\n Café de Colombia\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n \n <span class=\"text-brown-400\">-</span>\n \n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Bogotá\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n <span class=\"text-brown-400\">-</span>\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n <span class=\"text-brown-400\">-</span>\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-700 italic max-w-xs\">\n Suave y aromático\n </td>\n </tr>\n \n </tbody>\n </table>\n </div>\n \n </div>\n \n\n \n \n <div>\n <h3 class=\"text-lg font-semibold text-brown-900 mb-3\">🏪 Favorite Roasters</h3>\n <div class=\"overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300\">\n <table class=\"min-w-full divide-y divide-brown-300\">\n <thead class=\"bg-brown-200/80\">\n <tr>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">Name</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">📍 Location</th>\n <th class=\"px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider\">🌐 Website</th>\n </tr>\n </thead>\n <tbody class=\"bg-brown-50/60 divide-y divide-brown-200\">\n \n <tr class=\"hover:bg-brown-100/60 transition-colors\">\n <td class=\"px-6 py-4 text-sm font-bold text-brown-900\">Кофейня Москва</td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n Москва, Россия\n </td>\n <td class=\"px-6 py-4 text-sm text-brown-900\">\n \n <span class=\"text-brown-400\">-</span>\n \n </td>\n </tr>\n \n </tbody>\n </table>\n </div>\n \n </div>\n \n\n \n</div>\n\n\n<div x-show=\"activeTab === 'gear'\" x-cloak class=\"space-y-6\">\n \n \n\n \n \n\n \n <div class=\"bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center text-brown-800 border border-brown-300\">\n <p class=\"font-medium\">No gear added yet.</p>\n </div>\n \n</div>\n" 7 + <div id="profile-stats-data" 8 + data-brews="0" 9 + data-beans="2" 10 + data-roasters="1" 11 + data-grinders="0" 12 + data-brewers="0" 13 + style="display: none;"></div> 14 + <div x-show="activeTab === 'brews'"> 15 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center border border-brown-300"> 16 + <p class="text-brown-800 text-lg font-medium"> 17 + No brews yet. 18 + </p> 19 + </div> 20 + </div> 21 + <div x-show="activeTab === 'beans'" x-cloak class="space-y-6"> 22 + <div> 23 + <h3 class="text-lg font-semibold text-brown-900 mb-3"> 24 + ☕ Coffee Beans 25 + </h3> 26 + <div class="overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300"> 27 + <table class="min-w-full divide-y divide-brown-300"> 28 + <thead class="bg-brown-200/80"> 29 + <tr> 30 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 31 + Name 32 + </th> 33 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 34 + ☕ Roaster 35 + </th> 36 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 37 + 📍 Origin 38 + </th> 39 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 40 + 🔥 Roast 41 + </th> 42 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 43 + 🌱 Process 44 + </th> 45 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap"> 46 + 📝 Description 47 + </th> 48 + </tr> 49 + </thead> 50 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 51 + <tr class="hover:bg-brown-100/60 transition-colors"> 52 + <td class="px-6 py-4 text-sm font-bold text-brown-900"> 53 + エチオピア イルガチェフェ 54 + </td> 55 + <td class="px-6 py-4 text-sm text-brown-900"> 56 + <span class="text-brown-400"> 57 + - 58 + </span> 59 + </td> 60 + <td class="px-6 py-4 text-sm text-brown-900"> 61 + 日本 62 + </td> 63 + <td class="px-6 py-4 text-sm text-brown-900"> 64 + <span class="text-brown-400"> 65 + - 66 + </span> 67 + </td> 68 + <td class="px-6 py-4 text-sm text-brown-900"> 69 + <span class="text-brown-400"> 70 + - 71 + </span> 72 + </td> 73 + <td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs"> 74 + 明るく花のような香り 75 + </td> 76 + </tr> 77 + <tr class="hover:bg-brown-100/60 transition-colors"> 78 + <td class="px-6 py-4 text-sm font-bold text-brown-900"> 79 + Café de Colombia 80 + </td> 81 + <td class="px-6 py-4 text-sm text-brown-900"> 82 + <span class="text-brown-400"> 83 + - 84 + </span> 85 + </td> 86 + <td class="px-6 py-4 text-sm text-brown-900"> 87 + Bogotá 88 + </td> 89 + <td class="px-6 py-4 text-sm text-brown-900"> 90 + <span class="text-brown-400"> 91 + - 92 + </span> 93 + </td> 94 + <td class="px-6 py-4 text-sm text-brown-900"> 95 + <span class="text-brown-400"> 96 + - 97 + </span> 98 + </td> 99 + <td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs"> 100 + Suave y aromático 101 + </td> 102 + </tr> 103 + </tbody> 104 + </table> 105 + </div> 106 + </div> 107 + <div> 108 + <h3 class="text-lg font-semibold text-brown-900 mb-3"> 109 + 🏪 Favorite Roasters 110 + </h3> 111 + <div class="overflow-x-auto bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl border border-brown-300"> 112 + <table class="min-w-full divide-y divide-brown-300"> 113 + <thead class="bg-brown-200/80"> 114 + <tr> 115 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 116 + Name 117 + </th> 118 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 119 + 📍 Location 120 + </th> 121 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider"> 122 + 🌐 Website 123 + </th> 124 + </tr> 125 + </thead> 126 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 127 + <tr class="hover:bg-brown-100/60 transition-colors"> 128 + <td class="px-6 py-4 text-sm font-bold text-brown-900"> 129 + Кофейня Москва 130 + </td> 131 + <td class="px-6 py-4 text-sm text-brown-900"> 132 + Москва, Россия 133 + </td> 134 + <td class="px-6 py-4 text-sm text-brown-900"> 135 + <span class="text-brown-400"> 136 + - 137 + </span> 138 + </td> 139 + </tr> 140 + </tbody> 141 + </table> 142 + </div> 143 + </div> 144 + </div> 145 + <div x-show="activeTab === 'gear'" x-cloak class="space-y-6"> 146 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 text-center text-brown-800 border border-brown-300"> 147 + <p class="font-medium"> 148 + No gear added yet. 149 + </p> 150 + </div> 151 + </div>
+59 -1
internal/bff/__snapshots__/profile_with_unsafe_avatar_url.snap
··· 4 4 file_name: feed_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n<div class=\"space-y-4\">\n \n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/badavatar\" class=\"flex-shrink-0\">\n \n \n \n <div class=\"w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition\">\n <span class=\"text-brown-600 text-sm\">?</span>\n </div>\n \n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/badavatar\" class=\"font-medium text-brown-900 truncate hover:text-brown-700 hover:underline\">Bad Avatar</a>\n \n <a href=\"/profile/badavatar\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@badavatar</a>\n </div>\n <span class=\"text-brown-500 text-sm\">2 minutes ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n 🫘 added a new bean\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200\">\n <div class=\"text-base mb-2\">\n <span class=\"font-bold text-brown-900\">\n Test Bean\n </span>\n \n </div>\n <div class=\"text-sm text-brown-700 space-y-1\">\n \n <div><span class=\"text-brown-600\">Origin:</span> Test Origin</div>\n \n \n <div><span class=\"text-brown-600\">Roast:</span> Medium</div>\n \n \n <div><span class=\"text-brown-600\">Process:</span> Natural</div>\n \n \n <div class=\"mt-2 text-brown-800 italic\">\"Sweet and fruity with notes of blueberry\"</div>\n \n </div>\n </div>\n \n </div>\n \n \n \n</div>\n" 7 + <div class="space-y-4"> 8 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 9 + <div class="flex items-center gap-3 mb-3"> 10 + <a href="/profile/badavatar" class="flex-shrink-0"> 11 + <div class="w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition"> 12 + <span class="text-brown-600 text-sm"> 13 + ? 14 + </span> 15 + </div> 16 + </a> 17 + <div class="flex-1 min-w-0"> 18 + <div class="flex items-center gap-2"> 19 + <a href="/profile/badavatar" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline"> 20 + Bad Avatar 21 + </a> 22 + <a href="/profile/badavatar" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 23 + @badavatar 24 + </a> 25 + </div> 26 + <span class="text-brown-500 text-sm"> 27 + 2 minutes ago 28 + </span> 29 + </div> 30 + </div> 31 + <div class="mb-2 text-sm text-brown-700"> 32 + 🫘 added a new bean 33 + </div> 34 + <div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200"> 35 + <div class="text-base mb-2"> 36 + <span class="font-bold text-brown-900"> 37 + Test Bean 38 + </span> 39 + </div> 40 + <div class="text-sm text-brown-700 space-y-1"> 41 + <div> 42 + <span class="text-brown-600"> 43 + Origin: 44 + </span> 45 + Test Origin 46 + </div> 47 + <div> 48 + <span class="text-brown-600"> 49 + Roast: 50 + </span> 51 + Medium 52 + </div> 53 + <div> 54 + <span class="text-brown-600"> 55 + Process: 56 + </span> 57 + Natural 58 + </div> 59 + <div class="mt-2 text-brown-800 italic"> 60 + "Sweet and fruity with notes of blueberry" 61 + </div> 62 + </div> 63 + </div> 64 + </div> 65 + </div>
+29
internal/bff/__snapshots__/roaster_form_renders.snap
··· 1 + --- 2 + title: roaster_form_renders 3 + test_name: TestNewRoasterForm_Snapshot/roaster_form_renders 4 + file_name: form_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 8 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 9 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3> 10 + <div class="space-y-4"> 11 + <input type="text" x-model="roasterForm.name" placeholder="Name *" 12 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 13 + <input type="text" x-model="roasterForm.location" placeholder="Location" 14 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 15 + <input type="url" x-model="roasterForm.website" placeholder="Website" 16 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 17 + <div class="flex gap-2"> 18 + <button @click="saveRoaster()" 19 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 20 + Save 21 + </button> 22 + <button @click="showRoasterForm = false" 23 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 24 + Cancel 25 + </button> 26 + </div> 27 + </div> 28 + </div> 29 + </div>
+48 -1
internal/bff/__snapshots__/roaster_item.snap
··· 4 4 file_name: feed_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n<div class=\"space-y-4\">\n \n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/roastmaster\" class=\"flex-shrink-0\">\n \n \n \n <img src=\"https://cdn.bsky.app/avatar2.jpg\" alt=\"\" class=\"w-10 h-10 rounded-full object-cover hover:ring-2 hover:ring-brown-600 transition\" />\n \n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/roastmaster\" class=\"font-medium text-brown-900 truncate hover:text-brown-700 hover:underline\">Roast Master</a>\n \n <a href=\"/profile/roastmaster\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@roastmaster</a>\n </div>\n <span class=\"text-brown-500 text-sm\">10 minutes ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n 🏪 added a new roaster\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200\">\n <div class=\"text-base mb-2\">\n <span class=\"font-bold text-brown-900\">Heart Coffee Roasters</span>\n </div>\n <div class=\"text-sm text-brown-700 space-y-1\">\n \n <div><span class=\"text-brown-600\">Location:</span> Portland, OR</div>\n \n \n \n \n <div><span class=\"text-brown-600\">Website:</span> <a href=\"https://heartroasters.com\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-brown-800 hover:underline\">https://heartroasters.com</a></div>\n \n \n </div>\n </div>\n \n </div>\n \n \n \n</div>\n" 7 + <div class="space-y-4"> 8 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 9 + <div class="flex items-center gap-3 mb-3"> 10 + <a href="/profile/roastmaster" class="flex-shrink-0"> 11 + <img src="https://cdn.bsky.app/avatar2.jpg" alt="" class="w-10 h-10 rounded-full object-cover hover:ring-2 hover:ring-brown-600 transition" /> 12 + </a> 13 + <div class="flex-1 min-w-0"> 14 + <div class="flex items-center gap-2"> 15 + <a href="/profile/roastmaster" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline"> 16 + Roast Master 17 + </a> 18 + <a href="/profile/roastmaster" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 19 + @roastmaster 20 + </a> 21 + </div> 22 + <span class="text-brown-500 text-sm"> 23 + 10 minutes ago 24 + </span> 25 + </div> 26 + </div> 27 + <div class="mb-2 text-sm text-brown-700"> 28 + 🏪 added a new roaster 29 + </div> 30 + <div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200"> 31 + <div class="text-base mb-2"> 32 + <span class="font-bold text-brown-900"> 33 + Heart Coffee Roasters 34 + </span> 35 + </div> 36 + <div class="text-sm text-brown-700 space-y-1"> 37 + <div> 38 + <span class="text-brown-600"> 39 + Location: 40 + </span> 41 + Portland, OR 42 + </div> 43 + <div> 44 + <span class="text-brown-600"> 45 + Website: 46 + </span> 47 + <a href="https://heartroasters.com" target="_blank" rel="noopener noreferrer" class="text-brown-800 hover:underline"> 48 + https://heartroasters.com 49 + </a> 50 + </div> 51 + </div> 52 + </div> 53 + </div> 54 + </div>
+37 -1
internal/bff/__snapshots__/roaster_with_unsafe_website_url.snap
··· 4 4 file_name: feed_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n<div class=\"space-y-4\">\n \n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/hacker\" class=\"flex-shrink-0\">\n \n <div class=\"w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition\">\n <span class=\"text-brown-600 text-sm\">?</span>\n </div>\n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/hacker\" class=\"font-medium text-brown-900 truncate hover:text-brown-700 hover:underline\">Hacker</a>\n \n <a href=\"/profile/hacker\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@hacker</a>\n </div>\n <span class=\"text-brown-500 text-sm\">1 minute ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n 🏪 added a new roaster\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200\">\n <div class=\"text-base mb-2\">\n <span class=\"font-bold text-brown-900\">Sketchy Roaster</span>\n </div>\n <div class=\"text-sm text-brown-700 space-y-1\">\n \n \n \n \n \n </div>\n </div>\n \n </div>\n \n \n \n</div>\n" 7 + <div class="space-y-4"> 8 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 9 + <div class="flex items-center gap-3 mb-3"> 10 + <a href="/profile/hacker" class="flex-shrink-0"> 11 + <div class="w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition"> 12 + <span class="text-brown-600 text-sm"> 13 + ? 14 + </span> 15 + </div> 16 + </a> 17 + <div class="flex-1 min-w-0"> 18 + <div class="flex items-center gap-2"> 19 + <a href="/profile/hacker" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline"> 20 + Hacker 21 + </a> 22 + <a href="/profile/hacker" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 23 + @hacker 24 + </a> 25 + </div> 26 + <span class="text-brown-500 text-sm"> 27 + 1 minute ago 28 + </span> 29 + </div> 30 + </div> 31 + <div class="mb-2 text-sm text-brown-700"> 32 + 🏪 added a new roaster 33 + </div> 34 + <div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200"> 35 + <div class="text-base mb-2"> 36 + <span class="font-bold text-brown-900"> 37 + Sketchy Roaster 38 + </span> 39 + </div> 40 + <div class="text-sm text-brown-700 space-y-1"></div> 41 + </div> 42 + </div> 43 + </div>
+210
internal/bff/__snapshots__/roasters_empty.snap
··· 1 + --- 2 + title: roasters empty 3 + test_name: TestManageContent_RoastersTab_Snapshot/roasters_empty 4 + file_name: partial_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="tab === 'beans'"> 8 + <div class="mb-4 flex justify-between items-center"> 9 + <h3 class="text-xl font-semibold text-brown-900"> 10 + Coffee Beans 11 + </h3> 12 + <button 13 + @click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}" 14 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 15 + + Add Bean 16 + </button> 17 + </div> 18 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 19 + No beans yet. Add your first bean to get started! 20 + </div> 21 + </div> 22 + <div x-show="tab === 'roasters'"> 23 + <div class="mb-4 flex justify-between items-center"> 24 + <h3 class="text-xl font-semibold text-brown-900"> 25 + Roasters 26 + </h3> 27 + <button 28 + @click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}" 29 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 30 + + Add Roaster 31 + </button> 32 + </div> 33 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 34 + No roasters yet. Add your first roaster! 35 + </div> 36 + </div> 37 + <div x-show="tab === 'grinders'"> 38 + <div class="mb-4 flex justify-between items-center"> 39 + <h3 class="text-xl font-semibold text-brown-900"> 40 + Grinders 41 + </h3> 42 + <button 43 + @click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}" 44 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 45 + + Add Grinder 46 + </button> 47 + </div> 48 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 49 + No grinders yet. Add your first grinder! 50 + </div> 51 + </div> 52 + <div x-show="tab === 'brewers'"> 53 + <div class="mb-4 flex justify-between items-center"> 54 + <h3 class="text-xl font-semibold text-brown-900"> 55 + Brewers 56 + </h3> 57 + <button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}" 58 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 59 + + Add Brewer 60 + </button> 61 + </div> 62 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 63 + No brewers yet. Add your first brewer! 64 + </div> 65 + </div> 66 + <div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 67 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 68 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3> 69 + <div class="space-y-4"> 70 + <input type="text" x-model="beanForm.name" placeholder="Name *" 71 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 72 + <input type="text" x-model="beanForm.origin" placeholder="Origin *" 73 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 74 + <select x-model="beanForm.roaster_rkey" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 75 + <option value=""> 76 + Select Roaster (Optional) 77 + </option> 78 + </select> 79 + <select x-model="beanForm.roast_level" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 80 + <option value=""> 81 + Select Roast Level (Optional) 82 + </option> 83 + <option value="Ultra-Light"> 84 + Ultra-Light 85 + </option> 86 + <option value="Light"> 87 + Light 88 + </option> 89 + <option value="Medium-Light"> 90 + Medium-Light 91 + </option> 92 + <option value="Medium"> 93 + Medium 94 + </option> 95 + <option value="Medium-Dark"> 96 + Medium-Dark 97 + </option> 98 + <option value="Dark"> 99 + Dark 100 + </option> 101 + </select> 102 + <input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)" 103 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 104 + <textarea x-model="beanForm.description" placeholder="Description" rows="3" 105 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 106 + <div class="flex gap-2"> 107 + <button @click="saveBean()" 108 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 109 + Save 110 + </button> 111 + <button @click="showBeanForm = false" 112 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 113 + Cancel 114 + </button> 115 + </div> 116 + </div> 117 + </div> 118 + </div> 119 + <div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 120 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 121 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3> 122 + <div class="space-y-4"> 123 + <input type="text" x-model="roasterForm.name" placeholder="Name *" 124 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 125 + <input type="text" x-model="roasterForm.location" placeholder="Location" 126 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 127 + <input type="url" x-model="roasterForm.website" placeholder="Website" 128 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 129 + <div class="flex gap-2"> 130 + <button @click="saveRoaster()" 131 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 132 + Save 133 + </button> 134 + <button @click="showRoasterForm = false" 135 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 136 + Cancel 137 + </button> 138 + </div> 139 + </div> 140 + </div> 141 + </div> 142 + <div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 143 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 144 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3> 145 + <div class="space-y-4"> 146 + <input type="text" x-model="grinderForm.name" placeholder="Name *" 147 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 148 + <select x-model="grinderForm.grinder_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 149 + <option value=""> 150 + Select Grinder Type * 151 + </option> 152 + <option value="Hand"> 153 + Hand 154 + </option> 155 + <option value="Electric"> 156 + Electric 157 + </option> 158 + <option value="Portable Electric"> 159 + Portable Electric 160 + </option> 161 + </select> 162 + <select x-model="grinderForm.burr_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 163 + <option value=""> 164 + Select Burr Type (Optional) 165 + </option> 166 + <option value="Conical"> 167 + Conical 168 + </option> 169 + <option value="Flat"> 170 + Flat 171 + </option> 172 + </select> 173 + <textarea x-model="grinderForm.notes" placeholder="Notes" rows="3" 174 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 175 + <div class="flex gap-2"> 176 + <button @click="saveGrinder()" 177 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 178 + Save 179 + </button> 180 + <button @click="showGrinderForm = false" 181 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 182 + Cancel 183 + </button> 184 + </div> 185 + </div> 186 + </div> 187 + </div> 188 + <div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 189 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 190 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3> 191 + <div class="space-y-4"> 192 + <input type="text" x-model="brewerForm.name" placeholder="Name *" 193 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 194 + <input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)" 195 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 196 + <textarea x-model="brewerForm.description" placeholder="Description" rows="3" 197 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 198 + <div class="flex gap-2"> 199 + <button @click="saveBrewer()" 200 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 201 + Save 202 + </button> 203 + <button @click="showBrewerForm = false" 204 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 205 + Cancel 206 + </button> 207 + </div> 208 + </div> 209 + </div> 210 + </div>
+276
internal/bff/__snapshots__/roasters_with_data.snap
··· 1 + --- 2 + title: roasters with data 3 + test_name: TestManageContent_RoastersTab_Snapshot/roasters_with_data 4 + file_name: partial_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="tab === 'beans'"> 8 + <div class="mb-4 flex justify-between items-center"> 9 + <h3 class="text-xl font-semibold text-brown-900"> 10 + Coffee Beans 11 + </h3> 12 + <button 13 + @click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}" 14 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 15 + + Add Bean 16 + </button> 17 + </div> 18 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 19 + No beans yet. Add your first bean to get started! 20 + </div> 21 + </div> 22 + <div x-show="tab === 'roasters'"> 23 + <div class="mb-4 flex justify-between items-center"> 24 + <h3 class="text-xl font-semibold text-brown-900"> 25 + Roasters 26 + </h3> 27 + <button 28 + @click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}" 29 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 30 + + Add Roaster 31 + </button> 32 + </div> 33 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 shadow-xl rounded-xl overflow-x-auto border border-brown-300"> 34 + <table class="min-w-full divide-y divide-brown-300"> 35 + <thead class="bg-brown-200/80"> 36 + <tr> 37 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 38 + Name 39 + </th> 40 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 41 + 📍 Location 42 + </th> 43 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 44 + 🌐 Website 45 + </th> 46 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 47 + Actions 48 + </th> 49 + </tr> 50 + </thead> 51 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 52 + <tr class="hover:bg-brown-100/60 transition-colors"> 53 + <td class="px-6 py-4 text-sm font-medium text-brown-900"> 54 + Onyx Coffee Lab 55 + </td> 56 + <td class="px-6 py-4 text-sm text-brown-900"> 57 + Bentonville, AR 58 + </td> 59 + <td class="px-6 py-4 text-sm text-brown-900"> 60 + <a href="https://onyxcoffeelab.com" target="_blank" rel="noopener noreferrer" 61 + class="text-brown-700 hover:underline font-medium"> 62 + https://onyxcoffeelab.com 63 + </a> 64 + </td> 65 + <td class="px-6 py-4 text-sm font-medium space-x-2"> 66 + <button @click="editRoaster('roaster1', 'Onyx Coffee Lab', 'Bentonville, AR', 'https://onyxcoffeelab.com')" 67 + class="text-brown-700 hover:text-brown-900 font-medium"> 68 + Edit 69 + </button> 70 + <button @click="deleteRoaster('roaster1')" 71 + class="text-brown-600 hover:text-brown-800 font-medium"> 72 + Delete 73 + </button> 74 + </td> 75 + </tr> 76 + <tr class="hover:bg-brown-100/60 transition-colors"> 77 + <td class="px-6 py-4 text-sm font-medium text-brown-900"> 78 + Counter Culture Coffee 79 + </td> 80 + <td class="px-6 py-4 text-sm text-brown-900"></td> 81 + <td class="px-6 py-4 text-sm text-brown-900"></td> 82 + <td class="px-6 py-4 text-sm font-medium space-x-2"> 83 + <button @click="editRoaster('roaster2', 'Counter Culture Coffee', '', '')" 84 + class="text-brown-700 hover:text-brown-900 font-medium"> 85 + Edit 86 + </button> 87 + <button @click="deleteRoaster('roaster2')" 88 + class="text-brown-600 hover:text-brown-800 font-medium"> 89 + Delete 90 + </button> 91 + </td> 92 + </tr> 93 + </tbody> 94 + </table> 95 + </div> 96 + </div> 97 + <div x-show="tab === 'grinders'"> 98 + <div class="mb-4 flex justify-between items-center"> 99 + <h3 class="text-xl font-semibold text-brown-900"> 100 + Grinders 101 + </h3> 102 + <button 103 + @click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}" 104 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 105 + + Add Grinder 106 + </button> 107 + </div> 108 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 109 + No grinders yet. Add your first grinder! 110 + </div> 111 + </div> 112 + <div x-show="tab === 'brewers'"> 113 + <div class="mb-4 flex justify-between items-center"> 114 + <h3 class="text-xl font-semibold text-brown-900"> 115 + Brewers 116 + </h3> 117 + <button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}" 118 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 119 + + Add Brewer 120 + </button> 121 + </div> 122 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 123 + No brewers yet. Add your first brewer! 124 + </div> 125 + </div> 126 + <div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 127 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 128 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3> 129 + <div class="space-y-4"> 130 + <input type="text" x-model="beanForm.name" placeholder="Name *" 131 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 132 + <input type="text" x-model="beanForm.origin" placeholder="Origin *" 133 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 134 + <select x-model="beanForm.roaster_rkey" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 135 + <option value=""> 136 + Select Roaster (Optional) 137 + </option> 138 + <option value="roaster1"> 139 + Onyx Coffee Lab 140 + </option> 141 + <option value="roaster2"> 142 + Counter Culture Coffee 143 + </option> 144 + </select> 145 + <select x-model="beanForm.roast_level" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 146 + <option value=""> 147 + Select Roast Level (Optional) 148 + </option> 149 + <option value="Ultra-Light"> 150 + Ultra-Light 151 + </option> 152 + <option value="Light"> 153 + Light 154 + </option> 155 + <option value="Medium-Light"> 156 + Medium-Light 157 + </option> 158 + <option value="Medium"> 159 + Medium 160 + </option> 161 + <option value="Medium-Dark"> 162 + Medium-Dark 163 + </option> 164 + <option value="Dark"> 165 + Dark 166 + </option> 167 + </select> 168 + <input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)" 169 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 170 + <textarea x-model="beanForm.description" placeholder="Description" rows="3" 171 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 172 + <div class="flex gap-2"> 173 + <button @click="saveBean()" 174 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 175 + Save 176 + </button> 177 + <button @click="showBeanForm = false" 178 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 179 + Cancel 180 + </button> 181 + </div> 182 + </div> 183 + </div> 184 + </div> 185 + <div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 186 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 187 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3> 188 + <div class="space-y-4"> 189 + <input type="text" x-model="roasterForm.name" placeholder="Name *" 190 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 191 + <input type="text" x-model="roasterForm.location" placeholder="Location" 192 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 193 + <input type="url" x-model="roasterForm.website" placeholder="Website" 194 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 195 + <div class="flex gap-2"> 196 + <button @click="saveRoaster()" 197 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 198 + Save 199 + </button> 200 + <button @click="showRoasterForm = false" 201 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 202 + Cancel 203 + </button> 204 + </div> 205 + </div> 206 + </div> 207 + </div> 208 + <div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 209 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 210 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3> 211 + <div class="space-y-4"> 212 + <input type="text" x-model="grinderForm.name" placeholder="Name *" 213 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 214 + <select x-model="grinderForm.grinder_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 215 + <option value=""> 216 + Select Grinder Type * 217 + </option> 218 + <option value="Hand"> 219 + Hand 220 + </option> 221 + <option value="Electric"> 222 + Electric 223 + </option> 224 + <option value="Portable Electric"> 225 + Portable Electric 226 + </option> 227 + </select> 228 + <select x-model="grinderForm.burr_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 229 + <option value=""> 230 + Select Burr Type (Optional) 231 + </option> 232 + <option value="Conical"> 233 + Conical 234 + </option> 235 + <option value="Flat"> 236 + Flat 237 + </option> 238 + </select> 239 + <textarea x-model="grinderForm.notes" placeholder="Notes" rows="3" 240 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 241 + <div class="flex gap-2"> 242 + <button @click="saveGrinder()" 243 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 244 + Save 245 + </button> 246 + <button @click="showGrinderForm = false" 247 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 248 + Cancel 249 + </button> 250 + </div> 251 + </div> 252 + </div> 253 + </div> 254 + <div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 255 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 256 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3> 257 + <div class="space-y-4"> 258 + <input type="text" x-model="brewerForm.name" placeholder="Name *" 259 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 260 + <input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)" 261 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 262 + <textarea x-model="brewerForm.description" placeholder="Description" rows="3" 263 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 264 + <div class="flex gap-2"> 265 + <button @click="saveBrewer()" 266 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 267 + Save 268 + </button> 269 + <button @click="showBrewerForm = false" 270 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 271 + Cancel 272 + </button> 273 + </div> 274 + </div> 275 + </div> 276 + </div>
+251
internal/bff/__snapshots__/roasters_with_unsafe_url.snap
··· 1 + --- 2 + title: roasters with unsafe url 3 + test_name: TestManageContent_RoastersTab_Snapshot/roasters_with_unsafe_url 4 + file_name: partial_template_snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + <div x-show="tab === 'beans'"> 8 + <div class="mb-4 flex justify-between items-center"> 9 + <h3 class="text-xl font-semibold text-brown-900"> 10 + Coffee Beans 11 + </h3> 12 + <button 13 + @click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}" 14 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 15 + + Add Bean 16 + </button> 17 + </div> 18 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 19 + No beans yet. Add your first bean to get started! 20 + </div> 21 + </div> 22 + <div x-show="tab === 'roasters'"> 23 + <div class="mb-4 flex justify-between items-center"> 24 + <h3 class="text-xl font-semibold text-brown-900"> 25 + Roasters 26 + </h3> 27 + <button 28 + @click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}" 29 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 30 + + Add Roaster 31 + </button> 32 + </div> 33 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 shadow-xl rounded-xl overflow-x-auto border border-brown-300"> 34 + <table class="min-w-full divide-y divide-brown-300"> 35 + <thead class="bg-brown-200/80"> 36 + <tr> 37 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 38 + Name 39 + </th> 40 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 41 + 📍 Location 42 + </th> 43 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 44 + 🌐 Website 45 + </th> 46 + <th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase"> 47 + Actions 48 + </th> 49 + </tr> 50 + </thead> 51 + <tbody class="bg-brown-50/60 divide-y divide-brown-200"> 52 + <tr class="hover:bg-brown-100/60 transition-colors"> 53 + <td class="px-6 py-4 text-sm font-medium text-brown-900"> 54 + Test Roaster 55 + </td> 56 + <td class="px-6 py-4 text-sm text-brown-900"> 57 + Test Location 58 + </td> 59 + <td class="px-6 py-4 text-sm text-brown-900"></td> 60 + <td class="px-6 py-4 text-sm font-medium space-x-2"> 61 + <button @click="editRoaster('roaster1', 'Test Roaster', 'Test Location', 'javascript:alert(\&#39;xss\&#39;)')" 62 + class="text-brown-700 hover:text-brown-900 font-medium"> 63 + Edit 64 + </button> 65 + <button @click="deleteRoaster('roaster1')" 66 + class="text-brown-600 hover:text-brown-800 font-medium"> 67 + Delete 68 + </button> 69 + </td> 70 + </tr> 71 + </tbody> 72 + </table> 73 + </div> 74 + </div> 75 + <div x-show="tab === 'grinders'"> 76 + <div class="mb-4 flex justify-between items-center"> 77 + <h3 class="text-xl font-semibold text-brown-900"> 78 + Grinders 79 + </h3> 80 + <button 81 + @click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}" 82 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 83 + + Add Grinder 84 + </button> 85 + </div> 86 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 87 + No grinders yet. Add your first grinder! 88 + </div> 89 + </div> 90 + <div x-show="tab === 'brewers'"> 91 + <div class="mb-4 flex justify-between items-center"> 92 + <h3 class="text-xl font-semibold text-brown-900"> 93 + Brewers 94 + </h3> 95 + <button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}" 96 + class="bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 transition-all font-medium shadow-md hover:shadow-lg"> 97 + + Add Brewer 98 + </button> 99 + </div> 100 + <div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200"> 101 + No brewers yet. Add your first brewer! 102 + </div> 103 + </div> 104 + <div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 105 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 106 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3> 107 + <div class="space-y-4"> 108 + <input type="text" x-model="beanForm.name" placeholder="Name *" 109 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 110 + <input type="text" x-model="beanForm.origin" placeholder="Origin *" 111 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 112 + <select x-model="beanForm.roaster_rkey" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 113 + <option value=""> 114 + Select Roaster (Optional) 115 + </option> 116 + <option value="roaster1"> 117 + Test Roaster 118 + </option> 119 + </select> 120 + <select x-model="beanForm.roast_level" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 121 + <option value=""> 122 + Select Roast Level (Optional) 123 + </option> 124 + <option value="Ultra-Light"> 125 + Ultra-Light 126 + </option> 127 + <option value="Light"> 128 + Light 129 + </option> 130 + <option value="Medium-Light"> 131 + Medium-Light 132 + </option> 133 + <option value="Medium"> 134 + Medium 135 + </option> 136 + <option value="Medium-Dark"> 137 + Medium-Dark 138 + </option> 139 + <option value="Dark"> 140 + Dark 141 + </option> 142 + </select> 143 + <input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)" 144 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 145 + <textarea x-model="beanForm.description" placeholder="Description" rows="3" 146 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 147 + <div class="flex gap-2"> 148 + <button @click="saveBean()" 149 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 150 + Save 151 + </button> 152 + <button @click="showBeanForm = false" 153 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 154 + Cancel 155 + </button> 156 + </div> 157 + </div> 158 + </div> 159 + </div> 160 + <div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 161 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 162 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3> 163 + <div class="space-y-4"> 164 + <input type="text" x-model="roasterForm.name" placeholder="Name *" 165 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 166 + <input type="text" x-model="roasterForm.location" placeholder="Location" 167 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 168 + <input type="url" x-model="roasterForm.website" placeholder="Website" 169 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 170 + <div class="flex gap-2"> 171 + <button @click="saveRoaster()" 172 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 173 + Save 174 + </button> 175 + <button @click="showRoasterForm = false" 176 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 177 + Cancel 178 + </button> 179 + </div> 180 + </div> 181 + </div> 182 + </div> 183 + <div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 184 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 185 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3> 186 + <div class="space-y-4"> 187 + <input type="text" x-model="grinderForm.name" placeholder="Name *" 188 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 189 + <select x-model="grinderForm.grinder_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 190 + <option value=""> 191 + Select Grinder Type * 192 + </option> 193 + <option value="Hand"> 194 + Hand 195 + </option> 196 + <option value="Electric"> 197 + Electric 198 + </option> 199 + <option value="Portable Electric"> 200 + Portable Electric 201 + </option> 202 + </select> 203 + <select x-model="grinderForm.burr_type" class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"> 204 + <option value=""> 205 + Select Burr Type (Optional) 206 + </option> 207 + <option value="Conical"> 208 + Conical 209 + </option> 210 + <option value="Flat"> 211 + Flat 212 + </option> 213 + </select> 214 + <textarea x-model="grinderForm.notes" placeholder="Notes" rows="3" 215 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 216 + <div class="flex gap-2"> 217 + <button @click="saveGrinder()" 218 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 219 + Save 220 + </button> 221 + <button @click="showGrinderForm = false" 222 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 223 + Cancel 224 + </button> 225 + </div> 226 + </div> 227 + </div> 228 + </div> 229 + <div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 230 + <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl border-2 border-brown-300 p-8 max-w-md w-full mx-4 shadow-2xl"> 231 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3> 232 + <div class="space-y-4"> 233 + <input type="text" x-model="brewerForm.name" placeholder="Name *" 234 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 235 + <input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)" 236 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 237 + <textarea x-model="brewerForm.description" placeholder="Description" rows="3" 238 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600"></textarea> 239 + <div class="flex gap-2"> 240 + <button @click="saveBrewer()" 241 + class="flex-1 bg-gradient-to-r from-brown-700 to-brown-800 text-white px-4 py-2 rounded-lg hover:from-brown-800 hover:to-brown-900 font-medium transition-all shadow-md"> 242 + Save 243 + </button> 244 + <button @click="showBrewerForm = false" 245 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors"> 246 + Cancel 247 + </button> 248 + </div> 249 + </div> 250 + </div> 251 + </div>
+46 -1
internal/bff/__snapshots__/special_characters_in_content.snap
··· 4 4 file_name: feed_template_snapshot_test.go 5 5 version: 0.1.0 6 6 --- 7 - "\n<div class=\"space-y-4\">\n \n \n <div class=\"bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow\">\n \n <div class=\"flex items-center gap-3 mb-3\">\n <a href=\"/profile/special.chars\" class=\"flex-shrink-0\">\n \n <div class=\"w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition\">\n <span class=\"text-brown-600 text-sm\">?</span>\n </div>\n \n </a>\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2\">\n \n <a href=\"/profile/special.chars\" class=\"font-medium text-brown-900 truncate hover:text-brown-700 hover:underline\">User &amp; Co.</a>\n \n <a href=\"/profile/special.chars\" class=\"text-brown-600 text-sm truncate hover:text-brown-700 hover:underline\">@special.chars</a>\n </div>\n <span class=\"text-brown-500 text-sm\">5 seconds ago</span>\n </div>\n </div>\n\n \n <div class=\"mb-2 text-sm text-brown-700\">\n ☕ added a new brew\n </div>\n\n \n \n \n <div class=\"bg-white/60 backdrop-blur rounded-lg p-4 border border-brown-200\">\n \n <div class=\"flex items-start justify-between gap-3 mb-3\">\n <div class=\"flex-1 min-w-0\">\n \n <div class=\"font-bold text-brown-900 text-base\">\n Bean with &amp; ampersand\n </div>\n \n <div class=\"text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5\">\n \n \n \n \n </div>\n \n </div>\n \n <span class=\"inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-amber-100 text-amber-900 flex-shrink-0\">\n ⭐ 8/10\n </span>\n \n </div>\n \n \n \n \n \n <div class=\"grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700\">\n \n \n \n \n </div>\n\n \n <div class=\"mt-3 text-sm text-brown-800 italic border-t border-brown-200 pt-2\">\n \"Notes with &#34;quotes&#34; and &lt;html&gt;tags&lt;/html&gt; and &#39;single quotes&#39;\"\n </div>\n \n </div>\n \n </div>\n \n \n \n</div>\n" 7 + <div class="space-y-4"> 8 + <div class="bg-gradient-to-br from-brown-50 to-brown-100 rounded-lg shadow-md border border-brown-200 p-4 hover:shadow-lg transition-shadow"> 9 + <div class="flex items-center gap-3 mb-3"> 10 + <a href="/profile/special.chars" class="flex-shrink-0"> 11 + <div class="w-10 h-10 rounded-full bg-brown-300 flex items-center justify-center hover:ring-2 hover:ring-brown-600 transition"> 12 + <span class="text-brown-600 text-sm"> 13 + ? 14 + </span> 15 + </div> 16 + </a> 17 + <div class="flex-1 min-w-0"> 18 + <div class="flex items-center gap-2"> 19 + <a href="/profile/special.chars" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline"> 20 + User &amp; Co. 21 + </a> 22 + <a href="/profile/special.chars" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline"> 23 + @special.chars 24 + </a> 25 + </div> 26 + <span class="text-brown-500 text-sm"> 27 + 5 seconds ago 28 + </span> 29 + </div> 30 + </div> 31 + <div class="mb-2 text-sm text-brown-700"> 32 + ☕ added a new brew 33 + </div> 34 + <div class="bg-white/60 backdrop-blur rounded-lg p-4 border border-brown-200"> 35 + <div class="flex items-start justify-between gap-3 mb-3"> 36 + <div class="flex-1 min-w-0"> 37 + <div class="font-bold text-brown-900 text-base"> 38 + Bean with &amp; ampersand 39 + </div> 40 + <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"></div> 41 + </div> 42 + <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-amber-100 text-amber-900 flex-shrink-0"> 43 + ⭐ 8/10 44 + </span> 45 + </div> 46 + <div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700"></div> 47 + <div class="mt-3 text-sm text-brown-800 italic border-t border-brown-200 pt-2"> 48 + "Notes with &#34;quotes&#34; and &lt;html&gt;tags&lt;/html&gt; and &#39;single quotes&#39;" 49 + </div> 50 + </div> 51 + </div> 52 + </div>
+9 -9
internal/bff/feed_template_snapshot_test.go
··· 237 237 if err != nil { 238 238 t.Fatalf("Failed to execute template: %v", err) 239 239 } 240 - shutter.Snap(t, tt.name, result) 240 + shutter.SnapString(t, tt.name, formatHTML(result)) 241 241 }) 242 242 } 243 243 } ··· 277 277 if err != nil { 278 278 t.Fatalf("Failed to execute template: %v", err) 279 279 } 280 - shutter.Snap(t, tt.name, result) 280 + shutter.SnapString(t, tt.name, formatHTML(result)) 281 281 }) 282 282 } 283 283 } ··· 296 296 if err != nil { 297 297 t.Fatalf("Failed to execute template: %v", err) 298 298 } 299 - shutter.Snap(t, "roaster item", result) 299 + shutter.SnapString(t, "roaster item", formatHTML(result)) 300 300 } 301 301 302 302 func TestFeedTemplate_GrinderItem_Snapshot(t *testing.T) { ··· 313 313 if err != nil { 314 314 t.Fatalf("Failed to execute template: %v", err) 315 315 } 316 - shutter.Snap(t, "grinder item", result) 316 + shutter.SnapString(t, "grinder item", formatHTML(result)) 317 317 } 318 318 319 319 func TestFeedTemplate_BrewerItem_Snapshot(t *testing.T) { ··· 330 330 if err != nil { 331 331 t.Fatalf("Failed to execute template: %v", err) 332 332 } 333 - shutter.Snap(t, "brewer item", result) 333 + shutter.SnapString(t, "brewer item", formatHTML(result)) 334 334 } 335 335 336 336 // Test mixed feeds and edge cases ··· 383 383 if err != nil { 384 384 t.Fatalf("Failed to execute template: %v", err) 385 385 } 386 - shutter.Snap(t, "mixed feed all types", result) 386 + shutter.SnapString(t, "mixed feed all types", formatHTML(result)) 387 387 } 388 388 389 389 func TestFeedTemplate_EmptyFeed_Snapshot(t *testing.T) { ··· 415 415 if err != nil { 416 416 t.Fatalf("Failed to execute template: %v", err) 417 417 } 418 - shutter.Snap(t, tt.name, result) 418 + shutter.SnapString(t, tt.name, formatHTML(result)) 419 419 }) 420 420 } 421 421 } ··· 462 462 if err != nil { 463 463 t.Fatalf("Failed to execute template: %v", err) 464 464 } 465 - shutter.Snap(t, tt.name, result) 465 + shutter.SnapString(t, tt.name, formatHTML(result)) 466 466 }) 467 467 } 468 468 } ··· 492 492 if err != nil { 493 493 t.Fatalf("Failed to execute template: %v", err) 494 494 } 495 - shutter.Snap(t, "special characters in content", result) 495 + shutter.SnapString(t, "special characters in content", formatHTML(result)) 496 496 }
+380
internal/bff/form_template_snapshot_test.go
··· 1 + package bff 2 + 3 + import ( 4 + "bytes" 5 + "html/template" 6 + "testing" 7 + "time" 8 + 9 + "arabica/internal/models" 10 + 11 + "github.com/ptdewey/shutter" 12 + ) 13 + 14 + func TestBrewForm_NewBrew_Snapshot(t *testing.T) { 15 + tests := []struct { 16 + name string 17 + data map[string]interface{} 18 + }{ 19 + { 20 + name: "new brew with populated selects", 21 + data: map[string]interface{}{ 22 + "Brew": nil, 23 + "Beans": []*models.Bean{ 24 + {RKey: "bean1", Name: "Ethiopian Yirgacheffe", Origin: "Ethiopia", RoastLevel: "Light"}, 25 + {RKey: "bean2", Name: "", Origin: "Colombia", RoastLevel: "Medium"}, 26 + }, 27 + "Grinders": []*models.Grinder{ 28 + {RKey: "grinder1", Name: "Baratza Encore"}, 29 + {RKey: "grinder2", Name: "Comandante C40"}, 30 + }, 31 + "Brewers": []*models.Brewer{ 32 + {RKey: "brewer1", Name: "Hario V60"}, 33 + {RKey: "brewer2", Name: "AeroPress"}, 34 + }, 35 + "Roasters": []*models.Roaster{ 36 + {RKey: "roaster1", Name: "Blue Bottle"}, 37 + {RKey: "roaster2", Name: "Counter Culture"}, 38 + }, 39 + }, 40 + }, 41 + { 42 + name: "new brew with empty selects", 43 + data: map[string]interface{}{ 44 + "Brew": nil, 45 + "Beans": []*models.Bean{}, 46 + "Grinders": []*models.Grinder{}, 47 + "Brewers": []*models.Brewer{}, 48 + "Roasters": []*models.Roaster{}, 49 + }, 50 + }, 51 + { 52 + name: "new brew with nil collections", 53 + data: map[string]interface{}{ 54 + "Brew": nil, 55 + "Beans": nil, 56 + "Grinders": nil, 57 + "Brewers": nil, 58 + "Roasters": nil, 59 + }, 60 + }, 61 + } 62 + 63 + tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles( 64 + "../../templates/brew_form.tmpl", 65 + "../../templates/partials/new_bean_form.tmpl", 66 + "../../templates/partials/new_grinder_form.tmpl", 67 + "../../templates/partials/new_brewer_form.tmpl", 68 + )) 69 + 70 + for _, tt := range tests { 71 + t.Run(tt.name, func(t *testing.T) { 72 + var buf bytes.Buffer 73 + err := tmpl.ExecuteTemplate(&buf, "content", tt.data) 74 + if err != nil { 75 + t.Fatalf("template execution failed: %v", err) 76 + } 77 + shutter.SnapString(t, tt.name, formatHTML(buf.String())) 78 + }) 79 + } 80 + } 81 + 82 + func TestBrewForm_EditBrew_Snapshot(t *testing.T) { 83 + timestamp := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC) 84 + 85 + tests := []struct { 86 + name string 87 + data map[string]interface{} 88 + }{ 89 + { 90 + name: "edit brew with complete data", 91 + data: map[string]interface{}{ 92 + "Brew": &BrewData{ 93 + Brew: &models.Brew{ 94 + RKey: "brew123", 95 + BeanRKey: "bean1", 96 + GrinderRKey: "grinder1", 97 + BrewerRKey: "brewer1", 98 + CoffeeAmount: 18, 99 + WaterAmount: 300, 100 + GrindSize: "18", 101 + Temperature: 93.5, 102 + TimeSeconds: 180, 103 + TastingNotes: "Bright citrus notes with floral aroma. Clean finish.", 104 + Rating: 8, 105 + CreatedAt: timestamp, 106 + Pours: []*models.Pour{ 107 + {PourNumber: 1, WaterAmount: 50, TimeSeconds: 30}, 108 + {PourNumber: 2, WaterAmount: 100, TimeSeconds: 45}, 109 + {PourNumber: 3, WaterAmount: 150, TimeSeconds: 60}, 110 + }, 111 + }, 112 + PoursJSON: `[{"pourNumber":1,"waterAmount":50,"timeSeconds":30},{"pourNumber":2,"waterAmount":100,"timeSeconds":45},{"pourNumber":3,"waterAmount":150,"timeSeconds":60}]`, 113 + }, 114 + "Beans": []*models.Bean{ 115 + {RKey: "bean1", Name: "Ethiopian Yirgacheffe", Origin: "Ethiopia", RoastLevel: "Light"}, 116 + {RKey: "bean2", Name: "Colombian Supremo", Origin: "Colombia", RoastLevel: "Medium"}, 117 + }, 118 + "Grinders": []*models.Grinder{ 119 + {RKey: "grinder1", Name: "Baratza Encore"}, 120 + {RKey: "grinder2", Name: "Comandante C40"}, 121 + }, 122 + "Brewers": []*models.Brewer{ 123 + {RKey: "brewer1", Name: "Hario V60"}, 124 + {RKey: "brewer2", Name: "AeroPress"}, 125 + }, 126 + "Roasters": []*models.Roaster{ 127 + {RKey: "roaster1", Name: "Blue Bottle"}, 128 + }, 129 + }, 130 + }, 131 + { 132 + name: "edit brew with minimal data", 133 + data: map[string]interface{}{ 134 + "Brew": &BrewData{ 135 + Brew: &models.Brew{ 136 + RKey: "brew456", 137 + BeanRKey: "bean1", 138 + Rating: 5, 139 + CreatedAt: timestamp, 140 + Pours: nil, 141 + }, 142 + PoursJSON: "", 143 + }, 144 + "Beans": []*models.Bean{ 145 + {RKey: "bean1", Name: "House Blend", Origin: "Brazil", RoastLevel: "Medium"}, 146 + }, 147 + "Grinders": nil, 148 + "Brewers": nil, 149 + "Roasters": nil, 150 + }, 151 + }, 152 + { 153 + name: "edit brew with pours json", 154 + data: map[string]interface{}{ 155 + "Brew": &BrewData{ 156 + Brew: &models.Brew{ 157 + RKey: "brew789", 158 + BeanRKey: "bean1", 159 + Rating: 7, 160 + CreatedAt: timestamp, 161 + Pours: []*models.Pour{ 162 + {PourNumber: 1, WaterAmount: 60, TimeSeconds: 30}, 163 + {PourNumber: 2, WaterAmount: 120, TimeSeconds: 60}, 164 + }, 165 + }, 166 + PoursJSON: `[{"pourNumber":1,"waterAmount":60,"timeSeconds":30},{"pourNumber":2,"waterAmount":120,"timeSeconds":60}]`, 167 + }, 168 + "Beans": []*models.Bean{ 169 + {RKey: "bean1", Origin: "Kenya", RoastLevel: "Light"}, 170 + }, 171 + "Grinders": []*models.Grinder{}, 172 + "Brewers": []*models.Brewer{}, 173 + "Roasters": []*models.Roaster{}, 174 + }, 175 + }, 176 + { 177 + name: "edit brew without loaded collections", 178 + data: map[string]interface{}{ 179 + "Brew": &BrewData{ 180 + Brew: &models.Brew{ 181 + RKey: "brew999", 182 + BeanRKey: "bean1", 183 + GrinderRKey: "grinder1", 184 + BrewerRKey: "brewer1", 185 + Rating: 6, 186 + CreatedAt: timestamp, 187 + }, 188 + PoursJSON: "", 189 + }, 190 + "Beans": nil, 191 + "Grinders": nil, 192 + "Brewers": nil, 193 + "Roasters": nil, 194 + }, 195 + }, 196 + } 197 + 198 + tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles( 199 + "../../templates/brew_form.tmpl", 200 + "../../templates/partials/new_bean_form.tmpl", 201 + "../../templates/partials/new_grinder_form.tmpl", 202 + "../../templates/partials/new_brewer_form.tmpl", 203 + )) 204 + 205 + for _, tt := range tests { 206 + t.Run(tt.name, func(t *testing.T) { 207 + var buf bytes.Buffer 208 + err := tmpl.ExecuteTemplate(&buf, "content", tt.data) 209 + if err != nil { 210 + t.Fatalf("template execution failed: %v", err) 211 + } 212 + shutter.SnapString(t, tt.name, formatHTML(buf.String())) 213 + }) 214 + } 215 + } 216 + 217 + func TestNewBeanForm_Snapshot(t *testing.T) { 218 + tests := []struct { 219 + name string 220 + data map[string]interface{} 221 + }{ 222 + { 223 + name: "bean form with roasters", 224 + data: map[string]interface{}{ 225 + "Roasters": []*models.Roaster{ 226 + {RKey: "roaster1", Name: "Blue Bottle Coffee"}, 227 + {RKey: "roaster2", Name: "Counter Culture Coffee"}, 228 + {RKey: "roaster3", Name: "Stumptown Coffee Roasters"}, 229 + }, 230 + }, 231 + }, 232 + { 233 + name: "bean form without roasters", 234 + data: map[string]interface{}{ 235 + "Roasters": []*models.Roaster{}, 236 + }, 237 + }, 238 + { 239 + name: "bean form with nil roasters", 240 + data: map[string]interface{}{ 241 + "Roasters": nil, 242 + }, 243 + }, 244 + } 245 + 246 + tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles( 247 + "../../templates/partials/new_bean_form.tmpl", 248 + )) 249 + 250 + for _, tt := range tests { 251 + t.Run(tt.name, func(t *testing.T) { 252 + var buf bytes.Buffer 253 + err := tmpl.ExecuteTemplate(&buf, "new_bean_form", tt.data) 254 + if err != nil { 255 + t.Fatalf("template execution failed: %v", err) 256 + } 257 + shutter.SnapString(t, tt.name, formatHTML(buf.String())) 258 + }) 259 + } 260 + } 261 + 262 + func TestNewGrinderForm_Snapshot(t *testing.T) { 263 + tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles( 264 + "../../templates/partials/new_grinder_form.tmpl", 265 + )) 266 + 267 + t.Run("grinder form renders", func(t *testing.T) { 268 + var buf bytes.Buffer 269 + err := tmpl.ExecuteTemplate(&buf, "new_grinder_form", nil) 270 + if err != nil { 271 + t.Fatalf("template execution failed: %v", err) 272 + } 273 + shutter.SnapString(t, "grinder_form_renders", formatHTML(buf.String())) 274 + }) 275 + } 276 + 277 + func TestNewBrewerForm_Snapshot(t *testing.T) { 278 + tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles( 279 + "../../templates/partials/new_brewer_form.tmpl", 280 + )) 281 + 282 + t.Run("brewer form renders", func(t *testing.T) { 283 + var buf bytes.Buffer 284 + err := tmpl.ExecuteTemplate(&buf, "new_brewer_form", nil) 285 + if err != nil { 286 + t.Fatalf("template execution failed: %v", err) 287 + } 288 + shutter.SnapString(t, "brewer_form_renders", formatHTML(buf.String())) 289 + }) 290 + } 291 + 292 + func TestNewRoasterForm_Snapshot(t *testing.T) { 293 + tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles( 294 + "../../templates/partials/new_roaster_form.tmpl", 295 + )) 296 + 297 + t.Run("roaster form renders", func(t *testing.T) { 298 + var buf bytes.Buffer 299 + err := tmpl.ExecuteTemplate(&buf, "new_roaster_form", nil) 300 + if err != nil { 301 + t.Fatalf("template execution failed: %v", err) 302 + } 303 + shutter.SnapString(t, "roaster_form_renders", formatHTML(buf.String())) 304 + }) 305 + } 306 + 307 + func TestBrewForm_SpecialCharacters_Snapshot(t *testing.T) { 308 + timestamp := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC) 309 + 310 + tests := []struct { 311 + name string 312 + data map[string]interface{} 313 + }{ 314 + { 315 + name: "brew with html in tasting notes", 316 + data: map[string]interface{}{ 317 + "Brew": &BrewData{ 318 + Brew: &models.Brew{ 319 + RKey: "brew1", 320 + BeanRKey: "bean1", 321 + TastingNotes: "<script>alert('xss')</script>Bright & fruity, \"amazing\" taste", 322 + Rating: 8, 323 + CreatedAt: timestamp, 324 + }, 325 + PoursJSON: "", 326 + }, 327 + "Beans": []*models.Bean{ 328 + {RKey: "bean1", Name: "Test <strong>Bean</strong>", Origin: "Ethiopia", RoastLevel: "Light"}, 329 + }, 330 + "Grinders": []*models.Grinder{}, 331 + "Brewers": []*models.Brewer{}, 332 + "Roasters": []*models.Roaster{}, 333 + }, 334 + }, 335 + { 336 + name: "brew with unicode characters", 337 + data: map[string]interface{}{ 338 + "Brew": &BrewData{ 339 + Brew: &models.Brew{ 340 + RKey: "brew2", 341 + BeanRKey: "bean1", 342 + TastingNotes: "日本のコーヒー 🇯🇵 - フルーティーで酸味が強い\n\nЯркий вкус с цитрусовыми нотами\n\nCafé con notas de caramelo", 343 + GrindSize: "中挽き (medium)", 344 + Rating: 9, 345 + CreatedAt: timestamp, 346 + }, 347 + PoursJSON: "", 348 + }, 349 + "Beans": []*models.Bean{ 350 + {RKey: "bean1", Name: "Café Especial™", Origin: "Costa Rica", RoastLevel: "Medium"}, 351 + }, 352 + "Grinders": []*models.Grinder{ 353 + {RKey: "grinder1", Name: "Comandante® C40 MK3"}, 354 + }, 355 + "Brewers": []*models.Brewer{ 356 + {RKey: "brewer1", Name: "Hario V60 (02)"}, 357 + }, 358 + "Roasters": []*models.Roaster{}, 359 + }, 360 + }, 361 + } 362 + 363 + tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles( 364 + "../../templates/brew_form.tmpl", 365 + "../../templates/partials/new_bean_form.tmpl", 366 + "../../templates/partials/new_grinder_form.tmpl", 367 + "../../templates/partials/new_brewer_form.tmpl", 368 + )) 369 + 370 + for _, tt := range tests { 371 + t.Run(tt.name, func(t *testing.T) { 372 + var buf bytes.Buffer 373 + err := tmpl.ExecuteTemplate(&buf, "content", tt.data) 374 + if err != nil { 375 + t.Fatalf("template execution failed: %v", err) 376 + } 377 + shutter.SnapString(t, tt.name, formatHTML(buf.String())) 378 + }) 379 + } 380 + }
+380
internal/bff/partial_template_snapshot_test.go
··· 1 + package bff 2 + 3 + import ( 4 + "bytes" 5 + "html/template" 6 + "testing" 7 + "time" 8 + 9 + "arabica/internal/models" 10 + 11 + "github.com/ptdewey/shutter" 12 + ) 13 + 14 + func TestBrewListContent_Snapshot(t *testing.T) { 15 + timestamp := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC) 16 + 17 + tests := []struct { 18 + name string 19 + data map[string]interface{} 20 + }{ 21 + { 22 + name: "empty brew list own profile", 23 + data: map[string]interface{}{ 24 + "Brews": []*models.Brew{}, 25 + "IsOwnProfile": true, 26 + }, 27 + }, 28 + { 29 + name: "empty brew list other profile", 30 + data: map[string]interface{}{ 31 + "Brews": []*models.Brew{}, 32 + "IsOwnProfile": false, 33 + }, 34 + }, 35 + { 36 + name: "brew list with complete data", 37 + data: map[string]interface{}{ 38 + "Brews": []*models.Brew{ 39 + { 40 + RKey: "brew1", 41 + BeanRKey: "bean1", 42 + CoffeeAmount: 18, 43 + WaterAmount: 250, 44 + Temperature: 93.0, 45 + TimeSeconds: 180, 46 + GrindSize: "Medium-fine", 47 + Rating: 8, 48 + TastingNotes: "Bright citrus notes with floral aroma. Clean finish.", 49 + CreatedAt: timestamp, 50 + Bean: &models.Bean{ 51 + Name: "Ethiopian Yirgacheffe", 52 + Origin: "Ethiopia", 53 + RoastLevel: "Light", 54 + Roaster: &models.Roaster{ 55 + Name: "Onyx Coffee Lab", 56 + }, 57 + }, 58 + GrinderObj: &models.Grinder{ 59 + Name: "Comandante C40", 60 + }, 61 + BrewerObj: &models.Brewer{ 62 + Name: "Hario V60", 63 + }, 64 + Pours: []*models.Pour{ 65 + {PourNumber: 1, WaterAmount: 50, TimeSeconds: 30}, 66 + {PourNumber: 2, WaterAmount: 100, TimeSeconds: 45}, 67 + {PourNumber: 3, WaterAmount: 100, TimeSeconds: 60}, 68 + }, 69 + }, 70 + { 71 + RKey: "brew2", 72 + BeanRKey: "bean2", 73 + Rating: 6, 74 + CreatedAt: timestamp.Add(-24 * time.Hour), 75 + Bean: &models.Bean{ 76 + Origin: "Colombia", 77 + RoastLevel: "Medium", 78 + }, 79 + Method: "AeroPress", 80 + }, 81 + }, 82 + "IsOwnProfile": true, 83 + }, 84 + }, 85 + { 86 + name: "brew list minimal data", 87 + data: map[string]interface{}{ 88 + "Brews": []*models.Brew{ 89 + { 90 + RKey: "brew3", 91 + BeanRKey: "bean3", 92 + CreatedAt: timestamp, 93 + }, 94 + }, 95 + "IsOwnProfile": false, 96 + }, 97 + }, 98 + } 99 + 100 + tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles( 101 + "../../templates/partials/brew_list_content.tmpl", 102 + )) 103 + 104 + for _, tt := range tests { 105 + t.Run(tt.name, func(t *testing.T) { 106 + var buf bytes.Buffer 107 + err := tmpl.ExecuteTemplate(&buf, "brew_list_content", tt.data) 108 + if err != nil { 109 + t.Fatalf("template execution failed: %v", err) 110 + } 111 + shutter.SnapString(t, tt.name, formatHTML(buf.String())) 112 + }) 113 + } 114 + } 115 + 116 + func TestManageContent_BeansTab_Snapshot(t *testing.T) { 117 + tests := []struct { 118 + name string 119 + data map[string]interface{} 120 + }{ 121 + { 122 + name: "beans empty", 123 + data: map[string]interface{}{ 124 + "Beans": []*models.Bean{}, 125 + "Roasters": []*models.Roaster{}, 126 + }, 127 + }, 128 + { 129 + name: "beans with roaster", 130 + data: map[string]interface{}{ 131 + "Beans": []*models.Bean{ 132 + { 133 + RKey: "bean1", 134 + Name: "Ethiopian Yirgacheffe", 135 + Origin: "Ethiopia", 136 + RoastLevel: "Light", 137 + Process: "Washed", 138 + Description: "Bright and fruity with notes of blueberry", 139 + RoasterRKey: "roaster1", 140 + Roaster: &models.Roaster{ 141 + RKey: "roaster1", 142 + Name: "Onyx Coffee Lab", 143 + }, 144 + }, 145 + { 146 + RKey: "bean2", 147 + Origin: "Colombia", 148 + RoastLevel: "Medium", 149 + }, 150 + }, 151 + "Roasters": []*models.Roaster{ 152 + {RKey: "roaster1", Name: "Onyx Coffee Lab"}, 153 + {RKey: "roaster2", Name: "Counter Culture"}, 154 + }, 155 + }, 156 + }, 157 + } 158 + 159 + tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles( 160 + "../../templates/partials/manage_content.tmpl", 161 + )) 162 + 163 + for _, tt := range tests { 164 + t.Run(tt.name, func(t *testing.T) { 165 + var buf bytes.Buffer 166 + err := tmpl.ExecuteTemplate(&buf, "manage_content", tt.data) 167 + if err != nil { 168 + t.Fatalf("template execution failed: %v", err) 169 + } 170 + shutter.SnapString(t, tt.name, formatHTML(buf.String())) 171 + }) 172 + } 173 + } 174 + 175 + func TestManageContent_RoastersTab_Snapshot(t *testing.T) { 176 + tests := []struct { 177 + name string 178 + data map[string]interface{} 179 + }{ 180 + { 181 + name: "roasters empty", 182 + data: map[string]interface{}{ 183 + "Roasters": []*models.Roaster{}, 184 + }, 185 + }, 186 + { 187 + name: "roasters with data", 188 + data: map[string]interface{}{ 189 + "Roasters": []*models.Roaster{ 190 + { 191 + RKey: "roaster1", 192 + Name: "Onyx Coffee Lab", 193 + Location: "Bentonville, AR", 194 + Website: "https://onyxcoffeelab.com", 195 + }, 196 + { 197 + RKey: "roaster2", 198 + Name: "Counter Culture Coffee", 199 + }, 200 + }, 201 + }, 202 + }, 203 + { 204 + name: "roasters with unsafe url", 205 + data: map[string]interface{}{ 206 + "Roasters": []*models.Roaster{ 207 + { 208 + RKey: "roaster1", 209 + Name: "Test Roaster", 210 + Location: "Test Location", 211 + Website: "javascript:alert('xss')", 212 + }, 213 + }, 214 + }, 215 + }, 216 + } 217 + 218 + tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles( 219 + "../../templates/partials/manage_content.tmpl", 220 + )) 221 + 222 + for _, tt := range tests { 223 + t.Run(tt.name, func(t *testing.T) { 224 + var buf bytes.Buffer 225 + err := tmpl.ExecuteTemplate(&buf, "manage_content", tt.data) 226 + if err != nil { 227 + t.Fatalf("template execution failed: %v", err) 228 + } 229 + shutter.SnapString(t, tt.name, formatHTML(buf.String())) 230 + }) 231 + } 232 + } 233 + 234 + func TestManageContent_GrindersTab_Snapshot(t *testing.T) { 235 + tests := []struct { 236 + name string 237 + data map[string]interface{} 238 + }{ 239 + { 240 + name: "grinders empty", 241 + data: map[string]interface{}{ 242 + "Grinders": []*models.Grinder{}, 243 + }, 244 + }, 245 + { 246 + name: "grinders with data", 247 + data: map[string]interface{}{ 248 + "Grinders": []*models.Grinder{ 249 + { 250 + RKey: "grinder1", 251 + Name: "Comandante C40 MK3", 252 + GrinderType: "Hand", 253 + BurrType: "Conical", 254 + Notes: "Excellent consistency, great for pour-over", 255 + }, 256 + { 257 + RKey: "grinder2", 258 + Name: "Baratza Encore", 259 + GrinderType: "Electric", 260 + BurrType: "Conical", 261 + }, 262 + }, 263 + }, 264 + }, 265 + } 266 + 267 + tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles( 268 + "../../templates/partials/manage_content.tmpl", 269 + )) 270 + 271 + for _, tt := range tests { 272 + t.Run(tt.name, func(t *testing.T) { 273 + var buf bytes.Buffer 274 + err := tmpl.ExecuteTemplate(&buf, "manage_content", tt.data) 275 + if err != nil { 276 + t.Fatalf("template execution failed: %v", err) 277 + } 278 + shutter.SnapString(t, tt.name, formatHTML(buf.String())) 279 + }) 280 + } 281 + } 282 + 283 + func TestManageContent_BrewersTab_Snapshot(t *testing.T) { 284 + tests := []struct { 285 + name string 286 + data map[string]interface{} 287 + }{ 288 + { 289 + name: "brewers empty", 290 + data: map[string]interface{}{ 291 + "Brewers": []*models.Brewer{}, 292 + }, 293 + }, 294 + { 295 + name: "brewers with data", 296 + data: map[string]interface{}{ 297 + "Brewers": []*models.Brewer{ 298 + { 299 + RKey: "brewer1", 300 + Name: "Hario V60", 301 + BrewerType: "Pour-Over", 302 + Description: "Cone-shaped dripper for clean, bright brews", 303 + }, 304 + { 305 + RKey: "brewer2", 306 + Name: "AeroPress", 307 + }, 308 + }, 309 + }, 310 + }, 311 + } 312 + 313 + tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles( 314 + "../../templates/partials/manage_content.tmpl", 315 + )) 316 + 317 + for _, tt := range tests { 318 + t.Run(tt.name, func(t *testing.T) { 319 + var buf bytes.Buffer 320 + err := tmpl.ExecuteTemplate(&buf, "manage_content", tt.data) 321 + if err != nil { 322 + t.Fatalf("template execution failed: %v", err) 323 + } 324 + shutter.SnapString(t, tt.name, formatHTML(buf.String())) 325 + }) 326 + } 327 + } 328 + 329 + func TestManageContent_SpecialCharacters_Snapshot(t *testing.T) { 330 + tests := []struct { 331 + name string 332 + data map[string]interface{} 333 + }{ 334 + { 335 + name: "beans with special characters and html", 336 + data: map[string]interface{}{ 337 + "Beans": []*models.Bean{ 338 + { 339 + RKey: "bean1", 340 + Name: "Café <script>alert('xss')</script> Especial", 341 + Origin: "Costa Rica™", 342 + RoastLevel: "Medium", 343 + Process: "Honey & Washed", 344 + Description: "\"Amazing\" coffee with <strong>bold</strong> flavor", 345 + }, 346 + }, 347 + "Roasters": []*models.Roaster{}, 348 + }, 349 + }, 350 + { 351 + name: "grinders with unicode", 352 + data: map[string]interface{}{ 353 + "Grinders": []*models.Grinder{ 354 + { 355 + RKey: "grinder1", 356 + Name: "手動コーヒーミル Comandante® C40", 357 + GrinderType: "Hand", 358 + BurrType: "Conical", 359 + Notes: "日本語のノート - Отличная кофемолка 🇯🇵", 360 + }, 361 + }, 362 + }, 363 + }, 364 + } 365 + 366 + tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles( 367 + "../../templates/partials/manage_content.tmpl", 368 + )) 369 + 370 + for _, tt := range tests { 371 + t.Run(tt.name, func(t *testing.T) { 372 + var buf bytes.Buffer 373 + err := tmpl.ExecuteTemplate(&buf, "manage_content", tt.data) 374 + if err != nil { 375 + t.Fatalf("template execution failed: %v", err) 376 + } 377 + shutter.SnapString(t, tt.name, formatHTML(buf.String())) 378 + }) 379 + } 380 + }
+5 -5
internal/bff/profile_template_snapshot_test.go
··· 102 102 if err != nil { 103 103 t.Fatalf("Failed to execute template: %v", err) 104 104 } 105 - shutter.Snap(t, tt.name, buf.String()) 105 + shutter.SnapString(t, tt.name, formatHTML(buf.String())) 106 106 }) 107 107 } 108 108 } ··· 165 165 if err != nil { 166 166 t.Fatalf("Failed to execute template: %v", err) 167 167 } 168 - shutter.Snap(t, "profile with gear collection", buf.String()) 168 + shutter.SnapString(t, "profile with gear collection", formatHTML(buf.String())) 169 169 } 170 170 171 171 func TestProfileContent_URLSecurity_Snapshot(t *testing.T) { ··· 230 230 if err != nil { 231 231 t.Fatalf("Failed to execute template: %v", err) 232 232 } 233 - shutter.Snap(t, tt.name, buf.String()) 233 + shutter.SnapString(t, tt.name, formatHTML(buf.String())) 234 234 }) 235 235 } 236 236 } ··· 276 276 if err != nil { 277 277 t.Fatalf("Failed to execute template: %v", err) 278 278 } 279 - shutter.Snap(t, "profile with special characters", buf.String()) 279 + shutter.SnapString(t, "profile with special characters", formatHTML(buf.String())) 280 280 } 281 281 282 282 func TestProfileContent_Unicode_Snapshot(t *testing.T) { ··· 327 327 if err != nil { 328 328 t.Fatalf("Failed to execute template: %v", err) 329 329 } 330 - shutter.Snap(t, "profile with unicode content", buf.String()) 330 + shutter.SnapString(t, "profile with unicode content", formatHTML(buf.String())) 331 331 }
+57
internal/bff/testutil.go
··· 1 + package bff 2 + 3 + import ( 4 + "bytes" 5 + "strings" 6 + 7 + "github.com/yosssi/gohtml" 8 + ) 9 + 10 + // formatHTML formats HTML for snapshot testing with 2-space indentation 11 + func formatHTML(html string) string { 12 + // Configure gohtml for 2-space indentation 13 + formatted := gohtml.Format(html) 14 + 15 + // Post-process to ensure consistent formatting: 16 + // 1. Remove excessive blank lines 17 + lines := strings.Split(formatted, "\n") 18 + var result []string 19 + prevBlank := false 20 + 21 + for _, line := range lines { 22 + isBlank := strings.TrimSpace(line) == "" 23 + if isBlank && prevBlank { 24 + // Skip consecutive blank lines 25 + continue 26 + } 27 + result = append(result, line) 28 + prevBlank = isBlank 29 + } 30 + 31 + // Join and trim 32 + output := strings.Join(result, "\n") 33 + output = strings.TrimSpace(output) 34 + 35 + return output 36 + } 37 + 38 + // execTemplate is a helper for executing templates and formatting the output 39 + func execTemplate(tmpl interface{}, templateName string, data interface{}) (string, error) { 40 + var buf bytes.Buffer 41 + 42 + type executor interface { 43 + ExecuteTemplate(*bytes.Buffer, string, interface{}) error 44 + } 45 + 46 + t, ok := tmpl.(executor) 47 + if !ok { 48 + panic("template does not implement ExecuteTemplate") 49 + } 50 + 51 + err := t.ExecuteTemplate(&buf, templateName, data) 52 + if err != nil { 53 + return "", err 54 + } 55 + 56 + return formatHTML(buf.String()), nil 57 + }