Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

fix collectionItem count on collection list

+65 -10
+10
backend/internal/api/collections.go
··· 228 228 profiles := fetchProfilesForDIDs(s.db, []string{authorDID}) 229 229 creator := profiles[authorDID] 230 230 231 + collectionURIs := make([]string, len(collections)) 232 + for i, c := range collections { 233 + collectionURIs[i] = c.URI 234 + } 235 + itemCounts, _ := s.db.GetCollectionItemCounts(collectionURIs) 236 + 231 237 apiCollections := make([]APICollection, len(collections)) 232 238 for i, c := range collections { 233 239 icon := "" ··· 246 252 Creator: creator, 247 253 CreatedAt: c.CreatedAt, 248 254 IndexedAt: c.IndexedAt, 255 + ItemsCount: itemCounts[c.URI], 249 256 } 250 257 } 251 258 ··· 472 479 profiles := fetchProfilesForDIDs(s.db, []string{collection.AuthorDID}) 473 480 creator := profiles[collection.AuthorDID] 474 481 482 + itemCounts, _ := s.db.GetCollectionItemCounts([]string{collection.URI}) 483 + 475 484 icon := "" 476 485 if collection.Icon != nil { 477 486 icon = *collection.Icon ··· 489 498 Creator: creator, 490 499 CreatedAt: collection.CreatedAt, 491 500 IndexedAt: collection.IndexedAt, 501 + ItemsCount: itemCounts[collection.URI], 492 502 } 493 503 494 504 w.Header().Set("Content-Type", "application/json")
+1
backend/internal/api/hydration.go
··· 141 141 Creator Author `json:"creator"` 142 142 CreatedAt time.Time `json:"createdAt"` 143 143 IndexedAt time.Time `json:"indexedAt"` 144 + ItemsCount int `json:"itemCount"` 144 145 } 145 146 146 147 type APICollectionItem struct {
+35
backend/internal/db/queries_collections.go
··· 223 223 return uris, nil 224 224 } 225 225 226 + func (db *DB) GetCollectionItemCounts(uris []string) (map[string]int, error) { 227 + if len(uris) == 0 { 228 + return map[string]int{}, nil 229 + } 230 + 231 + query := db.Rebind(` 232 + SELECT collection_uri, COUNT(*) 233 + FROM collection_items 234 + WHERE collection_uri IN (` + buildPlaceholders(len(uris)) + `) 235 + GROUP BY collection_uri 236 + `) 237 + 238 + args := make([]interface{}, len(uris)) 239 + for i, uri := range uris { 240 + args[i] = uri 241 + } 242 + 243 + rows, err := db.Query(query, args...) 244 + if err != nil { 245 + return nil, err 246 + } 247 + defer rows.Close() 248 + 249 + counts := make(map[string]int) 250 + for rows.Next() { 251 + var uri string 252 + var count int 253 + if err := rows.Scan(&uri, &count); err != nil { 254 + return nil, err 255 + } 256 + counts[uri] = count 257 + } 258 + return counts, nil 259 + } 260 + 226 261 func (db *DB) GetCollectionsByURIs(uris []string) ([]Collection, error) { 227 262 if len(uris) == 0 { 228 263 return []Collection{}, nil
+8 -1
web/src/views/collections/CollectionDetail.tsx
··· 1 1 import React, { useEffect, useState } from "react"; 2 + import { Link } from "react-router-dom"; 2 3 import { 3 4 getCollection, 4 5 getCollectionItems, ··· 143 144 {items.length} items 144 145 </span> 145 146 <span> 146 - by {collection.creator.displayName || collection.creator.handle} 147 + by{" "} 148 + <Link 149 + to={`/profile/${collection.creator.did}`} 150 + className="hover:text-primary-600 dark:hover:text-primary-400 hover:underline transition-colors" 151 + > 152 + {collection.creator.displayName || collection.creator.handle} 153 + </Link> 147 154 </span> 148 155 </div> 149 156 </div>
+1 -1
web/src/views/collections/Collections.tsx
··· 140 140 href={`/${collection.creator?.handle || user?.handle}/collection/${(collection.uri || "").split("/").pop()}`} 141 141 className="group card p-4 hover:ring-primary-300 dark:hover:ring-primary-600 transition-all flex items-center gap-4" 142 142 > 143 - <div className="p-2.5 bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded-xl"> 143 + <div className="w-10 h-10 flex items-center justify-center shrink-0 bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded-xl"> 144 144 <CollectionIcon icon={collection.icon} size={20} /> 145 145 </div> 146 146 <div className="flex-1 min-w-0">
+10 -8
web/src/views/core/Settings.tsx
··· 176 176 <button 177 177 key={opt.value} 178 178 onClick={() => setTheme(opt.value)} 179 - className={`flex-1 flex flex-col items-center gap-2 p-4 rounded-xl border-2 transition-all ${theme === opt.value 179 + className={`flex-1 flex flex-col items-center gap-2 p-4 rounded-xl border-2 transition-all ${ 180 + theme === opt.value 180 181 ? "border-primary-500 bg-primary-50 dark:bg-primary-900/20" 181 182 : "border-surface-200 dark:border-surface-700 hover:border-surface-300 dark:hover:border-surface-600" 182 - }`} 183 + }`} 183 184 > 184 185 <opt.icon 185 186 size={24} ··· 574 575 label: string; 575 576 icon: typeof Eye; 576 577 }[] = [ 577 - { value: "warn", label: "Warn", icon: EyeOff }, 578 - { value: "hide", label: "Hide", icon: XCircle }, 579 - { value: "ignore", label: "Ignore", icon: Eye }, 580 - ]; 578 + { value: "warn", label: "Warn", icon: EyeOff }, 579 + { value: "hide", label: "Hide", icon: XCircle }, 580 + { value: "ignore", label: "Ignore", icon: Eye }, 581 + ]; 581 582 return ( 582 583 <div 583 584 key={label} ··· 597 598 opt.value, 598 599 ) 599 600 } 600 - className={`px-2.5 py-1 text-xs font-medium rounded-lg transition-all flex items-center gap-1 ${current === opt.value 601 + className={`px-2.5 py-1 text-xs font-medium rounded-lg transition-all flex items-center gap-1 ${ 602 + current === opt.value 601 603 ? opt.value === "hide" 602 604 ? "bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400" 603 605 : opt.value === "warn" 604 606 ? "bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400" 605 607 : "bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400" 606 608 : "text-surface-400 dark:text-surface-500 hover:bg-surface-200 dark:hover:bg-surface-700" 607 - }`} 609 + }`} 608 610 > 609 611 <opt.icon size={12} /> 610 612 {opt.label}