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: show bean rating on my brews page

authored by

Patrick Dewey and committed by tangled.org 742471f2 045769ec

+209 -112
+41 -5
internal/handlers/entities.go
··· 90 90 } 91 91 } 92 92 93 - // Render manage partial 94 - if err := components.ManagePartial(components.ManagePartialProps{ 93 + // Fetch entity usage counts and avg ratings from witness cache 94 + props := components.ManagePartialProps{ 95 95 Beans: beans, 96 96 Roasters: roasters, 97 97 Grinders: grinders, 98 98 Brewers: brewers, 99 99 Recipes: recipes, 100 - }).Render(r.Context(), w); err != nil { 100 + } 101 + if h.feedIndex != nil { 102 + did, _ := atproto.GetAuthenticatedDID(r.Context()) 103 + props.OwnerDID = did 104 + props.BeanBrewCounts = h.feedIndex.BrewCountsByBeanURI(r.Context(), did) 105 + props.GrinderBrewCounts = h.feedIndex.BrewCountsByGrinderURI(r.Context(), did) 106 + props.BrewerBrewCounts = h.feedIndex.BrewCountsByBrewerURI(r.Context(), did) 107 + props.RoasterBeanCounts = h.feedIndex.BeanCountsByRoasterURI(r.Context(), did) 108 + props.BeanAvgBrewRatings = make(map[string]float64) 109 + for uri, stats := range h.feedIndex.AvgBrewRatingByBeanURI(r.Context(), did) { 110 + props.BeanAvgBrewRatings[uri] = stats.Average 111 + } 112 + props.RoasterAvgBrewRatings = make(map[string]float64) 113 + for uri, stats := range h.feedIndex.AvgBrewRatingByRoasterURI(r.Context(), did) { 114 + props.RoasterAvgBrewRatings[uri] = stats.Average 115 + } 116 + } 117 + 118 + // Render manage partial 119 + if err := components.ManagePartial(props).Render(r.Context(), w); err != nil { 101 120 http.Error(w, "Failed to render content", http.StatusInternalServerError) 102 121 log.Error().Err(err).Msg("Failed to render manage partial") 103 122 } ··· 498 517 } 499 518 } 500 519 501 - if err := components.ManagePartial(components.ManagePartialProps{ 520 + refreshProps := components.ManagePartialProps{ 502 521 Beans: beans, 503 522 Roasters: roasters, 504 523 Grinders: grinders, 505 524 Brewers: brewers, 506 525 Recipes: recipes, 507 - }).Render(r.Context(), w); err != nil { 526 + } 527 + if h.feedIndex != nil { 528 + refreshProps.OwnerDID = didStr 529 + refreshProps.BeanBrewCounts = h.feedIndex.BrewCountsByBeanURI(r.Context(), didStr) 530 + refreshProps.GrinderBrewCounts = h.feedIndex.BrewCountsByGrinderURI(r.Context(), didStr) 531 + refreshProps.BrewerBrewCounts = h.feedIndex.BrewCountsByBrewerURI(r.Context(), didStr) 532 + refreshProps.RoasterBeanCounts = h.feedIndex.BeanCountsByRoasterURI(r.Context(), didStr) 533 + refreshProps.BeanAvgBrewRatings = make(map[string]float64) 534 + for uri, stats := range h.feedIndex.AvgBrewRatingByBeanURI(r.Context(), didStr) { 535 + refreshProps.BeanAvgBrewRatings[uri] = stats.Average 536 + } 537 + refreshProps.RoasterAvgBrewRatings = make(map[string]float64) 538 + for uri, stats := range h.feedIndex.AvgBrewRatingByRoasterURI(r.Context(), didStr) { 539 + refreshProps.RoasterAvgBrewRatings[uri] = stats.Average 540 + } 541 + } 542 + 543 + if err := components.ManagePartial(refreshProps).Render(r.Context(), w); err != nil { 508 544 http.Error(w, "Failed to render content", http.StatusInternalServerError) 509 545 log.Error().Err(err).Msg("Failed to render manage partial after refresh") 510 546 }
+133 -93
internal/web/components/entity_tables.templ
··· 38 38 if len(props.Beans) == 0 { 39 39 @EmptyState(EmptyStateProps{Message: "No beans yet."}) 40 40 } else { 41 - <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> 41 + <div class="grid grid-cols-1 lg:grid-cols-2 gap-4"> 42 42 for _, bean := range props.Beans { 43 43 @BeanCard(bean, props.ShowActions, props.OwnerHandle, entityCount(props.BrewCounts, props.OwnerDID, atproto.NSIDBean, bean.RKey), entityAvgRating(props.AvgBrewRatings, props.OwnerDID, atproto.NSIDBean, bean.RKey)) 44 44 } ··· 49 49 // BeanCard renders a single bean as a compact card 50 50 templ BeanCard(bean *models.Bean, showActions bool, ownerHandle string, brewCount int, avgBrewRating float64) { 51 51 <div class="feed-card feed-card-bean"> 52 + <!-- Header: date + actions --> 53 + <div class="flex items-center justify-between mb-2"> 54 + <div class="text-sm text-brown-600"> 55 + if !bean.CreatedAt.IsZero() { 56 + <time datetime={ bff.FormatISO(bean.CreatedAt) } data-local="date">{ bean.CreatedAt.Format("Jan 2, 2006") }</time> 57 + } 58 + </div> 59 + if showActions { 60 + <div class="flex items-center gap-1"> 61 + <button 62 + hx-get={ "/api/modals/bean/" + bean.RKey } 63 + hx-target="#modal-container" 64 + hx-swap="innerHTML" 65 + class="text-brown-600 hover:text-brown-900 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 66 + >Edit</button> 67 + <button 68 + hx-delete={ "/api/beans/" + bean.RKey } 69 + hx-confirm="Are you sure you want to delete this bean?" 70 + hx-target="closest .feed-card" 71 + hx-swap="outerHTML swap:0.3s" 72 + class="text-brown-500 hover:text-brown-800 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 73 + >Delete</button> 74 + </div> 75 + } 76 + </div> 52 77 <div class="feed-content-box-sm"> 53 - <div class="flex items-start justify-between gap-2 mb-2"> 78 + <div class="flex items-start justify-between gap-3 mb-2"> 54 79 <div class="min-w-0"> 55 - <div class="font-bold text-brown-900 text-base truncate"> 80 + <div class="font-bold text-brown-900 text-base break-words"> 56 81 if ownerHandle != "" { 57 82 <a href={ templ.SafeURL(fmt.Sprintf("/beans/%s?owner=%s", bean.RKey, ownerHandle)) } class="hover:underline">{ bean.Name }</a> 58 83 } else { ··· 68 93 </div> 69 94 } 70 95 </div> 71 - if showActions { 72 - <div class="flex items-center gap-1 flex-shrink-0"> 73 - <button 74 - hx-get={ "/api/modals/bean/" + bean.RKey } 75 - hx-target="#modal-container" 76 - hx-swap="innerHTML" 77 - class="text-brown-600 hover:text-brown-900 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 78 - >Edit</button> 79 - <button 80 - hx-delete={ "/api/beans/" + bean.RKey } 81 - hx-confirm="Are you sure you want to delete this bean?" 82 - hx-target="closest .feed-card" 83 - hx-swap="outerHTML swap:0.3s" 84 - class="text-brown-500 hover:text-brown-800 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 85 - >Delete</button> 86 - </div> 96 + if bean.Rating != nil { 97 + <span class="badge-rating flex-shrink-0"> 98 + @IconStar() 99 + { bff.FormatBeanRating(bean.Rating) } 100 + </span> 87 101 } 88 102 </div> 89 103 <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-1"> ··· 111 125 { bean.Process } 112 126 </span> 113 127 } 114 - if bean.Rating != nil { 115 - <span class="inline-flex items-center gap-1"> 116 - @IconStar() 117 - { bff.FormatBeanRating(bean.Rating) } 118 - </span> 119 - } 120 128 </div> 121 129 if bean.Description != "" { 122 130 <div class="mt-2 text-sm text-brown-800 italic line-clamp-2">"{ bean.Description }"</div> ··· 175 183 // RoasterCard renders a single roaster as a compact card 176 184 templ RoasterCard(roaster *models.Roaster, showActions bool, ownerHandle string, beanCount int, avgBrewRating float64) { 177 185 <div class="feed-card feed-card-roaster"> 186 + <!-- Header: date + actions --> 187 + <div class="flex items-center justify-between mb-2"> 188 + <div class="text-sm text-brown-600"> 189 + if !roaster.CreatedAt.IsZero() { 190 + <time datetime={ bff.FormatISO(roaster.CreatedAt) } data-local="date">{ roaster.CreatedAt.Format("Jan 2, 2006") }</time> 191 + } 192 + </div> 193 + if showActions { 194 + <div class="flex items-center gap-1"> 195 + <button 196 + hx-get={ "/api/modals/roaster/" + roaster.RKey } 197 + hx-target="#modal-container" 198 + hx-swap="innerHTML" 199 + class="text-brown-600 hover:text-brown-900 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 200 + >Edit</button> 201 + <button 202 + hx-delete={ "/api/roasters/" + roaster.RKey } 203 + hx-confirm="Are you sure you want to delete this roaster?" 204 + hx-target="closest .feed-card" 205 + hx-swap="outerHTML swap:0.3s" 206 + class="text-brown-500 hover:text-brown-800 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 207 + >Delete</button> 208 + </div> 209 + } 210 + </div> 178 211 <div class="feed-content-box-sm"> 179 212 <div class="flex items-start justify-between gap-2 mb-2"> 180 213 <div class="min-w-0"> ··· 186 219 } 187 220 </div> 188 221 </div> 189 - if showActions { 190 - <div class="flex items-center gap-1 flex-shrink-0"> 191 - <button 192 - hx-get={ "/api/modals/roaster/" + roaster.RKey } 193 - hx-target="#modal-container" 194 - hx-swap="innerHTML" 195 - class="text-brown-600 hover:text-brown-900 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 196 - >Edit</button> 197 - <button 198 - hx-delete={ "/api/roasters/" + roaster.RKey } 199 - hx-confirm="Are you sure you want to delete this roaster?" 200 - hx-target="closest .feed-card" 201 - hx-swap="outerHTML swap:0.3s" 202 - class="text-brown-500 hover:text-brown-800 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 203 - >Delete</button> 204 - </div> 205 - } 206 222 </div> 207 223 <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-1"> 208 224 if roaster.Location != "" { ··· 267 283 // GrinderCard renders a single grinder as a compact card 268 284 templ GrinderCard(grinder *models.Grinder, showActions bool, ownerHandle string, brewCount int) { 269 285 <div class="feed-card feed-card-grinder"> 286 + <!-- Header: date + actions --> 287 + <div class="flex items-center justify-between mb-2"> 288 + <div class="text-sm text-brown-600"> 289 + if !grinder.CreatedAt.IsZero() { 290 + <time datetime={ bff.FormatISO(grinder.CreatedAt) } data-local="date">{ grinder.CreatedAt.Format("Jan 2, 2006") }</time> 291 + } 292 + </div> 293 + if showActions { 294 + <div class="flex items-center gap-1"> 295 + <button 296 + hx-get={ "/api/modals/grinder/" + grinder.RKey } 297 + hx-target="#modal-container" 298 + hx-swap="innerHTML" 299 + class="text-brown-600 hover:text-brown-900 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 300 + >Edit</button> 301 + <button 302 + hx-delete={ "/api/grinders/" + grinder.RKey } 303 + hx-confirm="Are you sure you want to delete this grinder?" 304 + hx-target="closest .feed-card" 305 + hx-swap="outerHTML swap:0.3s" 306 + class="text-brown-500 hover:text-brown-800 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 307 + >Delete</button> 308 + </div> 309 + } 310 + </div> 270 311 <div class="feed-content-box-sm"> 271 312 <div class="flex items-start justify-between gap-2 mb-2"> 272 313 <div class="min-w-0"> ··· 278 319 } 279 320 </div> 280 321 </div> 281 - if showActions { 282 - <div class="flex items-center gap-1 flex-shrink-0"> 283 - <button 284 - hx-get={ "/api/modals/grinder/" + grinder.RKey } 285 - hx-target="#modal-container" 286 - hx-swap="innerHTML" 287 - class="text-brown-600 hover:text-brown-900 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 288 - >Edit</button> 289 - <button 290 - hx-delete={ "/api/grinders/" + grinder.RKey } 291 - hx-confirm="Are you sure you want to delete this grinder?" 292 - hx-target="closest .feed-card" 293 - hx-swap="outerHTML swap:0.3s" 294 - class="text-brown-500 hover:text-brown-800 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 295 - >Delete</button> 296 - </div> 297 - } 298 322 </div> 299 323 <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-1"> 300 324 if grinder.GrinderType != "" { ··· 350 374 // BrewerCard renders a single brewer as a compact card 351 375 templ BrewerCard(brewer *models.Brewer, showActions bool, ownerHandle string, brewCount int) { 352 376 <div class="feed-card feed-card-brewer"> 377 + <!-- Header: date + actions --> 378 + <div class="flex items-center justify-between mb-2"> 379 + <div class="text-sm text-brown-600"> 380 + if !brewer.CreatedAt.IsZero() { 381 + <time datetime={ bff.FormatISO(brewer.CreatedAt) } data-local="date">{ brewer.CreatedAt.Format("Jan 2, 2006") }</time> 382 + } 383 + </div> 384 + if showActions { 385 + <div class="flex items-center gap-1"> 386 + <button 387 + hx-get={ "/api/modals/brewer/" + brewer.RKey } 388 + hx-target="#modal-container" 389 + hx-swap="innerHTML" 390 + class="text-brown-600 hover:text-brown-900 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 391 + >Edit</button> 392 + <button 393 + hx-delete={ "/api/brewers/" + brewer.RKey } 394 + hx-confirm="Are you sure you want to delete this brewer?" 395 + hx-target="closest .feed-card" 396 + hx-swap="outerHTML swap:0.3s" 397 + class="text-brown-500 hover:text-brown-800 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 398 + >Delete</button> 399 + </div> 400 + } 401 + </div> 353 402 <div class="feed-content-box-sm"> 354 403 <div class="flex items-start justify-between gap-2 mb-2"> 355 404 <div class="min-w-0"> ··· 361 410 } 362 411 </div> 363 412 </div> 364 - if showActions { 365 - <div class="flex items-center gap-1 flex-shrink-0"> 366 - <button 367 - hx-get={ "/api/modals/brewer/" + brewer.RKey } 368 - hx-target="#modal-container" 369 - hx-swap="innerHTML" 370 - class="text-brown-600 hover:text-brown-900 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 371 - >Edit</button> 372 - <button 373 - hx-delete={ "/api/brewers/" + brewer.RKey } 374 - hx-confirm="Are you sure you want to delete this brewer?" 375 - hx-target="closest .feed-card" 376 - hx-swap="outerHTML swap:0.3s" 377 - class="text-brown-500 hover:text-brown-800 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 378 - >Delete</button> 379 - </div> 380 - } 381 413 </div> 382 414 if brewer.BrewerType != "" { 383 415 <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-1"> ··· 424 456 // RecipeCard renders a single recipe as a compact card 425 457 templ RecipeCard(recipe *models.Recipe, showActions bool) { 426 458 <div class="feed-card feed-card-recipe"> 459 + <!-- Header: date + actions --> 460 + <div class="flex items-center justify-between mb-2"> 461 + <div class="text-sm text-brown-600"> 462 + if !recipe.CreatedAt.IsZero() { 463 + <time datetime={ bff.FormatISO(recipe.CreatedAt) } data-local="date">{ recipe.CreatedAt.Format("Jan 2, 2006") }</time> 464 + } 465 + </div> 466 + if showActions { 467 + <div class="flex items-center gap-1"> 468 + <button 469 + hx-get={ "/api/modals/recipe/" + recipe.RKey } 470 + hx-target="#modal-container" 471 + hx-swap="innerHTML" 472 + class="text-brown-600 hover:text-brown-900 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 473 + >Edit</button> 474 + <button 475 + hx-delete={ "/api/recipes/" + recipe.RKey } 476 + hx-confirm="Are you sure you want to delete this recipe?" 477 + hx-target="closest .feed-card" 478 + hx-swap="outerHTML swap:0.3s" 479 + class="text-brown-500 hover:text-brown-800 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 480 + >Delete</button> 481 + </div> 482 + } 483 + </div> 427 484 <div class="feed-content-box-sm"> 428 485 <div class="flex items-start justify-between gap-2 mb-2"> 429 486 <div class="min-w-0"> ··· 444 501 </div> 445 502 } 446 503 </div> 447 - if showActions { 448 - <div class="flex items-center gap-1 flex-shrink-0"> 449 - <button 450 - hx-get={ "/api/modals/recipe/" + recipe.RKey } 451 - hx-target="#modal-container" 452 - hx-swap="innerHTML" 453 - class="text-brown-600 hover:text-brown-900 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 454 - >Edit</button> 455 - <button 456 - hx-delete={ "/api/recipes/" + recipe.RKey } 457 - hx-confirm="Are you sure you want to delete this recipe?" 458 - hx-target="closest .feed-card" 459 - hx-swap="outerHTML swap:0.3s" 460 - class="text-brown-500 hover:text-brown-800 text-sm font-medium px-1.5 py-0.5 rounded hover:bg-brown-200" 461 - >Delete</button> 462 - </div> 463 - } 464 504 </div> 465 505 <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-1"> 466 506 if recipe.CoffeeAmount > 0 {
+35 -14
internal/web/components/manage_partial.templ
··· 9 9 Grinders []*models.Grinder 10 10 Brewers []*models.Brewer 11 11 Recipes []*models.Recipe 12 + // Entity usage counts and ratings (keyed by AT-URI) 13 + BeanBrewCounts map[string]int 14 + GrinderBrewCounts map[string]int 15 + BrewerBrewCounts map[string]int 16 + RoasterBeanCounts map[string]int 17 + BeanAvgBrewRatings map[string]float64 18 + RoasterAvgBrewRatings map[string]float64 19 + OwnerDID string 12 20 } 13 21 14 22 // ManagePartial renders the manage page content tables (for HTMX loading) 15 23 templ ManagePartial(props ManagePartialProps) { 16 - @ManageBeansTab(props.Beans) 17 - @ManageRoastersTab(props.Roasters) 18 - @ManageGrindersTab(props.Grinders) 19 - @ManageBrewersTab(props.Brewers) 24 + @ManageBeansTab(props.Beans, props.BeanBrewCounts, props.BeanAvgBrewRatings, props.OwnerDID) 25 + @ManageRoastersTab(props.Roasters, props.RoasterBeanCounts, props.RoasterAvgBrewRatings, props.OwnerDID) 26 + @ManageGrindersTab(props.Grinders, props.GrinderBrewCounts, props.OwnerDID) 27 + @ManageBrewersTab(props.Brewers, props.BrewerBrewCounts, props.OwnerDID) 20 28 @ManageRecipesTab(props.Recipes) 21 29 } 22 30 23 31 // ManageBeansTab renders the beans tab content with open/closed sections 24 - templ ManageBeansTab(beans []*models.Bean) { 32 + templ ManageBeansTab(beans []*models.Bean, brewCounts map[string]int, avgBrewRatings map[string]float64, ownerDID string) { 25 33 <div x-show="tab === 'beans'"> 26 34 <div class="mb-4 flex justify-between items-center"> 27 35 <h3 class="text-xl font-semibold text-brown-900">Coffee Beans</h3> ··· 38 46 <div> 39 47 <h4 class="text-lg font-semibold text-brown-900 mb-3">Open Bags</h4> 40 48 @BeanCards(BeanCardsProps{ 41 - Beans: filterOpenBeans(beans), 42 - ShowActions: true, 49 + Beans: filterOpenBeans(beans), 50 + ShowActions: true, 51 + BrewCounts: brewCounts, 52 + AvgBrewRatings: avgBrewRatings, 53 + OwnerDID: ownerDID, 43 54 }) 44 55 </div> 45 56 <div> 46 57 <h4 class="text-lg font-semibold text-brown-900 mb-3">Closed Bags</h4> 47 58 @BeanCards(BeanCardsProps{ 48 - Beans: filterClosedBeans(beans), 49 - ShowActions: true, 59 + Beans: filterClosedBeans(beans), 60 + ShowActions: true, 61 + BrewCounts: brewCounts, 62 + AvgBrewRatings: avgBrewRatings, 63 + OwnerDID: ownerDID, 50 64 }) 51 65 </div> 52 66 </div> ··· 54 68 } 55 69 56 70 // ManageRoastersTab renders the roasters tab content 57 - templ ManageRoastersTab(roasters []*models.Roaster) { 71 + templ ManageRoastersTab(roasters []*models.Roaster, beanCounts map[string]int, avgBrewRatings map[string]float64, ownerDID string) { 58 72 <div x-show="tab === 'roasters'"> 59 73 <div class="mb-4 flex justify-between items-center"> 60 74 <h3 class="text-xl font-semibold text-brown-900">Roasters</h3> ··· 68 82 </button> 69 83 </div> 70 84 @RoastersTable(RoastersTableProps{ 71 - Roasters: roasters, 72 - ShowActions: true, 85 + Roasters: roasters, 86 + ShowActions: true, 87 + BeanCounts: beanCounts, 88 + AvgBrewRatings: avgBrewRatings, 89 + OwnerDID: ownerDID, 73 90 }) 74 91 </div> 75 92 } 76 93 77 94 // ManageGrindersTab renders the grinders tab content 78 - templ ManageGrindersTab(grinders []*models.Grinder) { 95 + templ ManageGrindersTab(grinders []*models.Grinder, brewCounts map[string]int, ownerDID string) { 79 96 <div x-show="tab === 'grinders'"> 80 97 <div class="mb-4 flex justify-between items-center"> 81 98 <h3 class="text-xl font-semibold text-brown-900">Grinders</h3> ··· 91 108 @GrindersTable(GrindersTableProps{ 92 109 Grinders: grinders, 93 110 ShowActions: true, 111 + BrewCounts: brewCounts, 112 + OwnerDID: ownerDID, 94 113 }) 95 114 </div> 96 115 } 97 116 98 117 // ManageBrewersTab renders the brewers tab content 99 - templ ManageBrewersTab(brewers []*models.Brewer) { 118 + templ ManageBrewersTab(brewers []*models.Brewer, brewCounts map[string]int, ownerDID string) { 100 119 <div x-show="tab === 'brewers'"> 101 120 <div class="mb-4 flex justify-between items-center"> 102 121 <h3 class="text-xl font-semibold text-brown-900">Brewers</h3> ··· 112 131 @BrewersTable(BrewersTableProps{ 113 132 Brewers: brewers, 114 133 ShowActions: true, 134 + BrewCounts: brewCounts, 135 + OwnerDID: ownerDID, 115 136 }) 116 137 </div> 117 138 }