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

Configure Feed

Select the types of activity you want to include in your feed.

feat: improve profile page

pdewey 24039250 686fdf43

+254 -78
+3 -1
internal/bff/render.go
··· 311 311 IsAuthenticated bool 312 312 UserDID string 313 313 UserProfile *UserProfile 314 + IsOwnProfile bool // Whether viewing user is the profile owner 314 315 } 315 316 316 317 // RenderProfile renders a user's public profile page 317 - func RenderProfile(w http.ResponseWriter, profile *atproto.Profile, brews []*models.Brew, beans []*models.Bean, roasters []*models.Roaster, grinders []*models.Grinder, brewers []*models.Brewer, isAuthenticated bool, userDID string, userProfile *UserProfile) error { 318 + func RenderProfile(w http.ResponseWriter, profile *atproto.Profile, brews []*models.Brew, beans []*models.Bean, roasters []*models.Roaster, grinders []*models.Grinder, brewers []*models.Brewer, isAuthenticated bool, userDID string, userProfile *UserProfile, isOwnProfile bool) error { 318 319 t, err := parsePageTemplate("profile.tmpl") 319 320 if err != nil { 320 321 return err ··· 336 337 IsAuthenticated: isAuthenticated, 337 338 UserDID: userDID, 338 339 UserProfile: userProfile, 340 + IsOwnProfile: isOwnProfile, 339 341 } 340 342 return t.ExecuteTemplate(w, "layout", data) 341 343 }
+4 -1
internal/handlers/handlers.go
··· 1414 1414 userProfile = h.getUserProfile(ctx, didStr) 1415 1415 } 1416 1416 1417 + // Check if the viewing user is the profile owner 1418 + isOwnProfile := isAuthenticated && didStr == did 1419 + 1417 1420 // Render profile page 1418 - if err := bff.RenderProfile(w, profile, brews, beans, roasters, grinders, brewers, isAuthenticated, didStr, userProfile); err != nil { 1421 + if err := bff.RenderProfile(w, profile, brews, beans, roasters, grinders, brewers, isAuthenticated, didStr, userProfile, isOwnProfile); err != nil { 1419 1422 http.Error(w, "Failed to render page", http.StatusInternalServerError) 1420 1423 log.Error().Err(err).Msg("Failed to render profile page") 1421 1424 }
+2 -76
templates/layout.tmpl
··· 10 10 <link rel="icon" href="/static/favicon.svg" type="image/svg+xml" /> 11 11 <link rel="icon" href="/static/favicon-32.svg" type="image/svg+xml" sizes="32x32" /> 12 12 <link rel="apple-touch-icon" href="/static/icon-192.svg" /> 13 - <link rel="stylesheet" href="/static/css/output.css?v=0.1.8" /> 13 + <link rel="stylesheet" href="/static/css/output.css?v=0.1.9" /> 14 14 <style>[x-cloak] { display: none !important; }</style> 15 15 <link rel="manifest" href="/static/manifest.json" /> 16 16 <!-- Alpine.js - Using standard build due to CDN MIME type issues with CSP build --> ··· 24 24 <script src="/static/js/sw-register.js"></script> 25 25 </head> 26 26 <body class="bg-brown-50 min-h-full flex flex-col"> 27 - <nav class="bg-gradient-to-br from-brown-800 to-brown-900 text-white shadow-xl border-b-2 border-brown-600"> 28 - <div class="container mx-auto px-4 py-4"> 29 - <div class="flex items-center justify-between"> 30 - <!-- Logo - always visible --> 31 - <a href="/" class="flex items-center gap-2 hover:opacity-80 transition"> 32 - <h1 class="text-2xl font-bold">☕ Arabica</h1> 33 - <span class="text-xs bg-amber-400 text-brown-900 px-2 py-1 rounded-md font-semibold shadow-sm">ALPHA</span> 34 - </a> 35 - 36 - <!-- Navigation links --> 37 - <div class="flex items-center gap-4"> 38 - <!-- Home link - mobile only --> 39 - <a href="/" class="md:hidden hover:text-brown-100 transition-colors font-medium">Home</a> 40 - 41 - {{if .IsAuthenticated}} 42 - <!-- Brews link --> 43 - <a href="/brews" class="hover:text-brown-100 transition-colors font-medium">Brews</a> 44 - 45 - <!-- New Brew button - commented out for now 46 - <a href="/brews/new" class="hover:text-brown-100 transition-colors font-medium">New Brew</a> 47 - --> 48 - 49 - <!-- User profile dropdown --> 50 - <div x-data="{ open: false }" class="relative"> 51 - <button @click="open = !open" @click.outside="open = false" class="flex items-center gap-2 hover:opacity-80 transition focus:outline-none"> 52 - {{if and .UserProfile .UserProfile.Avatar}} 53 - {{$safeAvatar := safeAvatarURL .UserProfile.Avatar}} 54 - {{if $safeAvatar}} 55 - <img src="{{$safeAvatar}}" alt="" class="w-8 h-8 rounded-full object-cover ring-2 ring-brown-600" /> 56 - {{else}} 57 - <div class="w-8 h-8 rounded-full bg-brown-600 flex items-center justify-center ring-2 ring-brown-500"> 58 - <span class="text-sm font-medium">{{if and .UserProfile .UserProfile.DisplayName}}{{slice .UserProfile.DisplayName 0 1}}{{else}}?{{end}}</span> 59 - </div> 60 - {{end}} 61 - {{else}} 62 - <div class="w-8 h-8 rounded-full bg-brown-600 flex items-center justify-center ring-2 ring-brown-500"> 63 - <span class="text-sm font-medium">{{if and .UserProfile .UserProfile.DisplayName}}{{slice .UserProfile.DisplayName 0 1}}{{else}}?{{end}}</span> 64 - </div> 65 - {{end}} 66 - <svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': open }" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 67 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> 68 - </svg> 69 - </button> 70 - 71 - <!-- Dropdown menu --> 72 - <div x-show="open" x-cloak x-transition:enter="transition ease-out duration-100" x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-end="transform opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-end="transform opacity-0 scale-95" class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-brown-200 py-1 z-50"> 73 - {{if and .UserProfile .UserProfile.Handle}} 74 - <div class="px-4 py-2 border-b border-brown-100"> 75 - <p class="text-sm font-medium text-brown-900 truncate">{{if .UserProfile.DisplayName}}{{.UserProfile.DisplayName}}{{else}}{{.UserProfile.Handle}}{{end}}</p> 76 - <p class="text-xs text-brown-500 truncate">@{{.UserProfile.Handle}}</p> 77 - </div> 78 - {{end}} 79 - <a href="/profile/{{if and .UserProfile .UserProfile.Handle}}{{.UserProfile.Handle}}{{else}}{{.UserDID}}{{end}}" class="block px-4 py-2 text-sm text-brown-700 hover:bg-brown-50 transition-colors"> 80 - View Profile 81 - </a> 82 - <a href="/manage" class="block px-4 py-2 text-sm text-brown-700 hover:bg-brown-50 transition-colors"> 83 - Manage Records 84 - </a> 85 - <a href="#" class="block px-4 py-2 text-sm text-brown-400 cursor-not-allowed"> 86 - Settings (coming soon) 87 - </a> 88 - <div class="border-t border-brown-100 mt-1 pt-1"> 89 - <form action="/logout" method="POST"> 90 - <button type="submit" class="w-full text-left px-4 py-2 text-sm text-brown-700 hover:bg-brown-50 transition-colors"> 91 - Logout 92 - </button> 93 - </form> 94 - </div> 95 - </div> 96 - </div> 97 - {{end}} 98 - </div> 99 - </div> 100 - </div> 101 - </nav> 27 + {{template "header" .}} 102 28 <main class="flex-grow container mx-auto px-4 py-8"> 103 29 {{template "content" .}} 104 30 </main>
+73
templates/partials/header.tmpl
··· 1 + {{define "header"}} 2 + <nav class="bg-gradient-to-br from-brown-800 to-brown-900 text-white shadow-xl border-b-2 border-brown-600"> 3 + <div class="container mx-auto px-4 py-4"> 4 + <div class="flex items-center justify-between"> 5 + <!-- Logo - always visible --> 6 + <a href="/" class="flex items-center gap-2 hover:opacity-80 transition"> 7 + <h1 class="text-2xl font-bold">☕ Arabica</h1> 8 + <span class="text-xs bg-amber-400 text-brown-900 px-2 py-1 rounded-md font-semibold shadow-sm">ALPHA</span> 9 + </a> 10 + 11 + <!-- Navigation links --> 12 + <div class="flex items-center gap-4"> 13 + <!-- Home link - mobile only --> 14 + <!-- <a href="/" class="md:hidden hover:text-brown-100 transition-colors font-medium">Home</a> --> 15 + 16 + {{if .IsAuthenticated}} 17 + <!-- User profile dropdown --> 18 + <div x-data="{ open: false }" class="relative"> 19 + <button @click="open = !open" @click.outside="open = false" class="flex items-center gap-2 hover:opacity-80 transition focus:outline-none"> 20 + {{if and .UserProfile .UserProfile.Avatar}} 21 + {{$safeAvatar := safeAvatarURL .UserProfile.Avatar}} 22 + {{if $safeAvatar}} 23 + <img src="{{$safeAvatar}}" alt="" class="w-8 h-8 rounded-full object-cover ring-2 ring-brown-600" /> 24 + {{else}} 25 + <div class="w-8 h-8 rounded-full bg-brown-600 flex items-center justify-center ring-2 ring-brown-500"> 26 + <span class="text-sm font-medium">{{if and .UserProfile .UserProfile.DisplayName}}{{slice .UserProfile.DisplayName 0 1}}{{else}}?{{end}}</span> 27 + </div> 28 + {{end}} 29 + {{else}} 30 + <div class="w-8 h-8 rounded-full bg-brown-600 flex items-center justify-center ring-2 ring-brown-500"> 31 + <span class="text-sm font-medium">{{if and .UserProfile .UserProfile.DisplayName}}{{slice .UserProfile.DisplayName 0 1}}{{else}}?{{end}}</span> 32 + </div> 33 + {{end}} 34 + <svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': open }" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 35 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> 36 + </svg> 37 + </button> 38 + 39 + <!-- Dropdown menu --> 40 + <div x-show="open" x-cloak x-transition:enter="transition ease-out duration-100" x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-end="transform opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-end="transform opacity-0 scale-95" class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-brown-200 py-1 z-50"> 41 + {{if and .UserProfile .UserProfile.Handle}} 42 + <div class="px-4 py-2 border-b border-brown-100"> 43 + <p class="text-sm font-medium text-brown-900 truncate">{{if .UserProfile.DisplayName}}{{.UserProfile.DisplayName}}{{else}}{{.UserProfile.Handle}}{{end}}</p> 44 + <p class="text-xs text-brown-500 truncate">@{{.UserProfile.Handle}}</p> 45 + </div> 46 + {{end}} 47 + <a href="/profile/{{if and .UserProfile .UserProfile.Handle}}{{.UserProfile.Handle}}{{else}}{{.UserDID}}{{end}}" class="block px-4 py-2 text-sm text-brown-700 hover:bg-brown-50 transition-colors"> 48 + View Profile 49 + </a> 50 + <a href="/brews" class="block px-4 py-2 text-sm text-brown-700 hover:bg-brown-50 transition-colors"> 51 + My Brews 52 + </a> 53 + <a href="/manage" class="block px-4 py-2 text-sm text-brown-700 hover:bg-brown-50 transition-colors"> 54 + Manage Records 55 + </a> 56 + <a href="#" class="block px-4 py-2 text-sm text-brown-400 cursor-not-allowed"> 57 + Settings (coming soon) 58 + </a> 59 + <div class="border-t border-brown-100 mt-1 pt-1"> 60 + <form action="/logout" method="POST"> 61 + <button type="submit" class="w-full text-left px-4 py-2 text-sm text-brown-700 hover:bg-brown-50 transition-colors"> 62 + Logout 63 + </button> 64 + </form> 65 + </div> 66 + </div> 67 + </div> 68 + {{end}} 69 + </div> 70 + </div> 71 + </div> 72 + </nav> 73 + {{end}}
+22
templates/partials/new_roaster_form.tmpl
··· 1 + {{define "new_roaster_form"}} 2 + <!-- Roaster Form Modal --> 3 + <div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 4 + <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"> 5 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3> 6 + <div class="space-y-4"> 7 + <input type="text" x-model="roasterForm.name" placeholder="Name *" 8 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 9 + <input type="text" x-model="roasterForm.location" placeholder="Location" 10 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 11 + <input type="url" x-model="roasterForm.website" placeholder="Website" 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 + <div class="flex gap-2"> 14 + <button @click="saveRoaster()" 15 + 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">Save</button> 16 + <button @click="showRoasterForm = false" 17 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">Cancel</button> 18 + </div> 19 + </div> 20 + </div> 21 + </div> 22 + {{end}}
+150
templates/profile.tmpl
··· 1 1 {{define "content"}} 2 + {{if .IsOwnProfile}} 3 + <!-- Load manage page JavaScript before Alpine.js processes the page --> 4 + <script src="/static/js/manage-page.js"></script> 5 + <div class="max-w-4xl mx-auto" x-data="managePage()"> 6 + {{else}} 2 7 <div class="max-w-4xl mx-auto"> 8 + {{end}} 3 9 <!-- Profile Header --> 4 10 <div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-6 mb-6 border border-brown-300"> 5 11 <div class="flex items-center gap-4"> ··· 129 135 </tbody> 130 136 </table> 131 137 </div> 138 + {{if $.IsOwnProfile}} 139 + <div class="mt-3 text-center"> 140 + <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"> 141 + <span>+</span> 142 + <span>Add New Bean</span> 143 + </button> 144 + </div> 145 + {{end}} 132 146 </div> 133 147 {{end}} 134 148 ··· 169 183 </tbody> 170 184 </table> 171 185 </div> 186 + {{if $.IsOwnProfile}} 187 + <div class="mt-3 text-center"> 188 + <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"> 189 + <span>+</span> 190 + <span>Add New Roaster</span> 191 + </button> 192 + </div> 193 + {{end}} 172 194 </div> 173 195 {{end}} 174 196 ··· 213 235 </tbody> 214 236 </table> 215 237 </div> 238 + {{if $.IsOwnProfile}} 239 + <div class="mt-3 text-center"> 240 + <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"> 241 + <span>+</span> 242 + <span>Add New Grinder</span> 243 + </button> 244 + </div> 245 + {{end}} 216 246 </div> 217 247 {{end}} 218 248 ··· 244 274 </tbody> 245 275 </table> 246 276 </div> 277 + {{if $.IsOwnProfile}} 278 + <div class="mt-3 text-center"> 279 + <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"> 280 + <span>+</span> 281 + <span>Add New Brewer</span> 282 + </button> 283 + </div> 284 + {{end}} 247 285 </div> 248 286 {{end}} 249 287 ··· 254 292 {{end}} 255 293 </div> 256 294 </div> 295 + 296 + {{if .IsOwnProfile}} 297 + <!-- Bean Form Modal --> 298 + <div x-cloak x-show="showBeanForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 299 + <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"> 300 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBean ? 'Edit Bean' : 'Add Bean'"></h3> 301 + <div class="space-y-4"> 302 + <input type="text" x-model="beanForm.name" placeholder="Name *" 303 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 304 + <input type="text" x-model="beanForm.origin" placeholder="Origin *" 305 + class="w-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 + <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"> 307 + <option value="">Select Roaster (Optional)</option> 308 + {{range .Roasters}} 309 + <option value="{{.RKey}}">{{.Name}}</option> 310 + {{end}} 311 + </select> 312 + <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"> 313 + <option value="">Select Roast Level (Optional)</option> 314 + <option value="Ultra-Light">Ultra-Light</option> 315 + <option value="Light">Light</option> 316 + <option value="Medium-Light">Medium-Light</option> 317 + <option value="Medium">Medium</option> 318 + <option value="Medium-Dark">Medium-Dark</option> 319 + <option value="Dark">Dark</option> 320 + </select> 321 + <input type="text" x-model="beanForm.process" placeholder="Process (e.g. Washed, Natural, Honey)" 322 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 323 + <textarea x-model="beanForm.description" placeholder="Description" rows="3" 324 + class="w-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> 325 + <div class="flex gap-2"> 326 + <button @click="saveBean()" 327 + 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">Save</button> 328 + <button @click="showBeanForm = false" 329 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">Cancel</button> 330 + </div> 331 + </div> 332 + </div> 333 + </div> 334 + 335 + <!-- Roaster Form Modal --> 336 + <div x-cloak x-show="showRoasterForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 337 + <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"> 338 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingRoaster ? 'Edit Roaster' : 'Add Roaster'"></h3> 339 + <div class="space-y-4"> 340 + <input type="text" x-model="roasterForm.name" placeholder="Name *" 341 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 342 + <input type="text" x-model="roasterForm.location" placeholder="Location" 343 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 344 + <input type="url" x-model="roasterForm.website" placeholder="Website" 345 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 346 + <div class="flex gap-2"> 347 + <button @click="saveRoaster()" 348 + 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">Save</button> 349 + <button @click="showRoasterForm = false" 350 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">Cancel</button> 351 + </div> 352 + </div> 353 + </div> 354 + </div> 355 + 356 + <!-- Grinder Form Modal --> 357 + <div x-cloak x-show="showGrinderForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 358 + <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"> 359 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingGrinder ? 'Edit Grinder' : 'Add Grinder'"></h3> 360 + <div class="space-y-4"> 361 + <input type="text" x-model="grinderForm.name" placeholder="Name *" 362 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 363 + <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"> 364 + <option value="">Select Grinder Type *</option> 365 + <option value="Hand">Hand</option> 366 + <option value="Electric">Electric</option> 367 + <option value="Portable Electric">Portable Electric</option> 368 + </select> 369 + <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"> 370 + <option value="">Select Burr Type (Optional)</option> 371 + <option value="Conical">Conical</option> 372 + <option value="Flat">Flat</option> 373 + </select> 374 + <textarea x-model="grinderForm.notes" placeholder="Notes" rows="3" 375 + class="w-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> 376 + <div class="flex gap-2"> 377 + <button @click="saveGrinder()" 378 + 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">Save</button> 379 + <button @click="showGrinderForm = false" 380 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">Cancel</button> 381 + </div> 382 + </div> 383 + </div> 384 + </div> 385 + 386 + <!-- Brewer Form Modal --> 387 + <div x-cloak x-show="showBrewerForm" class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> 388 + <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"> 389 + <h3 class="text-xl font-semibold mb-4 text-brown-900" x-text="editingBrewer ? 'Edit Brewer' : 'Add Brewer'"></h3> 390 + <div class="space-y-4"> 391 + <input type="text" x-model="brewerForm.name" placeholder="Name *" 392 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 393 + <input type="text" x-model="brewerForm.brewer_type" placeholder="Type (e.g., Pour-Over, Immersion, Espresso)" 394 + class="w-full rounded-lg border-2 border-brown-300 bg-white shadow-sm py-2 px-3 focus:border-brown-600 focus:ring-brown-600" /> 395 + <textarea x-model="brewerForm.description" placeholder="Description" rows="3" 396 + class="w-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> 397 + <div class="flex gap-2"> 398 + <button @click="saveBrewer()" 399 + 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">Save</button> 400 + <button @click="showBrewerForm = false" 401 + class="flex-1 bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">Cancel</button> 402 + </div> 403 + </div> 404 + </div> 405 + </div> 406 + {{end}} 257 407 </div> 258 408 259 409 <style>