tangled
alpha
login
or
join now
arabica.social
/
arabica
7
fork
atom
Coffee journaling on ATProto (alpha)
alpha.arabica.social
coffee
7
fork
atom
overview
issues
pulls
pipelines
feat: snapshot testing improvements
pdewey.com
1 month ago
918a7566
3ad8ad2e
verified
This commit was signed with the committer's
known signature
.
pdewey.com
SSH Key Fingerprint:
SHA256:ePOVkJstqVLchGK8m9/OGQG+aFNHD5XN3xjvW9wKCA4=
+9155
-37
60 changed files
expand all
collapse all
unified
split
go.mod
go.sum
internal
bff
__snapshots__
bean_form_with_nil_roasters.snap
bean_form_with_roasters.snap
bean_form_without_roasters.snap
bean_with_missing_optional_fields.snap
bean_with_roaster.snap
bean_without_roaster.snap
beans_empty.snap
beans_with_roaster.snap
beans_with_special_characters_and_html.snap
brew_list_minimal_data.snap
brew_list_with_complete_data.snap
brew_with_html_in_tasting_notes.snap
brew_with_minimal_data.snap
brew_with_unicode_bean_name.snap
brew_with_unicode_characters.snap
brewer_form_renders.snap
brewer_item.snap
brewers_empty.snap
brewers_with_data.snap
complete_brew_with_all_fields.snap
edit_brew_with_complete_data.snap
edit_brew_with_minimal_data.snap
edit_brew_with_pours_json.snap
edit_brew_without_loaded_collections.snap
empty_brew_list_other_profile.snap
empty_brew_list_own_profile.snap
empty_feed_authenticated.snap
empty_feed_unauthenticated.snap
grinder_form_renders.snap
grinder_item.snap
grinders_empty.snap
grinders_with_data.snap
grinders_with_unicode.snap
mixed_feed_all_types.snap
new_brew_with_empty_selects.snap
new_brew_with_nil_collections.snap
new_brew_with_populated_selects.snap
nil_feed.snap
profile_roaster_with_invalid_url_protocol.snap
profile_roaster_with_unsafe_website_url.snap
profile_with_empty_beans.snap
profile_with_gear_collection.snap
profile_with_multiple_beans.snap
profile_with_special_characters.snap
profile_with_unicode_content.snap
profile_with_unsafe_avatar_url.snap
roaster_form_renders.snap
roaster_item.snap
roaster_with_unsafe_website_url.snap
roasters_empty.snap
roasters_with_data.snap
roasters_with_unsafe_url.snap
special_characters_in_content.snap
feed_template_snapshot_test.go
form_template_snapshot_test.go
partial_template_snapshot_test.go
profile_template_snapshot_test.go
testutil.go
+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
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
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
82
+
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
83
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
92
+
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
93
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
1
+
---
2
2
+
title: bean form with nil roasters
3
3
+
test_name: TestNewBeanForm_Snapshot/bean_form_with_nil_roasters
4
4
+
file_name: form_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
8
8
+
<h4 class="font-medium mb-3 text-gray-800">
9
9
+
Add New Bean
10
10
+
</h4>
11
11
+
<div class="space-y-3">
12
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
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
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
15
+
<option value="">
16
16
+
Select Roaster (Optional)
17
17
+
</option>
18
18
+
</select>
19
19
+
<select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
20
20
+
<option value="">
21
21
+
Select Roast Level (Optional)
22
22
+
</option>
23
23
+
<option value="Ultra-Light">
24
24
+
Ultra-Light
25
25
+
</option>
26
26
+
<option value="Light">
27
27
+
Light
28
28
+
</option>
29
29
+
<option value="Medium-Light">
30
30
+
Medium-Light
31
31
+
</option>
32
32
+
<option value="Medium">
33
33
+
Medium
34
34
+
</option>
35
35
+
<option value="Medium-Dark">
36
36
+
Medium-Dark
37
37
+
</option>
38
38
+
<option value="Dark">
39
39
+
Dark
40
40
+
</option>
41
41
+
</select>
42
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
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
44
+
<div class="flex gap-2">
45
45
+
<button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
46
46
+
Add
47
47
+
</button>
48
48
+
<button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
49
49
+
Cancel
50
50
+
</button>
51
51
+
</div>
52
52
+
</div>
53
53
+
</div>
+62
internal/bff/__snapshots__/bean_form_with_roasters.snap
···
1
1
+
---
2
2
+
title: bean form with roasters
3
3
+
test_name: TestNewBeanForm_Snapshot/bean_form_with_roasters
4
4
+
file_name: form_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
8
8
+
<h4 class="font-medium mb-3 text-gray-800">
9
9
+
Add New Bean
10
10
+
</h4>
11
11
+
<div class="space-y-3">
12
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
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
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
15
+
<option value="">
16
16
+
Select Roaster (Optional)
17
17
+
</option>
18
18
+
<option value="roaster1">
19
19
+
Blue Bottle Coffee
20
20
+
</option>
21
21
+
<option value="roaster2">
22
22
+
Counter Culture Coffee
23
23
+
</option>
24
24
+
<option value="roaster3">
25
25
+
Stumptown Coffee Roasters
26
26
+
</option>
27
27
+
</select>
28
28
+
<select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
29
29
+
<option value="">
30
30
+
Select Roast Level (Optional)
31
31
+
</option>
32
32
+
<option value="Ultra-Light">
33
33
+
Ultra-Light
34
34
+
</option>
35
35
+
<option value="Light">
36
36
+
Light
37
37
+
</option>
38
38
+
<option value="Medium-Light">
39
39
+
Medium-Light
40
40
+
</option>
41
41
+
<option value="Medium">
42
42
+
Medium
43
43
+
</option>
44
44
+
<option value="Medium-Dark">
45
45
+
Medium-Dark
46
46
+
</option>
47
47
+
<option value="Dark">
48
48
+
Dark
49
49
+
</option>
50
50
+
</select>
51
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
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
53
+
<div class="flex gap-2">
54
54
+
<button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
55
55
+
Add
56
56
+
</button>
57
57
+
<button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
58
58
+
Cancel
59
59
+
</button>
60
60
+
</div>
61
61
+
</div>
62
62
+
</div>
+53
internal/bff/__snapshots__/bean_form_without_roasters.snap
···
1
1
+
---
2
2
+
title: bean form without roasters
3
3
+
test_name: TestNewBeanForm_Snapshot/bean_form_without_roasters
4
4
+
file_name: form_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
8
8
+
<h4 class="font-medium mb-3 text-gray-800">
9
9
+
Add New Bean
10
10
+
</h4>
11
11
+
<div class="space-y-3">
12
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
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
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
15
+
<option value="">
16
16
+
Select Roaster (Optional)
17
17
+
</option>
18
18
+
</select>
19
19
+
<select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
20
20
+
<option value="">
21
21
+
Select Roast Level (Optional)
22
22
+
</option>
23
23
+
<option value="Ultra-Light">
24
24
+
Ultra-Light
25
25
+
</option>
26
26
+
<option value="Light">
27
27
+
Light
28
28
+
</option>
29
29
+
<option value="Medium-Light">
30
30
+
Medium-Light
31
31
+
</option>
32
32
+
<option value="Medium">
33
33
+
Medium
34
34
+
</option>
35
35
+
<option value="Medium-Dark">
36
36
+
Medium-Dark
37
37
+
</option>
38
38
+
<option value="Dark">
39
39
+
Dark
40
40
+
</option>
41
41
+
</select>
42
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
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
44
+
<div class="flex gap-2">
45
45
+
<button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
46
46
+
Add
47
47
+
</button>
48
48
+
<button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
49
49
+
Cancel
50
50
+
</button>
51
51
+
</div>
52
52
+
</div>
53
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
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
7
+
<div id="profile-stats-data"
8
8
+
data-brews="0"
9
9
+
data-beans="1"
10
10
+
data-roasters="0"
11
11
+
data-grinders="0"
12
12
+
data-brewers="0"
13
13
+
style="display: none;"></div>
14
14
+
<div x-show="activeTab === 'brews'">
15
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
16
+
<p class="text-brown-800 text-lg mb-4 font-medium">
17
17
+
No brews yet! Start tracking your coffee journey.
18
18
+
</p>
19
19
+
<a href="/brews/new"
20
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
21
+
Add Your First Brew
22
22
+
</a>
23
23
+
</div>
24
24
+
</div>
25
25
+
<div x-show="activeTab === 'beans'" x-cloak class="space-y-6">
26
26
+
<div>
27
27
+
<h3 class="text-lg font-semibold text-brown-900 mb-3">
28
28
+
☕ Coffee Beans
29
29
+
</h3>
30
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
31
+
<table class="min-w-full divide-y divide-brown-300">
32
32
+
<thead class="bg-brown-200/80">
33
33
+
<tr>
34
34
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
35
35
+
Name
36
36
+
</th>
37
37
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
38
38
+
☕ Roaster
39
39
+
</th>
40
40
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
41
41
+
📍 Origin
42
42
+
</th>
43
43
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
44
44
+
🔥 Roast
45
45
+
</th>
46
46
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
47
47
+
🌱 Process
48
48
+
</th>
49
49
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
50
50
+
📝 Description
51
51
+
</th>
52
52
+
</tr>
53
53
+
</thead>
54
54
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
55
55
+
<tr class="hover:bg-brown-100/60 transition-colors">
56
56
+
<td class="px-6 py-4 text-sm font-bold text-brown-900">
57
57
+
Mystery Bean
58
58
+
</td>
59
59
+
<td class="px-6 py-4 text-sm text-brown-900">
60
60
+
<span class="text-brown-400">
61
61
+
-
62
62
+
</span>
63
63
+
</td>
64
64
+
<td class="px-6 py-4 text-sm text-brown-900">
65
65
+
<span class="text-brown-400">
66
66
+
-
67
67
+
</span>
68
68
+
</td>
69
69
+
<td class="px-6 py-4 text-sm text-brown-900">
70
70
+
<span class="text-brown-400">
71
71
+
-
72
72
+
</span>
73
73
+
</td>
74
74
+
<td class="px-6 py-4 text-sm text-brown-900">
75
75
+
<span class="text-brown-400">
76
76
+
-
77
77
+
</span>
78
78
+
</td>
79
79
+
<td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs">
80
80
+
<span class="text-brown-400 not-italic">
81
81
+
-
82
82
+
</span>
83
83
+
</td>
84
84
+
</tr>
85
85
+
</tbody>
86
86
+
</table>
87
87
+
</div>
88
88
+
<div class="mt-3 text-center">
89
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
90
+
<span>
91
91
+
+
92
92
+
</span>
93
93
+
<span>
94
94
+
Add New Bean
95
95
+
</span>
96
96
+
</button>
97
97
+
</div>
98
98
+
</div>
99
99
+
</div>
100
100
+
<div x-show="activeTab === 'gear'" x-cloak class="space-y-6">
101
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
102
+
<p class="font-medium">
103
103
+
No gear added yet.
104
104
+
</p>
105
105
+
</div>
106
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
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
7
+
<div class="space-y-4">
8
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
9
+
<div class="flex items-center gap-3 mb-3">
10
10
+
<a href="/profile/roaster.pro" class="flex-shrink-0">
11
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
12
+
<span class="text-brown-600 text-sm">
13
13
+
?
14
14
+
</span>
15
15
+
</div>
16
16
+
</a>
17
17
+
<div class="flex-1 min-w-0">
18
18
+
<div class="flex items-center gap-2">
19
19
+
<a href="/profile/roaster.pro" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline">
20
20
+
Pro Roaster
21
21
+
</a>
22
22
+
<a href="/profile/roaster.pro" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
23
23
+
@roaster.pro
24
24
+
</a>
25
25
+
</div>
26
26
+
<span class="text-brown-500 text-sm">
27
27
+
5 minutes ago
28
28
+
</span>
29
29
+
</div>
30
30
+
</div>
31
31
+
<div class="mb-2 text-sm text-brown-700">
32
32
+
🫘 added a new bean
33
33
+
</div>
34
34
+
<div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200">
35
35
+
<div class="text-base mb-2">
36
36
+
<span class="font-bold text-brown-900">
37
37
+
Kenya AA
38
38
+
</span>
39
39
+
<span class="text-brown-700">
40
40
+
from Onyx Coffee Lab
41
41
+
</span>
42
42
+
</div>
43
43
+
<div class="text-sm text-brown-700 space-y-1">
44
44
+
<div>
45
45
+
<span class="text-brown-600">
46
46
+
Origin:
47
47
+
</span>
48
48
+
Kenya
49
49
+
</div>
50
50
+
<div>
51
51
+
<span class="text-brown-600">
52
52
+
Roast:
53
53
+
</span>
54
54
+
Medium
55
55
+
</div>
56
56
+
<div>
57
57
+
<span class="text-brown-600">
58
58
+
Process:
59
59
+
</span>
60
60
+
Natural
61
61
+
</div>
62
62
+
<div class="mt-2 text-brown-800 italic">
63
63
+
"Sweet and fruity with notes of blueberry"
64
64
+
</div>
65
65
+
</div>
66
66
+
</div>
67
67
+
</div>
68
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
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
7
+
<div class="space-y-4">
8
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
9
+
<div class="flex items-center gap-3 mb-3">
10
10
+
<a href="/profile/homebrewer" class="flex-shrink-0">
11
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
12
+
<span class="text-brown-600 text-sm">
13
13
+
?
14
14
+
</span>
15
15
+
</div>
16
16
+
</a>
17
17
+
<div class="flex-1 min-w-0">
18
18
+
<div class="flex items-center gap-2">
19
19
+
<a href="/profile/homebrewer" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline">
20
20
+
Home Brewer
21
21
+
</a>
22
22
+
<a href="/profile/homebrewer" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
23
23
+
@homebrewer
24
24
+
</a>
25
25
+
</div>
26
26
+
<span class="text-brown-500 text-sm">
27
27
+
1 day ago
28
28
+
</span>
29
29
+
</div>
30
30
+
</div>
31
31
+
<div class="mb-2 text-sm text-brown-700">
32
32
+
🫘 added a new bean
33
33
+
</div>
34
34
+
<div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200">
35
35
+
<div class="text-base mb-2">
36
36
+
<span class="font-bold text-brown-900">
37
37
+
Colombian Supremo
38
38
+
</span>
39
39
+
</div>
40
40
+
<div class="text-sm text-brown-700 space-y-1">
41
41
+
<div>
42
42
+
<span class="text-brown-600">
43
43
+
Origin:
44
44
+
</span>
45
45
+
Colombia
46
46
+
</div>
47
47
+
<div>
48
48
+
<span class="text-brown-600">
49
49
+
Roast:
50
50
+
</span>
51
51
+
Medium
52
52
+
</div>
53
53
+
<div>
54
54
+
<span class="text-brown-600">
55
55
+
Process:
56
56
+
</span>
57
57
+
Natural
58
58
+
</div>
59
59
+
<div class="mt-2 text-brown-800 italic">
60
60
+
"Sweet and fruity with notes of blueberry"
61
61
+
</div>
62
62
+
</div>
63
63
+
</div>
64
64
+
</div>
65
65
+
</div>
+210
internal/bff/__snapshots__/beans_empty.snap
···
1
1
+
---
2
2
+
title: beans empty
3
3
+
test_name: TestManageContent_BeansTab_Snapshot/beans_empty
4
4
+
file_name: partial_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="tab === 'beans'">
8
8
+
<div class="mb-4 flex justify-between items-center">
9
9
+
<h3 class="text-xl font-semibold text-brown-900">
10
10
+
Coffee Beans
11
11
+
</h3>
12
12
+
<button
13
13
+
@click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}"
14
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
15
+
+ Add Bean
16
16
+
</button>
17
17
+
</div>
18
18
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
19
19
+
No beans yet. Add your first bean to get started!
20
20
+
</div>
21
21
+
</div>
22
22
+
<div x-show="tab === 'roasters'">
23
23
+
<div class="mb-4 flex justify-between items-center">
24
24
+
<h3 class="text-xl font-semibold text-brown-900">
25
25
+
Roasters
26
26
+
</h3>
27
27
+
<button
28
28
+
@click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}"
29
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
30
+
+ Add Roaster
31
31
+
</button>
32
32
+
</div>
33
33
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
34
34
+
No roasters yet. Add your first roaster!
35
35
+
</div>
36
36
+
</div>
37
37
+
<div x-show="tab === 'grinders'">
38
38
+
<div class="mb-4 flex justify-between items-center">
39
39
+
<h3 class="text-xl font-semibold text-brown-900">
40
40
+
Grinders
41
41
+
</h3>
42
42
+
<button
43
43
+
@click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}"
44
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
45
+
+ Add Grinder
46
46
+
</button>
47
47
+
</div>
48
48
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
49
49
+
No grinders yet. Add your first grinder!
50
50
+
</div>
51
51
+
</div>
52
52
+
<div x-show="tab === 'brewers'">
53
53
+
<div class="mb-4 flex justify-between items-center">
54
54
+
<h3 class="text-xl font-semibold text-brown-900">
55
55
+
Brewers
56
56
+
</h3>
57
57
+
<button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}"
58
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
59
+
+ Add Brewer
60
60
+
</button>
61
61
+
</div>
62
62
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
63
63
+
No brewers yet. Add your first brewer!
64
64
+
</div>
65
65
+
</div>
66
66
+
<div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
67
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
68
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3>
69
69
+
<div class="space-y-4">
70
70
+
<input type="text" x-model="beanForm.name" placeholder="Name *"
71
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
72
+
<input type="text" x-model="beanForm.origin" placeholder="Origin *"
73
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
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
75
+
<option value="">
76
76
+
Select Roaster (Optional)
77
77
+
</option>
78
78
+
</select>
79
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
80
+
<option value="">
81
81
+
Select Roast Level (Optional)
82
82
+
</option>
83
83
+
<option value="Ultra-Light">
84
84
+
Ultra-Light
85
85
+
</option>
86
86
+
<option value="Light">
87
87
+
Light
88
88
+
</option>
89
89
+
<option value="Medium-Light">
90
90
+
Medium-Light
91
91
+
</option>
92
92
+
<option value="Medium">
93
93
+
Medium
94
94
+
</option>
95
95
+
<option value="Medium-Dark">
96
96
+
Medium-Dark
97
97
+
</option>
98
98
+
<option value="Dark">
99
99
+
Dark
100
100
+
</option>
101
101
+
</select>
102
102
+
<input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)"
103
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
104
+
<textarea x-model="beanForm.description" placeholder="Description" rows="3"
105
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
106
+
<div class="flex gap-2">
107
107
+
<button @click="saveBean()"
108
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
109
+
Save
110
110
+
</button>
111
111
+
<button @click="showBeanForm = false"
112
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
113
+
Cancel
114
114
+
</button>
115
115
+
</div>
116
116
+
</div>
117
117
+
</div>
118
118
+
</div>
119
119
+
<div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
120
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
121
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3>
122
122
+
<div class="space-y-4">
123
123
+
<input type="text" x-model="roasterForm.name" placeholder="Name *"
124
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
125
+
<input type="text" x-model="roasterForm.location" placeholder="Location"
126
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
127
+
<input type="url" x-model="roasterForm.website" placeholder="Website"
128
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
129
+
<div class="flex gap-2">
130
130
+
<button @click="saveRoaster()"
131
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
132
+
Save
133
133
+
</button>
134
134
+
<button @click="showRoasterForm = false"
135
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
136
+
Cancel
137
137
+
</button>
138
138
+
</div>
139
139
+
</div>
140
140
+
</div>
141
141
+
</div>
142
142
+
<div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
143
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
144
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3>
145
145
+
<div class="space-y-4">
146
146
+
<input type="text" x-model="grinderForm.name" placeholder="Name *"
147
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
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
149
+
<option value="">
150
150
+
Select Grinder Type *
151
151
+
</option>
152
152
+
<option value="Hand">
153
153
+
Hand
154
154
+
</option>
155
155
+
<option value="Electric">
156
156
+
Electric
157
157
+
</option>
158
158
+
<option value="Portable Electric">
159
159
+
Portable Electric
160
160
+
</option>
161
161
+
</select>
162
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
163
+
<option value="">
164
164
+
Select Burr Type (Optional)
165
165
+
</option>
166
166
+
<option value="Conical">
167
167
+
Conical
168
168
+
</option>
169
169
+
<option value="Flat">
170
170
+
Flat
171
171
+
</option>
172
172
+
</select>
173
173
+
<textarea x-model="grinderForm.notes" placeholder="Notes" rows="3"
174
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
175
+
<div class="flex gap-2">
176
176
+
<button @click="saveGrinder()"
177
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
178
+
Save
179
179
+
</button>
180
180
+
<button @click="showGrinderForm = false"
181
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
182
+
Cancel
183
183
+
</button>
184
184
+
</div>
185
185
+
</div>
186
186
+
</div>
187
187
+
</div>
188
188
+
<div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
189
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
190
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3>
191
191
+
<div class="space-y-4">
192
192
+
<input type="text" x-model="brewerForm.name" placeholder="Name *"
193
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
194
+
<input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)"
195
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
196
+
<textarea x-model="brewerForm.description" placeholder="Description" rows="3"
197
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
198
+
<div class="flex gap-2">
199
199
+
<button @click="saveBrewer()"
200
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
201
+
Save
202
202
+
</button>
203
203
+
<button @click="showBrewerForm = false"
204
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
205
+
Cancel
206
206
+
</button>
207
207
+
</div>
208
208
+
</div>
209
209
+
</div>
210
210
+
</div>
+353
internal/bff/__snapshots__/beans_with_roaster.snap
···
1
1
+
---
2
2
+
title: beans with roaster
3
3
+
test_name: TestManageContent_BeansTab_Snapshot/beans_with_roaster
4
4
+
file_name: partial_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="tab === 'beans'">
8
8
+
<div class="mb-4 flex justify-between items-center">
9
9
+
<h3 class="text-xl font-semibold text-brown-900">
10
10
+
Coffee Beans
11
11
+
</h3>
12
12
+
<button
13
13
+
@click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}"
14
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
15
+
+ Add Bean
16
16
+
</button>
17
17
+
</div>
18
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
19
+
<table class="min-w-full divide-y divide-brown-300">
20
20
+
<thead class="bg-brown-200/80">
21
21
+
<tr>
22
22
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap">
23
23
+
Name
24
24
+
</th>
25
25
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap">
26
26
+
📍 Origin
27
27
+
</th>
28
28
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap">
29
29
+
☕ Roaster
30
30
+
</th>
31
31
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap">
32
32
+
🔥 Roast Level
33
33
+
</th>
34
34
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap">
35
35
+
🌱 Process
36
36
+
</th>
37
37
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap">
38
38
+
📝 Description
39
39
+
</th>
40
40
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap">
41
41
+
Actions
42
42
+
</th>
43
43
+
</tr>
44
44
+
</thead>
45
45
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
46
46
+
<tr class="hover:bg-brown-100/60 transition-colors">
47
47
+
<td class="px-6 py-4 text-sm font-medium text-brown-900">
48
48
+
Ethiopian Yirgacheffe
49
49
+
</td>
50
50
+
<td class="px-6 py-4 text-sm text-brown-900">
51
51
+
Ethiopia
52
52
+
</td>
53
53
+
<td class="px-6 py-4 text-sm text-brown-900">
54
54
+
Onyx Coffee Lab
55
55
+
</td>
56
56
+
<td class="px-6 py-4 text-sm text-brown-900">
57
57
+
Light
58
58
+
</td>
59
59
+
<td class="px-6 py-4 text-sm text-brown-900">
60
60
+
Washed
61
61
+
</td>
62
62
+
<td class="px-6 py-4 text-sm text-brown-700">
63
63
+
Bright and fruity with notes of blueberry
64
64
+
</td>
65
65
+
<td class="px-6 py-4 text-sm font-medium space-x-2">
66
66
+
<button @click="editBean('bean1', 'Ethiopian Yirgacheffe', 'Ethiopia', 'Light', 'Washed', 'Bright and fruity with notes of blueberry', 'roaster1')"
67
67
+
class="text-brown-700 hover:text-brown-900 font-medium">
68
68
+
Edit
69
69
+
</button>
70
70
+
<button @click="deleteBean('bean1')"
71
71
+
class="text-brown-600 hover:text-brown-800 font-medium">
72
72
+
Delete
73
73
+
</button>
74
74
+
</td>
75
75
+
</tr>
76
76
+
<tr class="hover:bg-brown-100/60 transition-colors">
77
77
+
<td class="px-6 py-4 text-sm font-medium text-brown-900"></td>
78
78
+
<td class="px-6 py-4 text-sm text-brown-900">
79
79
+
Colombia
80
80
+
</td>
81
81
+
<td class="px-6 py-4 text-sm text-brown-900">
82
82
+
<span class="text-brown-400">
83
83
+
-
84
84
+
</span>
85
85
+
</td>
86
86
+
<td class="px-6 py-4 text-sm text-brown-900">
87
87
+
Medium
88
88
+
</td>
89
89
+
<td class="px-6 py-4 text-sm text-brown-900"></td>
90
90
+
<td class="px-6 py-4 text-sm text-brown-700"></td>
91
91
+
<td class="px-6 py-4 text-sm font-medium space-x-2">
92
92
+
<button @click="editBean('bean2', '', 'Colombia', 'Medium', '', '', '')"
93
93
+
class="text-brown-700 hover:text-brown-900 font-medium">
94
94
+
Edit
95
95
+
</button>
96
96
+
<button @click="deleteBean('bean2')"
97
97
+
class="text-brown-600 hover:text-brown-800 font-medium">
98
98
+
Delete
99
99
+
</button>
100
100
+
</td>
101
101
+
</tr>
102
102
+
</tbody>
103
103
+
</table>
104
104
+
</div>
105
105
+
</div>
106
106
+
<div x-show="tab === 'roasters'">
107
107
+
<div class="mb-4 flex justify-between items-center">
108
108
+
<h3 class="text-xl font-semibold text-brown-900">
109
109
+
Roasters
110
110
+
</h3>
111
111
+
<button
112
112
+
@click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}"
113
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
114
+
+ Add Roaster
115
115
+
</button>
116
116
+
</div>
117
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
118
+
<table class="min-w-full divide-y divide-brown-300">
119
119
+
<thead class="bg-brown-200/80">
120
120
+
<tr>
121
121
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
122
122
+
Name
123
123
+
</th>
124
124
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
125
125
+
📍 Location
126
126
+
</th>
127
127
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
128
128
+
🌐 Website
129
129
+
</th>
130
130
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
131
131
+
Actions
132
132
+
</th>
133
133
+
</tr>
134
134
+
</thead>
135
135
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
136
136
+
<tr class="hover:bg-brown-100/60 transition-colors">
137
137
+
<td class="px-6 py-4 text-sm font-medium text-brown-900">
138
138
+
Onyx Coffee Lab
139
139
+
</td>
140
140
+
<td class="px-6 py-4 text-sm text-brown-900"></td>
141
141
+
<td class="px-6 py-4 text-sm text-brown-900"></td>
142
142
+
<td class="px-6 py-4 text-sm font-medium space-x-2">
143
143
+
<button @click="editRoaster('roaster1', 'Onyx Coffee Lab', '', '')"
144
144
+
class="text-brown-700 hover:text-brown-900 font-medium">
145
145
+
Edit
146
146
+
</button>
147
147
+
<button @click="deleteRoaster('roaster1')"
148
148
+
class="text-brown-600 hover:text-brown-800 font-medium">
149
149
+
Delete
150
150
+
</button>
151
151
+
</td>
152
152
+
</tr>
153
153
+
<tr class="hover:bg-brown-100/60 transition-colors">
154
154
+
<td class="px-6 py-4 text-sm font-medium text-brown-900">
155
155
+
Counter Culture
156
156
+
</td>
157
157
+
<td class="px-6 py-4 text-sm text-brown-900"></td>
158
158
+
<td class="px-6 py-4 text-sm text-brown-900"></td>
159
159
+
<td class="px-6 py-4 text-sm font-medium space-x-2">
160
160
+
<button @click="editRoaster('roaster2', 'Counter Culture', '', '')"
161
161
+
class="text-brown-700 hover:text-brown-900 font-medium">
162
162
+
Edit
163
163
+
</button>
164
164
+
<button @click="deleteRoaster('roaster2')"
165
165
+
class="text-brown-600 hover:text-brown-800 font-medium">
166
166
+
Delete
167
167
+
</button>
168
168
+
</td>
169
169
+
</tr>
170
170
+
</tbody>
171
171
+
</table>
172
172
+
</div>
173
173
+
</div>
174
174
+
<div x-show="tab === 'grinders'">
175
175
+
<div class="mb-4 flex justify-between items-center">
176
176
+
<h3 class="text-xl font-semibold text-brown-900">
177
177
+
Grinders
178
178
+
</h3>
179
179
+
<button
180
180
+
@click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}"
181
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
182
+
+ Add Grinder
183
183
+
</button>
184
184
+
</div>
185
185
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
186
186
+
No grinders yet. Add your first grinder!
187
187
+
</div>
188
188
+
</div>
189
189
+
<div x-show="tab === 'brewers'">
190
190
+
<div class="mb-4 flex justify-between items-center">
191
191
+
<h3 class="text-xl font-semibold text-brown-900">
192
192
+
Brewers
193
193
+
</h3>
194
194
+
<button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}"
195
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
196
+
+ Add Brewer
197
197
+
</button>
198
198
+
</div>
199
199
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
200
200
+
No brewers yet. Add your first brewer!
201
201
+
</div>
202
202
+
</div>
203
203
+
<div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
204
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
205
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3>
206
206
+
<div class="space-y-4">
207
207
+
<input type="text" x-model="beanForm.name" placeholder="Name *"
208
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
209
+
<input type="text" x-model="beanForm.origin" placeholder="Origin *"
210
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
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
212
+
<option value="">
213
213
+
Select Roaster (Optional)
214
214
+
</option>
215
215
+
<option value="roaster1">
216
216
+
Onyx Coffee Lab
217
217
+
</option>
218
218
+
<option value="roaster2">
219
219
+
Counter Culture
220
220
+
</option>
221
221
+
</select>
222
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
223
+
<option value="">
224
224
+
Select Roast Level (Optional)
225
225
+
</option>
226
226
+
<option value="Ultra-Light">
227
227
+
Ultra-Light
228
228
+
</option>
229
229
+
<option value="Light">
230
230
+
Light
231
231
+
</option>
232
232
+
<option value="Medium-Light">
233
233
+
Medium-Light
234
234
+
</option>
235
235
+
<option value="Medium">
236
236
+
Medium
237
237
+
</option>
238
238
+
<option value="Medium-Dark">
239
239
+
Medium-Dark
240
240
+
</option>
241
241
+
<option value="Dark">
242
242
+
Dark
243
243
+
</option>
244
244
+
</select>
245
245
+
<input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)"
246
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
247
+
<textarea x-model="beanForm.description" placeholder="Description" rows="3"
248
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
249
+
<div class="flex gap-2">
250
250
+
<button @click="saveBean()"
251
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
252
+
Save
253
253
+
</button>
254
254
+
<button @click="showBeanForm = false"
255
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
256
+
Cancel
257
257
+
</button>
258
258
+
</div>
259
259
+
</div>
260
260
+
</div>
261
261
+
</div>
262
262
+
<div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
263
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
264
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3>
265
265
+
<div class="space-y-4">
266
266
+
<input type="text" x-model="roasterForm.name" placeholder="Name *"
267
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
268
+
<input type="text" x-model="roasterForm.location" placeholder="Location"
269
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
270
+
<input type="url" x-model="roasterForm.website" placeholder="Website"
271
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
272
+
<div class="flex gap-2">
273
273
+
<button @click="saveRoaster()"
274
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
275
+
Save
276
276
+
</button>
277
277
+
<button @click="showRoasterForm = false"
278
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
279
+
Cancel
280
280
+
</button>
281
281
+
</div>
282
282
+
</div>
283
283
+
</div>
284
284
+
</div>
285
285
+
<div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
286
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
287
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3>
288
288
+
<div class="space-y-4">
289
289
+
<input type="text" x-model="grinderForm.name" placeholder="Name *"
290
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
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
292
+
<option value="">
293
293
+
Select Grinder Type *
294
294
+
</option>
295
295
+
<option value="Hand">
296
296
+
Hand
297
297
+
</option>
298
298
+
<option value="Electric">
299
299
+
Electric
300
300
+
</option>
301
301
+
<option value="Portable Electric">
302
302
+
Portable Electric
303
303
+
</option>
304
304
+
</select>
305
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
306
+
<option value="">
307
307
+
Select Burr Type (Optional)
308
308
+
</option>
309
309
+
<option value="Conical">
310
310
+
Conical
311
311
+
</option>
312
312
+
<option value="Flat">
313
313
+
Flat
314
314
+
</option>
315
315
+
</select>
316
316
+
<textarea x-model="grinderForm.notes" placeholder="Notes" rows="3"
317
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
318
+
<div class="flex gap-2">
319
319
+
<button @click="saveGrinder()"
320
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
321
+
Save
322
322
+
</button>
323
323
+
<button @click="showGrinderForm = false"
324
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
325
+
Cancel
326
326
+
</button>
327
327
+
</div>
328
328
+
</div>
329
329
+
</div>
330
330
+
</div>
331
331
+
<div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
332
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
333
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3>
334
334
+
<div class="space-y-4">
335
335
+
<input type="text" x-model="brewerForm.name" placeholder="Name *"
336
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
337
+
<input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)"
338
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
339
+
<textarea x-model="brewerForm.description" placeholder="Description" rows="3"
340
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
341
+
<div class="flex gap-2">
342
342
+
<button @click="saveBrewer()"
343
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
344
+
Save
345
345
+
</button>
346
346
+
<button @click="showBrewerForm = false"
347
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
348
+
Cancel
349
349
+
</button>
350
350
+
</div>
351
351
+
</div>
352
352
+
</div>
353
353
+
</div>
+270
internal/bff/__snapshots__/beans_with_special_characters_and_html.snap
···
1
1
+
---
2
2
+
title: beans with special characters and html
3
3
+
test_name: TestManageContent_SpecialCharacters_Snapshot/beans_with_special_characters_and_html
4
4
+
file_name: partial_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="tab === 'beans'">
8
8
+
<div class="mb-4 flex justify-between items-center">
9
9
+
<h3 class="text-xl font-semibold text-brown-900">
10
10
+
Coffee Beans
11
11
+
</h3>
12
12
+
<button
13
13
+
@click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}"
14
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
15
+
+ Add Bean
16
16
+
</button>
17
17
+
</div>
18
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
19
+
<table class="min-w-full divide-y divide-brown-300">
20
20
+
<thead class="bg-brown-200/80">
21
21
+
<tr>
22
22
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap">
23
23
+
Name
24
24
+
</th>
25
25
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap">
26
26
+
📍 Origin
27
27
+
</th>
28
28
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap">
29
29
+
☕ Roaster
30
30
+
</th>
31
31
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap">
32
32
+
🔥 Roast Level
33
33
+
</th>
34
34
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap">
35
35
+
🌱 Process
36
36
+
</th>
37
37
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap">
38
38
+
📝 Description
39
39
+
</th>
40
40
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase whitespace-nowrap">
41
41
+
Actions
42
42
+
</th>
43
43
+
</tr>
44
44
+
</thead>
45
45
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
46
46
+
<tr class="hover:bg-brown-100/60 transition-colors">
47
47
+
<td class="px-6 py-4 text-sm font-medium text-brown-900">
48
48
+
Café <script>alert('xss')</script> Especial
49
49
+
</td>
50
50
+
<td class="px-6 py-4 text-sm text-brown-900">
51
51
+
Costa Rica™
52
52
+
</td>
53
53
+
<td class="px-6 py-4 text-sm text-brown-900">
54
54
+
<span class="text-brown-400">
55
55
+
-
56
56
+
</span>
57
57
+
</td>
58
58
+
<td class="px-6 py-4 text-sm text-brown-900">
59
59
+
Medium
60
60
+
</td>
61
61
+
<td class="px-6 py-4 text-sm text-brown-900">
62
62
+
Honey & Washed
63
63
+
</td>
64
64
+
<td class="px-6 py-4 text-sm text-brown-700">
65
65
+
"Amazing" coffee with <strong>bold</strong> flavor
66
66
+
</td>
67
67
+
<td class="px-6 py-4 text-sm font-medium space-x-2">
68
68
+
<button @click="editBean('bean1', 'Café <script>alert(\'xss\')</script> Especial', 'Costa Rica™', 'Medium', 'Honey & Washed', '\"Amazing\" coffee with <strong>bold</strong> flavor', '')"
69
69
+
class="text-brown-700 hover:text-brown-900 font-medium">
70
70
+
Edit
71
71
+
</button>
72
72
+
<button @click="deleteBean('bean1')"
73
73
+
class="text-brown-600 hover:text-brown-800 font-medium">
74
74
+
Delete
75
75
+
</button>
76
76
+
</td>
77
77
+
</tr>
78
78
+
</tbody>
79
79
+
</table>
80
80
+
</div>
81
81
+
</div>
82
82
+
<div x-show="tab === 'roasters'">
83
83
+
<div class="mb-4 flex justify-between items-center">
84
84
+
<h3 class="text-xl font-semibold text-brown-900">
85
85
+
Roasters
86
86
+
</h3>
87
87
+
<button
88
88
+
@click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}"
89
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
90
+
+ Add Roaster
91
91
+
</button>
92
92
+
</div>
93
93
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
94
94
+
No roasters yet. Add your first roaster!
95
95
+
</div>
96
96
+
</div>
97
97
+
<div x-show="tab === 'grinders'">
98
98
+
<div class="mb-4 flex justify-between items-center">
99
99
+
<h3 class="text-xl font-semibold text-brown-900">
100
100
+
Grinders
101
101
+
</h3>
102
102
+
<button
103
103
+
@click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}"
104
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
105
+
+ Add Grinder
106
106
+
</button>
107
107
+
</div>
108
108
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
109
109
+
No grinders yet. Add your first grinder!
110
110
+
</div>
111
111
+
</div>
112
112
+
<div x-show="tab === 'brewers'">
113
113
+
<div class="mb-4 flex justify-between items-center">
114
114
+
<h3 class="text-xl font-semibold text-brown-900">
115
115
+
Brewers
116
116
+
</h3>
117
117
+
<button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}"
118
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
119
+
+ Add Brewer
120
120
+
</button>
121
121
+
</div>
122
122
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
123
123
+
No brewers yet. Add your first brewer!
124
124
+
</div>
125
125
+
</div>
126
126
+
<div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
127
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
128
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3>
129
129
+
<div class="space-y-4">
130
130
+
<input type="text" x-model="beanForm.name" placeholder="Name *"
131
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
132
+
<input type="text" x-model="beanForm.origin" placeholder="Origin *"
133
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
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
135
+
<option value="">
136
136
+
Select Roaster (Optional)
137
137
+
</option>
138
138
+
</select>
139
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
140
+
<option value="">
141
141
+
Select Roast Level (Optional)
142
142
+
</option>
143
143
+
<option value="Ultra-Light">
144
144
+
Ultra-Light
145
145
+
</option>
146
146
+
<option value="Light">
147
147
+
Light
148
148
+
</option>
149
149
+
<option value="Medium-Light">
150
150
+
Medium-Light
151
151
+
</option>
152
152
+
<option value="Medium">
153
153
+
Medium
154
154
+
</option>
155
155
+
<option value="Medium-Dark">
156
156
+
Medium-Dark
157
157
+
</option>
158
158
+
<option value="Dark">
159
159
+
Dark
160
160
+
</option>
161
161
+
</select>
162
162
+
<input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)"
163
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
164
+
<textarea x-model="beanForm.description" placeholder="Description" rows="3"
165
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
166
+
<div class="flex gap-2">
167
167
+
<button @click="saveBean()"
168
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
169
+
Save
170
170
+
</button>
171
171
+
<button @click="showBeanForm = false"
172
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
173
+
Cancel
174
174
+
</button>
175
175
+
</div>
176
176
+
</div>
177
177
+
</div>
178
178
+
</div>
179
179
+
<div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
180
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
181
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3>
182
182
+
<div class="space-y-4">
183
183
+
<input type="text" x-model="roasterForm.name" placeholder="Name *"
184
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
185
+
<input type="text" x-model="roasterForm.location" placeholder="Location"
186
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
187
+
<input type="url" x-model="roasterForm.website" placeholder="Website"
188
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
189
+
<div class="flex gap-2">
190
190
+
<button @click="saveRoaster()"
191
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
192
+
Save
193
193
+
</button>
194
194
+
<button @click="showRoasterForm = false"
195
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
196
+
Cancel
197
197
+
</button>
198
198
+
</div>
199
199
+
</div>
200
200
+
</div>
201
201
+
</div>
202
202
+
<div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
203
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
204
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3>
205
205
+
<div class="space-y-4">
206
206
+
<input type="text" x-model="grinderForm.name" placeholder="Name *"
207
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
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
209
+
<option value="">
210
210
+
Select Grinder Type *
211
211
+
</option>
212
212
+
<option value="Hand">
213
213
+
Hand
214
214
+
</option>
215
215
+
<option value="Electric">
216
216
+
Electric
217
217
+
</option>
218
218
+
<option value="Portable Electric">
219
219
+
Portable Electric
220
220
+
</option>
221
221
+
</select>
222
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
223
+
<option value="">
224
224
+
Select Burr Type (Optional)
225
225
+
</option>
226
226
+
<option value="Conical">
227
227
+
Conical
228
228
+
</option>
229
229
+
<option value="Flat">
230
230
+
Flat
231
231
+
</option>
232
232
+
</select>
233
233
+
<textarea x-model="grinderForm.notes" placeholder="Notes" rows="3"
234
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
235
+
<div class="flex gap-2">
236
236
+
<button @click="saveGrinder()"
237
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
238
+
Save
239
239
+
</button>
240
240
+
<button @click="showGrinderForm = false"
241
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
242
+
Cancel
243
243
+
</button>
244
244
+
</div>
245
245
+
</div>
246
246
+
</div>
247
247
+
</div>
248
248
+
<div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
249
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
250
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3>
251
251
+
<div class="space-y-4">
252
252
+
<input type="text" x-model="brewerForm.name" placeholder="Name *"
253
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
254
+
<input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)"
255
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
256
+
<textarea x-model="brewerForm.description" placeholder="Description" rows="3"
257
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
258
+
<div class="flex gap-2">
259
259
+
<button @click="saveBrewer()"
260
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
261
+
Save
262
262
+
</button>
263
263
+
<button @click="showBrewerForm = false"
264
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
265
+
Cancel
266
266
+
</button>
267
267
+
</div>
268
268
+
</div>
269
269
+
</div>
270
270
+
</div>
+76
internal/bff/__snapshots__/brew_list_minimal_data.snap
···
1
1
+
---
2
2
+
title: brew list minimal data
3
3
+
test_name: TestBrewListContent_Snapshot/brew_list_minimal_data
4
4
+
file_name: partial_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
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
8
+
<table class="min-w-full divide-y divide-brown-300">
9
9
+
<thead class="bg-brown-200/80">
10
10
+
<tr>
11
11
+
<th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
12
12
+
Date
13
13
+
</th>
14
14
+
<th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
15
15
+
Bean
16
16
+
</th>
17
17
+
<th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
18
18
+
Brewer
19
19
+
</th>
20
20
+
<th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
21
21
+
Variables
22
22
+
</th>
23
23
+
<th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
24
24
+
Notes
25
25
+
</th>
26
26
+
<th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
27
27
+
Rating
28
28
+
</th>
29
29
+
<th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
30
30
+
Actions
31
31
+
</th>
32
32
+
</tr>
33
33
+
</thead>
34
34
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
35
35
+
<tr class="hover:bg-brown-100/60 transition-colors">
36
36
+
<td class="px-4 py-4 whitespace-nowrap text-sm text-brown-900 font-medium align-top">
37
37
+
<div>
38
38
+
Jan 15
39
39
+
</div>
40
40
+
<div class="text-xs text-brown-600">
41
41
+
2024
42
42
+
</div>
43
43
+
</td>
44
44
+
<td class="px-4 py-4 text-sm text-brown-900 align-top">
45
45
+
<span class="text-brown-400">
46
46
+
-
47
47
+
</span>
48
48
+
</td>
49
49
+
<td class="px-4 py-4 text-sm text-brown-900 align-top">
50
50
+
<span class="text-brown-400">
51
51
+
-
52
52
+
</span>
53
53
+
</td>
54
54
+
<td class="px-4 py-4 text-xs text-brown-700 align-top">
55
55
+
<div class="space-y-1"></div>
56
56
+
</td>
57
57
+
<td class="px-4 py-4 text-xs text-brown-800 align-top max-w-xs">
58
58
+
<span class="text-brown-400">
59
59
+
-
60
60
+
</span>
61
61
+
</td>
62
62
+
<td class="px-4 py-4 whitespace-nowrap text-sm text-brown-900 align-top">
63
63
+
<span class="text-brown-400">
64
64
+
-
65
65
+
</span>
66
66
+
</td>
67
67
+
<td class="px-4 py-4 whitespace-nowrap text-sm font-medium space-x-2 align-top">
68
68
+
<a href="/brews/brew3"
69
69
+
class="text-brown-700 hover:text-brown-900 font-medium">
70
70
+
View
71
71
+
</a>
72
72
+
</td>
73
73
+
</tr>
74
74
+
</tbody>
75
75
+
</table>
76
76
+
</div>
+182
internal/bff/__snapshots__/brew_list_with_complete_data.snap
···
1
1
+
---
2
2
+
title: brew list with complete data
3
3
+
test_name: TestBrewListContent_Snapshot/brew_list_with_complete_data
4
4
+
file_name: partial_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
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
8
+
<table class="min-w-full divide-y divide-brown-300">
9
9
+
<thead class="bg-brown-200/80">
10
10
+
<tr>
11
11
+
<th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
12
12
+
Date
13
13
+
</th>
14
14
+
<th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
15
15
+
Bean
16
16
+
</th>
17
17
+
<th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
18
18
+
Brewer
19
19
+
</th>
20
20
+
<th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
21
21
+
Variables
22
22
+
</th>
23
23
+
<th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
24
24
+
Notes
25
25
+
</th>
26
26
+
<th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
27
27
+
Rating
28
28
+
</th>
29
29
+
<th class="px-4 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
30
30
+
Actions
31
31
+
</th>
32
32
+
</tr>
33
33
+
</thead>
34
34
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
35
35
+
<tr class="hover:bg-brown-100/60 transition-colors">
36
36
+
<td class="px-4 py-4 whitespace-nowrap text-sm text-brown-900 font-medium align-top">
37
37
+
<div>
38
38
+
Jan 15
39
39
+
</div>
40
40
+
<div class="text-xs text-brown-600">
41
41
+
2024
42
42
+
</div>
43
43
+
</td>
44
44
+
<td class="px-4 py-4 text-sm text-brown-900 align-top">
45
45
+
<div class="font-bold text-brown-900">
46
46
+
Ethiopian Yirgacheffe
47
47
+
</div>
48
48
+
<div class="text-xs text-brown-700 mt-0.5">
49
49
+
<span class="font-medium">
50
50
+
Onyx Coffee Lab
51
51
+
</span>
52
52
+
</div>
53
53
+
<div class="text-xs text-brown-600 mt-0.5 flex flex-wrap gap-x-2 gap-y-0.5">
54
54
+
<span class="inline-flex items-center gap-0.5">
55
55
+
📍 Ethiopia
56
56
+
</span>
57
57
+
<span class="inline-flex items-center gap-0.5">
58
58
+
🔥 Light
59
59
+
</span>
60
60
+
<span class="inline-flex items-center gap-0.5">
61
61
+
⚖️ 18g
62
62
+
</span>
63
63
+
</div>
64
64
+
</td>
65
65
+
<td class="px-4 py-4 text-sm text-brown-900 align-top">
66
66
+
<div class="font-medium text-brown-900">
67
67
+
Hario V60
68
68
+
</div>
69
69
+
</td>
70
70
+
<td class="px-4 py-4 text-xs text-brown-700 align-top">
71
71
+
<div class="space-y-1">
72
72
+
<div>
73
73
+
<span class="text-brown-600">
74
74
+
Grinder:
75
75
+
</span>
76
76
+
Comandante C40 (Medium-fine)
77
77
+
</div>
78
78
+
<div>
79
79
+
<span class="text-brown-600">
80
80
+
Temp:
81
81
+
</span>
82
82
+
93.0°C
83
83
+
</div>
84
84
+
<div>
85
85
+
<span class="text-brown-600">
86
86
+
Pours:
87
87
+
</span>
88
88
+
</div>
89
89
+
<div class="pl-2 text-brown-600">
90
90
+
• 50g @ 30s
91
91
+
</div>
92
92
+
<div class="pl-2 text-brown-600">
93
93
+
• 100g @ 45s
94
94
+
</div>
95
95
+
<div class="pl-2 text-brown-600">
96
96
+
• 100g @ 1m
97
97
+
</div>
98
98
+
<div>
99
99
+
<span class="text-brown-600">
100
100
+
Time:
101
101
+
</span>
102
102
+
3m
103
103
+
</div>
104
104
+
</div>
105
105
+
</td>
106
106
+
<td class="px-4 py-4 text-xs text-brown-800 align-top max-w-xs">
107
107
+
<div class="italic line-clamp-3">
108
108
+
Bright citrus notes with floral aroma. Clean finish.
109
109
+
</div>
110
110
+
</td>
111
111
+
<td class="px-4 py-4 whitespace-nowrap text-sm text-brown-900 align-top">
112
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
113
+
⭐ 8/10
114
114
+
</span>
115
115
+
</td>
116
116
+
<td class="px-4 py-4 whitespace-nowrap text-sm font-medium space-x-2 align-top">
117
117
+
<a href="/brews/brew1"
118
118
+
class="text-brown-700 hover:text-brown-900 font-medium">
119
119
+
View
120
120
+
</a>
121
121
+
<button hx-delete="/brews/brew1"
122
122
+
hx-confirm="Are you sure you want to delete this brew?" hx-target="closest tr"
123
123
+
hx-swap="outerHTML swap:1s" class="text-brown-600 hover:text-brown-800 font-medium">
124
124
+
Delete
125
125
+
</button>
126
126
+
</td>
127
127
+
</tr>
128
128
+
<tr class="hover:bg-brown-100/60 transition-colors">
129
129
+
<td class="px-4 py-4 whitespace-nowrap text-sm text-brown-900 font-medium align-top">
130
130
+
<div>
131
131
+
Jan 14
132
132
+
</div>
133
133
+
<div class="text-xs text-brown-600">
134
134
+
2024
135
135
+
</div>
136
136
+
</td>
137
137
+
<td class="px-4 py-4 text-sm text-brown-900 align-top">
138
138
+
<div class="font-bold text-brown-900">
139
139
+
Colombia
140
140
+
</div>
141
141
+
<div class="text-xs text-brown-600 mt-0.5 flex flex-wrap gap-x-2 gap-y-0.5">
142
142
+
<span class="inline-flex items-center gap-0.5">
143
143
+
📍 Colombia
144
144
+
</span>
145
145
+
<span class="inline-flex items-center gap-0.5">
146
146
+
🔥 Medium
147
147
+
</span>
148
148
+
</div>
149
149
+
</td>
150
150
+
<td class="px-4 py-4 text-sm text-brown-900 align-top">
151
151
+
<div class="font-medium text-brown-900">
152
152
+
AeroPress
153
153
+
</div>
154
154
+
</td>
155
155
+
<td class="px-4 py-4 text-xs text-brown-700 align-top">
156
156
+
<div class="space-y-1"></div>
157
157
+
</td>
158
158
+
<td class="px-4 py-4 text-xs text-brown-800 align-top max-w-xs">
159
159
+
<span class="text-brown-400">
160
160
+
-
161
161
+
</span>
162
162
+
</td>
163
163
+
<td class="px-4 py-4 whitespace-nowrap text-sm text-brown-900 align-top">
164
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
165
+
⭐ 6/10
166
166
+
</span>
167
167
+
</td>
168
168
+
<td class="px-4 py-4 whitespace-nowrap text-sm font-medium space-x-2 align-top">
169
169
+
<a href="/brews/brew2"
170
170
+
class="text-brown-700 hover:text-brown-900 font-medium">
171
171
+
View
172
172
+
</a>
173
173
+
<button hx-delete="/brews/brew2"
174
174
+
hx-confirm="Are you sure you want to delete this brew?" hx-target="closest tr"
175
175
+
hx-swap="outerHTML swap:1s" class="text-brown-600 hover:text-brown-800 font-medium">
176
176
+
Delete
177
177
+
</button>
178
178
+
</td>
179
179
+
</tr>
180
180
+
</tbody>
181
181
+
</table>
182
182
+
</div>
+348
internal/bff/__snapshots__/brew_with_html_in_tasting_notes.snap
···
1
1
+
---
2
2
+
title: brew with html in tasting notes
3
3
+
test_name: TestBrewForm_SpecialCharacters_Snapshot/brew_with_html_in_tasting_notes
4
4
+
file_name: form_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<script src="/static/js/brew-form.js"></script>
8
8
+
<div class="max-w-2xl mx-auto">
9
9
+
<div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300">
10
10
+
<h2 class="text-3xl font-bold text-brown-900 mb-6">
11
11
+
Edit Brew
12
12
+
</h2>
13
13
+
<form
14
14
+
15
15
+
hx-put="/brews/brew1"
16
16
+
17
17
+
hx-target="body"
18
18
+
class="space-y-6"
19
19
+
x-data="brewForm()"
20
20
+
>
21
21
+
<div>
22
22
+
<label class="block text-sm font-medium text-brown-900 mb-2">
23
23
+
Coffee Bean
24
24
+
</label>
25
25
+
<div class="flex gap-2">
26
26
+
<select
27
27
+
name="bean_rkey"
28
28
+
required
29
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
30
+
<option value="">
31
31
+
Select a bean...
32
32
+
</option>
33
33
+
<option
34
34
+
value="bean1"
35
35
+
selected
36
36
+
class="truncate">
37
37
+
Test <strong>Bean</strong> (Ethiopia - Light)
38
38
+
</option>
39
39
+
</select>
40
40
+
<button
41
41
+
type="button"
42
42
+
@click="showNewBean = true"
43
43
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
44
44
+
+ New
45
45
+
</button>
46
46
+
</div>
47
47
+
<div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
48
48
+
<h4 class="font-medium mb-3 text-gray-800">
49
49
+
Add New Bean
50
50
+
</h4>
51
51
+
<div class="space-y-3">
52
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
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
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
55
+
<option value="">
56
56
+
Select Roaster (Optional)
57
57
+
</option>
58
58
+
</select>
59
59
+
<select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
60
60
+
<option value="">
61
61
+
Select Roast Level (Optional)
62
62
+
</option>
63
63
+
<option value="Ultra-Light">
64
64
+
Ultra-Light
65
65
+
</option>
66
66
+
<option value="Light">
67
67
+
Light
68
68
+
</option>
69
69
+
<option value="Medium-Light">
70
70
+
Medium-Light
71
71
+
</option>
72
72
+
<option value="Medium">
73
73
+
Medium
74
74
+
</option>
75
75
+
<option value="Medium-Dark">
76
76
+
Medium-Dark
77
77
+
</option>
78
78
+
<option value="Dark">
79
79
+
Dark
80
80
+
</option>
81
81
+
</select>
82
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
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
84
+
<div class="flex gap-2">
85
85
+
<button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
86
86
+
Add
87
87
+
</button>
88
88
+
<button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
89
89
+
Cancel
90
90
+
</button>
91
91
+
</div>
92
92
+
</div>
93
93
+
</div>
94
94
+
</div>
95
95
+
<div>
96
96
+
<label class="block text-sm font-medium text-brown-900 mb-2">
97
97
+
Coffee Amount (grams)
98
98
+
</label>
99
99
+
<input
100
100
+
type="number"
101
101
+
name="coffee_amount"
102
102
+
step="0.1"
103
103
+
104
104
+
placeholder="e.g. 18"
105
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
106
+
<p class="text-sm text-brown-700 mt-1">
107
107
+
Amount of ground coffee used
108
108
+
</p>
109
109
+
</div>
110
110
+
<div>
111
111
+
<label class="block text-sm font-medium text-brown-900 mb-2">
112
112
+
Grinder
113
113
+
</label>
114
114
+
<div class="flex gap-2">
115
115
+
<select
116
116
+
name="grinder_rkey"
117
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
118
+
<option value="">
119
119
+
Select a grinder...
120
120
+
</option>
121
121
+
</select>
122
122
+
<button
123
123
+
type="button"
124
124
+
@click="showNewGrinder = true"
125
125
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
126
126
+
+ New
127
127
+
</button>
128
128
+
</div>
129
129
+
<div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
130
130
+
<h4 class="font-medium mb-3 text-gray-800">
131
131
+
Add New Grinder
132
132
+
</h4>
133
133
+
<div class="space-y-3">
134
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
135
+
<select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
136
136
+
<option value="">
137
137
+
Grinder Type (Optional)
138
138
+
</option>
139
139
+
<option value="Hand">
140
140
+
Hand
141
141
+
</option>
142
142
+
<option value="Electric">
143
143
+
Electric
144
144
+
</option>
145
145
+
<option value="Electric Hand">
146
146
+
Electric Hand
147
147
+
</option>
148
148
+
</select>
149
149
+
<select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
150
150
+
<option value="">
151
151
+
Burr Type (Optional)
152
152
+
</option>
153
153
+
<option value="Conical">
154
154
+
Conical
155
155
+
</option>
156
156
+
<option value="Flat">
157
157
+
Flat
158
158
+
</option>
159
159
+
</select>
160
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
161
+
<div class="flex gap-2">
162
162
+
<button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
163
163
+
Add
164
164
+
</button>
165
165
+
<button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
166
166
+
Cancel
167
167
+
</button>
168
168
+
</div>
169
169
+
</div>
170
170
+
</div>
171
171
+
</div>
172
172
+
<div>
173
173
+
<label class="block text-sm font-medium text-brown-900 mb-2">
174
174
+
Grind Size
175
175
+
</label>
176
176
+
<input
177
177
+
type="text"
178
178
+
name="grind_size"
179
179
+
value=""
180
180
+
placeholder="e.g. 18, Medium, 3.5, Fine"
181
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
182
+
<p class="text-sm text-brown-700 mt-1">
183
183
+
Enter a number (grinder setting) or description (e.g. "Medium", "Fine")
184
184
+
</p>
185
185
+
</div>
186
186
+
<div>
187
187
+
<label class="block text-sm font-medium text-brown-900 mb-2">
188
188
+
Brew Method
189
189
+
</label>
190
190
+
<div class="flex gap-2">
191
191
+
<select
192
192
+
name="brewer_rkey"
193
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
194
+
<option value="">
195
195
+
Select brew method...
196
196
+
</option>
197
197
+
</select>
198
198
+
<button
199
199
+
type="button"
200
200
+
@click="showNewBrewer = true"
201
201
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
202
202
+
+ New
203
203
+
</button>
204
204
+
</div>
205
205
+
<div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
206
206
+
<h4 class="font-medium mb-3 text-gray-800">
207
207
+
Add New Brewer
208
208
+
</h4>
209
209
+
<div class="space-y-3">
210
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
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
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
213
+
<div class="flex gap-2">
214
214
+
<button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
215
215
+
Add
216
216
+
</button>
217
217
+
<button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
218
218
+
Cancel
219
219
+
</button>
220
220
+
</div>
221
221
+
</div>
222
222
+
</div>
223
223
+
</div>
224
224
+
<div>
225
225
+
<label class="block text-sm font-medium text-brown-900 mb-2">
226
226
+
Water Amount (grams)
227
227
+
</label>
228
228
+
<input
229
229
+
type="number"
230
230
+
name="water_amount"
231
231
+
step="1"
232
232
+
233
233
+
placeholder="e.g. 250"
234
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
235
+
<p class="text-sm text-brown-700 mt-1">
236
236
+
Total water used (or leave empty if using pours below)
237
237
+
</p>
238
238
+
</div>
239
239
+
<div>
240
240
+
<div class="flex items-center justify-between mb-2">
241
241
+
<label class="block text-sm font-medium text-brown-900">
242
242
+
Pours (Optional)
243
243
+
</label>
244
244
+
<button
245
245
+
type="button"
246
246
+
@click="addPour()"
247
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
248
+
+ Add Pour
249
249
+
</button>
250
250
+
</div>
251
251
+
<p class="text-sm text-brown-700 mb-3">
252
252
+
Track individual pours for bloom and subsequent additions
253
253
+
</p>
254
254
+
<div class="space-y-3">
255
255
+
<template x-for="(pour, index) in pours" :key="index">
256
256
+
<div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200">
257
257
+
<div class="flex-1">
258
258
+
<label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label>
259
259
+
<input
260
260
+
type="number"
261
261
+
:name="'pour_water_' + index"
262
262
+
x-model="pour.water"
263
263
+
placeholder="Water (g)"
264
264
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
265
265
+
</div>
266
266
+
<div class="flex-1">
267
267
+
<label class="text-xs text-brown-700 font-medium">
268
268
+
Time (sec)
269
269
+
</label>
270
270
+
<input
271
271
+
type="number"
272
272
+
:name="'pour_time_' + index"
273
273
+
x-model="pour.time"
274
274
+
placeholder="e.g. 45"
275
275
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
276
276
+
</div>
277
277
+
<button
278
278
+
type="button"
279
279
+
@click="removePour(index)"
280
280
+
class="text-brown-700 hover:text-brown-900 mt-5 font-bold"
281
281
+
x-show="pours.length > 0">
282
282
+
✕
283
283
+
</button>
284
284
+
</div>
285
285
+
</template>
286
286
+
</div>
287
287
+
</div>
288
288
+
<div>
289
289
+
<label class="block text-sm font-medium text-brown-900 mb-2">
290
290
+
Temperature
291
291
+
</label>
292
292
+
<input
293
293
+
type="number"
294
294
+
name="temperature"
295
295
+
step="0.1"
296
296
+
297
297
+
placeholder="e.g. 93.5"
298
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
299
+
</div>
300
300
+
<div>
301
301
+
<label class="block text-sm font-medium text-brown-900 mb-2">
302
302
+
Brew Time (seconds)
303
303
+
</label>
304
304
+
<input
305
305
+
type="number"
306
306
+
name="time_seconds"
307
307
+
308
308
+
placeholder="e.g. 180"
309
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
310
+
</div>
311
311
+
<div>
312
312
+
<label class="block text-sm font-medium text-brown-900 mb-2">
313
313
+
Tasting Notes
314
314
+
</label>
315
315
+
<textarea
316
316
+
name="tasting_notes"
317
317
+
rows="4"
318
318
+
placeholder="Describe the flavors, aroma, and your thoughts..."
319
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"><script>alert('xss')</script>Bright & fruity, "amazing" taste</textarea>
320
320
+
</div>
321
321
+
<div>
322
322
+
<label class="block text-sm font-medium text-brown-900 mb-2">
323
323
+
Rating
324
324
+
</label>
325
325
+
<input
326
326
+
type="range"
327
327
+
name="rating"
328
328
+
min="1"
329
329
+
max="10"
330
330
+
value="8"
331
331
+
x-model="rating"
332
332
+
x-init="rating = $el.value"
333
333
+
class="w-full accent-brown-700"/>
334
334
+
<div class="text-center text-2xl font-bold text-brown-800">
335
335
+
<span x-text="rating"></span>
336
336
+
/10
337
337
+
</div>
338
338
+
</div>
339
339
+
<div>
340
340
+
<button
341
341
+
type="submit"
342
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
343
+
Update Brew
344
344
+
</button>
345
345
+
</div>
346
346
+
</form>
347
347
+
</div>
348
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
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
7
+
<div class="space-y-4">
8
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
9
+
<div class="flex items-center gap-3 mb-3">
10
10
+
<a href="/profile/newbie" class="flex-shrink-0">
11
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
12
+
<span class="text-brown-600 text-sm">
13
13
+
?
14
14
+
</span>
15
15
+
</div>
16
16
+
</a>
17
17
+
<div class="flex-1 min-w-0">
18
18
+
<div class="flex items-center gap-2">
19
19
+
<a href="/profile/newbie" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
20
20
+
@newbie
21
21
+
</a>
22
22
+
</div>
23
23
+
<span class="text-brown-500 text-sm">
24
24
+
1 minute ago
25
25
+
</span>
26
26
+
</div>
27
27
+
</div>
28
28
+
<div class="mb-2 text-sm text-brown-700">
29
29
+
☕ added a new brew
30
30
+
</div>
31
31
+
<div class="bg-white/60 backdrop-blur rounded-lg p-4 border border-brown-200">
32
32
+
<div class="flex items-start justify-between gap-3 mb-3">
33
33
+
<div class="flex-1 min-w-0">
34
34
+
<div class="font-bold text-brown-900 text-base">
35
35
+
House Blend
36
36
+
</div>
37
37
+
<div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"></div>
38
38
+
</div>
39
39
+
</div>
40
40
+
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700"></div>
41
41
+
</div>
42
42
+
</div>
43
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
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
7
+
<div class="space-y-4">
8
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
9
+
<div class="flex items-center gap-3 mb-3">
10
10
+
<a href="/profile/japan.coffee" class="flex-shrink-0">
11
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
12
+
<span class="text-brown-600 text-sm">
13
13
+
?
14
14
+
</span>
15
15
+
</div>
16
16
+
</a>
17
17
+
<div class="flex-1 min-w-0">
18
18
+
<div class="flex items-center gap-2">
19
19
+
<a href="/profile/japan.coffee" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline">
20
20
+
日本のコーヒー
21
21
+
</a>
22
22
+
<a href="/profile/japan.coffee" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
23
23
+
@japan.coffee
24
24
+
</a>
25
25
+
</div>
26
26
+
<span class="text-brown-500 text-sm">
27
27
+
3 hours ago
28
28
+
</span>
29
29
+
</div>
30
30
+
</div>
31
31
+
<div class="mb-2 text-sm text-brown-700">
32
32
+
☕ added a new brew
33
33
+
</div>
34
34
+
<div class="bg-white/60 backdrop-blur rounded-lg p-4 border border-brown-200">
35
35
+
<div class="flex items-start justify-between gap-3 mb-3">
36
36
+
<div class="flex-1 min-w-0">
37
37
+
<div class="font-bold text-brown-900 text-base">
38
38
+
コーヒー豆
39
39
+
</div>
40
40
+
<div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5">
41
41
+
<span class="inline-flex items-center gap-0.5">
42
42
+
📍 日本
43
43
+
</span>
44
44
+
</div>
45
45
+
</div>
46
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
47
+
⭐ 8/10
48
48
+
</span>
49
49
+
</div>
50
50
+
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700"></div>
51
51
+
</div>
52
52
+
</div>
53
53
+
</div>
+364
internal/bff/__snapshots__/brew_with_unicode_characters.snap
···
1
1
+
---
2
2
+
title: brew with unicode characters
3
3
+
test_name: TestBrewForm_SpecialCharacters_Snapshot/brew_with_unicode_characters
4
4
+
file_name: form_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<script src="/static/js/brew-form.js"></script>
8
8
+
<div class="max-w-2xl mx-auto">
9
9
+
<div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300">
10
10
+
<h2 class="text-3xl font-bold text-brown-900 mb-6">
11
11
+
Edit Brew
12
12
+
</h2>
13
13
+
<form
14
14
+
15
15
+
hx-put="/brews/brew2"
16
16
+
17
17
+
hx-target="body"
18
18
+
class="space-y-6"
19
19
+
x-data="brewForm()"
20
20
+
>
21
21
+
<div>
22
22
+
<label class="block text-sm font-medium text-brown-900 mb-2">
23
23
+
Coffee Bean
24
24
+
</label>
25
25
+
<div class="flex gap-2">
26
26
+
<select
27
27
+
name="bean_rkey"
28
28
+
required
29
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
30
+
<option value="">
31
31
+
Select a bean...
32
32
+
</option>
33
33
+
<option
34
34
+
value="bean1"
35
35
+
selected
36
36
+
class="truncate">
37
37
+
Café Especial™ (Costa Rica - Medium)
38
38
+
</option>
39
39
+
</select>
40
40
+
<button
41
41
+
type="button"
42
42
+
@click="showNewBean = true"
43
43
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
44
44
+
+ New
45
45
+
</button>
46
46
+
</div>
47
47
+
<div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
48
48
+
<h4 class="font-medium mb-3 text-gray-800">
49
49
+
Add New Bean
50
50
+
</h4>
51
51
+
<div class="space-y-3">
52
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
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
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
55
+
<option value="">
56
56
+
Select Roaster (Optional)
57
57
+
</option>
58
58
+
</select>
59
59
+
<select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
60
60
+
<option value="">
61
61
+
Select Roast Level (Optional)
62
62
+
</option>
63
63
+
<option value="Ultra-Light">
64
64
+
Ultra-Light
65
65
+
</option>
66
66
+
<option value="Light">
67
67
+
Light
68
68
+
</option>
69
69
+
<option value="Medium-Light">
70
70
+
Medium-Light
71
71
+
</option>
72
72
+
<option value="Medium">
73
73
+
Medium
74
74
+
</option>
75
75
+
<option value="Medium-Dark">
76
76
+
Medium-Dark
77
77
+
</option>
78
78
+
<option value="Dark">
79
79
+
Dark
80
80
+
</option>
81
81
+
</select>
82
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
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
84
+
<div class="flex gap-2">
85
85
+
<button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
86
86
+
Add
87
87
+
</button>
88
88
+
<button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
89
89
+
Cancel
90
90
+
</button>
91
91
+
</div>
92
92
+
</div>
93
93
+
</div>
94
94
+
</div>
95
95
+
<div>
96
96
+
<label class="block text-sm font-medium text-brown-900 mb-2">
97
97
+
Coffee Amount (grams)
98
98
+
</label>
99
99
+
<input
100
100
+
type="number"
101
101
+
name="coffee_amount"
102
102
+
step="0.1"
103
103
+
104
104
+
placeholder="e.g. 18"
105
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
106
+
<p class="text-sm text-brown-700 mt-1">
107
107
+
Amount of ground coffee used
108
108
+
</p>
109
109
+
</div>
110
110
+
<div>
111
111
+
<label class="block text-sm font-medium text-brown-900 mb-2">
112
112
+
Grinder
113
113
+
</label>
114
114
+
<div class="flex gap-2">
115
115
+
<select
116
116
+
name="grinder_rkey"
117
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
118
+
<option value="">
119
119
+
Select a grinder...
120
120
+
</option>
121
121
+
<option
122
122
+
value="grinder1"
123
123
+
124
124
+
class="truncate">
125
125
+
Comandante® C40 MK3
126
126
+
</option>
127
127
+
</select>
128
128
+
<button
129
129
+
type="button"
130
130
+
@click="showNewGrinder = true"
131
131
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
132
132
+
+ New
133
133
+
</button>
134
134
+
</div>
135
135
+
<div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
136
136
+
<h4 class="font-medium mb-3 text-gray-800">
137
137
+
Add New Grinder
138
138
+
</h4>
139
139
+
<div class="space-y-3">
140
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
141
+
<select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
142
142
+
<option value="">
143
143
+
Grinder Type (Optional)
144
144
+
</option>
145
145
+
<option value="Hand">
146
146
+
Hand
147
147
+
</option>
148
148
+
<option value="Electric">
149
149
+
Electric
150
150
+
</option>
151
151
+
<option value="Electric Hand">
152
152
+
Electric Hand
153
153
+
</option>
154
154
+
</select>
155
155
+
<select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
156
156
+
<option value="">
157
157
+
Burr Type (Optional)
158
158
+
</option>
159
159
+
<option value="Conical">
160
160
+
Conical
161
161
+
</option>
162
162
+
<option value="Flat">
163
163
+
Flat
164
164
+
</option>
165
165
+
</select>
166
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
167
+
<div class="flex gap-2">
168
168
+
<button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
169
169
+
Add
170
170
+
</button>
171
171
+
<button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
172
172
+
Cancel
173
173
+
</button>
174
174
+
</div>
175
175
+
</div>
176
176
+
</div>
177
177
+
</div>
178
178
+
<div>
179
179
+
<label class="block text-sm font-medium text-brown-900 mb-2">
180
180
+
Grind Size
181
181
+
</label>
182
182
+
<input
183
183
+
type="text"
184
184
+
name="grind_size"
185
185
+
value="中挽き (medium)"
186
186
+
placeholder="e.g. 18, Medium, 3.5, Fine"
187
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
188
+
<p class="text-sm text-brown-700 mt-1">
189
189
+
Enter a number (grinder setting) or description (e.g. "Medium", "Fine")
190
190
+
</p>
191
191
+
</div>
192
192
+
<div>
193
193
+
<label class="block text-sm font-medium text-brown-900 mb-2">
194
194
+
Brew Method
195
195
+
</label>
196
196
+
<div class="flex gap-2">
197
197
+
<select
198
198
+
name="brewer_rkey"
199
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
200
+
<option value="">
201
201
+
Select brew method...
202
202
+
</option>
203
203
+
<option
204
204
+
value="brewer1"
205
205
+
206
206
+
class="truncate">
207
207
+
Hario V60 (02)
208
208
+
</option>
209
209
+
</select>
210
210
+
<button
211
211
+
type="button"
212
212
+
@click="showNewBrewer = true"
213
213
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
214
214
+
+ New
215
215
+
</button>
216
216
+
</div>
217
217
+
<div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
218
218
+
<h4 class="font-medium mb-3 text-gray-800">
219
219
+
Add New Brewer
220
220
+
</h4>
221
221
+
<div class="space-y-3">
222
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
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
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
225
+
<div class="flex gap-2">
226
226
+
<button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
227
227
+
Add
228
228
+
</button>
229
229
+
<button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
230
230
+
Cancel
231
231
+
</button>
232
232
+
</div>
233
233
+
</div>
234
234
+
</div>
235
235
+
</div>
236
236
+
<div>
237
237
+
<label class="block text-sm font-medium text-brown-900 mb-2">
238
238
+
Water Amount (grams)
239
239
+
</label>
240
240
+
<input
241
241
+
type="number"
242
242
+
name="water_amount"
243
243
+
step="1"
244
244
+
245
245
+
placeholder="e.g. 250"
246
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
247
+
<p class="text-sm text-brown-700 mt-1">
248
248
+
Total water used (or leave empty if using pours below)
249
249
+
</p>
250
250
+
</div>
251
251
+
<div>
252
252
+
<div class="flex items-center justify-between mb-2">
253
253
+
<label class="block text-sm font-medium text-brown-900">
254
254
+
Pours (Optional)
255
255
+
</label>
256
256
+
<button
257
257
+
type="button"
258
258
+
@click="addPour()"
259
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
260
+
+ Add Pour
261
261
+
</button>
262
262
+
</div>
263
263
+
<p class="text-sm text-brown-700 mb-3">
264
264
+
Track individual pours for bloom and subsequent additions
265
265
+
</p>
266
266
+
<div class="space-y-3">
267
267
+
<template x-for="(pour, index) in pours" :key="index">
268
268
+
<div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200">
269
269
+
<div class="flex-1">
270
270
+
<label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label>
271
271
+
<input
272
272
+
type="number"
273
273
+
:name="'pour_water_' + index"
274
274
+
x-model="pour.water"
275
275
+
placeholder="Water (g)"
276
276
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
277
277
+
</div>
278
278
+
<div class="flex-1">
279
279
+
<label class="text-xs text-brown-700 font-medium">
280
280
+
Time (sec)
281
281
+
</label>
282
282
+
<input
283
283
+
type="number"
284
284
+
:name="'pour_time_' + index"
285
285
+
x-model="pour.time"
286
286
+
placeholder="e.g. 45"
287
287
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
288
288
+
</div>
289
289
+
<button
290
290
+
type="button"
291
291
+
@click="removePour(index)"
292
292
+
class="text-brown-700 hover:text-brown-900 mt-5 font-bold"
293
293
+
x-show="pours.length > 0">
294
294
+
✕
295
295
+
</button>
296
296
+
</div>
297
297
+
</template>
298
298
+
</div>
299
299
+
</div>
300
300
+
<div>
301
301
+
<label class="block text-sm font-medium text-brown-900 mb-2">
302
302
+
Temperature
303
303
+
</label>
304
304
+
<input
305
305
+
type="number"
306
306
+
name="temperature"
307
307
+
step="0.1"
308
308
+
309
309
+
placeholder="e.g. 93.5"
310
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
311
+
</div>
312
312
+
<div>
313
313
+
<label class="block text-sm font-medium text-brown-900 mb-2">
314
314
+
Brew Time (seconds)
315
315
+
</label>
316
316
+
<input
317
317
+
type="number"
318
318
+
name="time_seconds"
319
319
+
320
320
+
placeholder="e.g. 180"
321
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
322
+
</div>
323
323
+
<div>
324
324
+
<label class="block text-sm font-medium text-brown-900 mb-2">
325
325
+
Tasting Notes
326
326
+
</label>
327
327
+
<textarea
328
328
+
name="tasting_notes"
329
329
+
rows="4"
330
330
+
placeholder="Describe the flavors, aroma, and your thoughts..."
331
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
332
+
333
333
+
Яркий вкус с цитрусовыми нотами
334
334
+
335
335
+
Café con notas de caramelo</textarea>
336
336
+
</div>
337
337
+
<div>
338
338
+
<label class="block text-sm font-medium text-brown-900 mb-2">
339
339
+
Rating
340
340
+
</label>
341
341
+
<input
342
342
+
type="range"
343
343
+
name="rating"
344
344
+
min="1"
345
345
+
max="10"
346
346
+
value="9"
347
347
+
x-model="rating"
348
348
+
x-init="rating = $el.value"
349
349
+
class="w-full accent-brown-700"/>
350
350
+
<div class="text-center text-2xl font-bold text-brown-800">
351
351
+
<span x-text="rating"></span>
352
352
+
/10
353
353
+
</div>
354
354
+
</div>
355
355
+
<div>
356
356
+
<button
357
357
+
type="submit"
358
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
359
+
Update Brew
360
360
+
</button>
361
361
+
</div>
362
362
+
</form>
363
363
+
</div>
364
364
+
</div>
+24
internal/bff/__snapshots__/brewer_form_renders.snap
···
1
1
+
---
2
2
+
title: brewer_form_renders
3
3
+
test_name: TestNewBrewerForm_Snapshot/brewer_form_renders
4
4
+
file_name: form_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
8
8
+
<h4 class="font-medium mb-3 text-gray-800">
9
9
+
Add New Brewer
10
10
+
</h4>
11
11
+
<div class="space-y-3">
12
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
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
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
15
+
<div class="flex gap-2">
16
16
+
<button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
17
17
+
Add
18
18
+
</button>
19
19
+
<button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
20
20
+
Cancel
21
21
+
</button>
22
22
+
</div>
23
23
+
</div>
24
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
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
7
+
<div class="space-y-4">
8
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
9
+
<div class="flex items-center gap-3 mb-3">
10
10
+
<a href="/profile/pourover.fan" class="flex-shrink-0">
11
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
12
+
<span class="text-brown-600 text-sm">
13
13
+
?
14
14
+
</span>
15
15
+
</div>
16
16
+
</a>
17
17
+
<div class="flex-1 min-w-0">
18
18
+
<div class="flex items-center gap-2">
19
19
+
<a href="/profile/pourover.fan" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline">
20
20
+
Pour Over Fan
21
21
+
</a>
22
22
+
<a href="/profile/pourover.fan" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
23
23
+
@pourover.fan
24
24
+
</a>
25
25
+
</div>
26
26
+
<span class="text-brown-500 text-sm">
27
27
+
2 days ago
28
28
+
</span>
29
29
+
</div>
30
30
+
</div>
31
31
+
<div class="mb-2 text-sm text-brown-700">
32
32
+
☕ added a new brewer
33
33
+
</div>
34
34
+
<div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200">
35
35
+
<div class="text-base mb-2">
36
36
+
<span class="font-bold text-brown-900">
37
37
+
Kalita Wave 185
38
38
+
</span>
39
39
+
</div>
40
40
+
<div class="text-sm text-brown-800 italic">
41
41
+
"Flat-bottom dripper with wave filters"
42
42
+
</div>
43
43
+
</div>
44
44
+
</div>
45
45
+
</div>
+210
internal/bff/__snapshots__/brewers_empty.snap
···
1
1
+
---
2
2
+
title: brewers empty
3
3
+
test_name: TestManageContent_BrewersTab_Snapshot/brewers_empty
4
4
+
file_name: partial_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="tab === 'beans'">
8
8
+
<div class="mb-4 flex justify-between items-center">
9
9
+
<h3 class="text-xl font-semibold text-brown-900">
10
10
+
Coffee Beans
11
11
+
</h3>
12
12
+
<button
13
13
+
@click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}"
14
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
15
+
+ Add Bean
16
16
+
</button>
17
17
+
</div>
18
18
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
19
19
+
No beans yet. Add your first bean to get started!
20
20
+
</div>
21
21
+
</div>
22
22
+
<div x-show="tab === 'roasters'">
23
23
+
<div class="mb-4 flex justify-between items-center">
24
24
+
<h3 class="text-xl font-semibold text-brown-900">
25
25
+
Roasters
26
26
+
</h3>
27
27
+
<button
28
28
+
@click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}"
29
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
30
+
+ Add Roaster
31
31
+
</button>
32
32
+
</div>
33
33
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
34
34
+
No roasters yet. Add your first roaster!
35
35
+
</div>
36
36
+
</div>
37
37
+
<div x-show="tab === 'grinders'">
38
38
+
<div class="mb-4 flex justify-between items-center">
39
39
+
<h3 class="text-xl font-semibold text-brown-900">
40
40
+
Grinders
41
41
+
</h3>
42
42
+
<button
43
43
+
@click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}"
44
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
45
+
+ Add Grinder
46
46
+
</button>
47
47
+
</div>
48
48
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
49
49
+
No grinders yet. Add your first grinder!
50
50
+
</div>
51
51
+
</div>
52
52
+
<div x-show="tab === 'brewers'">
53
53
+
<div class="mb-4 flex justify-between items-center">
54
54
+
<h3 class="text-xl font-semibold text-brown-900">
55
55
+
Brewers
56
56
+
</h3>
57
57
+
<button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}"
58
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
59
+
+ Add Brewer
60
60
+
</button>
61
61
+
</div>
62
62
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
63
63
+
No brewers yet. Add your first brewer!
64
64
+
</div>
65
65
+
</div>
66
66
+
<div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
67
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
68
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3>
69
69
+
<div class="space-y-4">
70
70
+
<input type="text" x-model="beanForm.name" placeholder="Name *"
71
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
72
+
<input type="text" x-model="beanForm.origin" placeholder="Origin *"
73
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
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
75
+
<option value="">
76
76
+
Select Roaster (Optional)
77
77
+
</option>
78
78
+
</select>
79
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
80
+
<option value="">
81
81
+
Select Roast Level (Optional)
82
82
+
</option>
83
83
+
<option value="Ultra-Light">
84
84
+
Ultra-Light
85
85
+
</option>
86
86
+
<option value="Light">
87
87
+
Light
88
88
+
</option>
89
89
+
<option value="Medium-Light">
90
90
+
Medium-Light
91
91
+
</option>
92
92
+
<option value="Medium">
93
93
+
Medium
94
94
+
</option>
95
95
+
<option value="Medium-Dark">
96
96
+
Medium-Dark
97
97
+
</option>
98
98
+
<option value="Dark">
99
99
+
Dark
100
100
+
</option>
101
101
+
</select>
102
102
+
<input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)"
103
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
104
+
<textarea x-model="beanForm.description" placeholder="Description" rows="3"
105
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
106
+
<div class="flex gap-2">
107
107
+
<button @click="saveBean()"
108
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
109
+
Save
110
110
+
</button>
111
111
+
<button @click="showBeanForm = false"
112
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
113
+
Cancel
114
114
+
</button>
115
115
+
</div>
116
116
+
</div>
117
117
+
</div>
118
118
+
</div>
119
119
+
<div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
120
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
121
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3>
122
122
+
<div class="space-y-4">
123
123
+
<input type="text" x-model="roasterForm.name" placeholder="Name *"
124
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
125
+
<input type="text" x-model="roasterForm.location" placeholder="Location"
126
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
127
+
<input type="url" x-model="roasterForm.website" placeholder="Website"
128
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
129
+
<div class="flex gap-2">
130
130
+
<button @click="saveRoaster()"
131
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
132
+
Save
133
133
+
</button>
134
134
+
<button @click="showRoasterForm = false"
135
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
136
+
Cancel
137
137
+
</button>
138
138
+
</div>
139
139
+
</div>
140
140
+
</div>
141
141
+
</div>
142
142
+
<div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
143
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
144
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3>
145
145
+
<div class="space-y-4">
146
146
+
<input type="text" x-model="grinderForm.name" placeholder="Name *"
147
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
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
149
+
<option value="">
150
150
+
Select Grinder Type *
151
151
+
</option>
152
152
+
<option value="Hand">
153
153
+
Hand
154
154
+
</option>
155
155
+
<option value="Electric">
156
156
+
Electric
157
157
+
</option>
158
158
+
<option value="Portable Electric">
159
159
+
Portable Electric
160
160
+
</option>
161
161
+
</select>
162
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
163
+
<option value="">
164
164
+
Select Burr Type (Optional)
165
165
+
</option>
166
166
+
<option value="Conical">
167
167
+
Conical
168
168
+
</option>
169
169
+
<option value="Flat">
170
170
+
Flat
171
171
+
</option>
172
172
+
</select>
173
173
+
<textarea x-model="grinderForm.notes" placeholder="Notes" rows="3"
174
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
175
+
<div class="flex gap-2">
176
176
+
<button @click="saveGrinder()"
177
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
178
+
Save
179
179
+
</button>
180
180
+
<button @click="showGrinderForm = false"
181
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
182
+
Cancel
183
183
+
</button>
184
184
+
</div>
185
185
+
</div>
186
186
+
</div>
187
187
+
</div>
188
188
+
<div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
189
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
190
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3>
191
191
+
<div class="space-y-4">
192
192
+
<input type="text" x-model="brewerForm.name" placeholder="Name *"
193
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
194
+
<input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)"
195
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
196
+
<textarea x-model="brewerForm.description" placeholder="Description" rows="3"
197
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
198
+
<div class="flex gap-2">
199
199
+
<button @click="saveBrewer()"
200
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
201
+
Save
202
202
+
</button>
203
203
+
<button @click="showBrewerForm = false"
204
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
205
+
Cancel
206
206
+
</button>
207
207
+
</div>
208
208
+
</div>
209
209
+
</div>
210
210
+
</div>
+279
internal/bff/__snapshots__/brewers_with_data.snap
···
1
1
+
---
2
2
+
title: brewers with data
3
3
+
test_name: TestManageContent_BrewersTab_Snapshot/brewers_with_data
4
4
+
file_name: partial_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="tab === 'beans'">
8
8
+
<div class="mb-4 flex justify-between items-center">
9
9
+
<h3 class="text-xl font-semibold text-brown-900">
10
10
+
Coffee Beans
11
11
+
</h3>
12
12
+
<button
13
13
+
@click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}"
14
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
15
+
+ Add Bean
16
16
+
</button>
17
17
+
</div>
18
18
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
19
19
+
No beans yet. Add your first bean to get started!
20
20
+
</div>
21
21
+
</div>
22
22
+
<div x-show="tab === 'roasters'">
23
23
+
<div class="mb-4 flex justify-between items-center">
24
24
+
<h3 class="text-xl font-semibold text-brown-900">
25
25
+
Roasters
26
26
+
</h3>
27
27
+
<button
28
28
+
@click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}"
29
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
30
+
+ Add Roaster
31
31
+
</button>
32
32
+
</div>
33
33
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
34
34
+
No roasters yet. Add your first roaster!
35
35
+
</div>
36
36
+
</div>
37
37
+
<div x-show="tab === 'grinders'">
38
38
+
<div class="mb-4 flex justify-between items-center">
39
39
+
<h3 class="text-xl font-semibold text-brown-900">
40
40
+
Grinders
41
41
+
</h3>
42
42
+
<button
43
43
+
@click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}"
44
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
45
+
+ Add Grinder
46
46
+
</button>
47
47
+
</div>
48
48
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
49
49
+
No grinders yet. Add your first grinder!
50
50
+
</div>
51
51
+
</div>
52
52
+
<div x-show="tab === 'brewers'">
53
53
+
<div class="mb-4 flex justify-between items-center">
54
54
+
<h3 class="text-xl font-semibold text-brown-900">
55
55
+
Brewers
56
56
+
</h3>
57
57
+
<button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}"
58
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
59
+
+ Add Brewer
60
60
+
</button>
61
61
+
</div>
62
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
63
+
<table class="min-w-full divide-y divide-brown-300">
64
64
+
<thead class="bg-brown-200/80">
65
65
+
<tr>
66
66
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
67
67
+
Name
68
68
+
</th>
69
69
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
70
70
+
🔧 Type
71
71
+
</th>
72
72
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
73
73
+
📝 Description
74
74
+
</th>
75
75
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
76
76
+
Actions
77
77
+
</th>
78
78
+
</tr>
79
79
+
</thead>
80
80
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
81
81
+
<tr class="hover:bg-brown-100/60 transition-colors"
82
82
+
data-rkey="brewer1"
83
83
+
data-name="Hario V60"
84
84
+
data-brewer-type="Pour-Over"
85
85
+
data-description="Cone-shaped dripper for clean, bright brews">
86
86
+
<td class="px-6 py-4 text-sm font-medium text-brown-900">
87
87
+
Hario V60
88
88
+
</td>
89
89
+
<td class="px-6 py-4 text-sm text-brown-900">
90
90
+
Pour-Over
91
91
+
</td>
92
92
+
<td class="px-6 py-4 text-sm text-brown-700">
93
93
+
Cone-shaped dripper for clean, bright brews
94
94
+
</td>
95
95
+
<td class="px-6 py-4 text-sm font-medium space-x-2">
96
96
+
<button @click="editBrewerFromRow($el.closest('tr'))"
97
97
+
class="text-brown-700 hover:text-brown-900 font-medium">
98
98
+
Edit
99
99
+
</button>
100
100
+
<button @click="deleteBrewer($el.closest('tr').dataset.rkey)"
101
101
+
class="text-brown-600 hover:text-brown-800 font-medium">
102
102
+
Delete
103
103
+
</button>
104
104
+
</td>
105
105
+
</tr>
106
106
+
<tr class="hover:bg-brown-100/60 transition-colors"
107
107
+
data-rkey="brewer2"
108
108
+
data-name="AeroPress"
109
109
+
data-brewer-type=""
110
110
+
data-description="">
111
111
+
<td class="px-6 py-4 text-sm font-medium text-brown-900">
112
112
+
AeroPress
113
113
+
</td>
114
114
+
<td class="px-6 py-4 text-sm text-brown-900">
115
115
+
<span class="text-brown-400">
116
116
+
-
117
117
+
</span>
118
118
+
</td>
119
119
+
<td class="px-6 py-4 text-sm text-brown-700"></td>
120
120
+
<td class="px-6 py-4 text-sm font-medium space-x-2">
121
121
+
<button @click="editBrewerFromRow($el.closest('tr'))"
122
122
+
class="text-brown-700 hover:text-brown-900 font-medium">
123
123
+
Edit
124
124
+
</button>
125
125
+
<button @click="deleteBrewer($el.closest('tr').dataset.rkey)"
126
126
+
class="text-brown-600 hover:text-brown-800 font-medium">
127
127
+
Delete
128
128
+
</button>
129
129
+
</td>
130
130
+
</tr>
131
131
+
</tbody>
132
132
+
</table>
133
133
+
</div>
134
134
+
</div>
135
135
+
<div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
136
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
137
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3>
138
138
+
<div class="space-y-4">
139
139
+
<input type="text" x-model="beanForm.name" placeholder="Name *"
140
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
141
+
<input type="text" x-model="beanForm.origin" placeholder="Origin *"
142
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
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
144
+
<option value="">
145
145
+
Select Roaster (Optional)
146
146
+
</option>
147
147
+
</select>
148
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
149
+
<option value="">
150
150
+
Select Roast Level (Optional)
151
151
+
</option>
152
152
+
<option value="Ultra-Light">
153
153
+
Ultra-Light
154
154
+
</option>
155
155
+
<option value="Light">
156
156
+
Light
157
157
+
</option>
158
158
+
<option value="Medium-Light">
159
159
+
Medium-Light
160
160
+
</option>
161
161
+
<option value="Medium">
162
162
+
Medium
163
163
+
</option>
164
164
+
<option value="Medium-Dark">
165
165
+
Medium-Dark
166
166
+
</option>
167
167
+
<option value="Dark">
168
168
+
Dark
169
169
+
</option>
170
170
+
</select>
171
171
+
<input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)"
172
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
173
+
<textarea x-model="beanForm.description" placeholder="Description" rows="3"
174
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
175
+
<div class="flex gap-2">
176
176
+
<button @click="saveBean()"
177
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
178
+
Save
179
179
+
</button>
180
180
+
<button @click="showBeanForm = false"
181
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
182
+
Cancel
183
183
+
</button>
184
184
+
</div>
185
185
+
</div>
186
186
+
</div>
187
187
+
</div>
188
188
+
<div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
189
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
190
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3>
191
191
+
<div class="space-y-4">
192
192
+
<input type="text" x-model="roasterForm.name" placeholder="Name *"
193
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
194
+
<input type="text" x-model="roasterForm.location" placeholder="Location"
195
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
196
+
<input type="url" x-model="roasterForm.website" placeholder="Website"
197
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
198
+
<div class="flex gap-2">
199
199
+
<button @click="saveRoaster()"
200
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
201
+
Save
202
202
+
</button>
203
203
+
<button @click="showRoasterForm = false"
204
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
205
+
Cancel
206
206
+
</button>
207
207
+
</div>
208
208
+
</div>
209
209
+
</div>
210
210
+
</div>
211
211
+
<div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
212
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
213
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3>
214
214
+
<div class="space-y-4">
215
215
+
<input type="text" x-model="grinderForm.name" placeholder="Name *"
216
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
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
218
+
<option value="">
219
219
+
Select Grinder Type *
220
220
+
</option>
221
221
+
<option value="Hand">
222
222
+
Hand
223
223
+
</option>
224
224
+
<option value="Electric">
225
225
+
Electric
226
226
+
</option>
227
227
+
<option value="Portable Electric">
228
228
+
Portable Electric
229
229
+
</option>
230
230
+
</select>
231
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
232
+
<option value="">
233
233
+
Select Burr Type (Optional)
234
234
+
</option>
235
235
+
<option value="Conical">
236
236
+
Conical
237
237
+
</option>
238
238
+
<option value="Flat">
239
239
+
Flat
240
240
+
</option>
241
241
+
</select>
242
242
+
<textarea x-model="grinderForm.notes" placeholder="Notes" rows="3"
243
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
244
+
<div class="flex gap-2">
245
245
+
<button @click="saveGrinder()"
246
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
247
+
Save
248
248
+
</button>
249
249
+
<button @click="showGrinderForm = false"
250
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
251
+
Cancel
252
252
+
</button>
253
253
+
</div>
254
254
+
</div>
255
255
+
</div>
256
256
+
</div>
257
257
+
<div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
258
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
259
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3>
260
260
+
<div class="space-y-4">
261
261
+
<input type="text" x-model="brewerForm.name" placeholder="Name *"
262
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
263
+
<input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)"
264
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
265
+
<textarea x-model="brewerForm.description" placeholder="Description" rows="3"
266
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
267
+
<div class="flex gap-2">
268
268
+
<button @click="saveBrewer()"
269
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
270
+
Save
271
271
+
</button>
272
272
+
<button @click="showBrewerForm = false"
273
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
274
+
Cancel
275
275
+
</button>
276
276
+
</div>
277
277
+
</div>
278
278
+
</div>
279
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
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
7
+
<div class="space-y-4">
8
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
9
+
<div class="flex items-center gap-3 mb-3">
10
10
+
<a href="/profile/coffee.lover" class="flex-shrink-0">
11
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
12
+
</a>
13
13
+
<div class="flex-1 min-w-0">
14
14
+
<div class="flex items-center gap-2">
15
15
+
<a href="/profile/coffee.lover" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline">
16
16
+
Coffee Enthusiast
17
17
+
</a>
18
18
+
<a href="/profile/coffee.lover" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
19
19
+
@coffee.lover
20
20
+
</a>
21
21
+
</div>
22
22
+
<span class="text-brown-500 text-sm">
23
23
+
2 hours ago
24
24
+
</span>
25
25
+
</div>
26
26
+
</div>
27
27
+
<div class="mb-2 text-sm text-brown-700">
28
28
+
☕ added a new brew
29
29
+
</div>
30
30
+
<div class="bg-white/60 backdrop-blur rounded-lg p-4 border border-brown-200">
31
31
+
<div class="flex items-start justify-between gap-3 mb-3">
32
32
+
<div class="flex-1 min-w-0">
33
33
+
<div class="font-bold text-brown-900 text-base">
34
34
+
Ethiopian Yirgacheffe
35
35
+
</div>
36
36
+
<div class="text-sm text-brown-700 mt-0.5">
37
37
+
<span class="font-medium">
38
38
+
🏪 Onyx Coffee Lab
39
39
+
</span>
40
40
+
</div>
41
41
+
<div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5">
42
42
+
<span class="inline-flex items-center gap-0.5">
43
43
+
📍 Ethiopia
44
44
+
</span>
45
45
+
<span class="inline-flex items-center gap-0.5">
46
46
+
🔥 Light
47
47
+
</span>
48
48
+
<span class="inline-flex items-center gap-0.5">
49
49
+
🌱 Washed
50
50
+
</span>
51
51
+
<span class="inline-flex items-center gap-0.5">
52
52
+
⚖️ 16g
53
53
+
</span>
54
54
+
</div>
55
55
+
</div>
56
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
57
+
⭐ 9/10
58
58
+
</span>
59
59
+
</div>
60
60
+
<div class="mb-2">
61
61
+
<span class="text-xs text-brown-600">
62
62
+
Brewer:
63
63
+
</span>
64
64
+
<span class="text-sm font-semibold text-brown-900">
65
65
+
Hario V60
66
66
+
</span>
67
67
+
</div>
68
68
+
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700">
69
69
+
<div>
70
70
+
<span class="text-brown-600">
71
71
+
Grinder:
72
72
+
</span>
73
73
+
1Zpresso JX-Pro (Medium-fine)
74
74
+
</div>
75
75
+
<div class="col-span-2">
76
76
+
<span class="text-brown-600">
77
77
+
Pours:
78
78
+
</span>
79
79
+
<div class="pl-2 text-brown-600">
80
80
+
• 50g @ 30s
81
81
+
</div>
82
82
+
<div class="pl-2 text-brown-600">
83
83
+
• 100g @ 45s
84
84
+
</div>
85
85
+
<div class="pl-2 text-brown-600">
86
86
+
• 100g @ 1m
87
87
+
</div>
88
88
+
</div>
89
89
+
<div>
90
90
+
<span class="text-brown-600">
91
91
+
Temp:
92
92
+
</span>
93
93
+
93.0°C
94
94
+
</div>
95
95
+
<div>
96
96
+
<span class="text-brown-600">
97
97
+
Time:
98
98
+
</span>
99
99
+
3m
100
100
+
</div>
101
101
+
</div>
102
102
+
<div class="mt-3 text-sm text-brown-800 italic border-t border-brown-200 pt-2">
103
103
+
"Bright citrus notes with floral aroma"
104
104
+
</div>
105
105
+
</div>
106
106
+
</div>
107
107
+
</div>
+383
internal/bff/__snapshots__/edit_brew_with_complete_data.snap
···
1
1
+
---
2
2
+
title: edit brew with complete data
3
3
+
test_name: TestBrewForm_EditBrew_Snapshot/edit_brew_with_complete_data
4
4
+
file_name: form_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<script src="/static/js/brew-form.js"></script>
8
8
+
<div class="max-w-2xl mx-auto">
9
9
+
<div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300">
10
10
+
<h2 class="text-3xl font-bold text-brown-900 mb-6">
11
11
+
Edit Brew
12
12
+
</h2>
13
13
+
<form
14
14
+
15
15
+
hx-put="/brews/brew123"
16
16
+
17
17
+
hx-target="body"
18
18
+
class="space-y-6"
19
19
+
x-data="brewForm()"
20
20
+
21
21
+
data-pours='[{"pourNumber":1,"waterAmount":50,"timeSeconds":30},{"pourNumber":2,"waterAmount":100,"timeSeconds":45},{"pourNumber":3,"waterAmount":150,"timeSeconds":60}]'
22
22
+
>
23
23
+
<div>
24
24
+
<label class="block text-sm font-medium text-brown-900 mb-2">
25
25
+
Coffee Bean
26
26
+
</label>
27
27
+
<div class="flex gap-2">
28
28
+
<select
29
29
+
name="bean_rkey"
30
30
+
required
31
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
32
+
<option value="">
33
33
+
Select a bean...
34
34
+
</option>
35
35
+
<option
36
36
+
value="bean1"
37
37
+
selected
38
38
+
class="truncate">
39
39
+
Ethiopian Yirgacheffe (Ethiopia - Light)
40
40
+
</option>
41
41
+
<option
42
42
+
value="bean2"
43
43
+
44
44
+
class="truncate">
45
45
+
Colombian Supremo (Colombia - Medium)
46
46
+
</option>
47
47
+
</select>
48
48
+
<button
49
49
+
type="button"
50
50
+
@click="showNewBean = true"
51
51
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
52
52
+
+ New
53
53
+
</button>
54
54
+
</div>
55
55
+
<div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
56
56
+
<h4 class="font-medium mb-3 text-gray-800">
57
57
+
Add New Bean
58
58
+
</h4>
59
59
+
<div class="space-y-3">
60
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
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
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
63
+
<option value="">
64
64
+
Select Roaster (Optional)
65
65
+
</option>
66
66
+
<option value="roaster1">
67
67
+
Blue Bottle
68
68
+
</option>
69
69
+
</select>
70
70
+
<select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
71
71
+
<option value="">
72
72
+
Select Roast Level (Optional)
73
73
+
</option>
74
74
+
<option value="Ultra-Light">
75
75
+
Ultra-Light
76
76
+
</option>
77
77
+
<option value="Light">
78
78
+
Light
79
79
+
</option>
80
80
+
<option value="Medium-Light">
81
81
+
Medium-Light
82
82
+
</option>
83
83
+
<option value="Medium">
84
84
+
Medium
85
85
+
</option>
86
86
+
<option value="Medium-Dark">
87
87
+
Medium-Dark
88
88
+
</option>
89
89
+
<option value="Dark">
90
90
+
Dark
91
91
+
</option>
92
92
+
</select>
93
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
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
95
+
<div class="flex gap-2">
96
96
+
<button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
97
97
+
Add
98
98
+
</button>
99
99
+
<button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
100
100
+
Cancel
101
101
+
</button>
102
102
+
</div>
103
103
+
</div>
104
104
+
</div>
105
105
+
</div>
106
106
+
<div>
107
107
+
<label class="block text-sm font-medium text-brown-900 mb-2">
108
108
+
Coffee Amount (grams)
109
109
+
</label>
110
110
+
<input
111
111
+
type="number"
112
112
+
name="coffee_amount"
113
113
+
step="0.1"
114
114
+
value="18"
115
115
+
placeholder="e.g. 18"
116
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
117
+
<p class="text-sm text-brown-700 mt-1">
118
118
+
Amount of ground coffee used
119
119
+
</p>
120
120
+
</div>
121
121
+
<div>
122
122
+
<label class="block text-sm font-medium text-brown-900 mb-2">
123
123
+
Grinder
124
124
+
</label>
125
125
+
<div class="flex gap-2">
126
126
+
<select
127
127
+
name="grinder_rkey"
128
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
129
+
<option value="">
130
130
+
Select a grinder...
131
131
+
</option>
132
132
+
<option
133
133
+
value="grinder1"
134
134
+
selected
135
135
+
class="truncate">
136
136
+
Baratza Encore
137
137
+
</option>
138
138
+
<option
139
139
+
value="grinder2"
140
140
+
141
141
+
class="truncate">
142
142
+
Comandante C40
143
143
+
</option>
144
144
+
</select>
145
145
+
<button
146
146
+
type="button"
147
147
+
@click="showNewGrinder = true"
148
148
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
149
149
+
+ New
150
150
+
</button>
151
151
+
</div>
152
152
+
<div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
153
153
+
<h4 class="font-medium mb-3 text-gray-800">
154
154
+
Add New Grinder
155
155
+
</h4>
156
156
+
<div class="space-y-3">
157
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
158
+
<select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
159
159
+
<option value="">
160
160
+
Grinder Type (Optional)
161
161
+
</option>
162
162
+
<option value="Hand">
163
163
+
Hand
164
164
+
</option>
165
165
+
<option value="Electric">
166
166
+
Electric
167
167
+
</option>
168
168
+
<option value="Electric Hand">
169
169
+
Electric Hand
170
170
+
</option>
171
171
+
</select>
172
172
+
<select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
173
173
+
<option value="">
174
174
+
Burr Type (Optional)
175
175
+
</option>
176
176
+
<option value="Conical">
177
177
+
Conical
178
178
+
</option>
179
179
+
<option value="Flat">
180
180
+
Flat
181
181
+
</option>
182
182
+
</select>
183
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
184
+
<div class="flex gap-2">
185
185
+
<button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
186
186
+
Add
187
187
+
</button>
188
188
+
<button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
189
189
+
Cancel
190
190
+
</button>
191
191
+
</div>
192
192
+
</div>
193
193
+
</div>
194
194
+
</div>
195
195
+
<div>
196
196
+
<label class="block text-sm font-medium text-brown-900 mb-2">
197
197
+
Grind Size
198
198
+
</label>
199
199
+
<input
200
200
+
type="text"
201
201
+
name="grind_size"
202
202
+
value="18"
203
203
+
placeholder="e.g. 18, Medium, 3.5, Fine"
204
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
205
+
<p class="text-sm text-brown-700 mt-1">
206
206
+
Enter a number (grinder setting) or description (e.g. "Medium", "Fine")
207
207
+
</p>
208
208
+
</div>
209
209
+
<div>
210
210
+
<label class="block text-sm font-medium text-brown-900 mb-2">
211
211
+
Brew Method
212
212
+
</label>
213
213
+
<div class="flex gap-2">
214
214
+
<select
215
215
+
name="brewer_rkey"
216
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
217
+
<option value="">
218
218
+
Select brew method...
219
219
+
</option>
220
220
+
<option
221
221
+
value="brewer1"
222
222
+
selected
223
223
+
class="truncate">
224
224
+
Hario V60
225
225
+
</option>
226
226
+
<option
227
227
+
value="brewer2"
228
228
+
229
229
+
class="truncate">
230
230
+
AeroPress
231
231
+
</option>
232
232
+
</select>
233
233
+
<button
234
234
+
type="button"
235
235
+
@click="showNewBrewer = true"
236
236
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
237
237
+
+ New
238
238
+
</button>
239
239
+
</div>
240
240
+
<div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
241
241
+
<h4 class="font-medium mb-3 text-gray-800">
242
242
+
Add New Brewer
243
243
+
</h4>
244
244
+
<div class="space-y-3">
245
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
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
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
248
+
<div class="flex gap-2">
249
249
+
<button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
250
250
+
Add
251
251
+
</button>
252
252
+
<button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
253
253
+
Cancel
254
254
+
</button>
255
255
+
</div>
256
256
+
</div>
257
257
+
</div>
258
258
+
</div>
259
259
+
<div>
260
260
+
<label class="block text-sm font-medium text-brown-900 mb-2">
261
261
+
Water Amount (grams)
262
262
+
</label>
263
263
+
<input
264
264
+
type="number"
265
265
+
name="water_amount"
266
266
+
step="1"
267
267
+
value="300"
268
268
+
placeholder="e.g. 250"
269
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
270
+
<p class="text-sm text-brown-700 mt-1">
271
271
+
Total water used (or leave empty if using pours below)
272
272
+
</p>
273
273
+
</div>
274
274
+
<div>
275
275
+
<div class="flex items-center justify-between mb-2">
276
276
+
<label class="block text-sm font-medium text-brown-900">
277
277
+
Pours (Optional)
278
278
+
</label>
279
279
+
<button
280
280
+
type="button"
281
281
+
@click="addPour()"
282
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
283
+
+ Add Pour
284
284
+
</button>
285
285
+
</div>
286
286
+
<p class="text-sm text-brown-700 mb-3">
287
287
+
Track individual pours for bloom and subsequent additions
288
288
+
</p>
289
289
+
<div class="space-y-3">
290
290
+
<template x-for="(pour, index) in pours" :key="index">
291
291
+
<div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200">
292
292
+
<div class="flex-1">
293
293
+
<label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label>
294
294
+
<input
295
295
+
type="number"
296
296
+
:name="'pour_water_' + index"
297
297
+
x-model="pour.water"
298
298
+
placeholder="Water (g)"
299
299
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
300
300
+
</div>
301
301
+
<div class="flex-1">
302
302
+
<label class="text-xs text-brown-700 font-medium">
303
303
+
Time (sec)
304
304
+
</label>
305
305
+
<input
306
306
+
type="number"
307
307
+
:name="'pour_time_' + index"
308
308
+
x-model="pour.time"
309
309
+
placeholder="e.g. 45"
310
310
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
311
311
+
</div>
312
312
+
<button
313
313
+
type="button"
314
314
+
@click="removePour(index)"
315
315
+
class="text-brown-700 hover:text-brown-900 mt-5 font-bold"
316
316
+
x-show="pours.length > 0">
317
317
+
✕
318
318
+
</button>
319
319
+
</div>
320
320
+
</template>
321
321
+
</div>
322
322
+
</div>
323
323
+
<div>
324
324
+
<label class="block text-sm font-medium text-brown-900 mb-2">
325
325
+
Temperature
326
326
+
</label>
327
327
+
<input
328
328
+
type="number"
329
329
+
name="temperature"
330
330
+
step="0.1"
331
331
+
value="93.5"
332
332
+
placeholder="e.g. 93.5"
333
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
334
+
</div>
335
335
+
<div>
336
336
+
<label class="block text-sm font-medium text-brown-900 mb-2">
337
337
+
Brew Time (seconds)
338
338
+
</label>
339
339
+
<input
340
340
+
type="number"
341
341
+
name="time_seconds"
342
342
+
value="180"
343
343
+
placeholder="e.g. 180"
344
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
345
+
</div>
346
346
+
<div>
347
347
+
<label class="block text-sm font-medium text-brown-900 mb-2">
348
348
+
Tasting Notes
349
349
+
</label>
350
350
+
<textarea
351
351
+
name="tasting_notes"
352
352
+
rows="4"
353
353
+
placeholder="Describe the flavors, aroma, and your thoughts..."
354
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
355
+
</div>
356
356
+
<div>
357
357
+
<label class="block text-sm font-medium text-brown-900 mb-2">
358
358
+
Rating
359
359
+
</label>
360
360
+
<input
361
361
+
type="range"
362
362
+
name="rating"
363
363
+
min="1"
364
364
+
max="10"
365
365
+
value="8"
366
366
+
x-model="rating"
367
367
+
x-init="rating = $el.value"
368
368
+
class="w-full accent-brown-700"/>
369
369
+
<div class="text-center text-2xl font-bold text-brown-800">
370
370
+
<span x-text="rating"></span>
371
371
+
/10
372
372
+
</div>
373
373
+
</div>
374
374
+
<div>
375
375
+
<button
376
376
+
type="submit"
377
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
378
+
Update Brew
379
379
+
</button>
380
380
+
</div>
381
381
+
</form>
382
382
+
</div>
383
383
+
</div>
+348
internal/bff/__snapshots__/edit_brew_with_minimal_data.snap
···
1
1
+
---
2
2
+
title: edit brew with minimal data
3
3
+
test_name: TestBrewForm_EditBrew_Snapshot/edit_brew_with_minimal_data
4
4
+
file_name: form_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<script src="/static/js/brew-form.js"></script>
8
8
+
<div class="max-w-2xl mx-auto">
9
9
+
<div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300">
10
10
+
<h2 class="text-3xl font-bold text-brown-900 mb-6">
11
11
+
Edit Brew
12
12
+
</h2>
13
13
+
<form
14
14
+
15
15
+
hx-put="/brews/brew456"
16
16
+
17
17
+
hx-target="body"
18
18
+
class="space-y-6"
19
19
+
x-data="brewForm()"
20
20
+
>
21
21
+
<div>
22
22
+
<label class="block text-sm font-medium text-brown-900 mb-2">
23
23
+
Coffee Bean
24
24
+
</label>
25
25
+
<div class="flex gap-2">
26
26
+
<select
27
27
+
name="bean_rkey"
28
28
+
required
29
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
30
+
<option value="">
31
31
+
Select a bean...
32
32
+
</option>
33
33
+
<option
34
34
+
value="bean1"
35
35
+
selected
36
36
+
class="truncate">
37
37
+
House Blend (Brazil - Medium)
38
38
+
</option>
39
39
+
</select>
40
40
+
<button
41
41
+
type="button"
42
42
+
@click="showNewBean = true"
43
43
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
44
44
+
+ New
45
45
+
</button>
46
46
+
</div>
47
47
+
<div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
48
48
+
<h4 class="font-medium mb-3 text-gray-800">
49
49
+
Add New Bean
50
50
+
</h4>
51
51
+
<div class="space-y-3">
52
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
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
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
55
+
<option value="">
56
56
+
Select Roaster (Optional)
57
57
+
</option>
58
58
+
</select>
59
59
+
<select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
60
60
+
<option value="">
61
61
+
Select Roast Level (Optional)
62
62
+
</option>
63
63
+
<option value="Ultra-Light">
64
64
+
Ultra-Light
65
65
+
</option>
66
66
+
<option value="Light">
67
67
+
Light
68
68
+
</option>
69
69
+
<option value="Medium-Light">
70
70
+
Medium-Light
71
71
+
</option>
72
72
+
<option value="Medium">
73
73
+
Medium
74
74
+
</option>
75
75
+
<option value="Medium-Dark">
76
76
+
Medium-Dark
77
77
+
</option>
78
78
+
<option value="Dark">
79
79
+
Dark
80
80
+
</option>
81
81
+
</select>
82
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
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
84
+
<div class="flex gap-2">
85
85
+
<button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
86
86
+
Add
87
87
+
</button>
88
88
+
<button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
89
89
+
Cancel
90
90
+
</button>
91
91
+
</div>
92
92
+
</div>
93
93
+
</div>
94
94
+
</div>
95
95
+
<div>
96
96
+
<label class="block text-sm font-medium text-brown-900 mb-2">
97
97
+
Coffee Amount (grams)
98
98
+
</label>
99
99
+
<input
100
100
+
type="number"
101
101
+
name="coffee_amount"
102
102
+
step="0.1"
103
103
+
104
104
+
placeholder="e.g. 18"
105
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
106
+
<p class="text-sm text-brown-700 mt-1">
107
107
+
Amount of ground coffee used
108
108
+
</p>
109
109
+
</div>
110
110
+
<div>
111
111
+
<label class="block text-sm font-medium text-brown-900 mb-2">
112
112
+
Grinder
113
113
+
</label>
114
114
+
<div class="flex gap-2">
115
115
+
<select
116
116
+
name="grinder_rkey"
117
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
118
+
<option value="">
119
119
+
Select a grinder...
120
120
+
</option>
121
121
+
</select>
122
122
+
<button
123
123
+
type="button"
124
124
+
@click="showNewGrinder = true"
125
125
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
126
126
+
+ New
127
127
+
</button>
128
128
+
</div>
129
129
+
<div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
130
130
+
<h4 class="font-medium mb-3 text-gray-800">
131
131
+
Add New Grinder
132
132
+
</h4>
133
133
+
<div class="space-y-3">
134
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
135
+
<select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
136
136
+
<option value="">
137
137
+
Grinder Type (Optional)
138
138
+
</option>
139
139
+
<option value="Hand">
140
140
+
Hand
141
141
+
</option>
142
142
+
<option value="Electric">
143
143
+
Electric
144
144
+
</option>
145
145
+
<option value="Electric Hand">
146
146
+
Electric Hand
147
147
+
</option>
148
148
+
</select>
149
149
+
<select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
150
150
+
<option value="">
151
151
+
Burr Type (Optional)
152
152
+
</option>
153
153
+
<option value="Conical">
154
154
+
Conical
155
155
+
</option>
156
156
+
<option value="Flat">
157
157
+
Flat
158
158
+
</option>
159
159
+
</select>
160
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
161
+
<div class="flex gap-2">
162
162
+
<button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
163
163
+
Add
164
164
+
</button>
165
165
+
<button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
166
166
+
Cancel
167
167
+
</button>
168
168
+
</div>
169
169
+
</div>
170
170
+
</div>
171
171
+
</div>
172
172
+
<div>
173
173
+
<label class="block text-sm font-medium text-brown-900 mb-2">
174
174
+
Grind Size
175
175
+
</label>
176
176
+
<input
177
177
+
type="text"
178
178
+
name="grind_size"
179
179
+
value=""
180
180
+
placeholder="e.g. 18, Medium, 3.5, Fine"
181
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
182
+
<p class="text-sm text-brown-700 mt-1">
183
183
+
Enter a number (grinder setting) or description (e.g. "Medium", "Fine")
184
184
+
</p>
185
185
+
</div>
186
186
+
<div>
187
187
+
<label class="block text-sm font-medium text-brown-900 mb-2">
188
188
+
Brew Method
189
189
+
</label>
190
190
+
<div class="flex gap-2">
191
191
+
<select
192
192
+
name="brewer_rkey"
193
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
194
+
<option value="">
195
195
+
Select brew method...
196
196
+
</option>
197
197
+
</select>
198
198
+
<button
199
199
+
type="button"
200
200
+
@click="showNewBrewer = true"
201
201
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
202
202
+
+ New
203
203
+
</button>
204
204
+
</div>
205
205
+
<div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
206
206
+
<h4 class="font-medium mb-3 text-gray-800">
207
207
+
Add New Brewer
208
208
+
</h4>
209
209
+
<div class="space-y-3">
210
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
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
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
213
+
<div class="flex gap-2">
214
214
+
<button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
215
215
+
Add
216
216
+
</button>
217
217
+
<button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
218
218
+
Cancel
219
219
+
</button>
220
220
+
</div>
221
221
+
</div>
222
222
+
</div>
223
223
+
</div>
224
224
+
<div>
225
225
+
<label class="block text-sm font-medium text-brown-900 mb-2">
226
226
+
Water Amount (grams)
227
227
+
</label>
228
228
+
<input
229
229
+
type="number"
230
230
+
name="water_amount"
231
231
+
step="1"
232
232
+
233
233
+
placeholder="e.g. 250"
234
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
235
+
<p class="text-sm text-brown-700 mt-1">
236
236
+
Total water used (or leave empty if using pours below)
237
237
+
</p>
238
238
+
</div>
239
239
+
<div>
240
240
+
<div class="flex items-center justify-between mb-2">
241
241
+
<label class="block text-sm font-medium text-brown-900">
242
242
+
Pours (Optional)
243
243
+
</label>
244
244
+
<button
245
245
+
type="button"
246
246
+
@click="addPour()"
247
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
248
+
+ Add Pour
249
249
+
</button>
250
250
+
</div>
251
251
+
<p class="text-sm text-brown-700 mb-3">
252
252
+
Track individual pours for bloom and subsequent additions
253
253
+
</p>
254
254
+
<div class="space-y-3">
255
255
+
<template x-for="(pour, index) in pours" :key="index">
256
256
+
<div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200">
257
257
+
<div class="flex-1">
258
258
+
<label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label>
259
259
+
<input
260
260
+
type="number"
261
261
+
:name="'pour_water_' + index"
262
262
+
x-model="pour.water"
263
263
+
placeholder="Water (g)"
264
264
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
265
265
+
</div>
266
266
+
<div class="flex-1">
267
267
+
<label class="text-xs text-brown-700 font-medium">
268
268
+
Time (sec)
269
269
+
</label>
270
270
+
<input
271
271
+
type="number"
272
272
+
:name="'pour_time_' + index"
273
273
+
x-model="pour.time"
274
274
+
placeholder="e.g. 45"
275
275
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
276
276
+
</div>
277
277
+
<button
278
278
+
type="button"
279
279
+
@click="removePour(index)"
280
280
+
class="text-brown-700 hover:text-brown-900 mt-5 font-bold"
281
281
+
x-show="pours.length > 0">
282
282
+
✕
283
283
+
</button>
284
284
+
</div>
285
285
+
</template>
286
286
+
</div>
287
287
+
</div>
288
288
+
<div>
289
289
+
<label class="block text-sm font-medium text-brown-900 mb-2">
290
290
+
Temperature
291
291
+
</label>
292
292
+
<input
293
293
+
type="number"
294
294
+
name="temperature"
295
295
+
step="0.1"
296
296
+
297
297
+
placeholder="e.g. 93.5"
298
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
299
+
</div>
300
300
+
<div>
301
301
+
<label class="block text-sm font-medium text-brown-900 mb-2">
302
302
+
Brew Time (seconds)
303
303
+
</label>
304
304
+
<input
305
305
+
type="number"
306
306
+
name="time_seconds"
307
307
+
308
308
+
placeholder="e.g. 180"
309
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
310
+
</div>
311
311
+
<div>
312
312
+
<label class="block text-sm font-medium text-brown-900 mb-2">
313
313
+
Tasting Notes
314
314
+
</label>
315
315
+
<textarea
316
316
+
name="tasting_notes"
317
317
+
rows="4"
318
318
+
placeholder="Describe the flavors, aroma, and your thoughts..."
319
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
320
+
</div>
321
321
+
<div>
322
322
+
<label class="block text-sm font-medium text-brown-900 mb-2">
323
323
+
Rating
324
324
+
</label>
325
325
+
<input
326
326
+
type="range"
327
327
+
name="rating"
328
328
+
min="1"
329
329
+
max="10"
330
330
+
value="5"
331
331
+
x-model="rating"
332
332
+
x-init="rating = $el.value"
333
333
+
class="w-full accent-brown-700"/>
334
334
+
<div class="text-center text-2xl font-bold text-brown-800">
335
335
+
<span x-text="rating"></span>
336
336
+
/10
337
337
+
</div>
338
338
+
</div>
339
339
+
<div>
340
340
+
<button
341
341
+
type="submit"
342
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
343
+
Update Brew
344
344
+
</button>
345
345
+
</div>
346
346
+
</form>
347
347
+
</div>
348
348
+
</div>
+350
internal/bff/__snapshots__/edit_brew_with_pours_json.snap
···
1
1
+
---
2
2
+
title: edit brew with pours json
3
3
+
test_name: TestBrewForm_EditBrew_Snapshot/edit_brew_with_pours_json
4
4
+
file_name: form_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<script src="/static/js/brew-form.js"></script>
8
8
+
<div class="max-w-2xl mx-auto">
9
9
+
<div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300">
10
10
+
<h2 class="text-3xl font-bold text-brown-900 mb-6">
11
11
+
Edit Brew
12
12
+
</h2>
13
13
+
<form
14
14
+
15
15
+
hx-put="/brews/brew789"
16
16
+
17
17
+
hx-target="body"
18
18
+
class="space-y-6"
19
19
+
x-data="brewForm()"
20
20
+
21
21
+
data-pours='[{"pourNumber":1,"waterAmount":60,"timeSeconds":30},{"pourNumber":2,"waterAmount":120,"timeSeconds":60}]'
22
22
+
>
23
23
+
<div>
24
24
+
<label class="block text-sm font-medium text-brown-900 mb-2">
25
25
+
Coffee Bean
26
26
+
</label>
27
27
+
<div class="flex gap-2">
28
28
+
<select
29
29
+
name="bean_rkey"
30
30
+
required
31
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
32
+
<option value="">
33
33
+
Select a bean...
34
34
+
</option>
35
35
+
<option
36
36
+
value="bean1"
37
37
+
selected
38
38
+
class="truncate">
39
39
+
Kenya - Light
40
40
+
</option>
41
41
+
</select>
42
42
+
<button
43
43
+
type="button"
44
44
+
@click="showNewBean = true"
45
45
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
46
46
+
+ New
47
47
+
</button>
48
48
+
</div>
49
49
+
<div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
50
50
+
<h4 class="font-medium mb-3 text-gray-800">
51
51
+
Add New Bean
52
52
+
</h4>
53
53
+
<div class="space-y-3">
54
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
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
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
57
+
<option value="">
58
58
+
Select Roaster (Optional)
59
59
+
</option>
60
60
+
</select>
61
61
+
<select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
62
62
+
<option value="">
63
63
+
Select Roast Level (Optional)
64
64
+
</option>
65
65
+
<option value="Ultra-Light">
66
66
+
Ultra-Light
67
67
+
</option>
68
68
+
<option value="Light">
69
69
+
Light
70
70
+
</option>
71
71
+
<option value="Medium-Light">
72
72
+
Medium-Light
73
73
+
</option>
74
74
+
<option value="Medium">
75
75
+
Medium
76
76
+
</option>
77
77
+
<option value="Medium-Dark">
78
78
+
Medium-Dark
79
79
+
</option>
80
80
+
<option value="Dark">
81
81
+
Dark
82
82
+
</option>
83
83
+
</select>
84
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
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
86
+
<div class="flex gap-2">
87
87
+
<button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
88
88
+
Add
89
89
+
</button>
90
90
+
<button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
91
91
+
Cancel
92
92
+
</button>
93
93
+
</div>
94
94
+
</div>
95
95
+
</div>
96
96
+
</div>
97
97
+
<div>
98
98
+
<label class="block text-sm font-medium text-brown-900 mb-2">
99
99
+
Coffee Amount (grams)
100
100
+
</label>
101
101
+
<input
102
102
+
type="number"
103
103
+
name="coffee_amount"
104
104
+
step="0.1"
105
105
+
106
106
+
placeholder="e.g. 18"
107
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
108
+
<p class="text-sm text-brown-700 mt-1">
109
109
+
Amount of ground coffee used
110
110
+
</p>
111
111
+
</div>
112
112
+
<div>
113
113
+
<label class="block text-sm font-medium text-brown-900 mb-2">
114
114
+
Grinder
115
115
+
</label>
116
116
+
<div class="flex gap-2">
117
117
+
<select
118
118
+
name="grinder_rkey"
119
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
120
+
<option value="">
121
121
+
Select a grinder...
122
122
+
</option>
123
123
+
</select>
124
124
+
<button
125
125
+
type="button"
126
126
+
@click="showNewGrinder = true"
127
127
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
128
128
+
+ New
129
129
+
</button>
130
130
+
</div>
131
131
+
<div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
132
132
+
<h4 class="font-medium mb-3 text-gray-800">
133
133
+
Add New Grinder
134
134
+
</h4>
135
135
+
<div class="space-y-3">
136
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
137
+
<select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
138
138
+
<option value="">
139
139
+
Grinder Type (Optional)
140
140
+
</option>
141
141
+
<option value="Hand">
142
142
+
Hand
143
143
+
</option>
144
144
+
<option value="Electric">
145
145
+
Electric
146
146
+
</option>
147
147
+
<option value="Electric Hand">
148
148
+
Electric Hand
149
149
+
</option>
150
150
+
</select>
151
151
+
<select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
152
152
+
<option value="">
153
153
+
Burr Type (Optional)
154
154
+
</option>
155
155
+
<option value="Conical">
156
156
+
Conical
157
157
+
</option>
158
158
+
<option value="Flat">
159
159
+
Flat
160
160
+
</option>
161
161
+
</select>
162
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
163
+
<div class="flex gap-2">
164
164
+
<button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
165
165
+
Add
166
166
+
</button>
167
167
+
<button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
168
168
+
Cancel
169
169
+
</button>
170
170
+
</div>
171
171
+
</div>
172
172
+
</div>
173
173
+
</div>
174
174
+
<div>
175
175
+
<label class="block text-sm font-medium text-brown-900 mb-2">
176
176
+
Grind Size
177
177
+
</label>
178
178
+
<input
179
179
+
type="text"
180
180
+
name="grind_size"
181
181
+
value=""
182
182
+
placeholder="e.g. 18, Medium, 3.5, Fine"
183
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
184
+
<p class="text-sm text-brown-700 mt-1">
185
185
+
Enter a number (grinder setting) or description (e.g. "Medium", "Fine")
186
186
+
</p>
187
187
+
</div>
188
188
+
<div>
189
189
+
<label class="block text-sm font-medium text-brown-900 mb-2">
190
190
+
Brew Method
191
191
+
</label>
192
192
+
<div class="flex gap-2">
193
193
+
<select
194
194
+
name="brewer_rkey"
195
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
196
+
<option value="">
197
197
+
Select brew method...
198
198
+
</option>
199
199
+
</select>
200
200
+
<button
201
201
+
type="button"
202
202
+
@click="showNewBrewer = true"
203
203
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
204
204
+
+ New
205
205
+
</button>
206
206
+
</div>
207
207
+
<div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
208
208
+
<h4 class="font-medium mb-3 text-gray-800">
209
209
+
Add New Brewer
210
210
+
</h4>
211
211
+
<div class="space-y-3">
212
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
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
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
215
+
<div class="flex gap-2">
216
216
+
<button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
217
217
+
Add
218
218
+
</button>
219
219
+
<button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
220
220
+
Cancel
221
221
+
</button>
222
222
+
</div>
223
223
+
</div>
224
224
+
</div>
225
225
+
</div>
226
226
+
<div>
227
227
+
<label class="block text-sm font-medium text-brown-900 mb-2">
228
228
+
Water Amount (grams)
229
229
+
</label>
230
230
+
<input
231
231
+
type="number"
232
232
+
name="water_amount"
233
233
+
step="1"
234
234
+
235
235
+
placeholder="e.g. 250"
236
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
237
+
<p class="text-sm text-brown-700 mt-1">
238
238
+
Total water used (or leave empty if using pours below)
239
239
+
</p>
240
240
+
</div>
241
241
+
<div>
242
242
+
<div class="flex items-center justify-between mb-2">
243
243
+
<label class="block text-sm font-medium text-brown-900">
244
244
+
Pours (Optional)
245
245
+
</label>
246
246
+
<button
247
247
+
type="button"
248
248
+
@click="addPour()"
249
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
250
+
+ Add Pour
251
251
+
</button>
252
252
+
</div>
253
253
+
<p class="text-sm text-brown-700 mb-3">
254
254
+
Track individual pours for bloom and subsequent additions
255
255
+
</p>
256
256
+
<div class="space-y-3">
257
257
+
<template x-for="(pour, index) in pours" :key="index">
258
258
+
<div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200">
259
259
+
<div class="flex-1">
260
260
+
<label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label>
261
261
+
<input
262
262
+
type="number"
263
263
+
:name="'pour_water_' + index"
264
264
+
x-model="pour.water"
265
265
+
placeholder="Water (g)"
266
266
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
267
267
+
</div>
268
268
+
<div class="flex-1">
269
269
+
<label class="text-xs text-brown-700 font-medium">
270
270
+
Time (sec)
271
271
+
</label>
272
272
+
<input
273
273
+
type="number"
274
274
+
:name="'pour_time_' + index"
275
275
+
x-model="pour.time"
276
276
+
placeholder="e.g. 45"
277
277
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
278
278
+
</div>
279
279
+
<button
280
280
+
type="button"
281
281
+
@click="removePour(index)"
282
282
+
class="text-brown-700 hover:text-brown-900 mt-5 font-bold"
283
283
+
x-show="pours.length > 0">
284
284
+
✕
285
285
+
</button>
286
286
+
</div>
287
287
+
</template>
288
288
+
</div>
289
289
+
</div>
290
290
+
<div>
291
291
+
<label class="block text-sm font-medium text-brown-900 mb-2">
292
292
+
Temperature
293
293
+
</label>
294
294
+
<input
295
295
+
type="number"
296
296
+
name="temperature"
297
297
+
step="0.1"
298
298
+
299
299
+
placeholder="e.g. 93.5"
300
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
301
+
</div>
302
302
+
<div>
303
303
+
<label class="block text-sm font-medium text-brown-900 mb-2">
304
304
+
Brew Time (seconds)
305
305
+
</label>
306
306
+
<input
307
307
+
type="number"
308
308
+
name="time_seconds"
309
309
+
310
310
+
placeholder="e.g. 180"
311
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
312
+
</div>
313
313
+
<div>
314
314
+
<label class="block text-sm font-medium text-brown-900 mb-2">
315
315
+
Tasting Notes
316
316
+
</label>
317
317
+
<textarea
318
318
+
name="tasting_notes"
319
319
+
rows="4"
320
320
+
placeholder="Describe the flavors, aroma, and your thoughts..."
321
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
322
+
</div>
323
323
+
<div>
324
324
+
<label class="block text-sm font-medium text-brown-900 mb-2">
325
325
+
Rating
326
326
+
</label>
327
327
+
<input
328
328
+
type="range"
329
329
+
name="rating"
330
330
+
min="1"
331
331
+
max="10"
332
332
+
value="7"
333
333
+
x-model="rating"
334
334
+
x-init="rating = $el.value"
335
335
+
class="w-full accent-brown-700"/>
336
336
+
<div class="text-center text-2xl font-bold text-brown-800">
337
337
+
<span x-text="rating"></span>
338
338
+
/10
339
339
+
</div>
340
340
+
</div>
341
341
+
<div>
342
342
+
<button
343
343
+
type="submit"
344
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
345
+
Update Brew
346
346
+
</button>
347
347
+
</div>
348
348
+
</form>
349
349
+
</div>
350
350
+
</div>
+351
internal/bff/__snapshots__/edit_brew_without_loaded_collections.snap
···
1
1
+
---
2
2
+
title: edit brew without loaded collections
3
3
+
test_name: TestBrewForm_EditBrew_Snapshot/edit_brew_without_loaded_collections
4
4
+
file_name: form_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<script src="/static/js/brew-form.js"></script>
8
8
+
<div class="max-w-2xl mx-auto">
9
9
+
<div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300">
10
10
+
<h2 class="text-3xl font-bold text-brown-900 mb-6">
11
11
+
Edit Brew
12
12
+
</h2>
13
13
+
<form
14
14
+
15
15
+
hx-put="/brews/brew999"
16
16
+
17
17
+
hx-target="body"
18
18
+
class="space-y-6"
19
19
+
x-data="brewForm()"
20
20
+
>
21
21
+
<div>
22
22
+
<label class="block text-sm font-medium text-brown-900 mb-2">
23
23
+
Coffee Bean
24
24
+
</label>
25
25
+
<div class="flex gap-2">
26
26
+
<select
27
27
+
name="bean_rkey"
28
28
+
required
29
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
30
+
<option value="">
31
31
+
Select a bean...
32
32
+
</option>
33
33
+
<option value="bean1" selected>
34
34
+
Loading...
35
35
+
</option>
36
36
+
</select>
37
37
+
<button
38
38
+
type="button"
39
39
+
@click="showNewBean = true"
40
40
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
41
41
+
+ New
42
42
+
</button>
43
43
+
</div>
44
44
+
<div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
45
45
+
<h4 class="font-medium mb-3 text-gray-800">
46
46
+
Add New Bean
47
47
+
</h4>
48
48
+
<div class="space-y-3">
49
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
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
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
52
+
<option value="">
53
53
+
Select Roaster (Optional)
54
54
+
</option>
55
55
+
</select>
56
56
+
<select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
57
57
+
<option value="">
58
58
+
Select Roast Level (Optional)
59
59
+
</option>
60
60
+
<option value="Ultra-Light">
61
61
+
Ultra-Light
62
62
+
</option>
63
63
+
<option value="Light">
64
64
+
Light
65
65
+
</option>
66
66
+
<option value="Medium-Light">
67
67
+
Medium-Light
68
68
+
</option>
69
69
+
<option value="Medium">
70
70
+
Medium
71
71
+
</option>
72
72
+
<option value="Medium-Dark">
73
73
+
Medium-Dark
74
74
+
</option>
75
75
+
<option value="Dark">
76
76
+
Dark
77
77
+
</option>
78
78
+
</select>
79
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
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
81
+
<div class="flex gap-2">
82
82
+
<button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
83
83
+
Add
84
84
+
</button>
85
85
+
<button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
86
86
+
Cancel
87
87
+
</button>
88
88
+
</div>
89
89
+
</div>
90
90
+
</div>
91
91
+
</div>
92
92
+
<div>
93
93
+
<label class="block text-sm font-medium text-brown-900 mb-2">
94
94
+
Coffee Amount (grams)
95
95
+
</label>
96
96
+
<input
97
97
+
type="number"
98
98
+
name="coffee_amount"
99
99
+
step="0.1"
100
100
+
101
101
+
placeholder="e.g. 18"
102
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
103
+
<p class="text-sm text-brown-700 mt-1">
104
104
+
Amount of ground coffee used
105
105
+
</p>
106
106
+
</div>
107
107
+
<div>
108
108
+
<label class="block text-sm font-medium text-brown-900 mb-2">
109
109
+
Grinder
110
110
+
</label>
111
111
+
<div class="flex gap-2">
112
112
+
<select
113
113
+
name="grinder_rkey"
114
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
115
+
<option value="">
116
116
+
Select a grinder...
117
117
+
</option>
118
118
+
<option value="grinder1" selected>
119
119
+
Loading...
120
120
+
</option>
121
121
+
</select>
122
122
+
<button
123
123
+
type="button"
124
124
+
@click="showNewGrinder = true"
125
125
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
126
126
+
+ New
127
127
+
</button>
128
128
+
</div>
129
129
+
<div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
130
130
+
<h4 class="font-medium mb-3 text-gray-800">
131
131
+
Add New Grinder
132
132
+
</h4>
133
133
+
<div class="space-y-3">
134
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
135
+
<select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
136
136
+
<option value="">
137
137
+
Grinder Type (Optional)
138
138
+
</option>
139
139
+
<option value="Hand">
140
140
+
Hand
141
141
+
</option>
142
142
+
<option value="Electric">
143
143
+
Electric
144
144
+
</option>
145
145
+
<option value="Electric Hand">
146
146
+
Electric Hand
147
147
+
</option>
148
148
+
</select>
149
149
+
<select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
150
150
+
<option value="">
151
151
+
Burr Type (Optional)
152
152
+
</option>
153
153
+
<option value="Conical">
154
154
+
Conical
155
155
+
</option>
156
156
+
<option value="Flat">
157
157
+
Flat
158
158
+
</option>
159
159
+
</select>
160
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
161
+
<div class="flex gap-2">
162
162
+
<button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
163
163
+
Add
164
164
+
</button>
165
165
+
<button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
166
166
+
Cancel
167
167
+
</button>
168
168
+
</div>
169
169
+
</div>
170
170
+
</div>
171
171
+
</div>
172
172
+
<div>
173
173
+
<label class="block text-sm font-medium text-brown-900 mb-2">
174
174
+
Grind Size
175
175
+
</label>
176
176
+
<input
177
177
+
type="text"
178
178
+
name="grind_size"
179
179
+
value=""
180
180
+
placeholder="e.g. 18, Medium, 3.5, Fine"
181
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
182
+
<p class="text-sm text-brown-700 mt-1">
183
183
+
Enter a number (grinder setting) or description (e.g. "Medium", "Fine")
184
184
+
</p>
185
185
+
</div>
186
186
+
<div>
187
187
+
<label class="block text-sm font-medium text-brown-900 mb-2">
188
188
+
Brew Method
189
189
+
</label>
190
190
+
<div class="flex gap-2">
191
191
+
<select
192
192
+
name="brewer_rkey"
193
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
194
+
<option value="">
195
195
+
Select brew method...
196
196
+
</option>
197
197
+
<option value="brewer1" selected>
198
198
+
Loading...
199
199
+
</option>
200
200
+
</select>
201
201
+
<button
202
202
+
type="button"
203
203
+
@click="showNewBrewer = true"
204
204
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
205
205
+
+ New
206
206
+
</button>
207
207
+
</div>
208
208
+
<div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
209
209
+
<h4 class="font-medium mb-3 text-gray-800">
210
210
+
Add New Brewer
211
211
+
</h4>
212
212
+
<div class="space-y-3">
213
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
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
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
216
+
<div class="flex gap-2">
217
217
+
<button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
218
218
+
Add
219
219
+
</button>
220
220
+
<button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
221
221
+
Cancel
222
222
+
</button>
223
223
+
</div>
224
224
+
</div>
225
225
+
</div>
226
226
+
</div>
227
227
+
<div>
228
228
+
<label class="block text-sm font-medium text-brown-900 mb-2">
229
229
+
Water Amount (grams)
230
230
+
</label>
231
231
+
<input
232
232
+
type="number"
233
233
+
name="water_amount"
234
234
+
step="1"
235
235
+
236
236
+
placeholder="e.g. 250"
237
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
238
+
<p class="text-sm text-brown-700 mt-1">
239
239
+
Total water used (or leave empty if using pours below)
240
240
+
</p>
241
241
+
</div>
242
242
+
<div>
243
243
+
<div class="flex items-center justify-between mb-2">
244
244
+
<label class="block text-sm font-medium text-brown-900">
245
245
+
Pours (Optional)
246
246
+
</label>
247
247
+
<button
248
248
+
type="button"
249
249
+
@click="addPour()"
250
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
251
+
+ Add Pour
252
252
+
</button>
253
253
+
</div>
254
254
+
<p class="text-sm text-brown-700 mb-3">
255
255
+
Track individual pours for bloom and subsequent additions
256
256
+
</p>
257
257
+
<div class="space-y-3">
258
258
+
<template x-for="(pour, index) in pours" :key="index">
259
259
+
<div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200">
260
260
+
<div class="flex-1">
261
261
+
<label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label>
262
262
+
<input
263
263
+
type="number"
264
264
+
:name="'pour_water_' + index"
265
265
+
x-model="pour.water"
266
266
+
placeholder="Water (g)"
267
267
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
268
268
+
</div>
269
269
+
<div class="flex-1">
270
270
+
<label class="text-xs text-brown-700 font-medium">
271
271
+
Time (sec)
272
272
+
</label>
273
273
+
<input
274
274
+
type="number"
275
275
+
:name="'pour_time_' + index"
276
276
+
x-model="pour.time"
277
277
+
placeholder="e.g. 45"
278
278
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
279
279
+
</div>
280
280
+
<button
281
281
+
type="button"
282
282
+
@click="removePour(index)"
283
283
+
class="text-brown-700 hover:text-brown-900 mt-5 font-bold"
284
284
+
x-show="pours.length > 0">
285
285
+
✕
286
286
+
</button>
287
287
+
</div>
288
288
+
</template>
289
289
+
</div>
290
290
+
</div>
291
291
+
<div>
292
292
+
<label class="block text-sm font-medium text-brown-900 mb-2">
293
293
+
Temperature
294
294
+
</label>
295
295
+
<input
296
296
+
type="number"
297
297
+
name="temperature"
298
298
+
step="0.1"
299
299
+
300
300
+
placeholder="e.g. 93.5"
301
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
302
+
</div>
303
303
+
<div>
304
304
+
<label class="block text-sm font-medium text-brown-900 mb-2">
305
305
+
Brew Time (seconds)
306
306
+
</label>
307
307
+
<input
308
308
+
type="number"
309
309
+
name="time_seconds"
310
310
+
311
311
+
placeholder="e.g. 180"
312
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
313
+
</div>
314
314
+
<div>
315
315
+
<label class="block text-sm font-medium text-brown-900 mb-2">
316
316
+
Tasting Notes
317
317
+
</label>
318
318
+
<textarea
319
319
+
name="tasting_notes"
320
320
+
rows="4"
321
321
+
placeholder="Describe the flavors, aroma, and your thoughts..."
322
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
323
+
</div>
324
324
+
<div>
325
325
+
<label class="block text-sm font-medium text-brown-900 mb-2">
326
326
+
Rating
327
327
+
</label>
328
328
+
<input
329
329
+
type="range"
330
330
+
name="rating"
331
331
+
min="1"
332
332
+
max="10"
333
333
+
value="6"
334
334
+
x-model="rating"
335
335
+
x-init="rating = $el.value"
336
336
+
class="w-full accent-brown-700"/>
337
337
+
<div class="text-center text-2xl font-bold text-brown-800">
338
338
+
<span x-text="rating"></span>
339
339
+
/10
340
340
+
</div>
341
341
+
</div>
342
342
+
<div>
343
343
+
<button
344
344
+
type="submit"
345
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
346
+
Update Brew
347
347
+
</button>
348
348
+
</div>
349
349
+
</form>
350
350
+
</div>
351
351
+
</div>
+11
internal/bff/__snapshots__/empty_brew_list_other_profile.snap
···
1
1
+
---
2
2
+
title: empty brew list other profile
3
3
+
test_name: TestBrewListContent_Snapshot/empty_brew_list_other_profile
4
4
+
file_name: partial_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
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
8
+
<p class="text-brown-800 text-lg font-medium">
9
9
+
No brews yet.
10
10
+
</p>
11
11
+
</div>
+15
internal/bff/__snapshots__/empty_brew_list_own_profile.snap
···
1
1
+
---
2
2
+
title: empty brew list own profile
3
3
+
test_name: TestBrewListContent_Snapshot/empty_brew_list_own_profile
4
4
+
file_name: partial_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
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
8
+
<p class="text-brown-800 text-lg mb-4 font-medium">
9
9
+
No brews yet! Start tracking your coffee journey.
10
10
+
</p>
11
11
+
<a href="/brews/new"
12
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
13
+
Add Your First Brew
14
14
+
</a>
15
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
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
7
+
<div class="space-y-4">
8
8
+
<div class="bg-brown-100 rounded-lg p-6 text-center text-brown-700 border border-brown-200">
9
9
+
<p class="mb-2 font-medium">
10
10
+
No activity in the feed yet.
11
11
+
</p>
12
12
+
<p class="text-sm">
13
13
+
Be the first to add something!
14
14
+
</p>
15
15
+
</div>
16
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
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
7
+
<div class="space-y-4">
8
8
+
<div class="bg-brown-100 rounded-lg p-6 text-center text-brown-700 border border-brown-200">
9
9
+
<p class="mb-2 font-medium">
10
10
+
No activity in the feed yet.
11
11
+
</p>
12
12
+
<p class="text-sm">
13
13
+
Be the first to add something!
14
14
+
</p>
15
15
+
</div>
16
16
+
</div>
+48
internal/bff/__snapshots__/grinder_form_renders.snap
···
1
1
+
---
2
2
+
title: grinder_form_renders
3
3
+
test_name: TestNewGrinderForm_Snapshot/grinder_form_renders
4
4
+
file_name: form_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
8
8
+
<h4 class="font-medium mb-3 text-gray-800">
9
9
+
Add New Grinder
10
10
+
</h4>
11
11
+
<div class="space-y-3">
12
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
13
+
<select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
14
14
+
<option value="">
15
15
+
Grinder Type (Optional)
16
16
+
</option>
17
17
+
<option value="Hand">
18
18
+
Hand
19
19
+
</option>
20
20
+
<option value="Electric">
21
21
+
Electric
22
22
+
</option>
23
23
+
<option value="Electric Hand">
24
24
+
Electric Hand
25
25
+
</option>
26
26
+
</select>
27
27
+
<select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
28
28
+
<option value="">
29
29
+
Burr Type (Optional)
30
30
+
</option>
31
31
+
<option value="Conical">
32
32
+
Conical
33
33
+
</option>
34
34
+
<option value="Flat">
35
35
+
Flat
36
36
+
</option>
37
37
+
</select>
38
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
39
+
<div class="flex gap-2">
40
40
+
<button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
41
41
+
Add
42
42
+
</button>
43
43
+
<button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
44
44
+
Cancel
45
45
+
</button>
46
46
+
</div>
47
47
+
</div>
48
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
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
7
+
<div class="space-y-4">
8
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
9
+
<div class="flex items-center gap-3 mb-3">
10
10
+
<a href="/profile/gearhead" class="flex-shrink-0">
11
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
12
+
<span class="text-brown-600 text-sm">
13
13
+
?
14
14
+
</span>
15
15
+
</div>
16
16
+
</a>
17
17
+
<div class="flex-1 min-w-0">
18
18
+
<div class="flex items-center gap-2">
19
19
+
<a href="/profile/gearhead" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline">
20
20
+
Coffee Gear Head
21
21
+
</a>
22
22
+
<a href="/profile/gearhead" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
23
23
+
@gearhead
24
24
+
</a>
25
25
+
</div>
26
26
+
<span class="text-brown-500 text-sm">
27
27
+
30 minutes ago
28
28
+
</span>
29
29
+
</div>
30
30
+
</div>
31
31
+
<div class="mb-2 text-sm text-brown-700">
32
32
+
⚙️ added a new grinder
33
33
+
</div>
34
34
+
<div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200">
35
35
+
<div class="text-base mb-2">
36
36
+
<span class="font-bold text-brown-900">
37
37
+
Comandante C40
38
38
+
</span>
39
39
+
</div>
40
40
+
<div class="text-sm text-brown-700 space-y-1">
41
41
+
<div>
42
42
+
<span class="text-brown-600">
43
43
+
Type:
44
44
+
</span>
45
45
+
Hand
46
46
+
</div>
47
47
+
<div>
48
48
+
<span class="text-brown-600">
49
49
+
Burr:
50
50
+
</span>
51
51
+
Conical
52
52
+
</div>
53
53
+
<div class="mt-2 text-brown-800 italic">
54
54
+
"Excellent for pour over"
55
55
+
</div>
56
56
+
</div>
57
57
+
</div>
58
58
+
</div>
59
59
+
</div>
+210
internal/bff/__snapshots__/grinders_empty.snap
···
1
1
+
---
2
2
+
title: grinders empty
3
3
+
test_name: TestManageContent_GrindersTab_Snapshot/grinders_empty
4
4
+
file_name: partial_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="tab === 'beans'">
8
8
+
<div class="mb-4 flex justify-between items-center">
9
9
+
<h3 class="text-xl font-semibold text-brown-900">
10
10
+
Coffee Beans
11
11
+
</h3>
12
12
+
<button
13
13
+
@click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}"
14
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
15
+
+ Add Bean
16
16
+
</button>
17
17
+
</div>
18
18
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
19
19
+
No beans yet. Add your first bean to get started!
20
20
+
</div>
21
21
+
</div>
22
22
+
<div x-show="tab === 'roasters'">
23
23
+
<div class="mb-4 flex justify-between items-center">
24
24
+
<h3 class="text-xl font-semibold text-brown-900">
25
25
+
Roasters
26
26
+
</h3>
27
27
+
<button
28
28
+
@click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}"
29
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
30
+
+ Add Roaster
31
31
+
</button>
32
32
+
</div>
33
33
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
34
34
+
No roasters yet. Add your first roaster!
35
35
+
</div>
36
36
+
</div>
37
37
+
<div x-show="tab === 'grinders'">
38
38
+
<div class="mb-4 flex justify-between items-center">
39
39
+
<h3 class="text-xl font-semibold text-brown-900">
40
40
+
Grinders
41
41
+
</h3>
42
42
+
<button
43
43
+
@click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}"
44
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
45
+
+ Add Grinder
46
46
+
</button>
47
47
+
</div>
48
48
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
49
49
+
No grinders yet. Add your first grinder!
50
50
+
</div>
51
51
+
</div>
52
52
+
<div x-show="tab === 'brewers'">
53
53
+
<div class="mb-4 flex justify-between items-center">
54
54
+
<h3 class="text-xl font-semibold text-brown-900">
55
55
+
Brewers
56
56
+
</h3>
57
57
+
<button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}"
58
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
59
+
+ Add Brewer
60
60
+
</button>
61
61
+
</div>
62
62
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
63
63
+
No brewers yet. Add your first brewer!
64
64
+
</div>
65
65
+
</div>
66
66
+
<div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
67
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
68
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3>
69
69
+
<div class="space-y-4">
70
70
+
<input type="text" x-model="beanForm.name" placeholder="Name *"
71
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
72
+
<input type="text" x-model="beanForm.origin" placeholder="Origin *"
73
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
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
75
+
<option value="">
76
76
+
Select Roaster (Optional)
77
77
+
</option>
78
78
+
</select>
79
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
80
+
<option value="">
81
81
+
Select Roast Level (Optional)
82
82
+
</option>
83
83
+
<option value="Ultra-Light">
84
84
+
Ultra-Light
85
85
+
</option>
86
86
+
<option value="Light">
87
87
+
Light
88
88
+
</option>
89
89
+
<option value="Medium-Light">
90
90
+
Medium-Light
91
91
+
</option>
92
92
+
<option value="Medium">
93
93
+
Medium
94
94
+
</option>
95
95
+
<option value="Medium-Dark">
96
96
+
Medium-Dark
97
97
+
</option>
98
98
+
<option value="Dark">
99
99
+
Dark
100
100
+
</option>
101
101
+
</select>
102
102
+
<input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)"
103
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
104
+
<textarea x-model="beanForm.description" placeholder="Description" rows="3"
105
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
106
+
<div class="flex gap-2">
107
107
+
<button @click="saveBean()"
108
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
109
+
Save
110
110
+
</button>
111
111
+
<button @click="showBeanForm = false"
112
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
113
+
Cancel
114
114
+
</button>
115
115
+
</div>
116
116
+
</div>
117
117
+
</div>
118
118
+
</div>
119
119
+
<div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
120
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
121
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3>
122
122
+
<div class="space-y-4">
123
123
+
<input type="text" x-model="roasterForm.name" placeholder="Name *"
124
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
125
+
<input type="text" x-model="roasterForm.location" placeholder="Location"
126
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
127
+
<input type="url" x-model="roasterForm.website" placeholder="Website"
128
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
129
+
<div class="flex gap-2">
130
130
+
<button @click="saveRoaster()"
131
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
132
+
Save
133
133
+
</button>
134
134
+
<button @click="showRoasterForm = false"
135
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
136
+
Cancel
137
137
+
</button>
138
138
+
</div>
139
139
+
</div>
140
140
+
</div>
141
141
+
</div>
142
142
+
<div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
143
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
144
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3>
145
145
+
<div class="space-y-4">
146
146
+
<input type="text" x-model="grinderForm.name" placeholder="Name *"
147
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
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
149
+
<option value="">
150
150
+
Select Grinder Type *
151
151
+
</option>
152
152
+
<option value="Hand">
153
153
+
Hand
154
154
+
</option>
155
155
+
<option value="Electric">
156
156
+
Electric
157
157
+
</option>
158
158
+
<option value="Portable Electric">
159
159
+
Portable Electric
160
160
+
</option>
161
161
+
</select>
162
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
163
+
<option value="">
164
164
+
Select Burr Type (Optional)
165
165
+
</option>
166
166
+
<option value="Conical">
167
167
+
Conical
168
168
+
</option>
169
169
+
<option value="Flat">
170
170
+
Flat
171
171
+
</option>
172
172
+
</select>
173
173
+
<textarea x-model="grinderForm.notes" placeholder="Notes" rows="3"
174
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
175
+
<div class="flex gap-2">
176
176
+
<button @click="saveGrinder()"
177
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
178
+
Save
179
179
+
</button>
180
180
+
<button @click="showGrinderForm = false"
181
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
182
+
Cancel
183
183
+
</button>
184
184
+
</div>
185
185
+
</div>
186
186
+
</div>
187
187
+
</div>
188
188
+
<div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
189
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
190
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3>
191
191
+
<div class="space-y-4">
192
192
+
<input type="text" x-model="brewerForm.name" placeholder="Name *"
193
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
194
+
<input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)"
195
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
196
+
<textarea x-model="brewerForm.description" placeholder="Description" rows="3"
197
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
198
+
<div class="flex gap-2">
199
199
+
<button @click="saveBrewer()"
200
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
201
+
Save
202
202
+
</button>
203
203
+
<button @click="showBrewerForm = false"
204
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
205
+
Cancel
206
206
+
</button>
207
207
+
</div>
208
208
+
</div>
209
209
+
</div>
210
210
+
</div>
+278
internal/bff/__snapshots__/grinders_with_data.snap
···
1
1
+
---
2
2
+
title: grinders with data
3
3
+
test_name: TestManageContent_GrindersTab_Snapshot/grinders_with_data
4
4
+
file_name: partial_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="tab === 'beans'">
8
8
+
<div class="mb-4 flex justify-between items-center">
9
9
+
<h3 class="text-xl font-semibold text-brown-900">
10
10
+
Coffee Beans
11
11
+
</h3>
12
12
+
<button
13
13
+
@click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}"
14
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
15
+
+ Add Bean
16
16
+
</button>
17
17
+
</div>
18
18
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
19
19
+
No beans yet. Add your first bean to get started!
20
20
+
</div>
21
21
+
</div>
22
22
+
<div x-show="tab === 'roasters'">
23
23
+
<div class="mb-4 flex justify-between items-center">
24
24
+
<h3 class="text-xl font-semibold text-brown-900">
25
25
+
Roasters
26
26
+
</h3>
27
27
+
<button
28
28
+
@click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}"
29
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
30
+
+ Add Roaster
31
31
+
</button>
32
32
+
</div>
33
33
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
34
34
+
No roasters yet. Add your first roaster!
35
35
+
</div>
36
36
+
</div>
37
37
+
<div x-show="tab === 'grinders'">
38
38
+
<div class="mb-4 flex justify-between items-center">
39
39
+
<h3 class="text-xl font-semibold text-brown-900">
40
40
+
Grinders
41
41
+
</h3>
42
42
+
<button
43
43
+
@click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}"
44
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
45
+
+ Add Grinder
46
46
+
</button>
47
47
+
</div>
48
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
49
+
<table class="min-w-full divide-y divide-brown-300">
50
50
+
<thead class="bg-brown-200/80">
51
51
+
<tr>
52
52
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
53
53
+
Name
54
54
+
</th>
55
55
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
56
56
+
🔧 Grinder Type
57
57
+
</th>
58
58
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
59
59
+
💎 Burr Type
60
60
+
</th>
61
61
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
62
62
+
📝 Notes
63
63
+
</th>
64
64
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
65
65
+
Actions
66
66
+
</th>
67
67
+
</tr>
68
68
+
</thead>
69
69
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
70
70
+
<tr class="hover:bg-brown-100/60 transition-colors">
71
71
+
<td class="px-6 py-4 text-sm font-medium text-brown-900">
72
72
+
Comandante C40 MK3
73
73
+
</td>
74
74
+
<td class="px-6 py-4 text-sm text-brown-900">
75
75
+
Hand
76
76
+
</td>
77
77
+
<td class="px-6 py-4 text-sm text-brown-900">
78
78
+
Conical
79
79
+
</td>
80
80
+
<td class="px-6 py-4 text-sm text-brown-700">
81
81
+
Excellent consistency, great for pour-over
82
82
+
</td>
83
83
+
<td class="px-6 py-4 text-sm font-medium space-x-2">
84
84
+
<button @click="editGrinder('grinder1', 'Comandante C40 MK3', 'Hand', 'Conical', 'Excellent consistency, great for pour-over')"
85
85
+
class="text-brown-700 hover:text-brown-900 font-medium">
86
86
+
Edit
87
87
+
</button>
88
88
+
<button @click="deleteGrinder('grinder1')"
89
89
+
class="text-brown-600 hover:text-brown-800 font-medium">
90
90
+
Delete
91
91
+
</button>
92
92
+
</td>
93
93
+
</tr>
94
94
+
<tr class="hover:bg-brown-100/60 transition-colors">
95
95
+
<td class="px-6 py-4 text-sm font-medium text-brown-900">
96
96
+
Baratza Encore
97
97
+
</td>
98
98
+
<td class="px-6 py-4 text-sm text-brown-900">
99
99
+
Electric
100
100
+
</td>
101
101
+
<td class="px-6 py-4 text-sm text-brown-900">
102
102
+
Conical
103
103
+
</td>
104
104
+
<td class="px-6 py-4 text-sm text-brown-700"></td>
105
105
+
<td class="px-6 py-4 text-sm font-medium space-x-2">
106
106
+
<button @click="editGrinder('grinder2', 'Baratza Encore', 'Electric', 'Conical', '')"
107
107
+
class="text-brown-700 hover:text-brown-900 font-medium">
108
108
+
Edit
109
109
+
</button>
110
110
+
<button @click="deleteGrinder('grinder2')"
111
111
+
class="text-brown-600 hover:text-brown-800 font-medium">
112
112
+
Delete
113
113
+
</button>
114
114
+
</td>
115
115
+
</tr>
116
116
+
</tbody>
117
117
+
</table>
118
118
+
</div>
119
119
+
</div>
120
120
+
<div x-show="tab === 'brewers'">
121
121
+
<div class="mb-4 flex justify-between items-center">
122
122
+
<h3 class="text-xl font-semibold text-brown-900">
123
123
+
Brewers
124
124
+
</h3>
125
125
+
<button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}"
126
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
127
+
+ Add Brewer
128
128
+
</button>
129
129
+
</div>
130
130
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
131
131
+
No brewers yet. Add your first brewer!
132
132
+
</div>
133
133
+
</div>
134
134
+
<div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
135
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
136
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3>
137
137
+
<div class="space-y-4">
138
138
+
<input type="text" x-model="beanForm.name" placeholder="Name *"
139
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
140
+
<input type="text" x-model="beanForm.origin" placeholder="Origin *"
141
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
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
143
+
<option value="">
144
144
+
Select Roaster (Optional)
145
145
+
</option>
146
146
+
</select>
147
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
148
+
<option value="">
149
149
+
Select Roast Level (Optional)
150
150
+
</option>
151
151
+
<option value="Ultra-Light">
152
152
+
Ultra-Light
153
153
+
</option>
154
154
+
<option value="Light">
155
155
+
Light
156
156
+
</option>
157
157
+
<option value="Medium-Light">
158
158
+
Medium-Light
159
159
+
</option>
160
160
+
<option value="Medium">
161
161
+
Medium
162
162
+
</option>
163
163
+
<option value="Medium-Dark">
164
164
+
Medium-Dark
165
165
+
</option>
166
166
+
<option value="Dark">
167
167
+
Dark
168
168
+
</option>
169
169
+
</select>
170
170
+
<input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)"
171
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
172
+
<textarea x-model="beanForm.description" placeholder="Description" rows="3"
173
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
174
+
<div class="flex gap-2">
175
175
+
<button @click="saveBean()"
176
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
177
+
Save
178
178
+
</button>
179
179
+
<button @click="showBeanForm = false"
180
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
181
+
Cancel
182
182
+
</button>
183
183
+
</div>
184
184
+
</div>
185
185
+
</div>
186
186
+
</div>
187
187
+
<div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
188
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
189
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3>
190
190
+
<div class="space-y-4">
191
191
+
<input type="text" x-model="roasterForm.name" placeholder="Name *"
192
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
193
+
<input type="text" x-model="roasterForm.location" placeholder="Location"
194
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
195
+
<input type="url" x-model="roasterForm.website" placeholder="Website"
196
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
197
+
<div class="flex gap-2">
198
198
+
<button @click="saveRoaster()"
199
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
200
+
Save
201
201
+
</button>
202
202
+
<button @click="showRoasterForm = false"
203
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
204
+
Cancel
205
205
+
</button>
206
206
+
</div>
207
207
+
</div>
208
208
+
</div>
209
209
+
</div>
210
210
+
<div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
211
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
212
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3>
213
213
+
<div class="space-y-4">
214
214
+
<input type="text" x-model="grinderForm.name" placeholder="Name *"
215
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
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
217
+
<option value="">
218
218
+
Select Grinder Type *
219
219
+
</option>
220
220
+
<option value="Hand">
221
221
+
Hand
222
222
+
</option>
223
223
+
<option value="Electric">
224
224
+
Electric
225
225
+
</option>
226
226
+
<option value="Portable Electric">
227
227
+
Portable Electric
228
228
+
</option>
229
229
+
</select>
230
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
231
+
<option value="">
232
232
+
Select Burr Type (Optional)
233
233
+
</option>
234
234
+
<option value="Conical">
235
235
+
Conical
236
236
+
</option>
237
237
+
<option value="Flat">
238
238
+
Flat
239
239
+
</option>
240
240
+
</select>
241
241
+
<textarea x-model="grinderForm.notes" placeholder="Notes" rows="3"
242
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
243
+
<div class="flex gap-2">
244
244
+
<button @click="saveGrinder()"
245
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
246
+
Save
247
247
+
</button>
248
248
+
<button @click="showGrinderForm = false"
249
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
250
+
Cancel
251
251
+
</button>
252
252
+
</div>
253
253
+
</div>
254
254
+
</div>
255
255
+
</div>
256
256
+
<div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
257
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
258
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3>
259
259
+
<div class="space-y-4">
260
260
+
<input type="text" x-model="brewerForm.name" placeholder="Name *"
261
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
262
+
<input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)"
263
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
264
+
<textarea x-model="brewerForm.description" placeholder="Description" rows="3"
265
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
266
+
<div class="flex gap-2">
267
267
+
<button @click="saveBrewer()"
268
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
269
+
Save
270
270
+
</button>
271
271
+
<button @click="showBrewerForm = false"
272
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
273
+
Cancel
274
274
+
</button>
275
275
+
</div>
276
276
+
</div>
277
277
+
</div>
278
278
+
</div>
+256
internal/bff/__snapshots__/grinders_with_unicode.snap
···
1
1
+
---
2
2
+
title: grinders with unicode
3
3
+
test_name: TestManageContent_SpecialCharacters_Snapshot/grinders_with_unicode
4
4
+
file_name: partial_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="tab === 'beans'">
8
8
+
<div class="mb-4 flex justify-between items-center">
9
9
+
<h3 class="text-xl font-semibold text-brown-900">
10
10
+
Coffee Beans
11
11
+
</h3>
12
12
+
<button
13
13
+
@click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}"
14
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
15
+
+ Add Bean
16
16
+
</button>
17
17
+
</div>
18
18
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
19
19
+
No beans yet. Add your first bean to get started!
20
20
+
</div>
21
21
+
</div>
22
22
+
<div x-show="tab === 'roasters'">
23
23
+
<div class="mb-4 flex justify-between items-center">
24
24
+
<h3 class="text-xl font-semibold text-brown-900">
25
25
+
Roasters
26
26
+
</h3>
27
27
+
<button
28
28
+
@click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}"
29
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
30
+
+ Add Roaster
31
31
+
</button>
32
32
+
</div>
33
33
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
34
34
+
No roasters yet. Add your first roaster!
35
35
+
</div>
36
36
+
</div>
37
37
+
<div x-show="tab === 'grinders'">
38
38
+
<div class="mb-4 flex justify-between items-center">
39
39
+
<h3 class="text-xl font-semibold text-brown-900">
40
40
+
Grinders
41
41
+
</h3>
42
42
+
<button
43
43
+
@click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}"
44
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
45
+
+ Add Grinder
46
46
+
</button>
47
47
+
</div>
48
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
49
+
<table class="min-w-full divide-y divide-brown-300">
50
50
+
<thead class="bg-brown-200/80">
51
51
+
<tr>
52
52
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
53
53
+
Name
54
54
+
</th>
55
55
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
56
56
+
🔧 Grinder Type
57
57
+
</th>
58
58
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
59
59
+
💎 Burr Type
60
60
+
</th>
61
61
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
62
62
+
📝 Notes
63
63
+
</th>
64
64
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
65
65
+
Actions
66
66
+
</th>
67
67
+
</tr>
68
68
+
</thead>
69
69
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
70
70
+
<tr class="hover:bg-brown-100/60 transition-colors">
71
71
+
<td class="px-6 py-4 text-sm font-medium text-brown-900">
72
72
+
手動コーヒーミル Comandante® C40
73
73
+
</td>
74
74
+
<td class="px-6 py-4 text-sm text-brown-900">
75
75
+
Hand
76
76
+
</td>
77
77
+
<td class="px-6 py-4 text-sm text-brown-900">
78
78
+
Conical
79
79
+
</td>
80
80
+
<td class="px-6 py-4 text-sm text-brown-700">
81
81
+
日本語のノート - Отличная кофемолка 🇯🇵
82
82
+
</td>
83
83
+
<td class="px-6 py-4 text-sm font-medium space-x-2">
84
84
+
<button @click="editGrinder('grinder1', '手動コーヒーミル Comandante® C40', 'Hand', 'Conical', '日本語のノート - Отличная кофемолка 🇯🇵')"
85
85
+
class="text-brown-700 hover:text-brown-900 font-medium">
86
86
+
Edit
87
87
+
</button>
88
88
+
<button @click="deleteGrinder('grinder1')"
89
89
+
class="text-brown-600 hover:text-brown-800 font-medium">
90
90
+
Delete
91
91
+
</button>
92
92
+
</td>
93
93
+
</tr>
94
94
+
</tbody>
95
95
+
</table>
96
96
+
</div>
97
97
+
</div>
98
98
+
<div x-show="tab === 'brewers'">
99
99
+
<div class="mb-4 flex justify-between items-center">
100
100
+
<h3 class="text-xl font-semibold text-brown-900">
101
101
+
Brewers
102
102
+
</h3>
103
103
+
<button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}"
104
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
105
+
+ Add Brewer
106
106
+
</button>
107
107
+
</div>
108
108
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
109
109
+
No brewers yet. Add your first brewer!
110
110
+
</div>
111
111
+
</div>
112
112
+
<div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
113
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
114
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3>
115
115
+
<div class="space-y-4">
116
116
+
<input type="text" x-model="beanForm.name" placeholder="Name *"
117
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
118
+
<input type="text" x-model="beanForm.origin" placeholder="Origin *"
119
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
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
121
+
<option value="">
122
122
+
Select Roaster (Optional)
123
123
+
</option>
124
124
+
</select>
125
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
126
+
<option value="">
127
127
+
Select Roast Level (Optional)
128
128
+
</option>
129
129
+
<option value="Ultra-Light">
130
130
+
Ultra-Light
131
131
+
</option>
132
132
+
<option value="Light">
133
133
+
Light
134
134
+
</option>
135
135
+
<option value="Medium-Light">
136
136
+
Medium-Light
137
137
+
</option>
138
138
+
<option value="Medium">
139
139
+
Medium
140
140
+
</option>
141
141
+
<option value="Medium-Dark">
142
142
+
Medium-Dark
143
143
+
</option>
144
144
+
<option value="Dark">
145
145
+
Dark
146
146
+
</option>
147
147
+
</select>
148
148
+
<input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)"
149
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
150
+
<textarea x-model="beanForm.description" placeholder="Description" rows="3"
151
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
152
+
<div class="flex gap-2">
153
153
+
<button @click="saveBean()"
154
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
155
+
Save
156
156
+
</button>
157
157
+
<button @click="showBeanForm = false"
158
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
159
+
Cancel
160
160
+
</button>
161
161
+
</div>
162
162
+
</div>
163
163
+
</div>
164
164
+
</div>
165
165
+
<div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
166
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
167
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3>
168
168
+
<div class="space-y-4">
169
169
+
<input type="text" x-model="roasterForm.name" placeholder="Name *"
170
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
171
+
<input type="text" x-model="roasterForm.location" placeholder="Location"
172
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
173
+
<input type="url" x-model="roasterForm.website" placeholder="Website"
174
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
175
+
<div class="flex gap-2">
176
176
+
<button @click="saveRoaster()"
177
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
178
+
Save
179
179
+
</button>
180
180
+
<button @click="showRoasterForm = false"
181
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
182
+
Cancel
183
183
+
</button>
184
184
+
</div>
185
185
+
</div>
186
186
+
</div>
187
187
+
</div>
188
188
+
<div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
189
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
190
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3>
191
191
+
<div class="space-y-4">
192
192
+
<input type="text" x-model="grinderForm.name" placeholder="Name *"
193
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
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
195
+
<option value="">
196
196
+
Select Grinder Type *
197
197
+
</option>
198
198
+
<option value="Hand">
199
199
+
Hand
200
200
+
</option>
201
201
+
<option value="Electric">
202
202
+
Electric
203
203
+
</option>
204
204
+
<option value="Portable Electric">
205
205
+
Portable Electric
206
206
+
</option>
207
207
+
</select>
208
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
209
+
<option value="">
210
210
+
Select Burr Type (Optional)
211
211
+
</option>
212
212
+
<option value="Conical">
213
213
+
Conical
214
214
+
</option>
215
215
+
<option value="Flat">
216
216
+
Flat
217
217
+
</option>
218
218
+
</select>
219
219
+
<textarea x-model="grinderForm.notes" placeholder="Notes" rows="3"
220
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
221
+
<div class="flex gap-2">
222
222
+
<button @click="saveGrinder()"
223
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
224
+
Save
225
225
+
</button>
226
226
+
<button @click="showGrinderForm = false"
227
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
228
+
Cancel
229
229
+
</button>
230
230
+
</div>
231
231
+
</div>
232
232
+
</div>
233
233
+
</div>
234
234
+
<div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
235
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
236
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3>
237
237
+
<div class="space-y-4">
238
238
+
<input type="text" x-model="brewerForm.name" placeholder="Name *"
239
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
240
+
<input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)"
241
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
242
+
<textarea x-model="brewerForm.description" placeholder="Description" rows="3"
243
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
244
+
<div class="flex gap-2">
245
245
+
<button @click="saveBrewer()"
246
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
247
+
Save
248
248
+
</button>
249
249
+
<button @click="showBrewerForm = false"
250
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
251
+
Cancel
252
252
+
</button>
253
253
+
</div>
254
254
+
</div>
255
255
+
</div>
256
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
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
7
+
<div class="space-y-4">
8
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
9
+
<div class="flex items-center gap-3 mb-3">
10
10
+
<a href="/profile/user1" class="flex-shrink-0">
11
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
12
+
<span class="text-brown-600 text-sm">
13
13
+
?
14
14
+
</span>
15
15
+
</div>
16
16
+
</a>
17
17
+
<div class="flex-1 min-w-0">
18
18
+
<div class="flex items-center gap-2">
19
19
+
<a href="/profile/user1" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline">
20
20
+
User One
21
21
+
</a>
22
22
+
<a href="/profile/user1" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
23
23
+
@user1
24
24
+
</a>
25
25
+
</div>
26
26
+
<span class="text-brown-500 text-sm">
27
27
+
1 hour ago
28
28
+
</span>
29
29
+
</div>
30
30
+
</div>
31
31
+
<div class="mb-2 text-sm text-brown-700">
32
32
+
☕ added a new brew
33
33
+
</div>
34
34
+
<div class="bg-white/60 backdrop-blur rounded-lg p-4 border border-brown-200">
35
35
+
<div class="flex items-start justify-between gap-3 mb-3">
36
36
+
<div class="flex-1 min-w-0">
37
37
+
<div class="font-bold text-brown-900 text-base">
38
38
+
Ethiopian Yirgacheffe
39
39
+
</div>
40
40
+
<div class="text-sm text-brown-700 mt-0.5">
41
41
+
<span class="font-medium">
42
42
+
🏪 Onyx
43
43
+
</span>
44
44
+
</div>
45
45
+
<div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5">
46
46
+
<span class="inline-flex items-center gap-0.5">
47
47
+
📍 Ethiopia
48
48
+
</span>
49
49
+
<span class="inline-flex items-center gap-0.5">
50
50
+
🔥 Light
51
51
+
</span>
52
52
+
<span class="inline-flex items-center gap-0.5">
53
53
+
🌱 Washed
54
54
+
</span>
55
55
+
<span class="inline-flex items-center gap-0.5">
56
56
+
⚖️ 16g
57
57
+
</span>
58
58
+
</div>
59
59
+
</div>
60
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
61
+
⭐ 9/10
62
62
+
</span>
63
63
+
</div>
64
64
+
<div class="mb-2">
65
65
+
<span class="text-xs text-brown-600">
66
66
+
Brewer:
67
67
+
</span>
68
68
+
<span class="text-sm font-semibold text-brown-900">
69
69
+
Hario V60
70
70
+
</span>
71
71
+
</div>
72
72
+
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700">
73
73
+
<div>
74
74
+
<span class="text-brown-600">
75
75
+
Grinder:
76
76
+
</span>
77
77
+
1Zpresso JX-Pro (Medium-fine)
78
78
+
</div>
79
79
+
<div class="col-span-2">
80
80
+
<span class="text-brown-600">
81
81
+
Pours:
82
82
+
</span>
83
83
+
<div class="pl-2 text-brown-600">
84
84
+
• 50g @ 30s
85
85
+
</div>
86
86
+
<div class="pl-2 text-brown-600">
87
87
+
• 100g @ 45s
88
88
+
</div>
89
89
+
<div class="pl-2 text-brown-600">
90
90
+
• 100g @ 1m
91
91
+
</div>
92
92
+
</div>
93
93
+
<div>
94
94
+
<span class="text-brown-600">
95
95
+
Temp:
96
96
+
</span>
97
97
+
93.0°C
98
98
+
</div>
99
99
+
<div>
100
100
+
<span class="text-brown-600">
101
101
+
Time:
102
102
+
</span>
103
103
+
3m
104
104
+
</div>
105
105
+
</div>
106
106
+
<div class="mt-3 text-sm text-brown-800 italic border-t border-brown-200 pt-2">
107
107
+
"Bright citrus notes with floral aroma"
108
108
+
</div>
109
109
+
</div>
110
110
+
</div>
111
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
112
+
<div class="flex items-center gap-3 mb-3">
113
113
+
<a href="/profile/user2" class="flex-shrink-0">
114
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
115
+
</a>
116
116
+
<div class="flex-1 min-w-0">
117
117
+
<div class="flex items-center gap-2">
118
118
+
<a href="/profile/user2" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline">
119
119
+
User Two
120
120
+
</a>
121
121
+
<a href="/profile/user2" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
122
122
+
@user2
123
123
+
</a>
124
124
+
</div>
125
125
+
<span class="text-brown-500 text-sm">
126
126
+
1.5 hours ago
127
127
+
</span>
128
128
+
</div>
129
129
+
</div>
130
130
+
<div class="mb-2 text-sm text-brown-700">
131
131
+
🫘 added a new bean
132
132
+
</div>
133
133
+
<div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200">
134
134
+
<div class="text-base mb-2">
135
135
+
<span class="font-bold text-brown-900">
136
136
+
Kenya AA
137
137
+
</span>
138
138
+
<span class="text-brown-700">
139
139
+
from Onyx Coffee Lab
140
140
+
</span>
141
141
+
</div>
142
142
+
<div class="text-sm text-brown-700 space-y-1">
143
143
+
<div>
144
144
+
<span class="text-brown-600">
145
145
+
Origin:
146
146
+
</span>
147
147
+
Kenya
148
148
+
</div>
149
149
+
<div>
150
150
+
<span class="text-brown-600">
151
151
+
Roast:
152
152
+
</span>
153
153
+
Medium
154
154
+
</div>
155
155
+
<div>
156
156
+
<span class="text-brown-600">
157
157
+
Process:
158
158
+
</span>
159
159
+
Natural
160
160
+
</div>
161
161
+
<div class="mt-2 text-brown-800 italic">
162
162
+
"Sweet and fruity with notes of blueberry"
163
163
+
</div>
164
164
+
</div>
165
165
+
</div>
166
166
+
</div>
167
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
168
+
<div class="flex items-center gap-3 mb-3">
169
169
+
<a href="/profile/user3" class="flex-shrink-0">
170
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
171
+
<span class="text-brown-600 text-sm">
172
172
+
?
173
173
+
</span>
174
174
+
</div>
175
175
+
</a>
176
176
+
<div class="flex-1 min-w-0">
177
177
+
<div class="flex items-center gap-2">
178
178
+
<a href="/profile/user3" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline">
179
179
+
User Three
180
180
+
</a>
181
181
+
<a href="/profile/user3" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
182
182
+
@user3
183
183
+
</a>
184
184
+
</div>
185
185
+
<span class="text-brown-500 text-sm">
186
186
+
2 hours ago
187
187
+
</span>
188
188
+
</div>
189
189
+
</div>
190
190
+
<div class="mb-2 text-sm text-brown-700">
191
191
+
🏪 added a new roaster
192
192
+
</div>
193
193
+
<div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200">
194
194
+
<div class="text-base mb-2">
195
195
+
<span class="font-bold text-brown-900">
196
196
+
Heart Coffee Roasters
197
197
+
</span>
198
198
+
</div>
199
199
+
<div class="text-sm text-brown-700 space-y-1">
200
200
+
<div>
201
201
+
<span class="text-brown-600">
202
202
+
Location:
203
203
+
</span>
204
204
+
Portland, OR
205
205
+
</div>
206
206
+
<div>
207
207
+
<span class="text-brown-600">
208
208
+
Website:
209
209
+
</span>
210
210
+
<a href="https://heartroasters.com" target="_blank" rel="noopener noreferrer" class="text-brown-800 hover:underline">
211
211
+
https://heartroasters.com
212
212
+
</a>
213
213
+
</div>
214
214
+
</div>
215
215
+
</div>
216
216
+
</div>
217
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
218
+
<div class="flex items-center gap-3 mb-3">
219
219
+
<a href="/profile/user4" class="flex-shrink-0">
220
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
221
+
<span class="text-brown-600 text-sm">
222
222
+
?
223
223
+
</span>
224
224
+
</div>
225
225
+
</a>
226
226
+
<div class="flex-1 min-w-0">
227
227
+
<div class="flex items-center gap-2">
228
228
+
<a href="/profile/user4" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
229
229
+
@user4
230
230
+
</a>
231
231
+
</div>
232
232
+
<span class="text-brown-500 text-sm">
233
233
+
2.5 hours ago
234
234
+
</span>
235
235
+
</div>
236
236
+
</div>
237
237
+
<div class="mb-2 text-sm text-brown-700">
238
238
+
⚙️ added a new grinder
239
239
+
</div>
240
240
+
<div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200">
241
241
+
<div class="text-base mb-2">
242
242
+
<span class="font-bold text-brown-900">
243
243
+
Comandante C40
244
244
+
</span>
245
245
+
</div>
246
246
+
<div class="text-sm text-brown-700 space-y-1">
247
247
+
<div>
248
248
+
<span class="text-brown-600">
249
249
+
Type:
250
250
+
</span>
251
251
+
Hand
252
252
+
</div>
253
253
+
<div>
254
254
+
<span class="text-brown-600">
255
255
+
Burr:
256
256
+
</span>
257
257
+
Conical
258
258
+
</div>
259
259
+
<div class="mt-2 text-brown-800 italic">
260
260
+
"Excellent for pour over"
261
261
+
</div>
262
262
+
</div>
263
263
+
</div>
264
264
+
</div>
265
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
266
+
<div class="flex items-center gap-3 mb-3">
267
267
+
<a href="/profile/user5" class="flex-shrink-0">
268
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
269
+
<span class="text-brown-600 text-sm">
270
270
+
?
271
271
+
</span>
272
272
+
</div>
273
273
+
</a>
274
274
+
<div class="flex-1 min-w-0">
275
275
+
<div class="flex items-center gap-2">
276
276
+
<a href="/profile/user5" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline">
277
277
+
User Five
278
278
+
</a>
279
279
+
<a href="/profile/user5" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
280
280
+
@user5
281
281
+
</a>
282
282
+
</div>
283
283
+
<span class="text-brown-500 text-sm">
284
284
+
3 hours ago
285
285
+
</span>
286
286
+
</div>
287
287
+
</div>
288
288
+
<div class="mb-2 text-sm text-brown-700">
289
289
+
☕ added a new brewer
290
290
+
</div>
291
291
+
<div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200">
292
292
+
<div class="text-base mb-2">
293
293
+
<span class="font-bold text-brown-900">
294
294
+
Kalita Wave 185
295
295
+
</span>
296
296
+
</div>
297
297
+
<div class="text-sm text-brown-800 italic">
298
298
+
"Flat-bottom dripper with wave filters"
299
299
+
</div>
300
300
+
</div>
301
301
+
</div>
302
302
+
</div>
+342
internal/bff/__snapshots__/new_brew_with_empty_selects.snap
···
1
1
+
---
2
2
+
title: new brew with empty selects
3
3
+
test_name: TestBrewForm_NewBrew_Snapshot/new_brew_with_empty_selects
4
4
+
file_name: form_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<script src="/static/js/brew-form.js"></script>
8
8
+
<div class="max-w-2xl mx-auto">
9
9
+
<div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300">
10
10
+
<h2 class="text-3xl font-bold text-brown-900 mb-6">
11
11
+
New Brew
12
12
+
</h2>
13
13
+
<form
14
14
+
15
15
+
hx-post="/brews"
16
16
+
17
17
+
hx-target="body"
18
18
+
class="space-y-6"
19
19
+
x-data="brewForm()"
20
20
+
>
21
21
+
<div>
22
22
+
<label class="block text-sm font-medium text-brown-900 mb-2">
23
23
+
Coffee Bean
24
24
+
</label>
25
25
+
<div class="flex gap-2">
26
26
+
<select
27
27
+
name="bean_rkey"
28
28
+
required
29
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
30
+
<option value="">
31
31
+
Select a bean...
32
32
+
</option>
33
33
+
</select>
34
34
+
<button
35
35
+
type="button"
36
36
+
@click="showNewBean = true"
37
37
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
38
38
+
+ New
39
39
+
</button>
40
40
+
</div>
41
41
+
<div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
42
42
+
<h4 class="font-medium mb-3 text-gray-800">
43
43
+
Add New Bean
44
44
+
</h4>
45
45
+
<div class="space-y-3">
46
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
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
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
49
+
<option value="">
50
50
+
Select Roaster (Optional)
51
51
+
</option>
52
52
+
</select>
53
53
+
<select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
54
54
+
<option value="">
55
55
+
Select Roast Level (Optional)
56
56
+
</option>
57
57
+
<option value="Ultra-Light">
58
58
+
Ultra-Light
59
59
+
</option>
60
60
+
<option value="Light">
61
61
+
Light
62
62
+
</option>
63
63
+
<option value="Medium-Light">
64
64
+
Medium-Light
65
65
+
</option>
66
66
+
<option value="Medium">
67
67
+
Medium
68
68
+
</option>
69
69
+
<option value="Medium-Dark">
70
70
+
Medium-Dark
71
71
+
</option>
72
72
+
<option value="Dark">
73
73
+
Dark
74
74
+
</option>
75
75
+
</select>
76
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
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
78
+
<div class="flex gap-2">
79
79
+
<button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
80
80
+
Add
81
81
+
</button>
82
82
+
<button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
83
83
+
Cancel
84
84
+
</button>
85
85
+
</div>
86
86
+
</div>
87
87
+
</div>
88
88
+
</div>
89
89
+
<div>
90
90
+
<label class="block text-sm font-medium text-brown-900 mb-2">
91
91
+
Coffee Amount (grams)
92
92
+
</label>
93
93
+
<input
94
94
+
type="number"
95
95
+
name="coffee_amount"
96
96
+
step="0.1"
97
97
+
98
98
+
placeholder="e.g. 18"
99
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
100
+
<p class="text-sm text-brown-700 mt-1">
101
101
+
Amount of ground coffee used
102
102
+
</p>
103
103
+
</div>
104
104
+
<div>
105
105
+
<label class="block text-sm font-medium text-brown-900 mb-2">
106
106
+
Grinder
107
107
+
</label>
108
108
+
<div class="flex gap-2">
109
109
+
<select
110
110
+
name="grinder_rkey"
111
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
112
+
<option value="">
113
113
+
Select a grinder...
114
114
+
</option>
115
115
+
</select>
116
116
+
<button
117
117
+
type="button"
118
118
+
@click="showNewGrinder = true"
119
119
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
120
120
+
+ New
121
121
+
</button>
122
122
+
</div>
123
123
+
<div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
124
124
+
<h4 class="font-medium mb-3 text-gray-800">
125
125
+
Add New Grinder
126
126
+
</h4>
127
127
+
<div class="space-y-3">
128
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
129
+
<select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
130
130
+
<option value="">
131
131
+
Grinder Type (Optional)
132
132
+
</option>
133
133
+
<option value="Hand">
134
134
+
Hand
135
135
+
</option>
136
136
+
<option value="Electric">
137
137
+
Electric
138
138
+
</option>
139
139
+
<option value="Electric Hand">
140
140
+
Electric Hand
141
141
+
</option>
142
142
+
</select>
143
143
+
<select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
144
144
+
<option value="">
145
145
+
Burr Type (Optional)
146
146
+
</option>
147
147
+
<option value="Conical">
148
148
+
Conical
149
149
+
</option>
150
150
+
<option value="Flat">
151
151
+
Flat
152
152
+
</option>
153
153
+
</select>
154
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
155
+
<div class="flex gap-2">
156
156
+
<button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
157
157
+
Add
158
158
+
</button>
159
159
+
<button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
160
160
+
Cancel
161
161
+
</button>
162
162
+
</div>
163
163
+
</div>
164
164
+
</div>
165
165
+
</div>
166
166
+
<div>
167
167
+
<label class="block text-sm font-medium text-brown-900 mb-2">
168
168
+
Grind Size
169
169
+
</label>
170
170
+
<input
171
171
+
type="text"
172
172
+
name="grind_size"
173
173
+
174
174
+
placeholder="e.g. 18, Medium, 3.5, Fine"
175
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
176
+
<p class="text-sm text-brown-700 mt-1">
177
177
+
Enter a number (grinder setting) or description (e.g. "Medium", "Fine")
178
178
+
</p>
179
179
+
</div>
180
180
+
<div>
181
181
+
<label class="block text-sm font-medium text-brown-900 mb-2">
182
182
+
Brew Method
183
183
+
</label>
184
184
+
<div class="flex gap-2">
185
185
+
<select
186
186
+
name="brewer_rkey"
187
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
188
+
<option value="">
189
189
+
Select brew method...
190
190
+
</option>
191
191
+
</select>
192
192
+
<button
193
193
+
type="button"
194
194
+
@click="showNewBrewer = true"
195
195
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
196
196
+
+ New
197
197
+
</button>
198
198
+
</div>
199
199
+
<div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
200
200
+
<h4 class="font-medium mb-3 text-gray-800">
201
201
+
Add New Brewer
202
202
+
</h4>
203
203
+
<div class="space-y-3">
204
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
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
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
207
+
<div class="flex gap-2">
208
208
+
<button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
209
209
+
Add
210
210
+
</button>
211
211
+
<button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
212
212
+
Cancel
213
213
+
</button>
214
214
+
</div>
215
215
+
</div>
216
216
+
</div>
217
217
+
</div>
218
218
+
<div>
219
219
+
<label class="block text-sm font-medium text-brown-900 mb-2">
220
220
+
Water Amount (grams)
221
221
+
</label>
222
222
+
<input
223
223
+
type="number"
224
224
+
name="water_amount"
225
225
+
step="1"
226
226
+
227
227
+
placeholder="e.g. 250"
228
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
229
+
<p class="text-sm text-brown-700 mt-1">
230
230
+
Total water used (or leave empty if using pours below)
231
231
+
</p>
232
232
+
</div>
233
233
+
<div>
234
234
+
<div class="flex items-center justify-between mb-2">
235
235
+
<label class="block text-sm font-medium text-brown-900">
236
236
+
Pours (Optional)
237
237
+
</label>
238
238
+
<button
239
239
+
type="button"
240
240
+
@click="addPour()"
241
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
242
+
+ Add Pour
243
243
+
</button>
244
244
+
</div>
245
245
+
<p class="text-sm text-brown-700 mb-3">
246
246
+
Track individual pours for bloom and subsequent additions
247
247
+
</p>
248
248
+
<div class="space-y-3">
249
249
+
<template x-for="(pour, index) in pours" :key="index">
250
250
+
<div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200">
251
251
+
<div class="flex-1">
252
252
+
<label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label>
253
253
+
<input
254
254
+
type="number"
255
255
+
:name="'pour_water_' + index"
256
256
+
x-model="pour.water"
257
257
+
placeholder="Water (g)"
258
258
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
259
259
+
</div>
260
260
+
<div class="flex-1">
261
261
+
<label class="text-xs text-brown-700 font-medium">
262
262
+
Time (sec)
263
263
+
</label>
264
264
+
<input
265
265
+
type="number"
266
266
+
:name="'pour_time_' + index"
267
267
+
x-model="pour.time"
268
268
+
placeholder="e.g. 45"
269
269
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
270
270
+
</div>
271
271
+
<button
272
272
+
type="button"
273
273
+
@click="removePour(index)"
274
274
+
class="text-brown-700 hover:text-brown-900 mt-5 font-bold"
275
275
+
x-show="pours.length > 0">
276
276
+
✕
277
277
+
</button>
278
278
+
</div>
279
279
+
</template>
280
280
+
</div>
281
281
+
</div>
282
282
+
<div>
283
283
+
<label class="block text-sm font-medium text-brown-900 mb-2">
284
284
+
Temperature
285
285
+
</label>
286
286
+
<input
287
287
+
type="number"
288
288
+
name="temperature"
289
289
+
step="0.1"
290
290
+
291
291
+
placeholder="e.g. 93.5"
292
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
293
+
</div>
294
294
+
<div>
295
295
+
<label class="block text-sm font-medium text-brown-900 mb-2">
296
296
+
Brew Time (seconds)
297
297
+
</label>
298
298
+
<input
299
299
+
type="number"
300
300
+
name="time_seconds"
301
301
+
302
302
+
placeholder="e.g. 180"
303
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
304
+
</div>
305
305
+
<div>
306
306
+
<label class="block text-sm font-medium text-brown-900 mb-2">
307
307
+
Tasting Notes
308
308
+
</label>
309
309
+
<textarea
310
310
+
name="tasting_notes"
311
311
+
rows="4"
312
312
+
placeholder="Describe the flavors, aroma, and your thoughts..."
313
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
314
+
</div>
315
315
+
<div>
316
316
+
<label class="block text-sm font-medium text-brown-900 mb-2">
317
317
+
Rating
318
318
+
</label>
319
319
+
<input
320
320
+
type="range"
321
321
+
name="rating"
322
322
+
min="1"
323
323
+
max="10"
324
324
+
value="5"
325
325
+
x-model="rating"
326
326
+
x-init="rating = $el.value"
327
327
+
class="w-full accent-brown-700"/>
328
328
+
<div class="text-center text-2xl font-bold text-brown-800">
329
329
+
<span x-text="rating"></span>
330
330
+
/10
331
331
+
</div>
332
332
+
</div>
333
333
+
<div>
334
334
+
<button
335
335
+
type="submit"
336
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
337
+
Save Brew
338
338
+
</button>
339
339
+
</div>
340
340
+
</form>
341
341
+
</div>
342
342
+
</div>
+342
internal/bff/__snapshots__/new_brew_with_nil_collections.snap
···
1
1
+
---
2
2
+
title: new brew with nil collections
3
3
+
test_name: TestBrewForm_NewBrew_Snapshot/new_brew_with_nil_collections
4
4
+
file_name: form_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<script src="/static/js/brew-form.js"></script>
8
8
+
<div class="max-w-2xl mx-auto">
9
9
+
<div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300">
10
10
+
<h2 class="text-3xl font-bold text-brown-900 mb-6">
11
11
+
New Brew
12
12
+
</h2>
13
13
+
<form
14
14
+
15
15
+
hx-post="/brews"
16
16
+
17
17
+
hx-target="body"
18
18
+
class="space-y-6"
19
19
+
x-data="brewForm()"
20
20
+
>
21
21
+
<div>
22
22
+
<label class="block text-sm font-medium text-brown-900 mb-2">
23
23
+
Coffee Bean
24
24
+
</label>
25
25
+
<div class="flex gap-2">
26
26
+
<select
27
27
+
name="bean_rkey"
28
28
+
required
29
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
30
+
<option value="">
31
31
+
Select a bean...
32
32
+
</option>
33
33
+
</select>
34
34
+
<button
35
35
+
type="button"
36
36
+
@click="showNewBean = true"
37
37
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
38
38
+
+ New
39
39
+
</button>
40
40
+
</div>
41
41
+
<div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
42
42
+
<h4 class="font-medium mb-3 text-gray-800">
43
43
+
Add New Bean
44
44
+
</h4>
45
45
+
<div class="space-y-3">
46
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
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
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
49
+
<option value="">
50
50
+
Select Roaster (Optional)
51
51
+
</option>
52
52
+
</select>
53
53
+
<select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
54
54
+
<option value="">
55
55
+
Select Roast Level (Optional)
56
56
+
</option>
57
57
+
<option value="Ultra-Light">
58
58
+
Ultra-Light
59
59
+
</option>
60
60
+
<option value="Light">
61
61
+
Light
62
62
+
</option>
63
63
+
<option value="Medium-Light">
64
64
+
Medium-Light
65
65
+
</option>
66
66
+
<option value="Medium">
67
67
+
Medium
68
68
+
</option>
69
69
+
<option value="Medium-Dark">
70
70
+
Medium-Dark
71
71
+
</option>
72
72
+
<option value="Dark">
73
73
+
Dark
74
74
+
</option>
75
75
+
</select>
76
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
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
78
+
<div class="flex gap-2">
79
79
+
<button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
80
80
+
Add
81
81
+
</button>
82
82
+
<button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
83
83
+
Cancel
84
84
+
</button>
85
85
+
</div>
86
86
+
</div>
87
87
+
</div>
88
88
+
</div>
89
89
+
<div>
90
90
+
<label class="block text-sm font-medium text-brown-900 mb-2">
91
91
+
Coffee Amount (grams)
92
92
+
</label>
93
93
+
<input
94
94
+
type="number"
95
95
+
name="coffee_amount"
96
96
+
step="0.1"
97
97
+
98
98
+
placeholder="e.g. 18"
99
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
100
+
<p class="text-sm text-brown-700 mt-1">
101
101
+
Amount of ground coffee used
102
102
+
</p>
103
103
+
</div>
104
104
+
<div>
105
105
+
<label class="block text-sm font-medium text-brown-900 mb-2">
106
106
+
Grinder
107
107
+
</label>
108
108
+
<div class="flex gap-2">
109
109
+
<select
110
110
+
name="grinder_rkey"
111
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
112
+
<option value="">
113
113
+
Select a grinder...
114
114
+
</option>
115
115
+
</select>
116
116
+
<button
117
117
+
type="button"
118
118
+
@click="showNewGrinder = true"
119
119
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
120
120
+
+ New
121
121
+
</button>
122
122
+
</div>
123
123
+
<div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
124
124
+
<h4 class="font-medium mb-3 text-gray-800">
125
125
+
Add New Grinder
126
126
+
</h4>
127
127
+
<div class="space-y-3">
128
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
129
+
<select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
130
130
+
<option value="">
131
131
+
Grinder Type (Optional)
132
132
+
</option>
133
133
+
<option value="Hand">
134
134
+
Hand
135
135
+
</option>
136
136
+
<option value="Electric">
137
137
+
Electric
138
138
+
</option>
139
139
+
<option value="Electric Hand">
140
140
+
Electric Hand
141
141
+
</option>
142
142
+
</select>
143
143
+
<select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
144
144
+
<option value="">
145
145
+
Burr Type (Optional)
146
146
+
</option>
147
147
+
<option value="Conical">
148
148
+
Conical
149
149
+
</option>
150
150
+
<option value="Flat">
151
151
+
Flat
152
152
+
</option>
153
153
+
</select>
154
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
155
+
<div class="flex gap-2">
156
156
+
<button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
157
157
+
Add
158
158
+
</button>
159
159
+
<button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
160
160
+
Cancel
161
161
+
</button>
162
162
+
</div>
163
163
+
</div>
164
164
+
</div>
165
165
+
</div>
166
166
+
<div>
167
167
+
<label class="block text-sm font-medium text-brown-900 mb-2">
168
168
+
Grind Size
169
169
+
</label>
170
170
+
<input
171
171
+
type="text"
172
172
+
name="grind_size"
173
173
+
174
174
+
placeholder="e.g. 18, Medium, 3.5, Fine"
175
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
176
+
<p class="text-sm text-brown-700 mt-1">
177
177
+
Enter a number (grinder setting) or description (e.g. "Medium", "Fine")
178
178
+
</p>
179
179
+
</div>
180
180
+
<div>
181
181
+
<label class="block text-sm font-medium text-brown-900 mb-2">
182
182
+
Brew Method
183
183
+
</label>
184
184
+
<div class="flex gap-2">
185
185
+
<select
186
186
+
name="brewer_rkey"
187
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
188
+
<option value="">
189
189
+
Select brew method...
190
190
+
</option>
191
191
+
</select>
192
192
+
<button
193
193
+
type="button"
194
194
+
@click="showNewBrewer = true"
195
195
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
196
196
+
+ New
197
197
+
</button>
198
198
+
</div>
199
199
+
<div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
200
200
+
<h4 class="font-medium mb-3 text-gray-800">
201
201
+
Add New Brewer
202
202
+
</h4>
203
203
+
<div class="space-y-3">
204
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
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
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
207
+
<div class="flex gap-2">
208
208
+
<button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
209
209
+
Add
210
210
+
</button>
211
211
+
<button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
212
212
+
Cancel
213
213
+
</button>
214
214
+
</div>
215
215
+
</div>
216
216
+
</div>
217
217
+
</div>
218
218
+
<div>
219
219
+
<label class="block text-sm font-medium text-brown-900 mb-2">
220
220
+
Water Amount (grams)
221
221
+
</label>
222
222
+
<input
223
223
+
type="number"
224
224
+
name="water_amount"
225
225
+
step="1"
226
226
+
227
227
+
placeholder="e.g. 250"
228
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
229
+
<p class="text-sm text-brown-700 mt-1">
230
230
+
Total water used (or leave empty if using pours below)
231
231
+
</p>
232
232
+
</div>
233
233
+
<div>
234
234
+
<div class="flex items-center justify-between mb-2">
235
235
+
<label class="block text-sm font-medium text-brown-900">
236
236
+
Pours (Optional)
237
237
+
</label>
238
238
+
<button
239
239
+
type="button"
240
240
+
@click="addPour()"
241
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
242
+
+ Add Pour
243
243
+
</button>
244
244
+
</div>
245
245
+
<p class="text-sm text-brown-700 mb-3">
246
246
+
Track individual pours for bloom and subsequent additions
247
247
+
</p>
248
248
+
<div class="space-y-3">
249
249
+
<template x-for="(pour, index) in pours" :key="index">
250
250
+
<div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200">
251
251
+
<div class="flex-1">
252
252
+
<label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label>
253
253
+
<input
254
254
+
type="number"
255
255
+
:name="'pour_water_' + index"
256
256
+
x-model="pour.water"
257
257
+
placeholder="Water (g)"
258
258
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
259
259
+
</div>
260
260
+
<div class="flex-1">
261
261
+
<label class="text-xs text-brown-700 font-medium">
262
262
+
Time (sec)
263
263
+
</label>
264
264
+
<input
265
265
+
type="number"
266
266
+
:name="'pour_time_' + index"
267
267
+
x-model="pour.time"
268
268
+
placeholder="e.g. 45"
269
269
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
270
270
+
</div>
271
271
+
<button
272
272
+
type="button"
273
273
+
@click="removePour(index)"
274
274
+
class="text-brown-700 hover:text-brown-900 mt-5 font-bold"
275
275
+
x-show="pours.length > 0">
276
276
+
✕
277
277
+
</button>
278
278
+
</div>
279
279
+
</template>
280
280
+
</div>
281
281
+
</div>
282
282
+
<div>
283
283
+
<label class="block text-sm font-medium text-brown-900 mb-2">
284
284
+
Temperature
285
285
+
</label>
286
286
+
<input
287
287
+
type="number"
288
288
+
name="temperature"
289
289
+
step="0.1"
290
290
+
291
291
+
placeholder="e.g. 93.5"
292
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
293
+
</div>
294
294
+
<div>
295
295
+
<label class="block text-sm font-medium text-brown-900 mb-2">
296
296
+
Brew Time (seconds)
297
297
+
</label>
298
298
+
<input
299
299
+
type="number"
300
300
+
name="time_seconds"
301
301
+
302
302
+
placeholder="e.g. 180"
303
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
304
+
</div>
305
305
+
<div>
306
306
+
<label class="block text-sm font-medium text-brown-900 mb-2">
307
307
+
Tasting Notes
308
308
+
</label>
309
309
+
<textarea
310
310
+
name="tasting_notes"
311
311
+
rows="4"
312
312
+
placeholder="Describe the flavors, aroma, and your thoughts..."
313
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
314
+
</div>
315
315
+
<div>
316
316
+
<label class="block text-sm font-medium text-brown-900 mb-2">
317
317
+
Rating
318
318
+
</label>
319
319
+
<input
320
320
+
type="range"
321
321
+
name="rating"
322
322
+
min="1"
323
323
+
max="10"
324
324
+
value="5"
325
325
+
x-model="rating"
326
326
+
x-init="rating = $el.value"
327
327
+
class="w-full accent-brown-700"/>
328
328
+
<div class="text-center text-2xl font-bold text-brown-800">
329
329
+
<span x-text="rating"></span>
330
330
+
/10
331
331
+
</div>
332
332
+
</div>
333
333
+
<div>
334
334
+
<button
335
335
+
type="submit"
336
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
337
+
Save Brew
338
338
+
</button>
339
339
+
</div>
340
340
+
</form>
341
341
+
</div>
342
342
+
</div>
+384
internal/bff/__snapshots__/new_brew_with_populated_selects.snap
···
1
1
+
---
2
2
+
title: new brew with populated selects
3
3
+
test_name: TestBrewForm_NewBrew_Snapshot/new_brew_with_populated_selects
4
4
+
file_name: form_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<script src="/static/js/brew-form.js"></script>
8
8
+
<div class="max-w-2xl mx-auto">
9
9
+
<div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300">
10
10
+
<h2 class="text-3xl font-bold text-brown-900 mb-6">
11
11
+
New Brew
12
12
+
</h2>
13
13
+
<form
14
14
+
15
15
+
hx-post="/brews"
16
16
+
17
17
+
hx-target="body"
18
18
+
class="space-y-6"
19
19
+
x-data="brewForm()"
20
20
+
>
21
21
+
<div>
22
22
+
<label class="block text-sm font-medium text-brown-900 mb-2">
23
23
+
Coffee Bean
24
24
+
</label>
25
25
+
<div class="flex gap-2">
26
26
+
<select
27
27
+
name="bean_rkey"
28
28
+
required
29
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
30
+
<option value="">
31
31
+
Select a bean...
32
32
+
</option>
33
33
+
<option
34
34
+
value="bean1"
35
35
+
36
36
+
class="truncate">
37
37
+
Ethiopian Yirgacheffe (Ethiopia - Light)
38
38
+
</option>
39
39
+
<option
40
40
+
value="bean2"
41
41
+
42
42
+
class="truncate">
43
43
+
Colombia - Medium
44
44
+
</option>
45
45
+
</select>
46
46
+
<button
47
47
+
type="button"
48
48
+
@click="showNewBean = true"
49
49
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
50
50
+
+ New
51
51
+
</button>
52
52
+
</div>
53
53
+
<div x-show="showNewBean" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
54
54
+
<h4 class="font-medium mb-3 text-gray-800">
55
55
+
Add New Bean
56
56
+
</h4>
57
57
+
<div class="space-y-3">
58
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
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
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
61
+
<option value="">
62
62
+
Select Roaster (Optional)
63
63
+
</option>
64
64
+
<option value="roaster1">
65
65
+
Blue Bottle
66
66
+
</option>
67
67
+
<option value="roaster2">
68
68
+
Counter Culture
69
69
+
</option>
70
70
+
</select>
71
71
+
<select x-model="newBean.roastLevel" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
72
72
+
<option value="">
73
73
+
Select Roast Level (Optional)
74
74
+
</option>
75
75
+
<option value="Ultra-Light">
76
76
+
Ultra-Light
77
77
+
</option>
78
78
+
<option value="Light">
79
79
+
Light
80
80
+
</option>
81
81
+
<option value="Medium-Light">
82
82
+
Medium-Light
83
83
+
</option>
84
84
+
<option value="Medium">
85
85
+
Medium
86
86
+
</option>
87
87
+
<option value="Medium-Dark">
88
88
+
Medium-Dark
89
89
+
</option>
90
90
+
<option value="Dark">
91
91
+
Dark
92
92
+
</option>
93
93
+
</select>
94
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
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
96
+
<div class="flex gap-2">
97
97
+
<button type="button" @click="addBean()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
98
98
+
Add
99
99
+
</button>
100
100
+
<button type="button" @click="showNewBean = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
101
101
+
Cancel
102
102
+
</button>
103
103
+
</div>
104
104
+
</div>
105
105
+
</div>
106
106
+
</div>
107
107
+
<div>
108
108
+
<label class="block text-sm font-medium text-brown-900 mb-2">
109
109
+
Coffee Amount (grams)
110
110
+
</label>
111
111
+
<input
112
112
+
type="number"
113
113
+
name="coffee_amount"
114
114
+
step="0.1"
115
115
+
116
116
+
placeholder="e.g. 18"
117
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
118
+
<p class="text-sm text-brown-700 mt-1">
119
119
+
Amount of ground coffee used
120
120
+
</p>
121
121
+
</div>
122
122
+
<div>
123
123
+
<label class="block text-sm font-medium text-brown-900 mb-2">
124
124
+
Grinder
125
125
+
</label>
126
126
+
<div class="flex gap-2">
127
127
+
<select
128
128
+
name="grinder_rkey"
129
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
130
+
<option value="">
131
131
+
Select a grinder...
132
132
+
</option>
133
133
+
<option
134
134
+
value="grinder1"
135
135
+
136
136
+
class="truncate">
137
137
+
Baratza Encore
138
138
+
</option>
139
139
+
<option
140
140
+
value="grinder2"
141
141
+
142
142
+
class="truncate">
143
143
+
Comandante C40
144
144
+
</option>
145
145
+
</select>
146
146
+
<button
147
147
+
type="button"
148
148
+
@click="showNewGrinder = true"
149
149
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
150
150
+
+ New
151
151
+
</button>
152
152
+
</div>
153
153
+
<div x-show="showNewGrinder" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
154
154
+
<h4 class="font-medium mb-3 text-gray-800">
155
155
+
Add New Grinder
156
156
+
</h4>
157
157
+
<div class="space-y-3">
158
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
159
+
<select x-model="newGrinder.grinderType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
160
160
+
<option value="">
161
161
+
Grinder Type (Optional)
162
162
+
</option>
163
163
+
<option value="Hand">
164
164
+
Hand
165
165
+
</option>
166
166
+
<option value="Electric">
167
167
+
Electric
168
168
+
</option>
169
169
+
<option value="Electric Hand">
170
170
+
Electric Hand
171
171
+
</option>
172
172
+
</select>
173
173
+
<select x-model="newGrinder.burrType" class="w-full rounded-md border-gray-300 bg-white shadow-sm py-2 px-3">
174
174
+
<option value="">
175
175
+
Burr Type (Optional)
176
176
+
</option>
177
177
+
<option value="Conical">
178
178
+
Conical
179
179
+
</option>
180
180
+
<option value="Flat">
181
181
+
Flat
182
182
+
</option>
183
183
+
</select>
184
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
185
+
<div class="flex gap-2">
186
186
+
<button type="button" @click="addGrinder()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
187
187
+
Add
188
188
+
</button>
189
189
+
<button type="button" @click="showNewGrinder = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
190
190
+
Cancel
191
191
+
</button>
192
192
+
</div>
193
193
+
</div>
194
194
+
</div>
195
195
+
</div>
196
196
+
<div>
197
197
+
<label class="block text-sm font-medium text-brown-900 mb-2">
198
198
+
Grind Size
199
199
+
</label>
200
200
+
<input
201
201
+
type="text"
202
202
+
name="grind_size"
203
203
+
204
204
+
placeholder="e.g. 18, Medium, 3.5, Fine"
205
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
206
+
<p class="text-sm text-brown-700 mt-1">
207
207
+
Enter a number (grinder setting) or description (e.g. "Medium", "Fine")
208
208
+
</p>
209
209
+
</div>
210
210
+
<div>
211
211
+
<label class="block text-sm font-medium text-brown-900 mb-2">
212
212
+
Brew Method
213
213
+
</label>
214
214
+
<div class="flex gap-2">
215
215
+
<select
216
216
+
name="brewer_rkey"
217
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
218
+
<option value="">
219
219
+
Select brew method...
220
220
+
</option>
221
221
+
<option
222
222
+
value="brewer1"
223
223
+
224
224
+
class="truncate">
225
225
+
Hario V60
226
226
+
</option>
227
227
+
<option
228
228
+
value="brewer2"
229
229
+
230
230
+
class="truncate">
231
231
+
AeroPress
232
232
+
</option>
233
233
+
</select>
234
234
+
<button
235
235
+
type="button"
236
236
+
@click="showNewBrewer = true"
237
237
+
class="bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
238
238
+
+ New
239
239
+
</button>
240
240
+
</div>
241
241
+
<div x-show="showNewBrewer" class="mt-4 p-4 bg-brown-100 rounded border border-brown-300">
242
242
+
<h4 class="font-medium mb-3 text-gray-800">
243
243
+
Add New Brewer
244
244
+
</h4>
245
245
+
<div class="space-y-3">
246
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
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
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
249
+
<div class="flex gap-2">
250
250
+
<button type="button" @click="addBrewer()" class="bg-brown-600 text-white px-4 py-2 rounded hover:bg-brown-700">
251
251
+
Add
252
252
+
</button>
253
253
+
<button type="button" @click="showNewBrewer = false" class="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400">
254
254
+
Cancel
255
255
+
</button>
256
256
+
</div>
257
257
+
</div>
258
258
+
</div>
259
259
+
</div>
260
260
+
<div>
261
261
+
<label class="block text-sm font-medium text-brown-900 mb-2">
262
262
+
Water Amount (grams)
263
263
+
</label>
264
264
+
<input
265
265
+
type="number"
266
266
+
name="water_amount"
267
267
+
step="1"
268
268
+
269
269
+
placeholder="e.g. 250"
270
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
271
+
<p class="text-sm text-brown-700 mt-1">
272
272
+
Total water used (or leave empty if using pours below)
273
273
+
</p>
274
274
+
</div>
275
275
+
<div>
276
276
+
<div class="flex items-center justify-between mb-2">
277
277
+
<label class="block text-sm font-medium text-brown-900">
278
278
+
Pours (Optional)
279
279
+
</label>
280
280
+
<button
281
281
+
type="button"
282
282
+
@click="addPour()"
283
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
284
+
+ Add Pour
285
285
+
</button>
286
286
+
</div>
287
287
+
<p class="text-sm text-brown-700 mb-3">
288
288
+
Track individual pours for bloom and subsequent additions
289
289
+
</p>
290
290
+
<div class="space-y-3">
291
291
+
<template x-for="(pour, index) in pours" :key="index">
292
292
+
<div class="flex gap-2 items-center bg-brown-50 p-3 rounded-lg border border-brown-200">
293
293
+
<div class="flex-1">
294
294
+
<label class="text-xs text-brown-700 font-medium" x-text="'Pour ' + (index + 1)"></label>
295
295
+
<input
296
296
+
type="number"
297
297
+
:name="'pour_water_' + index"
298
298
+
x-model="pour.water"
299
299
+
placeholder="Water (g)"
300
300
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
301
301
+
</div>
302
302
+
<div class="flex-1">
303
303
+
<label class="text-xs text-brown-700 font-medium">
304
304
+
Time (sec)
305
305
+
</label>
306
306
+
<input
307
307
+
type="number"
308
308
+
:name="'pour_time_' + index"
309
309
+
x-model="pour.time"
310
310
+
placeholder="e.g. 45"
311
311
+
class="w-full rounded-md border-brown-300 text-sm py-2 px-3 mt-1 bg-white"/>
312
312
+
</div>
313
313
+
<button
314
314
+
type="button"
315
315
+
@click="removePour(index)"
316
316
+
class="text-brown-700 hover:text-brown-900 mt-5 font-bold"
317
317
+
x-show="pours.length > 0">
318
318
+
✕
319
319
+
</button>
320
320
+
</div>
321
321
+
</template>
322
322
+
</div>
323
323
+
</div>
324
324
+
<div>
325
325
+
<label class="block text-sm font-medium text-brown-900 mb-2">
326
326
+
Temperature
327
327
+
</label>
328
328
+
<input
329
329
+
type="number"
330
330
+
name="temperature"
331
331
+
step="0.1"
332
332
+
333
333
+
placeholder="e.g. 93.5"
334
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
335
+
</div>
336
336
+
<div>
337
337
+
<label class="block text-sm font-medium text-brown-900 mb-2">
338
338
+
Brew Time (seconds)
339
339
+
</label>
340
340
+
<input
341
341
+
type="number"
342
342
+
name="time_seconds"
343
343
+
344
344
+
placeholder="e.g. 180"
345
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
346
+
</div>
347
347
+
<div>
348
348
+
<label class="block text-sm font-medium text-brown-900 mb-2">
349
349
+
Tasting Notes
350
350
+
</label>
351
351
+
<textarea
352
352
+
name="tasting_notes"
353
353
+
rows="4"
354
354
+
placeholder="Describe the flavors, aroma, and your thoughts..."
355
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
356
+
</div>
357
357
+
<div>
358
358
+
<label class="block text-sm font-medium text-brown-900 mb-2">
359
359
+
Rating
360
360
+
</label>
361
361
+
<input
362
362
+
type="range"
363
363
+
name="rating"
364
364
+
min="1"
365
365
+
max="10"
366
366
+
value="5"
367
367
+
x-model="rating"
368
368
+
x-init="rating = $el.value"
369
369
+
class="w-full accent-brown-700"/>
370
370
+
<div class="text-center text-2xl font-bold text-brown-800">
371
371
+
<span x-text="rating"></span>
372
372
+
/10
373
373
+
</div>
374
374
+
</div>
375
375
+
<div>
376
376
+
<button
377
377
+
type="submit"
378
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
379
+
Save Brew
380
380
+
</button>
381
381
+
</div>
382
382
+
</form>
383
383
+
</div>
384
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
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
7
+
<div class="space-y-4">
8
8
+
<div class="bg-brown-100 rounded-lg p-6 text-center text-brown-700 border border-brown-200">
9
9
+
<p class="mb-2 font-medium">
10
10
+
No activity in the feed yet.
11
11
+
</p>
12
12
+
<p class="text-sm">
13
13
+
Be the first to add something!
14
14
+
</p>
15
15
+
</div>
16
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
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
7
+
<div id="profile-stats-data"
8
8
+
data-brews="0"
9
9
+
data-beans="0"
10
10
+
data-roasters="1"
11
11
+
data-grinders="0"
12
12
+
data-brewers="0"
13
13
+
style="display: none;"></div>
14
14
+
<div x-show="activeTab === 'brews'">
15
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
16
+
<p class="text-brown-800 text-lg font-medium">
17
17
+
No brews yet.
18
18
+
</p>
19
19
+
</div>
20
20
+
</div>
21
21
+
<div x-show="activeTab === 'beans'" x-cloak class="space-y-6">
22
22
+
<div>
23
23
+
<h3 class="text-lg font-semibold text-brown-900 mb-3">
24
24
+
🏪 Favorite Roasters
25
25
+
</h3>
26
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
27
+
<table class="min-w-full divide-y divide-brown-300">
28
28
+
<thead class="bg-brown-200/80">
29
29
+
<tr>
30
30
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
31
31
+
Name
32
32
+
</th>
33
33
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
34
34
+
📍 Location
35
35
+
</th>
36
36
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
37
37
+
🌐 Website
38
38
+
</th>
39
39
+
</tr>
40
40
+
</thead>
41
41
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
42
42
+
<tr class="hover:bg-brown-100/60 transition-colors">
43
43
+
<td class="px-6 py-4 text-sm font-bold text-brown-900">
44
44
+
FTP Roaster
45
45
+
</td>
46
46
+
<td class="px-6 py-4 text-sm text-brown-900">
47
47
+
<span class="text-brown-400">
48
48
+
-
49
49
+
</span>
50
50
+
</td>
51
51
+
<td class="px-6 py-4 text-sm text-brown-900">
52
52
+
<span class="text-brown-400">
53
53
+
-
54
54
+
</span>
55
55
+
</td>
56
56
+
</tr>
57
57
+
</tbody>
58
58
+
</table>
59
59
+
</div>
60
60
+
</div>
61
61
+
</div>
62
62
+
<div x-show="activeTab === 'gear'" x-cloak class="space-y-6">
63
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
64
+
<p class="font-medium">
65
65
+
No gear added yet.
66
66
+
</p>
67
67
+
</div>
68
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
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
7
+
<div id="profile-stats-data"
8
8
+
data-brews="0"
9
9
+
data-beans="0"
10
10
+
data-roasters="1"
11
11
+
data-grinders="0"
12
12
+
data-brewers="0"
13
13
+
style="display: none;"></div>
14
14
+
<div x-show="activeTab === 'brews'">
15
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
16
+
<p class="text-brown-800 text-lg font-medium">
17
17
+
No brews yet.
18
18
+
</p>
19
19
+
</div>
20
20
+
</div>
21
21
+
<div x-show="activeTab === 'beans'" x-cloak class="space-y-6">
22
22
+
<div>
23
23
+
<h3 class="text-lg font-semibold text-brown-900 mb-3">
24
24
+
🏪 Favorite Roasters
25
25
+
</h3>
26
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
27
+
<table class="min-w-full divide-y divide-brown-300">
28
28
+
<thead class="bg-brown-200/80">
29
29
+
<tr>
30
30
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
31
31
+
Name
32
32
+
</th>
33
33
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
34
34
+
📍 Location
35
35
+
</th>
36
36
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
37
37
+
🌐 Website
38
38
+
</th>
39
39
+
</tr>
40
40
+
</thead>
41
41
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
42
42
+
<tr class="hover:bg-brown-100/60 transition-colors">
43
43
+
<td class="px-6 py-4 text-sm font-bold text-brown-900">
44
44
+
Sketchy Roaster
45
45
+
</td>
46
46
+
<td class="px-6 py-4 text-sm text-brown-900">
47
47
+
Unknown
48
48
+
</td>
49
49
+
<td class="px-6 py-4 text-sm text-brown-900">
50
50
+
<span class="text-brown-400">
51
51
+
-
52
52
+
</span>
53
53
+
</td>
54
54
+
</tr>
55
55
+
</tbody>
56
56
+
</table>
57
57
+
</div>
58
58
+
</div>
59
59
+
</div>
60
60
+
<div x-show="activeTab === 'gear'" x-cloak class="space-y-6">
61
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
62
+
<p class="font-medium">
63
63
+
No gear added yet.
64
64
+
</p>
65
65
+
</div>
66
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
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
7
+
<div id="profile-stats-data"
8
8
+
data-brews="0"
9
9
+
data-beans="0"
10
10
+
data-roasters="0"
11
11
+
data-grinders="0"
12
12
+
data-brewers="0"
13
13
+
style="display: none;"></div>
14
14
+
<div x-show="activeTab === 'brews'">
15
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
16
+
<p class="text-brown-800 text-lg font-medium">
17
17
+
No brews yet.
18
18
+
</p>
19
19
+
</div>
20
20
+
</div>
21
21
+
<div x-show="activeTab === 'beans'" x-cloak class="space-y-6">
22
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
23
+
<p class="font-medium">
24
24
+
No beans or roasters yet.
25
25
+
</p>
26
26
+
</div>
27
27
+
</div>
28
28
+
<div x-show="activeTab === 'gear'" x-cloak class="space-y-6">
29
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
30
+
<p class="font-medium">
31
31
+
No gear added yet.
32
32
+
</p>
33
33
+
</div>
34
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
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
7
+
<div id="profile-stats-data"
8
8
+
data-brews="0"
9
9
+
data-beans="0"
10
10
+
data-roasters="1"
11
11
+
data-grinders="2"
12
12
+
data-brewers="1"
13
13
+
style="display: none;"></div>
14
14
+
<div x-show="activeTab === 'brews'">
15
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
16
+
<p class="text-brown-800 text-lg mb-4 font-medium">
17
17
+
No brews yet! Start tracking your coffee journey.
18
18
+
</p>
19
19
+
<a href="/brews/new"
20
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
21
+
Add Your First Brew
22
22
+
</a>
23
23
+
</div>
24
24
+
</div>
25
25
+
<div x-show="activeTab === 'beans'" x-cloak class="space-y-6">
26
26
+
<div>
27
27
+
<h3 class="text-lg font-semibold text-brown-900 mb-3">
28
28
+
🏪 Favorite Roasters
29
29
+
</h3>
30
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
31
+
<table class="min-w-full divide-y divide-brown-300">
32
32
+
<thead class="bg-brown-200/80">
33
33
+
<tr>
34
34
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
35
35
+
Name
36
36
+
</th>
37
37
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
38
38
+
📍 Location
39
39
+
</th>
40
40
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
41
41
+
🌐 Website
42
42
+
</th>
43
43
+
</tr>
44
44
+
</thead>
45
45
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
46
46
+
<tr class="hover:bg-brown-100/60 transition-colors">
47
47
+
<td class="px-6 py-4 text-sm font-bold text-brown-900">
48
48
+
Heart Coffee
49
49
+
</td>
50
50
+
<td class="px-6 py-4 text-sm text-brown-900">
51
51
+
Portland, OR
52
52
+
</td>
53
53
+
<td class="px-6 py-4 text-sm text-brown-900">
54
54
+
<a href="https://heartroasters.com" target="_blank" rel="noopener noreferrer" class="text-brown-700 hover:underline font-medium">
55
55
+
Visit Site
56
56
+
</a>
57
57
+
</td>
58
58
+
</tr>
59
59
+
</tbody>
60
60
+
</table>
61
61
+
</div>
62
62
+
<div class="mt-3 text-center">
63
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
64
+
<span>
65
65
+
+
66
66
+
</span>
67
67
+
<span>
68
68
+
Add New Roaster
69
69
+
</span>
70
70
+
</button>
71
71
+
</div>
72
72
+
</div>
73
73
+
</div>
74
74
+
<div x-show="activeTab === 'gear'" x-cloak class="space-y-6">
75
75
+
<div>
76
76
+
<h3 class="text-lg font-semibold text-brown-900 mb-3">
77
77
+
⚙️ Grinders
78
78
+
</h3>
79
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
80
+
<table class="min-w-full divide-y divide-brown-300">
81
81
+
<thead class="bg-brown-200/80">
82
82
+
<tr>
83
83
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
84
84
+
Name
85
85
+
</th>
86
86
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
87
87
+
🔧 Type
88
88
+
</th>
89
89
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
90
90
+
💎 Burrs
91
91
+
</th>
92
92
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
93
93
+
📝 Notes
94
94
+
</th>
95
95
+
</tr>
96
96
+
</thead>
97
97
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
98
98
+
<tr class="hover:bg-brown-100/60 transition-colors">
99
99
+
<td class="px-6 py-4 text-sm font-bold text-brown-900">
100
100
+
Comandante C40
101
101
+
</td>
102
102
+
<td class="px-6 py-4 text-sm text-brown-900">
103
103
+
Hand
104
104
+
</td>
105
105
+
<td class="px-6 py-4 text-sm text-brown-900">
106
106
+
Conical
107
107
+
</td>
108
108
+
<td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs">
109
109
+
Perfect for pour over
110
110
+
</td>
111
111
+
</tr>
112
112
+
<tr class="hover:bg-brown-100/60 transition-colors">
113
113
+
<td class="px-6 py-4 text-sm font-bold text-brown-900">
114
114
+
Niche Zero
115
115
+
</td>
116
116
+
<td class="px-6 py-4 text-sm text-brown-900">
117
117
+
Electric
118
118
+
</td>
119
119
+
<td class="px-6 py-4 text-sm text-brown-900">
120
120
+
Conical
121
121
+
</td>
122
122
+
<td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs">
123
123
+
<span class="text-brown-400 not-italic">
124
124
+
-
125
125
+
</span>
126
126
+
</td>
127
127
+
</tr>
128
128
+
</tbody>
129
129
+
</table>
130
130
+
</div>
131
131
+
<div class="mt-3 text-center">
132
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
133
+
<span>
134
134
+
+
135
135
+
</span>
136
136
+
<span>
137
137
+
Add New Grinder
138
138
+
</span>
139
139
+
</button>
140
140
+
</div>
141
141
+
</div>
142
142
+
<div>
143
143
+
<h3 class="text-lg font-semibold text-brown-900 mb-3">
144
144
+
☕ Brewers
145
145
+
</h3>
146
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
147
+
<table class="min-w-full divide-y divide-brown-300">
148
148
+
<thead class="bg-brown-200/80">
149
149
+
<tr>
150
150
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
151
151
+
Name
152
152
+
</th>
153
153
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
154
154
+
🔧 Type
155
155
+
</th>
156
156
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
157
157
+
📝 Description
158
158
+
</th>
159
159
+
</tr>
160
160
+
</thead>
161
161
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
162
162
+
<tr class="hover:bg-brown-100/60 transition-colors">
163
163
+
<td class="px-6 py-4 text-sm font-bold text-brown-900">
164
164
+
Hario V60
165
165
+
</td>
166
166
+
<td class="px-6 py-4 text-sm text-brown-900">
167
167
+
Pour Over
168
168
+
</td>
169
169
+
<td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs">
170
170
+
Classic pour over cone
171
171
+
</td>
172
172
+
</tr>
173
173
+
</tbody>
174
174
+
</table>
175
175
+
</div>
176
176
+
<div class="mt-3 text-center">
177
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
178
+
<span>
179
179
+
+
180
180
+
</span>
181
181
+
<span>
182
182
+
Add New Brewer
183
183
+
</span>
184
184
+
</button>
185
185
+
</div>
186
186
+
</div>
187
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
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
7
+
<div id="profile-stats-data"
8
8
+
data-brews="0"
9
9
+
data-beans="2"
10
10
+
data-roasters="0"
11
11
+
data-grinders="0"
12
12
+
data-brewers="0"
13
13
+
style="display: none;"></div>
14
14
+
<div x-show="activeTab === 'brews'">
15
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
16
+
<p class="text-brown-800 text-lg mb-4 font-medium">
17
17
+
No brews yet! Start tracking your coffee journey.
18
18
+
</p>
19
19
+
<a href="/brews/new"
20
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
21
+
Add Your First Brew
22
22
+
</a>
23
23
+
</div>
24
24
+
</div>
25
25
+
<div x-show="activeTab === 'beans'" x-cloak class="space-y-6">
26
26
+
<div>
27
27
+
<h3 class="text-lg font-semibold text-brown-900 mb-3">
28
28
+
☕ Coffee Beans
29
29
+
</h3>
30
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
31
+
<table class="min-w-full divide-y divide-brown-300">
32
32
+
<thead class="bg-brown-200/80">
33
33
+
<tr>
34
34
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
35
35
+
Name
36
36
+
</th>
37
37
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
38
38
+
☕ Roaster
39
39
+
</th>
40
40
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
41
41
+
📍 Origin
42
42
+
</th>
43
43
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
44
44
+
🔥 Roast
45
45
+
</th>
46
46
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
47
47
+
🌱 Process
48
48
+
</th>
49
49
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
50
50
+
📝 Description
51
51
+
</th>
52
52
+
</tr>
53
53
+
</thead>
54
54
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
55
55
+
<tr class="hover:bg-brown-100/60 transition-colors">
56
56
+
<td class="px-6 py-4 text-sm font-bold text-brown-900">
57
57
+
Ethiopian Yirgacheffe
58
58
+
</td>
59
59
+
<td class="px-6 py-4 text-sm text-brown-900">
60
60
+
Onyx Coffee Lab
61
61
+
</td>
62
62
+
<td class="px-6 py-4 text-sm text-brown-900">
63
63
+
Ethiopia
64
64
+
</td>
65
65
+
<td class="px-6 py-4 text-sm text-brown-900">
66
66
+
Light
67
67
+
</td>
68
68
+
<td class="px-6 py-4 text-sm text-brown-900">
69
69
+
Washed
70
70
+
</td>
71
71
+
<td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs">
72
72
+
Bright and floral with citrus notes
73
73
+
</td>
74
74
+
</tr>
75
75
+
<tr class="hover:bg-brown-100/60 transition-colors">
76
76
+
<td class="px-6 py-4 text-sm font-bold text-brown-900">
77
77
+
Colombia Supremo
78
78
+
</td>
79
79
+
<td class="px-6 py-4 text-sm text-brown-900">
80
80
+
<span class="text-brown-400">
81
81
+
-
82
82
+
</span>
83
83
+
</td>
84
84
+
<td class="px-6 py-4 text-sm text-brown-900">
85
85
+
Colombia
86
86
+
</td>
87
87
+
<td class="px-6 py-4 text-sm text-brown-900">
88
88
+
Medium
89
89
+
</td>
90
90
+
<td class="px-6 py-4 text-sm text-brown-900">
91
91
+
Natural
92
92
+
</td>
93
93
+
<td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs">
94
94
+
<span class="text-brown-400 not-italic">
95
95
+
-
96
96
+
</span>
97
97
+
</td>
98
98
+
</tr>
99
99
+
</tbody>
100
100
+
</table>
101
101
+
</div>
102
102
+
<div class="mt-3 text-center">
103
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
104
+
<span>
105
105
+
+
106
106
+
</span>
107
107
+
<span>
108
108
+
Add New Bean
109
109
+
</span>
110
110
+
</button>
111
111
+
</div>
112
112
+
</div>
113
113
+
</div>
114
114
+
<div x-show="activeTab === 'gear'" x-cloak class="space-y-6">
115
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
116
+
<p class="font-medium">
117
117
+
No gear added yet.
118
118
+
</p>
119
119
+
</div>
120
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
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 <html> & "quotes"\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 & 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 'single' and "double" 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 & 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 <script>alert('xss')</script>\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
7
+
<div id="profile-stats-data"
8
8
+
data-brews="0"
9
9
+
data-beans="1"
10
10
+
data-roasters="0"
11
11
+
data-grinders="1"
12
12
+
data-brewers="0"
13
13
+
style="display: none;"></div>
14
14
+
<div x-show="activeTab === 'brews'">
15
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
16
+
<p class="text-brown-800 text-lg mb-4 font-medium">
17
17
+
No brews yet! Start tracking your coffee journey.
18
18
+
</p>
19
19
+
<a href="/brews/new"
20
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
21
+
Add Your First Brew
22
22
+
</a>
23
23
+
</div>
24
24
+
</div>
25
25
+
<div x-show="activeTab === 'beans'" x-cloak class="space-y-6">
26
26
+
<div>
27
27
+
<h3 class="text-lg font-semibold text-brown-900 mb-3">
28
28
+
☕ Coffee Beans
29
29
+
</h3>
30
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
31
+
<table class="min-w-full divide-y divide-brown-300">
32
32
+
<thead class="bg-brown-200/80">
33
33
+
<tr>
34
34
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
35
35
+
Name
36
36
+
</th>
37
37
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
38
38
+
☕ Roaster
39
39
+
</th>
40
40
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
41
41
+
📍 Origin
42
42
+
</th>
43
43
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
44
44
+
🔥 Roast
45
45
+
</th>
46
46
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
47
47
+
🌱 Process
48
48
+
</th>
49
49
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
50
50
+
📝 Description
51
51
+
</th>
52
52
+
</tr>
53
53
+
</thead>
54
54
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
55
55
+
<tr class="hover:bg-brown-100/60 transition-colors">
56
56
+
<td class="px-6 py-4 text-sm font-bold text-brown-900">
57
57
+
Bean with <html> & "quotes"
58
58
+
</td>
59
59
+
<td class="px-6 py-4 text-sm text-brown-900">
60
60
+
<span class="text-brown-400">
61
61
+
-
62
62
+
</span>
63
63
+
</td>
64
64
+
<td class="px-6 py-4 text-sm text-brown-900">
65
65
+
Colombia & Peru
66
66
+
</td>
67
67
+
<td class="px-6 py-4 text-sm text-brown-900">
68
68
+
<span class="text-brown-400">
69
69
+
-
70
70
+
</span>
71
71
+
</td>
72
72
+
<td class="px-6 py-4 text-sm text-brown-900">
73
73
+
<span class="text-brown-400">
74
74
+
-
75
75
+
</span>
76
76
+
</td>
77
77
+
<td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs">
78
78
+
Description with 'single' and "double" quotes
79
79
+
</td>
80
80
+
</tr>
81
81
+
</tbody>
82
82
+
</table>
83
83
+
</div>
84
84
+
<div class="mt-3 text-center">
85
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
86
+
<span>
87
87
+
+
88
88
+
</span>
89
89
+
<span>
90
90
+
Add New Bean
91
91
+
</span>
92
92
+
</button>
93
93
+
</div>
94
94
+
</div>
95
95
+
</div>
96
96
+
<div x-show="activeTab === 'gear'" x-cloak class="space-y-6">
97
97
+
<div>
98
98
+
<h3 class="text-lg font-semibold text-brown-900 mb-3">
99
99
+
⚙️ Grinders
100
100
+
</h3>
101
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
102
+
<table class="min-w-full divide-y divide-brown-300">
103
103
+
<thead class="bg-brown-200/80">
104
104
+
<tr>
105
105
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
106
106
+
Name
107
107
+
</th>
108
108
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
109
109
+
🔧 Type
110
110
+
</th>
111
111
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
112
112
+
💎 Burrs
113
113
+
</th>
114
114
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
115
115
+
📝 Notes
116
116
+
</th>
117
117
+
</tr>
118
118
+
</thead>
119
119
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
120
120
+
<tr class="hover:bg-brown-100/60 transition-colors">
121
121
+
<td class="px-6 py-4 text-sm font-bold text-brown-900">
122
122
+
Grinder & Co.
123
123
+
</td>
124
124
+
<td class="px-6 py-4 text-sm text-brown-900">
125
125
+
<span class="text-brown-400">
126
126
+
-
127
127
+
</span>
128
128
+
</td>
129
129
+
<td class="px-6 py-4 text-sm text-brown-900">
130
130
+
<span class="text-brown-400">
131
131
+
-
132
132
+
</span>
133
133
+
</td>
134
134
+
<td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs">
135
135
+
Notes with <script>alert('xss')</script>
136
136
+
</td>
137
137
+
</tr>
138
138
+
</tbody>
139
139
+
</table>
140
140
+
</div>
141
141
+
<div class="mt-3 text-center">
142
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
143
+
<span>
144
144
+
+
145
145
+
</span>
146
146
+
<span>
147
147
+
Add New Grinder
148
148
+
</span>
149
149
+
</button>
150
150
+
</div>
151
151
+
</div>
152
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
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
7
+
<div id="profile-stats-data"
8
8
+
data-brews="0"
9
9
+
data-beans="2"
10
10
+
data-roasters="1"
11
11
+
data-grinders="0"
12
12
+
data-brewers="0"
13
13
+
style="display: none;"></div>
14
14
+
<div x-show="activeTab === 'brews'">
15
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
16
+
<p class="text-brown-800 text-lg font-medium">
17
17
+
No brews yet.
18
18
+
</p>
19
19
+
</div>
20
20
+
</div>
21
21
+
<div x-show="activeTab === 'beans'" x-cloak class="space-y-6">
22
22
+
<div>
23
23
+
<h3 class="text-lg font-semibold text-brown-900 mb-3">
24
24
+
☕ Coffee Beans
25
25
+
</h3>
26
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
27
+
<table class="min-w-full divide-y divide-brown-300">
28
28
+
<thead class="bg-brown-200/80">
29
29
+
<tr>
30
30
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
31
31
+
Name
32
32
+
</th>
33
33
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
34
34
+
☕ Roaster
35
35
+
</th>
36
36
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
37
37
+
📍 Origin
38
38
+
</th>
39
39
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
40
40
+
🔥 Roast
41
41
+
</th>
42
42
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
43
43
+
🌱 Process
44
44
+
</th>
45
45
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider whitespace-nowrap">
46
46
+
📝 Description
47
47
+
</th>
48
48
+
</tr>
49
49
+
</thead>
50
50
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
51
51
+
<tr class="hover:bg-brown-100/60 transition-colors">
52
52
+
<td class="px-6 py-4 text-sm font-bold text-brown-900">
53
53
+
エチオピア イルガチェフェ
54
54
+
</td>
55
55
+
<td class="px-6 py-4 text-sm text-brown-900">
56
56
+
<span class="text-brown-400">
57
57
+
-
58
58
+
</span>
59
59
+
</td>
60
60
+
<td class="px-6 py-4 text-sm text-brown-900">
61
61
+
日本
62
62
+
</td>
63
63
+
<td class="px-6 py-4 text-sm text-brown-900">
64
64
+
<span class="text-brown-400">
65
65
+
-
66
66
+
</span>
67
67
+
</td>
68
68
+
<td class="px-6 py-4 text-sm text-brown-900">
69
69
+
<span class="text-brown-400">
70
70
+
-
71
71
+
</span>
72
72
+
</td>
73
73
+
<td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs">
74
74
+
明るく花のような香り
75
75
+
</td>
76
76
+
</tr>
77
77
+
<tr class="hover:bg-brown-100/60 transition-colors">
78
78
+
<td class="px-6 py-4 text-sm font-bold text-brown-900">
79
79
+
Café de Colombia
80
80
+
</td>
81
81
+
<td class="px-6 py-4 text-sm text-brown-900">
82
82
+
<span class="text-brown-400">
83
83
+
-
84
84
+
</span>
85
85
+
</td>
86
86
+
<td class="px-6 py-4 text-sm text-brown-900">
87
87
+
Bogotá
88
88
+
</td>
89
89
+
<td class="px-6 py-4 text-sm text-brown-900">
90
90
+
<span class="text-brown-400">
91
91
+
-
92
92
+
</span>
93
93
+
</td>
94
94
+
<td class="px-6 py-4 text-sm text-brown-900">
95
95
+
<span class="text-brown-400">
96
96
+
-
97
97
+
</span>
98
98
+
</td>
99
99
+
<td class="px-6 py-4 text-sm text-brown-700 italic max-w-xs">
100
100
+
Suave y aromático
101
101
+
</td>
102
102
+
</tr>
103
103
+
</tbody>
104
104
+
</table>
105
105
+
</div>
106
106
+
</div>
107
107
+
<div>
108
108
+
<h3 class="text-lg font-semibold text-brown-900 mb-3">
109
109
+
🏪 Favorite Roasters
110
110
+
</h3>
111
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
112
+
<table class="min-w-full divide-y divide-brown-300">
113
113
+
<thead class="bg-brown-200/80">
114
114
+
<tr>
115
115
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
116
116
+
Name
117
117
+
</th>
118
118
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
119
119
+
📍 Location
120
120
+
</th>
121
121
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase tracking-wider">
122
122
+
🌐 Website
123
123
+
</th>
124
124
+
</tr>
125
125
+
</thead>
126
126
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
127
127
+
<tr class="hover:bg-brown-100/60 transition-colors">
128
128
+
<td class="px-6 py-4 text-sm font-bold text-brown-900">
129
129
+
Кофейня Москва
130
130
+
</td>
131
131
+
<td class="px-6 py-4 text-sm text-brown-900">
132
132
+
Москва, Россия
133
133
+
</td>
134
134
+
<td class="px-6 py-4 text-sm text-brown-900">
135
135
+
<span class="text-brown-400">
136
136
+
-
137
137
+
</span>
138
138
+
</td>
139
139
+
</tr>
140
140
+
</tbody>
141
141
+
</table>
142
142
+
</div>
143
143
+
</div>
144
144
+
</div>
145
145
+
<div x-show="activeTab === 'gear'" x-cloak class="space-y-6">
146
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
147
+
<p class="font-medium">
148
148
+
No gear added yet.
149
149
+
</p>
150
150
+
</div>
151
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
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
7
+
<div class="space-y-4">
8
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
9
+
<div class="flex items-center gap-3 mb-3">
10
10
+
<a href="/profile/badavatar" class="flex-shrink-0">
11
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
12
+
<span class="text-brown-600 text-sm">
13
13
+
?
14
14
+
</span>
15
15
+
</div>
16
16
+
</a>
17
17
+
<div class="flex-1 min-w-0">
18
18
+
<div class="flex items-center gap-2">
19
19
+
<a href="/profile/badavatar" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline">
20
20
+
Bad Avatar
21
21
+
</a>
22
22
+
<a href="/profile/badavatar" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
23
23
+
@badavatar
24
24
+
</a>
25
25
+
</div>
26
26
+
<span class="text-brown-500 text-sm">
27
27
+
2 minutes ago
28
28
+
</span>
29
29
+
</div>
30
30
+
</div>
31
31
+
<div class="mb-2 text-sm text-brown-700">
32
32
+
🫘 added a new bean
33
33
+
</div>
34
34
+
<div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200">
35
35
+
<div class="text-base mb-2">
36
36
+
<span class="font-bold text-brown-900">
37
37
+
Test Bean
38
38
+
</span>
39
39
+
</div>
40
40
+
<div class="text-sm text-brown-700 space-y-1">
41
41
+
<div>
42
42
+
<span class="text-brown-600">
43
43
+
Origin:
44
44
+
</span>
45
45
+
Test Origin
46
46
+
</div>
47
47
+
<div>
48
48
+
<span class="text-brown-600">
49
49
+
Roast:
50
50
+
</span>
51
51
+
Medium
52
52
+
</div>
53
53
+
<div>
54
54
+
<span class="text-brown-600">
55
55
+
Process:
56
56
+
</span>
57
57
+
Natural
58
58
+
</div>
59
59
+
<div class="mt-2 text-brown-800 italic">
60
60
+
"Sweet and fruity with notes of blueberry"
61
61
+
</div>
62
62
+
</div>
63
63
+
</div>
64
64
+
</div>
65
65
+
</div>
+29
internal/bff/__snapshots__/roaster_form_renders.snap
···
1
1
+
---
2
2
+
title: roaster_form_renders
3
3
+
test_name: TestNewRoasterForm_Snapshot/roaster_form_renders
4
4
+
file_name: form_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
8
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
9
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3>
10
10
+
<div class="space-y-4">
11
11
+
<input type="text" x-model="roasterForm.name" placeholder="Name *"
12
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
13
+
<input type="text" x-model="roasterForm.location" placeholder="Location"
14
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
15
+
<input type="url" x-model="roasterForm.website" placeholder="Website"
16
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
17
+
<div class="flex gap-2">
18
18
+
<button @click="saveRoaster()"
19
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
20
+
Save
21
21
+
</button>
22
22
+
<button @click="showRoasterForm = false"
23
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
24
+
Cancel
25
25
+
</button>
26
26
+
</div>
27
27
+
</div>
28
28
+
</div>
29
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
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
7
+
<div class="space-y-4">
8
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
9
+
<div class="flex items-center gap-3 mb-3">
10
10
+
<a href="/profile/roastmaster" class="flex-shrink-0">
11
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
12
+
</a>
13
13
+
<div class="flex-1 min-w-0">
14
14
+
<div class="flex items-center gap-2">
15
15
+
<a href="/profile/roastmaster" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline">
16
16
+
Roast Master
17
17
+
</a>
18
18
+
<a href="/profile/roastmaster" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
19
19
+
@roastmaster
20
20
+
</a>
21
21
+
</div>
22
22
+
<span class="text-brown-500 text-sm">
23
23
+
10 minutes ago
24
24
+
</span>
25
25
+
</div>
26
26
+
</div>
27
27
+
<div class="mb-2 text-sm text-brown-700">
28
28
+
🏪 added a new roaster
29
29
+
</div>
30
30
+
<div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200">
31
31
+
<div class="text-base mb-2">
32
32
+
<span class="font-bold text-brown-900">
33
33
+
Heart Coffee Roasters
34
34
+
</span>
35
35
+
</div>
36
36
+
<div class="text-sm text-brown-700 space-y-1">
37
37
+
<div>
38
38
+
<span class="text-brown-600">
39
39
+
Location:
40
40
+
</span>
41
41
+
Portland, OR
42
42
+
</div>
43
43
+
<div>
44
44
+
<span class="text-brown-600">
45
45
+
Website:
46
46
+
</span>
47
47
+
<a href="https://heartroasters.com" target="_blank" rel="noopener noreferrer" class="text-brown-800 hover:underline">
48
48
+
https://heartroasters.com
49
49
+
</a>
50
50
+
</div>
51
51
+
</div>
52
52
+
</div>
53
53
+
</div>
54
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
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
7
+
<div class="space-y-4">
8
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
9
+
<div class="flex items-center gap-3 mb-3">
10
10
+
<a href="/profile/hacker" class="flex-shrink-0">
11
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
12
+
<span class="text-brown-600 text-sm">
13
13
+
?
14
14
+
</span>
15
15
+
</div>
16
16
+
</a>
17
17
+
<div class="flex-1 min-w-0">
18
18
+
<div class="flex items-center gap-2">
19
19
+
<a href="/profile/hacker" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline">
20
20
+
Hacker
21
21
+
</a>
22
22
+
<a href="/profile/hacker" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
23
23
+
@hacker
24
24
+
</a>
25
25
+
</div>
26
26
+
<span class="text-brown-500 text-sm">
27
27
+
1 minute ago
28
28
+
</span>
29
29
+
</div>
30
30
+
</div>
31
31
+
<div class="mb-2 text-sm text-brown-700">
32
32
+
🏪 added a new roaster
33
33
+
</div>
34
34
+
<div class="bg-white/60 backdrop-blur rounded-lg p-3 border border-brown-200">
35
35
+
<div class="text-base mb-2">
36
36
+
<span class="font-bold text-brown-900">
37
37
+
Sketchy Roaster
38
38
+
</span>
39
39
+
</div>
40
40
+
<div class="text-sm text-brown-700 space-y-1"></div>
41
41
+
</div>
42
42
+
</div>
43
43
+
</div>
+210
internal/bff/__snapshots__/roasters_empty.snap
···
1
1
+
---
2
2
+
title: roasters empty
3
3
+
test_name: TestManageContent_RoastersTab_Snapshot/roasters_empty
4
4
+
file_name: partial_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="tab === 'beans'">
8
8
+
<div class="mb-4 flex justify-between items-center">
9
9
+
<h3 class="text-xl font-semibold text-brown-900">
10
10
+
Coffee Beans
11
11
+
</h3>
12
12
+
<button
13
13
+
@click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}"
14
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
15
+
+ Add Bean
16
16
+
</button>
17
17
+
</div>
18
18
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
19
19
+
No beans yet. Add your first bean to get started!
20
20
+
</div>
21
21
+
</div>
22
22
+
<div x-show="tab === 'roasters'">
23
23
+
<div class="mb-4 flex justify-between items-center">
24
24
+
<h3 class="text-xl font-semibold text-brown-900">
25
25
+
Roasters
26
26
+
</h3>
27
27
+
<button
28
28
+
@click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}"
29
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
30
+
+ Add Roaster
31
31
+
</button>
32
32
+
</div>
33
33
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
34
34
+
No roasters yet. Add your first roaster!
35
35
+
</div>
36
36
+
</div>
37
37
+
<div x-show="tab === 'grinders'">
38
38
+
<div class="mb-4 flex justify-between items-center">
39
39
+
<h3 class="text-xl font-semibold text-brown-900">
40
40
+
Grinders
41
41
+
</h3>
42
42
+
<button
43
43
+
@click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}"
44
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
45
+
+ Add Grinder
46
46
+
</button>
47
47
+
</div>
48
48
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
49
49
+
No grinders yet. Add your first grinder!
50
50
+
</div>
51
51
+
</div>
52
52
+
<div x-show="tab === 'brewers'">
53
53
+
<div class="mb-4 flex justify-between items-center">
54
54
+
<h3 class="text-xl font-semibold text-brown-900">
55
55
+
Brewers
56
56
+
</h3>
57
57
+
<button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}"
58
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
59
+
+ Add Brewer
60
60
+
</button>
61
61
+
</div>
62
62
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
63
63
+
No brewers yet. Add your first brewer!
64
64
+
</div>
65
65
+
</div>
66
66
+
<div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
67
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
68
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3>
69
69
+
<div class="space-y-4">
70
70
+
<input type="text" x-model="beanForm.name" placeholder="Name *"
71
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
72
+
<input type="text" x-model="beanForm.origin" placeholder="Origin *"
73
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
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
75
+
<option value="">
76
76
+
Select Roaster (Optional)
77
77
+
</option>
78
78
+
</select>
79
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
80
+
<option value="">
81
81
+
Select Roast Level (Optional)
82
82
+
</option>
83
83
+
<option value="Ultra-Light">
84
84
+
Ultra-Light
85
85
+
</option>
86
86
+
<option value="Light">
87
87
+
Light
88
88
+
</option>
89
89
+
<option value="Medium-Light">
90
90
+
Medium-Light
91
91
+
</option>
92
92
+
<option value="Medium">
93
93
+
Medium
94
94
+
</option>
95
95
+
<option value="Medium-Dark">
96
96
+
Medium-Dark
97
97
+
</option>
98
98
+
<option value="Dark">
99
99
+
Dark
100
100
+
</option>
101
101
+
</select>
102
102
+
<input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)"
103
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
104
+
<textarea x-model="beanForm.description" placeholder="Description" rows="3"
105
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
106
+
<div class="flex gap-2">
107
107
+
<button @click="saveBean()"
108
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
109
+
Save
110
110
+
</button>
111
111
+
<button @click="showBeanForm = false"
112
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
113
+
Cancel
114
114
+
</button>
115
115
+
</div>
116
116
+
</div>
117
117
+
</div>
118
118
+
</div>
119
119
+
<div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
120
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
121
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3>
122
122
+
<div class="space-y-4">
123
123
+
<input type="text" x-model="roasterForm.name" placeholder="Name *"
124
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
125
+
<input type="text" x-model="roasterForm.location" placeholder="Location"
126
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
127
+
<input type="url" x-model="roasterForm.website" placeholder="Website"
128
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
129
+
<div class="flex gap-2">
130
130
+
<button @click="saveRoaster()"
131
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
132
+
Save
133
133
+
</button>
134
134
+
<button @click="showRoasterForm = false"
135
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
136
+
Cancel
137
137
+
</button>
138
138
+
</div>
139
139
+
</div>
140
140
+
</div>
141
141
+
</div>
142
142
+
<div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
143
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
144
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3>
145
145
+
<div class="space-y-4">
146
146
+
<input type="text" x-model="grinderForm.name" placeholder="Name *"
147
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
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
149
+
<option value="">
150
150
+
Select Grinder Type *
151
151
+
</option>
152
152
+
<option value="Hand">
153
153
+
Hand
154
154
+
</option>
155
155
+
<option value="Electric">
156
156
+
Electric
157
157
+
</option>
158
158
+
<option value="Portable Electric">
159
159
+
Portable Electric
160
160
+
</option>
161
161
+
</select>
162
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
163
+
<option value="">
164
164
+
Select Burr Type (Optional)
165
165
+
</option>
166
166
+
<option value="Conical">
167
167
+
Conical
168
168
+
</option>
169
169
+
<option value="Flat">
170
170
+
Flat
171
171
+
</option>
172
172
+
</select>
173
173
+
<textarea x-model="grinderForm.notes" placeholder="Notes" rows="3"
174
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
175
+
<div class="flex gap-2">
176
176
+
<button @click="saveGrinder()"
177
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
178
+
Save
179
179
+
</button>
180
180
+
<button @click="showGrinderForm = false"
181
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
182
+
Cancel
183
183
+
</button>
184
184
+
</div>
185
185
+
</div>
186
186
+
</div>
187
187
+
</div>
188
188
+
<div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
189
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
190
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3>
191
191
+
<div class="space-y-4">
192
192
+
<input type="text" x-model="brewerForm.name" placeholder="Name *"
193
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
194
+
<input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)"
195
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
196
+
<textarea x-model="brewerForm.description" placeholder="Description" rows="3"
197
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
198
+
<div class="flex gap-2">
199
199
+
<button @click="saveBrewer()"
200
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
201
+
Save
202
202
+
</button>
203
203
+
<button @click="showBrewerForm = false"
204
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
205
+
Cancel
206
206
+
</button>
207
207
+
</div>
208
208
+
</div>
209
209
+
</div>
210
210
+
</div>
+276
internal/bff/__snapshots__/roasters_with_data.snap
···
1
1
+
---
2
2
+
title: roasters with data
3
3
+
test_name: TestManageContent_RoastersTab_Snapshot/roasters_with_data
4
4
+
file_name: partial_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="tab === 'beans'">
8
8
+
<div class="mb-4 flex justify-between items-center">
9
9
+
<h3 class="text-xl font-semibold text-brown-900">
10
10
+
Coffee Beans
11
11
+
</h3>
12
12
+
<button
13
13
+
@click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}"
14
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
15
+
+ Add Bean
16
16
+
</button>
17
17
+
</div>
18
18
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
19
19
+
No beans yet. Add your first bean to get started!
20
20
+
</div>
21
21
+
</div>
22
22
+
<div x-show="tab === 'roasters'">
23
23
+
<div class="mb-4 flex justify-between items-center">
24
24
+
<h3 class="text-xl font-semibold text-brown-900">
25
25
+
Roasters
26
26
+
</h3>
27
27
+
<button
28
28
+
@click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}"
29
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
30
+
+ Add Roaster
31
31
+
</button>
32
32
+
</div>
33
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
34
+
<table class="min-w-full divide-y divide-brown-300">
35
35
+
<thead class="bg-brown-200/80">
36
36
+
<tr>
37
37
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
38
38
+
Name
39
39
+
</th>
40
40
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
41
41
+
📍 Location
42
42
+
</th>
43
43
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
44
44
+
🌐 Website
45
45
+
</th>
46
46
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
47
47
+
Actions
48
48
+
</th>
49
49
+
</tr>
50
50
+
</thead>
51
51
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
52
52
+
<tr class="hover:bg-brown-100/60 transition-colors">
53
53
+
<td class="px-6 py-4 text-sm font-medium text-brown-900">
54
54
+
Onyx Coffee Lab
55
55
+
</td>
56
56
+
<td class="px-6 py-4 text-sm text-brown-900">
57
57
+
Bentonville, AR
58
58
+
</td>
59
59
+
<td class="px-6 py-4 text-sm text-brown-900">
60
60
+
<a href="https://onyxcoffeelab.com" target="_blank" rel="noopener noreferrer"
61
61
+
class="text-brown-700 hover:underline font-medium">
62
62
+
https://onyxcoffeelab.com
63
63
+
</a>
64
64
+
</td>
65
65
+
<td class="px-6 py-4 text-sm font-medium space-x-2">
66
66
+
<button @click="editRoaster('roaster1', 'Onyx Coffee Lab', 'Bentonville, AR', 'https://onyxcoffeelab.com')"
67
67
+
class="text-brown-700 hover:text-brown-900 font-medium">
68
68
+
Edit
69
69
+
</button>
70
70
+
<button @click="deleteRoaster('roaster1')"
71
71
+
class="text-brown-600 hover:text-brown-800 font-medium">
72
72
+
Delete
73
73
+
</button>
74
74
+
</td>
75
75
+
</tr>
76
76
+
<tr class="hover:bg-brown-100/60 transition-colors">
77
77
+
<td class="px-6 py-4 text-sm font-medium text-brown-900">
78
78
+
Counter Culture Coffee
79
79
+
</td>
80
80
+
<td class="px-6 py-4 text-sm text-brown-900"></td>
81
81
+
<td class="px-6 py-4 text-sm text-brown-900"></td>
82
82
+
<td class="px-6 py-4 text-sm font-medium space-x-2">
83
83
+
<button @click="editRoaster('roaster2', 'Counter Culture Coffee', '', '')"
84
84
+
class="text-brown-700 hover:text-brown-900 font-medium">
85
85
+
Edit
86
86
+
</button>
87
87
+
<button @click="deleteRoaster('roaster2')"
88
88
+
class="text-brown-600 hover:text-brown-800 font-medium">
89
89
+
Delete
90
90
+
</button>
91
91
+
</td>
92
92
+
</tr>
93
93
+
</tbody>
94
94
+
</table>
95
95
+
</div>
96
96
+
</div>
97
97
+
<div x-show="tab === 'grinders'">
98
98
+
<div class="mb-4 flex justify-between items-center">
99
99
+
<h3 class="text-xl font-semibold text-brown-900">
100
100
+
Grinders
101
101
+
</h3>
102
102
+
<button
103
103
+
@click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}"
104
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
105
+
+ Add Grinder
106
106
+
</button>
107
107
+
</div>
108
108
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
109
109
+
No grinders yet. Add your first grinder!
110
110
+
</div>
111
111
+
</div>
112
112
+
<div x-show="tab === 'brewers'">
113
113
+
<div class="mb-4 flex justify-between items-center">
114
114
+
<h3 class="text-xl font-semibold text-brown-900">
115
115
+
Brewers
116
116
+
</h3>
117
117
+
<button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}"
118
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
119
+
+ Add Brewer
120
120
+
</button>
121
121
+
</div>
122
122
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
123
123
+
No brewers yet. Add your first brewer!
124
124
+
</div>
125
125
+
</div>
126
126
+
<div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
127
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
128
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3>
129
129
+
<div class="space-y-4">
130
130
+
<input type="text" x-model="beanForm.name" placeholder="Name *"
131
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
132
+
<input type="text" x-model="beanForm.origin" placeholder="Origin *"
133
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
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
135
+
<option value="">
136
136
+
Select Roaster (Optional)
137
137
+
</option>
138
138
+
<option value="roaster1">
139
139
+
Onyx Coffee Lab
140
140
+
</option>
141
141
+
<option value="roaster2">
142
142
+
Counter Culture Coffee
143
143
+
</option>
144
144
+
</select>
145
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
146
+
<option value="">
147
147
+
Select Roast Level (Optional)
148
148
+
</option>
149
149
+
<option value="Ultra-Light">
150
150
+
Ultra-Light
151
151
+
</option>
152
152
+
<option value="Light">
153
153
+
Light
154
154
+
</option>
155
155
+
<option value="Medium-Light">
156
156
+
Medium-Light
157
157
+
</option>
158
158
+
<option value="Medium">
159
159
+
Medium
160
160
+
</option>
161
161
+
<option value="Medium-Dark">
162
162
+
Medium-Dark
163
163
+
</option>
164
164
+
<option value="Dark">
165
165
+
Dark
166
166
+
</option>
167
167
+
</select>
168
168
+
<input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)"
169
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
170
+
<textarea x-model="beanForm.description" placeholder="Description" rows="3"
171
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
172
+
<div class="flex gap-2">
173
173
+
<button @click="saveBean()"
174
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
175
+
Save
176
176
+
</button>
177
177
+
<button @click="showBeanForm = false"
178
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
179
+
Cancel
180
180
+
</button>
181
181
+
</div>
182
182
+
</div>
183
183
+
</div>
184
184
+
</div>
185
185
+
<div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
186
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
187
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3>
188
188
+
<div class="space-y-4">
189
189
+
<input type="text" x-model="roasterForm.name" placeholder="Name *"
190
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
191
+
<input type="text" x-model="roasterForm.location" placeholder="Location"
192
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
193
+
<input type="url" x-model="roasterForm.website" placeholder="Website"
194
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
195
+
<div class="flex gap-2">
196
196
+
<button @click="saveRoaster()"
197
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
198
+
Save
199
199
+
</button>
200
200
+
<button @click="showRoasterForm = false"
201
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
202
+
Cancel
203
203
+
</button>
204
204
+
</div>
205
205
+
</div>
206
206
+
</div>
207
207
+
</div>
208
208
+
<div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
209
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
210
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3>
211
211
+
<div class="space-y-4">
212
212
+
<input type="text" x-model="grinderForm.name" placeholder="Name *"
213
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
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
215
+
<option value="">
216
216
+
Select Grinder Type *
217
217
+
</option>
218
218
+
<option value="Hand">
219
219
+
Hand
220
220
+
</option>
221
221
+
<option value="Electric">
222
222
+
Electric
223
223
+
</option>
224
224
+
<option value="Portable Electric">
225
225
+
Portable Electric
226
226
+
</option>
227
227
+
</select>
228
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
229
+
<option value="">
230
230
+
Select Burr Type (Optional)
231
231
+
</option>
232
232
+
<option value="Conical">
233
233
+
Conical
234
234
+
</option>
235
235
+
<option value="Flat">
236
236
+
Flat
237
237
+
</option>
238
238
+
</select>
239
239
+
<textarea x-model="grinderForm.notes" placeholder="Notes" rows="3"
240
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
241
+
<div class="flex gap-2">
242
242
+
<button @click="saveGrinder()"
243
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
244
+
Save
245
245
+
</button>
246
246
+
<button @click="showGrinderForm = false"
247
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
248
+
Cancel
249
249
+
</button>
250
250
+
</div>
251
251
+
</div>
252
252
+
</div>
253
253
+
</div>
254
254
+
<div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
255
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
256
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3>
257
257
+
<div class="space-y-4">
258
258
+
<input type="text" x-model="brewerForm.name" placeholder="Name *"
259
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
260
+
<input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)"
261
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
262
+
<textarea x-model="brewerForm.description" placeholder="Description" rows="3"
263
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
264
+
<div class="flex gap-2">
265
265
+
<button @click="saveBrewer()"
266
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
267
+
Save
268
268
+
</button>
269
269
+
<button @click="showBrewerForm = false"
270
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
271
+
Cancel
272
272
+
</button>
273
273
+
</div>
274
274
+
</div>
275
275
+
</div>
276
276
+
</div>
+251
internal/bff/__snapshots__/roasters_with_unsafe_url.snap
···
1
1
+
---
2
2
+
title: roasters with unsafe url
3
3
+
test_name: TestManageContent_RoastersTab_Snapshot/roasters_with_unsafe_url
4
4
+
file_name: partial_template_snapshot_test.go
5
5
+
version: 0.1.0
6
6
+
---
7
7
+
<div x-show="tab === 'beans'">
8
8
+
<div class="mb-4 flex justify-between items-center">
9
9
+
<h3 class="text-xl font-semibold text-brown-900">
10
10
+
Coffee Beans
11
11
+
</h3>
12
12
+
<button
13
13
+
@click="showBeanForm = true; editingBean = null; beanForm = {name: '', origin: '', roast_level: '', process: '', description: '', roaster_rkey: ''}"
14
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
15
+
+ Add Bean
16
16
+
</button>
17
17
+
</div>
18
18
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
19
19
+
No beans yet. Add your first bean to get started!
20
20
+
</div>
21
21
+
</div>
22
22
+
<div x-show="tab === 'roasters'">
23
23
+
<div class="mb-4 flex justify-between items-center">
24
24
+
<h3 class="text-xl font-semibold text-brown-900">
25
25
+
Roasters
26
26
+
</h3>
27
27
+
<button
28
28
+
@click="showRoasterForm = true; editingRoaster = null; roasterForm = {name: '', location: '', website: ''}"
29
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
30
+
+ Add Roaster
31
31
+
</button>
32
32
+
</div>
33
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
34
+
<table class="min-w-full divide-y divide-brown-300">
35
35
+
<thead class="bg-brown-200/80">
36
36
+
<tr>
37
37
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
38
38
+
Name
39
39
+
</th>
40
40
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
41
41
+
📍 Location
42
42
+
</th>
43
43
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
44
44
+
🌐 Website
45
45
+
</th>
46
46
+
<th class="px-6 py-3 text-left text-xs font-medium text-brown-900 uppercase">
47
47
+
Actions
48
48
+
</th>
49
49
+
</tr>
50
50
+
</thead>
51
51
+
<tbody class="bg-brown-50/60 divide-y divide-brown-200">
52
52
+
<tr class="hover:bg-brown-100/60 transition-colors">
53
53
+
<td class="px-6 py-4 text-sm font-medium text-brown-900">
54
54
+
Test Roaster
55
55
+
</td>
56
56
+
<td class="px-6 py-4 text-sm text-brown-900">
57
57
+
Test Location
58
58
+
</td>
59
59
+
<td class="px-6 py-4 text-sm text-brown-900"></td>
60
60
+
<td class="px-6 py-4 text-sm font-medium space-x-2">
61
61
+
<button @click="editRoaster('roaster1', 'Test Roaster', 'Test Location', 'javascript:alert(\'xss\')')"
62
62
+
class="text-brown-700 hover:text-brown-900 font-medium">
63
63
+
Edit
64
64
+
</button>
65
65
+
<button @click="deleteRoaster('roaster1')"
66
66
+
class="text-brown-600 hover:text-brown-800 font-medium">
67
67
+
Delete
68
68
+
</button>
69
69
+
</td>
70
70
+
</tr>
71
71
+
</tbody>
72
72
+
</table>
73
73
+
</div>
74
74
+
</div>
75
75
+
<div x-show="tab === 'grinders'">
76
76
+
<div class="mb-4 flex justify-between items-center">
77
77
+
<h3 class="text-xl font-semibold text-brown-900">
78
78
+
Grinders
79
79
+
</h3>
80
80
+
<button
81
81
+
@click="showGrinderForm = true; editingGrinder = null; grinderForm = {name: '', grinder_type: '', burr_type: '', notes: ''}"
82
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
83
+
+ Add Grinder
84
84
+
</button>
85
85
+
</div>
86
86
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
87
87
+
No grinders yet. Add your first grinder!
88
88
+
</div>
89
89
+
</div>
90
90
+
<div x-show="tab === 'brewers'">
91
91
+
<div class="mb-4 flex justify-between items-center">
92
92
+
<h3 class="text-xl font-semibold text-brown-900">
93
93
+
Brewers
94
94
+
</h3>
95
95
+
<button @click="showBrewerForm = true; editingBrewer = null; brewerForm = {name: '', brewer_type: '', description: ''}"
96
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
97
+
+ Add Brewer
98
98
+
</button>
99
99
+
</div>
100
100
+
<div class="bg-brown-100 rounded-lg p-8 text-center text-brown-700 border border-brown-200">
101
101
+
No brewers yet. Add your first brewer!
102
102
+
</div>
103
103
+
</div>
104
104
+
<div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
105
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
106
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3>
107
107
+
<div class="space-y-4">
108
108
+
<input type="text" x-model="beanForm.name" placeholder="Name *"
109
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
110
+
<input type="text" x-model="beanForm.origin" placeholder="Origin *"
111
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
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
113
+
<option value="">
114
114
+
Select Roaster (Optional)
115
115
+
</option>
116
116
+
<option value="roaster1">
117
117
+
Test Roaster
118
118
+
</option>
119
119
+
</select>
120
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
121
+
<option value="">
122
122
+
Select Roast Level (Optional)
123
123
+
</option>
124
124
+
<option value="Ultra-Light">
125
125
+
Ultra-Light
126
126
+
</option>
127
127
+
<option value="Light">
128
128
+
Light
129
129
+
</option>
130
130
+
<option value="Medium-Light">
131
131
+
Medium-Light
132
132
+
</option>
133
133
+
<option value="Medium">
134
134
+
Medium
135
135
+
</option>
136
136
+
<option value="Medium-Dark">
137
137
+
Medium-Dark
138
138
+
</option>
139
139
+
<option value="Dark">
140
140
+
Dark
141
141
+
</option>
142
142
+
</select>
143
143
+
<input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)"
144
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
145
+
<textarea x-model="beanForm.description" placeholder="Description" rows="3"
146
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
147
+
<div class="flex gap-2">
148
148
+
<button @click="saveBean()"
149
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
150
+
Save
151
151
+
</button>
152
152
+
<button @click="showBeanForm = false"
153
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
154
+
Cancel
155
155
+
</button>
156
156
+
</div>
157
157
+
</div>
158
158
+
</div>
159
159
+
</div>
160
160
+
<div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
161
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
162
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3>
163
163
+
<div class="space-y-4">
164
164
+
<input type="text" x-model="roasterForm.name" placeholder="Name *"
165
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
166
+
<input type="text" x-model="roasterForm.location" placeholder="Location"
167
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
168
+
<input type="url" x-model="roasterForm.website" placeholder="Website"
169
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
170
+
<div class="flex gap-2">
171
171
+
<button @click="saveRoaster()"
172
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
173
+
Save
174
174
+
</button>
175
175
+
<button @click="showRoasterForm = false"
176
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
177
+
Cancel
178
178
+
</button>
179
179
+
</div>
180
180
+
</div>
181
181
+
</div>
182
182
+
</div>
183
183
+
<div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
184
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
185
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3>
186
186
+
<div class="space-y-4">
187
187
+
<input type="text" x-model="grinderForm.name" placeholder="Name *"
188
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
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
190
+
<option value="">
191
191
+
Select Grinder Type *
192
192
+
</option>
193
193
+
<option value="Hand">
194
194
+
Hand
195
195
+
</option>
196
196
+
<option value="Electric">
197
197
+
Electric
198
198
+
</option>
199
199
+
<option value="Portable Electric">
200
200
+
Portable Electric
201
201
+
</option>
202
202
+
</select>
203
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
204
+
<option value="">
205
205
+
Select Burr Type (Optional)
206
206
+
</option>
207
207
+
<option value="Conical">
208
208
+
Conical
209
209
+
</option>
210
210
+
<option value="Flat">
211
211
+
Flat
212
212
+
</option>
213
213
+
</select>
214
214
+
<textarea x-model="grinderForm.notes" placeholder="Notes" rows="3"
215
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
216
+
<div class="flex gap-2">
217
217
+
<button @click="saveGrinder()"
218
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
219
+
Save
220
220
+
</button>
221
221
+
<button @click="showGrinderForm = false"
222
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
223
+
Cancel
224
224
+
</button>
225
225
+
</div>
226
226
+
</div>
227
227
+
</div>
228
228
+
</div>
229
229
+
<div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
230
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
231
+
<h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3>
232
232
+
<div class="space-y-4">
233
233
+
<input type="text" x-model="brewerForm.name" placeholder="Name *"
234
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
235
+
<input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)"
236
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
237
+
<textarea x-model="brewerForm.description" placeholder="Description" rows="3"
238
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
239
+
<div class="flex gap-2">
240
240
+
<button @click="saveBrewer()"
241
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
242
+
Save
243
243
+
</button>
244
244
+
<button @click="showBrewerForm = false"
245
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
246
+
Cancel
247
247
+
</button>
248
248
+
</div>
249
249
+
</div>
250
250
+
</div>
251
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
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 & 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 & 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 "quotes" and <html>tags</html> and 'single quotes'\"\n </div>\n \n </div>\n \n </div>\n \n \n \n</div>\n"
7
7
+
<div class="space-y-4">
8
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
9
+
<div class="flex items-center gap-3 mb-3">
10
10
+
<a href="/profile/special.chars" class="flex-shrink-0">
11
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
12
+
<span class="text-brown-600 text-sm">
13
13
+
?
14
14
+
</span>
15
15
+
</div>
16
16
+
</a>
17
17
+
<div class="flex-1 min-w-0">
18
18
+
<div class="flex items-center gap-2">
19
19
+
<a href="/profile/special.chars" class="font-medium text-brown-900 truncate hover:text-brown-700 hover:underline">
20
20
+
User & Co.
21
21
+
</a>
22
22
+
<a href="/profile/special.chars" class="text-brown-600 text-sm truncate hover:text-brown-700 hover:underline">
23
23
+
@special.chars
24
24
+
</a>
25
25
+
</div>
26
26
+
<span class="text-brown-500 text-sm">
27
27
+
5 seconds ago
28
28
+
</span>
29
29
+
</div>
30
30
+
</div>
31
31
+
<div class="mb-2 text-sm text-brown-700">
32
32
+
☕ added a new brew
33
33
+
</div>
34
34
+
<div class="bg-white/60 backdrop-blur rounded-lg p-4 border border-brown-200">
35
35
+
<div class="flex items-start justify-between gap-3 mb-3">
36
36
+
<div class="flex-1 min-w-0">
37
37
+
<div class="font-bold text-brown-900 text-base">
38
38
+
Bean with & ampersand
39
39
+
</div>
40
40
+
<div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"></div>
41
41
+
</div>
42
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
43
+
⭐ 8/10
44
44
+
</span>
45
45
+
</div>
46
46
+
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700"></div>
47
47
+
<div class="mt-3 text-sm text-brown-800 italic border-t border-brown-200 pt-2">
48
48
+
"Notes with "quotes" and <html>tags</html> and 'single quotes'"
49
49
+
</div>
50
50
+
</div>
51
51
+
</div>
52
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
240
-
shutter.Snap(t, tt.name, result)
240
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
280
-
shutter.Snap(t, tt.name, result)
280
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
299
-
shutter.Snap(t, "roaster item", result)
299
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
316
-
shutter.Snap(t, "grinder item", result)
316
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
333
-
shutter.Snap(t, "brewer item", result)
333
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
386
-
shutter.Snap(t, "mixed feed all types", result)
386
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
418
-
shutter.Snap(t, tt.name, result)
418
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
465
-
shutter.Snap(t, tt.name, result)
465
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
495
-
shutter.Snap(t, "special characters in content", result)
495
495
+
shutter.SnapString(t, "special characters in content", formatHTML(result))
496
496
}
+380
internal/bff/form_template_snapshot_test.go
···
1
1
+
package bff
2
2
+
3
3
+
import (
4
4
+
"bytes"
5
5
+
"html/template"
6
6
+
"testing"
7
7
+
"time"
8
8
+
9
9
+
"arabica/internal/models"
10
10
+
11
11
+
"github.com/ptdewey/shutter"
12
12
+
)
13
13
+
14
14
+
func TestBrewForm_NewBrew_Snapshot(t *testing.T) {
15
15
+
tests := []struct {
16
16
+
name string
17
17
+
data map[string]interface{}
18
18
+
}{
19
19
+
{
20
20
+
name: "new brew with populated selects",
21
21
+
data: map[string]interface{}{
22
22
+
"Brew": nil,
23
23
+
"Beans": []*models.Bean{
24
24
+
{RKey: "bean1", Name: "Ethiopian Yirgacheffe", Origin: "Ethiopia", RoastLevel: "Light"},
25
25
+
{RKey: "bean2", Name: "", Origin: "Colombia", RoastLevel: "Medium"},
26
26
+
},
27
27
+
"Grinders": []*models.Grinder{
28
28
+
{RKey: "grinder1", Name: "Baratza Encore"},
29
29
+
{RKey: "grinder2", Name: "Comandante C40"},
30
30
+
},
31
31
+
"Brewers": []*models.Brewer{
32
32
+
{RKey: "brewer1", Name: "Hario V60"},
33
33
+
{RKey: "brewer2", Name: "AeroPress"},
34
34
+
},
35
35
+
"Roasters": []*models.Roaster{
36
36
+
{RKey: "roaster1", Name: "Blue Bottle"},
37
37
+
{RKey: "roaster2", Name: "Counter Culture"},
38
38
+
},
39
39
+
},
40
40
+
},
41
41
+
{
42
42
+
name: "new brew with empty selects",
43
43
+
data: map[string]interface{}{
44
44
+
"Brew": nil,
45
45
+
"Beans": []*models.Bean{},
46
46
+
"Grinders": []*models.Grinder{},
47
47
+
"Brewers": []*models.Brewer{},
48
48
+
"Roasters": []*models.Roaster{},
49
49
+
},
50
50
+
},
51
51
+
{
52
52
+
name: "new brew with nil collections",
53
53
+
data: map[string]interface{}{
54
54
+
"Brew": nil,
55
55
+
"Beans": nil,
56
56
+
"Grinders": nil,
57
57
+
"Brewers": nil,
58
58
+
"Roasters": nil,
59
59
+
},
60
60
+
},
61
61
+
}
62
62
+
63
63
+
tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles(
64
64
+
"../../templates/brew_form.tmpl",
65
65
+
"../../templates/partials/new_bean_form.tmpl",
66
66
+
"../../templates/partials/new_grinder_form.tmpl",
67
67
+
"../../templates/partials/new_brewer_form.tmpl",
68
68
+
))
69
69
+
70
70
+
for _, tt := range tests {
71
71
+
t.Run(tt.name, func(t *testing.T) {
72
72
+
var buf bytes.Buffer
73
73
+
err := tmpl.ExecuteTemplate(&buf, "content", tt.data)
74
74
+
if err != nil {
75
75
+
t.Fatalf("template execution failed: %v", err)
76
76
+
}
77
77
+
shutter.SnapString(t, tt.name, formatHTML(buf.String()))
78
78
+
})
79
79
+
}
80
80
+
}
81
81
+
82
82
+
func TestBrewForm_EditBrew_Snapshot(t *testing.T) {
83
83
+
timestamp := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)
84
84
+
85
85
+
tests := []struct {
86
86
+
name string
87
87
+
data map[string]interface{}
88
88
+
}{
89
89
+
{
90
90
+
name: "edit brew with complete data",
91
91
+
data: map[string]interface{}{
92
92
+
"Brew": &BrewData{
93
93
+
Brew: &models.Brew{
94
94
+
RKey: "brew123",
95
95
+
BeanRKey: "bean1",
96
96
+
GrinderRKey: "grinder1",
97
97
+
BrewerRKey: "brewer1",
98
98
+
CoffeeAmount: 18,
99
99
+
WaterAmount: 300,
100
100
+
GrindSize: "18",
101
101
+
Temperature: 93.5,
102
102
+
TimeSeconds: 180,
103
103
+
TastingNotes: "Bright citrus notes with floral aroma. Clean finish.",
104
104
+
Rating: 8,
105
105
+
CreatedAt: timestamp,
106
106
+
Pours: []*models.Pour{
107
107
+
{PourNumber: 1, WaterAmount: 50, TimeSeconds: 30},
108
108
+
{PourNumber: 2, WaterAmount: 100, TimeSeconds: 45},
109
109
+
{PourNumber: 3, WaterAmount: 150, TimeSeconds: 60},
110
110
+
},
111
111
+
},
112
112
+
PoursJSON: `[{"pourNumber":1,"waterAmount":50,"timeSeconds":30},{"pourNumber":2,"waterAmount":100,"timeSeconds":45},{"pourNumber":3,"waterAmount":150,"timeSeconds":60}]`,
113
113
+
},
114
114
+
"Beans": []*models.Bean{
115
115
+
{RKey: "bean1", Name: "Ethiopian Yirgacheffe", Origin: "Ethiopia", RoastLevel: "Light"},
116
116
+
{RKey: "bean2", Name: "Colombian Supremo", Origin: "Colombia", RoastLevel: "Medium"},
117
117
+
},
118
118
+
"Grinders": []*models.Grinder{
119
119
+
{RKey: "grinder1", Name: "Baratza Encore"},
120
120
+
{RKey: "grinder2", Name: "Comandante C40"},
121
121
+
},
122
122
+
"Brewers": []*models.Brewer{
123
123
+
{RKey: "brewer1", Name: "Hario V60"},
124
124
+
{RKey: "brewer2", Name: "AeroPress"},
125
125
+
},
126
126
+
"Roasters": []*models.Roaster{
127
127
+
{RKey: "roaster1", Name: "Blue Bottle"},
128
128
+
},
129
129
+
},
130
130
+
},
131
131
+
{
132
132
+
name: "edit brew with minimal data",
133
133
+
data: map[string]interface{}{
134
134
+
"Brew": &BrewData{
135
135
+
Brew: &models.Brew{
136
136
+
RKey: "brew456",
137
137
+
BeanRKey: "bean1",
138
138
+
Rating: 5,
139
139
+
CreatedAt: timestamp,
140
140
+
Pours: nil,
141
141
+
},
142
142
+
PoursJSON: "",
143
143
+
},
144
144
+
"Beans": []*models.Bean{
145
145
+
{RKey: "bean1", Name: "House Blend", Origin: "Brazil", RoastLevel: "Medium"},
146
146
+
},
147
147
+
"Grinders": nil,
148
148
+
"Brewers": nil,
149
149
+
"Roasters": nil,
150
150
+
},
151
151
+
},
152
152
+
{
153
153
+
name: "edit brew with pours json",
154
154
+
data: map[string]interface{}{
155
155
+
"Brew": &BrewData{
156
156
+
Brew: &models.Brew{
157
157
+
RKey: "brew789",
158
158
+
BeanRKey: "bean1",
159
159
+
Rating: 7,
160
160
+
CreatedAt: timestamp,
161
161
+
Pours: []*models.Pour{
162
162
+
{PourNumber: 1, WaterAmount: 60, TimeSeconds: 30},
163
163
+
{PourNumber: 2, WaterAmount: 120, TimeSeconds: 60},
164
164
+
},
165
165
+
},
166
166
+
PoursJSON: `[{"pourNumber":1,"waterAmount":60,"timeSeconds":30},{"pourNumber":2,"waterAmount":120,"timeSeconds":60}]`,
167
167
+
},
168
168
+
"Beans": []*models.Bean{
169
169
+
{RKey: "bean1", Origin: "Kenya", RoastLevel: "Light"},
170
170
+
},
171
171
+
"Grinders": []*models.Grinder{},
172
172
+
"Brewers": []*models.Brewer{},
173
173
+
"Roasters": []*models.Roaster{},
174
174
+
},
175
175
+
},
176
176
+
{
177
177
+
name: "edit brew without loaded collections",
178
178
+
data: map[string]interface{}{
179
179
+
"Brew": &BrewData{
180
180
+
Brew: &models.Brew{
181
181
+
RKey: "brew999",
182
182
+
BeanRKey: "bean1",
183
183
+
GrinderRKey: "grinder1",
184
184
+
BrewerRKey: "brewer1",
185
185
+
Rating: 6,
186
186
+
CreatedAt: timestamp,
187
187
+
},
188
188
+
PoursJSON: "",
189
189
+
},
190
190
+
"Beans": nil,
191
191
+
"Grinders": nil,
192
192
+
"Brewers": nil,
193
193
+
"Roasters": nil,
194
194
+
},
195
195
+
},
196
196
+
}
197
197
+
198
198
+
tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles(
199
199
+
"../../templates/brew_form.tmpl",
200
200
+
"../../templates/partials/new_bean_form.tmpl",
201
201
+
"../../templates/partials/new_grinder_form.tmpl",
202
202
+
"../../templates/partials/new_brewer_form.tmpl",
203
203
+
))
204
204
+
205
205
+
for _, tt := range tests {
206
206
+
t.Run(tt.name, func(t *testing.T) {
207
207
+
var buf bytes.Buffer
208
208
+
err := tmpl.ExecuteTemplate(&buf, "content", tt.data)
209
209
+
if err != nil {
210
210
+
t.Fatalf("template execution failed: %v", err)
211
211
+
}
212
212
+
shutter.SnapString(t, tt.name, formatHTML(buf.String()))
213
213
+
})
214
214
+
}
215
215
+
}
216
216
+
217
217
+
func TestNewBeanForm_Snapshot(t *testing.T) {
218
218
+
tests := []struct {
219
219
+
name string
220
220
+
data map[string]interface{}
221
221
+
}{
222
222
+
{
223
223
+
name: "bean form with roasters",
224
224
+
data: map[string]interface{}{
225
225
+
"Roasters": []*models.Roaster{
226
226
+
{RKey: "roaster1", Name: "Blue Bottle Coffee"},
227
227
+
{RKey: "roaster2", Name: "Counter Culture Coffee"},
228
228
+
{RKey: "roaster3", Name: "Stumptown Coffee Roasters"},
229
229
+
},
230
230
+
},
231
231
+
},
232
232
+
{
233
233
+
name: "bean form without roasters",
234
234
+
data: map[string]interface{}{
235
235
+
"Roasters": []*models.Roaster{},
236
236
+
},
237
237
+
},
238
238
+
{
239
239
+
name: "bean form with nil roasters",
240
240
+
data: map[string]interface{}{
241
241
+
"Roasters": nil,
242
242
+
},
243
243
+
},
244
244
+
}
245
245
+
246
246
+
tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles(
247
247
+
"../../templates/partials/new_bean_form.tmpl",
248
248
+
))
249
249
+
250
250
+
for _, tt := range tests {
251
251
+
t.Run(tt.name, func(t *testing.T) {
252
252
+
var buf bytes.Buffer
253
253
+
err := tmpl.ExecuteTemplate(&buf, "new_bean_form", tt.data)
254
254
+
if err != nil {
255
255
+
t.Fatalf("template execution failed: %v", err)
256
256
+
}
257
257
+
shutter.SnapString(t, tt.name, formatHTML(buf.String()))
258
258
+
})
259
259
+
}
260
260
+
}
261
261
+
262
262
+
func TestNewGrinderForm_Snapshot(t *testing.T) {
263
263
+
tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles(
264
264
+
"../../templates/partials/new_grinder_form.tmpl",
265
265
+
))
266
266
+
267
267
+
t.Run("grinder form renders", func(t *testing.T) {
268
268
+
var buf bytes.Buffer
269
269
+
err := tmpl.ExecuteTemplate(&buf, "new_grinder_form", nil)
270
270
+
if err != nil {
271
271
+
t.Fatalf("template execution failed: %v", err)
272
272
+
}
273
273
+
shutter.SnapString(t, "grinder_form_renders", formatHTML(buf.String()))
274
274
+
})
275
275
+
}
276
276
+
277
277
+
func TestNewBrewerForm_Snapshot(t *testing.T) {
278
278
+
tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles(
279
279
+
"../../templates/partials/new_brewer_form.tmpl",
280
280
+
))
281
281
+
282
282
+
t.Run("brewer form renders", func(t *testing.T) {
283
283
+
var buf bytes.Buffer
284
284
+
err := tmpl.ExecuteTemplate(&buf, "new_brewer_form", nil)
285
285
+
if err != nil {
286
286
+
t.Fatalf("template execution failed: %v", err)
287
287
+
}
288
288
+
shutter.SnapString(t, "brewer_form_renders", formatHTML(buf.String()))
289
289
+
})
290
290
+
}
291
291
+
292
292
+
func TestNewRoasterForm_Snapshot(t *testing.T) {
293
293
+
tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles(
294
294
+
"../../templates/partials/new_roaster_form.tmpl",
295
295
+
))
296
296
+
297
297
+
t.Run("roaster form renders", func(t *testing.T) {
298
298
+
var buf bytes.Buffer
299
299
+
err := tmpl.ExecuteTemplate(&buf, "new_roaster_form", nil)
300
300
+
if err != nil {
301
301
+
t.Fatalf("template execution failed: %v", err)
302
302
+
}
303
303
+
shutter.SnapString(t, "roaster_form_renders", formatHTML(buf.String()))
304
304
+
})
305
305
+
}
306
306
+
307
307
+
func TestBrewForm_SpecialCharacters_Snapshot(t *testing.T) {
308
308
+
timestamp := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)
309
309
+
310
310
+
tests := []struct {
311
311
+
name string
312
312
+
data map[string]interface{}
313
313
+
}{
314
314
+
{
315
315
+
name: "brew with html in tasting notes",
316
316
+
data: map[string]interface{}{
317
317
+
"Brew": &BrewData{
318
318
+
Brew: &models.Brew{
319
319
+
RKey: "brew1",
320
320
+
BeanRKey: "bean1",
321
321
+
TastingNotes: "<script>alert('xss')</script>Bright & fruity, \"amazing\" taste",
322
322
+
Rating: 8,
323
323
+
CreatedAt: timestamp,
324
324
+
},
325
325
+
PoursJSON: "",
326
326
+
},
327
327
+
"Beans": []*models.Bean{
328
328
+
{RKey: "bean1", Name: "Test <strong>Bean</strong>", Origin: "Ethiopia", RoastLevel: "Light"},
329
329
+
},
330
330
+
"Grinders": []*models.Grinder{},
331
331
+
"Brewers": []*models.Brewer{},
332
332
+
"Roasters": []*models.Roaster{},
333
333
+
},
334
334
+
},
335
335
+
{
336
336
+
name: "brew with unicode characters",
337
337
+
data: map[string]interface{}{
338
338
+
"Brew": &BrewData{
339
339
+
Brew: &models.Brew{
340
340
+
RKey: "brew2",
341
341
+
BeanRKey: "bean1",
342
342
+
TastingNotes: "日本のコーヒー 🇯🇵 - フルーティーで酸味が強い\n\nЯркий вкус с цитрусовыми нотами\n\nCafé con notas de caramelo",
343
343
+
GrindSize: "中挽き (medium)",
344
344
+
Rating: 9,
345
345
+
CreatedAt: timestamp,
346
346
+
},
347
347
+
PoursJSON: "",
348
348
+
},
349
349
+
"Beans": []*models.Bean{
350
350
+
{RKey: "bean1", Name: "Café Especial™", Origin: "Costa Rica", RoastLevel: "Medium"},
351
351
+
},
352
352
+
"Grinders": []*models.Grinder{
353
353
+
{RKey: "grinder1", Name: "Comandante® C40 MK3"},
354
354
+
},
355
355
+
"Brewers": []*models.Brewer{
356
356
+
{RKey: "brewer1", Name: "Hario V60 (02)"},
357
357
+
},
358
358
+
"Roasters": []*models.Roaster{},
359
359
+
},
360
360
+
},
361
361
+
}
362
362
+
363
363
+
tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles(
364
364
+
"../../templates/brew_form.tmpl",
365
365
+
"../../templates/partials/new_bean_form.tmpl",
366
366
+
"../../templates/partials/new_grinder_form.tmpl",
367
367
+
"../../templates/partials/new_brewer_form.tmpl",
368
368
+
))
369
369
+
370
370
+
for _, tt := range tests {
371
371
+
t.Run(tt.name, func(t *testing.T) {
372
372
+
var buf bytes.Buffer
373
373
+
err := tmpl.ExecuteTemplate(&buf, "content", tt.data)
374
374
+
if err != nil {
375
375
+
t.Fatalf("template execution failed: %v", err)
376
376
+
}
377
377
+
shutter.SnapString(t, tt.name, formatHTML(buf.String()))
378
378
+
})
379
379
+
}
380
380
+
}
+380
internal/bff/partial_template_snapshot_test.go
···
1
1
+
package bff
2
2
+
3
3
+
import (
4
4
+
"bytes"
5
5
+
"html/template"
6
6
+
"testing"
7
7
+
"time"
8
8
+
9
9
+
"arabica/internal/models"
10
10
+
11
11
+
"github.com/ptdewey/shutter"
12
12
+
)
13
13
+
14
14
+
func TestBrewListContent_Snapshot(t *testing.T) {
15
15
+
timestamp := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)
16
16
+
17
17
+
tests := []struct {
18
18
+
name string
19
19
+
data map[string]interface{}
20
20
+
}{
21
21
+
{
22
22
+
name: "empty brew list own profile",
23
23
+
data: map[string]interface{}{
24
24
+
"Brews": []*models.Brew{},
25
25
+
"IsOwnProfile": true,
26
26
+
},
27
27
+
},
28
28
+
{
29
29
+
name: "empty brew list other profile",
30
30
+
data: map[string]interface{}{
31
31
+
"Brews": []*models.Brew{},
32
32
+
"IsOwnProfile": false,
33
33
+
},
34
34
+
},
35
35
+
{
36
36
+
name: "brew list with complete data",
37
37
+
data: map[string]interface{}{
38
38
+
"Brews": []*models.Brew{
39
39
+
{
40
40
+
RKey: "brew1",
41
41
+
BeanRKey: "bean1",
42
42
+
CoffeeAmount: 18,
43
43
+
WaterAmount: 250,
44
44
+
Temperature: 93.0,
45
45
+
TimeSeconds: 180,
46
46
+
GrindSize: "Medium-fine",
47
47
+
Rating: 8,
48
48
+
TastingNotes: "Bright citrus notes with floral aroma. Clean finish.",
49
49
+
CreatedAt: timestamp,
50
50
+
Bean: &models.Bean{
51
51
+
Name: "Ethiopian Yirgacheffe",
52
52
+
Origin: "Ethiopia",
53
53
+
RoastLevel: "Light",
54
54
+
Roaster: &models.Roaster{
55
55
+
Name: "Onyx Coffee Lab",
56
56
+
},
57
57
+
},
58
58
+
GrinderObj: &models.Grinder{
59
59
+
Name: "Comandante C40",
60
60
+
},
61
61
+
BrewerObj: &models.Brewer{
62
62
+
Name: "Hario V60",
63
63
+
},
64
64
+
Pours: []*models.Pour{
65
65
+
{PourNumber: 1, WaterAmount: 50, TimeSeconds: 30},
66
66
+
{PourNumber: 2, WaterAmount: 100, TimeSeconds: 45},
67
67
+
{PourNumber: 3, WaterAmount: 100, TimeSeconds: 60},
68
68
+
},
69
69
+
},
70
70
+
{
71
71
+
RKey: "brew2",
72
72
+
BeanRKey: "bean2",
73
73
+
Rating: 6,
74
74
+
CreatedAt: timestamp.Add(-24 * time.Hour),
75
75
+
Bean: &models.Bean{
76
76
+
Origin: "Colombia",
77
77
+
RoastLevel: "Medium",
78
78
+
},
79
79
+
Method: "AeroPress",
80
80
+
},
81
81
+
},
82
82
+
"IsOwnProfile": true,
83
83
+
},
84
84
+
},
85
85
+
{
86
86
+
name: "brew list minimal data",
87
87
+
data: map[string]interface{}{
88
88
+
"Brews": []*models.Brew{
89
89
+
{
90
90
+
RKey: "brew3",
91
91
+
BeanRKey: "bean3",
92
92
+
CreatedAt: timestamp,
93
93
+
},
94
94
+
},
95
95
+
"IsOwnProfile": false,
96
96
+
},
97
97
+
},
98
98
+
}
99
99
+
100
100
+
tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles(
101
101
+
"../../templates/partials/brew_list_content.tmpl",
102
102
+
))
103
103
+
104
104
+
for _, tt := range tests {
105
105
+
t.Run(tt.name, func(t *testing.T) {
106
106
+
var buf bytes.Buffer
107
107
+
err := tmpl.ExecuteTemplate(&buf, "brew_list_content", tt.data)
108
108
+
if err != nil {
109
109
+
t.Fatalf("template execution failed: %v", err)
110
110
+
}
111
111
+
shutter.SnapString(t, tt.name, formatHTML(buf.String()))
112
112
+
})
113
113
+
}
114
114
+
}
115
115
+
116
116
+
func TestManageContent_BeansTab_Snapshot(t *testing.T) {
117
117
+
tests := []struct {
118
118
+
name string
119
119
+
data map[string]interface{}
120
120
+
}{
121
121
+
{
122
122
+
name: "beans empty",
123
123
+
data: map[string]interface{}{
124
124
+
"Beans": []*models.Bean{},
125
125
+
"Roasters": []*models.Roaster{},
126
126
+
},
127
127
+
},
128
128
+
{
129
129
+
name: "beans with roaster",
130
130
+
data: map[string]interface{}{
131
131
+
"Beans": []*models.Bean{
132
132
+
{
133
133
+
RKey: "bean1",
134
134
+
Name: "Ethiopian Yirgacheffe",
135
135
+
Origin: "Ethiopia",
136
136
+
RoastLevel: "Light",
137
137
+
Process: "Washed",
138
138
+
Description: "Bright and fruity with notes of blueberry",
139
139
+
RoasterRKey: "roaster1",
140
140
+
Roaster: &models.Roaster{
141
141
+
RKey: "roaster1",
142
142
+
Name: "Onyx Coffee Lab",
143
143
+
},
144
144
+
},
145
145
+
{
146
146
+
RKey: "bean2",
147
147
+
Origin: "Colombia",
148
148
+
RoastLevel: "Medium",
149
149
+
},
150
150
+
},
151
151
+
"Roasters": []*models.Roaster{
152
152
+
{RKey: "roaster1", Name: "Onyx Coffee Lab"},
153
153
+
{RKey: "roaster2", Name: "Counter Culture"},
154
154
+
},
155
155
+
},
156
156
+
},
157
157
+
}
158
158
+
159
159
+
tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles(
160
160
+
"../../templates/partials/manage_content.tmpl",
161
161
+
))
162
162
+
163
163
+
for _, tt := range tests {
164
164
+
t.Run(tt.name, func(t *testing.T) {
165
165
+
var buf bytes.Buffer
166
166
+
err := tmpl.ExecuteTemplate(&buf, "manage_content", tt.data)
167
167
+
if err != nil {
168
168
+
t.Fatalf("template execution failed: %v", err)
169
169
+
}
170
170
+
shutter.SnapString(t, tt.name, formatHTML(buf.String()))
171
171
+
})
172
172
+
}
173
173
+
}
174
174
+
175
175
+
func TestManageContent_RoastersTab_Snapshot(t *testing.T) {
176
176
+
tests := []struct {
177
177
+
name string
178
178
+
data map[string]interface{}
179
179
+
}{
180
180
+
{
181
181
+
name: "roasters empty",
182
182
+
data: map[string]interface{}{
183
183
+
"Roasters": []*models.Roaster{},
184
184
+
},
185
185
+
},
186
186
+
{
187
187
+
name: "roasters with data",
188
188
+
data: map[string]interface{}{
189
189
+
"Roasters": []*models.Roaster{
190
190
+
{
191
191
+
RKey: "roaster1",
192
192
+
Name: "Onyx Coffee Lab",
193
193
+
Location: "Bentonville, AR",
194
194
+
Website: "https://onyxcoffeelab.com",
195
195
+
},
196
196
+
{
197
197
+
RKey: "roaster2",
198
198
+
Name: "Counter Culture Coffee",
199
199
+
},
200
200
+
},
201
201
+
},
202
202
+
},
203
203
+
{
204
204
+
name: "roasters with unsafe url",
205
205
+
data: map[string]interface{}{
206
206
+
"Roasters": []*models.Roaster{
207
207
+
{
208
208
+
RKey: "roaster1",
209
209
+
Name: "Test Roaster",
210
210
+
Location: "Test Location",
211
211
+
Website: "javascript:alert('xss')",
212
212
+
},
213
213
+
},
214
214
+
},
215
215
+
},
216
216
+
}
217
217
+
218
218
+
tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles(
219
219
+
"../../templates/partials/manage_content.tmpl",
220
220
+
))
221
221
+
222
222
+
for _, tt := range tests {
223
223
+
t.Run(tt.name, func(t *testing.T) {
224
224
+
var buf bytes.Buffer
225
225
+
err := tmpl.ExecuteTemplate(&buf, "manage_content", tt.data)
226
226
+
if err != nil {
227
227
+
t.Fatalf("template execution failed: %v", err)
228
228
+
}
229
229
+
shutter.SnapString(t, tt.name, formatHTML(buf.String()))
230
230
+
})
231
231
+
}
232
232
+
}
233
233
+
234
234
+
func TestManageContent_GrindersTab_Snapshot(t *testing.T) {
235
235
+
tests := []struct {
236
236
+
name string
237
237
+
data map[string]interface{}
238
238
+
}{
239
239
+
{
240
240
+
name: "grinders empty",
241
241
+
data: map[string]interface{}{
242
242
+
"Grinders": []*models.Grinder{},
243
243
+
},
244
244
+
},
245
245
+
{
246
246
+
name: "grinders with data",
247
247
+
data: map[string]interface{}{
248
248
+
"Grinders": []*models.Grinder{
249
249
+
{
250
250
+
RKey: "grinder1",
251
251
+
Name: "Comandante C40 MK3",
252
252
+
GrinderType: "Hand",
253
253
+
BurrType: "Conical",
254
254
+
Notes: "Excellent consistency, great for pour-over",
255
255
+
},
256
256
+
{
257
257
+
RKey: "grinder2",
258
258
+
Name: "Baratza Encore",
259
259
+
GrinderType: "Electric",
260
260
+
BurrType: "Conical",
261
261
+
},
262
262
+
},
263
263
+
},
264
264
+
},
265
265
+
}
266
266
+
267
267
+
tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles(
268
268
+
"../../templates/partials/manage_content.tmpl",
269
269
+
))
270
270
+
271
271
+
for _, tt := range tests {
272
272
+
t.Run(tt.name, func(t *testing.T) {
273
273
+
var buf bytes.Buffer
274
274
+
err := tmpl.ExecuteTemplate(&buf, "manage_content", tt.data)
275
275
+
if err != nil {
276
276
+
t.Fatalf("template execution failed: %v", err)
277
277
+
}
278
278
+
shutter.SnapString(t, tt.name, formatHTML(buf.String()))
279
279
+
})
280
280
+
}
281
281
+
}
282
282
+
283
283
+
func TestManageContent_BrewersTab_Snapshot(t *testing.T) {
284
284
+
tests := []struct {
285
285
+
name string
286
286
+
data map[string]interface{}
287
287
+
}{
288
288
+
{
289
289
+
name: "brewers empty",
290
290
+
data: map[string]interface{}{
291
291
+
"Brewers": []*models.Brewer{},
292
292
+
},
293
293
+
},
294
294
+
{
295
295
+
name: "brewers with data",
296
296
+
data: map[string]interface{}{
297
297
+
"Brewers": []*models.Brewer{
298
298
+
{
299
299
+
RKey: "brewer1",
300
300
+
Name: "Hario V60",
301
301
+
BrewerType: "Pour-Over",
302
302
+
Description: "Cone-shaped dripper for clean, bright brews",
303
303
+
},
304
304
+
{
305
305
+
RKey: "brewer2",
306
306
+
Name: "AeroPress",
307
307
+
},
308
308
+
},
309
309
+
},
310
310
+
},
311
311
+
}
312
312
+
313
313
+
tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles(
314
314
+
"../../templates/partials/manage_content.tmpl",
315
315
+
))
316
316
+
317
317
+
for _, tt := range tests {
318
318
+
t.Run(tt.name, func(t *testing.T) {
319
319
+
var buf bytes.Buffer
320
320
+
err := tmpl.ExecuteTemplate(&buf, "manage_content", tt.data)
321
321
+
if err != nil {
322
322
+
t.Fatalf("template execution failed: %v", err)
323
323
+
}
324
324
+
shutter.SnapString(t, tt.name, formatHTML(buf.String()))
325
325
+
})
326
326
+
}
327
327
+
}
328
328
+
329
329
+
func TestManageContent_SpecialCharacters_Snapshot(t *testing.T) {
330
330
+
tests := []struct {
331
331
+
name string
332
332
+
data map[string]interface{}
333
333
+
}{
334
334
+
{
335
335
+
name: "beans with special characters and html",
336
336
+
data: map[string]interface{}{
337
337
+
"Beans": []*models.Bean{
338
338
+
{
339
339
+
RKey: "bean1",
340
340
+
Name: "Café <script>alert('xss')</script> Especial",
341
341
+
Origin: "Costa Rica™",
342
342
+
RoastLevel: "Medium",
343
343
+
Process: "Honey & Washed",
344
344
+
Description: "\"Amazing\" coffee with <strong>bold</strong> flavor",
345
345
+
},
346
346
+
},
347
347
+
"Roasters": []*models.Roaster{},
348
348
+
},
349
349
+
},
350
350
+
{
351
351
+
name: "grinders with unicode",
352
352
+
data: map[string]interface{}{
353
353
+
"Grinders": []*models.Grinder{
354
354
+
{
355
355
+
RKey: "grinder1",
356
356
+
Name: "手動コーヒーミル Comandante® C40",
357
357
+
GrinderType: "Hand",
358
358
+
BurrType: "Conical",
359
359
+
Notes: "日本語のノート - Отличная кофемолка 🇯🇵",
360
360
+
},
361
361
+
},
362
362
+
},
363
363
+
},
364
364
+
}
365
365
+
366
366
+
tmpl := template.Must(template.New("test").Funcs(getTemplateFuncs()).ParseFiles(
367
367
+
"../../templates/partials/manage_content.tmpl",
368
368
+
))
369
369
+
370
370
+
for _, tt := range tests {
371
371
+
t.Run(tt.name, func(t *testing.T) {
372
372
+
var buf bytes.Buffer
373
373
+
err := tmpl.ExecuteTemplate(&buf, "manage_content", tt.data)
374
374
+
if err != nil {
375
375
+
t.Fatalf("template execution failed: %v", err)
376
376
+
}
377
377
+
shutter.SnapString(t, tt.name, formatHTML(buf.String()))
378
378
+
})
379
379
+
}
380
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
105
-
shutter.Snap(t, tt.name, buf.String())
105
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
168
-
shutter.Snap(t, "profile with gear collection", buf.String())
168
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
233
-
shutter.Snap(t, tt.name, buf.String())
233
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
279
-
shutter.Snap(t, "profile with special characters", buf.String())
279
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
330
-
shutter.Snap(t, "profile with unicode content", buf.String())
330
330
+
shutter.SnapString(t, "profile with unicode content", formatHTML(buf.String()))
331
331
}
+57
internal/bff/testutil.go
···
1
1
+
package bff
2
2
+
3
3
+
import (
4
4
+
"bytes"
5
5
+
"strings"
6
6
+
7
7
+
"github.com/yosssi/gohtml"
8
8
+
)
9
9
+
10
10
+
// formatHTML formats HTML for snapshot testing with 2-space indentation
11
11
+
func formatHTML(html string) string {
12
12
+
// Configure gohtml for 2-space indentation
13
13
+
formatted := gohtml.Format(html)
14
14
+
15
15
+
// Post-process to ensure consistent formatting:
16
16
+
// 1. Remove excessive blank lines
17
17
+
lines := strings.Split(formatted, "\n")
18
18
+
var result []string
19
19
+
prevBlank := false
20
20
+
21
21
+
for _, line := range lines {
22
22
+
isBlank := strings.TrimSpace(line) == ""
23
23
+
if isBlank && prevBlank {
24
24
+
// Skip consecutive blank lines
25
25
+
continue
26
26
+
}
27
27
+
result = append(result, line)
28
28
+
prevBlank = isBlank
29
29
+
}
30
30
+
31
31
+
// Join and trim
32
32
+
output := strings.Join(result, "\n")
33
33
+
output = strings.TrimSpace(output)
34
34
+
35
35
+
return output
36
36
+
}
37
37
+
38
38
+
// execTemplate is a helper for executing templates and formatting the output
39
39
+
func execTemplate(tmpl interface{}, templateName string, data interface{}) (string, error) {
40
40
+
var buf bytes.Buffer
41
41
+
42
42
+
type executor interface {
43
43
+
ExecuteTemplate(*bytes.Buffer, string, interface{}) error
44
44
+
}
45
45
+
46
46
+
t, ok := tmpl.(executor)
47
47
+
if !ok {
48
48
+
panic("template does not implement ExecuteTemplate")
49
49
+
}
50
50
+
51
51
+
err := t.ExecuteTemplate(&buf, templateName, data)
52
52
+
if err != nil {
53
53
+
return "", err
54
54
+
}
55
55
+
56
56
+
return formatHTML(buf.String()), nil
57
57
+
}