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: error banners on modals, onboarding suggestions

authored by

Patrick Dewey and committed by
Tangled
9e43ba72 8739a5c7

+44 -14
+3 -2
internal/web/components/brew_list_table.templ
··· 18 18 if len(props.Brews) == 0 { 19 19 if props.IsOwnProfile { 20 20 @EmptyState(EmptyStateProps{ 21 - Message: "No brews yet! Start tracking your coffee journey.", 21 + Message: "Your brew journal is empty.", 22 + SubMessage: "Log your first cup and start building your coffee story. Just pick a bean, choose your method, and rate the result.", 22 23 ActionURL: "/brews/new", 23 - ActionText: "Add Your First Brew", 24 + ActionText: "Log Your First Brew", 24 25 }) 25 26 } else { 26 27 @EmptyState(EmptyStateProps{
+15 -6
internal/web/components/dialog_modals.templ
··· 31 31 } 32 32 hx-trigger="submit" 33 33 hx-swap="none" 34 - hx-on::after-request="if(event.detail.successful) { this.closest('dialog').close(); htmx.trigger('body', 'refreshManage'); } else if(event.detail.xhr && event.detail.xhr.status === 401) { this.closest('dialog').close(); window.__showSessionExpiredModal(); }" 34 + x-data="{ serverError: '' }" 35 + hx-on::after-request="if(event.detail.successful) { this.closest('dialog').close(); htmx.trigger('body', 'refreshManage'); } else if(event.detail.xhr && event.detail.xhr.status === 401) { this.closest('dialog').close(); window.__showSessionExpiredModal(); } else { this._x_dataStack[0].serverError = event.detail.xhr ? 'Something went wrong. Please try again.' : 'Connection error. Check your network.'; }" 35 36 class="space-y-5" 36 37 > 38 + <div x-show="serverError" x-cloak class="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-800" x-text="serverError"></div> 37 39 <!-- Essentials --> 38 40 <div class="form-fieldset"> 39 41 <div class="form-fieldset-label">Essentials</div> ··· 297 299 } 298 300 hx-trigger="submit" 299 301 hx-swap="none" 300 - hx-on::after-request="if(event.detail.successful) { this.closest('dialog').close(); htmx.trigger('body', 'refreshManage'); } else if(event.detail.xhr && event.detail.xhr.status === 401) { this.closest('dialog').close(); window.__showSessionExpiredModal(); }" 302 + x-data="{ serverError: '' }" 303 + hx-on::after-request="if(event.detail.successful) { this.closest('dialog').close(); htmx.trigger('body', 'refreshManage'); } else if(event.detail.xhr && event.detail.xhr.status === 401) { this.closest('dialog').close(); window.__showSessionExpiredModal(); } else { this._x_dataStack[0].serverError = event.detail.xhr ? 'Something went wrong. Please try again.' : 'Connection error. Check your network.'; }" 301 304 class="space-y-5" 302 305 > 306 + <div x-show="serverError" x-cloak class="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-800" x-text="serverError"></div> 303 307 <!-- Essentials --> 304 308 <div class="form-fieldset"> 305 309 <div class="form-fieldset-label">Essentials</div> ··· 429 433 } 430 434 hx-trigger="submit" 431 435 hx-swap="none" 432 - hx-on::after-request="if(event.detail.successful) { this.closest('dialog').close(); htmx.trigger('body', 'refreshManage'); } else if(event.detail.xhr && event.detail.xhr.status === 401) { this.closest('dialog').close(); window.__showSessionExpiredModal(); }" 436 + x-data="{ serverError: '' }" 437 + hx-on::after-request="if(event.detail.successful) { this.closest('dialog').close(); htmx.trigger('body', 'refreshManage'); } else if(event.detail.xhr && event.detail.xhr.status === 401) { this.closest('dialog').close(); window.__showSessionExpiredModal(); } else { this._x_dataStack[0].serverError = event.detail.xhr ? 'Something went wrong. Please try again.' : 'Connection error. Check your network.'; }" 433 438 class="space-y-5" 434 439 > 440 + <div x-show="serverError" x-cloak class="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-800" x-text="serverError"></div> 435 441 <!-- Essentials --> 436 442 <div class="form-fieldset"> 437 443 <div class="form-fieldset-label">Essentials</div> ··· 555 561 } 556 562 hx-trigger="submit" 557 563 hx-swap="none" 558 - hx-on::after-request="if(event.detail.successful) { this.closest('dialog').close(); htmx.trigger('body', 'refreshManage'); } else if(event.detail.xhr && event.detail.xhr.status === 401) { this.closest('dialog').close(); window.__showSessionExpiredModal(); }" 564 + x-data="{ serverError: '' }" 565 + hx-on::after-request="if(event.detail.successful) { this.closest('dialog').close(); htmx.trigger('body', 'refreshManage'); } else if(event.detail.xhr && event.detail.xhr.status === 401) { this.closest('dialog').close(); window.__showSessionExpiredModal(); } else { this._x_dataStack[0].serverError = event.detail.xhr ? 'Something went wrong. Please try again.' : 'Connection error. Check your network.'; }" 559 566 class="space-y-5" 560 567 > 568 + <div x-show="serverError" x-cloak class="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-800" x-text="serverError"></div> 561 569 <!-- Essentials --> 562 570 <div class="form-fieldset"> 563 571 <div class="form-fieldset-label">Essentials</div> ··· 674 682 } 675 683 hx-trigger="submit" 676 684 hx-swap="none" 677 - hx-on::after-request="if(event.detail.successful) { this.closest('dialog').close(); htmx.trigger('body', 'refreshManage'); }" 685 + hx-on::after-request="if(event.detail.successful) { this.closest('dialog').close(); htmx.trigger('body', 'refreshManage'); } else { this._x_dataStack[0].serverError = event.detail.xhr ? 'Something went wrong. Please try again.' : 'Connection error. Check your network.'; }" 678 686 class="space-y-5" 679 - x-data={ fmt.Sprintf("{ pours: %s, addPour() { this.pours.push({water: '', time: ''}); }, removePour(i) { this.pours.splice(i, 1); } }", recipePourJSON(recipe)) } 687 + x-data={ fmt.Sprintf("{ serverError: '', pours: %s, addPour() { this.pours.push({water: '', time: ''}); }, removePour(i) { this.pours.splice(i, 1); } }", recipePourJSON(recipe)) } 680 688 > 689 + <div x-show="serverError" x-cloak class="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-800" x-text="serverError"></div> 681 690 <!-- Essentials --> 682 691 <div class="form-fieldset"> 683 692 <div class="form-fieldset-label">Essentials</div>
+5 -5
internal/web/components/entity_tables.templ
··· 75 75 // BeanCards renders a grid of bean cards 76 76 templ BeanCards(props BeanCardsProps) { 77 77 if len(props.Beans) == 0 { 78 - @EmptyState(EmptyStateProps{Message: "No beans yet."}) 78 + @EmptyState(EmptyStateProps{Message: "No beans yet.", SubMessage: "Add the beans you're brewing with. You can also add beans inline when logging a brew."}) 79 79 } else { 80 80 <div class="grid grid-cols-1 lg:grid-cols-2 gap-4"> 81 81 for _, bean := range props.Beans { ··· 185 185 // RoastersTable renders a grid of roaster cards 186 186 templ RoastersTable(props RoastersTableProps) { 187 187 if len(props.Roasters) == 0 { 188 - @EmptyState(EmptyStateProps{Message: "No roasters yet."}) 188 + @EmptyState(EmptyStateProps{Message: "No roasters yet.", SubMessage: "Add your favorite coffee roasters. They'll appear as options when adding beans."}) 189 189 } else { 190 190 <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> 191 191 for _, roaster := range props.Roasters { ··· 261 261 // GrindersTable renders a grid of grinder cards 262 262 templ GrindersTable(props GrindersTableProps) { 263 263 if len(props.Grinders) == 0 { 264 - @EmptyState(EmptyStateProps{Message: "No grinders yet."}) 264 + @EmptyState(EmptyStateProps{Message: "No grinders yet.", SubMessage: "Track your grinders to log grind settings with each brew."}) 265 265 } else { 266 266 <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> 267 267 for _, grinder := range props.Grinders { ··· 328 328 // BrewersTable renders a grid of brewer cards 329 329 templ BrewersTable(props BrewersTableProps) { 330 330 if len(props.Brewers) == 0 { 331 - @EmptyState(EmptyStateProps{Message: "No brewing devices yet."}) 331 + @EmptyState(EmptyStateProps{Message: "No brewing devices yet.", SubMessage: "Add your V60, AeroPress, espresso machine, or other brewers."}) 332 332 } else { 333 333 <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> 334 334 for _, brewer := range props.Brewers { ··· 386 386 // RecipesTable renders a grid of recipe cards 387 387 templ RecipesTable(props RecipesTableProps) { 388 388 if len(props.Recipes) == 0 { 389 - @EmptyState(EmptyStateProps{Message: "No recipes yet."}) 389 + @EmptyState(EmptyStateProps{Message: "No recipes yet.", SubMessage: "Save your favorite brew recipes to reuse them. You can also save a recipe directly from a brew."}) 390 390 } else { 391 391 <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> 392 392 for _, recipe := range props.Recipes {
+1 -1
internal/web/pages/about.templ
··· 9 9 <h1 class="text-4xl font-bold text-brown-900">About Arabica</h1> 10 10 <span class="text-sm bg-amber-400 text-brown-900 px-3 py-1 rounded-md font-semibold shadow-sm">ALPHA</span> 11 11 </div> 12 - <div class="bg-amber-50 border-l-4 border-amber-400 p-4 mb-6 rounded-r-lg"> 12 + <div class="bg-amber-50 border border-amber-200 p-4 mb-6 rounded-lg"> 13 13 <p class="text-sm text-brown-800"> 14 14 <strong>Alpha Software:</strong> Arabica is currently in early development. Features may change, and data structures could be modified in future updates. 15 15 </p>
+20
static/css/app.css
··· 490 490 color: var(--text-faint); 491 491 } 492 492 493 + /* Form validation errors */ 494 + .form-error { 495 + @apply text-xs mt-1 block; 496 + color: #b91c1c; 497 + } 498 + 499 + :root[data-theme="dark"] .form-error, 500 + .dark .form-error { 501 + color: #fca5a5; 502 + } 503 + 504 + .form-input-error { 505 + border-color: #b91c1c; 506 + } 507 + 508 + .form-input-error:focus { 509 + border-color: #b91c1c; 510 + box-shadow: 0 0 0 2px rgba(185, 28, 28, 0.15); 511 + } 512 + 493 513 /* Buttons */ 494 514 .btn { 495 515 @apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-medium transition-colors cursor-pointer;