this repo has no description
1
fork

Configure Feed

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

chore: Add footer and header to buttons page

+264 -126
+3 -2
cmd/tumble/main.go
··· 151 151 mux.HandleFunc("/irclink/", h.IRCLinkHandler) // Handles /irclink/?id and posts 152 152 153 153 mux.HandleFunc("/ogpreview.cgi", h.OGPreviewHandler) 154 - mux.HandleFunc("/buttons/button.cgi", h.ButtonHandler) 154 + mux.HandleFunc("/buttons/", h.ButtonHandler) // Handle /buttons/ with ButtonHandler (landing + result) 155 + mux.HandleFunc("/buttons/button.cgi", h.ButtonHandler) // Legacy explicit path 155 156 156 157 // v0 Routes (Aliased) 157 158 mux.HandleFunc("/v0/", h.Index) ··· 171 172 fileServer := http.FileServer(http.FS(assets.StaticFS)) 172 173 mux.Handle("/css/", fileServer) 173 174 mux.Handle("/img/", fileServer) 174 - mux.Handle("/buttons/", fileServer) 175 + // mux.Handle("/buttons/", fileServer) // Removed in favor of ButtonHandler check 175 176 mux.Handle("/favicon.ico", fileServer) 176 177 // Legacy static files 177 178 mux.Handle("/robots.txt", fileServer)
+2 -2
conf/config.yaml
··· 3 3 username: tumble 4 4 host: 127.0.0.1:13306 5 5 baseurl: localhost:8080 6 - mode: dev 6 + mode: development 7 7 logging: 8 - level: debug 8 + level: debug 9 9 output: tumble.log
+2 -1
internal/handler/docs.go
··· 22 22 23 23 // DocsHandler serves the Swagger UI page 24 24 func (h *Handler) DocsHandler(w http.ResponseWriter, r *http.Request) { 25 - data := map[string]string{ 25 + data := map[string]interface{}{ 26 26 "GitCommit": version.CommitHash, 27 27 "GitCommitURL": fmt.Sprintf("https://github.com/websages/tumble/commit/%s", version.CommitHash), 28 + "Hot": h.getHotHTML(r.Context()), 28 29 } 29 30 30 31 w.Header().Set("Content-Type", "text/html")
+38 -43
internal/handler/handlers.go
··· 1 1 package handler 2 2 3 3 import ( 4 + "context" 4 5 "fmt" 5 6 "html/template" 6 7 "log/slog" ··· 57 58 Poster string 58 59 FilterType string 59 60 IsFallbackContent bool 61 + } 62 + 63 + // Helper to fetch and render Hot Shit links 64 + func (h *Handler) getHotHTML(ctx context.Context) template.HTML { 65 + topLinks, err := h.Store.GetTopIRCLinks(ctx, 12, 6, 5) 66 + if err != nil { 67 + slog.Error("Failed to get top links", "error", err) 68 + return "" 69 + } 70 + hotHTML := "" 71 + for _, l := range topLinks { 72 + if len(l.Title) > 30 { 73 + l.Title = l.Title[:30] + "..." 74 + } 75 + content := fmt.Sprintf(`<a href="http://%s/irclink/?%d">%s</a>`, h.Config.BaseURL, l.ID, l.Title) 76 + data := map[string]interface{}{ 77 + "Content": template.HTML(content), 78 + } 79 + s, _ := h.Renderer.RenderToString("tumble_item_top5.html", data) 80 + hotHTML += s 81 + } 82 + return template.HTML(hotHTML) 60 83 } 61 84 62 85 func (h *Handler) Index(w http.ResponseWriter, r *http.Request) { ··· 311 334 } 312 335 313 336 // Hot Links (Side bar) - Only for HTML 314 - hotHTML := "" 337 + hotHTML := template.HTML("") 315 338 if dtype != "rss" && dtype != "xml" { 316 - // Perl: 12 to 6 days ago. 317 - topLinks, err := h.Store.GetTopIRCLinks(ctx, 12, 6, 5) 318 - if err == nil { 319 - for _, l := range topLinks { 320 - if len(l.Title) > 30 { 321 - l.Title = l.Title[:30] + "..." 322 - } 323 - content := fmt.Sprintf(`<a href="http://%s/irclink/?%d">%s</a>`, h.Config.BaseURL, l.ID, l.Title) 324 - 325 - data := map[string]interface{}{ 326 - "Content": template.HTML(content), 327 - } 328 - s, _ := h.Renderer.RenderToString("tumble_item_top5.html", data) 329 - hotHTML += s 330 - } 331 - } else { 332 - slog.Error("Failed to get top links", "error", err) 333 - } 334 - // Debug logging for Hot Links window 335 - slog.Info("Hot Links Debug", "count", len(topLinks), "start_days", 12, "end_days", 6) 339 + hotHTML = h.getHotHTML(ctx) 336 340 } 337 341 338 342 // Navigation ··· 365 369 viewData := IndexPageData{ 366 370 PageTitle: pageTitle, 367 371 Container: template.HTML(containerHTML), 368 - Hot: template.HTML(hotHTML), 372 + Hot: hotHTML, 369 373 NavP: template.HTML(navP), 370 374 NavN: template.HTML(navN), 371 375 BaseURL: template.HTML(h.Config.BaseURL), ··· 391 395 392 396 func (h *Handler) ButtonHandler(w http.ResponseWriter, r *http.Request) { 393 397 user := r.FormValue("user") 394 - if user == "" { 395 - w.Header().Set("Content-Type", "text/plain") 396 - w.Write([]byte("Oh no! You didn't enter your name!")) 397 - return 398 - } 399 - 400 - data := map[string]string{ 401 - "User": user, 402 - "BaseURL": h.Config.BaseURL, 398 + // If user is empty, template will show the landing page (form) 399 + // If user is present, template will show the bookmarklets 400 + data := map[string]interface{}{ 401 + "User": user, 402 + "BaseURL": h.Config.BaseURL, 403 + "Hot": h.getHotHTML(r.Context()), 404 + "GitCommit": version.CommitHash, 405 + "GitCommitURL": fmt.Sprintf("https://github.com/websages/tumble/commit/%s", version.CommitHash), 403 406 } 404 407 405 408 if err := h.Renderer.Render(w, "tumble_buttons.html", data); err != nil { ··· 447 450 containerHTML, _ = h.Renderer.RenderToString("tumble_item_text.html", data) 448 451 } 449 452 450 - // Hot links (Same logic as Index) 451 - hotHTML := "" 452 - topLinks, err := h.Store.GetTopIRCLinks(ctx, 12, 6, 5) 453 - if err == nil { 454 - for _, l := range topLinks { 455 - content := fmt.Sprintf(`<a href="http://%s/irclink/?%d">%s</a>`, h.Config.BaseURL, l.ID, l.Title) 456 - data := map[string]interface{}{"Content": template.HTML(content)} 457 - s, _ := h.Renderer.RenderToString("tumble_item_top5.html", data) 458 - hotHTML += s 459 - } 460 - } 453 + // Hot links 454 + hotHTML := h.getHotHTML(ctx) 461 455 462 456 viewData := IndexPageData{ 463 457 PageTitle: fmt.Sprintf(" &gt; %s", query), 464 458 Container: template.HTML(containerHTML), 465 - Hot: template.HTML(hotHTML), 459 + Hot: hotHTML, 466 460 BaseURL: template.HTML(h.Config.BaseURL), 467 461 } 468 462 ··· 539 533 "PrevPage": prevPage, 540 534 "HasNext": hasNext, 541 535 "Sort": sortBy, 536 + "Hot": h.getHotHTML(ctx), 542 537 } 543 538 544 539 if err := h.Renderer.Render(w, "stats.html", data); err != nil {
+144 -8
internal/templates/views/header.html
··· 4 4 --nav-header-bg: #f8f8f8; 5 5 --nav-border-color: #ddd; 6 6 --nav-text-color: #333333; 7 + --dropdown-bg: #ffffff; 8 + --dropdown-shadow: rgba(0,0,0,0.2); 7 9 } 8 10 [data-theme="dark"] { 9 11 --nav-header-bg: #161b22; 10 12 --nav-border-color: #30363d; 11 13 --nav-text-color: #c9d1d9; 14 + --dropdown-bg: #161b22; /* Matches header or sidebar bg */ 15 + --dropdown-shadow: rgba(0,0,0,0.5); 12 16 } 13 - .nav-header { padding: 10px 20px; background: var(--nav-header-bg); border-bottom: 1px solid var(--nav-border-color); display: flex; justify-content: space-between; align-items: center; font-family: sans-serif; } 14 - .nav-header a { text-decoration: none; color: var(--nav-text-color); font-weight: bold; } 15 - .nav-header a:hover { color: var(--nav-text-color); opacity: 0.8; } 17 + 18 + /* Header fixed at top */ 19 + .nav-header { 20 + position: fixed; 21 + top: 0; 22 + left: 0; 23 + right: 0; 24 + z-index: 1000; 25 + padding: 0 20px; 26 + height: 50px; 27 + background: var(--nav-header-bg); 28 + border-bottom: 1px solid var(--nav-border-color); 29 + display: flex; 30 + justify-content: space-between; 31 + align-items: center; 32 + font-family: sans-serif; 33 + } 34 + 35 + /* Compensate for fixed header */ 36 + body { 37 + padding-top: 51px; /* 50px height + 1px border */ 38 + } 39 + 40 + .nav-left { 41 + display: flex; 42 + gap: 20px; 43 + align-items: center; 44 + } 45 + 46 + .nav-right { 47 + display: flex; 48 + gap: 15px; 49 + align-items: center; 50 + } 51 + 52 + .nav-header a { 53 + text-decoration: none; 54 + color: var(--nav-text-color); 55 + font-weight: bold; 56 + font-size: 14px; 57 + } 58 + .nav-header a:hover { 59 + opacity: 0.8; 60 + } 61 + 62 + /* Search styling integration */ 63 + .nav-header input[type="text"] { 64 + padding: 5px 8px; 65 + border-radius: 4px; 66 + border: 1px solid #ccc; 67 + font-size: 13px; 68 + width: 150px; /* Fixed width */ 69 + } 70 + [data-theme="dark"] .nav-header input[type="text"] { 71 + background: #0d1117; 72 + color: #c9d1d9; 73 + border: 1px solid #30363d; 74 + } 16 75 17 76 #theme-toggle { 18 77 background: none; ··· 20 79 cursor: pointer; 21 80 color: var(--nav-text-color); 22 81 padding: 5px; 23 - margin-top: 0; 24 82 display: flex; 25 83 align-items: center; 26 84 justify-content: center; ··· 30 88 background-color: rgba(128, 128, 128, 0.1); 31 89 border-radius: 50%; 32 90 } 91 + 92 + /* Dropdown Styles */ 93 + .dropdown { 94 + position: relative; 95 + display: inline-block; 96 + height: 100%; 97 + display: flex; 98 + align-items: center; 99 + } 100 + 101 + .dropdown-trigger { 102 + cursor: pointer; 103 + font-weight: bold; 104 + color: var(--nav-text-color); 105 + font-size: 14px; 106 + } 107 + 108 + .dropdown-content { 109 + display: none; 110 + position: absolute; 111 + top: 40px; /* Below header */ 112 + right: 0; 113 + background-color: var(--dropdown-bg); 114 + min-width: 250px; 115 + box-shadow: 0px 8px 16px 0px var(--dropdown-shadow); 116 + z-index: 100; 117 + border: 1px solid var(--nav-border-color); 118 + border-radius: 4px; 119 + padding: 10px; 120 + text-align: left; 121 + } 122 + 123 + /* Show on hover */ 124 + .dropdown:hover .dropdown-content { 125 + display: block; 126 + } 127 + 128 + /* Adjust styles for internal items passed from .Hot */ 129 + .dropdown-content .sm { 130 + margin-bottom: 8px; 131 + font-size: 13px; 132 + } 133 + .dropdown-content .link a { 134 + font-weight: normal; /* Override bold from header a */ 135 + color: var(--link-color, #1e90ff); 136 + display: block; 137 + white-space: nowrap; 138 + overflow: hidden; 139 + text-overflow: ellipsis; 140 + } 141 + [data-theme="dark"] .dropdown-content .link a { 142 + color: var(--link-color, #58a6ff); 143 + } 33 144 </style> 34 145 35 146 <div class="nav-header"> 36 - <a href="/">&larr; Back to Tumble</a> 37 - <button id="theme-toggle" aria-label="Toggle Dark Mode"> 38 - <span class="material-symbols-rounded">contrast</span> 39 - </button> 147 + <div class="nav-left"> 148 + <a href="/">home</a> 149 + <a href="/buttons/">buttons</a> 150 + <a href="/index.xml">feed</a> 151 + <a href="/stats">stats</a> 152 + <a href="/api/docs">api docs</a> 153 + </div> 154 + 155 + <div class="nav-right"> 156 + <!-- Hot Shit Dropdown --> 157 + <div class="dropdown"> 158 + <span class="dropdown-trigger">Hot Shit &#9662;</span> 159 + <div class="dropdown-content"> 160 + {{if .Hot}} 161 + {{.Hot}} 162 + {{else}} 163 + <div style="padding:5px; color: var(--nav-text-color);">Nothing hot right now.</div> 164 + {{end}} 165 + </div> 166 + </div> 167 + 168 + <form action="/search.cgi" method="get" style="margin: 0;"> 169 + <input type="text" name="search" placeholder="search..." /> 170 + </form> 171 + 172 + <button id="theme-toggle" aria-label="Toggle Dark Mode"> 173 + <span class="material-symbols-rounded">contrast</span> 174 + </button> 175 + </div> 40 176 </div> 41 177 42 178 <!-- Theme Toggle Logic (Event Listener) -->
+1 -44
internal/templates/views/index.html
··· 283 283 </head> 284 284 285 285 <body> 286 + {{template "header" .}} 286 287 <div id="page"> 287 288 <div id="masthead"> 288 289 <a href="/">tumblefish.</a> 289 - <button id="theme-toggle" aria-label="Toggle Dark Mode"> 290 - <span class="material-symbols-rounded">contrast</span> 291 - </button> 292 290 </div> 293 291 294 292 <div id="sidebar"> 295 - <div class="item"> 296 - <div class="header">search it up</div> 297 - <form action="/search.cgi" id="search-form" method="get"> 298 - <input type="text" id="search" name="search" value="" size="15" /> 299 - </form> 300 - </div> 301 - <div class="item"> 302 - <div class="header">last week's hot shit</div> 303 - {{.Hot}} 304 - </div> 305 - <div class="item"> 306 - <div class="header">also</div> 307 - <div class="sm"> 308 - <div class="link"><a href="/buttons/">buttons</a></div> 309 - </div> 310 - <div class="sm"> 311 - <div class="link"><a href="/index.xml">feed</a></div> 312 - </div> 313 - <div class="sm"> 314 - <div class="link"><a href="/stats">stats</a></div> 315 - </div> 316 - <div class="sm"> 317 - <div class="link"><a href="/api/docs">api docs</a></div> 318 - </div> 319 - </div> 320 293 </div> 321 294 322 295 <div id="content"> ··· 342 315 343 316 {{template "footer" .}} 344 317 345 - <!-- Theme Toggle Logic --> 346 - <script> 347 - (function () { 348 - var toggle = document.getElementById("theme-toggle"); 349 - var html = document.documentElement; 350 318 351 - toggle.addEventListener("click", function () { 352 - if (html.getAttribute("data-theme") === "dark") { 353 - html.removeAttribute("data-theme"); 354 - localStorage.setItem("theme", "light"); 355 - } else { 356 - html.setAttribute("data-theme", "dark"); 357 - localStorage.setItem("theme", "dark"); 358 - } 359 - }); 360 - })(); 361 - </script> 362 319 </body> 363 320 </html>
+74 -26
internal/templates/views/tumble_buttons.html
··· 13 13 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 14 14 })(); 15 15 </script> 16 + <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" /> 16 17 <link rel="stylesheet" href="http://{{.BaseURL}}/css/screen.css" type="text/css" media="screen" /> 18 + <!-- Theme Init --> 19 + <script> 20 + (function () { 21 + var savedTheme = localStorage.getItem("theme"); 22 + var prefersDark = window.matchMedia( 23 + "(prefers-color-scheme: dark)" 24 + ).matches; 25 + if (savedTheme === "dark" || (!savedTheme && prefersDark)) { 26 + document.documentElement.setAttribute("data-theme", "dark"); 27 + } 28 + })(); 29 + </script> 17 30 </head> 18 31 32 + <style> 33 + /* Prevent grid rows from stretching to fill the page height */ 34 + #page { align-content: start; } 35 + </style> 36 + 19 37 <body> 38 + {{template "header" .}} 20 39 <div id="page"> 21 40 <div id="masthead"> 22 - tumblefish. 41 + <a href="/">tumblefish.</a> 23 42 </div> 24 - 43 + {{if .User}} 25 44 <div id="content"> 26 45 <div class="tumble_date"> 27 - <div class="tumble_date_date">!!</div> 28 - <div class="tumble_date_mon">buttons</div> 29 - <div class="tumble_date_day">yay!</div> 30 - </div> 31 - <div class="tumble_item_quote"> 32 - <div class="tumble_item_top"></div> 33 - <span class="tumble_item_quote_quote">So how do I install this crap??</span> 34 - <div class="tumble_item_bottom"></div> 35 - </div> 36 - <div class="tumble_item_ircLink"> 37 - <div class="tumble_item_top"></div> 38 - <span class="tumble_item_ircLink_title"> 39 - Drag this link: <a href="javascript:location.href='http://{{.BaseURL}}/irclink/?user={{.User}}&source=web&url='+encodeURIComponent(location.href)" onclick="window.alert('No clicky! Drag this link to your Bookmarks toolbar or menu, or right-click it and choose Bookmark This Link...');return false;">post to tumblefish!</a> up to your Bookmarks toolbar or menu. 40 - </span> 41 - <div class="tumble_item_bottom"></div> 42 - <div> 46 + <div class="tumble_date_date">!!</div> 47 + <div class="tumble_date_mon">buttons</div> 48 + <div class="tumble_date_day">yay!</div> 49 + </div> 50 + <div class="tumble_item_quote"> 51 + <div class="tumble_item_top"></div> 52 + <span class="tumble_item_quote_quote">So how do I install this crap??</span> 53 + <div class="tumble_item_bottom"></div> 54 + </div> 43 55 <div class="tumble_item_ircLink"> 44 - <div class="tumble_item_top"></div> 45 - <span class="tumble_item_ircLink_title">PS - Unfortunately, tumblebuttons don't work with Microsoft Internet Explorer. MSIE sucks. Stop using it.</span> 46 - <div class="tumble_item_bottom"></div> 56 + <div class="tumble_item_top"></div> 57 + <span class="tumble_item_ircLink_title"> 58 + Drag this link: <a href="javascript:location.href='http://{{.BaseURL}}/irclink/?user={{.User}}&source=web&url='+encodeURIComponent(location.href)" onclick="window.alert('No clicky! Drag this link to your Bookmarks toolbar or menu, or right-click it and choose Bookmark This Link...');return false;">post to tumblefish!</a> up to your Bookmarks toolbar or menu. 59 + </span> 60 + <div class="tumble_item_bottom"></div> 61 + </div> 62 + <div class="tumble_item_ircLink"> 63 + <div class="tumble_item_top"></div> 64 + <span class="tumble_item_ircLink_title">PS - Unfortunately, tumblebuttons don't work with Microsoft Internet Explorer. MSIE sucks. Stop using it.</span> 65 + <div class="tumble_item_bottom"></div> 66 + </div> 67 + <div class="tumble_item_ircLink"> 68 + <div class="tumble_item_top"></div> 69 + <span class="tumble_item_ircLink_title">PPS - If your name is Greg Buchanan and you just read the above postscript, you can suck my ass.</span> 70 + <div class="tumble_item_bottom"></div> 71 + </div> 47 72 </div> 48 - <div> 73 + {{else}} 74 + <div id="content"> 75 + <div class="tumble_date"> 76 + <div class="tumble_date_date">!!</div> 77 + <div class="tumble_date_mon">buttons</div> 78 + <div class="tumble_date_day">yay!</div> 79 + </div> 80 + <div class="tumble_item_quote"> 81 + <div class="tumble_item_top"></div> 82 + <span class="tumble_item_quote_quote">WTF is a tumblebutton?!?</span> 83 + <div class="tumble_item_bottom"></div> 84 + </div> 49 85 <div class="tumble_item_ircLink"> 50 - <div class="tumble_item_top"></div> 51 - <span class="tumble_item_ircLink_title">PPS - If your name is Greg Buchanan and you just read the above postscript, you can suck my ass.</span> 52 - <div class="tumble_item_bottom"></div> 53 - 86 + <div class="tumble_item_top"></div> 87 + <span class="tumble_item_ircLink_title">Buttons (bookmarklets) are links you add to your browser's Bookmarks toolbar or menu. They are an easy way to post your awesome links to tumblefish.</span> 88 + <div class="tumble_item_bottom"></div> 89 + </div> 90 + <div class="tumble_item_ircLink"> 91 + <div class="tumble_item_top"></div> 92 + <span class="tumble_item_ircLink_title">To get started, type your name in the box below.</span> 93 + <div class="tumble_item_bottom"></div> 94 + </div> 95 + <div class="tumble_item_ircLink"> 96 + <div class="tumble_item_top"></div> 97 + <span class="tumble_item_ircLink_title"><form method="post" action="button.cgi" name=""><input name="user" /><input type="submit" name="" value=" GO!! " /></form></span> 98 + <div class="tumble_item_bottom"></div> 99 + </div> 54 100 </div> 101 + {{end}} 55 102 </div> 103 + {{template "footer" .}} 56 104 </body> 57 105 58 106 </html>