nice clean recipes pear.dunkirk.sh
recipes
1
fork

Configure Feed

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

feat: add recent section

+128 -2
+21
internal/cache/cache.go
··· 84 84 return err 85 85 } 86 86 87 + func (c *Cache) Recent(limit int) ([]models.CachedRecipe, error) { 88 + rows, err := c.db.Query( 89 + "SELECT url, recipe, extraction_method, fetched_at FROM recipes ORDER BY fetched_at DESC LIMIT ?", 90 + limit, 91 + ) 92 + if err != nil { 93 + return nil, err 94 + } 95 + defer rows.Close() 96 + 97 + var results []models.CachedRecipe 98 + for rows.Next() { 99 + var cr models.CachedRecipe 100 + if err := rows.Scan(&cr.URL, &cr.Recipe, &cr.ExtractionMethod, &cr.FetchedAt); err != nil { 101 + return nil, err 102 + } 103 + results = append(results, cr) 104 + } 105 + return results, rows.Err() 106 + } 107 + 87 108 func (c *Cache) Close() error { 88 109 return c.db.Close() 89 110 }
+32 -1
main.go
··· 119 119 err error 120 120 } 121 121 122 + type indexRecentRecipe struct { 123 + Name string 124 + ImageURL string 125 + SourceURL string 126 + Domain string 127 + } 128 + 122 129 func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { 123 130 targetURL := r.URL.Query().Get("url") 124 131 if targetURL != "" { ··· 130 137 http.Redirect(w, r, "/"+targetURL, http.StatusFound) 131 138 return 132 139 } 133 - s.templates.ExecuteTemplate(w, "index_page", map[string]string{"GitHash": s.gitHash, "BaseURL": s.baseURL}) 140 + 141 + var recentRecipes []indexRecentRecipe 142 + cached, err := s.cache.Recent(6) 143 + if err != nil { 144 + log.Printf("cache recent error: %v", err) 145 + } 146 + for _, cr := range cached { 147 + var recipe models.Recipe 148 + if err := json.Unmarshal(cr.Recipe, &recipe); err != nil { 149 + continue 150 + } 151 + recentRecipes = append(recentRecipes, indexRecentRecipe{ 152 + Name: recipe.Name, 153 + ImageURL: recipe.ImageURL, 154 + SourceURL: cr.URL, 155 + Domain: recipe.SourceDomain, 156 + }) 157 + } 158 + 159 + data := map[string]interface{}{ 160 + "GitHash": s.gitHash, 161 + "BaseURL": s.baseURL, 162 + "Recent": recentRecipes, 163 + } 164 + s.templates.ExecuteTemplate(w, "index_page", data) 134 165 } 135 166 136 167 func (s *Server) handleRecipeQuery(w http.ResponseWriter, r *http.Request) {
+59 -1
ui/static/style.css
··· 483 483 .cook-code .ck-ing{color:#4ec9b0} 484 484 .cook-code .ck-tmr{color:#dcdcaa} 485 485 .cook-code .ck-qty{color:#b5cea8} 486 - .cook-code .ck-unit{color:#ce9178} 486 + .cook-code .ck-unit{color:#ce9178} 487 + 488 + .recent-recipes{margin-top:3rem} 489 + .recent-recipes h3{ 490 + font-size:0.85rem; 491 + text-transform:uppercase; 492 + letter-spacing:0.08em; 493 + color:var(--text-muted); 494 + border-bottom:2px solid var(--border); 495 + padding-bottom:0.35rem; 496 + margin-bottom:0.75rem; 497 + font-family:'Poppins',system-ui,sans-serif; 498 + } 499 + .recent-grid{ 500 + display:grid; 501 + grid-template-columns:repeat(auto-fill,minmax(200px,1fr)); 502 + gap:1rem; 503 + } 504 + .recent-card{ 505 + display:block; 506 + border:1px solid var(--border); 507 + border-radius:var(--radius); 508 + overflow:hidden; 509 + text-decoration:none; 510 + color:var(--text); 511 + transition:border-color 0.2s,box-shadow 0.2s; 512 + background:var(--surface); 513 + } 514 + .recent-card:hover{ 515 + border-color:var(--accent); 516 + box-shadow:0 2px 8px rgba(0,0,0,0.06); 517 + text-decoration:none; 518 + } 519 + .recent-card img{ 520 + width:100%; 521 + height:130px; 522 + object-fit:cover; 523 + } 524 + .recent-card-body{ 525 + padding:0.65rem 0.75rem; 526 + display:flex; 527 + flex-direction:column; 528 + gap:0.15rem; 529 + } 530 + .recent-card-name{ 531 + font-size:0.9rem; 532 + font-weight:500; 533 + line-height:1.3; 534 + display:-webkit-box; 535 + -webkit-line-clamp:2; 536 + -webkit-box-orient:vertical; 537 + overflow:hidden; 538 + } 539 + .recent-card-domain{ 540 + font-size:0.75rem; 541 + color:var(--text-muted); 542 + font-family:'Poppins',system-ui,sans-serif; 543 + } 544 + .recent-card:hover .recent-card-name{color:var(--accent)}
+16
ui/templates/index.html
··· 29 29 </form> 30 30 </div> 31 31 </div> 32 + {{if .Recent}} 33 + <div class="recent-recipes"> 34 + <h3>recently peared</h3> 35 + <div class="recent-grid"> 36 + {{range .Recent}} 37 + <a href="/{{trimProto .SourceURL}}" class="recent-card"> 38 + {{if .ImageURL}}<img src="{{.ImageURL}}" alt="{{.Name}}" loading="lazy">{{end}} 39 + <div class="recent-card-body"> 40 + <span class="recent-card-name">{{.Name}}</span> 41 + <span class="recent-card-domain">{{.Domain}}</span> 42 + </div> 43 + </a> 44 + {{end}} 45 + </div> 46 + </div> 47 + {{end}} 32 48 </div> 33 49 <footer> 34 50 <span>made by <a href="https://dunkirk.sh" target="_blank" rel="noopener">Kieran Klukas</a></span>