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

feat: switch to card-based representation of beans in manage and profile #16

merged opened by pdewey.com targeting main from push-wnpyruszormm
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:hm5f3dnm6jdhrc55qp2npdja/sh.tangled.repo.pull/3mfwm6zglp722
+87 -109
Diff #0
+65 -99
internal/web/components/entity_tables.templ
··· 5 5 "fmt" 6 6 ) 7 7 8 - // BeansTableProps defines props for the shared beans table 9 - type BeansTableProps struct { 8 + // BeanCardsProps defines props for the bean cards grid 9 + type BeanCardsProps struct { 10 10 Beans []*models.Bean 11 11 ShowActions bool // Whether to show Edit/Delete actions 12 - ShowStatus bool // Whether to show Status column (open/closed badge) 13 12 OwnerHandle string // If set, name links to view page with this owner 14 13 } 15 14 16 - // BeansTable renders a table of beans with configurable columns and actions 17 - templ BeansTable(props BeansTableProps) { 15 + // BeanCards renders a grid of bean cards 16 + templ BeanCards(props BeanCardsProps) { 18 17 if len(props.Beans) == 0 { 19 18 @EmptyState(EmptyStateProps{Message: "No beans yet."}) 20 19 } else { 21 - <div class="table-container overflow-x-auto"> 22 - <table class="table"> 23 - <thead class="table-header"> 24 - <tr> 25 - if props.ShowStatus { 26 - <th class="table-th whitespace-nowrap">๐Ÿ“Š Status</th> 27 - } 28 - <th class="table-th whitespace-nowrap">๐Ÿท๏ธ Name</th> 29 - <th class="table-th whitespace-nowrap">๐Ÿ“ Origin</th> 30 - <th class="table-th whitespace-nowrap">๐ŸŒฟ Variety</th> 31 - <th class="table-th whitespace-nowrap">โ˜• Roaster</th> 32 - <th class="table-th whitespace-nowrap">๐Ÿ”ฅ Roast Level</th> 33 - <th class="table-th whitespace-nowrap">๐ŸŒฑ Process</th> 34 - if props.ShowActions { 35 - <th class="table-th whitespace-nowrap">๐Ÿ“ Description</th> 36 - <th class="table-th whitespace-nowrap">โš™๏ธ Actions</th> 20 + <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> 21 + for _, bean := range props.Beans { 22 + @BeanCard(bean, props.ShowActions, props.OwnerHandle) 23 + } 24 + </div> 25 + } 26 + } 27 + 28 + // BeanCard renders a single bean as a compact card 29 + templ BeanCard(bean *models.Bean, showActions bool, ownerHandle string) { 30 + <div class="feed-card"> 31 + <div class="feed-content-box-sm"> 32 + <div class="flex items-start justify-between gap-2 mb-2"> 33 + <div class="min-w-0"> 34 + <div class="font-bold text-brown-900 text-base truncate"> 35 + if ownerHandle != "" { 36 + <a href={ templ.SafeURL(fmt.Sprintf("/beans/%s?owner=%s", bean.RKey, ownerHandle)) } class="hover:underline">{ bean.Name }</a> 37 + } else { 38 + { bean.Name } 37 39 } 38 - </tr> 39 - </thead> 40 - <tbody class="table-body"> 41 - for _, bean := range props.Beans { 42 - <tr class="table-row"> 43 - if props.ShowStatus { 44 - <td class="px-6 py-4 text-sm text-brown-900"> 45 - if bean.Closed { 46 - <span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-brown-200 text-brown-800">Closed</span> 47 - } else { 48 - <span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-green-100 text-green-800">Open</span> 49 - } 50 - </td> 51 - } 52 - <td class="px-6 py-4 text-sm font-medium text-brown-900"> 53 - if props.OwnerHandle != "" { 54 - <a href={ templ.SafeURL(fmt.Sprintf("/beans/%s?owner=%s", bean.RKey, props.OwnerHandle)) } class="hover:underline">{ bean.Name }</a> 55 - } else { 56 - { bean.Name } 57 - } 58 - </td> 59 - <td class="px-6 py-4 text-sm text-brown-900">{ bean.Origin }</td> 60 - <td class="px-6 py-4 text-sm text-brown-900"> 61 - if bean.Variety != "" { 62 - { bean.Variety } 63 - } else { 64 - <span class="text-brown-400">-</span> 65 - } 66 - </td> 67 - <td class="px-6 py-4 text-sm text-brown-900"> 68 - if bean.Roaster != nil && bean.Roaster.Name != "" { 69 - { bean.Roaster.Name } 70 - } else { 71 - <span class="text-brown-400">-</span> 72 - } 73 - </td> 74 - <td class="px-6 py-4 text-sm text-brown-900"> 75 - if bean.RoastLevel != "" { 76 - { bean.RoastLevel } 77 - } else { 78 - <span class="text-brown-400">-</span> 79 - } 80 - </td> 81 - <td class="px-6 py-4 text-sm text-brown-900"> 82 - if bean.Process != "" { 83 - { bean.Process } 84 - } else { 85 - <span class="text-brown-400">-</span> 86 - } 87 - </td> 88 - if props.ShowActions { 89 - <td class="px-6 py-4 text-sm text-brown-700"> 90 - if bean.Description != "" { 91 - { bean.Description } 92 - } else { 93 - <span class="text-brown-400">-</span> 94 - } 95 - </td> 96 - <td class="px-6 py-4 text-sm font-medium space-x-2"> 97 - <button 98 - hx-get={ "/api/modals/bean/" + bean.RKey } 99 - hx-target="#modal-container" 100 - hx-swap="innerHTML" 101 - class="text-brown-700 hover:text-brown-900 font-medium" 102 - >Edit</button> 103 - <button 104 - hx-delete={ "/api/beans/" + bean.RKey } 105 - hx-confirm="Are you sure you want to delete this bean?" 106 - hx-target="closest tr" 107 - hx-swap="outerHTML swap:0.5s" 108 - class="text-brown-600 hover:text-brown-800 font-medium" 109 - >Delete</button> 110 - </td> 111 - } 112 - </tr> 40 + </div> 41 + if bean.Roaster != nil && bean.Roaster.Name != "" { 42 + <div class="text-sm text-brown-700 mt-0.5"> 43 + <span class="font-medium">๐Ÿญ { bean.Roaster.Name }</span> 44 + </div> 113 45 } 114 - </tbody> 115 - </table> 46 + </div> 47 + if showActions { 48 + <div class="flex items-center gap-1 flex-shrink-0"> 49 + <button 50 + hx-get={ "/api/modals/bean/" + bean.RKey } 51 + hx-target="#modal-container" 52 + hx-swap="innerHTML" 53 + class="text-brown-600 hover:text-brown-900 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 54 + >Edit</button> 55 + <button 56 + hx-delete={ "/api/beans/" + bean.RKey } 57 + hx-confirm="Are you sure you want to delete this bean?" 58 + hx-target="closest .feed-card" 59 + hx-swap="outerHTML swap:0.3s" 60 + class="text-brown-500 hover:text-brown-800 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 61 + >Delete</button> 62 + </div> 63 + } 64 + </div> 65 + <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 66 + if bean.Origin != "" { 67 + <span class="inline-flex items-center gap-0.5">๐Ÿ“ { bean.Origin }</span> 68 + } 69 + if bean.Variety != "" { 70 + <span class="inline-flex items-center gap-0.5">๐ŸŒฟ { bean.Variety }</span> 71 + } 72 + if bean.RoastLevel != "" { 73 + <span class="inline-flex items-center gap-0.5">๐Ÿ”ฅ { bean.RoastLevel }</span> 74 + } 75 + if bean.Process != "" { 76 + <span class="inline-flex items-center gap-0.5">๐ŸŒฑ { bean.Process }</span> 77 + } 78 + </div> 79 + if bean.Description != "" { 80 + <div class="mt-2 text-sm text-brown-800 italic line-clamp-2">"{ bean.Description }"</div> 81 + } 116 82 </div> 117 - } 83 + </div> 118 84 } 119 85 120 86 // RoastersTableProps defines props for the shared roasters table
+17 -6
internal/web/components/manage_partial.templ
··· 18 18 @ManageBrewersTab(props.Brewers) 19 19 } 20 20 21 - // ManageBeansTab renders the beans tab content 21 + // ManageBeansTab renders the beans tab content with open/closed sections 22 22 templ ManageBeansTab(beans []*models.Bean) { 23 23 <div x-show="tab === 'beans'"> 24 24 <div class="mb-4 flex justify-between items-center"> ··· 32 32 + Add Bean 33 33 </button> 34 34 </div> 35 - @BeansTable(BeansTableProps{ 36 - Beans: beans, 37 - ShowActions: true, 38 - ShowStatus: true, 39 - }) 35 + <div class="space-y-6"> 36 + <div> 37 + <h4 class="text-lg font-semibold text-brown-900 mb-3">Open Bags</h4> 38 + @BeanCards(BeanCardsProps{ 39 + Beans: filterOpenBeans(beans), 40 + ShowActions: true, 41 + }) 42 + </div> 43 + <div> 44 + <h4 class="text-lg font-semibold text-brown-900 mb-3">Closed Bags</h4> 45 + @BeanCards(BeanCardsProps{ 46 + Beans: filterClosedBeans(beans), 47 + ShowActions: true, 48 + }) 49 + </div> 50 + </div> 40 51 </div> 41 52 } 42 53
+2 -4
internal/web/components/profile_partial.templ
··· 72 72 }) 73 73 } 74 74 } else { 75 - @BeansTable(BeansTableProps{ 75 + @BeanCards(BeanCardsProps{ 76 76 Beans: filterOpenBeans(beans), 77 77 ShowActions: false, 78 - ShowStatus: false, 79 78 OwnerHandle: profileHandle, 80 79 }) 81 80 } ··· 105 104 Message: "No closed bags.", 106 105 }) 107 106 } else { 108 - @BeansTable(BeansTableProps{ 107 + @BeanCards(BeanCardsProps{ 109 108 Beans: filterClosedBeans(beans), 110 109 ShowActions: false, 111 - ShowStatus: false, 112 110 OwnerHandle: profileHandle, 113 111 }) 114 112 }
+3
internal/web/pages/feed.templ
··· 436 436 if item.Bean.Origin != "" { 437 437 <span class="inline-flex items-center gap-0.5">๐Ÿ“ { item.Bean.Origin }</span> 438 438 } 439 + if item.Bean.Variety != "" { 440 + <span class="inline-flex items-center gap-0.5">๐ŸŒฟ { item.Bean.Variety }</span> 441 + } 439 442 if item.Bean.RoastLevel != "" { 440 443 <span class="inline-flex items-center gap-0.5">๐Ÿ”ฅ { item.Bean.RoastLevel }</span> 441 444 }

History

1 round 0 comments
sign up or login to add to the discussion
pdewey.com submitted #0
1 commit
expand
feat: switch to card-based representation of beans in manage and profile
expand 0 comments
pull request successfully merged